From fac6e588a8ed676e12a58b12d609a16c21721531 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sat, 11 Apr 2026 03:02:30 +0800 Subject: [PATCH] Formalize gaussian splat prepare-order pass --- ...缓存并入Library正式方案_完成归档_2026-04-11.md | 181 ++++++ .../builtin/shaders/gaussian-splat.shader | 8 +- .../XCEngine/Core/Asset/ArtifactFormats.h | 12 +- .../include/XCEngine/RHI/D3D12/D3D12Shader.h | 4 + .../Passes/BuiltinGaussianSplatPass.h | 61 +- .../XCEngine/Resources/Shader/Shader.h | 15 + engine/src/Core/Asset/AssetDatabase.cpp | 364 +++++++++++- engine/src/Core/Asset/ResourceManager.cpp | 34 +- engine/src/RHI/D3D12/D3D12Shader.cpp | 66 ++- .../ShaderCompiler/SpirvShaderCompiler.cpp | 24 +- .../Rendering/Internal/ShaderVariantUtils.h | 27 + .../Passes/BuiltinGaussianSplatPass.cpp | 538 +++++++++++++++++- .../Shader/Internal/ShaderArtifactLoader.cpp | 31 +- engine/src/Resources/Shader/Shader.cpp | 58 ++ .../unit/test_builtin_forward_pipeline.cpp | 109 +++- tests/Resources/Shader/test_shader.cpp | 65 +++ 16 files changed, 1529 insertions(+), 68 deletions(-) create mode 100644 docs/used/Shader统一预编译缓存并入Library正式方案_完成归档_2026-04-11.md diff --git a/docs/used/Shader统一预编译缓存并入Library正式方案_完成归档_2026-04-11.md b/docs/used/Shader统一预编译缓存并入Library正式方案_完成归档_2026-04-11.md new file mode 100644 index 00000000..d18e5849 --- /dev/null +++ b/docs/used/Shader统一预编译缓存并入Library正式方案_完成归档_2026-04-11.md @@ -0,0 +1,181 @@ +# Shader 统一预编译缓存并入 Library 正式方案 + +文档日期:2026-04-11 + +## 1. 目标 + +把 shader 的预编译结果正式并入现有 `Library/Artifacts` 体系,不再允许出现: + +1. project shader 走 `Library`,builtin shader 走裸加载。 +2. 运行时每次开编辑器都重新现场编译重 shader。 +3. 只修 D3D12,一到 Vulkan / OpenGL 又退回运行时临时编译。 + +本轮正式目标: + +1. builtin shader 与 project shader 统一走同一个 artifact 产物格式。 +2. `ShaderStageVariant` 保持现有 authoring / runtime 变体模型不变,不扩成“每后端一个 variant”。 +3. 每个 variant 可以携带“按后端区分的已编译 payload”。 +4. D3D12 / Vulkan / OpenGL 运行时优先消费 artifact 中的 payload,只有 miss 时才 fallback 到现场编译。 + +## 2. 架构原则 + +### 2.1 唯一正式缓存位置 + +唯一正式 shader 预编译缓存位置是: + +`project/Library/Artifacts/.../main.xcshader` + +不新增独立 runtime cache 目录,不做旁路缓存。 + +### 2.2 builtin shader 也必须纳入 Library + +外部标识仍然保持: + +`builtin://shaders/...` + +但在有 project root 时,真实加载路径应优先变成: + +`Library/Artifacts/.../main.xcshader` + +这样 builtin shader 与普通 `Assets/*.shader` 在缓存语义上完全一致。 + +### 2.3 不改变现有 variant 选择逻辑 + +现有很多代码和测试都依赖: + +1. authoring variant 可以是 `Generic` +2. 运行时按 backend 查找时先找精确 backend,再回退 `Generic` + +因此不把一个 `Generic` variant 展开成三份后端 variant,而是让一个 variant 内部携带: + +1. 旧字段 `compiledBinary` +2. 新字段 `backendCompiledBinaries` + +## 3. 数据模型 + +### 3.1 运行时 compile desc + +`RHI::ShaderCompileDesc` 新增: + +1. `compiledBinaryBackend` +2. `compiledBinary` + +含义: + +1. source / fileName 仍然描述“可回退到现场编译的输入” +2. compiledBinary 描述“当前后端可直接消费的已编译 payload” + +### 3.2 shader variant + +`Resources::ShaderStageVariant` 支持: + +1. `GetCompiledBinaryForBackend(...)` +2. `SetCompiledBinaryForBackend(...)` + +语义: + +1. backend-specific variant 可以继续把本后端 payload 放在旧 `compiledBinary` +2. `Generic` variant 的 D3D12 / Vulkan / OpenGL payload 放在 `backendCompiledBinaries` + +### 3.3 artifact schema + +shader artifact schema 升级为 `6`,`ShaderVariantArtifactHeader` 新增: + +1. `backendCompiledBinaryCount` + +并追加 `ShaderBackendCompiledBinaryArtifactHeader + payload` 序列。 + +## 4. 导入阶段 + +导入 `.shader` 时: + +1. 先用 `ShaderLoader` 生成现有 authoring/runtime variant。 +2. 对每个 pass / variant 生成运行时 compile desc。 +3. 按目标后端尝试预编译: + - D3D12:生成 DXBC + - Vulkan:生成 Vulkan SPIR-V + - OpenGL:生成 OpenGL-target SPIR-V +4. 把结果写回 variant。 +5. 最终统一写入 `main.xcshader` artifact。 + +后端目标规则: + +1. `Generic` variant 预编译到 D3D12 / Vulkan / OpenGL +2. backend-specific variant 只预编译自己的 backend + +失败策略: + +1. 预编译失败不阻断 import +2. artifact 仍然照常生成 +3. 运行时保持 fallback 能力 +4. 日志明确记录 pass / stage / backend / reason + +## 5. builtin shader 接入方式 + +### 5.1 ResourceManager + +`ResourceManager::LoadResource(...)` 在 shader + builtin path 情况下: + +1. 解析到真实 builtin shader 资产路径 +2. 在当前 project 的 `Library` 中确保对应 artifact +3. 真正加载 artifact +4. 资源对外 path 仍然恢复成 `builtin://...` + +### 5.2 AssetDatabase + +由于 builtin shader 资产位于 project root 外部: + +1. 允许绝对路径解析成相对 project root 的 `../engine/...` +2. 这些外部 source record 不写 `.meta` +3. 使用 synthetic GUID / synthetic meta hash +4. `Refresh()` 时显式扫描 builtin shader 资产,避免下次启动把 builtin artifact 当 orphan 清掉 + +## 6. 运行时消费 + +### 6.1 D3D12 + +1. `D3D12Shader` 增加 `InitializeFromBytecode(...)` +2. `CompileD3D12Shader(...)` 优先命中 `compiledBinaryBackend == D3D12` +3. miss 时才走 `D3DCompile(...)` +4. `CreateShader(...)` 统一复用这条逻辑 + +### 6.2 Vulkan + +1. `CompileSpirvShader(...)` 支持直接消费 `compiledBinaryBackend == Vulkan` +2. `VulkanDevice / VulkanPipelineState` 通过既有 `CompileVulkanShader(...)` 自动命中缓存 payload + +### 6.3 OpenGL + +1. 若命中 `compiledBinaryBackend == OpenGL` +2. 则把该 payload 作为 SPIR-V 输入 +3. 继续走现有 `SPIR-V -> GLSL` 转译路径 +4. 原始 HLSL source 仍保留给 sampler/texture 绑定推导逻辑使用 + +## 7. 验收标准 + +### 7.1 工程标准 + +1. `shader_tests` 全绿 +2. `XCEditor` 能完整编译通过 + +### 7.2 行为标准 + +1. shader artifact roundtrip 后不丢失 backend payload +2. builtin shader 与 project shader 都能命中 `Library/Artifacts` +3. D3D12 / Vulkan / OpenGL 运行时在 payload 存在时不再重复现场编译 + +### 7.3 性能标准 + +1. 影响 NanoVDB 首帧的 volumetric shader 不再在每次启动时重编 +2. editor 打开主场景时,shader stall 不再成为 `SceneReady` 的主瓶颈 + +## 8. 后续扩展 + +后续若继续做: + +1. import-time completeness 标记 +2. 运行时回写 artifact +3. PSO cache blob +4. 更完整的 shader compile telemetry + +都必须建立在本方案之上,不允许再引入平行缓存体系。 diff --git a/engine/assets/builtin/shaders/gaussian-splat.shader b/engine/assets/builtin/shaders/gaussian-splat.shader index 44e735f0..5afef25b 100644 --- a/engine/assets/builtin/shaders/gaussian-splat.shader +++ b/engine/assets/builtin/shaders/gaussian-splat.shader @@ -26,6 +26,7 @@ Shader "Builtin Gaussian Splat" float4 scaleReserved; }; + StructuredBuffer GaussianSplatOrderBuffer; StructuredBuffer GaussianSplatPositions; StructuredBuffer GaussianSplatOther; StructuredBuffer GaussianSplatColor; @@ -54,9 +55,10 @@ Shader "Builtin Gaussian Splat" { VSOutput output; const float2 corner = ResolveQuadCorner(vertexId); - const float3 localCenter = GaussianSplatPositions[instanceId]; - const GaussianSplatOtherData otherData = GaussianSplatOther[instanceId]; - const float4 colorOpacity = GaussianSplatColor[instanceId]; + const uint splatIndex = GaussianSplatOrderBuffer[instanceId]; + const float3 localCenter = GaussianSplatPositions[splatIndex]; + const GaussianSplatOtherData otherData = GaussianSplatOther[splatIndex]; + const float4 colorOpacity = GaussianSplatColor[splatIndex]; const float3 worldCenter = mul(gModelMatrix, float4(localCenter, 1.0)).xyz; const float maxAxisScale = diff --git a/engine/include/XCEngine/Core/Asset/ArtifactFormats.h b/engine/include/XCEngine/Core/Asset/ArtifactFormats.h index 1b16dbfe..673d85b8 100644 --- a/engine/include/XCEngine/Core/Asset/ArtifactFormats.h +++ b/engine/include/XCEngine/Core/Asset/ArtifactFormats.h @@ -16,7 +16,7 @@ namespace Resources { constexpr Core::uint32 kTextureArtifactSchemaVersion = 1; constexpr Core::uint32 kMaterialArtifactSchemaVersion = 6; constexpr Core::uint32 kMeshArtifactSchemaVersion = 2; -constexpr Core::uint32 kShaderArtifactSchemaVersion = 5; +constexpr Core::uint32 kShaderArtifactSchemaVersion = 6; constexpr Core::uint32 kUIDocumentArtifactSchemaVersion = 2; constexpr Core::uint32 kVolumeFieldArtifactSchemaVersion = 2; constexpr Core::uint32 kModelArtifactSchemaVersion = 1; @@ -104,7 +104,7 @@ struct ModelMaterialBindingArtifact { }; struct ShaderArtifactFileHeader { - char magic[8] = { 'X', 'C', 'S', 'H', 'D', '0', '5', '\0' }; + char magic[8] = { 'X', 'C', 'S', 'H', 'D', '0', '6', '\0' }; Core::uint32 schemaVersion = kShaderArtifactSchemaVersion; }; @@ -138,6 +138,14 @@ struct ShaderVariantArtifactHeader { Core::uint32 backend = 0; Core::uint32 keywordCount = 0; Core::uint64 compiledBinarySize = 0; + Core::uint32 backendCompiledBinaryCount = 0; + Core::uint32 reserved = 0; +}; + +struct ShaderBackendCompiledBinaryArtifactHeader { + Core::uint32 backend = 0; + Core::uint32 reserved = 0; + Core::uint64 compiledBinarySize = 0; }; struct ShaderKeywordDeclarationArtifactHeader { diff --git a/engine/include/XCEngine/RHI/D3D12/D3D12Shader.h b/engine/include/XCEngine/RHI/D3D12/D3D12Shader.h index 02f0e602..37a7705a 100644 --- a/engine/include/XCEngine/RHI/D3D12/D3D12Shader.h +++ b/engine/include/XCEngine/RHI/D3D12/D3D12Shader.h @@ -30,6 +30,10 @@ public: const D3D_SHADER_MACRO* macros, const char* entryPoint, const char* target); + bool InitializeFromBytecode( + const void* bytecodeData, + size_t bytecodeSize, + const char* target = nullptr); void Shutdown() override; const D3D12_SHADER_BYTECODE GetD3D12Bytecode() const; diff --git a/engine/include/XCEngine/Rendering/Passes/BuiltinGaussianSplatPass.h b/engine/include/XCEngine/Rendering/Passes/BuiltinGaussianSplatPass.h index ee0421cd..c11e7b6e 100644 --- a/engine/include/XCEngine/Rendering/Passes/BuiltinGaussianSplatPass.h +++ b/engine/include/XCEngine/Rendering/Passes/BuiltinGaussianSplatPass.h @@ -21,6 +21,7 @@ namespace XCEngine { namespace Components { class GameObject; +class GaussianSplatRendererComponent; } // namespace Components namespace Resources { @@ -31,6 +32,11 @@ class Shader; namespace Rendering { struct VisibleGaussianSplatItem; +namespace Passes { +namespace Internal { +class BuiltinGaussianSplatPassResources; +} // namespace Internal +} // namespace Passes namespace Passes { @@ -84,16 +90,20 @@ private: std::vector setLayouts; PassResourceBindingLocation perObject = {}; PassResourceBindingLocation material = {}; + PassResourceBindingLocation gaussianSplatSortDistanceBuffer = {}; + PassResourceBindingLocation gaussianSplatOrderBuffer = {}; PassResourceBindingLocation gaussianSplatPositionBuffer = {}; PassResourceBindingLocation gaussianSplatOtherBuffer = {}; PassResourceBindingLocation gaussianSplatColorBuffer = {}; PassResourceBindingLocation gaussianSplatSHBuffer = {}; + PassResourceBindingLocation gaussianSplatViewDataBuffer = {}; }; struct DynamicDescriptorSetKey { PassLayoutKey passLayout = {}; Core::uint32 setIndex = 0; Core::uint64 objectId = 0; + const Components::GaussianSplatRendererComponent* gaussianSplatRenderer = nullptr; const Resources::Material* material = nullptr; const Resources::GaussianSplat* gaussianSplat = nullptr; @@ -101,6 +111,7 @@ private: return passLayout == other.passLayout && setIndex == other.setIndex && objectId == other.objectId && + gaussianSplatRenderer == other.gaussianSplatRenderer && material == other.material && gaussianSplat == other.gaussianSplat; } @@ -111,6 +122,7 @@ private: size_t hash = PassLayoutKeyHash()(key.passLayout); hash ^= std::hash{}(key.setIndex) + 0x9e3779b9u + (hash << 6) + (hash >> 2); hash ^= std::hash{}(key.objectId) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= reinterpret_cast(key.gaussianSplatRenderer) + 0x9e3779b9u + (hash << 6) + (hash >> 2); hash ^= reinterpret_cast(key.material) + 0x9e3779b9u + (hash << 6) + (hash >> 2); hash ^= reinterpret_cast(key.gaussianSplat) + 0x9e3779b9u + (hash << 6) + (hash >> 2); return hash; @@ -120,10 +132,13 @@ private: struct CachedDescriptorSet { OwnedDescriptorSet descriptorSet = {}; Core::uint64 materialVersion = 0; + RHI::RHIResourceView* sortDistanceView = nullptr; + RHI::RHIResourceView* orderView = nullptr; RHI::RHIResourceView* positionsView = nullptr; RHI::RHIResourceView* otherView = nullptr; RHI::RHIResourceView* colorView = nullptr; RHI::RHIResourceView* shView = nullptr; + RHI::RHIResourceView* viewDataView = nullptr; }; struct ResolvedShaderPass { @@ -171,6 +186,32 @@ private: } }; + struct ComputePipelineKey { + const Resources::Shader* shader = nullptr; + Containers::String passName; + Containers::String keywordSignature; + + bool operator==(const ComputePipelineKey& other) const { + return shader == other.shader && + passName == other.passName && + keywordSignature == other.keywordSignature; + } + }; + + struct ComputePipelineKeyHash { + size_t operator()(const ComputePipelineKey& key) const noexcept { + size_t hash = reinterpret_cast(key.shader); + hash ^= std::hash{}(key.passName) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(key.keywordSignature) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + return hash; + } + }; + + enum class PassLayoutUsage : Core::uint8 { + Draw, + PrepareOrder + }; + bool EnsureInitialized(const RenderContext& context); bool CreateResources(const RenderContext& context); void DestroyResources(); @@ -180,14 +221,19 @@ private: ResolvedShaderPass ResolveGaussianSplatShaderPass( const RenderSceneData& sceneData, const Resources::Material* material) const; + ResolvedShaderPass ResolvePrepareOrderShaderPass(const RenderSceneData& sceneData) const; PassResourceLayout* GetOrCreatePassResourceLayout( const RenderContext& context, - const ResolvedShaderPass& resolvedShaderPass); + const ResolvedShaderPass& resolvedShaderPass, + PassLayoutUsage usage); RHI::RHIPipelineState* GetOrCreatePipelineState( const RenderContext& context, const RenderSurface& surface, const RenderSceneData& sceneData, const Resources::Material* material); + RHI::RHIPipelineState* GetOrCreateComputePipelineState( + const RenderContext& context, + const RenderSceneData& sceneData); bool CreateOwnedDescriptorSet( const BuiltinPassSetLayoutMetadata& setLayout, OwnedDescriptorSet& descriptorSet); @@ -197,12 +243,20 @@ private: const BuiltinPassSetLayoutMetadata& setLayout, Core::uint32 setIndex, Core::uint64 objectId, + const Components::GaussianSplatRendererComponent* gaussianSplatRenderer, const Resources::Material* material, const Resources::GaussianSplat* gaussianSplat, const MaterialConstantPayloadView& materialConstants, - const RenderResourceCache::CachedGaussianSplat& cachedGaussianSplat); + const RenderResourceCache::CachedGaussianSplat& cachedGaussianSplat, + RHI::RHIResourceView* sortDistanceView, + RHI::RHIResourceView* orderView, + RHI::RHIResourceView* viewDataView); void DestroyOwnedDescriptorSet(OwnedDescriptorSet& descriptorSet); void DestroyPassResourceLayout(PassResourceLayout& passLayout); + bool PrepareVisibleGaussianSplat( + const RenderContext& context, + const RenderSceneData& sceneData, + const VisibleGaussianSplatItem& visibleGaussianSplat); bool DrawVisibleGaussianSplat( const RenderContext& context, const RenderSurface& surface, @@ -212,11 +266,14 @@ private: RHI::RHIDevice* m_device = nullptr; RHI::RHIType m_backendType = RHI::RHIType::D3D12; Resources::ResourceHandle m_builtinGaussianSplatShader; + Resources::ResourceHandle m_builtinGaussianSplatUtilitiesShader; std::unique_ptr m_builtinGaussianSplatMaterial; RenderResourceCache m_resourceCache; + Internal::BuiltinGaussianSplatPassResources* m_passResources = nullptr; std::unordered_map m_passResourceLayouts; std::unordered_map m_pipelineStates; + std::unordered_map m_computePipelineStates; std::unordered_map m_dynamicDescriptorSets; }; diff --git a/engine/include/XCEngine/Resources/Shader/Shader.h b/engine/include/XCEngine/Resources/Shader/Shader.h index aef0424b..a6820868 100644 --- a/engine/include/XCEngine/Resources/Shader/Shader.h +++ b/engine/include/XCEngine/Resources/Shader/Shader.h @@ -87,6 +87,11 @@ struct ShaderResourceBindingDesc { Containers::String semantic; }; +struct ShaderBackendCompiledBinary { + ShaderBackend backend = ShaderBackend::Generic; + Containers::Array payload; +}; + struct ShaderStageVariant { ShaderType stage = ShaderType::Fragment; ShaderLanguage language = ShaderLanguage::GLSL; @@ -96,6 +101,16 @@ struct ShaderStageVariant { Containers::String profile; Containers::String sourceCode; Containers::Array compiledBinary; + Containers::Array backendCompiledBinaries; + + const Containers::Array* GetCompiledBinaryForBackend( + ShaderBackend targetBackend) const; + void SetCompiledBinaryForBackend( + ShaderBackend targetBackend, + const Containers::Array& binary); + void SetCompiledBinaryForBackend( + ShaderBackend targetBackend, + Containers::Array&& binary); }; struct ShaderPass { diff --git a/engine/src/Core/Asset/AssetDatabase.cpp b/engine/src/Core/Asset/AssetDatabase.cpp index 3b953a49..6d0c7b02 100644 --- a/engine/src/Core/Asset/AssetDatabase.cpp +++ b/engine/src/Core/Asset/AssetDatabase.cpp @@ -1,23 +1,33 @@ +#ifndef NOMINMAX +#define NOMINMAX +#endif + #include #include #include +#include +#include +#include #include #include #include #include #include #include +#include #include "Resources/GaussianSplat/Internal/GaussianSplatPlyImporter.h" #include #include #include #include #include +#include "Rendering/Internal/ShaderVariantUtils.h" #include #include #include +#include #include #include #include @@ -134,10 +144,294 @@ bool IsCurrentShaderArtifactFile(const fs::path& artifactPath) { return false; } - return std::memcmp(header.magic, "XCSHD05", 7) == 0 && + return std::memcmp(header.magic, "XCSHD06", 7) == 0 && header.schemaVersion == kShaderArtifactSchemaVersion; } +bool IsManagedProjectAssetPath(const Containers::String& relativePath) { + return relativePath == "Assets" || relativePath.StartsWith("Assets/"); +} + +bool UsesExternalSyntheticSourceRecord(const Containers::String& relativePath) { + return !relativePath.Empty() && !IsManagedProjectAssetPath(relativePath); +} + +Containers::String BuildSyntheticMetaHash( + const Containers::String& relativePath, + const Containers::String& importerName, + Core::uint32 importerVersion) { + Containers::String signature = relativePath; + signature += ":"; + signature += importerName; + signature += ":"; + signature += Containers::String(std::to_string(importerVersion).c_str()); + return HashStringToAssetGUID(signature).ToString(); +} + +const char* GetShaderStageLabel(ShaderType stage) { + switch (stage) { + case ShaderType::Vertex: + return "Vertex"; + case ShaderType::Fragment: + return "Fragment"; + case ShaderType::Geometry: + return "Geometry"; + case ShaderType::Compute: + return "Compute"; + case ShaderType::Hull: + return "Hull"; + case ShaderType::Domain: + return "Domain"; + default: + return "Unknown"; + } +} + +const char* GetShaderBackendLabel(ShaderBackend backend) { + switch (backend) { + case ShaderBackend::D3D12: + return "D3D12"; + case ShaderBackend::OpenGL: + return "OpenGL"; + case ShaderBackend::Vulkan: + return "Vulkan"; + case ShaderBackend::Generic: + default: + return "Generic"; + } +} + +void AppendCompiledPayload( + const std::vector& source, + Containers::Array& destination) { + destination.Resize(source.size()); + if (!source.empty()) { + std::memcpy(destination.Data(), source.data(), source.size()); + } +} + +void AppendCompiledPayload( + const std::vector& source, + Containers::Array& destination) { + destination.Resize(source.size() * sizeof(uint32_t)); + if (!source.empty()) { + std::memcpy(destination.Data(), source.data(), destination.Size()); + } +} + +std::string NarrowAscii(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 BuildD3D12MacroTable(const RHI::ShaderCompileDesc& desc, + std::vector& outNames, + std::vector& outDefinitions, + std::vector& outTable) { + outNames.clear(); + outDefinitions.clear(); + outTable.clear(); + + if (desc.macros.empty()) { + return true; + } + + outNames.reserve(desc.macros.size()); + outDefinitions.reserve(desc.macros.size()); + outTable.reserve(desc.macros.size() + 1u); + for (const RHI::ShaderCompileMacro& macro : desc.macros) { + std::string name; + name.reserve(macro.name.size()); + for (wchar_t ch : macro.name) { + name.push_back(static_cast(ch)); + } + if (name.empty()) { + continue; + } + + std::string definition; + definition.reserve(macro.definition.size()); + for (wchar_t ch : macro.definition) { + definition.push_back(static_cast(ch)); + } + + outNames.push_back(std::move(name)); + outDefinitions.push_back(std::move(definition)); + } + + for (size_t macroIndex = 0; macroIndex < outNames.size(); ++macroIndex) { + D3D_SHADER_MACRO macro = {}; + macro.Name = outNames[macroIndex].c_str(); + macro.Definition = + outDefinitions[macroIndex].empty() ? "1" : outDefinitions[macroIndex].c_str(); + outTable.push_back(macro); + } + outTable.push_back({ nullptr, nullptr }); + return true; +} + +bool TryPrecompileShaderVariantForBackend(const Containers::String& shaderPath, + const ShaderPass& pass, + ShaderStageVariant& variant, + ShaderBackend targetBackend, + Containers::String& outError) { + outError.Clear(); + + RHI::ShaderCompileDesc compileDesc = {}; + Rendering::Internal::ApplyShaderStageVariant( + shaderPath, + pass, + targetBackend, + variant, + compileDesc); + + if (targetBackend == ShaderBackend::D3D12) { + if (compileDesc.sourceLanguage != RHI::ShaderLanguage::HLSL) { + outError = "D3D12 precompile requires HLSL input."; + return false; + } + + const std::string entryPoint = NarrowAscii(compileDesc.entryPoint); + const std::string profile = NarrowAscii(compileDesc.profile); + + std::vector macroNames; + std::vector macroDefinitions; + std::vector macroTable; + BuildD3D12MacroTable(compileDesc, macroNames, macroDefinitions, macroTable); + const D3D_SHADER_MACRO* macroPtr = macroTable.empty() ? nullptr : macroTable.data(); + + RHI::D3D12Shader compiledShader; + if (!compiledShader.Compile( + compileDesc.source.data(), + compileDesc.source.size(), + compileDesc.fileName.empty() ? nullptr : compileDesc.fileName.c_str(), + macroPtr, + entryPoint.empty() ? nullptr : entryPoint.c_str(), + profile.empty() ? nullptr : profile.c_str())) { + outError = "FXC compile failed."; + return false; + } + + Containers::Array payload; + payload.Resize(compiledShader.GetBytecodeSize()); + if (compiledShader.GetBytecodeSize() > 0) { + std::memcpy( + payload.Data(), + compiledShader.GetBytecode(), + compiledShader.GetBytecodeSize()); + } + variant.SetCompiledBinaryForBackend(targetBackend, std::move(payload)); + return true; + } + + RHI::CompiledSpirvShader compiledShader = {}; + std::string errorMessage; + const bool compiled = + targetBackend == ShaderBackend::Vulkan + ? RHI::CompileVulkanShader(compileDesc, compiledShader, &errorMessage) + : RHI::CompileSpirvShader( + compileDesc, + RHI::SpirvTargetEnvironment::OpenGL, + compiledShader, + &errorMessage); + if (!compiled) { + outError = Containers::String( + errorMessage.empty() ? "SPIR-V compile failed." : errorMessage.c_str()); + return false; + } + + Containers::Array payload; + AppendCompiledPayload(compiledShader.spirvWords, payload); + variant.SetCompiledBinaryForBackend(targetBackend, std::move(payload)); + return true; +} + +void PrecompileShaderVariants(const Containers::String& shaderPath, Shader& shader) { + Containers::Array passNames; + passNames.Reserve(shader.GetPasses().Size()); + for (const ShaderPass& pass : shader.GetPasses()) { + passNames.PushBack(pass.name); + } + + for (const Containers::String& passName : passNames) { + ShaderPass* pass = shader.FindPass(passName); + if (pass == nullptr) { + continue; + } + + for (ShaderStageVariant& variant : pass->variants) { + Containers::Array targetBackends; + if (variant.backend == ShaderBackend::Generic) { + targetBackends.PushBack(ShaderBackend::D3D12); + targetBackends.PushBack(ShaderBackend::OpenGL); + targetBackends.PushBack(ShaderBackend::Vulkan); + } else { + targetBackends.PushBack(variant.backend); + } + + for (ShaderBackend targetBackend : targetBackends) { + Containers::String errorMessage; + if (TryPrecompileShaderVariantForBackend( + shaderPath, + *pass, + variant, + targetBackend, + errorMessage)) { + continue; + } + + Debug::Logger::Get().Warning( + Debug::LogCategory::Rendering, + Containers::String("[AssetDatabase] Shader backend precompile skipped path=") + + shaderPath + + " pass=" + + passName + + " stage=" + + GetShaderStageLabel(variant.stage) + + " backend=" + + GetShaderBackendLabel(targetBackend) + + " reason=" + + errorMessage); + } + } + } +} + +std::vector CollectBuiltinShaderAssetPaths() { + std::vector paths; + paths.reserve(14u); + + Containers::String resolvedPath; + const Containers::String builtinShaderPaths[] = { + GetBuiltinForwardLitShaderPath(), + GetBuiltinUnlitShaderPath(), + GetBuiltinDepthOnlyShaderPath(), + GetBuiltinShadowCasterShaderPath(), + GetBuiltinObjectIdShaderPath(), + GetBuiltinObjectIdOutlineShaderPath(), + GetBuiltinSelectionMaskShaderPath(), + GetBuiltinSelectionOutlineShaderPath(), + GetBuiltinSkyboxShaderPath(), + GetBuiltinGaussianSplatShaderPath(), + GetBuiltinGaussianSplatUtilitiesShaderPath(), + GetBuiltinVolumetricShaderPath(), + GetBuiltinColorScalePostProcessShaderPath(), + GetBuiltinFinalColorShaderPath() + }; + + for (const Containers::String& builtinPath : builtinShaderPaths) { + if (TryResolveBuiltinShaderAssetPath(builtinPath, resolvedPath) && !resolvedPath.Empty()) { + paths.push_back(fs::path(resolvedPath.CStr())); + } + } + + return paths; +} + std::string TrimCopy(const std::string& text) { const auto begin = std::find_if_not(text.begin(), text.end(), [](unsigned char ch) { return std::isspace(ch) != 0; @@ -714,6 +1008,8 @@ bool WriteShaderArtifactFile(const fs::path& artifactPath, const Shader& shader) variantHeader.keywordCount = static_cast(variant.requiredKeywords.enabledKeywords.Size()); variantHeader.compiledBinarySize = static_cast(variant.compiledBinary.Size()); + variantHeader.backendCompiledBinaryCount = + static_cast(variant.backendCompiledBinaries.Size()); output.write(reinterpret_cast(&variantHeader), sizeof(variantHeader)); WriteString(output, variant.entryPoint); @@ -727,6 +1023,17 @@ bool WriteShaderArtifactFile(const fs::path& artifactPath, const Shader& shader) reinterpret_cast(variant.compiledBinary.Data()), static_cast(variant.compiledBinary.Size())); } + for (const ShaderBackendCompiledBinary& record : variant.backendCompiledBinaries) { + ShaderBackendCompiledBinaryArtifactHeader binaryHeader; + binaryHeader.backend = static_cast(record.backend); + binaryHeader.compiledBinarySize = static_cast(record.payload.Size()); + output.write(reinterpret_cast(&binaryHeader), sizeof(binaryHeader)); + if (!record.payload.Empty()) { + output.write( + reinterpret_cast(record.payload.Data()), + static_cast(record.payload.Size())); + } + } } } @@ -843,12 +1150,7 @@ bool AssetDatabase::ResolvePath(const Containers::String& requestPath, const fs::path projectRootPath(m_projectRoot.CStr()); const fs::path relativePath = fs::relative(inputPath, projectRootPath, ec); if (!ec) { - const Containers::String normalizedRelative = NormalizePathString(relativePath); - if (normalizedRelative.StartsWith("Assets/") || normalizedRelative == "Assets") { - outRelativePath = normalizedRelative; - } else { - outRelativePath.Clear(); - } + outRelativePath = NormalizePathString(relativePath); } else { outRelativePath.Clear(); } @@ -1300,6 +1602,9 @@ AssetDatabase::MaintenanceStats AssetDatabase::ScanAssets() { if (fs::exists(assetsRootPath)) { ScanAssetPath(assetsRootPath, seenPaths); } + for (const fs::path& builtinShaderPath : CollectBuiltinShaderAssetPaths()) { + ScanAssetPath(builtinShaderPath, seenPaths); + } RemoveMissingRecords(seenPaths); stats.removedArtifactCount = CleanupOrphanedArtifacts(); SaveSourceAssetDB(); @@ -1446,6 +1751,47 @@ bool AssetDatabase::EnsureMetaForPath(const fs::path& sourcePath, outRecord.importerName = GetImporterNameForPath(relativePath, isFolder); outRecord.importerVersion = kCurrentImporterVersion; + if (UsesExternalSyntheticSourceRecord(relativePath)) { + if (!outRecord.guid.IsValid()) { + outRecord.guid = HashStringToAssetGUID(NormalizePathString(sourcePath)); + } + outRecord.metaPath.Clear(); + outRecord.metaHash = + BuildSyntheticMetaHash(relativePath, outRecord.importerName, outRecord.importerVersion); + + const auto duplicateGuidIt = m_sourcesByGuid.find(outRecord.guid); + if (duplicateGuidIt != m_sourcesByGuid.end() && + duplicateGuidIt->second.relativePath != relativePath) { + Containers::String duplicateSignature = NormalizePathString(sourcePath); + duplicateSignature += ":"; + duplicateSignature += relativePath; + outRecord.guid = HashStringToAssetGUID(duplicateSignature); + } + + if (isFolder) { + outRecord.sourceHash.Clear(); + outRecord.sourceFileSize = 0; + outRecord.sourceWriteTime = 0; + } else { + const Core::uint64 fileSize = GetFileSizeValue(sourcePath); + const Core::uint64 writeTime = GetFileWriteTimeValue(sourcePath); + if (existingIt != m_sourcesByPathKey.end() && + existingIt->second.sourceFileSize == fileSize && + existingIt->second.sourceWriteTime == writeTime && + !existingIt->second.sourceHash.Empty()) { + outRecord.sourceHash = existingIt->second.sourceHash; + } else { + outRecord.sourceHash = ComputeFileHash(sourcePath); + } + outRecord.sourceFileSize = fileSize; + outRecord.sourceWriteTime = writeTime; + } + + m_sourcesByPathKey[pathKey] = outRecord; + m_sourcesByGuid[outRecord.guid] = outRecord; + return true; + } + const fs::path metaPath(sourcePath.string() + ".meta"); outRecord.metaPath = NormalizeRelativePath(metaPath); const Containers::String expectedImporterName = GetImporterNameForPath(relativePath, isFolder); @@ -2100,10 +2446,14 @@ bool AssetDatabase::ImportShaderAsset(const SourceAssetRecord& sourceRecord, NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr()); LoadResult result = loader.Load(absolutePath); if (!result || result.resource == nullptr) { + if (!result.errorMessage.Empty()) { + SetLastErrorMessage(result.errorMessage); + } return false; } Shader* shader = static_cast(result.resource); + PrecompileShaderVariants(absolutePath, *shader); std::vector dependencies; if (!CollectShaderDependencies(sourceRecord, dependencies)) { delete shader; diff --git a/engine/src/Core/Asset/ResourceManager.cpp b/engine/src/Core/Asset/ResourceManager.cpp index 3d23d656..37d509b7 100644 --- a/engine/src/Core/Asset/ResourceManager.cpp +++ b/engine/src/Core/Asset/ResourceManager.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -548,6 +549,7 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path, } Containers::String loadPath = path; + Containers::String cachePath = path; AssetImportService::ImportedAsset resolvedAsset; ResourceType importableType = ResourceType::Unknown; const bool shouldUseProjectArtifact = @@ -560,6 +562,7 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path, resolvedAsset.artifactReady) { m_projectAssetIndex.RememberResolvedPath(resolvedAsset.assetGuid, resolvedAsset.relativePath); loadPath = resolvedAsset.runtimeLoadPath; + cachePath = loadPath; if (ShouldTraceResourcePath(path)) { Debug::Logger::Get().Info( Debug::LogCategory::FileSystem, @@ -568,6 +571,35 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path, " artifact=" + loadPath); } + } else if (type == ResourceType::Shader && IsBuiltinShaderPath(path)) { + Containers::String builtinShaderAssetPath; + if (TryResolveBuiltinShaderAssetPath(path, builtinShaderAssetPath)) { + ResourceType builtinImportableType = ResourceType::Unknown; + AssetImportService::ImportedAsset builtinResolvedAsset; + if (!m_resourceRoot.Empty() && + m_assetImportService.TryGetImportableResourceType( + builtinShaderAssetPath, + builtinImportableType) && + builtinImportableType == type && + m_assetImportService.EnsureArtifact( + builtinShaderAssetPath, + type, + builtinResolvedAsset) && + builtinResolvedAsset.artifactReady) { + loadPath = builtinResolvedAsset.runtimeLoadPath; + } else { + loadPath = builtinShaderAssetPath; + } + } + + if (ShouldTraceResourcePath(path)) { + Debug::Logger::Get().Info( + Debug::LogCategory::FileSystem, + Containers::String("[ResourceManager] LoadResource builtin shader path=") + + path + + " loadPath=" + + loadPath); + } } else if (ShouldTraceResourcePath(path)) { Debug::Logger::Get().Info( Debug::LogCategory::FileSystem, @@ -577,7 +609,7 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path, loadPath); } - const ResourceGUID guid = ResourceGUID::Generate(loadPath); + const ResourceGUID guid = ResourceGUID::Generate(cachePath); if (IResource* cached = FindInCache(guid)) { if (ShouldTraceResourcePath(path)) { diff --git a/engine/src/RHI/D3D12/D3D12Shader.cpp b/engine/src/RHI/D3D12/D3D12Shader.cpp index 94324ef2..3065db4c 100644 --- a/engine/src/RHI/D3D12/D3D12Shader.cpp +++ b/engine/src/RHI/D3D12/D3D12Shader.cpp @@ -24,6 +24,33 @@ std::string NarrowAscii(const std::wstring& value) { return result; } +ShaderType ResolveShaderTypeFromTarget(const char* target) { + if (target == nullptr) { + return ShaderType::Vertex; + } + + if (strstr(target, "vs_")) { + return ShaderType::Vertex; + } + if (strstr(target, "ps_")) { + return ShaderType::Fragment; + } + if (strstr(target, "gs_")) { + return ShaderType::Geometry; + } + if (strstr(target, "cs_")) { + return ShaderType::Compute; + } + if (strstr(target, "hs_")) { + return ShaderType::Hull; + } + if (strstr(target, "ds_")) { + return ShaderType::Domain; + } + + return ShaderType::Vertex; +} + class D3D12ShaderIncludeHandler final : public ID3DInclude { public: explicit D3D12ShaderIncludeHandler(const std::filesystem::path& sourcePath) @@ -127,15 +154,7 @@ bool D3D12Shader::CompileFromFile(const wchar_t* filePath, const char* entryPoin return false; } - if (strstr(target, "vs_")) { - m_type = ShaderType::Vertex; - } else if (strstr(target, "ps_")) { - m_type = ShaderType::Fragment; - } else if (strstr(target, "gs_")) { - m_type = ShaderType::Geometry; - } else if (strstr(target, "cs_")) { - m_type = ShaderType::Compute; - } + m_type = ResolveShaderTypeFromTarget(target); m_uniformsCached = false; return true; @@ -182,16 +201,29 @@ bool D3D12Shader::Compile(const void* sourceData, return false; } - if (strstr(target, "vs_")) { - m_type = ShaderType::Vertex; - } else if (strstr(target, "ps_")) { - m_type = ShaderType::Fragment; - } else if (strstr(target, "gs_")) { - m_type = ShaderType::Geometry; - } else if (strstr(target, "cs_")) { - m_type = ShaderType::Compute; + m_type = ResolveShaderTypeFromTarget(target); + + m_uniformsCached = false; + return true; +} + +bool D3D12Shader::InitializeFromBytecode(const void* bytecodeData, + size_t bytecodeSize, + const char* target) { + if (bytecodeData == nullptr || bytecodeSize == 0) { + return false; } + Shutdown(); + + ComPtr blob; + if (FAILED(D3DCreateBlob(bytecodeSize, &blob)) || blob == nullptr) { + return false; + } + + std::memcpy(blob->GetBufferPointer(), bytecodeData, bytecodeSize); + m_bytecode = std::move(blob); + m_type = ResolveShaderTypeFromTarget(target); m_uniformsCached = false; return true; } diff --git a/engine/src/RHI/ShaderCompiler/SpirvShaderCompiler.cpp b/engine/src/RHI/ShaderCompiler/SpirvShaderCompiler.cpp index 90779294..768d2840 100644 --- a/engine/src/RHI/ShaderCompiler/SpirvShaderCompiler.cpp +++ b/engine/src/RHI/ShaderCompiler/SpirvShaderCompiler.cpp @@ -272,6 +272,21 @@ bool IsHlslInput(const ShaderCompileDesc& desc) { return extension == ".hlsl"; } +bool HasMatchingCompiledSpirvBinary(const ShaderCompileDesc& desc, + SpirvTargetEnvironment targetEnvironment) { + if (desc.compiledBinary.empty()) { + return false; + } + + switch (targetEnvironment) { + case SpirvTargetEnvironment::OpenGL: + return desc.compiledBinaryBackend == ShaderBinaryBackend::OpenGL; + case SpirvTargetEnvironment::Vulkan: + default: + return desc.compiledBinaryBackend == ShaderBinaryBackend::Vulkan; + } +} + std::wstring GetEnvironmentVariableValue(const wchar_t* name) { const DWORD length = GetEnvironmentVariableW(name, nullptr, 0); if (length == 0) { @@ -1021,16 +1036,19 @@ bool CompileSpirvShader(const ShaderCompileDesc& desc, outShader.entryPoint = "main"; } - if (desc.source.empty() && desc.fileName.empty()) { + if (desc.source.empty() && desc.fileName.empty() && + !HasMatchingCompiledSpirvBinary(desc, targetEnvironment)) { if (errorMessage != nullptr) { *errorMessage = "No shader source or file name was provided."; } return false; } - if (IsSpirvInput(desc)) { + if (IsSpirvInput(desc) || HasMatchingCompiledSpirvBinary(desc, targetEnvironment)) { std::vector bytes; - if (!desc.source.empty()) { + if (HasMatchingCompiledSpirvBinary(desc, targetEnvironment)) { + bytes = desc.compiledBinary; + } else if (!desc.source.empty()) { bytes = desc.source; } else if (!LoadBinaryFile(std::filesystem::path(desc.fileName), bytes)) { if (errorMessage != nullptr) { diff --git a/engine/src/Rendering/Internal/ShaderVariantUtils.h b/engine/src/Rendering/Internal/ShaderVariantUtils.h index e30b42f8..348608ef 100644 --- a/engine/src/Rendering/Internal/ShaderVariantUtils.h +++ b/engine/src/Rendering/Internal/ShaderVariantUtils.h @@ -39,6 +39,20 @@ inline RHI::ShaderLanguage ToRHIShaderLanguage(Resources::ShaderLanguage languag } } +inline RHI::ShaderBinaryBackend ToRHIShaderBinaryBackend(Resources::ShaderBackend backend) { + switch (backend) { + case Resources::ShaderBackend::D3D12: + return RHI::ShaderBinaryBackend::D3D12; + case Resources::ShaderBackend::OpenGL: + return RHI::ShaderBinaryBackend::OpenGL; + case Resources::ShaderBackend::Vulkan: + return RHI::ShaderBinaryBackend::Vulkan; + case Resources::ShaderBackend::Generic: + default: + return RHI::ShaderBinaryBackend::Unknown; + } +} + inline std::wstring ToWideAscii(const Containers::String& value) { std::wstring wide; wide.reserve(value.Length()); @@ -360,6 +374,13 @@ inline void ApplyShaderStageVariant( compileDesc.sourceLanguage = ToRHIShaderLanguage(variant.language); compileDesc.entryPoint = ToWideAscii(variant.entryPoint); compileDesc.profile = ToWideAscii(variant.profile); + compileDesc.compiledBinaryBackend = ToRHIShaderBinaryBackend(variant.backend); + compileDesc.compiledBinary.clear(); + if (!variant.compiledBinary.Empty()) { + compileDesc.compiledBinary.assign( + variant.compiledBinary.Data(), + variant.compiledBinary.Data() + variant.compiledBinary.Size()); + } } inline std::wstring ResolveRuntimeShaderSourcePath(const Containers::String& shaderPath) { @@ -398,6 +419,12 @@ inline void ApplyShaderStageVariant( compileDesc.sourceLanguage = ToRHIShaderLanguage(variant.language); compileDesc.entryPoint = ToWideAscii(variant.entryPoint); compileDesc.profile = ToWideAscii(variant.profile); + compileDesc.compiledBinaryBackend = RHI::ShaderBinaryBackend::Unknown; + compileDesc.compiledBinary.clear(); + if (const Containers::Array* binary = variant.GetCompiledBinaryForBackend(backend)) { + compileDesc.compiledBinaryBackend = ToRHIShaderBinaryBackend(backend); + compileDesc.compiledBinary.assign(binary->Data(), binary->Data() + binary->Size()); + } InjectShaderBackendMacros(backend, compileDesc); } diff --git a/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp b/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp index 529689b7..bf2a9210 100644 --- a/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp +++ b/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp @@ -9,6 +9,7 @@ #include "Rendering/FrameData/VisibleGaussianSplatItem.h" #include "Rendering/Internal/RenderSurfacePipelineUtils.h" #include "Rendering/Internal/ShaderVariantUtils.h" +#include "Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.h" #include "Rendering/RenderSurface.h" #include "Resources/BuiltinResources.h" #include "Resources/GaussianSplat/GaussianSplat.h" @@ -49,6 +50,21 @@ const Resources::ShaderPass* FindCompatibleGaussianSplatPass( return nullptr; } +const Resources::ShaderPass* FindCompatibleComputePass( + const Resources::Shader& shader, + const Containers::String& passName, + const Resources::ShaderKeywordSet& keywordSet, + Resources::ShaderBackend backend) { + const Resources::ShaderPass* shaderPass = shader.FindPass(passName); + if (shaderPass == nullptr) { + return nullptr; + } + + return shader.FindVariant(passName, Resources::ShaderType::Compute, backend, keywordSet) != nullptr + ? shaderPass + : nullptr; +} + RHI::GraphicsPipelineDesc CreatePipelineDesc( RHI::RHIType backendType, RHI::RHIPipelineLayout* pipelineLayout, @@ -89,6 +105,88 @@ RHI::GraphicsPipelineDesc CreatePipelineDesc( return pipelineDesc; } +RHI::ComputePipelineDesc CreateComputePipelineDesc( + RHI::RHIType backendType, + RHI::RHIPipelineLayout* pipelineLayout, + const Resources::Shader& shader, + const Resources::ShaderPass& shaderPass, + const Containers::String& passName, + const Resources::ShaderKeywordSet& keywordSet) { + RHI::ComputePipelineDesc pipelineDesc = {}; + pipelineDesc.pipelineLayout = pipelineLayout; + + const Resources::ShaderBackend backend = ::XCEngine::Rendering::Internal::ToShaderBackend(backendType); + if (const Resources::ShaderStageVariant* computeVariant = + shader.FindVariant(passName, Resources::ShaderType::Compute, backend, keywordSet)) { + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( + shader.GetPath(), + shaderPass, + backend, + *computeVariant, + pipelineDesc.computeShader); + } + + return pipelineDesc; +} + +const RHI::DescriptorSetLayoutBinding* FindSetLayoutBinding( + const BuiltinPassSetLayoutMetadata& setLayout, + Core::uint32 binding) { + for (const RHI::DescriptorSetLayoutBinding& layoutBinding : setLayout.bindings) { + if (layoutBinding.binding == binding) { + return &layoutBinding; + } + } + + return nullptr; +} + +RHI::RHIResourceView* ResolveWorkingSetView( + const Internal::BuiltinGaussianSplatPassResources::CachedBufferView& bufferView, + const RHI::DescriptorSetLayoutBinding* layoutBinding) { + if (layoutBinding == nullptr) { + return nullptr; + } + + switch (static_cast(layoutBinding->type)) { + case RHI::DescriptorType::UAV: + return bufferView.unorderedAccessView; + case RHI::DescriptorType::SRV: + return bufferView.shaderResourceView; + default: + return nullptr; + } +} + +template +void BindDescriptorSetRanges( + Core::uint32 firstDescriptorSet, + std::vector& descriptorSets, + BindFn&& bindFn) { + const Core::uint32 descriptorSetCount = static_cast(descriptorSets.size()); + Core::uint32 rangeStart = 0u; + while (rangeStart < descriptorSetCount) { + while (rangeStart < descriptorSetCount && descriptorSets[rangeStart] == nullptr) { + ++rangeStart; + } + if (rangeStart >= descriptorSetCount) { + break; + } + + Core::uint32 rangeCount = 0u; + while ((rangeStart + rangeCount) < descriptorSetCount && + descriptorSets[rangeStart + rangeCount] != nullptr) { + ++rangeCount; + } + + bindFn( + firstDescriptorSet + rangeStart, + rangeCount, + descriptorSets.data() + rangeStart); + rangeStart += rangeCount; + } +} + } // namespace BuiltinGaussianSplatPass::~BuiltinGaussianSplatPass() { @@ -124,6 +222,19 @@ bool BuiltinGaussianSplatPass::PrepareGaussianSplatResources( cachedGaussianSplat->color.shaderResourceView == nullptr) { return false; } + + if (m_passResources == nullptr) { + return false; + } + + Internal::BuiltinGaussianSplatPassResources::WorkingSet* workingSet = nullptr; + if (!m_passResources->EnsureWorkingSet(m_device, visibleGaussianSplat, workingSet) || + workingSet == nullptr || + workingSet->sortDistances.unorderedAccessView == nullptr || + workingSet->orderIndices.shaderResourceView == nullptr || + workingSet->orderIndices.unorderedAccessView == nullptr) { + return false; + } } return true; @@ -177,6 +288,13 @@ bool BuiltinGaussianSplatPass::Execute(const RenderPassContext& context) { commandList->SetPrimitiveTopology(RHI::PrimitiveTopology::TriangleList); for (const VisibleGaussianSplatItem& visibleGaussianSplat : context.sceneData.visibleGaussianSplats) { + if (!PrepareVisibleGaussianSplat( + context.renderContext, + context.sceneData, + visibleGaussianSplat)) { + return false; + } + if (!DrawVisibleGaussianSplat( context.renderContext, context.surface, @@ -222,6 +340,16 @@ bool BuiltinGaussianSplatPass::CreateResources(const RenderContext& context) { return false; } + m_builtinGaussianSplatUtilitiesShader = Resources::ResourceManager::Get().Load( + Resources::GetBuiltinGaussianSplatUtilitiesShaderPath()); + if (!m_builtinGaussianSplatUtilitiesShader.IsValid()) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPass failed to load builtin gaussian splat utilities shader resource"); + DestroyResources(); + return false; + } + m_builtinGaussianSplatMaterial = std::make_unique(); Resources::IResource::ConstructParams params = {}; params.name = "BuiltinGaussianSplatMaterial"; @@ -230,10 +358,17 @@ bool BuiltinGaussianSplatPass::CreateResources(const RenderContext& context) { m_builtinGaussianSplatMaterial->Initialize(params); m_builtinGaussianSplatMaterial->SetShader(m_builtinGaussianSplatShader); m_builtinGaussianSplatMaterial->SetRenderQueue(Resources::MaterialRenderQueue::Transparent); + m_passResources = new Internal::BuiltinGaussianSplatPassResources(); return true; } void BuiltinGaussianSplatPass::DestroyResources() { + if (m_passResources != nullptr) { + m_passResources->Shutdown(); + delete m_passResources; + m_passResources = nullptr; + } + m_resourceCache.Shutdown(); for (auto& pipelinePair : m_pipelineStates) { @@ -244,6 +379,14 @@ void BuiltinGaussianSplatPass::DestroyResources() { } m_pipelineStates.clear(); + for (auto& pipelinePair : m_computePipelineStates) { + if (pipelinePair.second != nullptr) { + pipelinePair.second->Shutdown(); + delete pipelinePair.second; + } + } + m_computePipelineStates.clear(); + for (auto& descriptorSetPair : m_dynamicDescriptorSets) { DestroyOwnedDescriptorSet(descriptorSetPair.second.descriptorSet); } @@ -255,6 +398,7 @@ void BuiltinGaussianSplatPass::DestroyResources() { m_passResourceLayouts.clear(); m_builtinGaussianSplatMaterial.reset(); + m_builtinGaussianSplatUtilitiesShader.Reset(); m_builtinGaussianSplatShader.Reset(); m_device = nullptr; m_backendType = RHI::RHIType::D3D12; @@ -301,9 +445,30 @@ BuiltinGaussianSplatPass::ResolvedShaderPass BuiltinGaussianSplatPass::ResolveGa return resolved; } +BuiltinGaussianSplatPass::ResolvedShaderPass BuiltinGaussianSplatPass::ResolvePrepareOrderShaderPass( + const RenderSceneData& sceneData) const { + ResolvedShaderPass resolved = {}; + if (!m_builtinGaussianSplatUtilitiesShader.IsValid()) { + return resolved; + } + + const Resources::Shader* shader = m_builtinGaussianSplatUtilitiesShader.Get(); + const Resources::ShaderBackend backend = ::XCEngine::Rendering::Internal::ToShaderBackend(m_backendType); + const Containers::String passName("GaussianSplatPrepareOrder"); + if (const Resources::ShaderPass* shaderPass = + FindCompatibleComputePass(*shader, passName, sceneData.globalShaderKeywords, backend)) { + resolved.shader = shader; + resolved.pass = shaderPass; + resolved.passName = passName; + } + + return resolved; +} + BuiltinGaussianSplatPass::PassResourceLayout* BuiltinGaussianSplatPass::GetOrCreatePassResourceLayout( const RenderContext& context, - const ResolvedShaderPass& resolvedShaderPass) { + const ResolvedShaderPass& resolvedShaderPass, + PassLayoutUsage usage) { if (resolvedShaderPass.shader == nullptr || resolvedShaderPass.pass == nullptr) { return nullptr; } @@ -345,21 +510,36 @@ BuiltinGaussianSplatPass::PassResourceLayout* BuiltinGaussianSplatPass::GetOrCre passLayout.descriptorSetCount = bindingPlan.descriptorSetCount; passLayout.perObject = bindingPlan.perObject; passLayout.material = bindingPlan.material; + passLayout.gaussianSplatSortDistanceBuffer = bindingPlan.gaussianSplatSortDistanceBuffer; + passLayout.gaussianSplatOrderBuffer = bindingPlan.gaussianSplatOrderBuffer; passLayout.gaussianSplatPositionBuffer = bindingPlan.gaussianSplatPositionBuffer; passLayout.gaussianSplatOtherBuffer = bindingPlan.gaussianSplatOtherBuffer; passLayout.gaussianSplatColorBuffer = bindingPlan.gaussianSplatColorBuffer; passLayout.gaussianSplatSHBuffer = bindingPlan.gaussianSplatSHBuffer; + passLayout.gaussianSplatViewDataBuffer = bindingPlan.gaussianSplatViewDataBuffer; if (!passLayout.perObject.IsValid()) { return failLayout("BuiltinGaussianSplatPass requires a PerObject resource binding"); } - if (!passLayout.material.IsValid()) { - return failLayout("BuiltinGaussianSplatPass requires a Material resource binding"); - } - if (!passLayout.gaussianSplatPositionBuffer.IsValid() || - !passLayout.gaussianSplatOtherBuffer.IsValid() || - !passLayout.gaussianSplatColorBuffer.IsValid()) { - return failLayout("BuiltinGaussianSplatPass requires position, other, and color gaussian splat buffer bindings"); + + if (usage == PassLayoutUsage::Draw) { + if (!passLayout.material.IsValid()) { + return failLayout("BuiltinGaussianSplatPass requires a Material resource binding"); + } + if (!passLayout.gaussianSplatOrderBuffer.IsValid() || + !passLayout.gaussianSplatPositionBuffer.IsValid() || + !passLayout.gaussianSplatOtherBuffer.IsValid() || + !passLayout.gaussianSplatColorBuffer.IsValid()) { + return failLayout( + "BuiltinGaussianSplatPass draw pass requires order, position, other, and color gaussian splat buffer bindings"); + } + } else if (usage == PassLayoutUsage::PrepareOrder) { + if (!passLayout.gaussianSplatSortDistanceBuffer.IsValid() || + !passLayout.gaussianSplatOrderBuffer.IsValid() || + !passLayout.gaussianSplatPositionBuffer.IsValid()) { + return failLayout( + "BuiltinGaussianSplatPass prepare-order pass requires sort distance, order, and position gaussian splat buffer bindings"); + } } std::vector nativeSetLayouts(passLayout.setLayouts.size()); @@ -392,7 +572,10 @@ RHI::RHIPipelineState* BuiltinGaussianSplatPass::GetOrCreatePipelineState( return nullptr; } - PassResourceLayout* passLayout = GetOrCreatePassResourceLayout(context, resolvedShaderPass); + PassResourceLayout* passLayout = GetOrCreatePassResourceLayout( + context, + resolvedShaderPass, + PassLayoutUsage::Draw); if (passLayout == nullptr || passLayout->pipelineLayout == nullptr) { return nullptr; } @@ -440,6 +623,54 @@ RHI::RHIPipelineState* BuiltinGaussianSplatPass::GetOrCreatePipelineState( return pipelineState; } +RHI::RHIPipelineState* BuiltinGaussianSplatPass::GetOrCreateComputePipelineState( + const RenderContext& context, + const RenderSceneData& sceneData) { + const ResolvedShaderPass resolvedShaderPass = ResolvePrepareOrderShaderPass(sceneData); + if (resolvedShaderPass.shader == nullptr || resolvedShaderPass.pass == nullptr) { + return nullptr; + } + + PassResourceLayout* passLayout = GetOrCreatePassResourceLayout( + context, + resolvedShaderPass, + PassLayoutUsage::PrepareOrder); + if (passLayout == nullptr || passLayout->pipelineLayout == nullptr) { + return nullptr; + } + + ComputePipelineKey pipelineKey = {}; + pipelineKey.shader = resolvedShaderPass.shader; + pipelineKey.passName = resolvedShaderPass.passName; + pipelineKey.keywordSignature = + ::XCEngine::Rendering::Internal::BuildShaderKeywordSignature(sceneData.globalShaderKeywords); + + const auto existing = m_computePipelineStates.find(pipelineKey); + if (existing != m_computePipelineStates.end()) { + return existing->second; + } + + const RHI::ComputePipelineDesc pipelineDesc = + CreateComputePipelineDesc( + context.backendType, + passLayout->pipelineLayout, + *resolvedShaderPass.shader, + *resolvedShaderPass.pass, + resolvedShaderPass.passName, + sceneData.globalShaderKeywords); + RHI::RHIPipelineState* pipelineState = context.device->CreateComputePipelineState(pipelineDesc); + if (pipelineState == nullptr || !pipelineState->IsValid()) { + if (pipelineState != nullptr) { + pipelineState->Shutdown(); + delete pipelineState; + } + return nullptr; + } + + m_computePipelineStates.emplace(pipelineKey, pipelineState); + return pipelineState; +} + bool BuiltinGaussianSplatPass::CreateOwnedDescriptorSet( const BuiltinPassSetLayoutMetadata& setLayout, OwnedDescriptorSet& descriptorSet) { @@ -468,14 +699,19 @@ BuiltinGaussianSplatPass::CachedDescriptorSet* BuiltinGaussianSplatPass::GetOrCr const BuiltinPassSetLayoutMetadata& setLayout, Core::uint32 setIndex, Core::uint64 objectId, + const Components::GaussianSplatRendererComponent* gaussianSplatRenderer, const Resources::Material* material, const Resources::GaussianSplat* gaussianSplat, const MaterialConstantPayloadView& materialConstants, - const RenderResourceCache::CachedGaussianSplat& cachedGaussianSplat) { + const RenderResourceCache::CachedGaussianSplat& cachedGaussianSplat, + RHI::RHIResourceView* sortDistanceView, + RHI::RHIResourceView* orderView, + RHI::RHIResourceView* viewDataView) { DynamicDescriptorSetKey key = {}; key.passLayout = passLayoutKey; key.setIndex = setIndex; key.objectId = objectId; + key.gaussianSplatRenderer = gaussianSplatRenderer; key.material = material; key.gaussianSplat = gaussianSplat; @@ -486,6 +722,41 @@ BuiltinGaussianSplatPass::CachedDescriptorSet* BuiltinGaussianSplatPass::GetOrCr } } + const Internal::BuiltinGaussianSplatPassResources::WorkingSet* workingSet = + (gaussianSplatRenderer != nullptr && m_passResources != nullptr) + ? m_passResources->FindWorkingSet(gaussianSplatRenderer) + : nullptr; + RHI::RHIResourceView* resolvedSortDistanceView = sortDistanceView; + RHI::RHIResourceView* resolvedOrderView = orderView; + RHI::RHIResourceView* resolvedViewDataView = viewDataView; + + if (setLayout.usesGaussianSplatSortDistanceBuffer && workingSet != nullptr) { + if (const RHI::DescriptorSetLayoutBinding* layoutBinding = + FindSetLayoutBinding(setLayout, passLayout.gaussianSplatSortDistanceBuffer.binding)) { + if (RHI::RHIResourceView* view = ResolveWorkingSetView(workingSet->sortDistances, layoutBinding)) { + resolvedSortDistanceView = view; + } + } + } + + if (setLayout.usesGaussianSplatOrderBuffer && workingSet != nullptr) { + if (const RHI::DescriptorSetLayoutBinding* layoutBinding = + FindSetLayoutBinding(setLayout, passLayout.gaussianSplatOrderBuffer.binding)) { + if (RHI::RHIResourceView* view = ResolveWorkingSetView(workingSet->orderIndices, layoutBinding)) { + resolvedOrderView = view; + } + } + } + + if (setLayout.usesGaussianSplatViewDataBuffer && workingSet != nullptr) { + if (const RHI::DescriptorSetLayoutBinding* layoutBinding = + FindSetLayoutBinding(setLayout, passLayout.gaussianSplatViewDataBuffer.binding)) { + if (RHI::RHIResourceView* view = ResolveWorkingSetView(workingSet->viewData, layoutBinding)) { + resolvedViewDataView = view; + } + } + } + const Core::uint64 materialVersion = material != nullptr ? material->GetChangeVersion() : 0u; if (setLayout.usesMaterial) { if (!passLayout.material.IsValid() || @@ -516,6 +787,34 @@ BuiltinGaussianSplatPass::CachedDescriptorSet* BuiltinGaussianSplatPass::GetOrCr } } + if (setLayout.usesGaussianSplatSortDistanceBuffer) { + if (resolvedSortDistanceView == nullptr || + !passLayout.gaussianSplatSortDistanceBuffer.IsValid() || + passLayout.gaussianSplatSortDistanceBuffer.set != setIndex) { + return nullptr; + } + + if (cachedDescriptorSet.sortDistanceView != resolvedSortDistanceView) { + cachedDescriptorSet.descriptorSet.set->Update( + passLayout.gaussianSplatSortDistanceBuffer.binding, + resolvedSortDistanceView); + } + } + + if (setLayout.usesGaussianSplatOrderBuffer) { + if (resolvedOrderView == nullptr || + !passLayout.gaussianSplatOrderBuffer.IsValid() || + passLayout.gaussianSplatOrderBuffer.set != setIndex) { + return nullptr; + } + + if (cachedDescriptorSet.orderView != resolvedOrderView) { + cachedDescriptorSet.descriptorSet.set->Update( + passLayout.gaussianSplatOrderBuffer.binding, + resolvedOrderView); + } + } + if (setLayout.usesGaussianSplatOtherBuffer) { if (cachedGaussianSplat.other.shaderResourceView == nullptr || !passLayout.gaussianSplatOtherBuffer.IsValid() || @@ -558,11 +857,28 @@ BuiltinGaussianSplatPass::CachedDescriptorSet* BuiltinGaussianSplatPass::GetOrCr } } + if (setLayout.usesGaussianSplatViewDataBuffer) { + if (resolvedViewDataView == nullptr || + !passLayout.gaussianSplatViewDataBuffer.IsValid() || + passLayout.gaussianSplatViewDataBuffer.set != setIndex) { + return nullptr; + } + + if (cachedDescriptorSet.viewDataView != resolvedViewDataView) { + cachedDescriptorSet.descriptorSet.set->Update( + passLayout.gaussianSplatViewDataBuffer.binding, + resolvedViewDataView); + } + } + cachedDescriptorSet.materialVersion = materialVersion; + cachedDescriptorSet.sortDistanceView = resolvedSortDistanceView; + cachedDescriptorSet.orderView = resolvedOrderView; cachedDescriptorSet.positionsView = cachedGaussianSplat.positions.shaderResourceView; cachedDescriptorSet.otherView = cachedGaussianSplat.other.shaderResourceView; cachedDescriptorSet.colorView = cachedGaussianSplat.color.shaderResourceView; cachedDescriptorSet.shView = cachedGaussianSplat.sh.shaderResourceView; + cachedDescriptorSet.viewDataView = resolvedViewDataView; return &cachedDescriptorSet; } @@ -592,10 +908,169 @@ void BuiltinGaussianSplatPass::DestroyPassResourceLayout(PassResourceLayout& pas passLayout.descriptorSetCount = 0; passLayout.perObject = {}; passLayout.material = {}; + passLayout.gaussianSplatSortDistanceBuffer = {}; + passLayout.gaussianSplatOrderBuffer = {}; passLayout.gaussianSplatPositionBuffer = {}; passLayout.gaussianSplatOtherBuffer = {}; passLayout.gaussianSplatColorBuffer = {}; passLayout.gaussianSplatSHBuffer = {}; + passLayout.gaussianSplatViewDataBuffer = {}; +} + +bool BuiltinGaussianSplatPass::PrepareVisibleGaussianSplat( + const RenderContext& context, + const RenderSceneData& sceneData, + const VisibleGaussianSplatItem& visibleGaussianSplat) { + if (visibleGaussianSplat.gameObject == nullptr || + visibleGaussianSplat.gaussianSplat == nullptr || + !visibleGaussianSplat.gaussianSplat->IsValid() || + m_passResources == nullptr) { + return false; + } + + const RenderResourceCache::CachedGaussianSplat* cachedGaussianSplat = + m_resourceCache.GetOrCreateGaussianSplat(m_device, visibleGaussianSplat.gaussianSplat); + if (cachedGaussianSplat == nullptr || cachedGaussianSplat->positions.shaderResourceView == nullptr) { + return false; + } + + Internal::BuiltinGaussianSplatPassResources::WorkingSet* workingSet = nullptr; + if (!m_passResources->EnsureWorkingSet(m_device, visibleGaussianSplat, workingSet) || + workingSet == nullptr || + workingSet->sortDistances.unorderedAccessView == nullptr || + workingSet->orderIndices.unorderedAccessView == nullptr || + workingSet->orderIndices.shaderResourceView == nullptr) { + return false; + } + + const ResolvedShaderPass resolvedShaderPass = ResolvePrepareOrderShaderPass(sceneData); + if (resolvedShaderPass.shader == nullptr || resolvedShaderPass.pass == nullptr) { + return false; + } + + PassLayoutKey passLayoutKey = {}; + passLayoutKey.shader = resolvedShaderPass.shader; + passLayoutKey.passName = resolvedShaderPass.passName; + + PassResourceLayout* passLayout = GetOrCreatePassResourceLayout( + context, + resolvedShaderPass, + PassLayoutUsage::PrepareOrder); + RHI::RHIPipelineState* pipelineState = GetOrCreateComputePipelineState(context, sceneData); + if (passLayout == nullptr || pipelineState == nullptr) { + return false; + } + + RHI::RHICommandList* commandList = context.commandList; + + if (workingSet->sortDistances.currentState != RHI::ResourceStates::UnorderedAccess) { + commandList->TransitionBarrier( + workingSet->sortDistances.unorderedAccessView, + workingSet->sortDistances.currentState, + RHI::ResourceStates::UnorderedAccess); + workingSet->sortDistances.currentState = RHI::ResourceStates::UnorderedAccess; + } + + if (workingSet->orderIndices.currentState != RHI::ResourceStates::UnorderedAccess) { + commandList->TransitionBarrier( + workingSet->orderIndices.unorderedAccessView, + workingSet->orderIndices.currentState, + RHI::ResourceStates::UnorderedAccess); + workingSet->orderIndices.currentState = RHI::ResourceStates::UnorderedAccess; + } + + commandList->SetPipelineState(pipelineState); + + const PerObjectConstants perObjectConstants = { + sceneData.cameraData.projection, + sceneData.cameraData.view, + visibleGaussianSplat.localToWorld.Transpose(), + Math::Vector4(sceneData.cameraData.worldRight, 0.0f), + Math::Vector4(sceneData.cameraData.worldUp, 0.0f) + }; + + if (passLayout->descriptorSetCount > 0u) { + std::vector descriptorSets(passLayout->descriptorSetCount, nullptr); + for (Core::uint32 descriptorOffset = 0u; descriptorOffset < passLayout->descriptorSetCount; ++descriptorOffset) { + const Core::uint32 setIndex = passLayout->firstDescriptorSet + descriptorOffset; + if (setIndex >= passLayout->setLayouts.size()) { + return false; + } + + const BuiltinPassSetLayoutMetadata& setLayout = passLayout->setLayouts[setIndex]; + if (setLayout.layout.bindingCount == 0u) { + continue; + } + + if (!(setLayout.usesPerObject || + setLayout.usesGaussianSplatSortDistanceBuffer || + setLayout.usesGaussianSplatOrderBuffer || + setLayout.usesGaussianSplatPositionBuffer)) { + return false; + } + + const Core::uint64 objectId = + setLayout.usesPerObject ? visibleGaussianSplat.gameObject->GetID() : 0u; + const Resources::GaussianSplat* gaussianSplatKey = + (setLayout.usesGaussianSplatSortDistanceBuffer || + setLayout.usesGaussianSplatOrderBuffer || + setLayout.usesGaussianSplatPositionBuffer) + ? visibleGaussianSplat.gaussianSplat + : nullptr; + + CachedDescriptorSet* cachedDescriptorSet = GetOrCreateDynamicDescriptorSet( + passLayoutKey, + *passLayout, + setLayout, + setIndex, + objectId, + visibleGaussianSplat.gaussianSplatRenderer, + nullptr, + gaussianSplatKey, + MaterialConstantPayloadView(), + *cachedGaussianSplat, + workingSet->sortDistances.unorderedAccessView, + workingSet->orderIndices.unorderedAccessView, + workingSet->viewData.unorderedAccessView); + if (cachedDescriptorSet == nullptr || cachedDescriptorSet->descriptorSet.set == nullptr) { + return false; + } + + RHI::RHIDescriptorSet* descriptorSet = cachedDescriptorSet->descriptorSet.set; + if (setLayout.usesPerObject) { + if (!passLayout->perObject.IsValid() || passLayout->perObject.set != setIndex) { + return false; + } + + descriptorSet->WriteConstant( + passLayout->perObject.binding, + &perObjectConstants, + sizeof(perObjectConstants)); + } + + descriptorSets[descriptorOffset] = descriptorSet; + } + + BindDescriptorSetRanges( + passLayout->firstDescriptorSet, + descriptorSets, + [commandList, passLayout](Core::uint32 firstSet, Core::uint32 count, RHI::RHIDescriptorSet** sets) { + commandList->SetComputeDescriptorSets( + firstSet, + count, + sets, + passLayout->pipelineLayout); + }); + } + + commandList->Dispatch((cachedGaussianSplat->splatCount + 63u) / 64u, 1u, 1u); + + commandList->TransitionBarrier( + workingSet->orderIndices.shaderResourceView, + RHI::ResourceStates::UnorderedAccess, + RHI::ResourceStates::NonPixelShaderResource); + workingSet->orderIndices.currentState = RHI::ResourceStates::NonPixelShaderResource; + return true; } bool BuiltinGaussianSplatPass::DrawVisibleGaussianSplat( @@ -618,6 +1093,17 @@ bool BuiltinGaussianSplatPass::DrawVisibleGaussianSplat( return false; } + if (m_passResources == nullptr) { + return false; + } + + Internal::BuiltinGaussianSplatPassResources::WorkingSet* workingSet = nullptr; + if (!m_passResources->EnsureWorkingSet(m_device, visibleGaussianSplat, workingSet) || + workingSet == nullptr || + workingSet->orderIndices.shaderResourceView == nullptr) { + return false; + } + const Resources::Material* material = ResolveGaussianSplatMaterial(visibleGaussianSplat); if (material == nullptr) { return false; @@ -632,7 +1118,10 @@ bool BuiltinGaussianSplatPass::DrawVisibleGaussianSplat( passLayoutKey.shader = resolvedShaderPass.shader; passLayoutKey.passName = resolvedShaderPass.passName; - PassResourceLayout* passLayout = GetOrCreatePassResourceLayout(context, resolvedShaderPass); + PassResourceLayout* passLayout = GetOrCreatePassResourceLayout( + context, + resolvedShaderPass, + PassLayoutUsage::Draw); RHI::RHIPipelineState* pipelineState = GetOrCreatePipelineState(context, surface, sceneData, material); if (passLayout == nullptr || pipelineState == nullptr) { return false; @@ -663,8 +1152,13 @@ bool BuiltinGaussianSplatPass::DrawVisibleGaussianSplat( } const BuiltinPassSetLayoutMetadata& setLayout = passLayout->setLayouts[setIndex]; + if (setLayout.layout.bindingCount == 0u) { + continue; + } + if (!(setLayout.usesPerObject || setLayout.usesMaterial || + setLayout.usesGaussianSplatOrderBuffer || setLayout.usesGaussianSplatPositionBuffer || setLayout.usesGaussianSplatOtherBuffer || setLayout.usesGaussianSplatColorBuffer || @@ -677,7 +1171,8 @@ bool BuiltinGaussianSplatPass::DrawVisibleGaussianSplat( const Resources::Material* materialKey = setLayout.usesMaterial ? material : nullptr; const Resources::GaussianSplat* gaussianSplatKey = - (setLayout.usesGaussianSplatPositionBuffer || + (setLayout.usesGaussianSplatOrderBuffer || + setLayout.usesGaussianSplatPositionBuffer || setLayout.usesGaussianSplatOtherBuffer || setLayout.usesGaussianSplatColorBuffer || setLayout.usesGaussianSplatSHBuffer) @@ -690,10 +1185,14 @@ bool BuiltinGaussianSplatPass::DrawVisibleGaussianSplat( setLayout, setIndex, objectId, + visibleGaussianSplat.gaussianSplatRenderer, materialKey, gaussianSplatKey, materialConstants, - *cachedGaussianSplat); + *cachedGaussianSplat, + nullptr, + workingSet->orderIndices.shaderResourceView, + workingSet->viewData.shaderResourceView); if (cachedDescriptorSet == nullptr || cachedDescriptorSet->descriptorSet.set == nullptr) { return false; } @@ -713,11 +1212,16 @@ bool BuiltinGaussianSplatPass::DrawVisibleGaussianSplat( descriptorSets[descriptorOffset] = descriptorSet; } - commandList->SetGraphicsDescriptorSets( + BindDescriptorSetRanges( passLayout->firstDescriptorSet, - passLayout->descriptorSetCount, - descriptorSets.data(), - passLayout->pipelineLayout); + descriptorSets, + [commandList, passLayout](Core::uint32 firstSet, Core::uint32 count, RHI::RHIDescriptorSet** sets) { + commandList->SetGraphicsDescriptorSets( + firstSet, + count, + sets, + passLayout->pipelineLayout); + }); } ApplyDynamicRenderState(ResolveEffectiveRenderState(resolvedShaderPass.pass, material), *commandList); diff --git a/engine/src/Resources/Shader/Internal/ShaderArtifactLoader.cpp b/engine/src/Resources/Shader/Internal/ShaderArtifactLoader.cpp index 9d35ca00..ff5f2983 100644 --- a/engine/src/Resources/Shader/Internal/ShaderArtifactLoader.cpp +++ b/engine/src/Resources/Shader/Internal/ShaderArtifactLoader.cpp @@ -7,6 +7,7 @@ #include #include +#include namespace XCEngine { namespace Resources { @@ -64,7 +65,7 @@ LoadResult LoadShaderArtifact(const Containers::String& path) { const std::string magic(fileHeader.magic, fileHeader.magic + 7); const bool isCurrentSchema = - magic == "XCSHD05" && fileHeader.schemaVersion == kShaderArtifactSchemaVersion; + magic == "XCSHD06" && fileHeader.schemaVersion == kShaderArtifactSchemaVersion; if (!isCurrentSchema) { return LoadResult("Invalid shader artifact header: " + path); } @@ -187,6 +188,7 @@ LoadResult LoadShaderArtifact(const Containers::String& path) { ShaderStageVariant variant = {}; Core::uint64 compiledBinarySize = 0; Core::uint32 keywordCount = 0; + Core::uint32 backendCompiledBinaryCount = 0; ShaderVariantArtifactHeader variantHeader = {}; if (!ReadShaderArtifactValue(data, offset, variantHeader)) { return LoadResult("Failed to read shader artifact variants: " + path); @@ -197,6 +199,7 @@ LoadResult LoadShaderArtifact(const Containers::String& path) { variant.backend = static_cast(variantHeader.backend); keywordCount = variantHeader.keywordCount; compiledBinarySize = variantHeader.compiledBinarySize; + backendCompiledBinaryCount = variantHeader.backendCompiledBinaryCount; if (!ReadShaderArtifactString(data, offset, variant.entryPoint) || !ReadShaderArtifactString(data, offset, variant.profile) || @@ -227,6 +230,32 @@ LoadResult LoadShaderArtifact(const Containers::String& path) { offset += static_cast(compiledBinarySize); } + for (Core::uint32 binaryIndex = 0; + binaryIndex < backendCompiledBinaryCount; + ++binaryIndex) { + ShaderBackendCompiledBinaryArtifactHeader binaryHeader = {}; + if (!ReadShaderArtifactValue(data, offset, binaryHeader)) { + return LoadResult("Failed to read shader artifact backend binaries: " + path); + } + + if (offset + binaryHeader.compiledBinarySize > data.Size()) { + return LoadResult( + "Shader artifact backend binary payload is truncated: " + path); + } + + ShaderBackendCompiledBinary record = {}; + record.backend = static_cast(binaryHeader.backend); + if (binaryHeader.compiledBinarySize > 0) { + record.payload.Resize(static_cast(binaryHeader.compiledBinarySize)); + std::memcpy( + record.payload.Data(), + data.Data() + offset, + static_cast(binaryHeader.compiledBinarySize)); + offset += static_cast(binaryHeader.compiledBinarySize); + } + variant.backendCompiledBinaries.PushBack(std::move(record)); + } + shader->AddPassVariant(passName, variant); } } diff --git a/engine/src/Resources/Shader/Shader.cpp b/engine/src/Resources/Shader/Shader.cpp index 50a04094..c41c8864 100644 --- a/engine/src/Resources/Shader/Shader.cpp +++ b/engine/src/Resources/Shader/Shader.cpp @@ -1,5 +1,7 @@ #include +#include + namespace XCEngine { namespace Resources { @@ -40,6 +42,62 @@ Shader::Shader() = default; Shader::~Shader() = default; +const Containers::Array* ShaderStageVariant::GetCompiledBinaryForBackend( + ShaderBackend targetBackend) const { + if ((targetBackend == backend || targetBackend == ShaderBackend::Generic) && + !compiledBinary.Empty()) { + return &compiledBinary; + } + + for (const ShaderBackendCompiledBinary& record : backendCompiledBinaries) { + if (record.backend == targetBackend && !record.payload.Empty()) { + return &record.payload; + } + } + + return nullptr; +} + +void ShaderStageVariant::SetCompiledBinaryForBackend( + ShaderBackend targetBackend, + const Containers::Array& binary) { + if (targetBackend == backend || targetBackend == ShaderBackend::Generic) { + compiledBinary = binary; + return; + } + + for (ShaderBackendCompiledBinary& record : backendCompiledBinaries) { + if (record.backend == targetBackend) { + record.payload = binary; + return; + } + } + + ShaderBackendCompiledBinary& record = backendCompiledBinaries.EmplaceBack(); + record.backend = targetBackend; + record.payload = binary; +} + +void ShaderStageVariant::SetCompiledBinaryForBackend( + ShaderBackend targetBackend, + Containers::Array&& binary) { + if (targetBackend == backend || targetBackend == ShaderBackend::Generic) { + compiledBinary = std::move(binary); + return; + } + + for (ShaderBackendCompiledBinary& record : backendCompiledBinaries) { + if (record.backend == targetBackend) { + record.payload = std::move(binary); + return; + } + } + + ShaderBackendCompiledBinary& record = backendCompiledBinaries.EmplaceBack(); + record.backend = targetBackend; + record.payload = std::move(binary); +} + void Shader::Release() { m_uniforms.Clear(); m_attributes.Clear(); diff --git a/tests/Rendering/unit/test_builtin_forward_pipeline.cpp b/tests/Rendering/unit/test_builtin_forward_pipeline.cpp index 11c2d317..5aa4176d 100644 --- a/tests/Rendering/unit/test_builtin_forward_pipeline.cpp +++ b/tests/Rendering/unit/test_builtin_forward_pipeline.cpp @@ -885,7 +885,7 @@ TEST(BuiltinForwardPipeline_Test, BuiltinGaussianSplatShaderUsesAuthoringContrac const ShaderPass* pass = shader->FindPass("GaussianSplat"); ASSERT_NE(pass, nullptr); - EXPECT_EQ(pass->resources.Size(), 5u); + EXPECT_EQ(pass->resources.Size(), 6u); EXPECT_TRUE(pass->hasFixedFunctionState); EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::None); EXPECT_FALSE(pass->fixedFunctionState.depthWriteEnable); @@ -900,12 +900,22 @@ TEST(BuiltinForwardPipeline_Test, BuiltinGaussianSplatShaderUsesAuthoringContrac ASSERT_NE(opacityScale, nullptr); EXPECT_EQ(opacityScale->type, ShaderPropertyType::Float); + const ShaderResourceBindingDesc* order = + shader->FindPassResourceBinding("GaussianSplat", "GaussianSplatOrderBuffer"); + ASSERT_NE(order, nullptr); + EXPECT_EQ(order->type, ShaderResourceType::StructuredBuffer); + EXPECT_EQ(order->set, 2u); + EXPECT_EQ(order->binding, 0u); + EXPECT_EQ( + ResolveBuiltinPassResourceSemantic(*order), + BuiltinPassResourceSemantic::GaussianSplatOrderBuffer); + const ShaderResourceBindingDesc* positions = shader->FindPassResourceBinding("GaussianSplat", "GaussianSplatPositions"); ASSERT_NE(positions, nullptr); EXPECT_EQ(positions->type, ShaderResourceType::StructuredBuffer); EXPECT_EQ(positions->set, 2u); - EXPECT_EQ(positions->binding, 0u); + EXPECT_EQ(positions->binding, 1u); EXPECT_EQ( ResolveBuiltinPassResourceSemantic(*positions), BuiltinPassResourceSemantic::GaussianSplatPositionBuffer); @@ -915,7 +925,7 @@ TEST(BuiltinForwardPipeline_Test, BuiltinGaussianSplatShaderUsesAuthoringContrac ASSERT_NE(other, nullptr); EXPECT_EQ(other->type, ShaderResourceType::StructuredBuffer); EXPECT_EQ(other->set, 2u); - EXPECT_EQ(other->binding, 1u); + EXPECT_EQ(other->binding, 2u); EXPECT_EQ( ResolveBuiltinPassResourceSemantic(*other), BuiltinPassResourceSemantic::GaussianSplatOtherBuffer); @@ -925,7 +935,7 @@ TEST(BuiltinForwardPipeline_Test, BuiltinGaussianSplatShaderUsesAuthoringContrac ASSERT_NE(color, nullptr); EXPECT_EQ(color->type, ShaderResourceType::StructuredBuffer); EXPECT_EQ(color->set, 2u); - EXPECT_EQ(color->binding, 2u); + EXPECT_EQ(color->binding, 3u); EXPECT_EQ( ResolveBuiltinPassResourceSemantic(*color), BuiltinPassResourceSemantic::GaussianSplatColorBuffer); @@ -948,21 +958,24 @@ TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromLoaded BuiltinPassResourceBindingPlan plan = {}; String error; EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(*pass, plan, &error)) << error.CStr(); - ASSERT_EQ(plan.bindings.Size(), 5u); + ASSERT_EQ(plan.bindings.Size(), 6u); EXPECT_TRUE(plan.perObject.IsValid()); EXPECT_TRUE(plan.material.IsValid()); + EXPECT_TRUE(plan.gaussianSplatOrderBuffer.IsValid()); EXPECT_TRUE(plan.gaussianSplatPositionBuffer.IsValid()); EXPECT_TRUE(plan.gaussianSplatOtherBuffer.IsValid()); EXPECT_TRUE(plan.gaussianSplatColorBuffer.IsValid()); EXPECT_FALSE(plan.gaussianSplatSHBuffer.IsValid()); EXPECT_EQ(plan.perObject.set, 0u); EXPECT_EQ(plan.material.set, 1u); + EXPECT_EQ(plan.gaussianSplatOrderBuffer.set, 2u); + EXPECT_EQ(plan.gaussianSplatOrderBuffer.binding, 0u); EXPECT_EQ(plan.gaussianSplatPositionBuffer.set, 2u); - EXPECT_EQ(plan.gaussianSplatPositionBuffer.binding, 0u); + EXPECT_EQ(plan.gaussianSplatPositionBuffer.binding, 1u); EXPECT_EQ(plan.gaussianSplatOtherBuffer.set, 2u); - EXPECT_EQ(plan.gaussianSplatOtherBuffer.binding, 1u); + EXPECT_EQ(plan.gaussianSplatOtherBuffer.binding, 2u); EXPECT_EQ(plan.gaussianSplatColorBuffer.set, 2u); - EXPECT_EQ(plan.gaussianSplatColorBuffer.binding, 2u); + EXPECT_EQ(plan.gaussianSplatColorBuffer.binding, 3u); EXPECT_EQ(plan.firstDescriptorSet, 0u); EXPECT_EQ(plan.descriptorSetCount, 3u); @@ -971,10 +984,11 @@ TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromLoaded ASSERT_EQ(setLayouts.size(), 3u); EXPECT_TRUE(setLayouts[0].usesPerObject); EXPECT_TRUE(setLayouts[1].usesMaterial); + EXPECT_TRUE(setLayouts[2].usesGaussianSplatOrderBuffer); EXPECT_TRUE(setLayouts[2].usesGaussianSplatPositionBuffer); EXPECT_TRUE(setLayouts[2].usesGaussianSplatOtherBuffer); EXPECT_TRUE(setLayouts[2].usesGaussianSplatColorBuffer); - ASSERT_EQ(setLayouts[2].bindings.size(), 3u); + ASSERT_EQ(setLayouts[2].bindings.size(), 4u); EXPECT_EQ( static_cast(setLayouts[2].bindings[0].type), DescriptorType::SRV); @@ -984,6 +998,9 @@ TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromLoaded EXPECT_EQ( static_cast(setLayouts[2].bindings[2].type), DescriptorType::SRV); + EXPECT_EQ( + static_cast(setLayouts[2].bindings[3].type), + DescriptorType::SRV); EXPECT_EQ( setLayouts[2].bindings[0].resourceDimension, ResourceViewDimension::StructuredBuffer); @@ -993,6 +1010,9 @@ TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromLoaded EXPECT_EQ( setLayouts[2].bindings[2].resourceDimension, ResourceViewDimension::StructuredBuffer); + EXPECT_EQ( + setLayouts[2].bindings[3].resourceDimension, + ResourceViewDimension::StructuredBuffer); delete shader; } @@ -1034,16 +1054,20 @@ TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringGauss "register(b1)")); EXPECT_TRUE(SourceContainsRegisterBinding( d3d12Source, - "StructuredBuffer GaussianSplatPositions", + "StructuredBuffer GaussianSplatOrderBuffer", "register(t0)")); EXPECT_TRUE(SourceContainsRegisterBinding( d3d12Source, - "StructuredBuffer GaussianSplatOther", + "StructuredBuffer GaussianSplatPositions", "register(t1)")); EXPECT_TRUE(SourceContainsRegisterBinding( d3d12Source, - "StructuredBuffer GaussianSplatColor", + "StructuredBuffer GaussianSplatOther", "register(t2)")); + EXPECT_TRUE(SourceContainsRegisterBinding( + d3d12Source, + "StructuredBuffer GaussianSplatColor", + "register(t3)")); EXPECT_EQ(d3d12Source.find("space0"), std::string::npos); EXPECT_EQ(d3d12Source.find("space1"), std::string::npos); EXPECT_EQ(d3d12Source.find("space2"), std::string::npos); @@ -1073,16 +1097,20 @@ TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringGauss "register(b0, space1)")); EXPECT_TRUE(SourceContainsRegisterBinding( vulkanSource, - "StructuredBuffer GaussianSplatPositions", + "StructuredBuffer GaussianSplatOrderBuffer", "register(t0, space2)")); EXPECT_TRUE(SourceContainsRegisterBinding( vulkanSource, - "StructuredBuffer GaussianSplatOther", + "StructuredBuffer GaussianSplatPositions", "register(t1, space2)")); EXPECT_TRUE(SourceContainsRegisterBinding( vulkanSource, - "StructuredBuffer GaussianSplatColor", + "StructuredBuffer GaussianSplatOther", "register(t2, space2)")); + EXPECT_TRUE(SourceContainsRegisterBinding( + vulkanSource, + "StructuredBuffer GaussianSplatColor", + "register(t3, space2)")); delete shader; } @@ -2410,6 +2438,57 @@ TEST(BuiltinPassLayout_Test, AcceptsRuntimeMaterialRawUavBufferBindings) { EXPECT_TRUE(setLayouts[4].usesMaterialBuffers); } +TEST(BuiltinPassLayout_Test, AcceptsRuntimeMaterialTextureBindingsWithoutBuiltinSemanticMetadata) { + Array bindings; + + ShaderResourceBindingDesc perObjectBinding = {}; + perObjectBinding.name = "PerObjectConstants"; + perObjectBinding.type = ShaderResourceType::ConstantBuffer; + perObjectBinding.set = 0u; + perObjectBinding.binding = 0u; + perObjectBinding.semantic = "PerObject"; + bindings.PushBack(perObjectBinding); + + ShaderResourceBindingDesc materialBinding = {}; + materialBinding.name = "MaterialConstants"; + materialBinding.type = ShaderResourceType::ConstantBuffer; + materialBinding.set = 1u; + materialBinding.binding = 0u; + materialBinding.semantic = "Material"; + bindings.PushBack(materialBinding); + + ShaderResourceBindingDesc textureBinding = {}; + textureBinding.name = "_LightMap"; + textureBinding.type = ShaderResourceType::Texture2D; + textureBinding.set = 4u; + textureBinding.binding = 1u; + bindings.PushBack(textureBinding); + + BuiltinPassResourceBindingPlan plan = {}; + String error; + ASSERT_TRUE(TryBuildBuiltinPassResourceBindingPlan(bindings, plan, &error)) << error.CStr(); + EXPECT_TRUE(plan.usesMaterialTextures); + ASSERT_EQ(plan.materialTextureBindings.Size(), 1u); + EXPECT_EQ(plan.materialTextureBindings[0].name, "_LightMap"); + EXPECT_EQ(plan.materialTextureBindings[0].semantic, BuiltinPassResourceSemantic::MaterialTexture); + EXPECT_EQ(plan.materialTextureBindings[0].resourceType, ShaderResourceType::Texture2D); + EXPECT_EQ(plan.materialTextureBindings[0].location.set, 4u); + EXPECT_EQ(plan.materialTextureBindings[0].location.binding, 1u); + + std::vector setLayouts; + ASSERT_TRUE(TryBuildBuiltinPassSetLayouts(plan, setLayouts, &error)) << error.CStr(); + ASSERT_EQ(setLayouts.size(), 5u); + EXPECT_TRUE(setLayouts[4].usesTexture); + EXPECT_TRUE(setLayouts[4].usesMaterialTextures); + ASSERT_EQ(setLayouts[4].materialTextureBindings.size(), 1u); + EXPECT_EQ(setLayouts[4].materialTextureBindings[0].name, "_LightMap"); + ASSERT_EQ(setLayouts[4].bindings.size(), 1u); + EXPECT_EQ( + static_cast(setLayouts[4].bindings[0].type), + DescriptorType::SRV); + EXPECT_EQ(setLayouts[4].bindings[0].resourceDimension, ResourceViewDimension::Texture2D); +} + TEST(BuiltinDepthOnlyPass_Test, UsesFloat3PositionInputLayoutForStaticMeshVertices) { const InputLayoutDesc inputLayout = BuiltinDepthOnlyPass::BuildInputLayout(); diff --git a/tests/Resources/Shader/test_shader.cpp b/tests/Resources/Shader/test_shader.cpp index 16c82272..fa458621 100644 --- a/tests/Resources/Shader/test_shader.cpp +++ b/tests/Resources/Shader/test_shader.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include "Rendering/Internal/ShaderVariantUtils.h" using namespace XCEngine::Resources; using namespace XCEngine::Containers; @@ -77,6 +79,69 @@ TEST(Shader, AddGetAttributes) { EXPECT_EQ(attributes[0].name, "aPosition"); } +TEST(Shader, StoresBackendCompiledBinariesPerBackend) { + ShaderStageVariant variant = {}; + variant.stage = ShaderType::Fragment; + variant.language = ShaderLanguage::HLSL; + variant.backend = ShaderBackend::Generic; + + Array d3d12Payload; + d3d12Payload.PushBack(0x01); + d3d12Payload.PushBack(0x02); + variant.SetCompiledBinaryForBackend(ShaderBackend::D3D12, d3d12Payload); + + Array vulkanPayload; + vulkanPayload.PushBack(0x03); + vulkanPayload.PushBack(0x04); + vulkanPayload.PushBack(0x05); + variant.SetCompiledBinaryForBackend(ShaderBackend::Vulkan, vulkanPayload); + + const Array* d3d12Binary = + variant.GetCompiledBinaryForBackend(ShaderBackend::D3D12); + ASSERT_NE(d3d12Binary, nullptr); + ASSERT_EQ(d3d12Binary->Size(), 2u); + EXPECT_EQ((*d3d12Binary)[1], 0x02); + + const Array* vulkanBinary = + variant.GetCompiledBinaryForBackend(ShaderBackend::Vulkan); + ASSERT_NE(vulkanBinary, nullptr); + ASSERT_EQ(vulkanBinary->Size(), 3u); + EXPECT_EQ((*vulkanBinary)[2], 0x05); + + EXPECT_EQ(variant.GetCompiledBinaryForBackend(ShaderBackend::OpenGL), nullptr); +} + +TEST(Shader, ApplyShaderStageVariantCarriesMatchedBackendCompiledBinary) { + ShaderPass pass = {}; + pass.name = "ForwardLit"; + + ShaderStageVariant variant = {}; + variant.stage = ShaderType::Fragment; + variant.language = ShaderLanguage::HLSL; + variant.backend = ShaderBackend::Generic; + variant.entryPoint = "MainPS"; + variant.profile = "ps_5_1"; + variant.sourceCode = "float4 MainPS() : SV_TARGET { return 1; }"; + + Array vulkanPayload; + vulkanPayload.PushBack(0x07); + vulkanPayload.PushBack(0x08); + vulkanPayload.PushBack(0x09); + variant.SetCompiledBinaryForBackend(ShaderBackend::Vulkan, vulkanPayload); + + XCEngine::RHI::ShaderCompileDesc compileDesc = {}; + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( + pass, + ShaderBackend::Vulkan, + variant, + compileDesc); + + EXPECT_EQ(compileDesc.compiledBinaryBackend, XCEngine::RHI::ShaderBinaryBackend::Vulkan); + ASSERT_EQ(compileDesc.compiledBinary.size(), 3u); + EXPECT_EQ(compileDesc.compiledBinary[0], 0x07); + EXPECT_EQ(compileDesc.compiledBinary[2], 0x09); +} + TEST(Shader, FindsBackendSpecificVariantAndFallsBackToGeneric) { Shader shader;