From f3c017b16845fab18ada57c6df94429a65b5aec3 Mon Sep 17 00:00:00 2001 From: nmzik Date: Wed, 14 Oct 2026 15:17:03 +0200 Subject: [PATCH] shader: fold reads through empty scalar descriptor tables Co-authored-by: fxpw --- .../ir/passes/ResourceMaterialization.cpp | 32 ++++- .../ir/passes/ResourceMaterialization.h | 1 + tests/ResourceMaterializationTests.cpp | 110 ++++++++++++++++++ tests/ShaderRecompilerComputeTests.cpp | 36 +++++- 4 files changed, 170 insertions(+), 9 deletions(-) diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index 967259077..c180dcf88 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -60,6 +60,15 @@ bool NullImageDescriptor(const DescriptorValue& descriptor) { return descriptor.dwords[0] == 0 && (descriptor.dwords[1] & 0xffu) == 0; } +bool NullBufferDescriptor(Value value) { + const auto* handle = value.Resolve().TryInstruction(); + if (handle == nullptr || handle->GetOpcode() != ValueOpcode::GetBufferResource || + handle->NumArgs() != 4u) return false; + for (size_t index = 0; index < 4u; ++index) + if (handle->Arg(index).Resolve() != Value(0u)) return false; + return true; +} + bool ValidImageDescriptor(const DescriptorValue& descriptor, bool r128 = false) { const auto& words = descriptor.dwords; // Reject texture descriptors with nonzero reserved bits. @@ -1378,6 +1387,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime base.formatted ? descriptor.Format() : Prospero::BufferFormat::kInvalid; buffer.descriptor_swizzle = base.formatted ? descriptor.DstSelXYZW() : DstSel(4, 5, 6, 7); buffer.zero_stride_oob = descriptor.OutOfBounds() == 0u && stride == 0u; + buffer.scalar_empty = base.scalar && descriptor.GetSize() == 0u; } snapshot.images.resize(program.info.images.size()); specialization.images.resize(program.info.images.size()); @@ -1543,6 +1553,17 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization& } auto& memory_info = program.memory_info; + // Resolve empty scalar-table words before folding the indirect descriptors that use them. + for (auto* block: program.blocks) { + for (auto& inst: *block) { + if (inst.GetOpcode() != ValueOpcode::ReadConstBuffer) continue; + const auto& memory = memory_info[inst.Flags().index]; + if (memory.kind == ResourceKind::ScalarBuffer && + specialization.buffers[memory.resource].scalar_empty && + buffers[memory.resource].indirect_root == BufferResource::NoIndirectBuffer) + inst.ReplaceUsesWith(Value(0u)); + } + } const ImageRemap image_remap(specialization); for (auto* block: program.blocks) { for (auto it = block->begin(); it != block->end(); ++it) { @@ -1563,14 +1584,17 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization& } if (BufferAccessOf(inst.GetOpcode()) == BufferAccess::Read) { const auto& memory = memory_info[inst.Flags().index]; - if (memory.kind == ResourceKind::Buffer && + const bool native_oob = memory.kind == ResourceKind::Buffer && specialization.buffers[memory.resource].zero_stride_oob && - buffers[memory.resource].indirect_root == BufferResource::NoIndirectBuffer) { + buffers[memory.resource].indirect_root == BufferResource::NoIndirectBuffer; + const bool null_indirect = memory.kind == ResourceKind::IndirectBuffer && + NullBufferDescriptor(inst.Arg(0)); + if (native_oob || null_indirect) { // Bounds mode 0 checks offset >= stride, so zero stride - // makes every vector read out of bounds regardless of its address. + // makes every vector read out of bounds. An all-zero V# is unbound. const auto count = BufferComponentCount(inst.GetOpcode()); std::array values {Value(0u), Value(0u), Value(0u), Value(0u)}; - if (memory.formatted && !memory.typed) { + if (native_oob && memory.formatted && !memory.typed) { const auto& buffer = buffers[memory.resource]; const auto format = Format::GetFormatInfo(buffer.descriptor_format); const bool d16 = memory.data_bits == 16u; diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h index 6ad72cb36..b2b9706da 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h @@ -13,6 +13,7 @@ struct ResourceSpecialization { Prospero::BufferFormat descriptor_format = Prospero::BufferFormat::kInvalid; uint32_t descriptor_swizzle = DstSel(4, 5, 6, 7); bool zero_stride_oob = false; + bool scalar_empty = false; uint32_t indirect_root = BufferResource::NoIndirectBuffer; uint32_t indirect_mapping_offset = 0; uint32_t indirect_search_iterations = 0; diff --git a/tests/ResourceMaterializationTests.cpp b/tests/ResourceMaterializationTests.cpp index bab545b90..271bb7907 100644 --- a/tests/ResourceMaterializationTests.cpp +++ b/tests/ResourceMaterializationTests.cpp @@ -1024,6 +1024,115 @@ void TestCyclicControlPredicatesRemainUnknown() { "cyclic control lost a possible branch during reachability"); } +void TestEmptyScalarBufferSpecialization() { + using namespace Libs::Graphics::ShaderRecompiler::IR; + struct Fixture { + Program program; + std::vector> uses; + Inst *nonnull = nullptr; + }; + const auto make_fixture = [] { + Fixture fixture; + auto &program = fixture.program; + program.stage = Libs::Graphics::ShaderType::Compute; + program.srt_plan_complete = program.resource_tracking_complete = true; + program.user_data_count = 4u; + auto &block = AddValueBlock(program); + DescriptorSource source; + source.dword_count = 4u; + for (uint32_t word = 0; word < 4u; ++word) + source.dwords[word] = Value(&block.AppendNewInst(ValueOpcode::GetUserData, + {Value(static_cast(word))})); + program.descriptor_sources.push_back(source); + program.info.buffers.push_back({.source = 0u, .read = true, .scalar = true}); + const auto table = Value(&block.AppendNewInst(ValueOpcode::GetBufferResource, + {source.dwords[0], source.dwords[1], source.dwords[2], source.dwords[3]})); + std::array words; + for (uint32_t word = 0; word < 4u; ++word) { + program.memory_info.push_back({.kind = ResourceKind::ScalarBuffer, .resource = 0u, + .offset = word * 4u}); + auto &read = block.AppendNewInst(ValueOpcode::ReadConstBuffer, {table, Value(0u)}); + read.SetFlags(MemoryFlags{.index = word}); + words[word] = Value(&read); + } + const auto handle = Value(&block.AppendNewInst(ValueOpcode::GetBufferResource, + {words[0], words[1], words[2], words[3]})); + for (const auto opcode : {ValueOpcode::ReadConstBuffer, ValueOpcode::LoadBufferU8, + ValueOpcode::LoadBufferU16, ValueOpcode::LoadBufferU32, + ValueOpcode::LoadBufferU32x2, ValueOpcode::LoadBufferU32x3, + ValueOpcode::LoadBufferU32x4}) { + const auto index = static_cast(program.memory_info.size()); + program.memory_info.push_back({.kind = ResourceKind::IndirectBuffer, .resource = UINT32_MAX, + .data_dwords = BufferComponentCount(opcode), + .data_bits = opcode == ValueOpcode::LoadBufferU8 ? 8u : opcode == ValueOpcode::LoadBufferU16 ? 16u : 32u, + .formatted = opcode == ValueOpcode::LoadBufferU32}); + auto &read = opcode == ValueOpcode::ReadConstBuffer + ? block.AppendNewInst(opcode, {handle, Value(0u)}) + : block.AppendNewInst(opcode, {handle, Value(0u), Value(0u), Value(0u), Value(true)}); + read.SetFlags(MemoryFlags{.index = index}); + fixture.uses.emplace_back(&block.AppendNewInst(ValueOpcode::Identity, {Value(&read)}), read.GetType()); + } + const auto nonnull = Value(&block.AppendNewInst(ValueOpcode::GetBufferResource, + {words[0], words[1], words[2], Value(1u)})); + auto &read = block.AppendNewInst(ValueOpcode::ReadConstBuffer, {nonnull, Value(0u)}); + read.SetFlags(MemoryFlags{.index = 4u}); + fixture.nonnull = &block.AppendNewInst(ValueOpcode::Identity, {Value(&read)}); + return fixture; + }; + auto fixture = make_fixture(); + auto plan = ExtractResourcePlan(fixture.program); + std::array descriptor{0x1000u, 8u << 16u, 0u, 3u << 28u}; + uint32_t reads = 0u; + const SrtRuntime runtime{.user_data = descriptor, .read_memory = RejectSpecializationRead, + .userdata = &reads, .read_specialization_memory = RejectSpecializationRead}; + ResourceSnapshot snapshot; + ResourceSpecialization specialization, empty, populated; + for (const uint32_t records : {0u, 1u, 0x20000000u, 0u}) { + descriptor[2] = records; + Check(MaterializeResources(plan, runtime, snapshot, specialization) && reads == 0u && + specialization.buffers[0].scalar_empty == (records == 0u), + "scalar emptiness read table contents or truncated its 64-bit byte size"); + if (records == 0u) { + if (empty.buffers.empty()) empty = specialization; + else Check(specialization == empty, "returning to an empty scalar table changed its specialization"); + } + else if (records == 1u) populated = specialization; + else Check(specialization == populated, "nonempty scalar size created a shader permutation"); + auto current = make_fixture(); + ApplyResourceSpecialization(current.program, specialization); + for (const auto &[use, type] : current.uses) { + const auto value = use->Arg(0).Resolve(); + Check(value.GetType() == type, "empty scalar-table folding changed the load result type"); + if (records != 0u) { + Check(value.TryInstruction() != nullptr && BufferAccessOf(value.Instruction()->GetOpcode()) == BufferAccess::Read, + "nonempty scalar table lost a runtime buffer load"); + } else if (value.IsImmediate()) { + Check(value == Value(uint8_t{0}) || value == Value(uint16_t{0}) || value == Value(0u), + "empty scalar table did not produce a typed zero"); + } else { + for (size_t component = 0; component < value.Instruction()->NumArgs(); ++component) + Check(value.Instruction()->Arg(component) == Value(0u), + "empty scalar table retained a nonzero vector component"); + } + } + Check(current.nonnull->Arg(0).Resolve().TryInstruction() != nullptr, + "a partly nonzero indirect descriptor was treated as unbound"); + } + Check(empty != populated, "empty-to-populated scalar transition reused the folded specialization"); + plan.info.buffers[0].scalar = false; + Check(MaterializeResources(plan, runtime, snapshot, specialization) && + !specialization.buffers[0].scalar_empty, + "vector-only resource acquired scalar-empty specialization"); + empty.buffers[0].indirect_root = 0u; + empty.buffers.push_back(empty.buffers[0]); + empty.buffers[1].scalar_empty = false; + ApplyResourceSpecialization(fixture.program, empty); + for (const auto &[use, type] : fixture.uses) + Check(use->Arg(0).GetType() == type && use->Arg(0).Resolve().TryInstruction() != nullptr && + BufferAccessOf(use->Arg(0).ResolveInstruction()->GetOpcode()) == BufferAccess::Read, + "empty root folded a scalar read with nonempty candidates"); +} + void TestMixedSamplerVariantsShareRuntimeDescriptor() { using namespace Libs::Graphics::ShaderRecompiler::IR; auto program = MixedSamplerProgram(); @@ -1145,6 +1254,7 @@ int main() { for (const bool independent_mask : {false, true}) TestEmptyTraversalSkipsUnmappedDescriptor(independent_mask); TestCyclicControlPredicatesRemainUnknown(); + TestEmptyScalarBufferSpecialization(); TestMixedSamplerVariantsShareRuntimeDescriptor(); for (const bool mixed : {false, true}) TestIndirectImageSamplerClasses(mixed); std::puts("ResourceMaterializationTests: all cases passed"); diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 55036be37..ced337eef 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -36778,12 +36778,12 @@ void CheckGpuSelectedRecordBufferAccess(VulkanHarness &vulkan) { for (const u32 table_bytes : {0u, 536u, 544u}) { TestCase test; test.name = wave == 32u ? "GpuSelectedRecordBufferAccess32" : "GpuSelectedRecordBufferAccess64"; - test.initial.assign((slot * 13u + keys.size() * wave * 8u) / 4u, 0xdeadbeefu); + test.initial.assign((slot * 13u + keys.size() * wave * 16u) / 4u, 0xdeadbeefu); test.has_user_data = test.has_compute_info = true; const std::array descriptors{selectors, 12u << 16u, keys.size(), 0x5204u, table, 0u, table_bytes, 0x5204u}; std::copy(descriptors.begin(), descriptors.end(), test.user_data.begin()); - const std::array output{slot * 13u, 8u << 16u, u32(keys.size()) * wave, 0x5204u}; + const std::array output{slot * 13u, 16u << 16u, u32(keys.size()) * wave, 0x5204u}; std::copy(output.begin(), output.end(), test.user_data.begin() + 16u); test.compute_info.wave_size = wave; test.compute_info.threads_num[0] = wave; @@ -36793,17 +36793,21 @@ void CheckGpuSelectedRecordBufferAccess(VulkanHarness &vulkan) { test.compute_info.workgroup_register = 24u; test.compute_info.lds_size_dwords = 0; test.dispatch_x = keys.size(); + test.bda_mappings = {{0u, 0u}}; auto &code = test.code; code.insert(code.end(), {EncodeSop2(0x26, 32, 24, InlineU32(12)), EncodeSmem0(0x08, 32, 0), EncodeSmem1(0u, 32), EncodeSopp(0x0c, 0), EncodeSop2(0x26, 33, 32, 255), 192u, EncodeSop2(0x02, 33, 33, 255), 96u, + EncodeSmem0(0x0a, 36, 2), EncodeSmem1(144u, 33), EncodeSmem0(0x0a, 8, 2), EncodeSmem1(160u, 33), EncodeSmem0(0x0a, 12, 2), EncodeSmem1(176u, 33), EncodeSopp(0x0c, 0), EncodeMubuf0(0x83, 0, true, false), EncodeMubuf1(4, 2, 0), + EncodeMubuf0(0x0a, 0, true, false), EncodeMubuf1(6, 9, 0), + EncodeMubuf0(0x00, 0, true, false), EncodeMubuf1(7, 9, 0), EncodeSopp(0x0c, 0), EncodeSop2(0x1e, 34, 24, InlineU32(std::countr_zero(wave))), EncodeVop2(0x25, 20, 34, 0), - EncodeMubuf0(0x1d, 0, true, false), EncodeMubuf1(4, 4, 20)}); + EncodeMubuf0(0x1e, 0, true, false), EncodeMubuf1(4, 4, 20)}); AppendVMovLiteral(&code, 10, 0xa5c39b72u); AppendSMovLiteral(&code, 126, 0x55555555u); AppendSMovLiteral(&code, 127, wave == 64u ? 0x55555555u : 0u); @@ -36814,11 +36818,30 @@ void CheckGpuSelectedRecordBufferAccess(VulkanHarness &vulkan) { static_cast(Prospero::BufferFormat::k16_16_16_16Float) << 12u | DstSel(4, 5, 6, 7), slot * (row + 7u), 1u << 16u, u32(keys.size()) * wave, 0x5204u}; std::copy(record.begin(), record.end(), test.initial.begin() + (table + 192u + row * 64u) / 4u); + // BDA loads use a separate tuple, as the captured shader's record members do. + std::copy_n(record.begin(), 4u, test.initial.begin() + (table + 176u + row * 64u) / 4u); test.initial[slot * (row + 1u) / 4u] = ((0x3800u + row) << 16u) | (0x3c00u + row); test.initial[slot * (row + 1u) / 4u + 1u] = ((0xc000u + row) << 16u) | (0x4000u + row); } // Compile against poison selectors. A separate GPU dispatch writes the actual keys. auto compiled = CompileCase(test, vulkan.SubgroupSize()); + if (table_bytes == 0u) + test.forbidden_spirv = {"convert_indirect_buffer_format"}; + else + test.required_spirv = {"get_bda_pointer", "convert_indirect_buffer_format"}; + CheckSpirvEmission(test, compiled.spirv); + u32 bda_function = 0, bda_calls = 0; + for (size_t offset = 5; offset < compiled.spirv.size();) { + const auto words = std::span(compiled.spirv).subspan(offset, compiled.spirv[offset] >> 16u); + const auto opcode = static_cast(words[0] & 0xffffu); + if (opcode == spv::OpName && + std::string_view(reinterpret_cast(words.data() + 2)) == "get_bda_pointer") + bda_function = words[1]; + if (opcode == spv::OpFunctionCall && words[3] == bda_function) ++bda_calls; + offset += words.size(); + } + Require(test.name, "empty descriptor table specialization", (bda_calls == 0u) == (table_bytes == 0u), + "empty table retained BDA reads or nonempty table lost GPU descriptor loads"); for (const auto &descriptor : compiled.resources.buffers) { ShaderBufferResource resource{}; std::copy_n(descriptor.dwords.begin(), 4u, resource.fields); @@ -36846,15 +36869,18 @@ void CheckGpuSelectedRecordBufferAccess(VulkanHarness &vulkan) { const u32 dynamic = keys[group] * 192u + 96u; const u32 row = dynamic / 64u; for (u32 lane = 0; lane < wave; ++lane) { - const u32 out = slot * 13u / 4u + (group * wave + lane) * 2u; + const u32 out = slot * 13u / 4u + (group * wave + lane) * 4u; const bool source = uint64_t{dynamic} + 160u + 16u <= table_bytes && lane == 0u; test.expected[out] = source ? test.initial[slot * (row + 1u) / 4u] : 0u; test.expected[out + 1u] = source ? test.initial[slot * (row + 1u) / 4u + 1u] : 0u; + const bool bda_source = uint64_t{dynamic} + 144u + 16u <= table_bytes && lane == 0u; + test.expected[out + 2u] = bda_source ? 0x3c00u + row : 0u; + test.expected[out + 3u] = bda_source ? 0x3f800000u + (row << 13u) : 0u; if (uint64_t{dynamic} + 176u + 16u <= table_bytes && (lane & 1u) == 0u) bytes[slot * (row + 7u) + group * wave + lane] = std::byte{0xc3}; } } - auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size()); + auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size(), true); vulkan.Dispatch(producer, producer_shader, buffer); vulkan.Dispatch(test, compiled, buffer); CompareWords(test, "GPU selectors, wrapped offsets, partial descriptors, OOB and masked byte stores",