diff --git a/engine/include/XCEngine/Core/Asset/ArtifactFormats.h b/engine/include/XCEngine/Core/Asset/ArtifactFormats.h index 02fbac46..0bd8aa59 100644 --- a/engine/include/XCEngine/Core/Asset/ArtifactFormats.h +++ b/engine/include/XCEngine/Core/Asset/ArtifactFormats.h @@ -13,7 +13,7 @@ namespace Resources { constexpr Core::uint32 kTextureArtifactSchemaVersion = 1; constexpr Core::uint32 kMaterialArtifactSchemaVersion = 3; constexpr Core::uint32 kMeshArtifactSchemaVersion = 2; -constexpr Core::uint32 kShaderArtifactSchemaVersion = 2; +constexpr Core::uint32 kShaderArtifactSchemaVersion = 3; constexpr Core::uint32 kUIDocumentArtifactSchemaVersion = 2; struct TextureArtifactHeader { @@ -73,7 +73,7 @@ struct MaterialPropertyArtifact { }; struct ShaderArtifactFileHeader { - char magic[8] = { 'X', 'C', 'S', 'H', 'D', '0', '2', '\0' }; + char magic[8] = { 'X', 'C', 'S', 'H', 'D', '0', '3', '\0' }; Core::uint32 schemaVersion = kShaderArtifactSchemaVersion; }; @@ -105,10 +105,18 @@ struct ShaderResourceArtifact { Core::uint32 binding = 0; }; +struct ShaderVariantArtifactHeaderV2 { + Core::uint32 stage = 0; + Core::uint32 language = 0; + Core::uint32 backend = 0; + Core::uint64 compiledBinarySize = 0; +}; + struct ShaderVariantArtifactHeader { Core::uint32 stage = 0; Core::uint32 language = 0; Core::uint32 backend = 0; + Core::uint32 keywordCount = 0; Core::uint64 compiledBinarySize = 0; }; diff --git a/engine/include/XCEngine/Rendering/Passes/BuiltinDepthStylePassBase.h b/engine/include/XCEngine/Rendering/Passes/BuiltinDepthStylePassBase.h index 93c72216..b3f09564 100644 --- a/engine/include/XCEngine/Rendering/Passes/BuiltinDepthStylePassBase.h +++ b/engine/include/XCEngine/Rendering/Passes/BuiltinDepthStylePassBase.h @@ -106,6 +106,7 @@ private: Resources::MaterialRenderState renderState; const Resources::Shader* shader = nullptr; Containers::String passName; + Containers::String keywordSignature; uint32_t renderTargetCount = 0; uint32_t renderTargetFormat = 0; uint32_t depthStencilFormat = 0; @@ -114,6 +115,7 @@ private: return renderState == other.renderState && shader == other.shader && passName == other.passName && + keywordSignature == other.keywordSignature && renderTargetCount == other.renderTargetCount && renderTargetFormat == other.renderTargetFormat && depthStencilFormat == other.depthStencilFormat; @@ -125,6 +127,7 @@ private: size_t hash = MaterialRenderStateHash()(key.renderState); hash ^= reinterpret_cast(key.shader) + 0x9e3779b9u + (hash << 6) + (hash >> 2); hash ^= std::hash{}(key.passName) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(key.keywordSignature) + 0x9e3779b9u + (hash << 6) + (hash >> 2); hash ^= std::hash{}(key.renderTargetCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2); hash ^= std::hash{}(key.renderTargetFormat) + 0x9e3779b9u + (hash << 6) + (hash >> 2); hash ^= std::hash{}(key.depthStencilFormat) + 0x9e3779b9u + (hash << 6) + (hash >> 2); diff --git a/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h b/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h index 0b8e6e17..c52c1350 100644 --- a/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h +++ b/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h @@ -192,11 +192,13 @@ private: Resources::MaterialRenderState renderState; const Resources::Shader* shader = nullptr; Containers::String passName; + Containers::String keywordSignature; bool operator==(const PipelineStateKey& other) const { return renderState == other.renderState && shader == other.shader && - passName == other.passName; + passName == other.passName && + keywordSignature == other.keywordSignature; } }; @@ -209,6 +211,7 @@ private: combine(reinterpret_cast(key.shader)); combine(std::hash{}(key.passName)); + combine(std::hash{}(key.keywordSignature)); return hash; } }; diff --git a/engine/include/XCEngine/Resources/Shader/Shader.h b/engine/include/XCEngine/Resources/Shader/Shader.h index 2581ea1b..de55c526 100644 --- a/engine/include/XCEngine/Resources/Shader/Shader.h +++ b/engine/include/XCEngine/Resources/Shader/Shader.h @@ -88,6 +88,7 @@ struct ShaderStageVariant { ShaderType stage = ShaderType::Fragment; ShaderLanguage language = ShaderLanguage::GLSL; ShaderBackend backend = ShaderBackend::Generic; + ShaderKeywordSet requiredKeywords; Containers::String entryPoint; Containers::String profile; Containers::String sourceCode; @@ -167,7 +168,8 @@ public: const ShaderStageVariant* FindVariant( const Containers::String& passName, ShaderType stage, - ShaderBackend backend = ShaderBackend::Generic) const; + ShaderBackend backend = ShaderBackend::Generic, + const ShaderKeywordSet& enabledKeywords = ShaderKeywordSet()) const; class IRHIShader* GetRHIResource() const { return m_rhiResource; } void SetRHIResource(class IRHIShader* resource); diff --git a/engine/include/XCEngine/Resources/Shader/ShaderKeywordTypes.h b/engine/include/XCEngine/Resources/Shader/ShaderKeywordTypes.h index 17d0b201..565071b9 100644 --- a/engine/include/XCEngine/Resources/Shader/ShaderKeywordTypes.h +++ b/engine/include/XCEngine/Resources/Shader/ShaderKeywordTypes.h @@ -4,6 +4,9 @@ #include #include +#include +#include + namespace XCEngine { namespace Resources { @@ -30,5 +33,84 @@ struct ShaderKeywordSet { } }; +inline bool IsShaderKeywordPlaceholderToken(const Containers::String& keyword) { + return keyword == Containers::String("_") || + keyword == Containers::String("__"); +} + +inline Containers::String NormalizeShaderKeywordToken(const Containers::String& keyword) { + const Containers::String normalized = keyword.Trim(); + if (normalized.Empty() || IsShaderKeywordPlaceholderToken(normalized)) { + return Containers::String(); + } + + return normalized; +} + +inline bool CompareShaderKeywordTokens( + const Containers::String& left, + const Containers::String& right) { + return std::strcmp(left.CStr(), right.CStr()) < 0; +} + +inline void NormalizeShaderKeywordSetInPlace(ShaderKeywordSet& keywordSet) { + Containers::Array normalizedKeywords; + normalizedKeywords.Reserve(keywordSet.enabledKeywords.Size()); + + for (const Containers::String& keyword : keywordSet.enabledKeywords) { + const Containers::String normalizedKeyword = NormalizeShaderKeywordToken(keyword); + if (normalizedKeyword.Empty()) { + continue; + } + + bool alreadyExists = false; + for (const Containers::String& existingKeyword : normalizedKeywords) { + if (existingKeyword == normalizedKeyword) { + alreadyExists = true; + break; + } + } + + if (!alreadyExists) { + normalizedKeywords.PushBack(normalizedKeyword); + } + } + + std::sort( + normalizedKeywords.begin(), + normalizedKeywords.end(), + CompareShaderKeywordTokens); + keywordSet.enabledKeywords = normalizedKeywords; +} + +inline bool ShaderKeywordSetContains( + const ShaderKeywordSet& keywordSet, + const Containers::String& keyword) { + const Containers::String normalizedKeyword = NormalizeShaderKeywordToken(keyword); + if (normalizedKeyword.Empty()) { + return false; + } + + for (const Containers::String& enabledKeyword : keywordSet.enabledKeywords) { + if (enabledKeyword == normalizedKeyword) { + return true; + } + } + + return false; +} + +inline bool IsShaderKeywordSubset( + const ShaderKeywordSet& requiredKeywords, + const ShaderKeywordSet& enabledKeywords) { + for (const Containers::String& keyword : requiredKeywords.enabledKeywords) { + if (!ShaderKeywordSetContains(enabledKeywords, keyword)) { + return false; + } + } + + return true; +} + } // namespace Resources } // namespace XCEngine diff --git a/engine/src/Core/Asset/AssetDatabase.cpp b/engine/src/Core/Asset/AssetDatabase.cpp index 2434d687..4f274625 100644 --- a/engine/src/Core/Asset/AssetDatabase.cpp +++ b/engine/src/Core/Asset/AssetDatabase.cpp @@ -569,12 +569,17 @@ bool WriteShaderArtifactFile(const fs::path& artifactPath, const Shader& shader) variantHeader.stage = static_cast(variant.stage); variantHeader.language = static_cast(variant.language); variantHeader.backend = static_cast(variant.backend); + variantHeader.keywordCount = + static_cast(variant.requiredKeywords.enabledKeywords.Size()); variantHeader.compiledBinarySize = static_cast(variant.compiledBinary.Size()); output.write(reinterpret_cast(&variantHeader), sizeof(variantHeader)); WriteString(output, variant.entryPoint); WriteString(output, variant.profile); WriteString(output, variant.sourceCode); + for (const Containers::String& keyword : variant.requiredKeywords.enabledKeywords) { + WriteString(output, keyword); + } if (!variant.compiledBinary.Empty()) { output.write( reinterpret_cast(variant.compiledBinary.Data()), diff --git a/engine/src/Rendering/Detail/ShaderVariantUtils.h b/engine/src/Rendering/Detail/ShaderVariantUtils.h index ac52f80b..9e9c8ec4 100644 --- a/engine/src/Rendering/Detail/ShaderVariantUtils.h +++ b/engine/src/Rendering/Detail/ShaderVariantUtils.h @@ -55,12 +55,38 @@ inline void ApplyShaderStageVariant( compileDesc.profile = ToWideAscii(variant.profile); } +inline Containers::String BuildShaderKeywordSignature( + const Resources::ShaderKeywordSet& keywordSet) { + Resources::ShaderKeywordSet normalizedKeywords = keywordSet; + Resources::NormalizeShaderKeywordSetInPlace(normalizedKeywords); + + Containers::String signature; + for (size_t keywordIndex = 0; keywordIndex < normalizedKeywords.enabledKeywords.Size(); ++keywordIndex) { + if (keywordIndex > 0) { + signature += ";"; + } + + signature += normalizedKeywords.enabledKeywords[keywordIndex]; + } + + return signature; +} + inline bool ShaderPassHasGraphicsVariants( const Resources::Shader& shader, const Containers::String& passName, - Resources::ShaderBackend backend) { - return shader.FindVariant(passName, Resources::ShaderType::Vertex, backend) != nullptr && - shader.FindVariant(passName, Resources::ShaderType::Fragment, backend) != nullptr; + Resources::ShaderBackend backend, + const Resources::ShaderKeywordSet& enabledKeywords = Resources::ShaderKeywordSet()) { + return shader.FindVariant( + passName, + Resources::ShaderType::Vertex, + backend, + enabledKeywords) != nullptr && + shader.FindVariant( + passName, + Resources::ShaderType::Fragment, + backend, + enabledKeywords) != nullptr; } } // namespace Detail diff --git a/engine/src/Rendering/Passes/BuiltinDepthStylePassBaseResources.cpp b/engine/src/Rendering/Passes/BuiltinDepthStylePassBaseResources.cpp index f426f875..d05d9485 100644 --- a/engine/src/Rendering/Passes/BuiltinDepthStylePassBaseResources.cpp +++ b/engine/src/Rendering/Passes/BuiltinDepthStylePassBaseResources.cpp @@ -81,12 +81,14 @@ RHI::GraphicsPipelineDesc CreatePipelineDesc( pipelineDesc.depthStencilState.depthFunc = static_cast(RHI::ComparisonFunc::LessEqual); const Resources::ShaderBackend backend = ::XCEngine::Rendering::Detail::ToShaderBackend(backendType); + const Resources::ShaderKeywordSet keywordSet = + material != nullptr ? material->GetKeywordSet() : Resources::ShaderKeywordSet(); if (const Resources::ShaderStageVariant* vertexVariant = - shader.FindVariant(passName, Resources::ShaderType::Vertex, backend)) { + shader.FindVariant(passName, Resources::ShaderType::Vertex, backend, keywordSet)) { ::XCEngine::Rendering::Detail::ApplyShaderStageVariant(*vertexVariant, pipelineDesc.vertexShader); } if (const Resources::ShaderStageVariant* fragmentVariant = - shader.FindVariant(passName, Resources::ShaderType::Fragment, backend)) { + shader.FindVariant(passName, Resources::ShaderType::Fragment, backend, keywordSet)) { ::XCEngine::Rendering::Detail::ApplyShaderStageVariant(*fragmentVariant, pipelineDesc.fragmentShader); } @@ -165,6 +167,8 @@ BuiltinDepthStylePassBase::ResolvedShaderPass BuiltinDepthStylePassBase::Resolve } const bool shaderHasExplicitBuiltinMetadata = ShaderHasExplicitBuiltinMetadata(*shader); + const Resources::ShaderKeywordSet keywordSet = + ownerMaterial != nullptr ? ownerMaterial->GetKeywordSet() : Resources::ShaderKeywordSet(); auto tryAcceptPass = [this, shader, &resolved](const Resources::ShaderPass& shaderPass) -> bool { @@ -182,7 +186,11 @@ BuiltinDepthStylePassBase::ResolvedShaderPass BuiltinDepthStylePassBase::Resolve for (const Resources::ShaderPass& shaderPass : shader->GetPasses()) { if (!ShaderPassMatchesBuiltinPass(shaderPass, m_passType) || - !::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants(*shader, shaderPass.name, backend)) { + !::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants( + *shader, + shaderPass.name, + backend, + keywordSet)) { continue; } @@ -199,7 +207,8 @@ BuiltinDepthStylePassBase::ResolvedShaderPass BuiltinDepthStylePassBase::Resolve ::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants( *shader, explicitPass->name, - backend) && + backend, + keywordSet) && tryAcceptPass(*explicitPass)) { return true; } @@ -340,6 +349,9 @@ RHI::RHIPipelineState* BuiltinDepthStylePassBase::GetOrCreatePipelineState( material != nullptr ? material->GetRenderState() : Resources::MaterialRenderState(); pipelineKey.shader = resolvedShaderPass.shader; pipelineKey.passName = resolvedShaderPass.passName; + pipelineKey.keywordSignature = + ::XCEngine::Rendering::Detail::BuildShaderKeywordSignature( + material != nullptr ? material->GetKeywordSet() : Resources::ShaderKeywordSet()); pipelineKey.renderTargetCount = ResolveSurfaceColorAttachmentCount(surface); pipelineKey.renderTargetFormat = static_cast(ResolveSurfaceColorFormat(surface)); pipelineKey.depthStencilFormat = static_cast(ResolveSurfaceDepthFormat(surface)); diff --git a/engine/src/Rendering/Pipelines/BuiltinForwardPipelineResources.cpp b/engine/src/Rendering/Pipelines/BuiltinForwardPipelineResources.cpp index 97d55fd6..4fe9f676 100644 --- a/engine/src/Rendering/Pipelines/BuiltinForwardPipelineResources.cpp +++ b/engine/src/Rendering/Pipelines/BuiltinForwardPipelineResources.cpp @@ -29,10 +29,16 @@ const Resources::ShaderPass* FindCompatibleSurfacePass( BuiltinMaterialPass pass, Resources::ShaderBackend backend) { const bool shaderHasExplicitBuiltinMetadata = ShaderHasExplicitBuiltinMetadata(shader); + const Resources::ShaderKeywordSet keywordSet = + material != nullptr ? material->GetKeywordSet() : Resources::ShaderKeywordSet(); for (const Resources::ShaderPass& shaderPass : shader.GetPasses()) { if (ShaderPassMatchesBuiltinPass(shaderPass, pass) && - ::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants(shader, shaderPass.name, backend)) { + ::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants( + shader, + shaderPass.name, + backend, + keywordSet)) { return &shaderPass; } } @@ -42,7 +48,11 @@ const Resources::ShaderPass* FindCompatibleSurfacePass( !material->GetShaderPass().Empty()) { const Resources::ShaderPass* explicitPass = shader.FindPass(material->GetShaderPass()); if (explicitPass != nullptr && - ::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants(shader, explicitPass->name, backend)) { + ::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants( + shader, + explicitPass->name, + backend, + keywordSet)) { return explicitPass; } } @@ -57,18 +67,30 @@ const Resources::ShaderPass* FindCompatibleSurfacePass( const Resources::ShaderPass* defaultPass = shader.FindPass("ForwardLit"); if (defaultPass != nullptr && - ::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants(shader, defaultPass->name, backend)) { + ::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants( + shader, + defaultPass->name, + backend, + keywordSet)) { return defaultPass; } defaultPass = shader.FindPass("Default"); if (defaultPass != nullptr && - ::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants(shader, defaultPass->name, backend)) { + ::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants( + shader, + defaultPass->name, + backend, + keywordSet)) { return defaultPass; } if (shader.GetPassCount() > 0 && - ::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants(shader, shader.GetPasses()[0].name, backend)) { + ::XCEngine::Rendering::Detail::ShaderPassHasGraphicsVariants( + shader, + shader.GetPasses()[0].name, + backend, + keywordSet)) { return &shader.GetPasses()[0]; } @@ -93,10 +115,12 @@ RHI::GraphicsPipelineDesc CreatePipelineDesc( pipelineDesc.inputLayout = BuiltinForwardPipeline::BuildInputLayout(); const Resources::ShaderBackend backend = ::XCEngine::Rendering::Detail::ToShaderBackend(backendType); + const Resources::ShaderKeywordSet keywordSet = + material != nullptr ? material->GetKeywordSet() : Resources::ShaderKeywordSet(); const Resources::ShaderStageVariant* vertexVariant = - shader.FindVariant(passName, Resources::ShaderType::Vertex, backend); + shader.FindVariant(passName, Resources::ShaderType::Vertex, backend, keywordSet); const Resources::ShaderStageVariant* fragmentVariant = - shader.FindVariant(passName, Resources::ShaderType::Fragment, backend); + shader.FindVariant(passName, Resources::ShaderType::Fragment, backend, keywordSet); if (vertexVariant != nullptr) { ::XCEngine::Rendering::Detail::ApplyShaderStageVariant(*vertexVariant, pipelineDesc.vertexShader); } @@ -269,6 +293,9 @@ RHI::RHIPipelineState* BuiltinForwardPipeline::GetOrCreatePipelineState( material != nullptr ? material->GetRenderState() : Resources::MaterialRenderState(); pipelineKey.shader = resolvedShaderPass.shader; pipelineKey.passName = resolvedShaderPass.passName; + pipelineKey.keywordSignature = + ::XCEngine::Rendering::Detail::BuildShaderKeywordSignature( + material != nullptr ? material->GetKeywordSet() : Resources::ShaderKeywordSet()); const auto existing = m_pipelineStates.find(pipelineKey); if (existing != m_pipelineStates.end()) { diff --git a/engine/src/Resources/Material/Material.cpp b/engine/src/Resources/Material/Material.cpp index f0f7f2aa..d7de01f4 100644 --- a/engine/src/Resources/Material/Material.cpp +++ b/engine/src/Resources/Material/Material.cpp @@ -95,21 +95,6 @@ bool IsTextureMaterialPropertyType(MaterialPropertyType type) { return type == MaterialPropertyType::Texture || type == MaterialPropertyType::Cubemap; } -Containers::String NormalizeShaderKeyword(const Containers::String& keyword) { - const Containers::String normalized = keyword.Trim(); - if (normalized.Empty() || - normalized == Containers::String("_") || - normalized == Containers::String("__")) { - return Containers::String(); - } - - return normalized; -} - -bool CompareShaderKeywords(const Containers::String& left, const Containers::String& right) { - return std::strcmp(left.CStr(), right.CStr()) < 0; -} - MaterialPropertyType GetMaterialPropertyTypeForShaderProperty(ShaderPropertyType type) { switch (type) { case ShaderPropertyType::Float: @@ -439,7 +424,7 @@ Containers::String Material::GetTagValue(Core::uint32 index) const { } void Material::EnableKeyword(const Containers::String& keyword) { - const Containers::String normalizedKeyword = NormalizeShaderKeyword(keyword); + const Containers::String normalizedKeyword = NormalizeShaderKeywordToken(keyword); if (normalizedKeyword.Empty()) { return; } @@ -458,12 +443,12 @@ void Material::EnableKeyword(const Containers::String& keyword) { std::sort( m_keywordSet.enabledKeywords.begin(), m_keywordSet.enabledKeywords.end(), - CompareShaderKeywords); + CompareShaderKeywordTokens); MarkChanged(false); } void Material::DisableKeyword(const Containers::String& keyword) { - const Containers::String normalizedKeyword = NormalizeShaderKeyword(keyword); + const Containers::String normalizedKeyword = NormalizeShaderKeywordToken(keyword); if (normalizedKeyword.Empty()) { return; } @@ -477,7 +462,7 @@ void Material::DisableKeyword(const Containers::String& keyword) { std::sort( m_keywordSet.enabledKeywords.begin(), m_keywordSet.enabledKeywords.end(), - CompareShaderKeywords); + CompareShaderKeywordTokens); MarkChanged(false); return; } @@ -493,7 +478,7 @@ void Material::SetKeywordEnabled(const Containers::String& keyword, bool enabled } bool Material::IsKeywordEnabled(const Containers::String& keyword) const { - const Containers::String normalizedKeyword = NormalizeShaderKeyword(keyword); + const Containers::String normalizedKeyword = NormalizeShaderKeywordToken(keyword); if (normalizedKeyword.Empty()) { return false; } @@ -1098,7 +1083,7 @@ void Material::SyncShaderSchemaKeywords(bool removeUnknownKeywords) { std::sort( m_keywordSet.enabledKeywords.begin(), m_keywordSet.enabledKeywords.end(), - CompareShaderKeywords); + CompareShaderKeywordTokens); } void Material::MarkChanged(bool updateConstantBuffer) { diff --git a/engine/src/Resources/Material/MaterialLoader.cpp b/engine/src/Resources/Material/MaterialLoader.cpp index e31e13bf..10bf59d3 100644 --- a/engine/src/Resources/Material/MaterialLoader.cpp +++ b/engine/src/Resources/Material/MaterialLoader.cpp @@ -376,14 +376,7 @@ std::string TrimCopy(const std::string& text) { } Containers::String NormalizeMaterialKeywordToken(const Containers::String& keyword) { - const Containers::String normalized = keyword.Trim(); - if (normalized.Empty() || - normalized == Containers::String("_") || - normalized == Containers::String("__")) { - return Containers::String(); - } - - return normalized; + return NormalizeShaderKeywordToken(keyword); } bool IsJsonValueTerminator(char ch) { diff --git a/engine/src/Resources/Shader/Shader.cpp b/engine/src/Resources/Shader/Shader.cpp index 894d9731..a0c124d9 100644 --- a/engine/src/Resources/Shader/Shader.cpp +++ b/engine/src/Resources/Shader/Shader.cpp @@ -7,19 +7,15 @@ namespace { const char* kLegacyShaderPassName = "Default"; -bool IsShaderKeywordPlaceholder(const Containers::String& keyword) { - return keyword == Containers::String("_") || - keyword == Containers::String("__"); -} - bool PassDeclaresKeywordInternal(const ShaderPass& pass, const Containers::String& keyword) { - if (keyword.Empty() || IsShaderKeywordPlaceholder(keyword)) { + const Containers::String normalizedKeyword = NormalizeShaderKeywordToken(keyword); + if (normalizedKeyword.Empty()) { return false; } for (const ShaderKeywordDeclaration& declaration : pass.keywordDeclarations) { for (const Containers::String& option : declaration.options) { - if (!IsShaderKeywordPlaceholder(option) && option == keyword) { + if (NormalizeShaderKeywordToken(option) == normalizedKeyword) { return true; } } @@ -28,6 +24,18 @@ bool PassDeclaresKeywordInternal(const ShaderPass& pass, const Containers::Strin return false; } +const ShaderStageVariant* SelectMoreSpecificVariant( + const ShaderStageVariant* currentBest, + const ShaderStageVariant& candidate) { + if (currentBest == nullptr || + candidate.requiredKeywords.enabledKeywords.Size() > + currentBest->requiredKeywords.enabledKeywords.Size()) { + return &candidate; + } + + return currentBest; +} + } // namespace Shader::Shader() = default; @@ -112,7 +120,9 @@ void Shader::AddPassVariant( const Containers::String& passName, const ShaderStageVariant& variant) { ShaderPass& pass = GetOrCreatePass(passName); - pass.variants.PushBack(variant); + ShaderStageVariant normalizedVariant = variant; + NormalizeShaderKeywordSetInPlace(normalizedVariant.requiredKeywords); + pass.variants.PushBack(normalizedVariant); } void Shader::SetPassTag( @@ -214,28 +224,38 @@ const ShaderResourceBindingDesc* Shader::FindPassResourceBinding( const ShaderStageVariant* Shader::FindVariant( const Containers::String& passName, ShaderType stage, - ShaderBackend backend) const { + ShaderBackend backend, + const ShaderKeywordSet& enabledKeywords) const { const ShaderPass* pass = FindPass(passName); if (pass == nullptr) { return nullptr; } + ShaderKeywordSet normalizedEnabledKeywords = enabledKeywords; + NormalizeShaderKeywordSetInPlace(normalizedEnabledKeywords); + + const ShaderStageVariant* exactBackendVariant = nullptr; const ShaderStageVariant* genericVariant = nullptr; for (const ShaderStageVariant& variant : pass->variants) { if (variant.stage != stage) { continue; } - if (variant.backend == backend) { - return &variant; + if (!IsShaderKeywordSubset(variant.requiredKeywords, normalizedEnabledKeywords)) { + continue; } - if (variant.backend == ShaderBackend::Generic && genericVariant == nullptr) { - genericVariant = &variant; + if (variant.backend == backend) { + exactBackendVariant = SelectMoreSpecificVariant(exactBackendVariant, variant); + continue; + } + + if (variant.backend == ShaderBackend::Generic) { + genericVariant = SelectMoreSpecificVariant(genericVariant, variant); } } - return genericVariant; + return exactBackendVariant != nullptr ? exactBackendVariant : genericVariant; } void Shader::SetRHIResource(class IRHIShader* resource) { diff --git a/engine/src/Resources/Shader/ShaderLoader.cpp b/engine/src/Resources/Shader/ShaderLoader.cpp index dca30af2..8cd0cf60 100644 --- a/engine/src/Resources/Shader/ShaderLoader.cpp +++ b/engine/src/Resources/Shader/ShaderLoader.cpp @@ -369,6 +369,35 @@ bool SplitTopLevelArrayElements(const std::string& arrayText, std::vector keywordElements; + if (!SplitTopLevelArrayElements(arrayText, keywordElements)) { + return false; + } + + outKeywordSet = ShaderKeywordSet(); + outKeywordSet.enabledKeywords.Reserve(keywordElements.size()); + + for (const std::string& keywordElement : keywordElements) { + Containers::String keyword; + size_t nextPos = 0; + if (!ParseQuotedString(keywordElement, 0, keyword, &nextPos)) { + return false; + } + + if (SkipWhitespace(keywordElement, nextPos) != keywordElement.size()) { + return false; + } + + outKeywordSet.enabledKeywords.PushBack(keyword); + } + + NormalizeShaderKeywordSetInPlace(outKeywordSet); + return true; +} + bool TryParseShaderType(const Containers::String& value, ShaderType& outType) { const Containers::String normalized = value.Trim().ToLower(); if (normalized == "vertex" || normalized == "vs") { @@ -2223,6 +2252,9 @@ size_t CalculateShaderMemorySize(const Shader& shader) { } } for (const ShaderStageVariant& variant : pass.variants) { + for (const Containers::String& keyword : variant.requiredKeywords.enabledKeywords) { + memorySize += keyword.Length(); + } memorySize += variant.entryPoint.Length(); memorySize += variant.profile.Length(); memorySize += variant.sourceCode.Length(); @@ -2458,6 +2490,12 @@ LoadResult LoadShaderManifest(const Containers::String& path, const std::string& variant.profile = GetDefaultProfile(variant.language, variant.backend, variant.stage); } + std::string keywordsArray; + if (TryExtractArray(variantObject, "keywords", keywordsArray) && + !TryParseShaderKeywordsArray(keywordsArray, variant.requiredKeywords)) { + return LoadResult("Shader manifest variant keywords could not be parsed: " + path); + } + shader->AddPassVariant(passName, variant); } } @@ -2480,9 +2518,10 @@ LoadResult LoadShaderArtifact(const Containers::String& path) { const std::string magic(fileHeader.magic, fileHeader.magic + 7); const bool isLegacySchema = magic == "XCSHD01" && fileHeader.schemaVersion == 1u; + const bool isSchemaV2 = magic == "XCSHD02" && fileHeader.schemaVersion == 2u; const bool isCurrentSchema = - magic == "XCSHD02" && fileHeader.schemaVersion == kShaderArtifactSchemaVersion; - if (!isLegacySchema && !isCurrentSchema) { + magic == "XCSHD03" && fileHeader.schemaVersion == kShaderArtifactSchemaVersion; + if (!isLegacySchema && !isSchemaV2 && !isCurrentSchema) { return LoadResult("Invalid shader artifact header: " + path); } @@ -2604,29 +2643,58 @@ LoadResult LoadShaderArtifact(const Containers::String& path) { for (Core::uint32 variantIndex = 0; variantIndex < variantCount; ++variantIndex) { ShaderStageVariant variant = {}; - ShaderVariantArtifactHeader variantHeader; - if (!ReadShaderArtifactValue(data, offset, variantHeader) || - !ReadShaderArtifactString(data, offset, variant.entryPoint) || + Core::uint64 compiledBinarySize = 0; + Core::uint32 keywordCount = 0; + if (isCurrentSchema) { + ShaderVariantArtifactHeader variantHeader = {}; + if (!ReadShaderArtifactValue(data, offset, variantHeader)) { + return LoadResult("Failed to read shader artifact variants: " + path); + } + + variant.stage = static_cast(variantHeader.stage); + variant.language = static_cast(variantHeader.language); + variant.backend = static_cast(variantHeader.backend); + keywordCount = variantHeader.keywordCount; + compiledBinarySize = variantHeader.compiledBinarySize; + } else { + ShaderVariantArtifactHeaderV2 variantHeader = {}; + if (!ReadShaderArtifactValue(data, offset, variantHeader)) { + return LoadResult("Failed to read shader artifact variants: " + path); + } + + variant.stage = static_cast(variantHeader.stage); + variant.language = static_cast(variantHeader.language); + variant.backend = static_cast(variantHeader.backend); + compiledBinarySize = variantHeader.compiledBinarySize; + } + + if (!ReadShaderArtifactString(data, offset, variant.entryPoint) || !ReadShaderArtifactString(data, offset, variant.profile) || !ReadShaderArtifactString(data, offset, variant.sourceCode)) { return LoadResult("Failed to read shader artifact variants: " + path); } - variant.stage = static_cast(variantHeader.stage); - variant.language = static_cast(variantHeader.language); - variant.backend = static_cast(variantHeader.backend); + for (Core::uint32 keywordIndex = 0; keywordIndex < keywordCount; ++keywordIndex) { + Containers::String keyword; + if (!ReadShaderArtifactString(data, offset, keyword)) { + return LoadResult("Failed to read shader artifact variant keywords: " + path); + } - if (variantHeader.compiledBinarySize > 0) { - if (offset + variantHeader.compiledBinarySize > data.Size()) { + variant.requiredKeywords.enabledKeywords.PushBack(keyword); + } + NormalizeShaderKeywordSetInPlace(variant.requiredKeywords); + + if (compiledBinarySize > 0) { + if (offset + compiledBinarySize > data.Size()) { return LoadResult("Shader artifact variant binary payload is truncated: " + path); } - variant.compiledBinary.Resize(static_cast(variantHeader.compiledBinarySize)); + variant.compiledBinary.Resize(static_cast(compiledBinarySize)); std::memcpy( variant.compiledBinary.Data(), data.Data() + offset, - static_cast(variantHeader.compiledBinarySize)); - offset += static_cast(variantHeader.compiledBinarySize); + static_cast(compiledBinarySize)); + offset += static_cast(compiledBinarySize); } shader->AddPassVariant(passName, variant); diff --git a/new_editor/src/editor/UIEditorCommandDispatcher.cpp b/new_editor/src/Core/UIEditorCommandDispatcher.cpp similarity index 100% rename from new_editor/src/editor/UIEditorCommandDispatcher.cpp rename to new_editor/src/Core/UIEditorCommandDispatcher.cpp diff --git a/new_editor/src/editor/UIEditorCommandRegistry.cpp b/new_editor/src/Core/UIEditorCommandRegistry.cpp similarity index 100% rename from new_editor/src/editor/UIEditorCommandRegistry.cpp rename to new_editor/src/Core/UIEditorCommandRegistry.cpp diff --git a/new_editor/src/editor/UIEditorMenuModel.cpp b/new_editor/src/Core/UIEditorMenuModel.cpp similarity index 100% rename from new_editor/src/editor/UIEditorMenuModel.cpp rename to new_editor/src/Core/UIEditorMenuModel.cpp diff --git a/new_editor/src/editor/UIEditorPanelRegistry.cpp b/new_editor/src/Core/UIEditorPanelRegistry.cpp similarity index 100% rename from new_editor/src/editor/UIEditorPanelRegistry.cpp rename to new_editor/src/Core/UIEditorPanelRegistry.cpp diff --git a/new_editor/src/editor/UIEditorShortcutManager.cpp b/new_editor/src/Core/UIEditorShortcutManager.cpp similarity index 100% rename from new_editor/src/editor/UIEditorShortcutManager.cpp rename to new_editor/src/Core/UIEditorShortcutManager.cpp diff --git a/new_editor/src/editor/UIEditorWorkspaceController.cpp b/new_editor/src/Core/UIEditorWorkspaceController.cpp similarity index 100% rename from new_editor/src/editor/UIEditorWorkspaceController.cpp rename to new_editor/src/Core/UIEditorWorkspaceController.cpp diff --git a/new_editor/src/editor/UIEditorWorkspaceLayoutPersistence.cpp b/new_editor/src/Core/UIEditorWorkspaceLayoutPersistence.cpp similarity index 100% rename from new_editor/src/editor/UIEditorWorkspaceLayoutPersistence.cpp rename to new_editor/src/Core/UIEditorWorkspaceLayoutPersistence.cpp diff --git a/new_editor/src/editor/UIEditorWorkspaceModel.cpp b/new_editor/src/Core/UIEditorWorkspaceModel.cpp similarity index 100% rename from new_editor/src/editor/UIEditorWorkspaceModel.cpp rename to new_editor/src/Core/UIEditorWorkspaceModel.cpp diff --git a/new_editor/src/editor/UIEditorWorkspaceSession.cpp b/new_editor/src/Core/UIEditorWorkspaceSession.cpp similarity index 100% rename from new_editor/src/editor/UIEditorWorkspaceSession.cpp rename to new_editor/src/Core/UIEditorWorkspaceSession.cpp diff --git a/new_editor/src/host/AutoScreenshot.cpp b/new_editor/src/Platform/AutoScreenshot.cpp similarity index 100% rename from new_editor/src/host/AutoScreenshot.cpp rename to new_editor/src/Platform/AutoScreenshot.cpp diff --git a/new_editor/src/host/NativeRenderer.cpp b/new_editor/src/Platform/NativeRenderer.cpp similarity index 100% rename from new_editor/src/host/NativeRenderer.cpp rename to new_editor/src/Platform/NativeRenderer.cpp diff --git a/tests/Resources/Shader/test_shader.cpp b/tests/Resources/Shader/test_shader.cpp index 2fdce22c..3fce62f6 100644 --- a/tests/Resources/Shader/test_shader.cpp +++ b/tests/Resources/Shader/test_shader.cpp @@ -144,6 +144,88 @@ TEST(Shader, FindsBackendSpecificVariantAndFallsBackToGeneric) { EXPECT_EQ(openglVariant->sourceCode, "generic fragment"); } +TEST(Shader, FindsMostSpecificKeywordVariantAndPrefersExactBackend) { + Shader shader; + + ShaderStageVariant baseFragment = {}; + baseFragment.stage = ShaderType::Fragment; + baseFragment.language = ShaderLanguage::GLSL; + baseFragment.backend = ShaderBackend::Generic; + baseFragment.sourceCode = "generic base"; + shader.AddPassVariant("ForwardLit", baseFragment); + + ShaderStageVariant genericKeywordFragment = {}; + genericKeywordFragment.stage = ShaderType::Fragment; + genericKeywordFragment.language = ShaderLanguage::GLSL; + genericKeywordFragment.backend = ShaderBackend::Generic; + genericKeywordFragment.requiredKeywords.enabledKeywords.PushBack("XC_DEBUG"); + genericKeywordFragment.requiredKeywords.enabledKeywords.PushBack("XC_ALPHA_TEST"); + genericKeywordFragment.requiredKeywords.enabledKeywords.PushBack("XC_DEBUG"); + genericKeywordFragment.sourceCode = "generic keyword"; + shader.AddPassVariant("ForwardLit", genericKeywordFragment); + + ShaderStageVariant d3d12KeywordFragment = {}; + d3d12KeywordFragment.stage = ShaderType::Fragment; + d3d12KeywordFragment.language = ShaderLanguage::HLSL; + d3d12KeywordFragment.backend = ShaderBackend::D3D12; + d3d12KeywordFragment.requiredKeywords.enabledKeywords.PushBack("XC_ALPHA_TEST"); + d3d12KeywordFragment.sourceCode = "d3d12 keyword"; + shader.AddPassVariant("ForwardLit", d3d12KeywordFragment); + + ShaderKeywordSet enabledKeywords = {}; + enabledKeywords.enabledKeywords.PushBack("XC_ALPHA_TEST"); + enabledKeywords.enabledKeywords.PushBack("XC_DEBUG"); + + const ShaderStageVariant* d3d12Variant = + shader.FindVariant( + "ForwardLit", + ShaderType::Fragment, + ShaderBackend::D3D12, + enabledKeywords); + ASSERT_NE(d3d12Variant, nullptr); + EXPECT_EQ(d3d12Variant->sourceCode, "d3d12 keyword"); + + const ShaderStageVariant* openglVariant = + shader.FindVariant( + "ForwardLit", + ShaderType::Fragment, + ShaderBackend::OpenGL, + enabledKeywords); + ASSERT_NE(openglVariant, nullptr); + EXPECT_EQ(openglVariant->sourceCode, "generic keyword"); + ASSERT_EQ(openglVariant->requiredKeywords.enabledKeywords.Size(), 2u); + EXPECT_EQ(openglVariant->requiredKeywords.enabledKeywords[0], "XC_ALPHA_TEST"); + EXPECT_EQ(openglVariant->requiredKeywords.enabledKeywords[1], "XC_DEBUG"); +} + +TEST(Shader, RejectsVariantWhenRequiredKeywordsAreMissing) { + Shader shader; + + ShaderStageVariant keywordFragment = {}; + keywordFragment.stage = ShaderType::Fragment; + keywordFragment.language = ShaderLanguage::GLSL; + keywordFragment.backend = ShaderBackend::OpenGL; + keywordFragment.requiredKeywords.enabledKeywords.PushBack("XC_CLIP"); + keywordFragment.sourceCode = "clip fragment"; + shader.AddPassVariant("ForwardLit", keywordFragment); + + EXPECT_EQ( + shader.FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL), + nullptr); + + ShaderKeywordSet enabledKeywords = {}; + enabledKeywords.enabledKeywords.PushBack("XC_CLIP"); + + const ShaderStageVariant* matchedVariant = + shader.FindVariant( + "ForwardLit", + ShaderType::Fragment, + ShaderBackend::OpenGL, + enabledKeywords); + ASSERT_NE(matchedVariant, nullptr); + EXPECT_EQ(matchedVariant->sourceCode, "clip fragment"); +} + TEST(Shader, StoresPerPassTags) { Shader shader; shader.SetPassTag("ForwardLit", "LightMode", "ForwardBase"); diff --git a/tests/Resources/Shader/test_shader_loader.cpp b/tests/Resources/Shader/test_shader_loader.cpp index d228cc58..9cf67eb4 100644 --- a/tests/Resources/Shader/test_shader_loader.cpp +++ b/tests/Resources/Shader/test_shader_loader.cpp @@ -382,6 +382,66 @@ TEST(ShaderLoader, LoadLegacyBackendSplitShaderAuthoringBuildsRuntimeContract) { fs::remove_all(shaderRoot); } +TEST(ShaderLoader, LoadShaderManifestParsesVariantKeywordsAndKeywordAwareLookup) { + namespace fs = std::filesystem; + + const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_manifest_variant_keywords"; + const fs::path manifestPath = shaderRoot / "variant_keywords.shader"; + + fs::remove_all(shaderRoot); + fs::create_directories(shaderRoot); + + { + std::ofstream manifest(manifestPath); + ASSERT_TRUE(manifest.is_open()); + manifest << "{\n"; + manifest << " \"name\": \"VariantKeywordShader\",\n"; + manifest << " \"passes\": [\n"; + manifest << " {\n"; + manifest << " \"name\": \"ForwardLit\",\n"; + manifest << " \"variants\": [\n"; + manifest << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"sourceCode\": \"#version 430\\nvoid main() {}\\n\" },\n"; + manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"sourceCode\": \"#version 430\\n// BASE_FRAGMENT\\nvoid main() {}\\n\" },\n"; + manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"keywords\": [\"XC_ALPHA_TEST\", \"_\", \"XC_FOG\", \"XC_ALPHA_TEST\"], \"sourceCode\": \"#version 430\\n// KEYWORD_FRAGMENT\\nvoid main() {}\\n\" }\n"; + manifest << " ]\n"; + manifest << " }\n"; + manifest << " ]\n"; + manifest << "}\n"; + } + + ShaderLoader loader; + LoadResult result = loader.Load(manifestPath.string().c_str()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + Shader* shader = static_cast(result.resource); + ASSERT_NE(shader, nullptr); + + const ShaderStageVariant* baseFragment = + shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL); + ASSERT_NE(baseFragment, nullptr); + EXPECT_NE(std::string(baseFragment->sourceCode.CStr()).find("BASE_FRAGMENT"), std::string::npos); + + ShaderKeywordSet enabledKeywords = {}; + enabledKeywords.enabledKeywords.PushBack("XC_FOG"); + enabledKeywords.enabledKeywords.PushBack("XC_ALPHA_TEST"); + + const ShaderStageVariant* keywordFragment = + shader->FindVariant( + "ForwardLit", + ShaderType::Fragment, + ShaderBackend::OpenGL, + enabledKeywords); + ASSERT_NE(keywordFragment, nullptr); + EXPECT_NE(std::string(keywordFragment->sourceCode.CStr()).find("KEYWORD_FRAGMENT"), std::string::npos); + ASSERT_EQ(keywordFragment->requiredKeywords.enabledKeywords.Size(), 2u); + EXPECT_EQ(keywordFragment->requiredKeywords.enabledKeywords[0], "XC_ALPHA_TEST"); + EXPECT_EQ(keywordFragment->requiredKeywords.enabledKeywords[1], "XC_FOG"); + + delete shader; + fs::remove_all(shaderRoot); +} + TEST(ShaderLoader, LoadUnityStyleSingleSourceShaderAuthoringBuildsGenericHlslVariants) { namespace fs = std::filesystem; @@ -680,7 +740,8 @@ TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactAndLoaderReadsItBack) { manifest << " ],\n"; manifest << " \"variants\": [\n"; manifest << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"lit.vert.glsl\" },\n"; - manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"lit.frag.glsl\" }\n"; + manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"lit.frag.glsl\" },\n"; + manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"keywords\": [\"XC_ALPHA_TEST\", \"XC_FOG\"], \"sourceCode\": \"#version 430\\n// ARTIFACT_GL_PS_KEYWORD\\nvoid main() {}\\n\" }\n"; manifest << " ]\n"; manifest << " }\n"; manifest << " ]\n"; @@ -711,7 +772,7 @@ TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactAndLoaderReadsItBack) { const ShaderPass* pass = shader->FindPass("ForwardLit"); ASSERT_NE(pass, nullptr); - ASSERT_EQ(pass->variants.Size(), 2u); + ASSERT_EQ(pass->variants.Size(), 3u); ASSERT_EQ(pass->resources.Size(), 1u); const ShaderStageVariant* fragmentVariant = @@ -719,6 +780,22 @@ TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactAndLoaderReadsItBack) { ASSERT_NE(fragmentVariant, nullptr); EXPECT_NE(std::string(fragmentVariant->sourceCode.CStr()).find("ARTIFACT_GL_PS"), std::string::npos); + ShaderKeywordSet enabledKeywords = {}; + enabledKeywords.enabledKeywords.PushBack("XC_FOG"); + enabledKeywords.enabledKeywords.PushBack("XC_ALPHA_TEST"); + + const ShaderStageVariant* keywordVariant = + shader->FindVariant( + "ForwardLit", + ShaderType::Fragment, + ShaderBackend::OpenGL, + enabledKeywords); + ASSERT_NE(keywordVariant, nullptr); + EXPECT_NE(std::string(keywordVariant->sourceCode.CStr()).find("ARTIFACT_GL_PS_KEYWORD"), std::string::npos); + ASSERT_EQ(keywordVariant->requiredKeywords.enabledKeywords.Size(), 2u); + EXPECT_EQ(keywordVariant->requiredKeywords.enabledKeywords[0], "XC_ALPHA_TEST"); + EXPECT_EQ(keywordVariant->requiredKeywords.enabledKeywords[1], "XC_FOG"); + delete shader; database.Shutdown(); fs::remove_all(projectRoot);