From 5d57101da3f3ceb0cab40ff8a6177e3458b47a66 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 7 Apr 2026 11:06:05 +0800 Subject: [PATCH] resources: extract shader authoring parser --- engine/CMakeLists.txt | 2 + .../Shader/ShaderAuthoringParser.cpp | 2108 +++++++++++++++++ .../Resources/Shader/ShaderAuthoringParser.h | 99 + engine/src/Resources/Shader/ShaderLoader.cpp | 1956 +-------------- 4 files changed, 2210 insertions(+), 1955 deletions(-) create mode 100644 engine/src/Resources/Shader/ShaderAuthoringParser.cpp create mode 100644 engine/src/Resources/Shader/ShaderAuthoringParser.h diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 1408c24c..9b345d2a 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -387,6 +387,8 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Material/MaterialLoader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/Shader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/ShaderLoader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/ShaderAuthoringParser.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/ShaderAuthoringParser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioClip/AudioClip.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioClip/AudioLoader.cpp ${XCENGINE_OPTIONAL_UI_RESOURCE_SOURCES} diff --git a/engine/src/Resources/Shader/ShaderAuthoringParser.cpp b/engine/src/Resources/Shader/ShaderAuthoringParser.cpp new file mode 100644 index 00000000..10fe881d --- /dev/null +++ b/engine/src/Resources/Shader/ShaderAuthoringParser.cpp @@ -0,0 +1,2108 @@ +#include "ShaderAuthoringParser.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace XCEngine { +namespace Resources { + +std::string ToStdString(const Containers::String& value) { + return std::string(value.CStr()); +} + +Containers::String NormalizePathString(const std::filesystem::path& path) { + return Containers::String(path.lexically_normal().generic_string().c_str()); +} + +size_t SkipWhitespace(const std::string& text, size_t pos) { + while (pos < text.size() && std::isspace(static_cast(text[pos])) != 0) { + ++pos; + } + return pos; +} + +std::string TrimCopy(const std::string& text) { + const size_t first = SkipWhitespace(text, 0); + if (first >= text.size()) { + return std::string(); + } + + size_t last = text.size(); + while (last > first && std::isspace(static_cast(text[last - 1])) != 0) { + --last; + } + + return text.substr(first, last - first); +} + +bool ParseQuotedString( + const std::string& text, + size_t quotePos, + Containers::String& outValue, + size_t* nextPos = nullptr) { + if (quotePos >= text.size() || text[quotePos] != '"') { + return false; + } + + std::string parsed; + ++quotePos; + + while (quotePos < text.size()) { + const char ch = text[quotePos]; + if (ch == '\\') { + if (quotePos + 1 >= text.size()) { + return false; + } + + parsed.push_back(text[quotePos + 1]); + quotePos += 2; + continue; + } + + if (ch == '"') { + outValue = parsed.c_str(); + if (nextPos != nullptr) { + *nextPos = quotePos + 1; + } + return true; + } + + parsed.push_back(ch); + ++quotePos; + } + + return false; +} + +bool TryParseShaderLanguage(const Containers::String& value, ShaderLanguage& outLanguage) { + const Containers::String normalized = value.Trim().ToLower(); + if (normalized == "glsl") { + outLanguage = ShaderLanguage::GLSL; + return true; + } + if (normalized == "hlsl") { + outLanguage = ShaderLanguage::HLSL; + return true; + } + if (normalized == "spirv" || normalized == "spv") { + outLanguage = ShaderLanguage::SPIRV; + return true; + } + + return false; +} + +bool TryParseShaderBackend(const Containers::String& value, ShaderBackend& outBackend) { + const Containers::String normalized = value.Trim().ToLower(); + if (normalized == "generic") { + outBackend = ShaderBackend::Generic; + return true; + } + if (normalized == "d3d12" || normalized == "dx12") { + outBackend = ShaderBackend::D3D12; + return true; + } + if (normalized == "opengl" || normalized == "gl") { + outBackend = ShaderBackend::OpenGL; + return true; + } + if (normalized == "vulkan" || normalized == "vk") { + outBackend = ShaderBackend::Vulkan; + return true; + } + + return false; +} + +bool TryParseShaderPropertyType(const Containers::String& value, ShaderPropertyType& outType) { + const Containers::String normalized = value.Trim().ToLower(); + if (normalized == "float") { + outType = ShaderPropertyType::Float; + return true; + } + if (normalized == "range") { + outType = ShaderPropertyType::Range; + return true; + } + if (normalized == "int" || normalized == "integer") { + outType = ShaderPropertyType::Int; + return true; + } + if (normalized == "vector" || normalized == "float4") { + outType = ShaderPropertyType::Vector; + return true; + } + if (normalized == "color") { + outType = ShaderPropertyType::Color; + return true; + } + if (normalized == "2d" || normalized == "texture2d" || normalized == "texture") { + outType = ShaderPropertyType::Texture2D; + return true; + } + if (normalized == "cube" || normalized == "cubemap" || normalized == "texturecube") { + outType = ShaderPropertyType::TextureCube; + return true; + } + + return false; +} + +bool TryParseShaderResourceType(const Containers::String& value, ShaderResourceType& outType) { + const Containers::String normalized = value.Trim().ToLower(); + if (normalized == "constantbuffer" || normalized == "cbuffer" || normalized == "cbv") { + outType = ShaderResourceType::ConstantBuffer; + return true; + } + if (normalized == "texture2d" || normalized == "texture" || normalized == "srvtexture2d") { + outType = ShaderResourceType::Texture2D; + return true; + } + if (normalized == "texturecube" || normalized == "cubemap") { + outType = ShaderResourceType::TextureCube; + return true; + } + if (normalized == "sampler" || normalized == "samplerstate") { + outType = ShaderResourceType::Sampler; + return true; + } + + return false; +} + +Containers::String ResolveShaderDependencyPath( + const Containers::String& dependencyPath, + const Containers::String& sourcePath) { + if (dependencyPath.Empty()) { + return dependencyPath; + } + + const std::filesystem::path dependencyFsPath(dependencyPath.CStr()); + if (dependencyFsPath.is_absolute()) { + return NormalizePathString(dependencyFsPath); + } + + const std::filesystem::path sourceFsPath(sourcePath.CStr()); + if (sourceFsPath.is_absolute()) { + return NormalizePathString(sourceFsPath.parent_path() / dependencyFsPath); + } + + const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot(); + if (!resourceRoot.Empty()) { + return NormalizePathString( + std::filesystem::path(resourceRoot.CStr()) / + sourceFsPath.parent_path() / + dependencyFsPath); + } + + return NormalizePathString(sourceFsPath.parent_path() / dependencyFsPath); +} + +struct ExtractedProgramBlock { + enum class Kind { + SharedInclude, + PassProgram + }; + + Kind kind = Kind::PassProgram; + Containers::String sourceText; + size_t markerLine = 0; +}; + +bool TryTokenizeQuotedArguments(const std::string& line, std::vector& outTokens); + +std::string StripAuthoringLineComment(const std::string& line) { + bool inString = false; + bool escaped = false; + for (size_t index = 0; index + 1 < line.size(); ++index) { + const char ch = line[index]; + if (escaped) { + escaped = false; + continue; + } + + if (ch == '\\') { + escaped = true; + continue; + } + + if (ch == '"') { + inString = !inString; + continue; + } + + if (!inString && ch == '/' && line[index + 1] == '/') { + return line.substr(0, index); + } + } + + return line; +} + +bool StartsWithKeyword(const std::string& line, const char* keyword) { + const std::string keywordString(keyword); + if (line.size() < keywordString.size() || + line.compare(0, keywordString.size(), keywordString) != 0) { + return false; + } + + return line.size() == keywordString.size() || + std::isspace(static_cast(line[keywordString.size()])) != 0; +} + +void SplitShaderAuthoringLines( + const std::string& sourceText, + std::vector& outLines) { + outLines.clear(); + + std::istringstream stream(sourceText); + std::string rawLine; + while (std::getline(stream, rawLine)) { + std::string line = TrimCopy(StripAuthoringLineComment(rawLine)); + if (line.empty()) { + continue; + } + + if (!StartsWithKeyword(line, "Tags") && + line.size() > 1 && + line.back() == '{') { + line.pop_back(); + const std::string prefix = TrimCopy(line); + if (!prefix.empty()) { + outLines.push_back(prefix); + } + outLines.push_back("{"); + continue; + } + + if (line.size() > 1 && line.front() == '}') { + outLines.push_back("}"); + line = TrimCopy(line.substr(1)); + if (!line.empty()) { + outLines.push_back(line); + } + continue; + } + + outLines.push_back(line); + } +} + +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) { + return true; + } + } + + return false; +} + +MaterialRenderState BuildUnityDefaultFixedFunctionState() { + MaterialRenderState state = {}; + state.blendEnable = false; + state.srcBlend = MaterialBlendFactor::One; + state.dstBlend = MaterialBlendFactor::Zero; + state.srcBlendAlpha = MaterialBlendFactor::One; + state.dstBlendAlpha = MaterialBlendFactor::Zero; + state.blendOp = MaterialBlendOp::Add; + state.blendOpAlpha = MaterialBlendOp::Add; + state.colorWriteMask = 0xF; + state.depthTestEnable = true; + state.depthWriteEnable = true; + state.depthFunc = MaterialComparisonFunc::LessEqual; + state.cullMode = MaterialCullMode::Back; + return state; +} + +void EnsureAuthoringFixedFunctionStateInitialized( + bool& hasFixedFunctionState, + MaterialRenderState& fixedFunctionState) { + if (!hasFixedFunctionState) { + hasFixedFunctionState = true; + fixedFunctionState = BuildUnityDefaultFixedFunctionState(); + } +} + +bool TryParseUnityStyleBoolDirectiveToken(const std::string& token, bool& outValue) { + const Containers::String normalized = Containers::String(token.c_str()).Trim().ToLower(); + if (normalized == "on") { + outValue = true; + return true; + } + if (normalized == "off") { + outValue = false; + return true; + } + return false; +} + +bool TryParseUnityStyleCullMode(const std::string& token, MaterialCullMode& outMode) { + const Containers::String normalized = Containers::String(token.c_str()).Trim().ToLower(); + if (normalized == "back") { + outMode = MaterialCullMode::Back; + return true; + } + if (normalized == "front") { + outMode = MaterialCullMode::Front; + return true; + } + if (normalized == "off") { + outMode = MaterialCullMode::None; + return true; + } + return false; +} + +bool TryParseUnityStyleComparisonFunc(const std::string& token, MaterialComparisonFunc& outFunc) { + const Containers::String normalized = Containers::String(token.c_str()).Trim().ToLower(); + if (normalized == "never") { + outFunc = MaterialComparisonFunc::Never; + return true; + } + if (normalized == "less") { + outFunc = MaterialComparisonFunc::Less; + return true; + } + if (normalized == "equal") { + outFunc = MaterialComparisonFunc::Equal; + return true; + } + if (normalized == "lequal" || normalized == "lessequal" || normalized == "less_equal") { + outFunc = MaterialComparisonFunc::LessEqual; + return true; + } + if (normalized == "greater") { + outFunc = MaterialComparisonFunc::Greater; + return true; + } + if (normalized == "notequal" || normalized == "not_equal") { + outFunc = MaterialComparisonFunc::NotEqual; + return true; + } + if (normalized == "gequal" || normalized == "greaterequal" || normalized == "greater_equal") { + outFunc = MaterialComparisonFunc::GreaterEqual; + return true; + } + if (normalized == "always") { + outFunc = MaterialComparisonFunc::Always; + return true; + } + return false; +} + +bool TryParseUnityStyleBlendFactor(const std::string& token, MaterialBlendFactor& outFactor) { + const Containers::String normalized = Containers::String(token.c_str()).Trim().ToLower(); + if (normalized == "zero") { + outFactor = MaterialBlendFactor::Zero; + return true; + } + if (normalized == "one") { + outFactor = MaterialBlendFactor::One; + return true; + } + if (normalized == "srccolor" || normalized == "src_color") { + outFactor = MaterialBlendFactor::SrcColor; + return true; + } + if (normalized == "oneminussrccolor" || normalized == "one_minus_src_color" || normalized == "invsrccolor") { + outFactor = MaterialBlendFactor::InvSrcColor; + return true; + } + if (normalized == "srcalpha" || normalized == "src_alpha") { + outFactor = MaterialBlendFactor::SrcAlpha; + return true; + } + if (normalized == "oneminussrcalpha" || normalized == "one_minus_src_alpha" || normalized == "invsrcalpha") { + outFactor = MaterialBlendFactor::InvSrcAlpha; + return true; + } + if (normalized == "dstalpha" || normalized == "dst_alpha") { + outFactor = MaterialBlendFactor::DstAlpha; + return true; + } + if (normalized == "oneminusdstalpha" || normalized == "one_minus_dst_alpha" || normalized == "invdstalpha") { + outFactor = MaterialBlendFactor::InvDstAlpha; + return true; + } + if (normalized == "dstcolor" || normalized == "dst_color") { + outFactor = MaterialBlendFactor::DstColor; + return true; + } + if (normalized == "oneminusdstcolor" || normalized == "one_minus_dst_color" || normalized == "invdstcolor") { + outFactor = MaterialBlendFactor::InvDstColor; + return true; + } + if (normalized == "srcalphasaturate" || normalized == "src_alpha_saturate" || normalized == "srcalphasat") { + outFactor = MaterialBlendFactor::SrcAlphaSat; + return true; + } + return false; +} + +bool TryParseUnityStyleColorMask(const std::string& token, Core::uint8& outMask) { + const Containers::String normalized = Containers::String(token.c_str()).Trim().ToUpper(); + if (normalized == "0") { + outMask = 0u; + return true; + } + + Core::uint8 mask = 0u; + for (size_t index = 0; index < normalized.Length(); ++index) { + switch (normalized[index]) { + case 'R': + mask |= 0x1u; + break; + case 'G': + mask |= 0x2u; + break; + case 'B': + mask |= 0x4u; + break; + case 'A': + mask |= 0x8u; + break; + default: + return false; + } + } + + outMask = mask; + return true; +} + +bool TryParseUnityStyleBlendDirective( + const std::vector& tokens, + MaterialRenderState& outState) { + std::vector normalizedTokens; + normalizedTokens.reserve(tokens.size()); + for (const std::string& token : tokens) { + if (token == ",") { + continue; + } + + std::string normalizedToken = token; + while (!normalizedToken.empty() && normalizedToken.back() == ',') { + normalizedToken.pop_back(); + } + if (!normalizedToken.empty()) { + normalizedTokens.push_back(std::move(normalizedToken)); + } + } + + if (normalizedTokens.size() != 2u && + normalizedTokens.size() != 3u && + normalizedTokens.size() != 5u) { + return false; + } + + if (normalizedTokens.size() == 2u) { + bool enabled = false; + if (!TryParseUnityStyleBoolDirectiveToken(normalizedTokens[1], enabled)) { + return false; + } + + outState.blendEnable = enabled; + if (!enabled) { + outState.srcBlend = MaterialBlendFactor::One; + outState.dstBlend = MaterialBlendFactor::Zero; + outState.srcBlendAlpha = MaterialBlendFactor::One; + outState.dstBlendAlpha = MaterialBlendFactor::Zero; + } + return true; + } + + MaterialBlendFactor srcBlend = MaterialBlendFactor::One; + MaterialBlendFactor dstBlend = MaterialBlendFactor::Zero; + if (!TryParseUnityStyleBlendFactor(normalizedTokens[1], srcBlend) || + !TryParseUnityStyleBlendFactor(normalizedTokens[2], dstBlend)) { + return false; + } + + outState.blendEnable = true; + outState.srcBlend = srcBlend; + outState.dstBlend = dstBlend; + + if (normalizedTokens.size() == 5u) { + if (!TryParseUnityStyleBlendFactor(normalizedTokens[3], outState.srcBlendAlpha) || + !TryParseUnityStyleBlendFactor(normalizedTokens[4], outState.dstBlendAlpha)) { + return false; + } + } else { + outState.srcBlendAlpha = srcBlend; + outState.dstBlendAlpha = dstBlend; + } + + return true; +} + +void SetOrReplaceAuthoringTag( + std::vector& tags, + const Containers::String& name, + const Containers::String& value) { + for (AuthoringTagEntry& tag : tags) { + if (tag.name == name) { + tag.value = value; + return; + } + } + + tags.push_back({ name, value }); +} + +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] == "multi_compile_local" || + 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(); +} + +Containers::String BuildShaderKeywordSetSignature(const ShaderKeywordSet& keywordSet) { + ShaderKeywordSet normalizedKeywords = keywordSet; + 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; +} + +std::vector BuildShaderKeywordVariantSets( + const Containers::Array& declarations) { + std::vector keywordSets(1); + + for (const ShaderKeywordDeclaration& declaration : declarations) { + if (declaration.options.Empty()) { + continue; + } + + std::vector nextKeywordSets; + std::unordered_set seenSignatures; + nextKeywordSets.reserve(keywordSets.size() * declaration.options.Size()); + + for (const ShaderKeywordSet& currentKeywordSet : keywordSets) { + for (const Containers::String& option : declaration.options) { + ShaderKeywordSet nextKeywordSet = currentKeywordSet; + const Containers::String normalizedKeyword = NormalizeShaderKeywordToken(option); + if (!normalizedKeyword.Empty()) { + nextKeywordSet.enabledKeywords.PushBack(normalizedKeyword); + } + + NormalizeShaderKeywordSetInPlace(nextKeywordSet); + const std::string signature = + ToStdString(BuildShaderKeywordSetSignature(nextKeywordSet)); + if (seenSignatures.insert(signature).second) { + nextKeywordSets.push_back(std::move(nextKeywordSet)); + } + } + } + + if (!nextKeywordSets.empty()) { + keywordSets = std::move(nextKeywordSets); + } + } + + if (keywordSets.empty()) { + keywordSets.emplace_back(); + } + + return keywordSets; +} + +Containers::String BuildKeywordVariantSource( + const Containers::String& baseSource, + const ShaderKeywordSet& requiredKeywords) { + if (requiredKeywords.enabledKeywords.Empty()) { + return baseSource; + } + + std::string defineBlock; + for (const Containers::String& keyword : requiredKeywords.enabledKeywords) { + defineBlock += "#define "; + defineBlock += ToStdString(keyword); + defineBlock += " 1\n"; + } + + if (baseSource.Empty()) { + return Containers::String(defineBlock.c_str()); + } + + const std::string sourceText = ToStdString(baseSource); + + size_t lineStart = 0; + while (lineStart < sourceText.size()) { + const size_t lineEnd = sourceText.find_first_of("\r\n", lineStart); + const size_t lineLength = + lineEnd == std::string::npos ? sourceText.size() - lineStart : lineEnd - lineStart; + const std::string line = sourceText.substr(lineStart, lineLength); + const std::string trimmedLine = TrimCopy(line); + + if (trimmedLine.empty() || + trimmedLine.rfind("//", 0) == 0u || + trimmedLine.rfind("/*", 0) == 0u || + trimmedLine.rfind("*", 0) == 0u) { + if (lineEnd == std::string::npos) { + break; + } + + lineStart = lineEnd + 1u; + if (sourceText[lineEnd] == '\r' && + lineStart < sourceText.size() && + sourceText[lineStart] == '\n') { + ++lineStart; + } + continue; + } + + if (trimmedLine.rfind("#version", 0) != 0u) { + break; + } + + size_t insertionPos = lineEnd == std::string::npos ? sourceText.size() : lineEnd + 1u; + if (lineEnd != std::string::npos && + sourceText[lineEnd] == '\r' && + insertionPos < sourceText.size() && + sourceText[insertionPos] == '\n') { + ++insertionPos; + } + + std::string variantSource = sourceText.substr(0, insertionPos); + variantSource += defineBlock; + variantSource += sourceText.substr(insertionPos); + return Containers::String(variantSource.c_str()); + } + + std::string variantSource = defineBlock; + variantSource += '\n'; + variantSource += sourceText; + return Containers::String(variantSource.c_str()); +} + +bool TryParseShaderKeywordDeclarationPragma( + const std::vector& pragmaTokens, + ShaderKeywordDeclaration& outDeclaration) { + outDeclaration = {}; + if (pragmaTokens.size() < 3u) { + return false; + } + + if (pragmaTokens[1] == "multi_compile") { + outDeclaration.type = ShaderKeywordDeclarationType::MultiCompile; + } else if (pragmaTokens[1] == "multi_compile_local") { + outDeclaration.type = ShaderKeywordDeclarationType::MultiCompileLocal; + } else if (pragmaTokens[1] == "shader_feature") { + outDeclaration.type = ShaderKeywordDeclarationType::ShaderFeature; + } else if (pragmaTokens[1] == "shader_feature_local") { + outDeclaration.type = ShaderKeywordDeclarationType::ShaderFeatureLocal; + } else { + return false; + } + + for (size_t tokenIndex = 2; tokenIndex < pragmaTokens.size(); ++tokenIndex) { + outDeclaration.options.PushBack(pragmaTokens[tokenIndex].c_str()); + } + + return !outDeclaration.options.Empty(); +} + +size_t FindMatchingDelimiter( + const std::string& text, + size_t openPos, + char openChar, + char closeChar) { + if (openPos >= text.size() || text[openPos] != openChar) { + return std::string::npos; + } + + bool inString = false; + bool escaped = false; + int depth = 0; + for (size_t pos = openPos; pos < text.size(); ++pos) { + const char ch = text[pos]; + if (escaped) { + escaped = false; + continue; + } + + if (ch == '\\') { + escaped = true; + continue; + } + + if (ch == '"') { + inString = !inString; + continue; + } + + if (inString) { + continue; + } + + if (ch == openChar) { + ++depth; + } else if (ch == closeChar) { + --depth; + if (depth == 0) { + return pos; + } + } + } + + return std::string::npos; +} + +size_t FindFirstTopLevelChar(const std::string& text, char target) { + bool inString = false; + bool escaped = false; + int roundDepth = 0; + int squareDepth = 0; + + for (size_t pos = 0; pos < text.size(); ++pos) { + const char ch = text[pos]; + if (escaped) { + escaped = false; + continue; + } + + if (ch == '\\') { + escaped = true; + continue; + } + + if (ch == '"') { + inString = !inString; + continue; + } + + if (inString) { + continue; + } + + if (ch == target && roundDepth == 0 && squareDepth == 0) { + return pos; + } + + if (ch == '(') { + ++roundDepth; + continue; + } + if (ch == ')') { + --roundDepth; + continue; + } + if (ch == '[') { + ++squareDepth; + continue; + } + if (ch == ']') { + --squareDepth; + continue; + } + + } + + return std::string::npos; +} + +bool SplitCommaSeparatedAuthoring(const std::string& text, std::vector& outParts) { + outParts.clear(); + + bool inString = false; + bool escaped = false; + int roundDepth = 0; + int squareDepth = 0; + size_t partStart = 0; + + for (size_t pos = 0; pos < text.size(); ++pos) { + const char ch = text[pos]; + if (escaped) { + escaped = false; + continue; + } + + if (ch == '\\') { + escaped = true; + continue; + } + + if (ch == '"') { + inString = !inString; + continue; + } + + if (inString) { + continue; + } + + if (ch == '(') { + ++roundDepth; + continue; + } + if (ch == ')') { + --roundDepth; + continue; + } + if (ch == '[') { + ++squareDepth; + continue; + } + if (ch == ']') { + --squareDepth; + continue; + } + + if (ch == ',' && roundDepth == 0 && squareDepth == 0) { + outParts.push_back(TrimCopy(text.substr(partStart, pos - partStart))); + partStart = pos + 1; + } + } + + outParts.push_back(TrimCopy(text.substr(partStart))); + return true; +} + +Containers::String UnquoteAuthoringValue(const std::string& text) { + const std::string trimmed = TrimCopy(text); + if (trimmed.size() >= 2 && + trimmed.front() == '"' && + trimmed.back() == '"') { + Containers::String parsed; + if (ParseQuotedString(trimmed, 0, parsed)) { + return parsed; + } + } + + return Containers::String(trimmed.c_str()); +} + +bool TryTokenizeQuotedArguments(const std::string& line, std::vector& outTokens) { + outTokens.clear(); + + std::string current; + bool inString = false; + bool escaped = false; + + for (size_t pos = 0; pos < line.size(); ++pos) { + const char ch = line[pos]; + if (escaped) { + current.push_back(ch); + escaped = false; + continue; + } + + if (ch == '\\' && inString) { + escaped = true; + continue; + } + + if (ch == '"') { + inString = !inString; + continue; + } + + if (!inString && std::isspace(static_cast(ch)) != 0) { + if (!current.empty()) { + outTokens.push_back(current); + current.clear(); + } + continue; + } + + current.push_back(ch); + } + + if (inString) { + return false; + } + + if (!current.empty()) { + outTokens.push_back(current); + } + + return !outTokens.empty(); +} + +bool TryParseInlineTagAssignments( + const std::string& line, + std::vector& outTags) { + const size_t openBrace = line.find('{'); + const size_t closeBrace = line.rfind('}'); + if (openBrace == std::string::npos || + closeBrace == std::string::npos || + closeBrace <= openBrace) { + return false; + } + + const std::string content = line.substr(openBrace + 1, closeBrace - openBrace - 1); + size_t pos = 0; + while (pos < content.size()) { + pos = SkipWhitespace(content, pos); + while (pos < content.size() && content[pos] == ',') { + ++pos; + pos = SkipWhitespace(content, pos); + } + if (pos >= content.size()) { + break; + } + + Containers::String key; + if (!ParseQuotedString(content, pos, key, &pos)) { + return false; + } + + pos = SkipWhitespace(content, pos); + if (pos >= content.size() || content[pos] != '=') { + return false; + } + + pos = SkipWhitespace(content, pos + 1); + Containers::String value; + if (!ParseQuotedString(content, pos, value, &pos)) { + return false; + } + + outTags.push_back({ key, value }); + } + + return true; +} + +bool TryParseSemanticAttributes( + const std::string& attributesText, + Containers::String& outSemantic) { + outSemantic.Clear(); + + size_t pos = 0; + while (pos < attributesText.size()) { + pos = attributesText.find('[', pos); + if (pos == std::string::npos) { + break; + } + + const size_t closePos = FindMatchingDelimiter(attributesText, pos, '[', ']'); + if (closePos == std::string::npos) { + return false; + } + + const std::string attributeBody = TrimCopy(attributesText.substr(pos + 1, closePos - pos - 1)); + if (attributeBody.size() > 10 && + attributeBody.compare(0, 9, "Semantic(") == 0 && + attributeBody.back() == ')') { + outSemantic = UnquoteAuthoringValue(attributeBody.substr(9, attributeBody.size() - 10)); + } + + pos = closePos + 1; + } + + return true; +} + +bool TryParseAuthoringPropertyLine( + const std::string& line, + ShaderPropertyDesc& outProperty) { + outProperty = {}; + + const size_t openParen = line.find('('); + if (openParen == std::string::npos) { + return false; + } + + const size_t closeParen = FindMatchingDelimiter(line, openParen, '(', ')'); + if (closeParen == std::string::npos) { + return false; + } + + outProperty.name = Containers::String(TrimCopy(line.substr(0, openParen)).c_str()); + if (outProperty.name.Empty()) { + return false; + } + + std::vector headerParts; + if (!SplitCommaSeparatedAuthoring(line.substr(openParen + 1, closeParen - openParen - 1), headerParts) || + headerParts.size() < 2u) { + return false; + } + + outProperty.displayName = UnquoteAuthoringValue(headerParts[0]); + std::string propertyTypeName = headerParts[1]; + for (size_t index = 2; index < headerParts.size(); ++index) { + propertyTypeName += ","; + propertyTypeName += headerParts[index]; + } + + propertyTypeName = TrimCopy(propertyTypeName); + const size_t rangePos = propertyTypeName.find('('); + if (rangePos != std::string::npos && + TrimCopy(propertyTypeName.substr(0, rangePos)) == "Range") { + propertyTypeName = "Range"; + } + + if (!TryParseShaderPropertyType(propertyTypeName.c_str(), outProperty.type)) { + return false; + } + + const size_t equalsPos = line.find('=', closeParen + 1); + if (equalsPos == std::string::npos) { + return false; + } + + const std::string tail = TrimCopy(line.substr(equalsPos + 1)); + const size_t attributePos = FindFirstTopLevelChar(tail, '['); + const std::string defaultValueText = + attributePos == std::string::npos ? tail : TrimCopy(tail.substr(0, attributePos)); + if (defaultValueText.empty()) { + return false; + } + + outProperty.defaultValue = UnquoteAuthoringValue(defaultValueText); + + if (attributePos != std::string::npos && + !TryParseSemanticAttributes(tail.substr(attributePos), outProperty.semantic)) { + return false; + } + + return true; +} + +bool TryParseAuthoringResourceLine( + const std::string& line, + ShaderResourceBindingDesc& outBinding) { + outBinding = {}; + + const size_t openParen = line.find('('); + if (openParen == std::string::npos) { + return false; + } + + const size_t closeParen = FindMatchingDelimiter(line, openParen, '(', ')'); + if (closeParen == std::string::npos) { + return false; + } + + outBinding.name = Containers::String(TrimCopy(line.substr(0, openParen)).c_str()); + if (outBinding.name.Empty()) { + return false; + } + + std::vector parts; + if (!SplitCommaSeparatedAuthoring(line.substr(openParen + 1, closeParen - openParen - 1), parts) || + parts.size() != 3u) { + return false; + } + + if (!TryParseShaderResourceType(parts[0].c_str(), outBinding.type)) { + return false; + } + + try { + outBinding.set = static_cast(std::stoul(parts[1])); + outBinding.binding = static_cast(std::stoul(parts[2])); + } catch (...) { + return false; + } + + const size_t attributePos = FindFirstTopLevelChar(line.substr(closeParen + 1), '['); + if (attributePos != std::string::npos) { + const std::string attributesText = line.substr(closeParen + 1 + attributePos); + if (!TryParseSemanticAttributes(attributesText, outBinding.semantic)) { + return false; + } + } + + return true; +} + +bool ParseLegacyBackendSplitShaderAuthoring( + const Containers::String& path, + const std::string& sourceText, + AuthoringShaderDesc& outDesc, + Containers::String* outError) { + (void)path; + outDesc = {}; + + enum class BlockKind { + None, + Shader, + Properties, + SubShader, + Pass, + Resources + }; + + auto fail = [&outError](const std::string& message, size_t lineNumber) -> bool { + if (outError != nullptr) { + *outError = Containers::String( + ("Legacy 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 blockStack; + BlockKind pendingBlock = BlockKind::None; + AuthoringSubShaderEntry* currentSubShader = nullptr; + AuthoringPassEntry* currentPass = nullptr; + bool inProgram = false; + + auto currentBlock = [&blockStack]() -> BlockKind { + return blockStack.empty() ? BlockKind::None : blockStack.back(); + }; + + for (size_t lineIndex = 0; lineIndex < lines.size(); ++lineIndex) { + const std::string& line = lines[lineIndex]; + const size_t humanLine = lineIndex + 1; + + if (inProgram) { + if (line == "ENDHLSL" || line == "ENDCG") { + inProgram = false; + continue; + } + + std::vector pragmaTokens; + if (!TryTokenizeQuotedArguments(line, pragmaTokens) || pragmaTokens.empty()) { + continue; + } + + if (pragmaTokens[0] != "#pragma") { + 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() >= 6u && pragmaTokens[1] == "backend") { + AuthoringBackendVariantEntry backendVariant = {}; + if (!TryParseShaderBackend(pragmaTokens[2].c_str(), backendVariant.backend)) { + return fail("invalid backend pragma backend name", humanLine); + } + if (!TryParseShaderLanguage(pragmaTokens[3].c_str(), backendVariant.language)) { + return fail("invalid backend pragma language name", humanLine); + } + + backendVariant.vertexSourcePath = pragmaTokens[4].c_str(); + backendVariant.fragmentSourcePath = pragmaTokens[5].c_str(); + if (pragmaTokens.size() >= 7u) { + backendVariant.vertexProfile = pragmaTokens[6].c_str(); + } + if (pragmaTokens.size() >= 8u) { + backendVariant.fragmentProfile = pragmaTokens[7].c_str(); + } + + currentPass->backendVariants.push_back(std::move(backendVariant)); + continue; + } + if (pragmaTokens.size() >= 2u && + (pragmaTokens[1] == "multi_compile" || + pragmaTokens[1] == "multi_compile_local" || + pragmaTokens[1] == "shader_feature" || + pragmaTokens[1] == "shader_feature_local")) { + ShaderKeywordDeclaration declaration = {}; + if (!TryParseShaderKeywordDeclarationPragma(pragmaTokens, declaration)) { + return fail("keyword pragma must declare at least one option", humanLine); + } + + currentPass->keywordDeclarations.PushBack(declaration); + continue; + } + continue; + } + + 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::Resources: + if (currentPass == nullptr) { + return fail("resources block must be inside a Pass", humanLine); + } + blockStack.push_back(BlockKind::Resources); + 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 (currentBlock() == BlockKind::Shader && StartsWithKeyword(line, "Fallback")) { + std::vector tokens; + if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() < 2u) { + return fail("Fallback directive is missing a value", humanLine); + } + outDesc.fallback = tokens[1].c_str(); + continue; + } + + if (StartsWithKeyword(line, "SubShader")) { + pendingBlock = BlockKind::SubShader; + continue; + } + + if (StartsWithKeyword(line, "Pass")) { + pendingBlock = BlockKind::Pass; + continue; + } + + if (line == "Resources") { + pendingBlock = BlockKind::Resources; + continue; + } + + 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 (currentBlock() == BlockKind::Resources) { + ShaderResourceBindingDesc resourceBinding = {}; + if (!TryParseAuthoringResourceLine(line, resourceBinding)) { + return fail("invalid Resources entry", humanLine); + } + currentPass->resources.PushBack(resourceBinding); + 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") { + inProgram = true; + continue; + } + } + + return fail("unsupported authoring statement: " + line, humanLine); + } + + if (inProgram) { + 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 (const AuthoringSubShaderEntry& subShader : outDesc.subShaders) { + if (subShader.passes.empty()) { + continue; + } + + for (const AuthoringPassEntry& pass : subShader.passes) { + if (pass.name.Empty()) { + return fail("a Pass is missing a Name directive", 0); + } + if (pass.backendVariants.empty()) { + return fail("a Pass is missing backend variants", 0); + } + } + } + + 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] == "multi_compile_local" || + pragmaTokens[1] == "shader_feature" || + pragmaTokens[1] == "shader_feature_local")) { + ShaderKeywordDeclaration declaration = {}; + if (!TryParseShaderKeywordDeclarationPragma(pragmaTokens, declaration)) { + return fail("keyword pragma must declare at least one option", humanLine); + } + + currentPass->keywordDeclarations.PushBack(declaration); + 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(); + currentPass->hasFixedFunctionState = true; + currentPass->fixedFunctionState = BuildUnityDefaultFixedFunctionState(); + if (currentSubShader->hasFixedFunctionState) { + currentPass->fixedFunctionState = currentSubShader->fixedFunctionState; + } + 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 (currentBlock() == BlockKind::Shader && StartsWithKeyword(line, "Fallback")) { + std::vector tokens; + if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() < 2u) { + return fail("Fallback directive is missing a value", humanLine); + } + outDesc.fallback = tokens[1].c_str(); + 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")) { + std::vector tokens; + if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() != 2u) { + return fail("LOD directive must provide a numeric value", humanLine); + } + + try { + const Core::uint32 lodValue = static_cast(std::stoul(tokens[1])); + SetOrReplaceAuthoringTag(currentSubShader->tags, "LOD", std::to_string(lodValue).c_str()); + } catch (...) { + return fail("LOD directive must provide a numeric value", humanLine); + } + continue; + } + + if (currentBlock() == BlockKind::SubShader && currentSubShader != nullptr) { + if (StartsWithKeyword(line, "Cull")) { + std::vector tokens; + if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() != 2u) { + return fail("Cull directive must use Front, Back, or Off", humanLine); + } + + EnsureAuthoringFixedFunctionStateInitialized( + currentSubShader->hasFixedFunctionState, + currentSubShader->fixedFunctionState); + if (!TryParseUnityStyleCullMode(tokens[1], currentSubShader->fixedFunctionState.cullMode)) { + return fail("Cull directive must use Front, Back, or Off", humanLine); + } + continue; + } + + if (StartsWithKeyword(line, "ZWrite")) { + std::vector tokens; + bool enabled = false; + if (!TryTokenizeQuotedArguments(line, tokens) || + tokens.size() != 2u || + !TryParseUnityStyleBoolDirectiveToken(tokens[1], enabled)) { + return fail("ZWrite directive must use On or Off", humanLine); + } + + EnsureAuthoringFixedFunctionStateInitialized( + currentSubShader->hasFixedFunctionState, + currentSubShader->fixedFunctionState); + currentSubShader->fixedFunctionState.depthWriteEnable = enabled; + continue; + } + + if (StartsWithKeyword(line, "ZTest")) { + std::vector tokens; + if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() != 2u) { + return fail("ZTest directive uses an unsupported compare function", humanLine); + } + + EnsureAuthoringFixedFunctionStateInitialized( + currentSubShader->hasFixedFunctionState, + currentSubShader->fixedFunctionState); + if (!TryParseUnityStyleComparisonFunc(tokens[1], currentSubShader->fixedFunctionState.depthFunc)) { + return fail("ZTest directive uses an unsupported compare function", humanLine); + } + currentSubShader->fixedFunctionState.depthTestEnable = true; + continue; + } + + if (StartsWithKeyword(line, "Blend")) { + std::vector tokens; + if (!TryTokenizeQuotedArguments(line, tokens)) { + return fail("Blend directive could not be tokenized", humanLine); + } + + for (size_t tokenIndex = 1; tokenIndex < tokens.size(); ++tokenIndex) { + if (!tokens[tokenIndex].empty() && tokens[tokenIndex].back() == ',') { + tokens[tokenIndex].pop_back(); + } + } + + EnsureAuthoringFixedFunctionStateInitialized( + currentSubShader->hasFixedFunctionState, + currentSubShader->fixedFunctionState); + if (!TryParseUnityStyleBlendDirective(tokens, currentSubShader->fixedFunctionState)) { + return fail("Blend directive uses an unsupported factor combination", humanLine); + } + continue; + } + + if (StartsWithKeyword(line, "ColorMask")) { + std::vector tokens; + if (!TryTokenizeQuotedArguments(line, tokens) || + (tokens.size() != 2u && tokens.size() != 3u)) { + return fail("ColorMask directive uses an unsupported channel mask", humanLine); + } + + EnsureAuthoringFixedFunctionStateInitialized( + currentSubShader->hasFixedFunctionState, + currentSubShader->fixedFunctionState); + if (!TryParseUnityStyleColorMask(tokens[1], currentSubShader->fixedFunctionState.colorWriteMask)) { + return fail("ColorMask directive uses an unsupported channel mask", humanLine); + } + 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 (StartsWithKeyword(line, "Cull")) { + std::vector tokens; + if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() != 2u) { + return fail("Cull directive must use Front, Back, or Off", humanLine); + } + + EnsureAuthoringFixedFunctionStateInitialized( + currentPass->hasFixedFunctionState, + currentPass->fixedFunctionState); + if (!TryParseUnityStyleCullMode(tokens[1], currentPass->fixedFunctionState.cullMode)) { + return fail("Cull directive must use Front, Back, or Off", humanLine); + } + continue; + } + + if (StartsWithKeyword(line, "ZWrite")) { + std::vector tokens; + bool enabled = false; + if (!TryTokenizeQuotedArguments(line, tokens) || + tokens.size() != 2u || + !TryParseUnityStyleBoolDirectiveToken(tokens[1], enabled)) { + return fail("ZWrite directive must use On or Off", humanLine); + } + + EnsureAuthoringFixedFunctionStateInitialized( + currentPass->hasFixedFunctionState, + currentPass->fixedFunctionState); + currentPass->fixedFunctionState.depthWriteEnable = enabled; + continue; + } + + if (StartsWithKeyword(line, "ZTest")) { + std::vector tokens; + if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() != 2u) { + return fail("ZTest directive uses an unsupported compare function", humanLine); + } + + EnsureAuthoringFixedFunctionStateInitialized( + currentPass->hasFixedFunctionState, + currentPass->fixedFunctionState); + if (!TryParseUnityStyleComparisonFunc(tokens[1], currentPass->fixedFunctionState.depthFunc)) { + return fail("ZTest directive uses an unsupported compare function", humanLine); + } + currentPass->fixedFunctionState.depthTestEnable = true; + continue; + } + + if (StartsWithKeyword(line, "Blend")) { + std::vector tokens; + if (!TryTokenizeQuotedArguments(line, tokens)) { + return fail("Blend directive could not be tokenized", humanLine); + } + + for (size_t tokenIndex = 1; tokenIndex < tokens.size(); ++tokenIndex) { + if (!tokens[tokenIndex].empty() && tokens[tokenIndex].back() == ',') { + tokens[tokenIndex].pop_back(); + } + } + + EnsureAuthoringFixedFunctionStateInitialized( + currentPass->hasFixedFunctionState, + currentPass->fixedFunctionState); + if (!TryParseUnityStyleBlendDirective(tokens, currentPass->fixedFunctionState)) { + return fail("Blend directive uses an unsupported factor combination", humanLine); + } + continue; + } + + if (StartsWithKeyword(line, "ColorMask")) { + std::vector tokens; + if (!TryTokenizeQuotedArguments(line, tokens) || + (tokens.size() != 2u && tokens.size() != 3u)) { + return fail("ColorMask directive uses an unsupported channel mask", humanLine); + } + + EnsureAuthoringFixedFunctionStateInitialized( + currentPass->hasFixedFunctionState, + currentPass->fixedFunctionState); + if (!TryParseUnityStyleColorMask(tokens[1], currentPass->fixedFunctionState.colorWriteMask)) { + return fail("ColorMask directive uses an unsupported channel mask", humanLine); + } + 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; +} +} // namespace Resources +} // namespace XCEngine diff --git a/engine/src/Resources/Shader/ShaderAuthoringParser.h b/engine/src/Resources/Shader/ShaderAuthoringParser.h new file mode 100644 index 00000000..7b36c3b9 --- /dev/null +++ b/engine/src/Resources/Shader/ShaderAuthoringParser.h @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace XCEngine { +namespace Resources { + +enum class ShaderAuthoringStyle { + NotShaderAuthoring = 0, + LegacyBackendSplit, + UnityStyleSingleSource +}; + +struct AuthoringTagEntry { + Containers::String name; + Containers::String value; +}; + +struct AuthoringBackendVariantEntry { + ShaderBackend backend = ShaderBackend::Generic; + ShaderLanguage language = ShaderLanguage::GLSL; + Containers::String vertexSourcePath; + Containers::String fragmentSourcePath; + Containers::String vertexProfile; + Containers::String fragmentProfile; +}; + +struct AuthoringPassEntry { + Containers::String name; + bool hasFixedFunctionState = false; + MaterialRenderState fixedFunctionState = {}; + std::vector tags; + Containers::Array resources; + Containers::Array keywordDeclarations; + Containers::String vertexEntryPoint; + Containers::String fragmentEntryPoint; + Containers::String sharedProgramSource; + Containers::String programSource; + Containers::String targetProfile; + std::vector backendVariants; +}; + +struct AuthoringSubShaderEntry { + bool hasFixedFunctionState = false; + MaterialRenderState fixedFunctionState = {}; + std::vector tags; + Containers::String sharedProgramSource; + std::vector passes; +}; + +struct AuthoringShaderDesc { + Containers::String name; + Containers::String fallback; + Containers::String sharedProgramSource; + Containers::Array properties; + std::vector subShaders; +}; + +void AppendAuthoringSourceBlock( + Containers::String& target, + const Containers::String& sourceBlock); + +void CollectQuotedIncludeDependencyPaths( + const Containers::String& sourcePath, + const Containers::String& sourceText, + std::unordered_set& seenPaths, + Containers::Array& outDependencies); + +Containers::String StripUnityStyleAuthoringPragmas(const Containers::String& sourceText); + +std::vector BuildShaderKeywordVariantSets( + const Containers::Array& declarations); + +Containers::String BuildKeywordVariantSource( + const Containers::String& baseSource, + const ShaderKeywordSet& requiredKeywords); + +ShaderAuthoringStyle DetectShaderAuthoringStyle(const std::string& sourceText); + +bool ParseLegacyBackendSplitShaderAuthoring( + const Containers::String& path, + const std::string& sourceText, + AuthoringShaderDesc& outDesc, + Containers::String* outError); + +bool ParseUnityStyleSingleSourceShaderAuthoring( + const Containers::String& path, + const std::string& sourceText, + AuthoringShaderDesc& outDesc, + Containers::String* outError); + +} // namespace Resources +} // namespace XCEngine \ No newline at end of file diff --git a/engine/src/Resources/Shader/ShaderLoader.cpp b/engine/src/Resources/Shader/ShaderLoader.cpp index 88a135e7..bb5e19e3 100644 --- a/engine/src/Resources/Shader/ShaderLoader.cpp +++ b/engine/src/Resources/Shader/ShaderLoader.cpp @@ -1,4 +1,5 @@ #include +#include "ShaderAuthoringParser.h" #include #include @@ -614,1961 +615,6 @@ bool ReadTextFile(const Containers::String& path, Containers::String& outText) { } size_t CalculateShaderMemorySize(const Shader& shader); -bool TryTokenizeQuotedArguments(const std::string& line, std::vector& outTokens); -MaterialRenderState BuildUnityDefaultFixedFunctionState(); -void EnsureAuthoringFixedFunctionStateInitialized( - bool& hasFixedFunctionState, - MaterialRenderState& fixedFunctionState); - -enum class ShaderAuthoringStyle { - NotShaderAuthoring = 0, - LegacyBackendSplit, - UnityStyleSingleSource -}; - -struct AuthoringTagEntry { - Containers::String name; - Containers::String value; -}; - -struct AuthoringBackendVariantEntry { - ShaderBackend backend = ShaderBackend::Generic; - ShaderLanguage language = ShaderLanguage::GLSL; - Containers::String vertexSourcePath; - Containers::String fragmentSourcePath; - Containers::String vertexProfile; - Containers::String fragmentProfile; -}; - -struct AuthoringPassEntry { - Containers::String name; - bool hasFixedFunctionState = false; - MaterialRenderState fixedFunctionState = {}; - std::vector tags; - Containers::Array resources; - Containers::Array keywordDeclarations; - Containers::String vertexEntryPoint; - Containers::String fragmentEntryPoint; - Containers::String sharedProgramSource; - Containers::String programSource; - Containers::String targetProfile; - std::vector backendVariants; -}; - -struct AuthoringSubShaderEntry { - bool hasFixedFunctionState = false; - MaterialRenderState fixedFunctionState = {}; - std::vector tags; - Containers::String sharedProgramSource; - std::vector passes; -}; - -struct AuthoringShaderDesc { - Containers::String name; - Containers::String fallback; - 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; - for (size_t index = 0; index + 1 < line.size(); ++index) { - const char ch = line[index]; - if (escaped) { - escaped = false; - continue; - } - - if (ch == '\\') { - escaped = true; - continue; - } - - if (ch == '"') { - inString = !inString; - continue; - } - - if (!inString && ch == '/' && line[index + 1] == '/') { - return line.substr(0, index); - } - } - - return line; -} - -bool StartsWithKeyword(const std::string& line, const char* keyword) { - const std::string keywordString(keyword); - if (line.size() < keywordString.size() || - line.compare(0, keywordString.size(), keywordString) != 0) { - return false; - } - - return line.size() == keywordString.size() || - std::isspace(static_cast(line[keywordString.size()])) != 0; -} - -void SplitShaderAuthoringLines( - const std::string& sourceText, - std::vector& outLines) { - outLines.clear(); - - std::istringstream stream(sourceText); - std::string rawLine; - while (std::getline(stream, rawLine)) { - std::string line = TrimCopy(StripAuthoringLineComment(rawLine)); - if (line.empty()) { - continue; - } - - if (!StartsWithKeyword(line, "Tags") && - line.size() > 1 && - line.back() == '{') { - line.pop_back(); - const std::string prefix = TrimCopy(line); - if (!prefix.empty()) { - outLines.push_back(prefix); - } - outLines.push_back("{"); - continue; - } - - if (line.size() > 1 && line.front() == '}') { - outLines.push_back("}"); - line = TrimCopy(line.substr(1)); - if (!line.empty()) { - outLines.push_back(line); - } - continue; - } - - outLines.push_back(line); - } -} - -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) { - return true; - } - } - - return false; -} - -MaterialRenderState BuildUnityDefaultFixedFunctionState() { - MaterialRenderState state = {}; - state.blendEnable = false; - state.srcBlend = MaterialBlendFactor::One; - state.dstBlend = MaterialBlendFactor::Zero; - state.srcBlendAlpha = MaterialBlendFactor::One; - state.dstBlendAlpha = MaterialBlendFactor::Zero; - state.blendOp = MaterialBlendOp::Add; - state.blendOpAlpha = MaterialBlendOp::Add; - state.colorWriteMask = 0xF; - state.depthTestEnable = true; - state.depthWriteEnable = true; - state.depthFunc = MaterialComparisonFunc::LessEqual; - state.cullMode = MaterialCullMode::Back; - return state; -} - -void EnsureAuthoringFixedFunctionStateInitialized( - bool& hasFixedFunctionState, - MaterialRenderState& fixedFunctionState) { - if (!hasFixedFunctionState) { - hasFixedFunctionState = true; - fixedFunctionState = BuildUnityDefaultFixedFunctionState(); - } -} - -bool TryParseUnityStyleBoolDirectiveToken(const std::string& token, bool& outValue) { - const Containers::String normalized = Containers::String(token.c_str()).Trim().ToLower(); - if (normalized == "on") { - outValue = true; - return true; - } - if (normalized == "off") { - outValue = false; - return true; - } - return false; -} - -bool TryParseUnityStyleCullMode(const std::string& token, MaterialCullMode& outMode) { - const Containers::String normalized = Containers::String(token.c_str()).Trim().ToLower(); - if (normalized == "back") { - outMode = MaterialCullMode::Back; - return true; - } - if (normalized == "front") { - outMode = MaterialCullMode::Front; - return true; - } - if (normalized == "off") { - outMode = MaterialCullMode::None; - return true; - } - return false; -} - -bool TryParseUnityStyleComparisonFunc(const std::string& token, MaterialComparisonFunc& outFunc) { - const Containers::String normalized = Containers::String(token.c_str()).Trim().ToLower(); - if (normalized == "never") { - outFunc = MaterialComparisonFunc::Never; - return true; - } - if (normalized == "less") { - outFunc = MaterialComparisonFunc::Less; - return true; - } - if (normalized == "equal") { - outFunc = MaterialComparisonFunc::Equal; - return true; - } - if (normalized == "lequal" || normalized == "lessequal" || normalized == "less_equal") { - outFunc = MaterialComparisonFunc::LessEqual; - return true; - } - if (normalized == "greater") { - outFunc = MaterialComparisonFunc::Greater; - return true; - } - if (normalized == "notequal" || normalized == "not_equal") { - outFunc = MaterialComparisonFunc::NotEqual; - return true; - } - if (normalized == "gequal" || normalized == "greaterequal" || normalized == "greater_equal") { - outFunc = MaterialComparisonFunc::GreaterEqual; - return true; - } - if (normalized == "always") { - outFunc = MaterialComparisonFunc::Always; - return true; - } - return false; -} - -bool TryParseUnityStyleBlendFactor(const std::string& token, MaterialBlendFactor& outFactor) { - const Containers::String normalized = Containers::String(token.c_str()).Trim().ToLower(); - if (normalized == "zero") { - outFactor = MaterialBlendFactor::Zero; - return true; - } - if (normalized == "one") { - outFactor = MaterialBlendFactor::One; - return true; - } - if (normalized == "srccolor" || normalized == "src_color") { - outFactor = MaterialBlendFactor::SrcColor; - return true; - } - if (normalized == "oneminussrccolor" || normalized == "one_minus_src_color" || normalized == "invsrccolor") { - outFactor = MaterialBlendFactor::InvSrcColor; - return true; - } - if (normalized == "srcalpha" || normalized == "src_alpha") { - outFactor = MaterialBlendFactor::SrcAlpha; - return true; - } - if (normalized == "oneminussrcalpha" || normalized == "one_minus_src_alpha" || normalized == "invsrcalpha") { - outFactor = MaterialBlendFactor::InvSrcAlpha; - return true; - } - if (normalized == "dstalpha" || normalized == "dst_alpha") { - outFactor = MaterialBlendFactor::DstAlpha; - return true; - } - if (normalized == "oneminusdstalpha" || normalized == "one_minus_dst_alpha" || normalized == "invdstalpha") { - outFactor = MaterialBlendFactor::InvDstAlpha; - return true; - } - if (normalized == "dstcolor" || normalized == "dst_color") { - outFactor = MaterialBlendFactor::DstColor; - return true; - } - if (normalized == "oneminusdstcolor" || normalized == "one_minus_dst_color" || normalized == "invdstcolor") { - outFactor = MaterialBlendFactor::InvDstColor; - return true; - } - if (normalized == "srcalphasaturate" || normalized == "src_alpha_saturate" || normalized == "srcalphasat") { - outFactor = MaterialBlendFactor::SrcAlphaSat; - return true; - } - return false; -} - -bool TryParseUnityStyleColorMask(const std::string& token, Core::uint8& outMask) { - const Containers::String normalized = Containers::String(token.c_str()).Trim().ToUpper(); - if (normalized == "0") { - outMask = 0u; - return true; - } - - Core::uint8 mask = 0u; - for (size_t index = 0; index < normalized.Length(); ++index) { - switch (normalized[index]) { - case 'R': - mask |= 0x1u; - break; - case 'G': - mask |= 0x2u; - break; - case 'B': - mask |= 0x4u; - break; - case 'A': - mask |= 0x8u; - break; - default: - return false; - } - } - - outMask = mask; - return true; -} - -bool TryParseUnityStyleBlendDirective( - const std::vector& tokens, - MaterialRenderState& outState) { - std::vector normalizedTokens; - normalizedTokens.reserve(tokens.size()); - for (const std::string& token : tokens) { - if (token == ",") { - continue; - } - - std::string normalizedToken = token; - while (!normalizedToken.empty() && normalizedToken.back() == ',') { - normalizedToken.pop_back(); - } - if (!normalizedToken.empty()) { - normalizedTokens.push_back(std::move(normalizedToken)); - } - } - - if (normalizedTokens.size() != 2u && - normalizedTokens.size() != 3u && - normalizedTokens.size() != 5u) { - return false; - } - - if (normalizedTokens.size() == 2u) { - bool enabled = false; - if (!TryParseUnityStyleBoolDirectiveToken(normalizedTokens[1], enabled)) { - return false; - } - - outState.blendEnable = enabled; - if (!enabled) { - outState.srcBlend = MaterialBlendFactor::One; - outState.dstBlend = MaterialBlendFactor::Zero; - outState.srcBlendAlpha = MaterialBlendFactor::One; - outState.dstBlendAlpha = MaterialBlendFactor::Zero; - } - return true; - } - - MaterialBlendFactor srcBlend = MaterialBlendFactor::One; - MaterialBlendFactor dstBlend = MaterialBlendFactor::Zero; - if (!TryParseUnityStyleBlendFactor(normalizedTokens[1], srcBlend) || - !TryParseUnityStyleBlendFactor(normalizedTokens[2], dstBlend)) { - return false; - } - - outState.blendEnable = true; - outState.srcBlend = srcBlend; - outState.dstBlend = dstBlend; - - if (normalizedTokens.size() == 5u) { - if (!TryParseUnityStyleBlendFactor(normalizedTokens[3], outState.srcBlendAlpha) || - !TryParseUnityStyleBlendFactor(normalizedTokens[4], outState.dstBlendAlpha)) { - return false; - } - } else { - outState.srcBlendAlpha = srcBlend; - outState.dstBlendAlpha = dstBlend; - } - - return true; -} - -void SetOrReplaceAuthoringTag( - std::vector& tags, - const Containers::String& name, - const Containers::String& value) { - for (AuthoringTagEntry& tag : tags) { - if (tag.name == name) { - tag.value = value; - return; - } - } - - tags.push_back({ name, value }); -} - -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] == "multi_compile_local" || - 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(); -} - -Containers::String BuildShaderKeywordSetSignature(const ShaderKeywordSet& keywordSet) { - ShaderKeywordSet normalizedKeywords = keywordSet; - 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; -} - -std::vector BuildShaderKeywordVariantSets( - const Containers::Array& declarations) { - std::vector keywordSets(1); - - for (const ShaderKeywordDeclaration& declaration : declarations) { - if (declaration.options.Empty()) { - continue; - } - - std::vector nextKeywordSets; - std::unordered_set seenSignatures; - nextKeywordSets.reserve(keywordSets.size() * declaration.options.Size()); - - for (const ShaderKeywordSet& currentKeywordSet : keywordSets) { - for (const Containers::String& option : declaration.options) { - ShaderKeywordSet nextKeywordSet = currentKeywordSet; - const Containers::String normalizedKeyword = NormalizeShaderKeywordToken(option); - if (!normalizedKeyword.Empty()) { - nextKeywordSet.enabledKeywords.PushBack(normalizedKeyword); - } - - NormalizeShaderKeywordSetInPlace(nextKeywordSet); - const std::string signature = - ToStdString(BuildShaderKeywordSetSignature(nextKeywordSet)); - if (seenSignatures.insert(signature).second) { - nextKeywordSets.push_back(std::move(nextKeywordSet)); - } - } - } - - if (!nextKeywordSets.empty()) { - keywordSets = std::move(nextKeywordSets); - } - } - - if (keywordSets.empty()) { - keywordSets.emplace_back(); - } - - return keywordSets; -} - -Containers::String BuildKeywordVariantSource( - const Containers::String& baseSource, - const ShaderKeywordSet& requiredKeywords) { - if (requiredKeywords.enabledKeywords.Empty()) { - return baseSource; - } - - std::string defineBlock; - for (const Containers::String& keyword : requiredKeywords.enabledKeywords) { - defineBlock += "#define "; - defineBlock += ToStdString(keyword); - defineBlock += " 1\n"; - } - - if (baseSource.Empty()) { - return Containers::String(defineBlock.c_str()); - } - - const std::string sourceText = ToStdString(baseSource); - - size_t lineStart = 0; - while (lineStart < sourceText.size()) { - const size_t lineEnd = sourceText.find_first_of("\r\n", lineStart); - const size_t lineLength = - lineEnd == std::string::npos ? sourceText.size() - lineStart : lineEnd - lineStart; - const std::string line = sourceText.substr(lineStart, lineLength); - const std::string trimmedLine = TrimCopy(line); - - if (trimmedLine.empty() || - trimmedLine.rfind("//", 0) == 0u || - trimmedLine.rfind("/*", 0) == 0u || - trimmedLine.rfind("*", 0) == 0u) { - if (lineEnd == std::string::npos) { - break; - } - - lineStart = lineEnd + 1u; - if (sourceText[lineEnd] == '\r' && - lineStart < sourceText.size() && - sourceText[lineStart] == '\n') { - ++lineStart; - } - continue; - } - - if (trimmedLine.rfind("#version", 0) != 0u) { - break; - } - - size_t insertionPos = lineEnd == std::string::npos ? sourceText.size() : lineEnd + 1u; - if (lineEnd != std::string::npos && - sourceText[lineEnd] == '\r' && - insertionPos < sourceText.size() && - sourceText[insertionPos] == '\n') { - ++insertionPos; - } - - std::string variantSource = sourceText.substr(0, insertionPos); - variantSource += defineBlock; - variantSource += sourceText.substr(insertionPos); - return Containers::String(variantSource.c_str()); - } - - std::string variantSource = defineBlock; - variantSource += '\n'; - variantSource += sourceText; - return Containers::String(variantSource.c_str()); -} - -bool TryParseShaderKeywordDeclarationPragma( - const std::vector& pragmaTokens, - ShaderKeywordDeclaration& outDeclaration) { - outDeclaration = {}; - if (pragmaTokens.size() < 3u) { - return false; - } - - if (pragmaTokens[1] == "multi_compile") { - outDeclaration.type = ShaderKeywordDeclarationType::MultiCompile; - } else if (pragmaTokens[1] == "multi_compile_local") { - outDeclaration.type = ShaderKeywordDeclarationType::MultiCompileLocal; - } else if (pragmaTokens[1] == "shader_feature") { - outDeclaration.type = ShaderKeywordDeclarationType::ShaderFeature; - } else if (pragmaTokens[1] == "shader_feature_local") { - outDeclaration.type = ShaderKeywordDeclarationType::ShaderFeatureLocal; - } else { - return false; - } - - for (size_t tokenIndex = 2; tokenIndex < pragmaTokens.size(); ++tokenIndex) { - outDeclaration.options.PushBack(pragmaTokens[tokenIndex].c_str()); - } - - return !outDeclaration.options.Empty(); -} - -size_t FindMatchingDelimiter( - const std::string& text, - size_t openPos, - char openChar, - char closeChar) { - if (openPos >= text.size() || text[openPos] != openChar) { - return std::string::npos; - } - - bool inString = false; - bool escaped = false; - int depth = 0; - for (size_t pos = openPos; pos < text.size(); ++pos) { - const char ch = text[pos]; - if (escaped) { - escaped = false; - continue; - } - - if (ch == '\\') { - escaped = true; - continue; - } - - if (ch == '"') { - inString = !inString; - continue; - } - - if (inString) { - continue; - } - - if (ch == openChar) { - ++depth; - } else if (ch == closeChar) { - --depth; - if (depth == 0) { - return pos; - } - } - } - - return std::string::npos; -} - -size_t FindFirstTopLevelChar(const std::string& text, char target) { - bool inString = false; - bool escaped = false; - int roundDepth = 0; - int squareDepth = 0; - - for (size_t pos = 0; pos < text.size(); ++pos) { - const char ch = text[pos]; - if (escaped) { - escaped = false; - continue; - } - - if (ch == '\\') { - escaped = true; - continue; - } - - if (ch == '"') { - inString = !inString; - continue; - } - - if (inString) { - continue; - } - - if (ch == target && roundDepth == 0 && squareDepth == 0) { - return pos; - } - - if (ch == '(') { - ++roundDepth; - continue; - } - if (ch == ')') { - --roundDepth; - continue; - } - if (ch == '[') { - ++squareDepth; - continue; - } - if (ch == ']') { - --squareDepth; - continue; - } - - } - - return std::string::npos; -} - -bool SplitCommaSeparatedAuthoring(const std::string& text, std::vector& outParts) { - outParts.clear(); - - bool inString = false; - bool escaped = false; - int roundDepth = 0; - int squareDepth = 0; - size_t partStart = 0; - - for (size_t pos = 0; pos < text.size(); ++pos) { - const char ch = text[pos]; - if (escaped) { - escaped = false; - continue; - } - - if (ch == '\\') { - escaped = true; - continue; - } - - if (ch == '"') { - inString = !inString; - continue; - } - - if (inString) { - continue; - } - - if (ch == '(') { - ++roundDepth; - continue; - } - if (ch == ')') { - --roundDepth; - continue; - } - if (ch == '[') { - ++squareDepth; - continue; - } - if (ch == ']') { - --squareDepth; - continue; - } - - if (ch == ',' && roundDepth == 0 && squareDepth == 0) { - outParts.push_back(TrimCopy(text.substr(partStart, pos - partStart))); - partStart = pos + 1; - } - } - - outParts.push_back(TrimCopy(text.substr(partStart))); - return true; -} - -Containers::String UnquoteAuthoringValue(const std::string& text) { - const std::string trimmed = TrimCopy(text); - if (trimmed.size() >= 2 && - trimmed.front() == '"' && - trimmed.back() == '"') { - Containers::String parsed; - if (ParseQuotedString(trimmed, 0, parsed)) { - return parsed; - } - } - - return Containers::String(trimmed.c_str()); -} - -bool TryTokenizeQuotedArguments(const std::string& line, std::vector& outTokens) { - outTokens.clear(); - - std::string current; - bool inString = false; - bool escaped = false; - - for (size_t pos = 0; pos < line.size(); ++pos) { - const char ch = line[pos]; - if (escaped) { - current.push_back(ch); - escaped = false; - continue; - } - - if (ch == '\\' && inString) { - escaped = true; - continue; - } - - if (ch == '"') { - inString = !inString; - continue; - } - - if (!inString && std::isspace(static_cast(ch)) != 0) { - if (!current.empty()) { - outTokens.push_back(current); - current.clear(); - } - continue; - } - - current.push_back(ch); - } - - if (inString) { - return false; - } - - if (!current.empty()) { - outTokens.push_back(current); - } - - return !outTokens.empty(); -} - -bool TryParseInlineTagAssignments( - const std::string& line, - std::vector& outTags) { - const size_t openBrace = line.find('{'); - const size_t closeBrace = line.rfind('}'); - if (openBrace == std::string::npos || - closeBrace == std::string::npos || - closeBrace <= openBrace) { - return false; - } - - const std::string content = line.substr(openBrace + 1, closeBrace - openBrace - 1); - size_t pos = 0; - while (pos < content.size()) { - pos = SkipWhitespace(content, pos); - while (pos < content.size() && content[pos] == ',') { - ++pos; - pos = SkipWhitespace(content, pos); - } - if (pos >= content.size()) { - break; - } - - Containers::String key; - if (!ParseQuotedString(content, pos, key, &pos)) { - return false; - } - - pos = SkipWhitespace(content, pos); - if (pos >= content.size() || content[pos] != '=') { - return false; - } - - pos = SkipWhitespace(content, pos + 1); - Containers::String value; - if (!ParseQuotedString(content, pos, value, &pos)) { - return false; - } - - outTags.push_back({ key, value }); - } - - return true; -} - -bool TryParseSemanticAttributes( - const std::string& attributesText, - Containers::String& outSemantic) { - outSemantic.Clear(); - - size_t pos = 0; - while (pos < attributesText.size()) { - pos = attributesText.find('[', pos); - if (pos == std::string::npos) { - break; - } - - const size_t closePos = FindMatchingDelimiter(attributesText, pos, '[', ']'); - if (closePos == std::string::npos) { - return false; - } - - const std::string attributeBody = TrimCopy(attributesText.substr(pos + 1, closePos - pos - 1)); - if (attributeBody.size() > 10 && - attributeBody.compare(0, 9, "Semantic(") == 0 && - attributeBody.back() == ')') { - outSemantic = UnquoteAuthoringValue(attributeBody.substr(9, attributeBody.size() - 10)); - } - - pos = closePos + 1; - } - - return true; -} - -bool TryParseAuthoringPropertyLine( - const std::string& line, - ShaderPropertyDesc& outProperty) { - outProperty = {}; - - const size_t openParen = line.find('('); - if (openParen == std::string::npos) { - return false; - } - - const size_t closeParen = FindMatchingDelimiter(line, openParen, '(', ')'); - if (closeParen == std::string::npos) { - return false; - } - - outProperty.name = Containers::String(TrimCopy(line.substr(0, openParen)).c_str()); - if (outProperty.name.Empty()) { - return false; - } - - std::vector headerParts; - if (!SplitCommaSeparatedAuthoring(line.substr(openParen + 1, closeParen - openParen - 1), headerParts) || - headerParts.size() < 2u) { - return false; - } - - outProperty.displayName = UnquoteAuthoringValue(headerParts[0]); - std::string propertyTypeName = headerParts[1]; - for (size_t index = 2; index < headerParts.size(); ++index) { - propertyTypeName += ","; - propertyTypeName += headerParts[index]; - } - - propertyTypeName = TrimCopy(propertyTypeName); - const size_t rangePos = propertyTypeName.find('('); - if (rangePos != std::string::npos && - TrimCopy(propertyTypeName.substr(0, rangePos)) == "Range") { - propertyTypeName = "Range"; - } - - if (!TryParseShaderPropertyType(propertyTypeName.c_str(), outProperty.type)) { - return false; - } - - const size_t equalsPos = line.find('=', closeParen + 1); - if (equalsPos == std::string::npos) { - return false; - } - - const std::string tail = TrimCopy(line.substr(equalsPos + 1)); - const size_t attributePos = FindFirstTopLevelChar(tail, '['); - const std::string defaultValueText = - attributePos == std::string::npos ? tail : TrimCopy(tail.substr(0, attributePos)); - if (defaultValueText.empty()) { - return false; - } - - outProperty.defaultValue = UnquoteAuthoringValue(defaultValueText); - - if (attributePos != std::string::npos && - !TryParseSemanticAttributes(tail.substr(attributePos), outProperty.semantic)) { - return false; - } - - return true; -} - -bool TryParseAuthoringResourceLine( - const std::string& line, - ShaderResourceBindingDesc& outBinding) { - outBinding = {}; - - const size_t openParen = line.find('('); - if (openParen == std::string::npos) { - return false; - } - - const size_t closeParen = FindMatchingDelimiter(line, openParen, '(', ')'); - if (closeParen == std::string::npos) { - return false; - } - - outBinding.name = Containers::String(TrimCopy(line.substr(0, openParen)).c_str()); - if (outBinding.name.Empty()) { - return false; - } - - std::vector parts; - if (!SplitCommaSeparatedAuthoring(line.substr(openParen + 1, closeParen - openParen - 1), parts) || - parts.size() != 3u) { - return false; - } - - if (!TryParseShaderResourceType(parts[0].c_str(), outBinding.type)) { - return false; - } - - try { - outBinding.set = static_cast(std::stoul(parts[1])); - outBinding.binding = static_cast(std::stoul(parts[2])); - } catch (...) { - return false; - } - - const size_t attributePos = FindFirstTopLevelChar(line.substr(closeParen + 1), '['); - if (attributePos != std::string::npos) { - const std::string attributesText = line.substr(closeParen + 1 + attributePos); - if (!TryParseSemanticAttributes(attributesText, outBinding.semantic)) { - return false; - } - } - - return true; -} - -bool ParseLegacyBackendSplitShaderAuthoring( - const Containers::String& path, - const std::string& sourceText, - AuthoringShaderDesc& outDesc, - Containers::String* outError) { - (void)path; - outDesc = {}; - - enum class BlockKind { - None, - Shader, - Properties, - SubShader, - Pass, - Resources - }; - - auto fail = [&outError](const std::string& message, size_t lineNumber) -> bool { - if (outError != nullptr) { - *outError = Containers::String( - ("Legacy 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 blockStack; - BlockKind pendingBlock = BlockKind::None; - AuthoringSubShaderEntry* currentSubShader = nullptr; - AuthoringPassEntry* currentPass = nullptr; - bool inProgram = false; - - auto currentBlock = [&blockStack]() -> BlockKind { - return blockStack.empty() ? BlockKind::None : blockStack.back(); - }; - - for (size_t lineIndex = 0; lineIndex < lines.size(); ++lineIndex) { - const std::string& line = lines[lineIndex]; - const size_t humanLine = lineIndex + 1; - - if (inProgram) { - if (line == "ENDHLSL" || line == "ENDCG") { - inProgram = false; - continue; - } - - std::vector pragmaTokens; - if (!TryTokenizeQuotedArguments(line, pragmaTokens) || pragmaTokens.empty()) { - continue; - } - - if (pragmaTokens[0] != "#pragma") { - 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() >= 6u && pragmaTokens[1] == "backend") { - AuthoringBackendVariantEntry backendVariant = {}; - if (!TryParseShaderBackend(pragmaTokens[2].c_str(), backendVariant.backend)) { - return fail("invalid backend pragma backend name", humanLine); - } - if (!TryParseShaderLanguage(pragmaTokens[3].c_str(), backendVariant.language)) { - return fail("invalid backend pragma language name", humanLine); - } - - backendVariant.vertexSourcePath = pragmaTokens[4].c_str(); - backendVariant.fragmentSourcePath = pragmaTokens[5].c_str(); - if (pragmaTokens.size() >= 7u) { - backendVariant.vertexProfile = pragmaTokens[6].c_str(); - } - if (pragmaTokens.size() >= 8u) { - backendVariant.fragmentProfile = pragmaTokens[7].c_str(); - } - - currentPass->backendVariants.push_back(std::move(backendVariant)); - continue; - } - if (pragmaTokens.size() >= 2u && - (pragmaTokens[1] == "multi_compile" || - pragmaTokens[1] == "multi_compile_local" || - pragmaTokens[1] == "shader_feature" || - pragmaTokens[1] == "shader_feature_local")) { - ShaderKeywordDeclaration declaration = {}; - if (!TryParseShaderKeywordDeclarationPragma(pragmaTokens, declaration)) { - return fail("keyword pragma must declare at least one option", humanLine); - } - - currentPass->keywordDeclarations.PushBack(declaration); - continue; - } - continue; - } - - 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::Resources: - if (currentPass == nullptr) { - return fail("resources block must be inside a Pass", humanLine); - } - blockStack.push_back(BlockKind::Resources); - 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 (currentBlock() == BlockKind::Shader && StartsWithKeyword(line, "Fallback")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() < 2u) { - return fail("Fallback directive is missing a value", humanLine); - } - outDesc.fallback = tokens[1].c_str(); - continue; - } - - if (StartsWithKeyword(line, "SubShader")) { - pendingBlock = BlockKind::SubShader; - continue; - } - - if (StartsWithKeyword(line, "Pass")) { - pendingBlock = BlockKind::Pass; - continue; - } - - if (line == "Resources") { - pendingBlock = BlockKind::Resources; - continue; - } - - 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 (currentBlock() == BlockKind::Resources) { - ShaderResourceBindingDesc resourceBinding = {}; - if (!TryParseAuthoringResourceLine(line, resourceBinding)) { - return fail("invalid Resources entry", humanLine); - } - currentPass->resources.PushBack(resourceBinding); - 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") { - inProgram = true; - continue; - } - } - - return fail("unsupported authoring statement: " + line, humanLine); - } - - if (inProgram) { - 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 (const AuthoringSubShaderEntry& subShader : outDesc.subShaders) { - if (subShader.passes.empty()) { - continue; - } - - for (const AuthoringPassEntry& pass : subShader.passes) { - if (pass.name.Empty()) { - return fail("a Pass is missing a Name directive", 0); - } - if (pass.backendVariants.empty()) { - return fail("a Pass is missing backend variants", 0); - } - } - } - - 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] == "multi_compile_local" || - pragmaTokens[1] == "shader_feature" || - pragmaTokens[1] == "shader_feature_local")) { - ShaderKeywordDeclaration declaration = {}; - if (!TryParseShaderKeywordDeclarationPragma(pragmaTokens, declaration)) { - return fail("keyword pragma must declare at least one option", humanLine); - } - - currentPass->keywordDeclarations.PushBack(declaration); - 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(); - currentPass->hasFixedFunctionState = true; - currentPass->fixedFunctionState = BuildUnityDefaultFixedFunctionState(); - if (currentSubShader->hasFixedFunctionState) { - currentPass->fixedFunctionState = currentSubShader->fixedFunctionState; - } - 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 (currentBlock() == BlockKind::Shader && StartsWithKeyword(line, "Fallback")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() < 2u) { - return fail("Fallback directive is missing a value", humanLine); - } - outDesc.fallback = tokens[1].c_str(); - 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")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() != 2u) { - return fail("LOD directive must provide a numeric value", humanLine); - } - - try { - const Core::uint32 lodValue = static_cast(std::stoul(tokens[1])); - SetOrReplaceAuthoringTag(currentSubShader->tags, "LOD", std::to_string(lodValue).c_str()); - } catch (...) { - return fail("LOD directive must provide a numeric value", humanLine); - } - continue; - } - - if (currentBlock() == BlockKind::SubShader && currentSubShader != nullptr) { - if (StartsWithKeyword(line, "Cull")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() != 2u) { - return fail("Cull directive must use Front, Back, or Off", humanLine); - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentSubShader->hasFixedFunctionState, - currentSubShader->fixedFunctionState); - if (!TryParseUnityStyleCullMode(tokens[1], currentSubShader->fixedFunctionState.cullMode)) { - return fail("Cull directive must use Front, Back, or Off", humanLine); - } - continue; - } - - if (StartsWithKeyword(line, "ZWrite")) { - std::vector tokens; - bool enabled = false; - if (!TryTokenizeQuotedArguments(line, tokens) || - tokens.size() != 2u || - !TryParseUnityStyleBoolDirectiveToken(tokens[1], enabled)) { - return fail("ZWrite directive must use On or Off", humanLine); - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentSubShader->hasFixedFunctionState, - currentSubShader->fixedFunctionState); - currentSubShader->fixedFunctionState.depthWriteEnable = enabled; - continue; - } - - if (StartsWithKeyword(line, "ZTest")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() != 2u) { - return fail("ZTest directive uses an unsupported compare function", humanLine); - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentSubShader->hasFixedFunctionState, - currentSubShader->fixedFunctionState); - if (!TryParseUnityStyleComparisonFunc(tokens[1], currentSubShader->fixedFunctionState.depthFunc)) { - return fail("ZTest directive uses an unsupported compare function", humanLine); - } - currentSubShader->fixedFunctionState.depthTestEnable = true; - continue; - } - - if (StartsWithKeyword(line, "Blend")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens)) { - return fail("Blend directive could not be tokenized", humanLine); - } - - for (size_t tokenIndex = 1; tokenIndex < tokens.size(); ++tokenIndex) { - if (!tokens[tokenIndex].empty() && tokens[tokenIndex].back() == ',') { - tokens[tokenIndex].pop_back(); - } - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentSubShader->hasFixedFunctionState, - currentSubShader->fixedFunctionState); - if (!TryParseUnityStyleBlendDirective(tokens, currentSubShader->fixedFunctionState)) { - return fail("Blend directive uses an unsupported factor combination", humanLine); - } - continue; - } - - if (StartsWithKeyword(line, "ColorMask")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens) || - (tokens.size() != 2u && tokens.size() != 3u)) { - return fail("ColorMask directive uses an unsupported channel mask", humanLine); - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentSubShader->hasFixedFunctionState, - currentSubShader->fixedFunctionState); - if (!TryParseUnityStyleColorMask(tokens[1], currentSubShader->fixedFunctionState.colorWriteMask)) { - return fail("ColorMask directive uses an unsupported channel mask", humanLine); - } - 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 (StartsWithKeyword(line, "Cull")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() != 2u) { - return fail("Cull directive must use Front, Back, or Off", humanLine); - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentPass->hasFixedFunctionState, - currentPass->fixedFunctionState); - if (!TryParseUnityStyleCullMode(tokens[1], currentPass->fixedFunctionState.cullMode)) { - return fail("Cull directive must use Front, Back, or Off", humanLine); - } - continue; - } - - if (StartsWithKeyword(line, "ZWrite")) { - std::vector tokens; - bool enabled = false; - if (!TryTokenizeQuotedArguments(line, tokens) || - tokens.size() != 2u || - !TryParseUnityStyleBoolDirectiveToken(tokens[1], enabled)) { - return fail("ZWrite directive must use On or Off", humanLine); - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentPass->hasFixedFunctionState, - currentPass->fixedFunctionState); - currentPass->fixedFunctionState.depthWriteEnable = enabled; - continue; - } - - if (StartsWithKeyword(line, "ZTest")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() != 2u) { - return fail("ZTest directive uses an unsupported compare function", humanLine); - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentPass->hasFixedFunctionState, - currentPass->fixedFunctionState); - if (!TryParseUnityStyleComparisonFunc(tokens[1], currentPass->fixedFunctionState.depthFunc)) { - return fail("ZTest directive uses an unsupported compare function", humanLine); - } - currentPass->fixedFunctionState.depthTestEnable = true; - continue; - } - - if (StartsWithKeyword(line, "Blend")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens)) { - return fail("Blend directive could not be tokenized", humanLine); - } - - for (size_t tokenIndex = 1; tokenIndex < tokens.size(); ++tokenIndex) { - if (!tokens[tokenIndex].empty() && tokens[tokenIndex].back() == ',') { - tokens[tokenIndex].pop_back(); - } - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentPass->hasFixedFunctionState, - currentPass->fixedFunctionState); - if (!TryParseUnityStyleBlendDirective(tokens, currentPass->fixedFunctionState)) { - return fail("Blend directive uses an unsupported factor combination", humanLine); - } - continue; - } - - if (StartsWithKeyword(line, "ColorMask")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens) || - (tokens.size() != 2u && tokens.size() != 3u)) { - return fail("ColorMask directive uses an unsupported channel mask", humanLine); - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentPass->hasFixedFunctionState, - currentPass->fixedFunctionState); - if (!TryParseUnityStyleColorMask(tokens[1], currentPass->fixedFunctionState.colorWriteMask)) { - return fail("ColorMask directive uses an unsupported channel mask", humanLine); - } - 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,