From bb0f4afe7d856cae6e4c91f72257b43bc89a924e Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Wed, 8 Apr 2026 18:29:16 +0800 Subject: [PATCH] Add Unity-style buffer shader resource support --- .../Builtin/BuiltinPassLayoutUtils.h | 23 +++ .../Builtin/BuiltinPassMetadataUtils.h | 8 + .../XCEngine/Resources/Shader/Shader.h | 6 +- .../src/Rendering/Detail/ShaderVariantUtils.h | 41 ++++ .../Internal/ShaderRuntimeBuildUtils.cpp | 142 ++++++++++++- .../Resources/Shader/ShaderSourceUtils.cpp | 16 ++ .../unit/test_camera_scene_renderer.cpp | 6 + tests/Resources/Shader/test_shader_loader.cpp | 188 ++++++++++++++++++ 8 files changed, 420 insertions(+), 10 deletions(-) diff --git a/engine/include/XCEngine/Rendering/Builtin/BuiltinPassLayoutUtils.h b/engine/include/XCEngine/Rendering/Builtin/BuiltinPassLayoutUtils.h index 3bb8b9a7..424c072f 100644 --- a/engine/include/XCEngine/Rendering/Builtin/BuiltinPassLayoutUtils.h +++ b/engine/include/XCEngine/Rendering/Builtin/BuiltinPassLayoutUtils.h @@ -369,7 +369,12 @@ inline RHI::DescriptorType ToBuiltinPassDescriptorType(Resources::ShaderResource return RHI::DescriptorType::CBV; case Resources::ShaderResourceType::Texture2D: case Resources::ShaderResourceType::TextureCube: + case Resources::ShaderResourceType::StructuredBuffer: + case Resources::ShaderResourceType::RawBuffer: return RHI::DescriptorType::SRV; + case Resources::ShaderResourceType::RWStructuredBuffer: + case Resources::ShaderResourceType::RWRawBuffer: + return RHI::DescriptorType::UAV; case Resources::ShaderResourceType::Sampler: return RHI::DescriptorType::Sampler; default: @@ -377,6 +382,23 @@ inline RHI::DescriptorType ToBuiltinPassDescriptorType(Resources::ShaderResource } } +inline RHI::ResourceViewDimension ToBuiltinPassResourceDimension(Resources::ShaderResourceType type) { + switch (type) { + case Resources::ShaderResourceType::Texture2D: + return RHI::ResourceViewDimension::Texture2D; + case Resources::ShaderResourceType::TextureCube: + return RHI::ResourceViewDimension::TextureCube; + case Resources::ShaderResourceType::StructuredBuffer: + case Resources::ShaderResourceType::RWStructuredBuffer: + return RHI::ResourceViewDimension::StructuredBuffer; + case Resources::ShaderResourceType::RawBuffer: + case Resources::ShaderResourceType::RWRawBuffer: + return RHI::ResourceViewDimension::RawBuffer; + default: + return RHI::ResourceViewDimension::Unknown; + } +} + inline RHI::DescriptorHeapType ResolveBuiltinPassDescriptorHeapType(Resources::ShaderResourceType type) { return type == Resources::ShaderResourceType::Sampler ? RHI::DescriptorHeapType::Sampler @@ -480,6 +502,7 @@ inline bool TryBuildBuiltinPassSetLayouts( layoutBinding.binding = binding.location.binding; layoutBinding.type = static_cast(descriptorType); layoutBinding.count = 1; + layoutBinding.resourceDimension = ToBuiltinPassResourceDimension(binding.resourceType); setLayout.bindings.push_back(layoutBinding); switch (binding.semantic) { diff --git a/engine/include/XCEngine/Rendering/Builtin/BuiltinPassMetadataUtils.h b/engine/include/XCEngine/Rendering/Builtin/BuiltinPassMetadataUtils.h index 3bbcafde..4c690268 100644 --- a/engine/include/XCEngine/Rendering/Builtin/BuiltinPassMetadataUtils.h +++ b/engine/include/XCEngine/Rendering/Builtin/BuiltinPassMetadataUtils.h @@ -210,6 +210,14 @@ inline const char* ShaderResourceTypeToString(Resources::ShaderResourceType type return "TextureCube"; case Resources::ShaderResourceType::Sampler: return "Sampler"; + case Resources::ShaderResourceType::StructuredBuffer: + return "StructuredBuffer"; + case Resources::ShaderResourceType::RawBuffer: + return "RawBuffer"; + case Resources::ShaderResourceType::RWStructuredBuffer: + return "RWStructuredBuffer"; + case Resources::ShaderResourceType::RWRawBuffer: + return "RWRawBuffer"; default: return "Unknown"; } diff --git a/engine/include/XCEngine/Resources/Shader/Shader.h b/engine/include/XCEngine/Resources/Shader/Shader.h index 88af7161..aef0424b 100644 --- a/engine/include/XCEngine/Resources/Shader/Shader.h +++ b/engine/include/XCEngine/Resources/Shader/Shader.h @@ -45,7 +45,11 @@ enum class ShaderResourceType : Core::uint8 { ConstantBuffer = 0, Texture2D, TextureCube, - Sampler + Sampler, + StructuredBuffer, + RawBuffer, + RWStructuredBuffer, + RWRawBuffer }; struct ShaderUniform { diff --git a/engine/src/Rendering/Detail/ShaderVariantUtils.h b/engine/src/Rendering/Detail/ShaderVariantUtils.h index e9e79c54..619f3ed3 100644 --- a/engine/src/Rendering/Detail/ShaderVariantUtils.h +++ b/engine/src/Rendering/Detail/ShaderVariantUtils.h @@ -137,6 +137,38 @@ inline bool TryRewriteHlslRegisterBindingWithName( return false; } + if (resourceType == Resources::ShaderResourceType::StructuredBuffer || + resourceType == Resources::ShaderResourceType::RWStructuredBuffer) { + const std::regex pattern( + "((?:globallycoherent\\s+)?(?:StructuredBuffer|RWStructuredBuffer)\\s*<[^;\\r\\n>]+>\\s+" + + escapedName + "\\s*)(:\\s*register\\s*\\([^\\)]*\\))?(\\s*;)", + std::regex::ECMAScript); + const std::string rewritten = + std::regex_replace(sourceText, pattern, "$1: " + registerClause + "$3"); + if (rewritten != sourceText) { + sourceText = rewritten; + return true; + } + + return false; + } + + if (resourceType == Resources::ShaderResourceType::RawBuffer || + resourceType == Resources::ShaderResourceType::RWRawBuffer) { + const std::regex pattern( + "((?:globallycoherent\\s+)?(?:ByteAddressBuffer|RWByteAddressBuffer)\\s+" + escapedName + + "\\s*)(:\\s*register\\s*\\([^\\)]*\\))?(\\s*;)", + std::regex::ECMAScript); + const std::string rewritten = + std::regex_replace(sourceText, pattern, "$1: " + registerClause + "$3"); + if (rewritten != sourceText) { + sourceText = rewritten; + return true; + } + + return false; + } + const std::regex pattern( "((?:Texture2D|TextureCube|SamplerState|SamplerComparisonState)\\s+" + escapedName + "\\s*)(:\\s*register\\s*\\([^\\)]*\\))?(\\s*;)", @@ -157,9 +189,14 @@ inline const char* TryGetHlslRegisterPrefix(Resources::ShaderResourceType type) return "b"; case Resources::ShaderResourceType::Texture2D: case Resources::ShaderResourceType::TextureCube: + case Resources::ShaderResourceType::StructuredBuffer: + case Resources::ShaderResourceType::RawBuffer: return "t"; case Resources::ShaderResourceType::Sampler: return "s"; + case Resources::ShaderResourceType::RWStructuredBuffer: + case Resources::ShaderResourceType::RWRawBuffer: + return "u"; default: return nullptr; } @@ -223,11 +260,15 @@ inline bool TryBuildRuntimeShaderBindings( break; case Resources::ShaderResourceType::Texture2D: case Resources::ShaderResourceType::TextureCube: + case Resources::ShaderResourceType::StructuredBuffer: + case Resources::ShaderResourceType::RawBuffer: binding.binding = nextTextureRegister++; break; case Resources::ShaderResourceType::Sampler: binding.binding = nextSamplerRegister++; break; + case Resources::ShaderResourceType::RWStructuredBuffer: + case Resources::ShaderResourceType::RWRawBuffer: default: binding.binding = nextUnorderedAccessRegister++; break; diff --git a/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.cpp b/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.cpp index 7345c598..2a44d9ef 100644 --- a/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.cpp +++ b/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.cpp @@ -10,8 +10,10 @@ #include #include +#include #include #include +#include #include #include #include @@ -190,6 +192,119 @@ void ImportConcretePass(Shader& shader, const ShaderPass& sourcePass) { shader.AddPass(sourcePass); } +bool IsBufferShaderResourceType(ShaderResourceType type) { + return type == ShaderResourceType::StructuredBuffer || + type == ShaderResourceType::RawBuffer || + type == ShaderResourceType::RWStructuredBuffer || + type == ShaderResourceType::RWRawBuffer; +} + +Core::uint32 ResolveDefaultAuthoringSet(ShaderResourceType type) { + switch (type) { + case ShaderResourceType::StructuredBuffer: + case ShaderResourceType::RawBuffer: + return 2u; + case ShaderResourceType::RWStructuredBuffer: + case ShaderResourceType::RWRawBuffer: + return 4u; + default: + return 0u; + } +} + +bool HasResourceBindingNamed( + const Containers::Array& bindings, + const Containers::String& name) { + for (const ShaderResourceBindingDesc& binding : bindings) { + if (binding.name == name) { + return true; + } + } + + return false; +} + +Core::uint32 FindNextBindingInSet( + const Containers::Array& bindings, + Core::uint32 setIndex) { + Core::uint32 nextBinding = 0; + for (const ShaderResourceBindingDesc& binding : bindings) { + if (binding.set != setIndex) { + continue; + } + + nextBinding = std::max(nextBinding, binding.binding + 1u); + } + + return nextBinding; +} + +bool TryParseHlslBufferResourceLine( + const std::string& line, + ShaderResourceType& outType, + Containers::String& outName) { + static const std::regex kStructuredPattern( + R"(^\s*(?:globallycoherent\s+)?(RWStructuredBuffer|StructuredBuffer)\s*<[^;\r\n>]+>\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?::\s*register\s*\([^\)]*\))?\s*;)", + std::regex::ECMAScript); + static const std::regex kRawPattern( + R"(^\s*(?:globallycoherent\s+)?(RWByteAddressBuffer|ByteAddressBuffer)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?::\s*register\s*\([^\)]*\))?\s*;)", + std::regex::ECMAScript); + + std::smatch match; + if (std::regex_match(line, match, kStructuredPattern) && match.size() >= 3u) { + outType = + match[1].str() == "RWStructuredBuffer" + ? ShaderResourceType::RWStructuredBuffer + : ShaderResourceType::StructuredBuffer; + outName = match[2].str().c_str(); + return true; + } + + if (std::regex_match(line, match, kRawPattern) && match.size() >= 3u) { + outType = + match[1].str() == "RWByteAddressBuffer" + ? ShaderResourceType::RWRawBuffer + : ShaderResourceType::RawBuffer; + outName = match[2].str().c_str(); + return true; + } + + return false; +} + +void AppendScannedBufferResourceBindings( + const Containers::String& sourceText, + Containers::Array& ioBindings) { + if (sourceText.Empty()) { + return; + } + + std::vector lines; + Internal::SplitShaderAuthoringLines(sourceText.CStr(), lines); + + for (const std::string& rawLine : lines) { + const std::string line = Internal::StripAuthoringLineComment(rawLine); + ShaderResourceType resourceType = ShaderResourceType::ConstantBuffer; + Containers::String resourceName; + if (!TryParseHlslBufferResourceLine(line, resourceType, resourceName)) { + continue; + } + + if (!IsBufferShaderResourceType(resourceType) || + resourceName.Empty() || + HasResourceBindingNamed(ioBindings, resourceName)) { + continue; + } + + ShaderResourceBindingDesc binding = {}; + binding.name = resourceName; + binding.type = resourceType; + binding.set = ResolveDefaultAuthoringSet(resourceType); + binding.binding = FindNextBindingInSet(ioBindings, binding.set); + ioBindings.PushBack(binding); + } +} + ShaderPass BuildConcretePass( const ShaderIR& shaderIR, const ShaderSubShaderIR& subShader, @@ -208,19 +323,19 @@ ShaderPass BuildConcretePass( for (const ShaderResourceBindingDesc& resourceBinding : pass.resources) { shaderPass.resources.PushBack(resourceBinding); } - if (shaderPass.resources.Empty()) { - Containers::Array defaultBindings; - if (::XCEngine::Rendering::TryBuildBuiltinPassDefaultResourceBindings(shaderPass, defaultBindings)) { - for (const ShaderResourceBindingDesc& resourceBinding : defaultBindings) { - shaderPass.resources.PushBack(resourceBinding); - } - } - } for (const ShaderKeywordDeclaration& keywordDeclaration : pass.keywordDeclarations) { shaderPass.keywordDeclarations.PushBack(keywordDeclaration); } - if (pass.programSource.Empty()) { + if (shaderPass.resources.Empty()) { + Containers::Array defaultBindings; + if (::XCEngine::Rendering::TryBuildBuiltinPassDefaultResourceBindings(shaderPass, defaultBindings)) { + for (const ShaderResourceBindingDesc& resourceBinding : defaultBindings) { + shaderPass.resources.PushBack(resourceBinding); + } + } + } + return shaderPass; } @@ -231,6 +346,15 @@ ShaderPass BuildConcretePass( AppendAuthoringSourceBlock(combinedSource, pass.programSource); const Containers::String strippedCombinedSource = StripShaderAuthoringPragmas(combinedSource); + if (shaderPass.resources.Empty()) { + Containers::Array defaultBindings; + if (::XCEngine::Rendering::TryBuildBuiltinPassDefaultResourceBindings(shaderPass, defaultBindings)) { + for (const ShaderResourceBindingDesc& resourceBinding : defaultBindings) { + shaderPass.resources.PushBack(resourceBinding); + } + } + } + AppendScannedBufferResourceBindings(strippedCombinedSource, shaderPass.resources); const std::vector keywordSets = BuildShaderKeywordVariantSets(pass.keywordDeclarations); diff --git a/engine/src/Resources/Shader/ShaderSourceUtils.cpp b/engine/src/Resources/Shader/ShaderSourceUtils.cpp index a9c029f1..44d09b3d 100644 --- a/engine/src/Resources/Shader/ShaderSourceUtils.cpp +++ b/engine/src/Resources/Shader/ShaderSourceUtils.cpp @@ -202,6 +202,22 @@ bool TryParseShaderResourceType(const Containers::String& value, ShaderResourceT outType = ShaderResourceType::Sampler; return true; } + if (normalized == "structuredbuffer" || normalized == "structured") { + outType = ShaderResourceType::StructuredBuffer; + return true; + } + if (normalized == "rawbuffer" || normalized == "byteaddressbuffer") { + outType = ShaderResourceType::RawBuffer; + return true; + } + if (normalized == "rwstructuredbuffer" || normalized == "rwstructured") { + outType = ShaderResourceType::RWStructuredBuffer; + return true; + } + if (normalized == "rwrawbuffer" || normalized == "rwbyteaddressbuffer") { + outType = ShaderResourceType::RWRawBuffer; + return true; + } return false; } diff --git a/tests/Rendering/unit/test_camera_scene_renderer.cpp b/tests/Rendering/unit/test_camera_scene_renderer.cpp index baae8ba8..31b8b84c 100644 --- a/tests/Rendering/unit/test_camera_scene_renderer.cpp +++ b/tests/Rendering/unit/test_camera_scene_renderer.cpp @@ -253,6 +253,9 @@ public: desc.dimension); } + XCEngine::RHI::RHIResourceView* CreateShaderResourceView( + XCEngine::RHI::RHIBuffer*, + const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; } XCEngine::RHI::RHIResourceView* CreateShaderResourceView( XCEngine::RHI::RHITexture*, const XCEngine::RHI::ResourceViewDesc& desc) override { @@ -264,6 +267,9 @@ public: static_cast(desc.format), desc.dimension); } + XCEngine::RHI::RHIResourceView* CreateUnorderedAccessView( + XCEngine::RHI::RHIBuffer*, + const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; } XCEngine::RHI::RHIResourceView* CreateUnorderedAccessView( XCEngine::RHI::RHITexture*, const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; } diff --git a/tests/Resources/Shader/test_shader_loader.cpp b/tests/Resources/Shader/test_shader_loader.cpp index 50f72891..99e74e90 100644 --- a/tests/Resources/Shader/test_shader_loader.cpp +++ b/tests/Resources/Shader/test_shader_loader.cpp @@ -6,6 +6,7 @@ #include #include #include +#include "Rendering/Detail/ShaderVariantUtils.h" #include #include @@ -497,6 +498,193 @@ TEST(ShaderLoader, LoadShaderAuthoringBuildsGenericHlslVariants) { fs::remove_all(shaderRoot); } +TEST(ShaderLoader, LoadShaderAuthoringCollectsUnityStyleBufferResources) { + namespace fs = std::filesystem; + + const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_buffer_resources_test"; + const fs::path shaderPath = shaderRoot / "buffer_resources.shader"; + + fs::remove_all(shaderRoot); + fs::create_directories(shaderRoot); + WriteTextFile( + shaderPath, + R"(Shader "Test/BufferResources" +{ + SubShader + { + Pass + { + Name "Volume" + HLSLPROGRAM + #pragma target 4.5 + #pragma vertex MainVS + #pragma fragment MainPS + StructuredBuffer InputBuffer; + ByteAddressBuffer RawInput; + RWStructuredBuffer OutputBuffer; + RWByteAddressBuffer OutputRaw; + struct VSInput + { + float3 positionOS : POSITION; + }; + float4 MainVS(VSInput input) : SV_POSITION + { + return float4(input.positionOS, 1.0); + } + float4 MainPS() : SV_TARGET + { + return float4(1.0, 1.0, 1.0, 1.0); + } + ENDHLSL + } + } +} +)"); + + ShaderLoader loader; + LoadResult result = loader.Load(shaderPath.string().c_str()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + Shader* shader = static_cast(result.resource); + ASSERT_NE(shader, nullptr); + + const ShaderPass* pass = shader->FindPass("Volume"); + ASSERT_NE(pass, nullptr); + ASSERT_EQ(pass->resources.Size(), 4u); + + const ShaderResourceBindingDesc* inputBuffer = + shader->FindPassResourceBinding("Volume", "InputBuffer"); + ASSERT_NE(inputBuffer, nullptr); + EXPECT_EQ(inputBuffer->type, ShaderResourceType::StructuredBuffer); + EXPECT_EQ(inputBuffer->set, 2u); + EXPECT_EQ(inputBuffer->binding, 0u); + + const ShaderResourceBindingDesc* rawInput = + shader->FindPassResourceBinding("Volume", "RawInput"); + ASSERT_NE(rawInput, nullptr); + EXPECT_EQ(rawInput->type, ShaderResourceType::RawBuffer); + EXPECT_EQ(rawInput->set, 2u); + EXPECT_EQ(rawInput->binding, 1u); + + const ShaderResourceBindingDesc* outputBuffer = + shader->FindPassResourceBinding("Volume", "OutputBuffer"); + ASSERT_NE(outputBuffer, nullptr); + EXPECT_EQ(outputBuffer->type, ShaderResourceType::RWStructuredBuffer); + EXPECT_EQ(outputBuffer->set, 4u); + EXPECT_EQ(outputBuffer->binding, 0u); + + const ShaderResourceBindingDesc* outputRaw = + shader->FindPassResourceBinding("Volume", "OutputRaw"); + ASSERT_NE(outputRaw, nullptr); + EXPECT_EQ(outputRaw->type, ShaderResourceType::RWRawBuffer); + EXPECT_EQ(outputRaw->set, 4u); + EXPECT_EQ(outputRaw->binding, 1u); + + delete shader; + fs::remove_all(shaderRoot); +} + +TEST(ShaderLoader, RuntimeShaderSourceRewritesUnityStyleBufferRegisters) { + namespace fs = std::filesystem; + + const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_buffer_runtime_test"; + const fs::path shaderPath = shaderRoot / "buffer_runtime.shader"; + + fs::remove_all(shaderRoot); + fs::create_directories(shaderRoot); + WriteTextFile( + shaderPath, + R"(Shader "Test/BufferRuntime" +{ + SubShader + { + Pass + { + Name "Volume" + HLSLPROGRAM + #pragma target 4.5 + #pragma vertex MainVS + #pragma fragment MainPS + StructuredBuffer InputBuffer; + ByteAddressBuffer RawInput; + RWStructuredBuffer OutputBuffer; + RWByteAddressBuffer OutputRaw; + struct VSInput + { + float3 positionOS : POSITION; + }; + float4 MainVS(VSInput input) : SV_POSITION + { + return float4(input.positionOS, 1.0); + } + float4 MainPS() : SV_TARGET + { + return float4(1.0, 0.5, 0.25, 1.0); + } + ENDHLSL + } + } +} +)"); + + ShaderLoader loader; + LoadResult result = loader.Load(shaderPath.string().c_str()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + Shader* shader = static_cast(result.resource); + ASSERT_NE(shader, nullptr); + + const ShaderPass* pass = shader->FindPass("Volume"); + ASSERT_NE(pass, nullptr); + + const ShaderStageVariant* fragmentVariant = + shader->FindVariant("Volume", ShaderType::Fragment, ShaderBackend::D3D12); + ASSERT_NE(fragmentVariant, nullptr); + const std::string d3d12Source = + ::XCEngine::Rendering::Detail::BuildRuntimeShaderSource( + *pass, + ShaderBackend::D3D12, + *fragmentVariant); + EXPECT_NE( + d3d12Source.find("StructuredBuffer InputBuffer: register(t0);"), + std::string::npos); + EXPECT_NE( + d3d12Source.find("ByteAddressBuffer RawInput: register(t1);"), + std::string::npos); + EXPECT_NE( + d3d12Source.find("RWStructuredBuffer OutputBuffer: register(u0);"), + std::string::npos); + EXPECT_NE( + d3d12Source.find("RWByteAddressBuffer OutputRaw: register(u1);"), + std::string::npos); + + const ShaderStageVariant* vulkanFragment = + shader->FindVariant("Volume", ShaderType::Fragment, ShaderBackend::Vulkan); + ASSERT_NE(vulkanFragment, nullptr); + const std::string vulkanSource = + ::XCEngine::Rendering::Detail::BuildRuntimeShaderSource( + *pass, + ShaderBackend::Vulkan, + *vulkanFragment); + EXPECT_NE( + vulkanSource.find("StructuredBuffer InputBuffer: register(t0, space2);"), + std::string::npos); + EXPECT_NE( + vulkanSource.find("ByteAddressBuffer RawInput: register(t1, space2);"), + std::string::npos); + EXPECT_NE( + vulkanSource.find("RWStructuredBuffer OutputBuffer: register(u0, space4);"), + std::string::npos); + EXPECT_NE( + vulkanSource.find("RWByteAddressBuffer OutputRaw: register(u1, space4);"), + std::string::npos); + + delete shader; + fs::remove_all(shaderRoot); +} + TEST(ShaderLoader, LoadShaderAuthoringParsesPassStateAndFallback) { namespace fs = std::filesystem;