From 806ef7422654734d4d66727af2fdc04bba214297 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Mon, 6 Apr 2026 17:21:40 +0800 Subject: [PATCH] rendering: split shader loader authoring modes --- engine/src/Resources/Shader/ShaderLoader.cpp | 716 +++++++++++++++++- tests/Resources/Shader/test_shader_loader.cpp | 192 ++++- 2 files changed, 865 insertions(+), 43 deletions(-) diff --git a/engine/src/Resources/Shader/ShaderLoader.cpp b/engine/src/Resources/Shader/ShaderLoader.cpp index d93864eb..c50ab723 100644 --- a/engine/src/Resources/Shader/ShaderLoader.cpp +++ b/engine/src/Resources/Shader/ShaderLoader.cpp @@ -585,6 +585,13 @@ bool ReadTextFile(const Containers::String& path, Containers::String& outText) { } size_t CalculateShaderMemorySize(const Shader& shader); +bool TryTokenizeQuotedArguments(const std::string& line, std::vector& outTokens); + +enum class ShaderAuthoringStyle { + NotShaderAuthoring = 0, + LegacyBackendSplit, + UnityStyleSingleSource +}; struct AuthoringTagEntry { Containers::String name; @@ -606,20 +613,36 @@ struct AuthoringPassEntry { Containers::Array resources; Containers::String vertexEntryPoint; Containers::String fragmentEntryPoint; + Containers::String sharedProgramSource; + Containers::String programSource; + Containers::String targetProfile; std::vector backendVariants; }; struct AuthoringSubShaderEntry { std::vector tags; + Containers::String sharedProgramSource; std::vector passes; }; struct AuthoringShaderDesc { Containers::String name; + Containers::String sharedProgramSource; Containers::Array properties; std::vector subShaders; }; +struct ExtractedProgramBlock { + enum class Kind { + SharedInclude, + PassProgram + }; + + Kind kind = Kind::PassProgram; + Containers::String sourceText; + size_t markerLine = 0; +}; + std::string StripAuthoringLineComment(const std::string& line) { bool inString = false; bool escaped = false; @@ -697,6 +720,210 @@ void SplitShaderAuthoringLines( } } +bool TryExtractProgramBlocks( + const std::string& sourceText, + std::vector& outBlocks, + Containers::String* outError) { + outBlocks.clear(); + + std::istringstream stream(sourceText); + std::string rawLine; + bool insideBlock = false; + ExtractedProgramBlock currentBlock = {}; + std::string blockSource; + size_t lineNumber = 0; + + auto fail = [&outError](const std::string& message, size_t humanLine) -> bool { + if (outError != nullptr) { + *outError = Containers::String( + ("Unity-style shader parse error at line " + std::to_string(humanLine) + ": " + message).c_str()); + } + return false; + }; + + while (std::getline(stream, rawLine)) { + ++lineNumber; + const std::string normalizedLine = TrimCopy(StripAuthoringLineComment(rawLine)); + + if (!insideBlock) { + if (normalizedLine == "HLSLINCLUDE" || normalizedLine == "CGINCLUDE") { + insideBlock = true; + currentBlock = {}; + currentBlock.kind = ExtractedProgramBlock::Kind::SharedInclude; + currentBlock.markerLine = lineNumber; + blockSource.clear(); + } else if (normalizedLine == "HLSLPROGRAM" || normalizedLine == "CGPROGRAM") { + insideBlock = true; + currentBlock = {}; + currentBlock.kind = ExtractedProgramBlock::Kind::PassProgram; + currentBlock.markerLine = lineNumber; + blockSource.clear(); + } + continue; + } + + if (normalizedLine == "ENDHLSL" || normalizedLine == "ENDCG") { + currentBlock.sourceText = blockSource.c_str(); + outBlocks.push_back(std::move(currentBlock)); + insideBlock = false; + blockSource.clear(); + continue; + } + + blockSource += rawLine; + blockSource += '\n'; + } + + if (insideBlock) { + return fail("program block was not closed", lineNumber); + } + + return true; +} + +bool ContainsBackendPragma(const std::vector& lines) { + for (const std::string& line : lines) { + if (line.rfind("#pragma backend", 0) == 0) { + return true; + } + } + + return false; +} + +bool ContainsResourcesBlock(const std::vector& lines) { + for (const std::string& line : lines) { + if (line == "Resources" || StartsWithKeyword(line, "Resources")) { + return true; + } + } + + return false; +} + +bool ContainsSingleSourceAuthoringConstructs(const std::vector& lines) { + for (const std::string& line : lines) { + if (line == "HLSLINCLUDE" || line == "CGINCLUDE") { + return true; + } + + if (line.rfind("#pragma target", 0) == 0 || + line.rfind("#pragma multi_compile", 0) == 0 || + line.rfind("#pragma shader_feature", 0) == 0 || + line.rfind("#pragma shader_feature_local", 0) == 0) { + return true; + } + } + + return false; +} + +ShaderAuthoringStyle DetectShaderAuthoringStyle(const std::string& sourceText) { + std::vector lines; + SplitShaderAuthoringLines(sourceText, lines); + if (lines.empty() || !StartsWithKeyword(lines.front(), "Shader")) { + return ShaderAuthoringStyle::NotShaderAuthoring; + } + + const bool hasBackendPragma = ContainsBackendPragma(lines); + const bool hasSingleSourceConstructs = ContainsSingleSourceAuthoringConstructs(lines); + + if (hasBackendPragma && !hasSingleSourceConstructs) { + return ShaderAuthoringStyle::LegacyBackendSplit; + } + + if (ContainsResourcesBlock(lines)) { + return ShaderAuthoringStyle::UnityStyleSingleSource; + } + + if (hasSingleSourceConstructs) { + return ShaderAuthoringStyle::UnityStyleSingleSource; + } + + return ShaderAuthoringStyle::UnityStyleSingleSource; +} + +void AppendAuthoringSourceBlock( + Containers::String& target, + const Containers::String& sourceBlock) { + if (sourceBlock.Empty()) { + return; + } + + if (!target.Empty()) { + target += '\n'; + } + target += sourceBlock; +} + +void CollectQuotedIncludeDependencyPaths( + const Containers::String& sourcePath, + const Containers::String& sourceText, + std::unordered_set& seenPaths, + Containers::Array& outDependencies) { + std::istringstream stream(ToStdString(sourceText)); + std::string rawLine; + while (std::getline(stream, rawLine)) { + const std::string line = TrimCopy(StripAuthoringLineComment(rawLine)); + if (line.rfind("#include", 0) != 0) { + continue; + } + + const size_t firstQuote = line.find('"'); + if (firstQuote == std::string::npos) { + continue; + } + + const size_t secondQuote = line.find('"', firstQuote + 1); + if (secondQuote == std::string::npos || secondQuote <= firstQuote + 1) { + continue; + } + + const Containers::String includePath(line.substr(firstQuote + 1, secondQuote - firstQuote - 1).c_str()); + const Containers::String resolvedPath = ResolveShaderDependencyPath(includePath, sourcePath); + const std::string key = ToStdString(resolvedPath); + if (!key.empty() && seenPaths.insert(key).second) { + outDependencies.PushBack(resolvedPath); + } + } +} + +bool IsUnityStyleAuthoringPragmaDirective(const std::string& line) { + std::vector pragmaTokens; + if (!TryTokenizeQuotedArguments(line, pragmaTokens) || + pragmaTokens.empty() || + pragmaTokens[0] != "#pragma" || + pragmaTokens.size() < 2u) { + return false; + } + + return pragmaTokens[1] == "vertex" || + pragmaTokens[1] == "fragment" || + pragmaTokens[1] == "target" || + pragmaTokens[1] == "multi_compile" || + pragmaTokens[1] == "shader_feature" || + pragmaTokens[1] == "shader_feature_local" || + pragmaTokens[1] == "backend"; +} + +Containers::String StripUnityStyleAuthoringPragmas(const Containers::String& sourceText) { + std::istringstream stream(ToStdString(sourceText)); + std::string rawLine; + std::string strippedSource; + + while (std::getline(stream, rawLine)) { + const std::string normalizedLine = TrimCopy(StripAuthoringLineComment(rawLine)); + if (IsUnityStyleAuthoringPragmaDirective(normalizedLine)) { + continue; + } + + strippedSource += rawLine; + strippedSource += '\n'; + } + + return strippedSource.c_str(); +} + size_t FindMatchingDelimiter( const std::string& text, size_t openPos, @@ -1104,7 +1331,7 @@ bool TryParseAuthoringResourceLine( return true; } -bool ParseUnityLikeShaderAuthoring( +bool ParseLegacyBackendSplitShaderAuthoring( const Containers::String& path, const std::string& sourceText, AuthoringShaderDesc& outDesc, @@ -1124,7 +1351,7 @@ bool ParseUnityLikeShaderAuthoring( auto fail = [&outError](const std::string& message, size_t lineNumber) -> bool { if (outError != nullptr) { *outError = Containers::String( - ("Unity-like shader parse error at line " + std::to_string(lineNumber) + ": " + message).c_str()); + ("Legacy shader parse error at line " + std::to_string(lineNumber) + ": " + message).c_str()); } return false; }; @@ -1360,6 +1587,321 @@ bool ParseUnityLikeShaderAuthoring( return true; } +bool ParseUnityStyleSingleSourceShaderAuthoring( + const Containers::String& path, + const std::string& sourceText, + AuthoringShaderDesc& outDesc, + Containers::String* outError) { + (void)path; + outDesc = {}; + + enum class BlockKind { + None, + Shader, + Properties, + SubShader, + Pass + }; + + auto fail = [&outError](const std::string& message, size_t lineNumber) -> bool { + if (outError != nullptr) { + *outError = Containers::String( + ("Unity-style shader parse error at line " + std::to_string(lineNumber) + ": " + message).c_str()); + } + return false; + }; + + std::vector lines; + SplitShaderAuthoringLines(sourceText, lines); + if (lines.empty()) { + return fail("shader file is empty", 0); + } + + std::vector extractedBlocks; + Containers::String extractionError; + if (!TryExtractProgramBlocks(sourceText, extractedBlocks, &extractionError)) { + if (outError != nullptr) { + *outError = extractionError; + } + return false; + } + + size_t nextExtractedBlock = 0; + std::vector blockStack; + BlockKind pendingBlock = BlockKind::None; + AuthoringSubShaderEntry* currentSubShader = nullptr; + AuthoringPassEntry* currentPass = nullptr; + bool inProgramBlock = false; + bool inSharedIncludeBlock = false; + + auto currentBlock = [&blockStack]() -> BlockKind { + return blockStack.empty() ? BlockKind::None : blockStack.back(); + }; + + auto consumeExtractedBlock = [&](ExtractedProgramBlock::Kind expectedKind, + Containers::String& destination, + bool append, + size_t humanLine) -> bool { + if (nextExtractedBlock >= extractedBlocks.size()) { + return fail("program block source extraction is out of sync", humanLine); + } + + const ExtractedProgramBlock& block = extractedBlocks[nextExtractedBlock++]; + if (block.kind != expectedKind) { + return fail("program block source extraction mismatched block kind", humanLine); + } + + if (append) { + AppendAuthoringSourceBlock(destination, block.sourceText); + } else { + destination = block.sourceText; + } + return true; + }; + + for (size_t lineIndex = 0; lineIndex < lines.size(); ++lineIndex) { + const std::string& line = lines[lineIndex]; + const size_t humanLine = lineIndex + 1; + + if (inSharedIncludeBlock || inProgramBlock) { + if (line == "ENDHLSL" || line == "ENDCG") { + inSharedIncludeBlock = false; + inProgramBlock = false; + continue; + } + + std::vector pragmaTokens; + if (!TryTokenizeQuotedArguments(line, pragmaTokens) || pragmaTokens.empty()) { + continue; + } + + if (pragmaTokens[0] != "#pragma") { + continue; + } + + if (pragmaTokens.size() >= 2u && pragmaTokens[1] == "backend") { + return fail("Unity-style single-source shaders must not use #pragma backend", humanLine); + } + + if (inSharedIncludeBlock) { + continue; + } + + if (pragmaTokens.size() >= 3u && pragmaTokens[1] == "vertex") { + currentPass->vertexEntryPoint = pragmaTokens[2].c_str(); + continue; + } + if (pragmaTokens.size() >= 3u && pragmaTokens[1] == "fragment") { + currentPass->fragmentEntryPoint = pragmaTokens[2].c_str(); + continue; + } + if (pragmaTokens.size() >= 3u && pragmaTokens[1] == "target") { + currentPass->targetProfile = pragmaTokens[2].c_str(); + continue; + } + if (pragmaTokens.size() >= 2u && + (pragmaTokens[1] == "multi_compile" || + pragmaTokens[1] == "shader_feature" || + pragmaTokens[1] == "shader_feature_local")) { + continue; + } + + return fail("unsupported pragma in Unity-style single-source shader", humanLine); + } + + if (line == "{") { + switch (pendingBlock) { + case BlockKind::Shader: + blockStack.push_back(BlockKind::Shader); + break; + case BlockKind::Properties: + blockStack.push_back(BlockKind::Properties); + break; + case BlockKind::SubShader: + outDesc.subShaders.emplace_back(); + currentSubShader = &outDesc.subShaders.back(); + blockStack.push_back(BlockKind::SubShader); + break; + case BlockKind::Pass: + if (currentSubShader == nullptr) { + return fail("pass block must be inside a SubShader", humanLine); + } + currentSubShader->passes.emplace_back(); + currentPass = ¤tSubShader->passes.back(); + blockStack.push_back(BlockKind::Pass); + break; + case BlockKind::None: + default: + return fail("unexpected opening brace", humanLine); + } + + pendingBlock = BlockKind::None; + continue; + } + + if (line == "}") { + if (blockStack.empty()) { + return fail("unexpected closing brace", humanLine); + } + + const BlockKind closingBlock = blockStack.back(); + blockStack.pop_back(); + if (closingBlock == BlockKind::Pass) { + currentPass = nullptr; + } else if (closingBlock == BlockKind::SubShader) { + currentSubShader = nullptr; + } + continue; + } + + if (StartsWithKeyword(line, "Shader")) { + std::vector tokens; + if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() < 2u) { + return fail("Shader declaration is missing a name", humanLine); + } + outDesc.name = tokens[1].c_str(); + pendingBlock = BlockKind::Shader; + continue; + } + + if (line == "Properties") { + pendingBlock = BlockKind::Properties; + continue; + } + + if (StartsWithKeyword(line, "SubShader")) { + pendingBlock = BlockKind::SubShader; + continue; + } + + if (StartsWithKeyword(line, "Pass")) { + pendingBlock = BlockKind::Pass; + continue; + } + + if (line == "Resources" || StartsWithKeyword(line, "Resources")) { + return fail("Unity-style single-source shaders must not declare Resources blocks", humanLine); + } + + if (StartsWithKeyword(line, "Tags")) { + std::vector parsedTags; + if (!TryParseInlineTagAssignments(line, parsedTags)) { + return fail("Tags block must use inline key/value pairs", humanLine); + } + + if (currentPass != nullptr) { + currentPass->tags.insert(currentPass->tags.end(), parsedTags.begin(), parsedTags.end()); + } else if (currentSubShader != nullptr) { + currentSubShader->tags.insert(currentSubShader->tags.end(), parsedTags.begin(), parsedTags.end()); + } else { + return fail("Tags block is only supported inside SubShader or Pass", humanLine); + } + continue; + } + + if (currentBlock() == BlockKind::Properties) { + ShaderPropertyDesc property = {}; + if (!TryParseAuthoringPropertyLine(line, property)) { + return fail("invalid Properties entry", humanLine); + } + outDesc.properties.PushBack(property); + continue; + } + + if (line == "HLSLINCLUDE" || line == "CGINCLUDE") { + if (currentPass != nullptr) { + return fail("HLSLINCLUDE is not supported inside a Pass block", humanLine); + } + + Containers::String* destination = nullptr; + if (currentBlock() == BlockKind::Shader) { + destination = &outDesc.sharedProgramSource; + } else if (currentBlock() == BlockKind::SubShader && currentSubShader != nullptr) { + destination = ¤tSubShader->sharedProgramSource; + } else { + return fail( + "HLSLINCLUDE is only supported directly inside Shader or SubShader in the new authoring mode", + humanLine); + } + + inSharedIncludeBlock = true; + if (!consumeExtractedBlock( + ExtractedProgramBlock::Kind::SharedInclude, + *destination, + true, + humanLine)) { + return false; + } + continue; + } + + if (currentBlock() == BlockKind::SubShader && StartsWithKeyword(line, "LOD")) { + continue; + } + + if (currentBlock() == BlockKind::Pass && currentPass != nullptr) { + if (StartsWithKeyword(line, "Name")) { + std::vector tokens; + if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() < 2u) { + return fail("pass Name directive is missing a value", humanLine); + } + currentPass->name = tokens[1].c_str(); + continue; + } + + if (line == "HLSLPROGRAM" || line == "CGPROGRAM") { + inProgramBlock = true; + if (!consumeExtractedBlock( + ExtractedProgramBlock::Kind::PassProgram, + currentPass->programSource, + false, + humanLine)) { + return false; + } + continue; + } + } + + return fail("unsupported authoring statement: " + line, humanLine); + } + + if (inSharedIncludeBlock || inProgramBlock) { + return fail("program block was not closed", lines.size()); + } + if (!blockStack.empty()) { + return fail("one or more blocks were not closed", lines.size()); + } + if (outDesc.name.Empty()) { + return fail("shader name is missing", 0); + } + if (outDesc.subShaders.empty()) { + return fail("shader does not declare any SubShader blocks", 0); + } + + for (AuthoringSubShaderEntry& subShader : outDesc.subShaders) { + if (subShader.passes.empty()) { + continue; + } + + for (AuthoringPassEntry& pass : subShader.passes) { + if (pass.name.Empty()) { + return fail("a Pass is missing a Name directive", 0); + } + if (pass.programSource.Empty()) { + return fail("a Pass is missing an HLSLPROGRAM block", 0); + } + if (pass.vertexEntryPoint.Empty()) { + return fail("a Pass is missing a #pragma vertex directive", 0); + } + if (pass.fragmentEntryPoint.Empty()) { + return fail("a Pass is missing a #pragma fragment directive", 0); + } + } + } + + return true; +} + LoadResult BuildShaderFromAuthoringDesc( const Containers::String& path, const AuthoringShaderDesc& authoringDesc) { @@ -1391,43 +1933,82 @@ LoadResult BuildShaderFromAuthoringDesc( shader->AddPassResourceBinding(pass.name, resourceBinding); } - for (const AuthoringBackendVariantEntry& backendVariant : pass.backendVariants) { + if (!pass.backendVariants.empty()) { + for (const AuthoringBackendVariantEntry& backendVariant : pass.backendVariants) { + ShaderStageVariant vertexVariant = {}; + vertexVariant.stage = ShaderType::Vertex; + vertexVariant.backend = backendVariant.backend; + vertexVariant.language = backendVariant.language; + vertexVariant.entryPoint = + backendVariant.language == ShaderLanguage::HLSL && !pass.vertexEntryPoint.Empty() + ? pass.vertexEntryPoint + : GetDefaultEntryPoint(backendVariant.language, ShaderType::Vertex); + vertexVariant.profile = !backendVariant.vertexProfile.Empty() + ? backendVariant.vertexProfile + : GetDefaultProfile(backendVariant.language, backendVariant.backend, ShaderType::Vertex); + + const Containers::String resolvedVertexPath = + ResolveShaderDependencyPath(backendVariant.vertexSourcePath, path); + if (!ReadTextFile(resolvedVertexPath, vertexVariant.sourceCode)) { + return LoadResult("Failed to read shader authoring vertex source: " + resolvedVertexPath); + } + shader->AddPassVariant(pass.name, vertexVariant); + + ShaderStageVariant fragmentVariant = {}; + fragmentVariant.stage = ShaderType::Fragment; + fragmentVariant.backend = backendVariant.backend; + fragmentVariant.language = backendVariant.language; + fragmentVariant.entryPoint = + backendVariant.language == ShaderLanguage::HLSL && !pass.fragmentEntryPoint.Empty() + ? pass.fragmentEntryPoint + : GetDefaultEntryPoint(backendVariant.language, ShaderType::Fragment); + fragmentVariant.profile = !backendVariant.fragmentProfile.Empty() + ? backendVariant.fragmentProfile + : GetDefaultProfile(backendVariant.language, backendVariant.backend, ShaderType::Fragment); + + const Containers::String resolvedFragmentPath = + ResolveShaderDependencyPath(backendVariant.fragmentSourcePath, path); + if (!ReadTextFile(resolvedFragmentPath, fragmentVariant.sourceCode)) { + return LoadResult("Failed to read shader authoring fragment source: " + resolvedFragmentPath); + } + shader->AddPassVariant(pass.name, fragmentVariant); + } + } else if (!pass.programSource.Empty()) { + Containers::String combinedSource; + AppendAuthoringSourceBlock(combinedSource, authoringDesc.sharedProgramSource); + AppendAuthoringSourceBlock(combinedSource, subShader.sharedProgramSource); + AppendAuthoringSourceBlock(combinedSource, pass.sharedProgramSource); + AppendAuthoringSourceBlock(combinedSource, pass.programSource); + combinedSource = StripUnityStyleAuthoringPragmas(combinedSource); + ShaderStageVariant vertexVariant = {}; vertexVariant.stage = ShaderType::Vertex; - vertexVariant.backend = backendVariant.backend; - vertexVariant.language = backendVariant.language; + vertexVariant.backend = ShaderBackend::Generic; + vertexVariant.language = ShaderLanguage::HLSL; vertexVariant.entryPoint = - backendVariant.language == ShaderLanguage::HLSL && !pass.vertexEntryPoint.Empty() + !pass.vertexEntryPoint.Empty() ? pass.vertexEntryPoint - : GetDefaultEntryPoint(backendVariant.language, ShaderType::Vertex); - vertexVariant.profile = !backendVariant.vertexProfile.Empty() - ? backendVariant.vertexProfile - : GetDefaultProfile(backendVariant.language, backendVariant.backend, ShaderType::Vertex); - - const Containers::String resolvedVertexPath = - ResolveShaderDependencyPath(backendVariant.vertexSourcePath, path); - if (!ReadTextFile(resolvedVertexPath, vertexVariant.sourceCode)) { - return LoadResult("Failed to read shader authoring vertex source: " + resolvedVertexPath); - } + : GetDefaultEntryPoint(ShaderLanguage::HLSL, ShaderType::Vertex); + vertexVariant.profile = GetDefaultProfile( + ShaderLanguage::HLSL, + ShaderBackend::Generic, + ShaderType::Vertex); + vertexVariant.sourceCode = combinedSource; shader->AddPassVariant(pass.name, vertexVariant); ShaderStageVariant fragmentVariant = {}; fragmentVariant.stage = ShaderType::Fragment; - fragmentVariant.backend = backendVariant.backend; - fragmentVariant.language = backendVariant.language; + fragmentVariant.backend = ShaderBackend::Generic; + fragmentVariant.language = ShaderLanguage::HLSL; fragmentVariant.entryPoint = - backendVariant.language == ShaderLanguage::HLSL && !pass.fragmentEntryPoint.Empty() + !pass.fragmentEntryPoint.Empty() ? pass.fragmentEntryPoint - : GetDefaultEntryPoint(backendVariant.language, ShaderType::Fragment); - fragmentVariant.profile = !backendVariant.fragmentProfile.Empty() - ? backendVariant.fragmentProfile - : GetDefaultProfile(backendVariant.language, backendVariant.backend, ShaderType::Fragment); - - const Containers::String resolvedFragmentPath = - ResolveShaderDependencyPath(backendVariant.fragmentSourcePath, path); - if (!ReadTextFile(resolvedFragmentPath, fragmentVariant.sourceCode)) { - return LoadResult("Failed to read shader authoring fragment source: " + resolvedFragmentPath); - } + : GetDefaultEntryPoint(ShaderLanguage::HLSL, ShaderType::Fragment); + fragmentVariant.profile = GetDefaultProfile( + ShaderLanguage::HLSL, + ShaderBackend::Generic, + ShaderType::Fragment); + fragmentVariant.sourceCode = combinedSource; shader->AddPassVariant(pass.name, fragmentVariant); } } @@ -1437,13 +2018,11 @@ LoadResult BuildShaderFromAuthoringDesc( return LoadResult(shader.release()); } -bool LooksLikeUnityLikeShaderAuthoring(const std::string& sourceText) { - std::vector lines; - SplitShaderAuthoringLines(sourceText, lines); - return !lines.empty() && StartsWithKeyword(lines.front(), "Shader"); +bool LooksLikeShaderAuthoring(const std::string& sourceText) { + return DetectShaderAuthoringStyle(sourceText) != ShaderAuthoringStyle::NotShaderAuthoring; } -bool CollectUnityLikeShaderDependencyPaths( +bool CollectLegacyBackendSplitShaderDependencyPaths( const Containers::String& path, const std::string& sourceText, Containers::Array& outDependencies) { @@ -1451,7 +2030,7 @@ bool CollectUnityLikeShaderDependencyPaths( AuthoringShaderDesc authoringDesc = {}; Containers::String parseError; - if (!ParseUnityLikeShaderAuthoring(path, sourceText, authoringDesc, &parseError)) { + if (!ParseLegacyBackendSplitShaderAuthoring(path, sourceText, authoringDesc, &parseError)) { return false; } @@ -1479,10 +2058,49 @@ bool CollectUnityLikeShaderDependencyPaths( return true; } -LoadResult LoadUnityLikeShaderAuthoring(const Containers::String& path, const std::string& sourceText) { +bool CollectUnityStyleSingleSourceShaderDependencyPaths( + const Containers::String& path, + const std::string& sourceText, + Containers::Array& outDependencies) { + outDependencies.Clear(); + AuthoringShaderDesc authoringDesc = {}; Containers::String parseError; - if (!ParseUnityLikeShaderAuthoring(path, sourceText, authoringDesc, &parseError)) { + if (!ParseUnityStyleSingleSourceShaderAuthoring(path, sourceText, authoringDesc, &parseError)) { + return false; + } + + std::unordered_set seenPaths; + CollectQuotedIncludeDependencyPaths(path, authoringDesc.sharedProgramSource, seenPaths, outDependencies); + for (const AuthoringSubShaderEntry& subShader : authoringDesc.subShaders) { + CollectQuotedIncludeDependencyPaths(path, subShader.sharedProgramSource, seenPaths, outDependencies); + for (const AuthoringPassEntry& pass : subShader.passes) { + CollectQuotedIncludeDependencyPaths(path, pass.sharedProgramSource, seenPaths, outDependencies); + CollectQuotedIncludeDependencyPaths(path, pass.programSource, seenPaths, outDependencies); + } + } + + return true; +} + +LoadResult LoadLegacyBackendSplitShaderAuthoring( + const Containers::String& path, + const std::string& sourceText) { + AuthoringShaderDesc authoringDesc = {}; + Containers::String parseError; + if (!ParseLegacyBackendSplitShaderAuthoring(path, sourceText, authoringDesc, &parseError)) { + return LoadResult(parseError); + } + + return BuildShaderFromAuthoringDesc(path, authoringDesc); +} + +LoadResult LoadUnityStyleSingleSourceShaderAuthoring( + const Containers::String& path, + const std::string& sourceText) { + AuthoringShaderDesc authoringDesc = {}; + Containers::String parseError; + if (!ParseUnityStyleSingleSourceShaderAuthoring(path, sourceText, authoringDesc, &parseError)) { return LoadResult(parseError); } @@ -2021,8 +2639,16 @@ LoadResult ShaderLoader::Load(const Containers::String& path, const ImportSettin if (ext == "shader" && LooksLikeShaderManifest(sourceText)) { return LoadShaderManifest(path, sourceText); } - if (ext == "shader" && LooksLikeUnityLikeShaderAuthoring(sourceText)) { - return LoadUnityLikeShaderAuthoring(path, sourceText); + if (ext == "shader") { + switch (DetectShaderAuthoringStyle(sourceText)) { + case ShaderAuthoringStyle::LegacyBackendSplit: + return LoadLegacyBackendSplitShaderAuthoring(path, sourceText); + case ShaderAuthoringStyle::UnityStyleSingleSource: + return LoadUnityStyleSingleSourceShaderAuthoring(path, sourceText); + case ShaderAuthoringStyle::NotShaderAuthoring: + default: + break; + } } return LoadLegacySingleStageShader(path, sourceText); @@ -2052,9 +2678,15 @@ bool ShaderLoader::CollectSourceDependencies(const Containers::String& path, const std::string sourceText = ToStdString(data); if (!LooksLikeShaderManifest(sourceText)) { - return LooksLikeUnityLikeShaderAuthoring(sourceText) - ? CollectUnityLikeShaderDependencyPaths(path, sourceText, outDependencies) - : true; + switch (DetectShaderAuthoringStyle(sourceText)) { + case ShaderAuthoringStyle::LegacyBackendSplit: + return CollectLegacyBackendSplitShaderDependencyPaths(path, sourceText, outDependencies); + case ShaderAuthoringStyle::UnityStyleSingleSource: + return CollectUnityStyleSingleSourceShaderDependencyPaths(path, sourceText, outDependencies); + case ShaderAuthoringStyle::NotShaderAuthoring: + default: + return true; + } } return CollectShaderManifestDependencyPaths(path, sourceText, outDependencies); diff --git a/tests/Resources/Shader/test_shader_loader.cpp b/tests/Resources/Shader/test_shader_loader.cpp index 5232bbed..134d5504 100644 --- a/tests/Resources/Shader/test_shader_loader.cpp +++ b/tests/Resources/Shader/test_shader_loader.cpp @@ -237,7 +237,7 @@ TEST(ShaderLoader, LoadShaderManifestBuildsMultiPassBackendVariants) { fs::remove_all(shaderRoot); } -TEST(ShaderLoader, LoadUnityLikeShaderAuthoringBuildsRuntimeContract) { +TEST(ShaderLoader, LoadLegacyBackendSplitShaderAuthoringBuildsRuntimeContract) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_test"; @@ -382,6 +382,196 @@ TEST(ShaderLoader, LoadUnityLikeShaderAuthoringBuildsRuntimeContract) { fs::remove_all(shaderRoot); } +TEST(ShaderLoader, LoadUnityStyleSingleSourceShaderAuthoringBuildsGenericHlslVariants) { + namespace fs = std::filesystem; + + const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_single_source_test"; + const fs::path includeRoot = shaderRoot / "shaderlib"; + const fs::path shaderPath = shaderRoot / "single_source.shader"; + + fs::remove_all(shaderRoot); + fs::create_directories(includeRoot); + + WriteTextFile( + includeRoot / "shared.hlsl", + R"(// XC_SINGLE_SOURCE_SHARED_INCLUDE +#define XC_SINGLE_SOURCE_SHARED_VALUE 1 +)"); + + WriteTextFile( + shaderPath, + R"(Shader "SingleSourceLit" +{ + Properties + { + _BaseColor ("Base Color", Color) = (1,1,1,1) [Semantic(BaseColor)] + } + HLSLINCLUDE + #include "shaderlib/shared.hlsl" + struct VSInput + { + float3 positionOS : POSITION; + }; + ENDHLSL + SubShader + { + Tags { "Queue" = "Geometry" } + LOD 200 + Pass + { + Name "ForwardLit" + Tags { "LightMode" = "ForwardLit" } + HLSLPROGRAM + #pragma target 4.5 + #pragma vertex Vert + #pragma fragment Frag + #pragma multi_compile _ XC_MAIN_LIGHT_SHADOWS + #pragma shader_feature_local _ XC_ALPHA_TEST + float4 Vert(VSInput input) : SV_POSITION + { + return float4(input.positionOS, 1.0); + } + float4 Frag() : SV_TARGET + { + return float4(1.0, 0.0, 0.0, 1.0); // XC_SINGLE_SOURCE_FRAG_BODY + } + ENDHLSL + } + } +} +)"); + + ShaderLoader loader; + LoadResult result = loader.Load(shaderPath.string().c_str()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + Shader* shader = static_cast(result.resource); + ASSERT_NE(shader, nullptr); + ASSERT_TRUE(shader->IsValid()); + EXPECT_EQ(shader->GetName(), "SingleSourceLit"); + ASSERT_EQ(shader->GetProperties().Size(), 1u); + ASSERT_EQ(shader->GetPassCount(), 1u); + + const ShaderPass* pass = shader->FindPass("ForwardLit"); + ASSERT_NE(pass, nullptr); + ASSERT_EQ(pass->tags.Size(), 2u); + EXPECT_EQ(pass->tags[0].name, "Queue"); + EXPECT_EQ(pass->tags[0].value, "Geometry"); + EXPECT_EQ(pass->tags[1].name, "LightMode"); + EXPECT_EQ(pass->tags[1].value, "ForwardLit"); + EXPECT_TRUE(pass->resources.Empty()); + ASSERT_EQ(pass->variants.Size(), 2u); + + const ShaderStageVariant* vertexVariant = + shader->FindVariant("ForwardLit", ShaderType::Vertex, ShaderBackend::D3D12); + ASSERT_NE(vertexVariant, nullptr); + EXPECT_EQ(vertexVariant->backend, ShaderBackend::Generic); + EXPECT_EQ(vertexVariant->language, ShaderLanguage::HLSL); + EXPECT_EQ(vertexVariant->entryPoint, "Vert"); + EXPECT_EQ(vertexVariant->profile, "vs_5_0"); + EXPECT_NE(std::string(vertexVariant->sourceCode.CStr()).find("#include \"shaderlib/shared.hlsl\""), std::string::npos); + EXPECT_NE(std::string(vertexVariant->sourceCode.CStr()).find("XC_SINGLE_SOURCE_FRAG_BODY"), std::string::npos); + + const ShaderStageVariant* fragmentVariant = + shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::D3D12); + ASSERT_NE(fragmentVariant, nullptr); + EXPECT_EQ(fragmentVariant->backend, ShaderBackend::Generic); + EXPECT_EQ(fragmentVariant->language, ShaderLanguage::HLSL); + EXPECT_EQ(fragmentVariant->entryPoint, "Frag"); + EXPECT_EQ(fragmentVariant->profile, "ps_5_0"); + + Array dependencies; + ASSERT_TRUE(loader.CollectSourceDependencies(shaderPath.string().c_str(), dependencies)); + ASSERT_EQ(dependencies.Size(), 1u); + EXPECT_EQ( + fs::path(dependencies[0].CStr()).lexically_normal(), + (includeRoot / "shared.hlsl").lexically_normal()); + + delete shader; + fs::remove_all(shaderRoot); +} + +TEST(ShaderLoader, LoadUnityStyleSingleSourceShaderAuthoringRejectsBackendPragma) { + namespace fs = std::filesystem; + + const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_single_source_reject_backend"; + const fs::path shaderPath = shaderRoot / "invalid_backend.shader"; + + fs::remove_all(shaderRoot); + fs::create_directories(shaderRoot); + + WriteTextFile( + shaderPath, + R"(Shader "InvalidBackend" +{ + SubShader + { + Pass + { + Name "ForwardLit" + HLSLPROGRAM + #pragma target 4.5 + #pragma vertex Vert + #pragma fragment Frag + #pragma backend D3D12 HLSL "forward.vs.hlsl" "forward.ps.hlsl" + float4 Vert() : SV_POSITION { return 0; } + float4 Frag() : SV_TARGET { return 1; } + ENDHLSL + } + } +} +)"); + + ShaderLoader loader; + LoadResult result = loader.Load(shaderPath.string().c_str()); + EXPECT_FALSE(result); + EXPECT_NE(std::string(result.errorMessage.CStr()).find("must not use #pragma backend"), std::string::npos); + + fs::remove_all(shaderRoot); +} + +TEST(ShaderLoader, LoadUnityStyleSingleSourceShaderAuthoringRejectsResourcesBlock) { + namespace fs = std::filesystem; + + const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_single_source_reject_resources"; + const fs::path shaderPath = shaderRoot / "invalid_resources.shader"; + + fs::remove_all(shaderRoot); + fs::create_directories(shaderRoot); + + WriteTextFile( + shaderPath, + R"(Shader "InvalidResources" +{ + SubShader + { + Pass + { + Name "ForwardLit" + Resources + { + MaterialConstants (ConstantBuffer, 0, 0) + } + HLSLPROGRAM + #pragma vertex Vert + #pragma fragment Frag + float4 Vert() : SV_POSITION { return 0; } + float4 Frag() : SV_TARGET { return 1; } + ENDHLSL + } + } +} +)"); + + ShaderLoader loader; + LoadResult result = loader.Load(shaderPath.string().c_str()); + EXPECT_FALSE(result); + EXPECT_NE(std::string(result.errorMessage.CStr()).find("must not declare Resources blocks"), std::string::npos); + + fs::remove_all(shaderRoot); +} + TEST(ShaderLoader, ResourceManagerLoadsShaderManifestRelativeToResourceRoot) { namespace fs = std::filesystem;