Add Unity-style buffer shader resource support

This commit is contained in:
2026-04-08 18:29:16 +08:00
parent 4728b09ae8
commit bb0f4afe7d
8 changed files with 420 additions and 10 deletions

View File

@@ -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<Core::uint32>(descriptorType);
layoutBinding.count = 1;
layoutBinding.resourceDimension = ToBuiltinPassResourceDimension(binding.resourceType);
setLayout.bindings.push_back(layoutBinding);
switch (binding.semantic) {

View File

@@ -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";
}

View File

@@ -45,7 +45,11 @@ enum class ShaderResourceType : Core::uint8 {
ConstantBuffer = 0,
Texture2D,
TextureCube,
Sampler
Sampler,
StructuredBuffer,
RawBuffer,
RWStructuredBuffer,
RWRawBuffer
};
struct ShaderUniform {

View File

@@ -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;

View File

@@ -10,8 +10,10 @@
#include <XCEngine/Resources/BuiltinResources.h>
#include <XCEngine/Resources/Shader/ShaderLoader.h>
#include <algorithm>
#include <filesystem>
#include <memory>
#include <regex>
#include <system_error>
#include <unordered_map>
#include <unordered_set>
@@ -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<ShaderResourceBindingDesc>& bindings,
const Containers::String& name) {
for (const ShaderResourceBindingDesc& binding : bindings) {
if (binding.name == name) {
return true;
}
}
return false;
}
Core::uint32 FindNextBindingInSet(
const Containers::Array<ShaderResourceBindingDesc>& 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<ShaderResourceBindingDesc>& ioBindings) {
if (sourceText.Empty()) {
return;
}
std::vector<std::string> 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<ShaderResourceBindingDesc> 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<ShaderResourceBindingDesc> 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<ShaderResourceBindingDesc> defaultBindings;
if (::XCEngine::Rendering::TryBuildBuiltinPassDefaultResourceBindings(shaderPass, defaultBindings)) {
for (const ShaderResourceBindingDesc& resourceBinding : defaultBindings) {
shaderPass.resources.PushBack(resourceBinding);
}
}
}
AppendScannedBufferResourceBindings(strippedCombinedSource, shaderPass.resources);
const std::vector<ShaderKeywordSet> keywordSets =
BuildShaderKeywordVariantSets(pass.keywordDeclarations);

View File

@@ -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;
}

View File

@@ -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<XCEngine::RHI::Format>(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; }

View File

@@ -6,6 +6,7 @@
#include <XCEngine/Resources/Shader/ShaderLoader.h>
#include <XCEngine/Core/Asset/ResourceTypes.h>
#include <XCEngine/Core/Containers/Array.h>
#include "Rendering/Detail/ShaderVariantUtils.h"
#include <chrono>
#include <cstring>
@@ -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<float4> InputBuffer;
ByteAddressBuffer RawInput;
RWStructuredBuffer<uint> 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<Shader*>(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<float4> InputBuffer;
ByteAddressBuffer RawInput;
RWStructuredBuffer<uint> 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<Shader*>(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<float4> InputBuffer: register(t0);"),
std::string::npos);
EXPECT_NE(
d3d12Source.find("ByteAddressBuffer RawInput: register(t1);"),
std::string::npos);
EXPECT_NE(
d3d12Source.find("RWStructuredBuffer<uint> 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<float4> InputBuffer: register(t0, space2);"),
std::string::npos);
EXPECT_NE(
vulkanSource.find("ByteAddressBuffer RawInput: register(t1, space2);"),
std::string::npos);
EXPECT_NE(
vulkanSource.find("RWStructuredBuffer<uint> 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;