Line data Source code
1 : /* Copyright (C) 2023 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
4 : * 0 A.D. is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 2 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * 0 A.D. is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "precompiled.h"
19 :
20 : #include "ShaderManager.h"
21 :
22 : #include "graphics/PreprocessorWrapper.h"
23 : #include "graphics/ShaderTechnique.h"
24 : #include "lib/config2.h"
25 : #include "lib/hash.h"
26 : #include "lib/timer.h"
27 : #include "lib/utf8.h"
28 : #include "ps/CLogger.h"
29 : #include "ps/CStrIntern.h"
30 : #include "ps/Filesystem.h"
31 : #include "ps/Profile.h"
32 : #include "ps/XML/Xeromyces.h"
33 : #include "ps/VideoMode.h"
34 : #include "renderer/backend/IDevice.h"
35 : #include "renderer/Renderer.h"
36 : #include "renderer/RenderingOptions.h"
37 :
38 : #define USE_SHADER_XML_VALIDATION 1
39 :
40 : #if USE_SHADER_XML_VALIDATION
41 : #include "ps/XML/RelaxNG.h"
42 : #include "ps/XML/XMLWriter.h"
43 : #endif
44 :
45 : #include <optional>
46 : #include <vector>
47 :
48 1 : TIMER_ADD_CLIENT(tc_ShaderValidation);
49 :
50 6 : CShaderManager::CShaderManager()
51 : {
52 : #if USE_SHADER_XML_VALIDATION
53 : {
54 12 : TIMER_ACCRUE(tc_ShaderValidation);
55 :
56 6 : if (!CXeromyces::AddValidator(g_VFS, "shader", "shaders/program.rng"))
57 6 : LOGERROR("CShaderManager: failed to load grammar shaders/program.rng");
58 : }
59 : #endif
60 :
61 : // Allow hotloading of textures
62 6 : RegisterFileReloadFunc(ReloadChangedFileCB, this);
63 6 : }
64 :
65 12 : CShaderManager::~CShaderManager()
66 : {
67 6 : UnregisterFileReloadFunc(ReloadChangedFileCB, this);
68 6 : }
69 :
70 0 : CShaderProgramPtr CShaderManager::LoadProgram(const CStr& name, const CShaderDefines& defines)
71 : {
72 0 : CacheKey key = { name, defines };
73 0 : std::map<CacheKey, CShaderProgramPtr>::iterator it = m_ProgramCache.find(key);
74 0 : if (it != m_ProgramCache.end())
75 0 : return it->second;
76 :
77 0 : CShaderProgramPtr program = CShaderProgram::Create(name, defines);
78 0 : if (program)
79 : {
80 0 : for (const VfsPath& path : program->GetFileDependencies())
81 0 : AddProgramFileDependency(program, path);
82 : }
83 : else
84 : {
85 0 : LOGERROR("Failed to load shader '%s'", name);
86 : }
87 :
88 0 : m_ProgramCache[key] = program;
89 0 : return program;
90 : }
91 :
92 0 : size_t CShaderManager::EffectCacheKeyHash::operator()(const EffectCacheKey& key) const
93 : {
94 0 : size_t hash = 0;
95 0 : hash_combine(hash, key.name.GetHash());
96 0 : hash_combine(hash, key.defines.GetHash());
97 0 : return hash;
98 : }
99 :
100 0 : bool CShaderManager::EffectCacheKey::operator==(const EffectCacheKey& b) const
101 : {
102 0 : return name == b.name && defines == b.defines;
103 : }
104 :
105 0 : CShaderTechniquePtr CShaderManager::LoadEffect(CStrIntern name)
106 : {
107 0 : return LoadEffect(name, CShaderDefines());
108 : }
109 :
110 0 : CShaderTechniquePtr CShaderManager::LoadEffect(CStrIntern name, const CShaderDefines& defines)
111 : {
112 : // Return the cached effect, if there is one
113 0 : EffectCacheKey key = { name, defines };
114 0 : EffectCacheMap::iterator it = m_EffectCache.find(key);
115 0 : if (it != m_EffectCache.end())
116 0 : return it->second;
117 :
118 : // First time we've seen this key, so construct a new effect:
119 0 : const VfsPath xmlFilename = L"shaders/effects/" + wstring_from_utf8(name.string()) + L".xml";
120 : CShaderTechniquePtr tech = std::make_shared<CShaderTechnique>(
121 0 : xmlFilename, defines, PipelineStateDescCallback{});
122 0 : if (!LoadTechnique(tech))
123 : {
124 0 : LOGERROR("Failed to load effect '%s'", name.c_str());
125 0 : tech = CShaderTechniquePtr();
126 : }
127 :
128 0 : m_EffectCache[key] = tech;
129 0 : return tech;
130 : }
131 :
132 60 : CShaderTechniquePtr CShaderManager::LoadEffect(
133 : CStrIntern name, const CShaderDefines& defines, const PipelineStateDescCallback& callback)
134 : {
135 : // We don't cache techniques with callbacks.
136 120 : const VfsPath xmlFilename = L"shaders/effects/" + wstring_from_utf8(name.string()) + L".xml";
137 120 : CShaderTechniquePtr technique = std::make_shared<CShaderTechnique>(xmlFilename, defines, callback);
138 60 : if (!LoadTechnique(technique))
139 : {
140 60 : LOGERROR("Failed to load effect '%s'", name.c_str());
141 60 : return {};
142 : }
143 0 : return technique;
144 : }
145 :
146 60 : bool CShaderManager::LoadTechnique(CShaderTechniquePtr& tech)
147 : {
148 120 : PROFILE2("loading technique");
149 60 : PROFILE2_ATTR("name: %s", tech->GetPath().string8().c_str());
150 :
151 60 : AddTechniqueFileDependency(tech, tech->GetPath());
152 :
153 120 : CXeromyces XeroFile;
154 60 : PSRETURN ret = XeroFile.Load(g_VFS, tech->GetPath());
155 60 : if (ret != PSRETURN_OK)
156 60 : return false;
157 :
158 0 : Renderer::Backend::IDevice* device = g_VideoMode.GetBackendDevice();
159 :
160 : // By default we assume that we have techinques for every dummy shader.
161 0 : if (device->GetBackend() == Renderer::Backend::Backend::DUMMY)
162 : {
163 0 : CShaderProgramPtr shaderProgram = LoadProgram("dummy", tech->GetShaderDefines());
164 0 : std::vector<CShaderPass> techPasses;
165 : Renderer::Backend::SGraphicsPipelineStateDesc passPipelineStateDesc =
166 0 : Renderer::Backend::MakeDefaultGraphicsPipelineStateDesc();
167 0 : passPipelineStateDesc.shaderProgram = shaderProgram->GetBackendShaderProgram();
168 0 : techPasses.emplace_back(
169 0 : device->CreateGraphicsPipelineState(passPipelineStateDesc), shaderProgram);
170 0 : tech->SetPasses(std::move(techPasses));
171 0 : return true;
172 : }
173 :
174 : // Define all the elements and attributes used in the XML file
175 : #define EL(x) int el_##x = XeroFile.GetElementID(#x)
176 : #define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
177 0 : EL(blend);
178 0 : EL(color);
179 0 : EL(cull);
180 0 : EL(define);
181 0 : EL(depth);
182 0 : EL(pass);
183 0 : EL(polygon);
184 0 : EL(require);
185 0 : EL(sort_by_distance);
186 0 : EL(stencil);
187 0 : AT(compare);
188 0 : AT(constant);
189 0 : AT(context);
190 0 : AT(depth_fail);
191 0 : AT(dst);
192 0 : AT(fail);
193 0 : AT(front_face);
194 0 : AT(func);
195 0 : AT(mask);
196 0 : AT(mask_read);
197 0 : AT(mask_red);
198 0 : AT(mask_green);
199 0 : AT(mask_blue);
200 0 : AT(mask_alpha);
201 0 : AT(mode);
202 0 : AT(name);
203 0 : AT(op);
204 0 : AT(pass);
205 0 : AT(reference);
206 0 : AT(shader);
207 0 : AT(shaders);
208 0 : AT(src);
209 0 : AT(test);
210 0 : AT(value);
211 : #undef AT
212 : #undef EL
213 :
214 : // Prepare the preprocessor for conditional tests
215 0 : CPreprocessorWrapper preprocessor;
216 0 : preprocessor.AddDefines(tech->GetShaderDefines());
217 :
218 0 : XMBElement root = XeroFile.GetRoot();
219 :
220 : // Find all the techniques that we can use, and their preference
221 :
222 0 : std::optional<XMBElement> usableTech;
223 0 : XERO_ITER_EL(root, technique)
224 : {
225 0 : bool isUsable = true;
226 0 : XERO_ITER_EL(technique, child)
227 : {
228 0 : XMBAttributeList attrs = child.GetAttributes();
229 :
230 : // TODO: require should be an attribute of the tech and not its child.
231 0 : if (child.GetNodeName() == el_require)
232 : {
233 0 : if (attrs.GetNamedItem(at_shaders) == "arb")
234 : {
235 0 : if (device->GetBackend() != Renderer::Backend::Backend::GL_ARB ||
236 0 : !device->GetCapabilities().ARBShaders)
237 : {
238 0 : isUsable = false;
239 : }
240 : }
241 0 : else if (attrs.GetNamedItem(at_shaders) == "glsl")
242 : {
243 0 : if (device->GetBackend() != Renderer::Backend::Backend::GL)
244 0 : isUsable = false;
245 : }
246 0 : else if (attrs.GetNamedItem(at_shaders) == "spirv")
247 : {
248 0 : if (device->GetBackend() != Renderer::Backend::Backend::VULKAN)
249 0 : isUsable = false;
250 : }
251 0 : else if (!attrs.GetNamedItem(at_context).empty())
252 : {
253 0 : CStr cond = attrs.GetNamedItem(at_context);
254 0 : if (!preprocessor.TestConditional(cond))
255 0 : isUsable = false;
256 : }
257 : }
258 : }
259 :
260 0 : if (isUsable)
261 : {
262 0 : usableTech.emplace(technique);
263 0 : break;
264 : }
265 : }
266 :
267 0 : if (!usableTech.has_value())
268 : {
269 0 : debug_warn(L"Can't find a usable technique");
270 0 : return false;
271 : }
272 :
273 0 : tech->SetSortByDistance(false);
274 :
275 0 : CShaderDefines techDefines = tech->GetShaderDefines();
276 0 : XERO_ITER_EL((*usableTech), Child)
277 : {
278 0 : if (Child.GetNodeName() == el_define)
279 : {
280 0 : techDefines.Add(CStrIntern(Child.GetAttributes().GetNamedItem(at_name)), CStrIntern(Child.GetAttributes().GetNamedItem(at_value)));
281 : }
282 0 : else if (Child.GetNodeName() == el_sort_by_distance)
283 : {
284 0 : tech->SetSortByDistance(true);
285 : }
286 : }
287 : // We don't want to have a shader context depending on the order of define and
288 : // pass tags.
289 : // TODO: we might want to implement that in a proper way via splitting passes
290 : // and tags in different groups in XML.
291 0 : std::vector<CShaderPass> techPasses;
292 0 : XERO_ITER_EL((*usableTech), Child)
293 : {
294 0 : if (Child.GetNodeName() == el_pass)
295 : {
296 0 : CShaderDefines passDefines = techDefines;
297 :
298 : Renderer::Backend::SGraphicsPipelineStateDesc passPipelineStateDesc =
299 0 : Renderer::Backend::MakeDefaultGraphicsPipelineStateDesc();
300 :
301 0 : XERO_ITER_EL(Child, Element)
302 : {
303 0 : if (Element.GetNodeName() == el_define)
304 : {
305 0 : passDefines.Add(CStrIntern(Element.GetAttributes().GetNamedItem(at_name)), CStrIntern(Element.GetAttributes().GetNamedItem(at_value)));
306 : }
307 0 : else if (Element.GetNodeName() == el_blend)
308 : {
309 0 : passPipelineStateDesc.blendState.enabled = true;
310 0 : passPipelineStateDesc.blendState.srcColorBlendFactor = passPipelineStateDesc.blendState.srcAlphaBlendFactor =
311 0 : Renderer::Backend::ParseBlendFactor(Element.GetAttributes().GetNamedItem(at_src));
312 0 : passPipelineStateDesc.blendState.dstColorBlendFactor = passPipelineStateDesc.blendState.dstAlphaBlendFactor =
313 0 : Renderer::Backend::ParseBlendFactor(Element.GetAttributes().GetNamedItem(at_dst));
314 0 : if (!Element.GetAttributes().GetNamedItem(at_op).empty())
315 : {
316 0 : passPipelineStateDesc.blendState.colorBlendOp = passPipelineStateDesc.blendState.alphaBlendOp =
317 0 : Renderer::Backend::ParseBlendOp(Element.GetAttributes().GetNamedItem(at_op));
318 : }
319 0 : if (!Element.GetAttributes().GetNamedItem(at_constant).empty())
320 : {
321 0 : if (!passPipelineStateDesc.blendState.constant.ParseString(
322 0 : Element.GetAttributes().GetNamedItem(at_constant)))
323 : {
324 0 : LOGERROR("Failed to parse blend constant: %s",
325 : Element.GetAttributes().GetNamedItem(at_constant).c_str());
326 : }
327 : }
328 : }
329 0 : else if (Element.GetNodeName() == el_color)
330 : {
331 0 : passPipelineStateDesc.blendState.colorWriteMask = 0;
332 : #define MASK_CHANNEL(ATTRIBUTE, VALUE) \
333 : if (Element.GetAttributes().GetNamedItem(ATTRIBUTE) == "TRUE") \
334 : passPipelineStateDesc.blendState.colorWriteMask |= Renderer::Backend::ColorWriteMask::VALUE
335 :
336 0 : MASK_CHANNEL(at_mask_red, RED);
337 0 : MASK_CHANNEL(at_mask_green, GREEN);
338 0 : MASK_CHANNEL(at_mask_blue, BLUE);
339 0 : MASK_CHANNEL(at_mask_alpha, ALPHA);
340 : #undef MASK_CHANNEL
341 : }
342 0 : else if (Element.GetNodeName() == el_cull)
343 : {
344 0 : if (!Element.GetAttributes().GetNamedItem(at_mode).empty())
345 : {
346 0 : passPipelineStateDesc.rasterizationState.cullMode =
347 0 : Renderer::Backend::ParseCullMode(Element.GetAttributes().GetNamedItem(at_mode));
348 : }
349 0 : if (!Element.GetAttributes().GetNamedItem(at_front_face).empty())
350 : {
351 0 : passPipelineStateDesc.rasterizationState.frontFace =
352 0 : Renderer::Backend::ParseFrontFace(Element.GetAttributes().GetNamedItem(at_front_face));
353 : }
354 : }
355 0 : else if (Element.GetNodeName() == el_depth)
356 : {
357 0 : if (!Element.GetAttributes().GetNamedItem(at_test).empty())
358 : {
359 0 : passPipelineStateDesc.depthStencilState.depthTestEnabled =
360 0 : Element.GetAttributes().GetNamedItem(at_test) == "TRUE";
361 : }
362 :
363 0 : if (!Element.GetAttributes().GetNamedItem(at_func).empty())
364 : {
365 0 : passPipelineStateDesc.depthStencilState.depthCompareOp =
366 0 : Renderer::Backend::ParseCompareOp(Element.GetAttributes().GetNamedItem(at_func));
367 : }
368 :
369 0 : if (!Element.GetAttributes().GetNamedItem(at_mask).empty())
370 : {
371 0 : passPipelineStateDesc.depthStencilState.depthWriteEnabled =
372 0 : Element.GetAttributes().GetNamedItem(at_mask) == "true";
373 : }
374 : }
375 0 : else if (Element.GetNodeName() == el_polygon)
376 : {
377 0 : if (!Element.GetAttributes().GetNamedItem(at_mode).empty())
378 : {
379 0 : passPipelineStateDesc.rasterizationState.polygonMode =
380 0 : Renderer::Backend::ParsePolygonMode(Element.GetAttributes().GetNamedItem(at_mode));
381 : }
382 : }
383 0 : else if (Element.GetNodeName() == el_stencil)
384 : {
385 0 : if (!Element.GetAttributes().GetNamedItem(at_test).empty())
386 : {
387 0 : passPipelineStateDesc.depthStencilState.stencilTestEnabled =
388 0 : Element.GetAttributes().GetNamedItem(at_test) == "TRUE";
389 : }
390 :
391 0 : if (!Element.GetAttributes().GetNamedItem(at_reference).empty())
392 : {
393 0 : passPipelineStateDesc.depthStencilState.stencilReference =
394 0 : Element.GetAttributes().GetNamedItem(at_reference).ToULong();
395 : }
396 0 : if (!Element.GetAttributes().GetNamedItem(at_mask_read).empty())
397 : {
398 0 : passPipelineStateDesc.depthStencilState.stencilReadMask =
399 0 : Element.GetAttributes().GetNamedItem(at_mask_read).ToULong();
400 : }
401 0 : if (!Element.GetAttributes().GetNamedItem(at_mask).empty())
402 : {
403 0 : passPipelineStateDesc.depthStencilState.stencilWriteMask =
404 0 : Element.GetAttributes().GetNamedItem(at_mask).ToULong();
405 : }
406 :
407 0 : if (!Element.GetAttributes().GetNamedItem(at_compare).empty())
408 : {
409 0 : passPipelineStateDesc.depthStencilState.stencilFrontFace.compareOp =
410 0 : passPipelineStateDesc.depthStencilState.stencilBackFace.compareOp =
411 0 : Renderer::Backend::ParseCompareOp(Element.GetAttributes().GetNamedItem(at_compare));
412 : }
413 0 : if (!Element.GetAttributes().GetNamedItem(at_fail).empty())
414 : {
415 0 : passPipelineStateDesc.depthStencilState.stencilFrontFace.failOp =
416 0 : passPipelineStateDesc.depthStencilState.stencilBackFace.failOp =
417 0 : Renderer::Backend::ParseStencilOp(Element.GetAttributes().GetNamedItem(at_fail));
418 : }
419 0 : if (!Element.GetAttributes().GetNamedItem(at_pass).empty())
420 : {
421 0 : passPipelineStateDesc.depthStencilState.stencilFrontFace.passOp =
422 0 : passPipelineStateDesc.depthStencilState.stencilBackFace.passOp =
423 0 : Renderer::Backend::ParseStencilOp(Element.GetAttributes().GetNamedItem(at_pass));
424 : }
425 0 : if (!Element.GetAttributes().GetNamedItem(at_depth_fail).empty())
426 : {
427 0 : passPipelineStateDesc.depthStencilState.stencilFrontFace.depthFailOp =
428 0 : passPipelineStateDesc.depthStencilState.stencilBackFace.depthFailOp =
429 0 : Renderer::Backend::ParseStencilOp(Element.GetAttributes().GetNamedItem(at_depth_fail));
430 : }
431 : }
432 : }
433 :
434 : // Load the shader program after we've read all the possibly-relevant <define>s.
435 : CShaderProgramPtr shaderProgram =
436 0 : LoadProgram(Child.GetAttributes().GetNamedItem(at_shader).c_str(), passDefines);
437 0 : if (shaderProgram)
438 : {
439 0 : for (const VfsPath& shaderProgramPath : shaderProgram->GetFileDependencies())
440 0 : AddTechniqueFileDependency(tech, shaderProgramPath);
441 0 : if (tech->GetPipelineStateDescCallback())
442 0 : tech->GetPipelineStateDescCallback()(passPipelineStateDesc);
443 0 : passPipelineStateDesc.shaderProgram = shaderProgram->GetBackendShaderProgram();
444 0 : techPasses.emplace_back(
445 0 : device->CreateGraphicsPipelineState(passPipelineStateDesc), shaderProgram);
446 : }
447 : }
448 : }
449 :
450 0 : tech->SetPasses(std::move(techPasses));
451 :
452 0 : return true;
453 : }
454 :
455 0 : size_t CShaderManager::GetNumEffectsLoaded() const
456 : {
457 0 : return m_EffectCache.size();
458 : }
459 :
460 0 : /*static*/ Status CShaderManager::ReloadChangedFileCB(void* param, const VfsPath& path)
461 : {
462 0 : return static_cast<CShaderManager*>(param)->ReloadChangedFile(path);
463 : }
464 :
465 0 : Status CShaderManager::ReloadChangedFile(const VfsPath& path)
466 : {
467 : // Find all shader programs using this file.
468 0 : const auto programs = m_HotloadPrograms.find(path);
469 0 : if (programs != m_HotloadPrograms.end())
470 : {
471 : // Reload all shader programs using this file.
472 0 : for (const std::weak_ptr<CShaderProgram>& ptr : programs->second)
473 0 : if (std::shared_ptr<CShaderProgram> program = ptr.lock())
474 0 : program->Reload();
475 : }
476 :
477 : // Find all shader techinques using this file. We need to reload them after
478 : // shader programs.
479 0 : const auto techniques = m_HotloadTechniques.find(path);
480 0 : if (techniques != m_HotloadTechniques.end())
481 : {
482 : // Reload all shader techinques using this file.
483 0 : for (const std::weak_ptr<CShaderTechnique>& ptr : techniques->second)
484 0 : if (std::shared_ptr<CShaderTechnique> technique = ptr.lock())
485 : {
486 0 : if (!LoadTechnique(technique))
487 0 : LOGERROR("Failed to reload technique '%s'", technique->GetPath().string8().c_str());
488 : }
489 : }
490 :
491 0 : return INFO::OK;
492 : }
493 :
494 60 : void CShaderManager::AddTechniqueFileDependency(const CShaderTechniquePtr& technique, const VfsPath& path)
495 : {
496 60 : m_HotloadTechniques[path].insert(technique);
497 60 : }
498 :
499 0 : void CShaderManager::AddProgramFileDependency(const CShaderProgramPtr& program, const VfsPath& path)
500 : {
501 0 : m_HotloadPrograms[path].insert(program);
502 3 : }
|