LCOV - code coverage report
Current view: top level - source/graphics - ShaderManager.cpp (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 28 249 11.2 %
Date: 2023-01-19 00:18:29 Functions: 7 16 43.8 %

          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 : }

Generated by: LCOV version 1.13