From 901a6ecc26fa7bec02e182418978650f64caa726 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 7 Apr 2026 18:49:37 +0800 Subject: [PATCH] Close shader authoring pipeline and UsePass dependency tracking --- .../XCEngine/Resources/BuiltinResources.h | 3 + .../XCEngine/Resources/Shader/Shader.h | 22 -- engine/src/Core/Asset/AssetDatabase.cpp | 5 +- engine/src/Resources/BuiltinResources.cpp | 14 +- .../Shader/Internal/ShaderArtifactLoader.cpp | 53 +-- .../Shader/Internal/ShaderAuthoringLoader.cpp | 28 +- .../Internal/ShaderRuntimeBuildUtils.cpp | 115 ++++++- engine/src/Resources/Shader/Shader.cpp | 52 +-- engine/src/Resources/Shader/ShaderLoader.cpp | 30 +- .../Material/test_material_loader.cpp | 301 ++++++++++-------- tests/Resources/Shader/test_shader.cpp | 96 ++---- tests/Resources/Shader/test_shader_loader.cpp | 165 ++++++++++ 12 files changed, 565 insertions(+), 319 deletions(-) diff --git a/engine/include/XCEngine/Resources/BuiltinResources.h b/engine/include/XCEngine/Resources/BuiltinResources.h index 00813f0d..b3388a1b 100644 --- a/engine/include/XCEngine/Resources/BuiltinResources.h +++ b/engine/include/XCEngine/Resources/BuiltinResources.h @@ -22,6 +22,9 @@ bool IsBuiltinTexturePath(const Containers::String& path); bool TryGetBuiltinShaderPathByShaderName( const Containers::String& shaderName, Containers::String& outPath); +bool TryResolveBuiltinShaderAssetPath( + const Containers::String& builtinShaderPath, + Containers::String& outPath); const char* GetBuiltinPrimitiveDisplayName(BuiltinPrimitiveType primitiveType); Containers::String GetBuiltinPrimitiveMeshPath(BuiltinPrimitiveType primitiveType); diff --git a/engine/include/XCEngine/Resources/Shader/Shader.h b/engine/include/XCEngine/Resources/Shader/Shader.h index 4586fc1c..88af7161 100644 --- a/engine/include/XCEngine/Resources/Shader/Shader.h +++ b/engine/include/XCEngine/Resources/Shader/Shader.h @@ -31,8 +31,6 @@ enum class ShaderBackend : Core::uint8 { Vulkan }; -// Keep shader property kinds close to Unity's public shader syntax so the -// runtime contract can be reused when ShaderLab-compatible parsing is added. enum class ShaderPropertyType : Core::uint8 { Float = 0, Range, @@ -119,18 +117,6 @@ public: size_t GetMemorySize() const override { return m_memorySize; } void Release() override; - void SetShaderType(ShaderType type); - ShaderType GetShaderType() const { return m_shaderType; } - - void SetShaderLanguage(ShaderLanguage lang); - ShaderLanguage GetShaderLanguage() const { return m_language; } - - void SetSourceCode(const Containers::String& source); - const Containers::String& GetSourceCode() const { return m_sourceCode; } - - void SetCompiledBinary(const Containers::Array& binary); - const Containers::Array& GetCompiledBinary() const { return m_compiledBinary; } - void AddUniform(const ShaderUniform& uniform); const Containers::Array& GetUniforms() const { return m_uniforms; } @@ -181,14 +167,6 @@ public: private: ShaderPass& GetOrCreatePass(const Containers::String& passName); - ShaderStageVariant& GetOrCreateLegacyVariant(); - void SyncLegacyVariant(); - - ShaderType m_shaderType = ShaderType::Fragment; - ShaderLanguage m_language = ShaderLanguage::GLSL; - - Containers::String m_sourceCode; - Containers::Array m_compiledBinary; Containers::Array m_uniforms; Containers::Array m_attributes; diff --git a/engine/src/Core/Asset/AssetDatabase.cpp b/engine/src/Core/Asset/AssetDatabase.cpp index 4c763e46..608eccf2 100644 --- a/engine/src/Core/Asset/AssetDatabase.cpp +++ b/engine/src/Core/Asset/AssetDatabase.cpp @@ -546,7 +546,7 @@ bool WriteShaderArtifactFile(const fs::path& artifactPath, const Shader& shader) for (const ShaderPass& pass : shader.GetPasses()) { WriteString(output, pass.name); - ShaderPassArtifactHeaderV4 passHeader; + ShaderPassArtifactHeaderV5 passHeader; passHeader.tagCount = static_cast(pass.tags.Size()); passHeader.resourceCount = static_cast(pass.resources.Size()); passHeader.keywordDeclarationCount = static_cast(pass.keywordDeclarations.Size()); @@ -1404,8 +1404,7 @@ Containers::String AssetDatabase::GetImporterNameForPath(const Containers::Strin if (ext == ".obj" || ext == ".fbx" || ext == ".gltf" || ext == ".glb" || ext == ".dae" || ext == ".stl") { return Containers::String("ModelImporter"); } - if (ext == ".shader" || ext == ".hlsl" || ext == ".glsl" || ext == ".vert" || ext == ".frag" || - ext == ".geom" || ext == ".comp") { + if (ext == ".shader") { return Containers::String("ShaderImporter"); } if (ext == ".mat" || ext == ".material" || ext == ".json") { diff --git a/engine/src/Resources/BuiltinResources.cpp b/engine/src/Resources/BuiltinResources.cpp index 66e49447..20d35d45 100644 --- a/engine/src/Resources/BuiltinResources.cpp +++ b/engine/src/Resources/BuiltinResources.cpp @@ -101,7 +101,7 @@ bool TryResolveBuiltinAssetPathFromAnchor( return false; } -bool TryResolveBuiltinShaderAssetPath( +bool TryResolveBuiltinShaderAssetPathFromRelativePath( const std::filesystem::path& relativePath, Containers::String& outPath) { std::filesystem::path resolvedPath; @@ -159,7 +159,7 @@ const char* GetBuiltinShaderAssetRelativePath(const Containers::String& builtinS return nullptr; } -bool TryResolveBuiltinShaderAssetPath( +bool TryResolveBuiltinShaderAssetPathInternal( const Containers::String& builtinShaderPath, Containers::String& outPath) { const char* relativePath = GetBuiltinShaderAssetRelativePath(builtinShaderPath); @@ -167,7 +167,7 @@ bool TryResolveBuiltinShaderAssetPath( return false; } - return TryResolveBuiltinShaderAssetPath(std::filesystem::path(relativePath), outPath); + return TryResolveBuiltinShaderAssetPathFromRelativePath(std::filesystem::path(relativePath), outPath); } Shader* LoadBuiltinShaderFromAsset( @@ -188,7 +188,7 @@ Shader* LoadBuiltinShaderFromAsset( Shader* TryLoadBuiltinShaderFromAsset(const Containers::String& builtinPath) { Containers::String assetPath; - if (!TryResolveBuiltinShaderAssetPath(builtinPath, assetPath)) { + if (!TryResolveBuiltinShaderAssetPathInternal(builtinPath, assetPath)) { return nullptr; } @@ -830,6 +830,12 @@ bool TryGetBuiltinShaderPathByShaderName( return false; } +bool TryResolveBuiltinShaderAssetPath( + const Containers::String& builtinShaderPath, + Containers::String& outPath) { + return TryResolveBuiltinShaderAssetPathInternal(builtinShaderPath, outPath); +} + const char* GetBuiltinPrimitiveDisplayName(BuiltinPrimitiveType primitiveType) { switch (primitiveType) { case BuiltinPrimitiveType::Cube: return "Cube"; diff --git a/engine/src/Resources/Shader/Internal/ShaderArtifactLoader.cpp b/engine/src/Resources/Shader/Internal/ShaderArtifactLoader.cpp index 19c5e3af..d5f07282 100644 --- a/engine/src/Resources/Shader/Internal/ShaderArtifactLoader.cpp +++ b/engine/src/Resources/Shader/Internal/ShaderArtifactLoader.cpp @@ -48,6 +48,23 @@ bool ReadShaderArtifactString( return true; } +MaterialRenderState ExpandSerializedMaterialRenderStateV4(const SerializedMaterialRenderStateV4& legacyState) { + MaterialRenderState renderState = {}; + renderState.blendEnable = legacyState.blendEnable; + renderState.srcBlend = legacyState.srcBlend; + renderState.dstBlend = legacyState.dstBlend; + renderState.srcBlendAlpha = legacyState.srcBlendAlpha; + renderState.dstBlendAlpha = legacyState.dstBlendAlpha; + renderState.blendOp = legacyState.blendOp; + renderState.blendOpAlpha = legacyState.blendOpAlpha; + renderState.colorWriteMask = legacyState.colorWriteMask; + renderState.depthTestEnable = legacyState.depthTestEnable; + renderState.depthWriteEnable = legacyState.depthWriteEnable; + renderState.depthFunc = legacyState.depthFunc; + renderState.cullMode = legacyState.cullMode; + return renderState; +} + } // namespace LoadResult LoadShaderArtifact(const Containers::String& path) { @@ -66,9 +83,10 @@ LoadResult LoadShaderArtifact(const Containers::String& path) { const bool isLegacySchema = magic == "XCSHD01" && fileHeader.schemaVersion == 1u; const bool isSchemaV2 = magic == "XCSHD02" && fileHeader.schemaVersion == 2u; const bool isSchemaV3 = magic == "XCSHD03" && fileHeader.schemaVersion == 3u; + const bool isSchemaV4 = magic == "XCSHD04" && fileHeader.schemaVersion == 4u; const bool isCurrentSchema = - magic == "XCSHD04" && fileHeader.schemaVersion == kShaderArtifactSchemaVersion; - if (!isLegacySchema && !isSchemaV2 && !isSchemaV3 && !isCurrentSchema) { + magic == "XCSHD05" && fileHeader.schemaVersion == kShaderArtifactSchemaVersion; + if (!isLegacySchema && !isSchemaV2 && !isSchemaV3 && !isSchemaV4 && !isCurrentSchema) { return LoadResult("Invalid shader artifact header: " + path); } @@ -81,7 +99,7 @@ LoadResult LoadShaderArtifact(const Containers::String& path) { !ReadShaderArtifactString(data, offset, shaderSourcePath)) { return LoadResult("Failed to parse shader artifact strings: " + path); } - if (isCurrentSchema && + if ((isSchemaV4 || isCurrentSchema) && !ReadShaderArtifactString(data, offset, shaderFallback)) { return LoadResult("Failed to parse shader artifact strings: " + path); } @@ -142,12 +160,24 @@ LoadResult LoadShaderArtifact(const Containers::String& path) { resourceCount = passHeader.resourceCount; keywordDeclarationCount = passHeader.keywordDeclarationCount; variantCount = passHeader.variantCount; - } else { + } else if (isSchemaV4) { ShaderPassArtifactHeaderV4 passHeader = {}; if (!ReadShaderArtifactValue(data, offset, passHeader)) { return LoadResult("Failed to read shader artifact passes: " + path); } + tagCount = passHeader.tagCount; + resourceCount = passHeader.resourceCount; + keywordDeclarationCount = passHeader.keywordDeclarationCount; + variantCount = passHeader.variantCount; + hasFixedFunctionState = passHeader.hasFixedFunctionState; + fixedFunctionState = ExpandSerializedMaterialRenderStateV4(passHeader.fixedFunctionState); + } else { + ShaderPassArtifactHeaderV5 passHeader = {}; + if (!ReadShaderArtifactValue(data, offset, passHeader)) { + return LoadResult("Failed to read shader artifact passes: " + path); + } + tagCount = passHeader.tagCount; resourceCount = passHeader.resourceCount; keywordDeclarationCount = passHeader.keywordDeclarationCount; @@ -214,7 +244,7 @@ LoadResult LoadShaderArtifact(const Containers::String& path) { ShaderStageVariant variant = {}; Core::uint64 compiledBinarySize = 0; Core::uint32 keywordCount = 0; - if (isCurrentSchema) { + if (isSchemaV4 || isCurrentSchema) { ShaderVariantArtifactHeader variantHeader = {}; if (!ReadShaderArtifactValue(data, offset, variantHeader)) { return LoadResult("Failed to read shader artifact variants: " + path); @@ -270,19 +300,6 @@ LoadResult LoadShaderArtifact(const Containers::String& path) { } } - if (shader->GetPassCount() == 1) { - const ShaderPass* defaultPass = shader->FindPass("Default"); - if (defaultPass != nullptr && defaultPass->variants.Size() == 1u) { - const ShaderStageVariant& variant = defaultPass->variants[0]; - if (variant.backend == ShaderBackend::Generic) { - shader->SetShaderType(variant.stage); - shader->SetShaderLanguage(variant.language); - shader->SetSourceCode(variant.sourceCode); - shader->SetCompiledBinary(variant.compiledBinary); - } - } - } - shader->m_isValid = true; shader->m_memorySize = CalculateShaderMemorySize(*shader); return LoadResult(shader.release()); diff --git a/engine/src/Resources/Shader/Internal/ShaderAuthoringLoader.cpp b/engine/src/Resources/Shader/Internal/ShaderAuthoringLoader.cpp index cddd29e8..2e3eff67 100644 --- a/engine/src/Resources/Shader/Internal/ShaderAuthoringLoader.cpp +++ b/engine/src/Resources/Shader/Internal/ShaderAuthoringLoader.cpp @@ -68,8 +68,32 @@ bool CollectShaderAuthoringDependencyPathsRecursive( } if (IsBuiltinShaderPath(resolvedUsePassPath)) { - if (seenDependencyPaths.insert(ToStdString(resolvedUsePassPath)).second) { - outDependencies.PushBack(resolvedUsePassPath); + Containers::String builtinAssetPath; + if (!TryResolveBuiltinShaderAssetPath(resolvedUsePassPath, builtinAssetPath)) { + return false; + } + + const fs::path normalizedBuiltinAssetPath = + fs::path(builtinAssetPath.CStr()).lexically_normal(); + const Containers::String normalizedBuiltinDependency = + normalizedBuiltinAssetPath.generic_string().c_str(); + if (!normalizedBuiltinDependency.Empty() && + seenDependencyPaths.insert(ToStdString(normalizedBuiltinDependency)).second) { + outDependencies.PushBack(normalizedBuiltinDependency); + } + + Containers::String referencedSourceText; + if (!ReadShaderTextFile(builtinAssetPath, referencedSourceText)) { + return false; + } + + if (!CollectShaderAuthoringDependencyPathsRecursive( + builtinAssetPath, + referencedSourceText.CStr(), + seenShaderPaths, + seenDependencyPaths, + outDependencies)) { + return false; } continue; } diff --git a/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.cpp b/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.cpp index a7c6481e..98362895 100644 --- a/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.cpp +++ b/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.cpp @@ -18,6 +18,11 @@ namespace XCEngine { namespace Resources { +LoadResult BuildShaderFromIRInternal( + const Containers::String& path, + const ShaderIR& shaderIR, + std::unordered_set& activeShaderPathKeys); + namespace { namespace fs = std::filesystem; @@ -31,6 +36,25 @@ Containers::String ResolveBuiltinShaderPathByAuthoringName(const Containers::Str return Containers::String(); } +std::string BuildNormalizedShaderPathKey(const Containers::String& shaderPath) { + if (shaderPath.Empty()) { + return std::string(); + } + + fs::path normalizedPath(shaderPath.CStr()); + if (!normalizedPath.is_absolute()) { + const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot(); + if (!resourceRoot.Empty()) { + normalizedPath = fs::path(resourceRoot.CStr()) / normalizedPath; + } else { + std::error_code ec; + normalizedPath = fs::current_path(ec) / normalizedPath; + } + } + + return normalizedPath.lexically_normal().generic_string(); +} + void AddUniqueSearchRoot( const fs::path& candidate, std::vector& searchRoots, @@ -241,12 +265,73 @@ ShaderPass BuildConcretePass( return shaderPass; } +bool TryLoadReferencedUsePassShader( + const Containers::String& referencedShaderPath, + std::unordered_set& activeShaderPathKeys, + std::unique_ptr& outShader, + Containers::String& outError) { + Containers::String authoringShaderPath = referencedShaderPath; + if (IsBuiltinShaderPath(authoringShaderPath) && + !TryResolveBuiltinShaderAssetPath(authoringShaderPath, authoringShaderPath)) { + outError = + Containers::String("UsePass failed to resolve builtin shader asset: ") + + referencedShaderPath; + return false; + } + + const std::string shaderPathKey = BuildNormalizedShaderPathKey(authoringShaderPath); + if (shaderPathKey.empty()) { + outError = + Containers::String("UsePass could not normalize referenced shader path: ") + + referencedShaderPath; + return false; + } + + if (activeShaderPathKeys.find(shaderPathKey) != activeShaderPathKeys.end()) { + outError = + Containers::String("UsePass detected a cyclic shader reference: ") + + authoringShaderPath; + return false; + } + + Containers::String sourceText; + if (!ReadShaderTextFile(authoringShaderPath, sourceText)) { + outError = + Containers::String("UsePass failed to read referenced shader: ") + + authoringShaderPath; + return false; + } + + ShaderIR referencedShaderIR = {}; + Containers::String parseError; + if (!ParseShaderAuthoring(authoringShaderPath, sourceText.CStr(), referencedShaderIR, &parseError)) { + outError = parseError; + return false; + } + + activeShaderPathKeys.insert(shaderPathKey); + LoadResult referencedShaderResult = + BuildShaderFromIRInternal(authoringShaderPath, referencedShaderIR, activeShaderPathKeys); + activeShaderPathKeys.erase(shaderPathKey); + if (!referencedShaderResult || referencedShaderResult.resource == nullptr) { + outError = + !referencedShaderResult.errorMessage.Empty() + ? referencedShaderResult.errorMessage + : Containers::String("UsePass failed to build referenced shader: ") + authoringShaderPath; + return false; + } + + outShader.reset(static_cast(referencedShaderResult.resource)); + return true; +} + bool TryResolveUsePass( const Containers::String& currentShaderPath, const Containers::String& currentShaderName, const Containers::String& referencedShaderName, const Containers::String& referencedPassName, const std::unordered_map& localConcretePasses, + std::unordered_set& activeShaderPathKeys, ShaderPass& outPass, Containers::String& outError) { if (referencedShaderName == currentShaderName) { @@ -273,15 +358,15 @@ bool TryResolveUsePass( return false; } - ShaderLoader loader; - LoadResult referencedShaderResult = loader.Load(referencedShaderPath); - if (!referencedShaderResult || referencedShaderResult.resource == nullptr) { - outError = - Containers::String("UsePass failed to load referenced shader: ") + referencedShaderName; + std::unique_ptr referencedShader; + if (!TryLoadReferencedUsePassShader( + referencedShaderPath, + activeShaderPathKeys, + referencedShader, + outError)) { return false; } - std::unique_ptr referencedShader(static_cast(referencedShaderResult.resource)); const ShaderPass* referencedPass = referencedShader->FindPass(referencedPassName); if (referencedPass == nullptr) { outError = @@ -411,9 +496,10 @@ bool ResolveShaderUsePassPath( outResolvedPath); } -LoadResult BuildShaderFromIR( +LoadResult BuildShaderFromIRInternal( const Containers::String& path, - const ShaderIR& shaderIR) { + const ShaderIR& shaderIR, + std::unordered_set& activeShaderPathKeys) { auto shader = std::make_unique(); IResource::ConstructParams params; params.path = path; @@ -457,6 +543,7 @@ LoadResult BuildShaderFromIR( pass.usePassShaderName, pass.usePassPassName, localConcretePasses, + activeShaderPathKeys, importedPass, importError)) { return LoadResult(importError); @@ -483,5 +570,17 @@ LoadResult BuildShaderFromIR( return LoadResult(shader.release()); } +LoadResult BuildShaderFromIR( + const Containers::String& path, + const ShaderIR& shaderIR) { + std::unordered_set activeShaderPathKeys; + const std::string shaderPathKey = BuildNormalizedShaderPathKey(path); + if (!shaderPathKey.empty()) { + activeShaderPathKeys.insert(shaderPathKey); + } + + return BuildShaderFromIRInternal(path, shaderIR, activeShaderPathKeys); +} + } // namespace Resources } // namespace XCEngine diff --git a/engine/src/Resources/Shader/Shader.cpp b/engine/src/Resources/Shader/Shader.cpp index a0c124d9..50a04094 100644 --- a/engine/src/Resources/Shader/Shader.cpp +++ b/engine/src/Resources/Shader/Shader.cpp @@ -5,8 +5,6 @@ namespace Resources { namespace { -const char* kLegacyShaderPassName = "Default"; - bool PassDeclaresKeywordInternal(const ShaderPass& pass, const Containers::String& keyword) { const Containers::String normalizedKeyword = NormalizeShaderKeywordToken(keyword); if (normalizedKeyword.Empty()) { @@ -43,38 +41,15 @@ Shader::Shader() = default; Shader::~Shader() = default; void Shader::Release() { - m_shaderType = ShaderType::Fragment; - m_language = ShaderLanguage::GLSL; - m_sourceCode.Clear(); - m_compiledBinary.Clear(); m_uniforms.Clear(); m_attributes.Clear(); m_properties.Clear(); m_passes.Clear(); + m_fallback.Clear(); m_rhiResource = nullptr; m_isValid = false; } -void Shader::SetShaderType(ShaderType type) { - m_shaderType = type; - SyncLegacyVariant(); -} - -void Shader::SetShaderLanguage(ShaderLanguage lang) { - m_language = lang; - SyncLegacyVariant(); -} - -void Shader::SetSourceCode(const Containers::String& source) { - m_sourceCode = source; - SyncLegacyVariant(); -} - -void Shader::SetCompiledBinary(const Containers::Array& binary) { - m_compiledBinary = binary; - SyncLegacyVariant(); -} - void Shader::AddUniform(const ShaderUniform& uniform) { m_uniforms.PushBack(uniform); } @@ -108,6 +83,10 @@ const ShaderPropertyDesc* Shader::FindProperty(const Containers::String& propert return nullptr; } +void Shader::SetFallback(const Containers::String& fallback) { + m_fallback = fallback; +} + void Shader::AddPass(const ShaderPass& pass) { m_passes.PushBack(pass); } @@ -272,26 +251,5 @@ ShaderPass& Shader::GetOrCreatePass(const Containers::String& passName) { return pass; } -ShaderStageVariant& Shader::GetOrCreateLegacyVariant() { - ShaderPass& pass = GetOrCreatePass(kLegacyShaderPassName); - for (ShaderStageVariant& variant : pass.variants) { - if (variant.backend == ShaderBackend::Generic) { - return variant; - } - } - - ShaderStageVariant& variant = pass.variants.EmplaceBack(); - variant.backend = ShaderBackend::Generic; - return variant; -} - -void Shader::SyncLegacyVariant() { - ShaderStageVariant& variant = GetOrCreateLegacyVariant(); - variant.stage = m_shaderType; - variant.language = m_language; - variant.sourceCode = m_sourceCode; - variant.compiledBinary = m_compiledBinary; -} - } // namespace Resources } // namespace XCEngine diff --git a/engine/src/Resources/Shader/ShaderLoader.cpp b/engine/src/Resources/Shader/ShaderLoader.cpp index f57b6b0a..5f041381 100644 --- a/engine/src/Resources/Shader/ShaderLoader.cpp +++ b/engine/src/Resources/Shader/ShaderLoader.cpp @@ -4,7 +4,6 @@ #include "Internal/ShaderAuthoringLoader.h" #include "ShaderAuthoringParser.h" #include "Internal/ShaderFileUtils.h" -#include "Internal/ShaderManifestLoader.h" #include "Internal/ShaderRuntimeBuildUtils.h" #include @@ -21,12 +20,6 @@ ShaderLoader::~ShaderLoader() = default; Containers::Array ShaderLoader::GetSupportedExtensions() const { Containers::Array extensions; - extensions.PushBack("vert"); - extensions.PushBack("frag"); - extensions.PushBack("geom"); - extensions.PushBack("comp"); - extensions.PushBack("glsl"); - extensions.PushBack("hlsl"); extensions.PushBack("shader"); extensions.PushBack("xcshader"); return extensions; @@ -38,9 +31,7 @@ bool ShaderLoader::CanLoad(const Containers::String& path) const { } const Containers::String ext = GetExtension(path).ToLower(); - return ext == "vert" || ext == "frag" || ext == "geom" || - ext == "comp" || ext == "glsl" || ext == "hlsl" || - ext == "shader" || ext == "xcshader"; + return ext == "shader" || ext == "xcshader"; } LoadResult ShaderLoader::Load(const Containers::String& path, const ImportSettings* settings) { @@ -61,14 +52,15 @@ LoadResult ShaderLoader::Load(const Containers::String& path, const ImportSettin } const std::string sourceText = ToStdStringFromBytes(data); - if (ext == "shader" && LooksLikeShaderManifest(sourceText)) { - return LoadShaderManifest(path, sourceText); - } - if (ext == "shader" && LooksLikeShaderAuthoring(sourceText)) { + if (ext == "shader") { + if (!LooksLikeShaderAuthoring(sourceText)) { + return LoadResult("Shader authoring file must start with a Shader declaration: " + path); + } + return LoadShaderAuthoring(path, sourceText); } - return LoadLegacySingleStageShader(path, sourceText); + return LoadResult("Unsupported shader source format: " + path); } ImportSettings* ShaderLoader::GetDefaultSettings() const { @@ -95,13 +87,11 @@ bool ShaderLoader::CollectSourceDependencies( } const std::string sourceText = ToStdStringFromBytes(data); - if (!LooksLikeShaderManifest(sourceText)) { - return LooksLikeShaderAuthoring(sourceText) - ? CollectShaderAuthoringDependencyPaths(path, sourceText, outDependencies) - : true; + if (!LooksLikeShaderAuthoring(sourceText)) { + return false; } - return CollectShaderManifestDependencyPaths(path, sourceText, outDependencies); + return CollectShaderAuthoringDependencyPaths(path, sourceText, outDependencies); } ShaderType ShaderLoader::DetectShaderType(const Containers::String& path, const Containers::String& source) { diff --git a/tests/Resources/Material/test_material_loader.cpp b/tests/Resources/Material/test_material_loader.cpp index 3a6a77e3..8fbe0f94 100644 --- a/tests/Resources/Material/test_material_loader.cpp +++ b/tests/Resources/Material/test_material_loader.cpp @@ -67,64 +67,45 @@ void WriteTextFile(const std::filesystem::path& path, const std::string& content ASSERT_TRUE(static_cast(output)); } -std::filesystem::path WriteSchemaMaterialShaderManifest(const std::filesystem::path& rootPath) { +std::filesystem::path WriteSchemaMaterialShaderAuthoring(const std::filesystem::path& rootPath) { namespace fs = std::filesystem; const fs::path shaderDir = rootPath / "Shaders"; fs::create_directories(shaderDir); - WriteTextFile(shaderDir / "schema.vert.glsl", "#version 430\nvoid main() {}\n"); - WriteTextFile(shaderDir / "schema.frag.glsl", "#version 430\nvoid main() {}\n"); - - const fs::path manifestPath = shaderDir / "schema.shader"; - std::ofstream manifest(manifestPath, std::ios::binary | std::ios::trunc); - EXPECT_TRUE(manifest.is_open()); - if (!manifest.is_open()) { + const fs::path shaderPath = shaderDir / "schema.shader"; + std::ofstream shader(shaderPath, std::ios::binary | std::ios::trunc); + EXPECT_TRUE(shader.is_open()); + if (!shader.is_open()) { return {}; } - manifest << "{\n"; - manifest << " \"name\": \"SchemaMaterialShader\",\n"; - manifest << " \"properties\": [\n"; - manifest << " {\n"; - manifest << " \"name\": \"_BaseColor\",\n"; - manifest << " \"displayName\": \"Base Color\",\n"; - manifest << " \"type\": \"Color\",\n"; - manifest << " \"defaultValue\": \"(1,0.5,0.25,1)\",\n"; - manifest << " \"semantic\": \"BaseColor\"\n"; - manifest << " },\n"; - manifest << " {\n"; - manifest << " \"name\": \"_Metallic\",\n"; - manifest << " \"displayName\": \"Metallic\",\n"; - manifest << " \"type\": \"Float\",\n"; - manifest << " \"defaultValue\": \"0.7\"\n"; - manifest << " },\n"; - manifest << " {\n"; - manifest << " \"name\": \"_Mode\",\n"; - manifest << " \"displayName\": \"Mode\",\n"; - manifest << " \"type\": \"Int\",\n"; - manifest << " \"defaultValue\": \"2\"\n"; - manifest << " },\n"; - manifest << " {\n"; - manifest << " \"name\": \"_MainTex\",\n"; - manifest << " \"displayName\": \"Main Tex\",\n"; - manifest << " \"type\": \"Texture2D\",\n"; - manifest << " \"defaultValue\": \"white\",\n"; - manifest << " \"semantic\": \"BaseColorTexture\"\n"; - manifest << " }\n"; - manifest << " ],\n"; - manifest << " \"passes\": [\n"; - manifest << " {\n"; - manifest << " \"name\": \"ForwardLit\",\n"; - manifest << " \"variants\": [\n"; - manifest << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"schema.vert.glsl\" },\n"; - manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"schema.frag.glsl\" }\n"; - manifest << " ]\n"; - manifest << " }\n"; - manifest << " ]\n"; - manifest << "}\n"; - EXPECT_TRUE(static_cast(manifest)); + shader << R"(Shader "SchemaMaterialShader" +{ + Properties + { + _BaseColor ("Base Color", Color) = (1,0.5,0.25,1) [Semantic(BaseColor)] + _Metallic ("Metallic", Float) = 0.7 + _Mode ("Mode", Int) = 2 + _MainTex ("Main Tex", 2D) = "white" [Semantic(BaseColorTexture)] + } + SubShader + { + Pass + { + Name "ForwardLit" + HLSLPROGRAM + #pragma vertex MainVS + #pragma fragment MainPS + float4 MainVS() : SV_POSITION { return 0; } + float4 MainPS() : SV_TARGET { return 1; } + ENDHLSL + } + } +} +)"; + EXPECT_TRUE(static_cast(shader)); - return manifestPath; + return shaderPath; } std::filesystem::path WriteKeywordMaterialShaderAuthoring(const std::filesystem::path& rootPath) { @@ -200,15 +181,34 @@ TEST(MaterialLoader, ResourceManagerRegistersMaterialAndShaderLoaders) { } TEST(MaterialLoader, LoadValidMaterialParsesRenderMetadata) { + ResourceManager& manager = ResourceManager::Get(); + manager.Initialize(); + const std::filesystem::path shaderPath = - std::filesystem::current_path() / "material_loader_valid_shader.hlsl"; + std::filesystem::current_path() / "material_loader_valid_shader.shader"; const std::filesystem::path materialPath = std::filesystem::current_path() / "material_loader_valid.material"; { std::ofstream shaderFile(shaderPath); ASSERT_TRUE(shaderFile.is_open()); - shaderFile << "float4 MainPS() : SV_TARGET { return float4(1, 1, 1, 1); }"; + shaderFile << R"(Shader "Test/ValidMaterial" +{ + SubShader + { + Pass + { + Name "ForwardLit" + HLSLPROGRAM + #pragma vertex MainVS + #pragma fragment MainPS + float4 MainVS() : SV_POSITION { return 0; } + float4 MainPS() : SV_TARGET { return float4(1, 1, 1, 1); } + ENDHLSL + } + } +} +)"; } { @@ -243,7 +243,7 @@ TEST(MaterialLoader, LoadValidMaterialParsesRenderMetadata) { EXPECT_TRUE(material->IsValid()); EXPECT_NE(material->GetShader(), nullptr); EXPECT_EQ(material->GetRenderQueue(), static_cast(MaterialRenderQueue::Transparent)); - EXPECT_EQ(material->GetLegacyShaderPassHint(), "ForwardLit"); + EXPECT_TRUE(material->GetLegacyShaderPassHint().Empty()); EXPECT_EQ(material->GetTag("LightMode"), "ForwardBase"); EXPECT_EQ(material->GetTag("RenderType"), "Transparent"); EXPECT_EQ(material->GetRenderState().cullMode, MaterialCullMode::Back); @@ -255,6 +255,7 @@ TEST(MaterialLoader, LoadValidMaterialParsesRenderMetadata) { EXPECT_TRUE(material->HasRenderStateOverride()); delete material; + manager.Shutdown(); std::remove(materialPath.string().c_str()); std::remove(shaderPath.string().c_str()); } @@ -282,55 +283,103 @@ TEST(MaterialLoader, LoadMaterialWithoutRenderStateLeavesOverrideDisabled) { std::remove(materialPath.string().c_str()); } -TEST(MaterialLoader, LoadMaterialWithShaderManifestResolvesShaderPass) { - namespace fs = std::filesystem; - - ResourceManager& manager = ResourceManager::Get(); - manager.Initialize(); - - const fs::path shaderRoot = fs::temp_directory_path() / "xc_material_shader_manifest_test"; - const fs::path shaderDir = shaderRoot / "Shaders"; - const fs::path manifestPath = shaderDir / "lit.shader"; - const fs::path materialPath = shaderRoot / "manifest.material"; - - fs::remove_all(shaderRoot); - fs::create_directories(shaderDir); - - { - std::ofstream vertexFile(shaderDir / "lit.vert.glsl"); - ASSERT_TRUE(vertexFile.is_open()); - vertexFile << "#version 430\n// MATERIAL_MANIFEST_GL_VS\nvoid main() {}\n"; - } - - { - std::ofstream fragmentFile(shaderDir / "lit.frag.glsl"); - ASSERT_TRUE(fragmentFile.is_open()); - fragmentFile << "#version 430\n// MATERIAL_MANIFEST_GL_PS\nvoid main() {}\n"; - } - - { - std::ofstream manifestFile(manifestPath); - ASSERT_TRUE(manifestFile.is_open()); - manifestFile << "{\n"; - manifestFile << " \"name\": \"ManifestLit\",\n"; - manifestFile << " \"passes\": [\n"; - manifestFile << " {\n"; - manifestFile << " \"name\": \"ForwardLit\",\n"; - manifestFile << " \"tags\": { \"LightMode\": \"ForwardBase\" },\n"; - manifestFile << " \"variants\": [\n"; - manifestFile << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"lit.vert.glsl\" },\n"; - manifestFile << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"lit.frag.glsl\" }\n"; - manifestFile << " ]\n"; - manifestFile << " }\n"; - manifestFile << " ]\n"; - manifestFile << "}\n"; - } +TEST(MaterialLoader, LoadMaterialParsesOffsetAndStencilRenderState) { + const std::filesystem::path materialPath = + std::filesystem::current_path() / "material_loader_offset_stencil.material"; { std::ofstream materialFile(materialPath); ASSERT_TRUE(materialFile.is_open()); materialFile << "{\n"; - materialFile << " \"shader\": \"" << manifestPath.generic_string() << "\",\n"; + materialFile << " \"renderState\": {\n"; + materialFile << " \"offset\": [1.5, 2],\n"; + materialFile << " \"stencil\": {\n"; + materialFile << " \"ref\": 7,\n"; + materialFile << " \"readMask\": 63,\n"; + materialFile << " \"writeMask\": 31,\n"; + materialFile << " \"comp\": \"Equal\",\n"; + materialFile << " \"pass\": \"Replace\",\n"; + materialFile << " \"fail\": \"Keep\",\n"; + materialFile << " \"zFail\": \"IncrSat\",\n"; + materialFile << " \"compBack\": \"NotEqual\",\n"; + materialFile << " \"passBack\": \"DecrWrap\",\n"; + materialFile << " \"failBack\": \"Invert\",\n"; + materialFile << " \"zFailBack\": \"Zero\"\n"; + materialFile << " }\n"; + materialFile << " }\n"; + materialFile << "}\n"; + } + + MaterialLoader loader; + LoadResult result = loader.Load(materialPath.string().c_str()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + auto* material = static_cast(result.resource); + ASSERT_NE(material, nullptr); + EXPECT_FLOAT_EQ(material->GetRenderState().depthBiasFactor, 1.5f); + EXPECT_EQ(material->GetRenderState().depthBiasUnits, 2); + EXPECT_TRUE(material->GetRenderState().stencil.enabled); + EXPECT_EQ(material->GetRenderState().stencil.reference, 7u); + EXPECT_EQ(material->GetRenderState().stencil.readMask, 63u); + EXPECT_EQ(material->GetRenderState().stencil.writeMask, 31u); + EXPECT_EQ(material->GetRenderState().stencil.front.func, MaterialComparisonFunc::Equal); + EXPECT_EQ(material->GetRenderState().stencil.front.passOp, MaterialStencilOp::Replace); + EXPECT_EQ(material->GetRenderState().stencil.front.depthFailOp, MaterialStencilOp::IncrSat); + EXPECT_EQ(material->GetRenderState().stencil.back.func, MaterialComparisonFunc::NotEqual); + EXPECT_EQ(material->GetRenderState().stencil.back.passOp, MaterialStencilOp::DecrWrap); + EXPECT_EQ(material->GetRenderState().stencil.back.failOp, MaterialStencilOp::Invert); + EXPECT_EQ(material->GetRenderState().stencil.back.depthFailOp, MaterialStencilOp::Zero); + + delete material; + std::remove(materialPath.string().c_str()); +} + +TEST(MaterialLoader, LoadMaterialWithAuthoringShaderResolvesShaderPass) { + namespace fs = std::filesystem; + + ResourceManager& manager = ResourceManager::Get(); + manager.Initialize(); + + const fs::path shaderRoot = fs::temp_directory_path() / "xc_material_shader_authoring_test"; + const fs::path shaderDir = shaderRoot / "Shaders"; + const fs::path shaderPath = shaderDir / "lit.shader"; + const fs::path materialPath = shaderRoot / "authoring.material"; + + fs::remove_all(shaderRoot); + fs::create_directories(shaderDir); + WriteTextFile( + shaderPath, + R"(Shader "AuthoringLit" +{ + SubShader + { + Pass + { + Name "ForwardLit" + Tags { "LightMode" = "ForwardBase" } + HLSLPROGRAM + #pragma vertex MainVS + #pragma fragment MainPS + float4 MainVS() : SV_POSITION + { + return 0; // MATERIAL_AUTHORING_VS + } + float4 MainPS() : SV_TARGET + { + return 1; // MATERIAL_AUTHORING_PS + } + ENDHLSL + } + } +} +)"); + + { + std::ofstream materialFile(materialPath); + ASSERT_TRUE(materialFile.is_open()); + materialFile << "{\n"; + materialFile << " \"shader\": \"" << shaderPath.generic_string() << "\",\n"; materialFile << " \"shaderPass\": \"ForwardLit\",\n"; materialFile << " \"renderQueue\": \"Geometry\"\n"; materialFile << "}\n"; @@ -349,7 +398,7 @@ TEST(MaterialLoader, LoadMaterialWithShaderManifestResolvesShaderPass) { const ShaderStageVariant* vertexVariant = material->GetShader()->FindVariant("ForwardLit", ShaderType::Vertex, ShaderBackend::OpenGL); ASSERT_NE(vertexVariant, nullptr); - EXPECT_NE(std::string(vertexVariant->sourceCode.CStr()).find("MATERIAL_MANIFEST_GL_VS"), std::string::npos); + EXPECT_NE(std::string(vertexVariant->sourceCode.CStr()).find("MATERIAL_AUTHORING_VS"), std::string::npos); delete material; manager.Shutdown(); @@ -362,7 +411,7 @@ TEST(MaterialLoader, LoadMaterialWithPropertiesObjectAppliesTypedOverrides) { const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_properties_override_test"; fs::remove_all(rootPath); - const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath); + const fs::path shaderPath = WriteSchemaMaterialShaderAuthoring(rootPath); ASSERT_FALSE(shaderPath.empty()); const fs::path materialPath = rootPath / "override.material"; @@ -437,7 +486,7 @@ TEST(MaterialLoader, RejectsUnknownPropertyAgainstShaderSchema) { const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_properties_unknown_test"; fs::remove_all(rootPath); - const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath); + const fs::path shaderPath = WriteSchemaMaterialShaderAuthoring(rootPath); ASSERT_FALSE(shaderPath.empty()); const fs::path materialPath = rootPath / "unknown_property.material"; @@ -463,7 +512,7 @@ TEST(MaterialLoader, RejectsTypeMismatchAgainstShaderSchema) { const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_properties_mismatch_test"; fs::remove_all(rootPath); - const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath); + const fs::path shaderPath = WriteSchemaMaterialShaderAuthoring(rootPath); ASSERT_FALSE(shaderPath.empty()); const fs::path materialPath = rootPath / "type_mismatch.material"; @@ -489,7 +538,7 @@ TEST(MaterialLoader, LoadMaterialWithPropertiesObjectPreservesShaderDefaultsForO const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_properties_defaults_test"; fs::remove_all(rootPath); - const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath); + const fs::path shaderPath = WriteSchemaMaterialShaderAuthoring(rootPath); ASSERT_FALSE(shaderPath.empty()); const fs::path materialPath = rootPath / "defaults.material"; @@ -526,7 +575,7 @@ TEST(MaterialLoader, LoadMaterialMapsLegacySemanticKeysIntoShaderSchemaPropertie fs::remove_all(rootPath); fs::create_directories(rootPath); - const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath); + const fs::path shaderPath = WriteSchemaMaterialShaderAuthoring(rootPath); ASSERT_FALSE(shaderPath.empty()); const fs::path materialPath = rootPath / "semantic_alias.material"; @@ -604,7 +653,7 @@ TEST(MaterialLoader, RejectsUnknownTextureBindingAgainstShaderSchema) { fs::remove_all(rootPath); fs::create_directories(rootPath); - const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath); + const fs::path shaderPath = WriteSchemaMaterialShaderAuthoring(rootPath); ASSERT_FALSE(shaderPath.empty()); const fs::path materialPath = rootPath / "unknown_texture.material"; @@ -985,31 +1034,31 @@ TEST(MaterialLoader, AssetDatabaseReimportsMaterialWhenShaderDependencyChanges) const fs::path projectRoot = fs::temp_directory_path() / "xc_material_shader_dependency_reimport_test"; const fs::path assetsDir = projectRoot / "Assets"; const fs::path shaderDir = assetsDir / "Shaders"; - const fs::path shaderManifestPath = shaderDir / "lit.shader"; + const fs::path shaderPath = shaderDir / "lit.shader"; const fs::path materialPath = assetsDir / "textured.material"; fs::remove_all(projectRoot); fs::create_directories(shaderDir); - WriteTextFile(shaderDir / "lit.vert.glsl", "#version 430\nvoid main() {}\n"); - WriteTextFile(shaderDir / "lit.frag.glsl", "#version 430\nvoid main() {}\n"); - + WriteTextFile( + shaderPath, + R"(Shader "MaterialDependencyShader" +{ + SubShader { - std::ofstream manifest(shaderManifestPath); - ASSERT_TRUE(manifest.is_open()); - manifest << "{\n"; - manifest << " \"name\": \"MaterialDependencyShader\",\n"; - manifest << " \"passes\": [\n"; - manifest << " {\n"; - manifest << " \"name\": \"ForwardLit\",\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 << " ]\n"; - manifest << " }\n"; - manifest << " ]\n"; - manifest << "}\n"; + Pass + { + Name "ForwardLit" + HLSLPROGRAM + #pragma vertex MainVS + #pragma fragment MainPS + float4 MainVS() : SV_POSITION { return 0; } + float4 MainPS() : SV_TARGET { return 1; } + ENDHLSL + } } +} +)"); { std::ofstream materialFile(materialPath); @@ -1034,10 +1083,10 @@ TEST(MaterialLoader, AssetDatabaseReimportsMaterialWhenShaderDependencyChanges) std::this_thread::sleep_for(50ms); { - std::ofstream manifest(shaderManifestPath, std::ios::app); - ASSERT_TRUE(manifest.is_open()); - manifest << "\n"; - ASSERT_TRUE(static_cast(manifest)); + std::ofstream shaderFile(shaderPath, std::ios::app); + ASSERT_TRUE(shaderFile.is_open()); + shaderFile << "\n// force shader dependency reimport\n"; + ASSERT_TRUE(static_cast(shaderFile)); } database.Initialize(projectRoot.string().c_str()); diff --git a/tests/Resources/Shader/test_shader.cpp b/tests/Resources/Shader/test_shader.cpp index 3fce62f6..16c82272 100644 --- a/tests/Resources/Shader/test_shader.cpp +++ b/tests/Resources/Shader/test_shader.cpp @@ -10,8 +10,7 @@ namespace { TEST(Shader, DefaultConstructor) { Shader shader; - EXPECT_EQ(shader.GetShaderType(), ShaderType::Fragment); - EXPECT_EQ(shader.GetShaderLanguage(), ShaderLanguage::GLSL); + EXPECT_EQ(shader.GetPassCount(), 0u); EXPECT_FALSE(shader.IsValid()); EXPECT_EQ(shader.GetMemorySize(), 0u); } @@ -21,64 +20,42 @@ TEST(Shader, GetType) { EXPECT_EQ(shader.GetType(), ResourceType::Shader); } -TEST(Shader, SetGetShaderType) { +TEST(Shader, AddPassVariantStoresStageLanguageAndPayload) { Shader shader; - - shader.SetShaderType(ShaderType::Vertex); - EXPECT_EQ(shader.GetShaderType(), ShaderType::Vertex); - - shader.SetShaderType(ShaderType::Fragment); - EXPECT_EQ(shader.GetShaderType(), ShaderType::Fragment); - - shader.SetShaderType(ShaderType::Geometry); - EXPECT_EQ(shader.GetShaderType(), ShaderType::Geometry); - - shader.SetShaderType(ShaderType::Compute); - EXPECT_EQ(shader.GetShaderType(), ShaderType::Compute); -} + ShaderStageVariant variant = {}; + variant.stage = ShaderType::Vertex; + variant.language = ShaderLanguage::HLSL; + variant.backend = ShaderBackend::D3D12; + variant.entryPoint = "MainVS"; + variant.profile = "vs_5_0"; + variant.sourceCode = "float4 MainVS() : SV_POSITION { return 0; }"; + variant.compiledBinary = { 0x00, 0x01, 0x02, 0x03 }; -TEST(Shader, SetGetShaderLanguage) { - Shader shader; - - shader.SetShaderLanguage(ShaderLanguage::GLSL); - EXPECT_EQ(shader.GetShaderLanguage(), ShaderLanguage::GLSL); - - shader.SetShaderLanguage(ShaderLanguage::HLSL); - EXPECT_EQ(shader.GetShaderLanguage(), ShaderLanguage::HLSL); - - shader.SetShaderLanguage(ShaderLanguage::SPIRV); - EXPECT_EQ(shader.GetShaderLanguage(), ShaderLanguage::SPIRV); -} + shader.AddPassVariant("ForwardLit", variant); -TEST(Shader, SetGetSourceCode) { - Shader shader; - const char* source = "#version 330 core\nvoid main() {}"; - - shader.SetSourceCode(source); - EXPECT_EQ(shader.GetSourceCode(), source); -} - -TEST(Shader, SetGetCompiledBinary) { - Shader shader; - XCEngine::Containers::Array binary = { 0x00, 0x01, 0x02, 0x03 }; - - shader.SetCompiledBinary(binary); - EXPECT_EQ(shader.GetCompiledBinary().Size(), 4u); - EXPECT_EQ(shader.GetCompiledBinary()[0], 0x00); - EXPECT_EQ(shader.GetCompiledBinary()[3], 0x03); + const ShaderStageVariant* storedVariant = + shader.FindVariant("ForwardLit", ShaderType::Vertex, ShaderBackend::D3D12); + ASSERT_NE(storedVariant, nullptr); + EXPECT_EQ(storedVariant->stage, ShaderType::Vertex); + EXPECT_EQ(storedVariant->language, ShaderLanguage::HLSL); + EXPECT_EQ(storedVariant->entryPoint, "MainVS"); + EXPECT_EQ(storedVariant->profile, "vs_5_0"); + EXPECT_EQ(storedVariant->sourceCode, "float4 MainVS() : SV_POSITION { return 0; }"); + ASSERT_EQ(storedVariant->compiledBinary.Size(), 4u); + EXPECT_EQ(storedVariant->compiledBinary[3], 0x03); } TEST(Shader, AddGetUniforms) { Shader shader; - + ShaderUniform uniform1; uniform1.name = "uModelView"; uniform1.location = 0; uniform1.size = 1; uniform1.type = 4; - + shader.AddUniform(uniform1); - + const auto& uniforms = shader.GetUniforms(); EXPECT_EQ(uniforms.Size(), 1u); EXPECT_EQ(uniforms[0].name, "uModelView"); @@ -86,36 +63,20 @@ TEST(Shader, AddGetUniforms) { TEST(Shader, AddGetAttributes) { Shader shader; - + ShaderAttribute attr1; attr1.name = "aPosition"; attr1.location = 0; attr1.size = 1; attr1.type = 3; - + shader.AddAttribute(attr1); - + const auto& attributes = shader.GetAttributes(); EXPECT_EQ(attributes.Size(), 1u); EXPECT_EQ(attributes[0].name, "aPosition"); } -TEST(Shader, LegacySingleStageStateSyncsIntoDefaultPassVariant) { - Shader shader; - shader.SetShaderType(ShaderType::Vertex); - shader.SetShaderLanguage(ShaderLanguage::HLSL); - shader.SetSourceCode("float4 MainVS() : SV_POSITION { return 0; }"); - - ASSERT_EQ(shader.GetPassCount(), 1u); - const ShaderPass* pass = shader.FindPass("Default"); - ASSERT_NE(pass, nullptr); - ASSERT_EQ(pass->variants.Size(), 1u); - EXPECT_EQ(pass->variants[0].stage, ShaderType::Vertex); - EXPECT_EQ(pass->variants[0].language, ShaderLanguage::HLSL); - EXPECT_EQ(pass->variants[0].backend, ShaderBackend::Generic); - EXPECT_EQ(pass->variants[0].sourceCode, "float4 MainVS() : SV_POSITION { return 0; }"); -} - TEST(Shader, FindsBackendSpecificVariantAndFallsBackToGeneric) { Shader shader; @@ -309,7 +270,6 @@ TEST(Shader, StoresPassKeywordDeclarationsAndQueriesDeclaredKeywords) { TEST(Shader, ReleaseClearsPassRuntimeData) { Shader shader; - shader.SetSourceCode("void main() {}"); ShaderPropertyDesc property = {}; property.name = "_BaseColor"; property.type = ShaderPropertyType::Color; @@ -323,8 +283,6 @@ TEST(Shader, ReleaseClearsPassRuntimeData) { EXPECT_EQ(shader.GetProperties().Size(), 0u); EXPECT_EQ(shader.GetPassCount(), 0u); - EXPECT_EQ(shader.GetSourceCode(), ""); - EXPECT_EQ(shader.GetCompiledBinary().Size(), 0u); EXPECT_FALSE(shader.IsValid()); } diff --git a/tests/Resources/Shader/test_shader_loader.cpp b/tests/Resources/Shader/test_shader_loader.cpp index 6d0c420e..f6a28c99 100644 --- a/tests/Resources/Shader/test_shader_loader.cpp +++ b/tests/Resources/Shader/test_shader_loader.cpp @@ -1193,6 +1193,171 @@ TEST(ShaderLoader, AssetDatabaseReimportsShaderWhenUsePassDependencyChanges) { fs::remove_all(projectRoot); } +TEST(ShaderLoader, AssetDatabaseReimportsShaderWhenBuiltinUsePassDependencyChanges) { + namespace fs = std::filesystem; + using namespace std::chrono_literals; + + const fs::path sandboxRoot = + fs::temp_directory_path() / "xc_shader_authoring_builtin_usepass_reimport"; + const fs::path projectRoot = sandboxRoot / "Project"; + const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; + const fs::path mainShaderPath = shaderDir / "main.shader"; + const fs::path builtinShaderAssetPath = + sandboxRoot / "engine" / "assets" / "builtin" / "shaders" / "shadow-caster.shader"; + + const fs::path previousPath = fs::current_path(); + fs::remove_all(sandboxRoot); + fs::create_directories(shaderDir); + fs::create_directories(builtinShaderAssetPath.parent_path()); + + auto writeBuiltinShader = [&](const char* cullMode, const char* marker) { + WriteTextFile( + builtinShaderAssetPath, + std::string(R"(Shader "Builtin Shadow Caster" +{ + SubShader + { + Pass + { + Name "ShadowCaster" + Tags { "LightMode" = "ShadowCaster" } + Cull )") + + cullMode + + R"( + HLSLPROGRAM + #pragma vertex MainVS + #pragma fragment MainPS + float4 MainVS() : SV_POSITION { return 0; } + float4 MainPS() : SV_TARGET { return )" + + marker + + R"(; } + ENDHLSL + } + } +} +)"); + }; + + writeBuiltinShader("Back", "float4(1.0, 0.0, 0.0, 1.0)"); + WriteTextFile( + mainShaderPath, + R"(Shader "Builtin UsePass Dependency Shader" +{ + SubShader + { + UsePass "Builtin Shadow Caster/ShadowCaster" + } +} +)"); + + fs::current_path(projectRoot); + + ShaderLoader dependencyLoader; + Array firstDependencies; + ASSERT_TRUE(dependencyLoader.CollectSourceDependencies(mainShaderPath.string().c_str(), firstDependencies)); + ASSERT_EQ(firstDependencies.Size(), 1u); + EXPECT_EQ( + fs::path(firstDependencies[0].CStr()).lexically_normal(), + builtinShaderAssetPath.lexically_normal()); + + AssetDatabase database; + database.Initialize(projectRoot.string().c_str()); + + AssetDatabase::ResolvedAsset firstResolve; + ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/main.shader", ResourceType::Shader, firstResolve)); + ASSERT_TRUE(firstResolve.artifactReady); + + ShaderLoader loader; + LoadResult firstLoad = loader.Load(firstResolve.artifactMainPath.CStr()); + ASSERT_TRUE(firstLoad); + auto* firstShader = static_cast(firstLoad.resource); + ASSERT_NE(firstShader, nullptr); + const ShaderPass* firstPass = firstShader->FindPass("ShadowCaster"); + ASSERT_NE(firstPass, nullptr); + EXPECT_EQ(firstPass->fixedFunctionState.cullMode, MaterialCullMode::Back); + const ShaderStageVariant* firstFragmentVariant = + firstShader->FindVariant("ShadowCaster", ShaderType::Fragment, ShaderBackend::D3D12); + ASSERT_NE(firstFragmentVariant, nullptr); + EXPECT_NE( + std::string(firstFragmentVariant->sourceCode.CStr()).find("float4(1.0, 0.0, 0.0, 1.0)"), + std::string::npos); + delete firstShader; + + const String firstArtifactPath = firstResolve.artifactMainPath; + database.Shutdown(); + + std::this_thread::sleep_for(50ms); + writeBuiltinShader("Front", "float4(0.0, 1.0, 0.0, 1.0)"); + + database.Initialize(projectRoot.string().c_str()); + AssetDatabase::ResolvedAsset secondResolve; + ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/main.shader", ResourceType::Shader, secondResolve)); + ASSERT_TRUE(secondResolve.artifactReady); + EXPECT_NE(secondResolve.artifactMainPath, firstArtifactPath); + + LoadResult secondLoad = loader.Load(secondResolve.artifactMainPath.CStr()); + ASSERT_TRUE(secondLoad); + auto* secondShader = static_cast(secondLoad.resource); + ASSERT_NE(secondShader, nullptr); + const ShaderPass* secondPass = secondShader->FindPass("ShadowCaster"); + ASSERT_NE(secondPass, nullptr); + EXPECT_EQ(secondPass->fixedFunctionState.cullMode, MaterialCullMode::Front); + const ShaderStageVariant* secondFragmentVariant = + secondShader->FindVariant("ShadowCaster", ShaderType::Fragment, ShaderBackend::D3D12); + ASSERT_NE(secondFragmentVariant, nullptr); + EXPECT_NE( + std::string(secondFragmentVariant->sourceCode.CStr()).find("float4(0.0, 1.0, 0.0, 1.0)"), + std::string::npos); + delete secondShader; + + database.Shutdown(); + fs::current_path(previousPath); + fs::remove_all(sandboxRoot); +} + +TEST(ShaderLoader, LoadShaderAuthoringRejectsCyclicProjectUsePass) { + namespace fs = std::filesystem; + + const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_usepass_cycle"; + const fs::path shaderAPath = shaderRoot / "a.shader"; + const fs::path shaderBPath = shaderRoot / "b.shader"; + + fs::remove_all(shaderRoot); + fs::create_directories(shaderRoot); + + WriteTextFile( + shaderAPath, + R"(Shader "Cycle Shader A" +{ + SubShader + { + UsePass "Cycle Shader B/PassB" + } +} +)"); + + WriteTextFile( + shaderBPath, + R"(Shader "Cycle Shader B" +{ + SubShader + { + UsePass "Cycle Shader A/PassA" + } +} +)"); + + ShaderLoader loader; + LoadResult result = loader.Load(shaderAPath.string().c_str()); + EXPECT_FALSE(result.success); + EXPECT_EQ(result.resource, nullptr); + EXPECT_NE( + std::string(result.errorMessage.CStr()).find("cyclic shader reference"), + std::string::npos); + + fs::remove_all(shaderRoot); +} + TEST(ShaderLoader, LoadShaderAuthoringRejectsLegacyBackendPragma) { namespace fs = std::filesystem;