diff --git a/CMakeLists.txt b/CMakeLists.txt index 0331c7c8..413e4f7d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -242,7 +242,9 @@ endif() add_subdirectory(engine) add_subdirectory(managed) -add_subdirectory(editor) +if(EXISTS "${CMAKE_SOURCE_DIR}/editor/CMakeLists.txt") + add_subdirectory(editor) +endif() add_subdirectory(new_editor) add_subdirectory(mvs/RenderDoc) add_subdirectory(tests) diff --git a/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/Color.hlsl b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/Color.hlsl new file mode 100644 index 00000000..7169b430 --- /dev/null +++ b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/Color.hlsl @@ -0,0 +1,14 @@ +#ifndef XC_CORE_COLOR_INCLUDED +#define XC_CORE_COLOR_INCLUDED + +float3 XcLinearToSrgb(float3 color) +{ + return pow(saturate(color), 1.0f / 2.2f); +} + +float3 XcSrgbToLinear(float3 color) +{ + return pow(saturate(color), 2.2f); +} + +#endif diff --git a/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/Packing.hlsl b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/Packing.hlsl new file mode 100644 index 00000000..8892f32a --- /dev/null +++ b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/Packing.hlsl @@ -0,0 +1,14 @@ +#ifndef XC_CORE_PACKING_INCLUDED +#define XC_CORE_PACKING_INCLUDED + +float2 XcPackNormalOctahedron(float3 normalWS) +{ + normalWS /= max(dot(abs(normalWS), 1.0f.xxx), 1e-5f); + float2 packed = normalWS.xy; + if (normalWS.z < 0.0f) { + packed = (1.0f - abs(packed.yx)) * (packed.xy >= 0.0f ? 1.0f : -1.0f); + } + return packed * 0.5f + 0.5f; +} + +#endif diff --git a/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/ShaderVariables.hlsl b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/ShaderVariables.hlsl new file mode 100644 index 00000000..9cf1b480 --- /dev/null +++ b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/ShaderVariables.hlsl @@ -0,0 +1,18 @@ +#ifndef XC_CORE_SHADER_VARIABLES_INCLUDED +#define XC_CORE_SHADER_VARIABLES_INCLUDED + +cbuffer PerObjectConstants +{ + float4x4 gProjectionMatrix; + float4x4 gViewMatrix; + float4x4 gModelMatrix; + float4x4 gNormalMatrix; +}; + +cbuffer MaterialConstants +{ + float4 gBaseColorFactor; + float4 gAlphaCutoffParams; +}; + +#endif diff --git a/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl new file mode 100644 index 00000000..cd633b90 --- /dev/null +++ b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl @@ -0,0 +1,31 @@ +#ifndef XC_CORE_SPACE_TRANSFORMS_INCLUDED +#define XC_CORE_SPACE_TRANSFORMS_INCLUDED + +#include "Packages/com.xcengine.render-pipelines.core/ShaderLibrary/ShaderVariables.hlsl" + +float4 TransformObjectToWorld(float3 positionOS) +{ + return mul(gModelMatrix, float4(positionOS, 1.0f)); +} + +float4 TransformWorldToView(float4 positionWS) +{ + return mul(gViewMatrix, positionWS); +} + +float4 TransformWorldToHClip(float4 positionWS) +{ + return mul(gProjectionMatrix, TransformWorldToView(positionWS)); +} + +float4 TransformObjectToHClip(float3 positionOS) +{ + return TransformWorldToHClip(TransformObjectToWorld(positionOS)); +} + +float3 TransformObjectToWorldNormal(float3 normalOS) +{ + return mul((float3x3)gNormalMatrix, normalOS); +} + +#endif diff --git a/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/TextureSampling.hlsl b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/TextureSampling.hlsl new file mode 100644 index 00000000..6921382f --- /dev/null +++ b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/TextureSampling.hlsl @@ -0,0 +1,9 @@ +#ifndef XC_CORE_TEXTURE_SAMPLING_INCLUDED +#define XC_CORE_TEXTURE_SAMPLING_INCLUDED + +float4 SampleTexture2D(Texture2D textureObject, SamplerState samplerState, float2 uv) +{ + return textureObject.Sample(samplerState, uv); +} + +#endif diff --git a/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl new file mode 100644 index 00000000..03a7c3f6 --- /dev/null +++ b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl @@ -0,0 +1,7 @@ +#ifndef XC_UNIVERSAL_CORE_INCLUDED +#define XC_UNIVERSAL_CORE_INCLUDED + +#include "Packages/com.xcengine.render-pipelines.core/ShaderLibrary/ShaderVariables.hlsl" +#include "Packages/com.xcengine.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl" + +#endif diff --git a/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Input.hlsl b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Input.hlsl new file mode 100644 index 00000000..84b839e5 --- /dev/null +++ b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Input.hlsl @@ -0,0 +1,19 @@ +#ifndef XC_UNIVERSAL_INPUT_INCLUDED +#define XC_UNIVERSAL_INPUT_INCLUDED + +struct Attributes +{ + float3 positionOS : POSITION; + float3 normalOS : NORMAL; + float2 texcoord : TEXCOORD0; +}; + +struct Varyings +{ + float4 positionCS : SV_POSITION; + float3 normalWS : TEXCOORD0; + float2 texcoord : TEXCOORD1; + float3 positionWS : TEXCOORD2; +}; + +#endif diff --git a/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/RealtimeLights.hlsl b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/RealtimeLights.hlsl new file mode 100644 index 00000000..46003210 --- /dev/null +++ b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/RealtimeLights.hlsl @@ -0,0 +1,22 @@ +#ifndef XC_UNIVERSAL_REALTIME_LIGHTS_INCLUDED +#define XC_UNIVERSAL_REALTIME_LIGHTS_INCLUDED + +static const int XC_MAX_ADDITIONAL_LIGHTS = 8; + +struct AdditionalLightData +{ + float4 colorAndIntensity; + float4 positionAndRange; + float4 directionAndType; + float4 spotAnglesAndFlags; +}; + +cbuffer LightingConstants +{ + float4 gMainLightDirectionAndIntensity; + float4 gMainLightColorAndFlags; + float4 gLightingParams; + AdditionalLightData gAdditionalLights[XC_MAX_ADDITIONAL_LIGHTS]; +}; + +#endif diff --git a/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/ShaderPass.hlsl b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/ShaderPass.hlsl new file mode 100644 index 00000000..10ece9dc --- /dev/null +++ b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/ShaderPass.hlsl @@ -0,0 +1,9 @@ +#ifndef XC_UNIVERSAL_SHADER_PASS_INCLUDED +#define XC_UNIVERSAL_SHADER_PASS_INCLUDED + +#define XC_PASS_FORWARD_LIT "ForwardLit" +#define XC_PASS_DEPTH_ONLY "DepthOnly" +#define XC_PASS_SHADOW_CASTER "ShadowCaster" +#define XC_PASS_UNLIT "Unlit" + +#endif diff --git a/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl new file mode 100644 index 00000000..6649025d --- /dev/null +++ b/engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl @@ -0,0 +1,15 @@ +#ifndef XC_UNIVERSAL_SURFACE_INPUT_INCLUDED +#define XC_UNIVERSAL_SURFACE_INPUT_INCLUDED + +#include "Packages/com.xcengine.render-pipelines.core/ShaderLibrary/TextureSampling.hlsl" +#include "Packages/com.xcengine.render-pipelines.core/ShaderLibrary/ShaderVariables.hlsl" + +Texture2D BaseColorTexture; +SamplerState LinearClampSampler; + +float4 SampleBaseColor(float2 uv) +{ + return SampleTexture2D(BaseColorTexture, LinearClampSampler, uv) * gBaseColorFactor; +} + +#endif diff --git a/engine/include/XCEngine/Core/Asset/AssetDatabase.h b/engine/include/XCEngine/Core/Asset/AssetDatabase.h index 12f277d8..19adcd79 100644 --- a/engine/include/XCEngine/Core/Asset/AssetDatabase.h +++ b/engine/include/XCEngine/Core/Asset/AssetDatabase.h @@ -183,7 +183,8 @@ private: bool CollectMaterialDependencies(const Material& material, std::vector& outDependencies) const; bool CollectShaderDependencies(const SourceAssetRecord& sourceRecord, - std::vector& outDependencies) const; + std::vector& outDependencies, + Containers::String* outError = nullptr) const; void ClearLastErrorMessage(); void SetLastErrorMessage(const Containers::String& message); diff --git a/engine/include/XCEngine/Resources/Shader/ShaderLoader.h b/engine/include/XCEngine/Resources/Shader/ShaderLoader.h index 6f7cd8f0..f47e704c 100644 --- a/engine/include/XCEngine/Resources/Shader/ShaderLoader.h +++ b/engine/include/XCEngine/Resources/Shader/ShaderLoader.h @@ -18,6 +18,9 @@ public: ImportSettings* GetDefaultSettings() const override; bool CollectSourceDependencies(const Containers::String& path, Containers::Array& outDependencies) const; + bool CollectSourceDependencies(const Containers::String& path, + Containers::Array& outDependencies, + Containers::String* outError) const; private: ShaderType DetectShaderType(const Containers::String& path, const Containers::String& source); diff --git a/engine/src/Core/Asset/AssetDatabase.cpp b/engine/src/Core/Asset/AssetDatabase.cpp index ed7a78b9..f09ed874 100644 --- a/engine/src/Core/Asset/AssetDatabase.cpp +++ b/engine/src/Core/Asset/AssetDatabase.cpp @@ -3007,7 +3007,11 @@ bool AssetDatabase::ImportShaderAsset(const SourceAssetRecord& sourceRecord, Shader* shader = static_cast(result.resource); PrecompileShaderVariants(absolutePath, *shader); std::vector dependencies; - if (!CollectShaderDependencies(sourceRecord, dependencies)) { + Containers::String dependencyError; + if (!CollectShaderDependencies(sourceRecord, dependencies, &dependencyError)) { + if (!dependencyError.Empty()) { + SetLastErrorMessage(dependencyError); + } delete shader; return false; } @@ -3493,14 +3497,18 @@ bool AssetDatabase::CollectMaterialDependencies( bool AssetDatabase::CollectShaderDependencies( const SourceAssetRecord& sourceRecord, - std::vector& outDependencies) const { + std::vector& outDependencies, + Containers::String* outError) const { outDependencies.clear(); + if (outError != nullptr) { + outError->Clear(); + } ShaderLoader loader; const Containers::String absolutePath = NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr()); Containers::Array dependencyPaths; - if (!loader.CollectSourceDependencies(absolutePath, dependencyPaths)) { + if (!loader.CollectSourceDependencies(absolutePath, dependencyPaths, outError)) { return false; } diff --git a/engine/src/Resources/Shader/Internal/ShaderAuthoringInternal.h b/engine/src/Resources/Shader/Internal/ShaderAuthoringInternal.h index f502c56c..c9a171db 100644 --- a/engine/src/Resources/Shader/Internal/ShaderAuthoringInternal.h +++ b/engine/src/Resources/Shader/Internal/ShaderAuthoringInternal.h @@ -71,11 +71,18 @@ void AppendAuthoringSourceBlock( Containers::String& target, const Containers::String& sourceBlock); -void CollectQuotedIncludeDependencyPaths( +bool CollectQuotedIncludeDependencyPaths( const Containers::String& sourcePath, const Containers::String& sourceText, std::unordered_set& seenPaths, - Containers::Array& outDependencies); + Containers::Array& outDependencies, + Containers::String* outError = nullptr); + +bool ExpandShaderPackageIncludeSource( + const Containers::String& sourcePath, + const Containers::String& sourceText, + Containers::String& outSourceText, + Containers::String* outError = nullptr); bool IsShaderAuthoringPragmaDirective(const std::string& line); diff --git a/engine/src/Resources/Shader/Internal/ShaderAuthoringLoader.cpp b/engine/src/Resources/Shader/Internal/ShaderAuthoringLoader.cpp index 2e3eff67..ce803c41 100644 --- a/engine/src/Resources/Shader/Internal/ShaderAuthoringLoader.cpp +++ b/engine/src/Resources/Shader/Internal/ShaderAuthoringLoader.cpp @@ -2,6 +2,7 @@ #include "../ShaderAuthoringParser.h" #include "../ShaderSourceUtils.h" +#include "ShaderAuthoringInternal.h" #include "ShaderFileUtils.h" #include "ShaderRuntimeBuildUtils.h" @@ -22,7 +23,8 @@ bool CollectShaderAuthoringDependencyPathsRecursive( const std::string& sourceText, std::unordered_set& seenShaderPaths, std::unordered_set& seenDependencyPaths, - Containers::Array& outDependencies) { + Containers::Array& outDependencies, + Containers::String* outError) { const fs::path normalizedShaderPath = fs::path(path.CStr()).lexically_normal(); const std::string shaderKey = normalizedShaderPath.generic_string(); if (!shaderKey.empty() && !seenShaderPaths.insert(shaderKey).second) { @@ -32,16 +34,45 @@ bool CollectShaderAuthoringDependencyPathsRecursive( ShaderIR shaderIR = {}; Containers::String parseError; if (!ParseShaderAuthoring(path, sourceText, shaderIR, &parseError)) { + if (outError != nullptr) { + *outError = parseError; + } return false; } - CollectQuotedIncludeDependencyPaths(path, shaderIR.sharedProgramSource, seenDependencyPaths, outDependencies); + if (!::XCEngine::Resources::Internal::CollectQuotedIncludeDependencyPaths( + path, + shaderIR.sharedProgramSource, + seenDependencyPaths, + outDependencies, + outError)) { + return false; + } for (const ShaderSubShaderIR& subShader : shaderIR.subShaders) { - CollectQuotedIncludeDependencyPaths(path, subShader.sharedProgramSource, seenDependencyPaths, outDependencies); + if (!::XCEngine::Resources::Internal::CollectQuotedIncludeDependencyPaths( + path, + subShader.sharedProgramSource, + seenDependencyPaths, + outDependencies, + outError)) { + return false; + } for (const ShaderPassIR& pass : subShader.passes) { if (!pass.isUsePass) { - CollectQuotedIncludeDependencyPaths(path, pass.sharedProgramSource, seenDependencyPaths, outDependencies); - CollectQuotedIncludeDependencyPaths(path, pass.programSource, seenDependencyPaths, outDependencies); + if (!::XCEngine::Resources::Internal::CollectQuotedIncludeDependencyPaths( + path, + pass.sharedProgramSource, + seenDependencyPaths, + outDependencies, + outError) || + !::XCEngine::Resources::Internal::CollectQuotedIncludeDependencyPaths( + path, + pass.programSource, + seenDependencyPaths, + outDependencies, + outError)) { + return false; + } continue; } @@ -53,6 +84,13 @@ bool CollectShaderAuthoringDependencyPathsRecursive( resolvedUsePassPath)) { Containers::String builtinShaderPath; if (!TryGetBuiltinShaderPathByShaderName(pass.usePassShaderName, builtinShaderPath)) { + if (outError != nullptr) { + *outError = + Containers::String("Failed to resolve shader UsePass: source=") + + path + + ", requested=" + + pass.usePassShaderName; + } return false; } @@ -70,6 +108,13 @@ bool CollectShaderAuthoringDependencyPathsRecursive( if (IsBuiltinShaderPath(resolvedUsePassPath)) { Containers::String builtinAssetPath; if (!TryResolveBuiltinShaderAssetPath(resolvedUsePassPath, builtinAssetPath)) { + if (outError != nullptr) { + *outError = + Containers::String("Failed to resolve builtin shader UsePass asset: source=") + + path + + ", requested=" + + resolvedUsePassPath; + } return false; } @@ -84,6 +129,13 @@ bool CollectShaderAuthoringDependencyPathsRecursive( Containers::String referencedSourceText; if (!ReadShaderTextFile(builtinAssetPath, referencedSourceText)) { + if (outError != nullptr) { + *outError = + Containers::String("Failed to read shader UsePass source: source=") + + path + + ", requested=" + + builtinAssetPath; + } return false; } @@ -92,7 +144,8 @@ bool CollectShaderAuthoringDependencyPathsRecursive( referencedSourceText.CStr(), seenShaderPaths, seenDependencyPaths, - outDependencies)) { + outDependencies, + outError)) { return false; } continue; @@ -109,6 +162,13 @@ bool CollectShaderAuthoringDependencyPathsRecursive( Containers::String referencedSourceText; if (!ReadShaderTextFile(resolvedUsePassPath, referencedSourceText)) { + if (outError != nullptr) { + *outError = + Containers::String("Failed to read shader UsePass source: source=") + + path + + ", requested=" + + resolvedUsePassPath; + } return false; } @@ -117,7 +177,8 @@ bool CollectShaderAuthoringDependencyPathsRecursive( referencedSourceText.CStr(), seenShaderPaths, seenDependencyPaths, - outDependencies)) { + outDependencies, + outError)) { return false; } } @@ -132,7 +193,18 @@ bool CollectShaderAuthoringDependencyPaths( const Containers::String& path, const std::string& sourceText, Containers::Array& outDependencies) { + return CollectShaderAuthoringDependencyPaths(path, sourceText, outDependencies, nullptr); +} + +bool CollectShaderAuthoringDependencyPaths( + const Containers::String& path, + const std::string& sourceText, + Containers::Array& outDependencies, + Containers::String* outError) { outDependencies.Clear(); + if (outError != nullptr) { + outError->Clear(); + } std::unordered_set seenShaderPaths; std::unordered_set seenDependencyPaths; @@ -141,7 +213,8 @@ bool CollectShaderAuthoringDependencyPaths( sourceText, seenShaderPaths, seenDependencyPaths, - outDependencies); + outDependencies, + outError); } LoadResult LoadShaderAuthoring( diff --git a/engine/src/Resources/Shader/Internal/ShaderAuthoringLoader.h b/engine/src/Resources/Shader/Internal/ShaderAuthoringLoader.h index 13bad78d..7d7e65a6 100644 --- a/engine/src/Resources/Shader/Internal/ShaderAuthoringLoader.h +++ b/engine/src/Resources/Shader/Internal/ShaderAuthoringLoader.h @@ -14,6 +14,12 @@ bool CollectShaderAuthoringDependencyPaths( const std::string& sourceText, Containers::Array& outDependencies); +bool CollectShaderAuthoringDependencyPaths( + const Containers::String& path, + const std::string& sourceText, + Containers::Array& outDependencies, + Containers::String* outError); + LoadResult LoadShaderAuthoring( const Containers::String& path, const std::string& sourceText); diff --git a/engine/src/Resources/Shader/Internal/ShaderAuthoringShared.cpp b/engine/src/Resources/Shader/Internal/ShaderAuthoringShared.cpp index 2b91c779..089aaadc 100644 --- a/engine/src/Resources/Shader/Internal/ShaderAuthoringShared.cpp +++ b/engine/src/Resources/Shader/Internal/ShaderAuthoringShared.cpp @@ -1,6 +1,7 @@ #include "ShaderAuthoringInternal.h" #include "../ShaderSourceUtils.h" +#include "ShaderFileUtils.h" #include #include @@ -29,6 +30,225 @@ Containers::String BuildShaderKeywordSetSignature(const ShaderKeywordSet& keywor return signature; } +void SetIncludeResolveError( + const Containers::String& sourcePath, + const Containers::String& includePath, + const Containers::String& includeChain, + Containers::String* outError) { + if (outError == nullptr) { + return; + } + + *outError = BuildShaderDependencyResolutionDiagnostic( + includePath, + sourcePath, + includeChain); +} + +Containers::String NormalizeIncludePathSeparators(const Containers::String& path) { + std::string normalized(path.CStr()); + for (char& ch : normalized) { + if (ch == '\\') { + ch = '/'; + } + } + + return Containers::String(normalized.c_str()); +} + +bool IsShaderPackageIncludePath(const Containers::String& includePath) { + return NormalizeIncludePathSeparators(includePath).StartsWith("Packages/"); +} + +bool TryParseQuotedIncludePath( + const std::string& rawLine, + Containers::String& outIncludePath) { + const std::string line = TrimCopy(StripAuthoringLineComment(rawLine)); + if (line.rfind("#include", 0) != 0) { + return false; + } + + const size_t firstQuote = line.find('"'); + if (firstQuote == std::string::npos) { + return false; + } + + const size_t secondQuote = line.find('"', firstQuote + 1); + if (secondQuote == std::string::npos || secondQuote <= firstQuote + 1) { + return false; + } + + outIncludePath = line.substr(firstQuote + 1, secondQuote - firstQuote - 1).c_str(); + return true; +} + +Containers::String BuildIncludeChainText( + const std::vector& includeChain, + const Containers::String& requestedIncludePath) { + Containers::String text; + for (const Containers::String& chainEntry : includeChain) { + if (chainEntry.Empty()) { + continue; + } + + if (!text.Empty()) { + text += " -> "; + } + text += chainEntry; + } + + if (!requestedIncludePath.Empty()) { + if (!text.Empty()) { + text += " -> "; + } + text += requestedIncludePath; + } + + return text; +} + +bool ExpandShaderPackageIncludeSourceRecursive( + const Containers::String& sourcePath, + const Containers::String& sourceText, + std::unordered_set& activeIncludePaths, + std::vector& includeChain, + Containers::String& outSourceText, + Containers::String* outError) { + outSourceText.Clear(); + + std::istringstream stream(ToStdString(sourceText)); + std::string rawLine; + while (std::getline(stream, rawLine)) { + Containers::String includePath; + if (!TryParseQuotedIncludePath(rawLine, includePath) || + !IsShaderPackageIncludePath(includePath)) { + outSourceText += rawLine.c_str(); + outSourceText += '\n'; + continue; + } + + const Containers::String resolvedPath = ResolveShaderDependencyPath(includePath, sourcePath); + if (resolvedPath.Empty()) { + SetIncludeResolveError( + sourcePath, + includePath, + BuildIncludeChainText(includeChain, includePath), + outError); + return false; + } + + const std::string activePathKey = ToStdString(resolvedPath); + if (!activeIncludePaths.insert(activePathKey).second) { + if (outError != nullptr) { + *outError = + Containers::String("Cyclic shader package include: requested=") + + includePath + + ", source=" + + sourcePath + + ", resolved=" + + resolvedPath + + ", includeChain=" + + BuildIncludeChainText(includeChain, includePath); + } + return false; + } + + Containers::String includedSourceText; + if (!ReadShaderTextFile(resolvedPath, includedSourceText)) { + activeIncludePaths.erase(activePathKey); + SetIncludeResolveError( + sourcePath, + includePath, + BuildIncludeChainText(includeChain, includePath), + outError); + return false; + } + + Containers::String expandedIncludedSource; + includeChain.push_back(resolvedPath); + if (!ExpandShaderPackageIncludeSourceRecursive( + resolvedPath, + includedSourceText, + activeIncludePaths, + includeChain, + expandedIncludedSource, + outError)) { + includeChain.pop_back(); + activeIncludePaths.erase(activePathKey); + return false; + } + includeChain.pop_back(); + activeIncludePaths.erase(activePathKey); + + outSourceText += expandedIncludedSource; + if (!expandedIncludedSource.Empty() && + expandedIncludedSource[expandedIncludedSource.Length() - 1] != '\n') { + outSourceText += '\n'; + } + } + + return true; +} + +bool CollectQuotedIncludeDependencyPathsRecursive( + const Containers::String& sourcePath, + const Containers::String& sourceText, + std::unordered_set& seenPaths, + std::vector& includeChain, + Containers::Array& outDependencies, + Containers::String* outError) { + std::istringstream stream(ToStdString(sourceText)); + std::string rawLine; + while (std::getline(stream, rawLine)) { + Containers::String includePath; + if (!TryParseQuotedIncludePath(rawLine, includePath)) { + continue; + } + + const Containers::String resolvedPath = ResolveShaderDependencyPath(includePath, sourcePath); + if (resolvedPath.Empty()) { + SetIncludeResolveError( + sourcePath, + includePath, + BuildIncludeChainText(includeChain, includePath), + outError); + return false; + } + + const std::string key = ToStdString(resolvedPath); + if (key.empty() || !seenPaths.insert(key).second) { + continue; + } + + outDependencies.PushBack(resolvedPath); + + Containers::String includedSourceText; + if (!ReadShaderTextFile(resolvedPath, includedSourceText)) { + SetIncludeResolveError( + sourcePath, + includePath, + BuildIncludeChainText(includeChain, includePath), + outError); + return false; + } + + includeChain.push_back(resolvedPath); + if (!CollectQuotedIncludeDependencyPathsRecursive( + resolvedPath, + includedSourceText, + seenPaths, + includeChain, + outDependencies, + outError)) { + includeChain.pop_back(); + return false; + } + includeChain.pop_back(); + } + + return true; +} + } // namespace void AppendAuthoringSourceBlock( @@ -44,36 +264,42 @@ void AppendAuthoringSourceBlock( target += sourceBlock; } -void CollectQuotedIncludeDependencyPaths( +bool 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; - } + Containers::Array& outDependencies, + Containers::String* outError) { + std::vector includeChain; + includeChain.push_back(sourcePath); + return CollectQuotedIncludeDependencyPathsRecursive( + sourcePath, + sourceText, + seenPaths, + includeChain, + outDependencies, + outError); +} - 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 ExpandShaderPackageIncludeSource( + const Containers::String& sourcePath, + const Containers::String& sourceText, + Containers::String& outSourceText, + Containers::String* outError) { + if (outError != nullptr) { + outError->Clear(); } + + std::unordered_set activeIncludePaths; + std::vector includeChain; + includeChain.push_back(sourcePath); + return ExpandShaderPackageIncludeSourceRecursive( + sourcePath, + sourceText, + activeIncludePaths, + includeChain, + outSourceText, + outError); } bool IsShaderAuthoringPragmaDirective(const std::string& line) { diff --git a/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.cpp b/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.cpp index eba2da3f..4ad70347 100644 --- a/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.cpp +++ b/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.cpp @@ -395,10 +395,15 @@ void AppendScannedAuthoringResourceBindings( } } -ShaderPass BuildConcretePass( +bool BuildConcretePass( + const Containers::String& path, const ShaderIR& shaderIR, const ShaderSubShaderIR& subShader, - const ShaderPassIR& pass) { + const ShaderPassIR& pass, + ShaderPass& outShaderPass, + Containers::String& outError) { + outError.Clear(); + ShaderPass shaderPass = {}; shaderPass.name = pass.name; shaderPass.hasFixedFunctionState = pass.hasFixedFunctionState; @@ -426,7 +431,8 @@ ShaderPass BuildConcretePass( } } - return shaderPass; + outShaderPass = std::move(shaderPass); + return true; } Containers::String combinedSource; @@ -435,7 +441,16 @@ ShaderPass BuildConcretePass( AppendAuthoringSourceBlock(combinedSource, pass.sharedProgramSource); AppendAuthoringSourceBlock(combinedSource, pass.programSource); - const Containers::String strippedCombinedSource = StripShaderAuthoringPragmas(combinedSource); + Containers::String expandedCombinedSource; + if (!Internal::ExpandShaderPackageIncludeSource( + path, + combinedSource, + expandedCombinedSource, + &outError)) { + return false; + } + + const Containers::String strippedCombinedSource = StripShaderAuthoringPragmas(expandedCombinedSource); if (shaderPass.resources.Empty()) { Containers::Array defaultBindings; if (::XCEngine::Rendering::TryBuildBuiltinPassDefaultResourceBindings(shaderPass, defaultBindings)) { @@ -504,7 +519,8 @@ ShaderPass BuildConcretePass( shaderPass.variants.PushBack(fragmentVariant); } - return shaderPass; + outShaderPass = std::move(shaderPass); + return true; } bool TryLoadReferencedUsePassShader( @@ -764,7 +780,17 @@ LoadResult BuildShaderFromIRInternal( continue; } - ShaderPass concretePass = BuildConcretePass(shaderIR, subShader, pass); + ShaderPass concretePass = {}; + Containers::String concretePassError; + if (!BuildConcretePass( + path, + shaderIR, + subShader, + pass, + concretePass, + concretePassError)) { + return LoadResult(concretePassError); + } const std::string passKey = ToStdString(concretePass.name); if (passKey.empty()) { return LoadResult("Shader authoring produced a pass with an empty name"); diff --git a/engine/src/Resources/Shader/ShaderLoader.cpp b/engine/src/Resources/Shader/ShaderLoader.cpp index 5f041381..61d6e2ef 100644 --- a/engine/src/Resources/Shader/ShaderLoader.cpp +++ b/engine/src/Resources/Shader/ShaderLoader.cpp @@ -70,7 +70,17 @@ ImportSettings* ShaderLoader::GetDefaultSettings() const { bool ShaderLoader::CollectSourceDependencies( const Containers::String& path, Containers::Array& outDependencies) const { + return CollectSourceDependencies(path, outDependencies, nullptr); +} + +bool ShaderLoader::CollectSourceDependencies( + const Containers::String& path, + Containers::Array& outDependencies, + Containers::String* outError) const { outDependencies.Clear(); + if (outError != nullptr) { + outError->Clear(); + } if (IsBuiltinShaderPath(path)) { return true; @@ -83,15 +93,23 @@ bool ShaderLoader::CollectSourceDependencies( const Containers::Array data = ReadShaderFileData(path); if (data.Empty()) { + if (outError != nullptr) { + *outError = Containers::String("Failed to read shader file: ") + path; + } return false; } const std::string sourceText = ToStdStringFromBytes(data); if (!LooksLikeShaderAuthoring(sourceText)) { + if (outError != nullptr) { + *outError = + Containers::String("Shader authoring file must start with a Shader declaration: ") + + path; + } return false; } - return CollectShaderAuthoringDependencyPaths(path, sourceText, outDependencies); + return CollectShaderAuthoringDependencyPaths(path, sourceText, outDependencies, outError); } ShaderType ShaderLoader::DetectShaderType(const Containers::String& path, const Containers::String& source) { diff --git a/engine/src/Resources/Shader/ShaderSourceUtils.cpp b/engine/src/Resources/Shader/ShaderSourceUtils.cpp index 44d09b3d..1f2d30b0 100644 --- a/engine/src/Resources/Shader/ShaderSourceUtils.cpp +++ b/engine/src/Resources/Shader/ShaderSourceUtils.cpp @@ -14,10 +14,66 @@ std::string ToStdString(const Containers::String& value) { namespace { +constexpr const char* kBuiltinShaderPackagePrefix = "Packages/"; +constexpr const char* kBuiltinShaderPackageRootRelativePath = + "engine/assets/builtin/shaders/Packages"; + +Containers::String NormalizeDependencyPathSeparators(const Containers::String& path) { + std::string normalized(path.CStr()); + for (char& ch : normalized) { + if (ch == '\\') { + ch = '/'; + } + } + + return Containers::String(normalized.c_str()); +} + Containers::String NormalizePathString(const std::filesystem::path& path) { return Containers::String(path.lexically_normal().generic_string().c_str()); } +bool TryResolvePathFromAnchor( + const std::filesystem::path& anchor, + const std::filesystem::path& relativePath, + std::filesystem::path& outPath) { + if (anchor.empty()) { + return false; + } + + std::error_code ec; + std::filesystem::path current = anchor.lexically_normal(); + if (std::filesystem::is_regular_file(current, ec)) { + current = current.parent_path(); + } + + while (!current.empty()) { + const std::filesystem::path candidate = (current / relativePath).lexically_normal(); + if (std::filesystem::exists(candidate, ec)) { + outPath = candidate; + return true; + } + + const std::filesystem::path parent = current.parent_path(); + if (parent == current) { + break; + } + current = parent; + } + + return false; +} + +bool ContainsParentTraversal(const std::filesystem::path& path) { + for (const std::filesystem::path& part : path) { + if (part == "..") { + return true; + } + } + + return false; +} + } // namespace size_t SkipWhitespace(const std::string& text, size_t pos) { @@ -229,9 +285,16 @@ Containers::String ResolveShaderDependencyPath( return dependencyPath; } - const std::filesystem::path dependencyFsPath(dependencyPath.CStr()); + const Containers::String normalizedDependencyPath = + NormalizeDependencyPathSeparators(dependencyPath); + + if (normalizedDependencyPath.StartsWith(kBuiltinShaderPackagePrefix)) { + return ResolveBuiltinShaderPackageDependencyPath(normalizedDependencyPath); + } + + const std::filesystem::path dependencyFsPath(normalizedDependencyPath.CStr()); if (dependencyFsPath.is_absolute()) { - return NormalizePathString(dependencyFsPath); + return Containers::String(); } const std::filesystem::path sourceFsPath(sourcePath.CStr()); @@ -250,5 +313,110 @@ Containers::String ResolveShaderDependencyPath( return NormalizePathString(sourceFsPath.parent_path() / dependencyFsPath); } +Containers::String BuildShaderDependencyResolutionDiagnostic( + const Containers::String& dependencyPath, + const Containers::String& sourcePath, + const Containers::String& includeChain) { + const Containers::String normalizedDependencyPath = + NormalizeDependencyPathSeparators(dependencyPath); + + Containers::String diagnostic = + Containers::String("Failed to resolve shader include: requested=") + + dependencyPath + + ", normalized=" + + normalizedDependencyPath + + ", source=" + + sourcePath + + ", builtinPackageRoot=" + + kBuiltinShaderPackageRootRelativePath; + + if (!includeChain.Empty()) { + diagnostic += ", includeChain="; + diagnostic += includeChain; + } + + std::filesystem::path normalizedCandidate; + if (normalizedDependencyPath.StartsWith(kBuiltinShaderPackagePrefix)) { + const std::filesystem::path dependencyFsPath(normalizedDependencyPath.CStr()); + const std::filesystem::path packageRelativePath = + dependencyFsPath.lexically_relative("Packages"); + if (!packageRelativePath.empty() && + !packageRelativePath.is_absolute() && + !ContainsParentTraversal(packageRelativePath)) { + normalizedCandidate = + (std::filesystem::path(kBuiltinShaderPackageRootRelativePath) / + packageRelativePath).lexically_normal(); + } + } else { + const std::filesystem::path dependencyFsPath(normalizedDependencyPath.CStr()); + const std::filesystem::path sourceFsPath(sourcePath.CStr()); + if (!dependencyFsPath.is_absolute()) { + normalizedCandidate = + (sourceFsPath.parent_path() / dependencyFsPath).lexically_normal(); + } + } + + diagnostic += ", normalizedCandidate="; + diagnostic += normalizedCandidate.empty() + ? Containers::String("") + : NormalizePathString(normalizedCandidate); + + diagnostic += ", searchedAnchors=[sourceRoot="; + diagnostic += NormalizePathString(std::filesystem::path(__FILE__)); + + const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot(); + diagnostic += ", resourceRoot="; + diagnostic += resourceRoot.Empty() ? Containers::String("") : resourceRoot; + + std::error_code ec; + diagnostic += ", currentPath="; + diagnostic += ec + ? Containers::String("") + : NormalizePathString(std::filesystem::current_path(ec)); + diagnostic += "]"; + + return diagnostic; +} + +Containers::String ResolveBuiltinShaderPackageDependencyPath( + const Containers::String& dependencyPath) { + const Containers::String normalizedDependencyPath = + NormalizeDependencyPathSeparators(dependencyPath); + + if (!normalizedDependencyPath.StartsWith(kBuiltinShaderPackagePrefix)) { + return Containers::String(); + } + + const std::filesystem::path dependencyFsPath(normalizedDependencyPath.CStr()); + const std::filesystem::path packageRelativePath = dependencyFsPath.lexically_relative("Packages"); + if (packageRelativePath.empty() || + packageRelativePath.is_absolute() || + ContainsParentTraversal(packageRelativePath)) { + return Containers::String(); + } + + const std::filesystem::path relativePath = + std::filesystem::path(kBuiltinShaderPackageRootRelativePath) / + packageRelativePath; + + std::filesystem::path resolvedPath; + if (TryResolvePathFromAnchor(std::filesystem::path(__FILE__), relativePath, resolvedPath)) { + return NormalizePathString(resolvedPath); + } + + const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot(); + if (!resourceRoot.Empty() && + TryResolvePathFromAnchor(std::filesystem::path(resourceRoot.CStr()), relativePath, resolvedPath)) { + return NormalizePathString(resolvedPath); + } + + std::error_code ec; + if (TryResolvePathFromAnchor(std::filesystem::current_path(ec), relativePath, resolvedPath)) { + return NormalizePathString(resolvedPath); + } + + return Containers::String(); +} + } // namespace Resources } // namespace XCEngine diff --git a/engine/src/Resources/Shader/ShaderSourceUtils.h b/engine/src/Resources/Shader/ShaderSourceUtils.h index 5dda50c7..a230fefa 100644 --- a/engine/src/Resources/Shader/ShaderSourceUtils.h +++ b/engine/src/Resources/Shader/ShaderSourceUtils.h @@ -29,5 +29,13 @@ Containers::String ResolveShaderDependencyPath( const Containers::String& dependencyPath, const Containers::String& sourcePath); +Containers::String BuildShaderDependencyResolutionDiagnostic( + const Containers::String& dependencyPath, + const Containers::String& sourcePath, + const Containers::String& includeChain = Containers::String()); + +Containers::String ResolveBuiltinShaderPackageDependencyPath( + const Containers::String& dependencyPath); + } // namespace Resources } // namespace XCEngine diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8b137891..74d9da79 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1 +1,12 @@ +add_executable(shader_tests + Resources/Shader/test_shader_dependencies.cpp +) + +target_link_libraries(shader_tests PRIVATE XCEngine) + +set_target_properties(shader_tests PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests" +) + +add_test(NAME shader_tests COMMAND shader_tests) diff --git a/tests/Resources/Shader/test_shader_dependencies.cpp b/tests/Resources/Shader/test_shader_dependencies.cpp new file mode 100644 index 00000000..55992b79 --- /dev/null +++ b/tests/Resources/Shader/test_shader_dependencies.cpp @@ -0,0 +1,1227 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +class ScopedCurrentPath { +public: + explicit ScopedCurrentPath(const std::filesystem::path& path) + : m_previousPath(std::filesystem::current_path()) { + std::filesystem::current_path(path); + } + + ScopedCurrentPath(const ScopedCurrentPath&) = delete; + ScopedCurrentPath& operator=(const ScopedCurrentPath&) = delete; + + ~ScopedCurrentPath() { + std::error_code ec; + std::filesystem::current_path(m_previousPath, ec); + } + +private: + std::filesystem::path m_previousPath; +}; + +bool ContainsPathSuffix( + const XCEngine::Containers::Array& paths, + const char* suffix) { + for (size_t index = 0; index < paths.Size(); ++index) { + const std::string path = paths[index].CStr(); + if (path.ends_with(suffix)) { + return true; + } + } + + return false; +} + +bool WriteTextFile(const std::filesystem::path& path, const char* text) { + std::filesystem::create_directories(path.parent_path()); + std::ofstream stream(path, std::ios::binary); + if (!stream) { + return false; + } + + stream << text; + return true; +} + +std::vector SplitTabFields(const std::string& line) { + std::vector fields; + size_t fieldStart = 0; + while (fieldStart <= line.size()) { + const size_t tabPos = line.find('\t', fieldStart); + if (tabPos == std::string::npos) { + fields.push_back(line.substr(fieldStart)); + break; + } + + fields.push_back(line.substr(fieldStart, tabPos - fieldStart)); + fieldStart = tabPos + 1; + } + + return fields; +} + +bool LoadSourceDbShaderRecord( + const std::filesystem::path& projectRoot, + const char* relativePath, + std::vector& outFields) { + std::ifstream input(projectRoot / "Library/assets.db"); + if (!input.is_open()) { + std::cerr << "failed to open source asset db\n"; + return false; + } + + std::string line; + while (std::getline(input, line)) { + if (line.empty() || line[0] == '#') { + continue; + } + + std::vector fields = SplitTabFields(line); + if (fields.size() >= 10 && fields[1] == relativePath) { + outFields = std::move(fields); + return true; + } + } + + std::cerr << "source asset db did not contain record: " << relativePath << "\n"; + return false; +} + +XCEngine::Core::uint64 GetFileSizeValue(const std::filesystem::path& path) { + std::error_code ec; + const auto size = std::filesystem::file_size(path, ec); + return ec ? 0 : static_cast(size); +} + +XCEngine::Core::uint64 GetFileWriteTimeValue(const std::filesystem::path& path) { + std::error_code ec; + const auto writeTime = std::filesystem::last_write_time(path, ec); + if (ec) { + return 0; + } + + return static_cast(writeTime.time_since_epoch().count()); +} + +bool WriteMinimalCurrentShaderArtifact(const std::filesystem::path& artifactPath) { + XCEngine::Resources::ShaderArtifactFileHeader fileHeader = {}; + XCEngine::Resources::ShaderArtifactHeader shaderHeader = {}; + + XCEngine::Containers::Array payload; + payload.Resize(sizeof(fileHeader) + sizeof(shaderHeader)); + std::memcpy(payload.Data(), &fileHeader, sizeof(fileHeader)); + std::memcpy( + payload.Data() + sizeof(fileHeader), + &shaderHeader, + sizeof(shaderHeader)); + + XCEngine::Resources::ArtifactContainerEntry entry = {}; + entry.name = "main"; + entry.resourceType = XCEngine::Resources::ResourceType::Shader; + entry.localID = XCEngine::Resources::kMainAssetLocalID; + entry.payload = std::move(payload); + + XCEngine::Resources::ArtifactContainerWriter writer; + writer.AddEntry(std::move(entry)); + XCEngine::Containers::String error; + if (!writer.WriteToFile(artifactPath.generic_string().c_str(), &error)) { + std::cerr << "failed to write minimal shader artifact: " + << error.CStr() << "\n"; + return false; + } + + return true; +} + +bool WriteShaderArtifactDbRecord( + const std::filesystem::path& projectRoot, + const std::vector& sourceFields, + const std::filesystem::path& dependencyPath, + XCEngine::Core::uint64 dependencyFileSize, + XCEngine::Core::uint64 dependencyWriteTime, + const std::string& artifactKey, + const std::string& mainArtifactPath) { + if (sourceFields.size() < 10) { + return false; + } + + const std::filesystem::path artifactDbPath = projectRoot / "Library/artifacts.db"; + std::filesystem::create_directories(artifactDbPath.parent_path()); + std::ofstream output(artifactDbPath, std::ios::out | std::ios::trunc); + if (!output.is_open()) { + std::cerr << "failed to write artifact db: " << artifactDbPath << "\n"; + return false; + } + + output << "# schema=2\n"; + output << "# artifactKey\tassetGuid\timporter\tversion\ttype\tartifactDir\tmainArtifact\tsourceHash\tmetaHash\tsize\twriteTime\tmainLocalID\tstorageKind\tmainEntryName\t(depPath\tdepHash\tdepSize\tdepWriteTime)...\n"; + output << artifactKey << '\t' + << sourceFields[0] << '\t' + << "ShaderImporter" << '\t' + << "8" << '\t' + << static_cast(XCEngine::Resources::ResourceType::Shader) << '\t' + << std::filesystem::path(mainArtifactPath).parent_path().generic_string() << '\t' + << mainArtifactPath << '\t' + << sourceFields[7] << '\t' + << sourceFields[6] << '\t' + << sourceFields[8] << '\t' + << sourceFields[9] << '\t' + << XCEngine::Resources::kMainAssetLocalID << '\t' + << static_cast( + XCEngine::Resources::ArtifactStorageKind::SingleFileContainer) << '\t' + << "main" << '\t' + << dependencyPath.lexically_normal().generic_string() << '\t' + << "dependency-hash-not-used-by-currentness-check" << '\t' + << dependencyFileSize << '\t' + << dependencyWriteTime << '\n'; + + return static_cast(output); +} + +bool CollectDependencies( + const std::filesystem::path& shaderPath, + XCEngine::Containers::Array& dependencies) { + XCEngine::Resources::ShaderLoader loader; + return loader.CollectSourceDependencies(shaderPath.generic_string().c_str(), dependencies); +} + +bool CollectDependencies( + const std::filesystem::path& shaderPath, + XCEngine::Containers::Array& dependencies, + XCEngine::Containers::String& error) { + XCEngine::Resources::ShaderLoader loader; + return loader.CollectSourceDependencies(shaderPath.generic_string().c_str(), dependencies, &error); +} + +std::string NarrowWideString(const std::wstring& value) { + std::string result; + result.reserve(value.size()); + for (wchar_t ch : value) { + result.push_back(static_cast(ch)); + } + + return result; +} + +bool RequireDependencySuffixes( + const XCEngine::Containers::Array& dependencies, + const std::vector& suffixes) { + for (const char* suffix : suffixes) { + if (ContainsPathSuffix(dependencies, suffix)) { + continue; + } + + std::cerr << "missing dependency suffix: " << suffix << "\n"; + for (const auto& dependency : dependencies) { + std::cerr << "dependency: " << dependency.CStr() << "\n"; + } + return false; + } + + return true; +} + +bool RequireNoDependencyPrefix( + const XCEngine::Containers::Array& dependencies, + const std::filesystem::path& prefixPath) { + const std::string prefix = prefixPath.lexically_normal().generic_string(); + for (const auto& dependency : dependencies) { + const std::string path = std::filesystem::path(dependency.CStr()) + .lexically_normal() + .generic_string(); + if (path.rfind(prefix, 0) == 0) { + std::cerr << "dependency unexpectedly resolved under shadow package root: " + << path << "\n"; + return false; + } + } + + return true; +} + +bool RequireDependencySuffixUnderPrefix( + const XCEngine::Containers::Array& dependencies, + const char* suffix, + const std::filesystem::path& prefixPath) { + const std::string prefix = prefixPath.lexically_normal().generic_string(); + bool sawSuffix = false; + for (const auto& dependency : dependencies) { + const std::string path = std::filesystem::path(dependency.CStr()) + .lexically_normal() + .generic_string(); + if (!path.ends_with(suffix)) { + continue; + } + + sawSuffix = true; + if (path.rfind(prefix, 0) == 0) { + return true; + } + + std::cerr << "dependency suffix resolved outside expected prefix: " + << path << " prefix=" << prefix << "\n"; + } + + if (!sawSuffix) { + std::cerr << "missing dependency suffix under prefix: " << suffix << "\n"; + } + + for (const auto& dependency : dependencies) { + std::cerr << "dependency: " << dependency.CStr() << "\n"; + } + return false; +} + +bool CompileD3D12ShaderVariant( + const std::filesystem::path& shaderPath, + const XCEngine::Resources::ShaderPass& pass, + const XCEngine::Resources::ShaderStageVariant& variant) { + XCEngine::RHI::ShaderCompileDesc compileDesc = {}; + XCEngine::Rendering::Internal::ApplyShaderStageVariant( + shaderPath.generic_string().c_str(), + pass, + XCEngine::Resources::ShaderBackend::D3D12, + variant, + compileDesc); + + std::vector macroNames; + std::vector macroDefinitions; + std::vector macros; + macroNames.reserve(compileDesc.macros.size()); + macroDefinitions.reserve(compileDesc.macros.size()); + macros.reserve(compileDesc.macros.size() + 1u); + for (const XCEngine::RHI::ShaderCompileMacro& macro : compileDesc.macros) { + macroNames.push_back(NarrowWideString(macro.name)); + macroDefinitions.push_back(NarrowWideString(macro.definition)); + } + for (size_t macroIndex = 0; macroIndex < macroNames.size(); ++macroIndex) { + D3D_SHADER_MACRO d3dMacro = {}; + d3dMacro.Name = macroNames[macroIndex].c_str(); + d3dMacro.Definition = + macroDefinitions[macroIndex].empty() ? "1" : macroDefinitions[macroIndex].c_str(); + macros.push_back(d3dMacro); + } + macros.push_back({ nullptr, nullptr }); + + const std::string entryPoint = NarrowWideString(compileDesc.entryPoint); + const std::string profile = NarrowWideString(compileDesc.profile); + + XCEngine::RHI::D3D12Shader compiledShader; + if (!compiledShader.Compile( + compileDesc.source.data(), + compileDesc.source.size(), + compileDesc.fileName.empty() ? nullptr : compileDesc.fileName.c_str(), + macros.data(), + entryPoint.empty() ? nullptr : entryPoint.c_str(), + profile.empty() ? nullptr : profile.c_str())) { + std::cerr << "D3D12 compile failed for entry=" << entryPoint << " profile=" << profile << "\n"; + return false; + } + + if (compiledShader.GetBytecodeSize() == 0) { + std::cerr << "D3D12 compile produced empty bytecode for entry=" << entryPoint << "\n"; + return false; + } + + return true; +} + +bool RequirePackageShaderLoadAndD3D12Compile(const std::filesystem::path& shaderPath) { + XCEngine::Resources::ShaderLoader loader; + XCEngine::Resources::LoadResult result = + loader.Load(shaderPath.generic_string().c_str()); + if (!result || result.resource == nullptr) { + std::cerr << "failed to load package compile shader: " + << result.errorMessage.CStr() << "\n"; + return false; + } + + std::unique_ptr shader( + static_cast(result.resource)); + const XCEngine::Resources::ShaderPass* pass = shader->FindPass("PackageCompile"); + if (pass == nullptr) { + std::cerr << "loaded package compile shader is missing PackageCompile pass\n"; + return false; + } + + const XCEngine::Resources::ShaderStageVariant* vertexVariant = + shader->FindVariant( + "PackageCompile", + XCEngine::Resources::ShaderType::Vertex, + XCEngine::Resources::ShaderBackend::Generic); + const XCEngine::Resources::ShaderStageVariant* fragmentVariant = + shader->FindVariant( + "PackageCompile", + XCEngine::Resources::ShaderType::Fragment, + XCEngine::Resources::ShaderBackend::Generic); + if (vertexVariant == nullptr || fragmentVariant == nullptr) { + std::cerr << "loaded package compile shader is missing graphics variants\n"; + return false; + } + + const std::string vertexSource = vertexVariant->sourceCode.CStr(); + if (vertexSource.find("Packages/") != std::string::npos || + vertexSource.find("TransformObjectToHClip") == std::string::npos || + vertexSource.find("XC_UNIVERSAL_CORE_INCLUDED") == std::string::npos) { + std::cerr << "package include was not expanded into compile-ready source\n"; + return false; + } + + return CompileD3D12ShaderVariant(shaderPath, *pass, *vertexVariant) && + CompileD3D12ShaderVariant(shaderPath, *pass, *fragmentVariant); +} + +bool RequirePackageDependencyCurrentPathFallback( + const std::filesystem::path& projectRoot) { + std::error_code cleanupError; + std::filesystem::remove_all(projectRoot, cleanupError); + + const char* includePath = + "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/CurrentPathFallbackOnly_20260425.hlsl"; + const char* resolvedSuffix = + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/CurrentPathFallbackOnly_20260425.hlsl"; + const std::filesystem::path shaderPath = + projectRoot / "current_path_fallback.shader"; + const std::filesystem::path dependencyPath = + projectRoot / resolvedSuffix; + + const std::string shaderSource = + std::string("Shader \"Tests/CurrentPathFallback\"\n") + + "{\n" + " HLSLINCLUDE\n" + " #include \"" + includePath + "\"\n" + " ENDHLSL\n" + " SubShader\n" + " {\n" + " }\n" + "}\n"; + + if (!WriteTextFile(shaderPath, shaderSource.c_str()) || + !WriteTextFile(dependencyPath, "float4 CurrentPathFallbackOnlyValue() { return 1.0f.xxxx; }\n")) { + std::cerr << "failed to prepare current-path package fallback fixtures\n"; + return false; + } + + XCEngine::Containers::Array dependencies; + XCEngine::Containers::String dependencyError; + { + ScopedCurrentPath scopedCurrentPath(projectRoot); + if (!CollectDependencies(shaderPath, dependencies, dependencyError)) { + std::cerr << "current-path package fallback dependency collection failed: " + << dependencyError.CStr() << "\n"; + return false; + } + } + + return RequireDependencySuffixUnderPrefix( + dependencies, + resolvedSuffix, + projectRoot); +} + +bool RequireShaderArtifactLoads( + const std::filesystem::path& projectRoot, + const XCEngine::Containers::String& artifactMainPath, + const char* label) { + if (artifactMainPath.Empty()) { + std::cerr << label << " artifact path is empty\n"; + return false; + } + + const std::filesystem::path absoluteArtifactPath = + projectRoot / artifactMainPath.CStr(); + if (!std::filesystem::exists(absoluteArtifactPath)) { + std::cerr << label << " artifact file does not exist: " + << absoluteArtifactPath << "\n"; + return false; + } + + XCEngine::Resources::ShaderLoader loader; + XCEngine::Resources::LoadResult result = + loader.Load(absoluteArtifactPath.generic_string().c_str()); + if (!result || result.resource == nullptr) { + std::cerr << label << " artifact failed to load: " + << result.errorMessage.CStr() << "\n"; + return false; + } + + delete static_cast(result.resource); + return true; +} + +bool RequireAssetDatabaseImportFailureDiagnostic(const std::filesystem::path& projectRoot) { + const std::filesystem::path shaderPath = + projectRoot / "Assets/MissingPackageInclude.shader"; + const char* shaderSource = R"(Shader "Tests/AssetDatabaseMissingPackageInclude" +{ + SubShader + { + Pass + { + Name "AssetDatabaseMissingPackageInclude" + HLSLPROGRAM + #pragma vertex MainVS + #pragma fragment MainPS + #include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/MissingAssetDatabaseFile.hlsl" + float4 MainVS(float3 positionOS : POSITION) : SV_POSITION + { + return float4(positionOS, 1.0f); + } + float4 MainPS() : SV_TARGET + { + return 1.0f.xxxx; + } + ENDHLSL + } + } +} +)"; + + if (!WriteTextFile(shaderPath, shaderSource)) { + std::cerr << "failed to write AssetDatabase missing include shader fixture: " + << shaderPath << "\n"; + return false; + } + + XCEngine::Resources::AssetDatabase assetDatabase; + assetDatabase.Initialize(projectRoot.generic_string().c_str()); + + XCEngine::Resources::AssetDatabase::ResolvedAsset resolvedAsset; + XCEngine::Resources::AssetDatabase::MaintenanceStats stats; + const bool imported = assetDatabase.ReimportAsset( + "Assets/MissingPackageInclude.shader", + resolvedAsset, + &stats); + const std::string errorText = assetDatabase.GetLastErrorMessage().CStr(); + assetDatabase.Shutdown(); + + if (imported) { + std::cerr << "AssetDatabase unexpectedly imported shader with missing package include\n"; + return false; + } + + if (errorText.find( + "requested=Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/MissingAssetDatabaseFile.hlsl") == + std::string::npos || + errorText.find( + "normalized=Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/MissingAssetDatabaseFile.hlsl") == + std::string::npos || + errorText.find("source=") == std::string::npos || + errorText.find("normalizedCandidate=") == std::string::npos || + errorText.find("searchedAnchors=[sourceRoot=") == std::string::npos || + errorText.find("includeChain=") == std::string::npos || + errorText.find("builtinPackageRoot=engine/assets/builtin/shaders/Packages") == + std::string::npos) { + std::cerr << "AssetDatabase missing package include diagnostic is not actionable: " + << errorText << "\n"; + return false; + } + + return true; +} + +bool RequirePackageEditProducesNewShaderArtifact( + const std::filesystem::path& projectRoot) { + std::error_code cleanupError; + std::filesystem::remove_all(projectRoot, cleanupError); + std::filesystem::create_directories(projectRoot / "Assets"); + + ScopedCurrentPath scopedCurrentPath(projectRoot); + + const char* relativeShaderPath = "Assets/PackageArtifactEdit.shader"; + const std::filesystem::path shaderPath = projectRoot / relativeShaderPath; + const std::filesystem::path dependencyPath = + projectRoot / + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/ArtifactEditTest.hlsl"; + + const char* shaderSource = R"(Shader "Tests/PackageArtifactEdit" +{ + HLSLINCLUDE + #include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/ArtifactEditTest.hlsl" + ENDHLSL + + SubShader + { + } +} +)"; + + if (!WriteTextFile(shaderPath, shaderSource) || + !WriteTextFile(dependencyPath, "float4 PackageArtifactEditValue() { return 1.0f.xxxx; }\n")) { + std::cerr << "failed to prepare package edit artifact fixtures\n"; + return false; + } + + XCEngine::Resources::AssetDatabase assetDatabase; + assetDatabase.Initialize(projectRoot.generic_string().c_str()); + + XCEngine::Resources::AssetDatabase::ResolvedAsset firstArtifact; + if (!assetDatabase.EnsureArtifact( + relativeShaderPath, + XCEngine::Resources::ResourceType::Shader, + firstArtifact)) { + std::cerr << "initial package-edit shader import failed: " + << assetDatabase.GetLastErrorMessage().CStr() << "\n"; + assetDatabase.Shutdown(); + return false; + } + + if (!firstArtifact.imported || + !RequireShaderArtifactLoads(projectRoot, firstArtifact.artifactMainPath, "initial package-edit shader")) { + std::cerr << "initial package-edit shader did not produce a valid new artifact\n"; + assetDatabase.Shutdown(); + return false; + } + + const std::string firstArtifactPath = firstArtifact.artifactMainPath.CStr(); + if (!WriteTextFile( + dependencyPath, + "float4 PackageArtifactEditValue() { return float4(0.25f, 0.5f, 0.75f, 1.0f); }\n" + "float4 PackageArtifactEditValueAfterEdit() { return 2.0f.xxxx; }\n")) { + std::cerr << "failed to edit package dependency fixture\n"; + assetDatabase.Shutdown(); + return false; + } + + XCEngine::Resources::AssetDatabase::ResolvedAsset secondArtifact; + if (!assetDatabase.EnsureArtifact( + relativeShaderPath, + XCEngine::Resources::ResourceType::Shader, + secondArtifact)) { + std::cerr << "package-edit shader reimport failed after dependency edit: " + << assetDatabase.GetLastErrorMessage().CStr() << "\n"; + assetDatabase.Shutdown(); + return false; + } + + const std::string secondArtifactPath = secondArtifact.artifactMainPath.CStr(); + if (!secondArtifact.imported || + secondArtifactPath.empty() || + secondArtifactPath == firstArtifactPath || + !RequireShaderArtifactLoads(projectRoot, secondArtifact.artifactMainPath, "edited package shader")) { + std::cerr << "package edit did not produce a distinct valid shader artifact; first=" + << firstArtifactPath << " second=" << secondArtifactPath << "\n"; + assetDatabase.Shutdown(); + return false; + } + + XCEngine::Resources::AssetDatabase::ResolvedAsset thirdArtifact; + if (!assetDatabase.EnsureArtifact( + relativeShaderPath, + XCEngine::Resources::ResourceType::Shader, + thirdArtifact)) { + std::cerr << "package-edit shader unchanged ensure failed: " + << assetDatabase.GetLastErrorMessage().CStr() << "\n"; + assetDatabase.Shutdown(); + return false; + } + + const std::string thirdArtifactPath = thirdArtifact.artifactMainPath.CStr(); + if (thirdArtifact.imported || thirdArtifactPath != secondArtifactPath) { + std::cerr << "unchanged package dependency did not reuse current shader artifact; second=" + << secondArtifactPath << " third=" << thirdArtifactPath << "\n"; + assetDatabase.Shutdown(); + return false; + } + + assetDatabase.Shutdown(); + return true; +} + +bool RequirePackageDependencyStaleRecordTriggersShaderReimport( + const std::filesystem::path& projectRoot) { + std::error_code cleanupError; + std::filesystem::remove_all(projectRoot, cleanupError); + + const char* relativeShaderPath = "Assets/StalePackageDependency.shader"; + const std::filesystem::path shaderPath = projectRoot / relativeShaderPath; + const std::filesystem::path dependencyPath = + projectRoot / + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/StaleArtifactDependency.hlsl"; + const std::string artifactKey = "0123456789abcdef0123456789abcdef"; + const std::string mainArtifactPath = + "Library/Artifacts/01/" + artifactKey + ".xcshader"; + + const char* shaderSource = R"(Shader "Tests/StalePackageDependency" +{ + SubShader + { + Pass + { + Name "StalePackageDependency" + HLSLPROGRAM + #pragma vertex MainVS + #pragma fragment MainPS + #include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/MissingAfterInvalidation.hlsl" + float4 MainVS(float3 positionOS : POSITION) : SV_POSITION + { + return float4(positionOS, 1.0f); + } + float4 MainPS() : SV_TARGET + { + return 1.0f.xxxx; + } + ENDHLSL + } + } +} +)"; + + if (!WriteTextFile(shaderPath, shaderSource) || + !WriteTextFile(dependencyPath, "float4 StaleArtifactDependencyValue() { return 1.0f.xxxx; }\n")) { + std::cerr << "failed to prepare stale package dependency fixtures\n"; + return false; + } + + { + XCEngine::Resources::AssetDatabase assetDatabase; + assetDatabase.Initialize(projectRoot.generic_string().c_str()); + assetDatabase.Refresh(); + assetDatabase.Shutdown(); + } + + std::vector sourceFields; + if (!LoadSourceDbShaderRecord(projectRoot, relativeShaderPath, sourceFields)) { + return false; + } + + if (!WriteMinimalCurrentShaderArtifact(projectRoot / mainArtifactPath)) { + std::cerr << "failed to write stale package dependency artifact fixture\n"; + return false; + } + + const XCEngine::Core::uint64 currentDependencySize = GetFileSizeValue(dependencyPath); + const XCEngine::Core::uint64 currentDependencyWriteTime = GetFileWriteTimeValue(dependencyPath); + if (!WriteShaderArtifactDbRecord( + projectRoot, + sourceFields, + dependencyPath, + currentDependencySize, + currentDependencyWriteTime, + artifactKey, + mainArtifactPath)) { + return false; + } + + { + XCEngine::Resources::AssetDatabase assetDatabase; + assetDatabase.Initialize(projectRoot.generic_string().c_str()); + + XCEngine::Resources::AssetDatabase::ResolvedAsset resolvedAsset; + if (!assetDatabase.EnsureArtifact( + relativeShaderPath, + XCEngine::Resources::ResourceType::Shader, + resolvedAsset)) { + std::cerr << "current package dependency record unexpectedly triggered import: " + << assetDatabase.GetLastErrorMessage().CStr() << "\n"; + assetDatabase.Shutdown(); + return false; + } + + if (resolvedAsset.imported || + resolvedAsset.artifactMainPath.CStr()[0] == '\0') { + std::cerr << "current package dependency record did not use existing artifact\n"; + assetDatabase.Shutdown(); + return false; + } + + assetDatabase.Shutdown(); + } + + const XCEngine::Core::uint64 staleWriteTime = + currentDependencyWriteTime == 1 ? 2 : 1; + if (!WriteShaderArtifactDbRecord( + projectRoot, + sourceFields, + dependencyPath, + currentDependencySize, + staleWriteTime, + artifactKey, + mainArtifactPath)) { + return false; + } + + { + XCEngine::Resources::AssetDatabase assetDatabase; + assetDatabase.Initialize(projectRoot.generic_string().c_str()); + + XCEngine::Resources::AssetDatabase::ResolvedAsset resolvedAsset; + const bool ensured = assetDatabase.EnsureArtifact( + relativeShaderPath, + XCEngine::Resources::ResourceType::Shader, + resolvedAsset); + const std::string errorText = assetDatabase.GetLastErrorMessage().CStr(); + assetDatabase.Shutdown(); + + if (ensured) { + std::cerr << "stale package dependency record did not trigger shader reimport\n"; + return false; + } + + if (errorText.find( + "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/MissingAfterInvalidation.hlsl") == + std::string::npos) { + std::cerr << "stale package dependency reimport did not reach shader loader diagnostic: " + << errorText << "\n"; + return false; + } + } + + return true; +} + +bool RequireIncludeFailureDiagnostic( + const char* label, + const XCEngine::Containers::String& error, + const std::string& requestedPath) { + const std::string errorText = error.CStr(); + if (errorText.find("requested=" + requestedPath) == std::string::npos || + errorText.find("normalized=") == std::string::npos || + errorText.find("source=") == std::string::npos || + errorText.find("normalizedCandidate=") == std::string::npos || + errorText.find("searchedAnchors=[sourceRoot=") == std::string::npos || + errorText.find("resourceRoot=") == std::string::npos || + errorText.find("currentPath=") == std::string::npos || + errorText.find("includeChain=") == std::string::npos || + errorText.find("builtinPackageRoot=engine/assets/builtin/shaders/Packages") == std::string::npos) { + std::cerr << label << " diagnostic is not actionable: " << errorText << "\n"; + return false; + } + + return true; +} + +} // namespace + +int main() { + const std::filesystem::path tempRoot = + std::filesystem::temp_directory_path() / + "xcengine_shader_dependency_tests"; + const std::filesystem::path shaderPath = tempRoot / "package_include.shader"; + + const char* shaderSource = R"(Shader "Tests/PackageInclude" +{ + SubShader + { + Pass + { + Name "PackageInclude" + HLSLPROGRAM + #pragma vertex MainVS + #pragma fragment MainPS + #include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl" + float4 MainVS(float3 positionOS : POSITION) : SV_POSITION + { + return TransformObjectToHClip(positionOS); + } + float4 MainPS() : SV_TARGET + { + return 1.0f.xxxx; + } + ENDHLSL + } + } +} +)"; + + if (!WriteTextFile(shaderPath, shaderSource)) { + std::cerr << "failed to write shader fixture: " << shaderPath << "\n"; + return 1; + } + + XCEngine::Containers::Array dependencies; + if (!CollectDependencies(shaderPath, dependencies)) { + std::cerr << "CollectSourceDependencies failed\n"; + return 1; + } + + if (!RequireDependencySuffixes( + dependencies, + { + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl", + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/ShaderVariables.hlsl", + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl", + })) { + return 1; + } + + const std::filesystem::path originalCurrentPath = std::filesystem::current_path(); + const std::filesystem::path shadowPackageRoot = + tempRoot / "engine/assets/builtin/shaders/Packages"; + const std::filesystem::path shadowCorePath = + shadowPackageRoot / + "com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl"; + if (!WriteTextFile(shadowCorePath, "float4 ShadowRootSelected() { return 1.0f.xxxx; }\n")) { + std::cerr << "failed to write shadow package include fixture: " << shadowCorePath << "\n"; + return 1; + } + + std::filesystem::current_path(tempRoot); + dependencies.Clear(); + if (!CollectDependencies(shaderPath, dependencies)) { + std::filesystem::current_path(originalCurrentPath); + std::cerr << "CollectSourceDependencies failed while current directory contained a shadow package root\n"; + return 1; + } + std::filesystem::current_path(originalCurrentPath); + + if (!RequireDependencySuffixes( + dependencies, + { + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl", + }) || + !RequireNoDependencyPrefix(dependencies, shadowPackageRoot)) { + return 1; + } + + const std::filesystem::path currentPathFallbackProjectRoot = + tempRoot / "current_path_fallback_project"; + if (!RequirePackageDependencyCurrentPathFallback(currentPathFallbackProjectRoot)) { + return 1; + } + + const std::filesystem::path packageCompileShaderPath = + tempRoot / "package_compile.shader"; + const char* packageCompileShaderSource = R"(Shader "Tests/PackageCompile" +{ + SubShader + { + Pass + { + Name "PackageCompile" + HLSLPROGRAM + #pragma vertex MainVS + #pragma fragment MainPS + #include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl" + struct Attributes + { + float3 positionOS : POSITION; + }; + struct Varyings + { + float4 positionCS : SV_POSITION; + }; + Varyings MainVS(Attributes input) + { + Varyings output; + output.positionCS = TransformObjectToHClip(input.positionOS); + return output; + } + float4 MainPS(Varyings input) : SV_TARGET + { + return input.positionCS; + } + ENDHLSL + } + } +} +)"; + + if (!WriteTextFile(packageCompileShaderPath, packageCompileShaderSource)) { + std::cerr << "failed to write package compile shader fixture: " << packageCompileShaderPath << "\n"; + return 1; + } + + if (!RequirePackageShaderLoadAndD3D12Compile(packageCompileShaderPath)) { + return 1; + } + + const std::filesystem::path backslashPackageIncludeShaderPath = + tempRoot / "backslash_package_include.shader"; + const char* backslashPackageIncludeShaderSource = R"(Shader "Tests/BackslashPackageInclude" +{ + SubShader + { + Pass + { + Name "BackslashPackageInclude" + HLSLPROGRAM + #pragma vertex MainVS + #pragma fragment MainPS + #include "Packages\com.xcengine.render-pipelines.universal\ShaderLibrary\Core.hlsl" + float4 MainVS(float3 positionOS : POSITION) : SV_POSITION + { + return TransformObjectToHClip(positionOS); + } + float4 MainPS() : SV_TARGET + { + return 1.0f.xxxx; + } + ENDHLSL + } + } +} +)"; + + if (!WriteTextFile(backslashPackageIncludeShaderPath, backslashPackageIncludeShaderSource)) { + std::cerr << "failed to write backslash package include shader fixture: " << backslashPackageIncludeShaderPath << "\n"; + return 1; + } + + dependencies.Clear(); + if (!CollectDependencies(backslashPackageIncludeShaderPath, dependencies)) { + std::cerr << "CollectSourceDependencies failed for backslash package include\n"; + return 1; + } + + if (!RequireDependencySuffixes( + dependencies, + { + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl", + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/ShaderVariables.hlsl", + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl", + })) { + return 1; + } + + const std::filesystem::path allPublicIncludesShaderPath = + tempRoot / "all_public_package_includes.shader"; + const char* allPublicIncludesShaderSource = R"(Shader "Tests/AllPublicPackageIncludes" +{ + SubShader + { + Pass + { + Name "AllPublicPackageIncludes" + HLSLPROGRAM + #pragma vertex MainVS + #pragma fragment MainPS + #include "Packages/com.xcengine.render-pipelines.core/ShaderLibrary/ShaderVariables.hlsl" + #include "Packages/com.xcengine.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl" + #include "Packages/com.xcengine.render-pipelines.core/ShaderLibrary/TextureSampling.hlsl" + #include "Packages/com.xcengine.render-pipelines.core/ShaderLibrary/Color.hlsl" + #include "Packages/com.xcengine.render-pipelines.core/ShaderLibrary/Packing.hlsl" + #include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl" + #include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Input.hlsl" + #include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl" + #include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/ShaderPass.hlsl" + #include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/RealtimeLights.hlsl" + float4 MainVS(float3 positionOS : POSITION) : SV_POSITION + { + return TransformObjectToHClip(positionOS); + } + float4 MainPS() : SV_TARGET + { + return XcLinearToSrgb(1.0f.xxx).xyzz; + } + ENDHLSL + } + } +} +)"; + + if (!WriteTextFile(allPublicIncludesShaderPath, allPublicIncludesShaderSource)) { + std::cerr << "failed to write all public package include shader fixture: " << allPublicIncludesShaderPath << "\n"; + return 1; + } + + dependencies.Clear(); + if (!CollectDependencies(allPublicIncludesShaderPath, dependencies)) { + std::cerr << "CollectSourceDependencies failed for all public package includes\n"; + return 1; + } + + if (!RequireDependencySuffixes( + dependencies, + { + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/ShaderVariables.hlsl", + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl", + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/TextureSampling.hlsl", + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/Color.hlsl", + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/Packing.hlsl", + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl", + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Input.hlsl", + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl", + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/ShaderPass.hlsl", + "engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/RealtimeLights.hlsl", + })) { + return 1; + } + + const std::filesystem::path missingIncludeShaderPath = + tempRoot / "missing_package_include.shader"; + const char* missingIncludeShaderSource = R"(Shader "Tests/MissingPackageInclude" +{ + SubShader + { + Pass + { + Name "MissingPackageInclude" + HLSLPROGRAM + #pragma vertex MainVS + #pragma fragment MainPS + #include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/MissingFile.hlsl" + float4 MainVS(float3 positionOS : POSITION) : SV_POSITION + { + return float4(positionOS, 1.0f); + } + float4 MainPS() : SV_TARGET + { + return 1.0f.xxxx; + } + ENDHLSL + } + } +} +)"; + + if (!WriteTextFile(missingIncludeShaderPath, missingIncludeShaderSource)) { + std::cerr << "failed to write missing include shader fixture: " << missingIncludeShaderPath << "\n"; + return 1; + } + + dependencies.Clear(); + XCEngine::Containers::String dependencyError; + if (CollectDependencies(missingIncludeShaderPath, dependencies, dependencyError)) { + std::cerr << "CollectSourceDependencies unexpectedly accepted a missing package include\n"; + return 1; + } + if (!RequireIncludeFailureDiagnostic( + "missing package include", + dependencyError, + "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/MissingFile.hlsl")) { + return 1; + } + + const std::filesystem::path escapedPackageShaderPath = + tempRoot / "escaped_package_include.shader"; + const char* escapedPackageShaderSource = R"(Shader "Tests/EscapedPackageInclude" +{ + SubShader + { + Pass + { + Name "EscapedPackageInclude" + HLSLPROGRAM + #pragma vertex MainVS + #pragma fragment MainPS + #include "Packages/../forward-lit.shader" + float4 MainVS(float3 positionOS : POSITION) : SV_POSITION + { + return float4(positionOS, 1.0f); + } + float4 MainPS() : SV_TARGET + { + return 1.0f.xxxx; + } + ENDHLSL + } + } +} +)"; + + if (!WriteTextFile(escapedPackageShaderPath, escapedPackageShaderSource)) { + std::cerr << "failed to write escaped package include shader fixture: " << escapedPackageShaderPath << "\n"; + return 1; + } + + dependencies.Clear(); + dependencyError.Clear(); + if (CollectDependencies(escapedPackageShaderPath, dependencies, dependencyError)) { + std::cerr << "CollectSourceDependencies unexpectedly accepted a package include outside Packages root\n"; + return 1; + } + if (!RequireIncludeFailureDiagnostic( + "escaped package include", + dependencyError, + "Packages/../forward-lit.shader")) { + return 1; + } + + const std::filesystem::path absoluteIncludeTargetPath = + tempRoot / "absolute_include_target.hlsl"; + if (!WriteTextFile(absoluteIncludeTargetPath, "float4 AbsoluteIncludeValue() { return 1.0f.xxxx; }\n")) { + std::cerr << "failed to write absolute include target fixture: " << absoluteIncludeTargetPath << "\n"; + return 1; + } + + const std::filesystem::path absoluteIncludeShaderPath = + tempRoot / "absolute_include.shader"; + const std::string absoluteIncludePath = absoluteIncludeTargetPath.generic_string(); + const std::string absoluteIncludeShaderSource = + "Shader \"Tests/AbsoluteInclude\"\n" + "{\n" + " SubShader\n" + " {\n" + " Pass\n" + " {\n" + " Name \"AbsoluteInclude\"\n" + " HLSLPROGRAM\n" + " #pragma vertex MainVS\n" + " #pragma fragment MainPS\n" + " #include \"" + absoluteIncludePath + "\"\n" + + " float4 MainVS(float3 positionOS : POSITION) : SV_POSITION\n" + " {\n" + " return float4(positionOS, 1.0f);\n" + " }\n" + " float4 MainPS() : SV_TARGET\n" + " {\n" + " return AbsoluteIncludeValue();\n" + " }\n" + " ENDHLSL\n" + " }\n" + " }\n" + "}\n"; + + if (!WriteTextFile(absoluteIncludeShaderPath, absoluteIncludeShaderSource.c_str())) { + std::cerr << "failed to write absolute include shader fixture: " << absoluteIncludeShaderPath << "\n"; + return 1; + } + + dependencies.Clear(); + dependencyError.Clear(); + if (CollectDependencies(absoluteIncludeShaderPath, dependencies, dependencyError)) { + std::cerr << "CollectSourceDependencies unexpectedly accepted an absolute include\n"; + return 1; + } + if (!RequireIncludeFailureDiagnostic( + "absolute include", + dependencyError, + absoluteIncludePath)) { + return 1; + } + + const std::filesystem::path assetDatabaseProjectRoot = + tempRoot / "asset_database_project"; + std::error_code cleanupError; + std::filesystem::remove_all(assetDatabaseProjectRoot, cleanupError); + if (!RequireAssetDatabaseImportFailureDiagnostic(assetDatabaseProjectRoot)) { + return 1; + } + + const std::filesystem::path artifactInvalidationProjectRoot = + tempRoot / "artifact_invalidation_project"; + if (!RequirePackageDependencyStaleRecordTriggersShaderReimport( + artifactInvalidationProjectRoot)) { + return 1; + } + + const std::filesystem::path packageEditProjectRoot = + tempRoot / "package_edit_artifact_project"; + if (!RequirePackageEditProducesNewShaderArtifact(packageEditProjectRoot)) { + return 1; + } + + return 0; +}