From 430de9bd1593c9abb56b2fb0eaf7c961a8d85d92 Mon Sep 17 00:00:00 2001 From: nmzik Date: Sun, 16 Aug 2026 09:30:45 +0200 Subject: [PATCH] graphics: support dynamic indirect image tables --- .../recompiler/backend/spirv/SpirvEmitter.cpp | 8 +- .../backend/spirv/spirvEmitterAnalysis.cpp | 7 + .../backend/spirv/spirvEmitterInternal.h | 2 + .../backend/spirv/spirvEmitterValueImage.cpp | 127 +++++++- src/graphics/shader/recompiler/ir/ShaderIR.h | 30 +- .../shader/recompiler/ir/ValueProgram.h | 1 + .../recompiler/ir/passes/BindingLayout.cpp | 7 +- .../ir/passes/ResourceMaterialization.cpp | 303 ++++++++++++++++-- .../ir/passes/ResourceMaterialization.h | 10 +- .../recompiler/ir/passes/ResourceTracking.cpp | 28 +- src/graphics/shader/shader.cpp | 13 +- tests/ResourceTrackingTests.cpp | 119 ++++++- tests/ShaderRecompilerComputeTests.cpp | 144 ++++++++- 13 files changed, 710 insertions(+), 89 deletions(-) diff --git a/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.cpp b/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.cpp index cc8c06826..03d247443 100644 --- a/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.cpp @@ -113,7 +113,13 @@ bool ValidateNativeProgram(const IR::Program& program, std::string* error) { if (!program.info.addresses.empty()) { Expect(Kind::AddressMemory, Dense(program.info.addresses.size())); } - if (program.values != nullptr && !program.values->srt_reads.empty()) { + const bool uses_flattened_runtime = + program.values != nullptr && + (!program.values->srt_reads.empty() || + std::ranges::any_of(program.info.images, [](const IR::ImageResource& image) { + return image.indirect_mapping_capacity != 0u; + })); + if (uses_flattened_runtime) { Expect(Kind::FlattenedSrt); } if (program.bindings.ShaderDataDwords() != 0 && program.bindings.push_constant_size == 0) { diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp index ec26a351b..091ae374f 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp @@ -445,6 +445,13 @@ uint32_t MakeSampledImage(EmitterState& state, const IR::MemoryInfo& mem, uint32 return sampled_image; } +uint32_t MakeSampledImage(EmitterState& state, const IR::MemoryInfo& mem, uint32_t use_pc, + ImageViewKind view, uint32_t image_resource) { + auto selected = mem; + selected.resource = image_resource; + return MakeSampledImage(state, selected, use_pc, view); +} + uint32_t StorageImageDescriptorPointer(EmitterState& state, uint32_t resource, bool uint_image, uint32_t use_pc, ImageViewKind view) { (void)use_pc; diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h index 93cbf4622..3d2ad7154 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h @@ -720,6 +720,8 @@ uint32_t LoadSamplerDescriptor(EmitterState& state, uint32_t sampler, uint32_t u uint32_t MakeSampledImage(EmitterState& state, const IR::MemoryInfo& mem, uint32_t use_pc, ImageViewKind view); +uint32_t MakeSampledImage(EmitterState& state, const IR::MemoryInfo& mem, uint32_t use_pc, + ImageViewKind view, uint32_t image_resource); ImageViewKind StorageImageViewKind(const EmitterState& state, const IR::MemoryInfo& mem, bool uint_image, uint32_t use_pc); diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterValueImage.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterValueImage.cpp index c95374bce..075f08e24 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterValueImage.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterValueImage.cpp @@ -1,6 +1,7 @@ #include "graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h" #include +#include namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter { namespace { @@ -423,11 +424,11 @@ bool EmitValueImage(ValueEmitContext& ctx, const IR::Inst& inst) { const auto layout = Layout(mem, view); const bool integer = mem.kind == IR::ResourceKind::ImageUint; const bool dref = HasFlag(mem, Decoder::ImageSampleFlagCompare); - const auto sampled = MakeSampledImage(state, mem, pc, view); const auto coord = CoordF32(ctx, mem, *address, layout.coord, ImageViewCoordinateComponents(view)); - const auto sample = state.builder.AllocateId(); if (op == IR::ValueOpcode::ImageGatherRaw) { + const auto sampled = MakeSampledImage(state, mem, pc, view); + const auto sample = state.builder.AllocateId(); std::vector words = dref ? std::vector {OpImageDrefGather, state.vec4_float_type, @@ -461,12 +462,10 @@ bool EmitValueImage(ValueEmitContext& ctx, const IR::Inst& inst) { : (dref ? OpImageSampleDrefImplicitLod : OpImageSampleImplicitLod); const auto result_type = dref ? state.float_type : (integer ? state.vec4_uint_type : state.vec4_float_type); - std::vector words {opcode, result_type, sample, sampled, coord}; - if (dref) { - words.push_back(layout.dref != NoImageComponent - ? AddressF32(ctx, mem, *address, layout.dref) - : ZeroF32(state)); - } + const auto dref_value = + dref ? (layout.dref != NoImageComponent ? AddressF32(ctx, mem, *address, layout.dref) + : ZeroF32(state)) + : 0u; uint32_t operand_mask = 0; std::vector operands; if (HasFlag(mem, Decoder::ImageSampleFlagDerivative)) { @@ -485,12 +484,114 @@ bool EmitValueImage(ValueEmitContext& ctx, const IR::Inst& inst) { operand_mask |= ImageOperandsBiasMask; operands.push_back(AddressF32(ctx, mem, *address, layout.bias)); } - if (operand_mask != 0u) { - words.push_back(operand_mask); - words.insert(words.end(), operands.begin(), operands.end()); + const auto EmitSample = [&](uint32_t resource) { + const auto sampled = MakeSampledImage(state, mem, pc, view, resource); + const auto sample = state.builder.AllocateId(); + std::vector words {opcode, result_type, sample, sampled, coord}; + if (dref) { + words.push_back(dref_value); + } + if (operand_mask != 0u) { + words.push_back(operand_mask); + words.insert(words.end(), operands.begin(), operands.end()); + } + state.builder.AddFunction(words); + return sample; + }; + const auto& image = state.program.info.images[mem.resource]; + if (image.indirect_root != mem.resource) { + ctx.Define(inst, ResultVector(ctx, EmitSample(mem.resource), integer, dref)); + return true; } - state.builder.AddFunction(words); - ctx.Define(inst, ResultVector(ctx, sample, integer, dref)); + const auto* handle = image_arg.ResolveInstruction(); + const auto* source = image.source < ctx.program.descriptor_sources.size() + ? &ctx.program.descriptor_sources[image.source] + : nullptr; + if (handle == nullptr || source == nullptr || !source->indirect_image.has_value() || + source->indirect_image->key_arg >= handle->NumArgs()) { + ctx.Fail(inst, "has invalid indirect image key provenance"); + return true; + } + const auto key = ctx.Def(handle->Arg(source->indirect_image->key_arg)); + if (state.flattened_srt_variable == 0 || image.indirect_mapping_capacity == 0u || + image.indirect_resources.size() < 2u) { + ctx.Fail(inst, "has no indirect image runtime mapping"); + return true; + } + const auto LoadMapping = [&](uint32_t index) { + const auto pointer = state.builder.AllocateId(); + state.builder.AddFunction({OpAccessChain, state.ptr_storage_buffer_uint, pointer, + state.flattened_srt_variable, ConstantU32(state, 0), index}); + const auto value = state.builder.AllocateId(); + state.builder.AddFunction({OpLoad, state.uint_type, value, pointer}); + return value; + }; + const auto mapping = ConstantU32(state, image.indirect_mapping_offset); + auto low = ConstantU32(state, 0u); + auto high = LoadMapping(mapping); + auto selected = ConstantU32(state, 0u); + for (uint32_t iteration = 0; iteration < std::bit_width(image.indirect_mapping_capacity); + iteration++) { + const auto active = Binary(state, OpULessThan, state.bool_type, low, high); + const auto mid = + Binary(state, OpShiftRightLogical, state.uint_type, + Binary(state, OpIAdd, state.uint_type, low, high), ConstantU32(state, 1u)); + const auto probe = state.builder.AllocateId(); + state.builder.AddFunction( + {OpSelect, state.uint_type, probe, active, mid, ConstantU32(state, 0u)}); + const auto entry = Binary(state, OpIAdd, state.uint_type, mapping, + Binary(state, OpIAdd, state.uint_type, + Binary(state, OpShiftLeftLogical, state.uint_type, + probe, ConstantU32(state, 1u)), + ConstantU32(state, 1u))); + const auto mapped_key = LoadMapping(entry); + const auto candidate = + LoadMapping(Binary(state, OpIAdd, state.uint_type, entry, ConstantU32(state, 1u))); + const auto equal = Binary(state, OpIEqual, state.bool_type, mapped_key, key); + const auto match = Binary(state, OpLogicalAnd, state.bool_type, active, equal); + const auto next_selected = state.builder.AllocateId(); + state.builder.AddFunction( + {OpSelect, state.uint_type, next_selected, match, candidate, selected}); + selected = next_selected; + const auto less = Binary(state, OpULessThan, state.bool_type, mapped_key, key); + const auto take_upper = Binary(state, OpLogicalAnd, state.bool_type, active, less); + const auto take_lower = Binary(state, OpLogicalAnd, state.bool_type, active, + Unary(state, OpLogicalNot, state.bool_type, less)); + const auto next_low = state.builder.AllocateId(); + state.builder.AddFunction( + {OpSelect, state.uint_type, next_low, take_upper, + Binary(state, OpIAdd, state.uint_type, mid, ConstantU32(state, 1u)), low}); + low = next_low; + const auto next_high = state.builder.AllocateId(); + state.builder.AddFunction( + {OpSelect, state.uint_type, next_high, take_lower, mid, high}); + high = next_high; + } + const auto default_label = state.builder.AllocateId(); + const auto merge_label = state.builder.AllocateId(); + std::vector labels(image.indirect_resources.size() - 1u); + std::vector switch_words {OpSwitch, selected, default_label}; + for (uint32_t candidate = 1; candidate < image.indirect_resources.size(); candidate++) { + labels[candidate - 1u] = state.builder.AllocateId(); + switch_words.push_back(candidate); + switch_words.push_back(labels[candidate - 1u]); + } + state.builder.AddFunction({OpSelectionMerge, merge_label, SelectionControlNone}); + state.builder.AddFunction(switch_words); + std::vector phi_words {OpPhi, result_type, state.builder.AllocateId()}; + state.builder.AddFunction({OpLabel, default_label}); + phi_words.push_back(EmitSample(image.indirect_resources[0])); + phi_words.push_back(default_label); + state.builder.AddFunction({OpBranch, merge_label}); + for (uint32_t candidate = 1; candidate < image.indirect_resources.size(); candidate++) { + state.builder.AddFunction({OpLabel, labels[candidate - 1u]}); + phi_words.push_back(EmitSample(image.indirect_resources[candidate])); + phi_words.push_back(labels[candidate - 1u]); + state.builder.AddFunction({OpBranch, merge_label}); + } + state.builder.AddFunction({OpLabel, merge_label}); + state.builder.AddFunction(phi_words); + ctx.Define(inst, ResultVector(ctx, phi_words[2], integer, dref)); return true; } const auto atomic_opcode = ImageAtomicOpcode(op); diff --git a/src/graphics/shader/recompiler/ir/ShaderIR.h b/src/graphics/shader/recompiler/ir/ShaderIR.h index fc87dbf21..16c869e90 100644 --- a/src/graphics/shader/recompiler/ir/ShaderIR.h +++ b/src/graphics/shader/recompiler/ir/ShaderIR.h @@ -190,18 +190,24 @@ enum class ImageMipMode { None, DynamicStorage }; constexpr uint32_t StorageImageIdentitySwizzle = 0x00000facu; struct ImageResource { - uint32_t source = 0; - uint32_t first_use_pc = 0; - ResourceKind kind = ResourceKind::None; - Decoder::ImageDimension dimension = Decoder::ImageDimension::Unknown; - ImageMipMode mip_mode = ImageMipMode::None; - uint32_t mip_count = 1; - uint32_t storage_swizzle = StorageImageIdentitySwizzle; - bool read = false; - bool written = false; - bool atomic = false; - bool depth_compare = false; - bool cube = false; + static constexpr uint32_t NoIndirectImage = UINT32_MAX; + + uint32_t source = 0; + uint32_t first_use_pc = 0; + ResourceKind kind = ResourceKind::None; + Decoder::ImageDimension dimension = Decoder::ImageDimension::Unknown; + ImageMipMode mip_mode = ImageMipMode::None; + uint32_t mip_count = 1; + uint32_t storage_swizzle = StorageImageIdentitySwizzle; + bool read = false; + bool written = false; + bool atomic = false; + bool depth_compare = false; + bool cube = false; + uint32_t indirect_root = NoIndirectImage; + uint32_t indirect_mapping_offset = 0; + uint32_t indirect_mapping_capacity = 0; + std::vector indirect_resources; bool operator==(const ImageResource& other) const = default; }; diff --git a/src/graphics/shader/recompiler/ir/ValueProgram.h b/src/graphics/shader/recompiler/ir/ValueProgram.h index 2743264ca..fbfb7355c 100644 --- a/src/graphics/shader/recompiler/ir/ValueProgram.h +++ b/src/graphics/shader/recompiler/ir/ValueProgram.h @@ -25,6 +25,7 @@ struct DescriptorSource { uint32_t heap_source = 0; uint32_t selector_stride = 0; uint32_t selector_offset = 0; + uint32_t key_arg = 0; bool operator==(const IndirectImage& other) const = default; }; diff --git a/src/graphics/shader/recompiler/ir/passes/BindingLayout.cpp b/src/graphics/shader/recompiler/ir/passes/BindingLayout.cpp index 51626ba05..10e04184f 100644 --- a/src/graphics/shader/recompiler/ir/passes/BindingLayout.cpp +++ b/src/graphics/shader/recompiler/ir/passes/BindingLayout.cpp @@ -230,7 +230,12 @@ bool AllocateBindings(Program& program, const BindingLayoutOptions& options, std } AddBinding(next, DescriptorBindingKind::AddressMemory, std::move(resources)); } - if (!program.values->srt_reads.empty()) { + const bool uses_flattened_runtime = + !program.values->srt_reads.empty() || + std::ranges::any_of(program.info.images, [](const ImageResource& image) { + return image.indirect_mapping_capacity != 0u; + }); + if (uses_flattened_runtime) { AddBinding(next, DescriptorBindingKind::FlattenedSrt); } diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index caee76313..179e8199a 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -176,7 +176,8 @@ bool ReadScalarBufferWord(const ShaderBufferResource& descriptor, uint32_t dynam bool MaterializeIndirectImage(const DescriptorSource::IndirectImage& indirect, const DescriptorValue& material_value, const DescriptorValue& heap_value, const SrtRuntime& runtime, - DescriptorValue& result, std::string* error, uint32_t use_pc) { + ResourceSnapshot::IndirectImage& result, std::string* error, + uint32_t use_pc) { ShaderBufferResource material; ShaderBufferResource heap; if (!DecodeBufferDescriptor(material_value, material) || @@ -228,9 +229,10 @@ bool MaterializeIndirectImage(const DescriptorSource::IndirectImage& indirect, } } - DescriptorValue invariant; - bool have_invariant = false; - for (const auto key: keys) { + ResourceSnapshot::IndirectImage next; + next.capacity = static_cast(probe_count + 1u); + next.keys = std::move(keys); + for (const auto key: next.keys) { DescriptorValue candidate; candidate.dword_count = 8u; const auto heap_offset = key << 5u; @@ -240,18 +242,18 @@ bool MaterializeIndirectImage(const DescriptorSource::IndirectImage& indirect, return false; } } - if (!have_invariant) { - invariant = candidate; - have_invariant = true; - } else if (candidate != invariant) { - if (error != nullptr) { - *error = - fmt::format("indirect image table at pc 0x{:08x} is not invariant", use_pc); - } - return false; + if (NullImageDescriptor(candidate) || !ValidImageDescriptor(candidate)) { + candidate.dwords.fill(0); + } + const auto found = std::ranges::find(next.descriptors, candidate); + if (found == next.descriptors.end()) { + next.descriptors.push_back(candidate); + next.candidates.push_back(static_cast(next.descriptors.size() - 1u)); + } else { + next.candidates.push_back(static_cast(found - next.descriptors.begin())); } } - result = invariant; + result = std::move(next); return true; } @@ -280,6 +282,18 @@ bool IsImageOp(ValueOpcode op) { } } +size_t FlattenedRuntimeDwords(const Program& program) { + size_t size = program.values != nullptr ? program.values->srt_reads.size() : 0u; + for (uint32_t resource = 0; resource < program.info.images.size(); resource++) { + const auto& image = program.info.images[resource]; + if (image.indirect_root == resource) { + size = std::max(size, static_cast(image.indirect_mapping_offset) + 1u + + static_cast(image.indirect_mapping_capacity) * 2u); + } + } + return size; +} + } // namespace bool ValidateResourceSnapshot(const Program& program, const ResourceSnapshot& snapshot, @@ -300,7 +314,7 @@ bool ValidateResourceSnapshot(const Program& program, const ResourceSnapshot& sn return false; } if (program.values == nullptr || - snapshot.flattened_srt.size() != program.values->srt_reads.size()) { + snapshot.flattened_srt.size() != FlattenedRuntimeDwords(program)) { if (error != nullptr) { *error = "flattened SRT snapshot does not match the shader plan"; } @@ -346,6 +360,36 @@ bool ValidateResourceSnapshot(const Program& program, const ResourceSnapshot& sn return false; } } + std::unordered_set indirect_resources; + for (const auto& table: snapshot.indirect_images) { + const auto* source = table.resource < program.info.images.size() + ? Source(program, program.info.images[table.resource].source) + : nullptr; + if (source == nullptr || !source->indirect_image.has_value() || + program.info.images[table.resource].indirect_root != ImageResource::NoIndirectImage || + table.keys.empty() || table.keys.size() != table.candidates.size() || + table.capacity < table.keys.size() || table.descriptors.empty() || + !indirect_resources.insert(table.resource).second) { + if (error != nullptr) { + *error = "indirect image snapshot does not match the shader plan"; + } + return false; + } + for (const auto candidate: table.candidates) { + if (candidate >= table.descriptors.size()) { + if (error != nullptr) { + *error = "indirect image snapshot has an invalid candidate"; + } + return false; + } + } + if (snapshot.images[table.resource] != table.descriptors[table.candidates[0]]) { + if (error != nullptr) { + *error = "indirect image snapshot has an inconsistent root descriptor"; + } + return false; + } + } return CheckWidth(snapshot.buffers, 4, "buffer") && CheckWidth(snapshot.images, 8, "image") && CheckWidth(snapshot.samplers, 4, "sampler"); } @@ -355,6 +399,12 @@ bool ValidateResourceSpecialization(const Program& program, const ResourceSnapsh if (!ValidateResourceSnapshot(program, snapshot, error)) { return false; } + if (!snapshot.indirect_images.empty()) { + if (error != nullptr) { + *error = "indirect image snapshot was not specialized"; + } + return false; + } for (uint32_t i = 0; i < program.info.buffers.size(); i++) { const auto& buffer = program.info.buffers[i]; ShaderBufferResource descriptor; @@ -384,6 +434,23 @@ bool ValidateResourceSpecialization(const Program& program, const ResourceSnapsh return false; } if (NullImageDescriptor(descriptor)) { + if (image.indirect_root != ImageResource::NoIndirectImage) { + const auto root = image.indirect_root; + if (root >= program.info.images.size() || + program.info.images[root].kind != image.kind || + program.info.images[root].dimension != image.dimension || + program.info.images[root].mip_mode != image.mip_mode || + program.info.images[root].mip_count != image.mip_count || + program.info.images[root].storage_swizzle != image.storage_swizzle || + program.info.images[root].cube != image.cube) { + if (error != nullptr) { + *error = fmt::format( + "null indirect image descriptor {} changed binding class", i); + } + return false; + } + continue; + } bool canonical_kind = image.kind == ResourceKind::Image || image.kind == ResourceKind::StorageImage; if (image.atomic) { @@ -522,11 +589,18 @@ bool MaterializeResources(const Program& program, const SrtRuntime& runtime, descriptor.dwords.fill(0); } } - next.images.reserve(program.info.images.size()); - for (const auto& image: program.info.images) { - const auto* source = Source(program, image.source); - DescriptorValue descriptor; + next.flattened_srt = std::move(flattened_srt); + next.flattened_srt.resize(FlattenedRuntimeDwords(program)); + next.images.resize(program.info.images.size()); + std::vector image_written(program.info.images.size()); + for (uint32_t image_index = 0; image_index < program.info.images.size(); image_index++) { + const auto& image = program.info.images[image_index]; + const auto* source = Source(program, image.source); if (source != nullptr && source->indirect_image.has_value()) { + if (image.indirect_root != ImageResource::NoIndirectImage && + image.indirect_root != image_index) { + continue; + } const std::array requests { DescriptorSourceRequest {source->indirect_image->material_source, image.first_use_pc}, @@ -537,19 +611,69 @@ bool MaterializeResources(const Program& program, const SrtRuntime& runtime, if (!EvaluateDescriptorSources(program, requests, clean_runtime, tables, error)) { return false; } - const auto& material = tables[0]; - const auto& heap = tables[1]; - if (!MaterializeIndirectImage(*source->indirect_image, material, heap, runtime, - descriptor, error, image.first_use_pc)) { + const auto& material = tables[0]; + const auto& heap = tables[1]; + ResourceSnapshot::IndirectImage table; + if (!MaterializeIndirectImage(*source->indirect_image, material, heap, runtime, table, + error, image.first_use_pc)) { return false; } + if (image.indirect_root == ImageResource::NoIndirectImage) { + next.images[image_index] = table.descriptors[table.candidates[0]]; + image_written[image_index] = 1u; + if (table.descriptors.size() > 1u) { + table.resource = image_index; + next.indirect_images.push_back(std::move(table)); + } + continue; + } + if (table.keys.size() > image.indirect_mapping_capacity || + table.descriptors.size() > image.indirect_resources.size()) { + if (error != nullptr) { + *error = fmt::format( + "indirect image table at pc 0x{:08x} changed candidate topology", + image.first_use_pc); + } + return false; + } + for (uint32_t candidate = 0; candidate < image.indirect_resources.size(); candidate++) { + const auto resource = image.indirect_resources[candidate]; + if (resource >= program.info.images.size() || + program.info.images[resource].indirect_root != image_index) { + if (error != nullptr) { + *error = "indirect image specialization has an invalid candidate resource"; + } + return false; + } + next.images[resource] = + table.descriptors[candidate < table.descriptors.size() ? candidate : 0u]; + image_written[resource] = 1u; + } + std::vector order(table.keys.size()); + std::iota(order.begin(), order.end(), 0u); + std::ranges::sort(order, {}, [&](uint32_t index) { return table.keys[index]; }); + const auto mapping = image.indirect_mapping_offset; + next.flattened_srt[mapping] = static_cast(table.keys.size()); + for (uint32_t entry = 0; entry < order.size(); entry++) { + const auto source = order[entry]; + const auto offset = mapping + 1u + entry * 2u; + next.flattened_srt[offset] = table.keys[source]; + next.flattened_srt[offset + 1] = table.candidates[source]; + } } else { - descriptor = *cursor++; + auto descriptor = *cursor++; + if (!ValidImageDescriptor(descriptor)) { + descriptor.dwords.fill(0); + } + next.images[image_index] = descriptor; + image_written[image_index] = 1u; } - if (!ValidImageDescriptor(descriptor)) { - descriptor.dwords.fill(0); + } + if (std::ranges::find(image_written, uint8_t {0}) != image_written.end()) { + if (error != nullptr) { + *error = "indirect image specialization left an unmaterialized candidate"; } - next.images.push_back(descriptor); + return false; } next.samplers.assign(cursor, cursor + program.info.samplers.size()); cursor += program.info.samplers.size(); @@ -584,7 +708,6 @@ bool MaterializeResources(const Program& program, const SrtRuntime& runtime, next.addresses.push_back({*runtime.flat_memory_base, *runtime.flat_memory_base}); } } - next.flattened_srt = std::move(flattened_srt); next.user_data.assign(runtime.user_data.begin(), runtime.user_data.end()); if (!ValidateResourceSnapshot(program, next, error)) { return false; @@ -593,7 +716,7 @@ bool MaterializeResources(const Program& program, const SrtRuntime& runtime, return true; } -bool SpecializeResources(Program& program, const ResourceSnapshot& snapshot, std::string* error) { +bool SpecializeResources(Program& program, ResourceSnapshot& snapshot, std::string* error) { if (!program.resource_tracking_complete || program.shader_info_complete || program.binding_layout_complete) { if (error != nullptr) { @@ -606,10 +729,50 @@ bool SpecializeResources(Program& program, const ResourceSnapshot& snapshot, std return false; } - auto next = program.info; + auto next = program.info; + auto next_snapshot = snapshot; + for (const auto& table: snapshot.indirect_images) { + if (table.resource >= next.images.size() || table.descriptors.size() < 2u || + next.images.size() + table.descriptors.size() - 1u > ShaderInfo::MaxImages) { + if (error != nullptr) { + *error = "indirect image candidates exceed the dense image resource limit"; + } + return false; + } + const auto root_image = next.images[table.resource]; + std::vector resources(table.descriptors.size()); + resources[0] = table.resource; + for (uint32_t candidate = 1; candidate < table.descriptors.size(); candidate++) { + resources[candidate] = static_cast(next.images.size()); + auto image = root_image; + image.indirect_root = table.resource; + next.images.push_back(std::move(image)); + next_snapshot.images.push_back(table.descriptors[candidate]); + } + auto& root = next.images[table.resource]; + root.indirect_root = table.resource; + root.indirect_mapping_offset = static_cast(next_snapshot.flattened_srt.size()); + root.indirect_mapping_capacity = table.capacity; + root.indirect_resources = std::move(resources); + next_snapshot.flattened_srt.resize(next_snapshot.flattened_srt.size() + 1u + + static_cast(table.capacity) * 2u); + std::vector order(table.keys.size()); + std::iota(order.begin(), order.end(), 0u); + std::ranges::sort(order, {}, [&](uint32_t index) { return table.keys[index]; }); + next_snapshot.flattened_srt[root.indirect_mapping_offset] = + static_cast(table.keys.size()); + for (uint32_t entry = 0; entry < order.size(); entry++) { + const auto source = order[entry]; + const auto offset = root.indirect_mapping_offset + 1u + entry * 2u; + next_snapshot.flattened_srt[offset] = table.keys[source]; + next_snapshot.flattened_srt[offset + 1] = table.candidates[source]; + } + next_snapshot.images[table.resource] = table.descriptors[0]; + } + next_snapshot.indirect_images.clear(); for (uint32_t i = 0; i < next.buffers.size(); i++) { ShaderBufferResource descriptor; - if (!DecodeBufferDescriptor(snapshot.buffers[i], descriptor)) { + if (!DecodeBufferDescriptor(next_snapshot.buffers[i], descriptor)) { if (error != nullptr) { *error = fmt::format("buffer descriptor {} has invalid width", i); } @@ -621,10 +784,10 @@ bool SpecializeResources(Program& program, const ResourceSnapshot& snapshot, std } for (uint32_t i = 0; i < next.addresses.size(); i++) { next.addresses[i].specialized_base = - AddressSpecialization(next.addresses[i], snapshot.addresses[i]); + AddressSpecialization(next.addresses[i], next_snapshot.addresses[i]); } for (uint32_t i = 0; i < next.images.size(); i++) { - const auto& descriptor = snapshot.images[i]; + const auto& descriptor = next_snapshot.images[i]; auto& image = next.images[i]; image.mip_count = StorageMipCount(image, descriptor); if (image.mip_count == 0u) { @@ -681,6 +844,68 @@ bool SpecializeResources(Program& program, const ResourceSnapshot& snapshot, std } } } + for (uint32_t root_index = 0; root_index < next.images.size(); root_index++) { + auto& root = next.images[root_index]; + if (root.indirect_root != root_index) { + continue; + } + if (root.indirect_mapping_capacity == 0u || root.indirect_resources.size() < 2u || + static_cast(root.indirect_mapping_offset) + 1u + + static_cast(root.indirect_mapping_capacity) * 2u > + next_snapshot.flattened_srt.size()) { + if (error != nullptr) { + *error = "indirect image specialization has an invalid key mapping"; + } + return false; + } + uint32_t exemplar = ImageResource::NoIndirectImage; + for (const auto resource: root.indirect_resources) { + if (resource >= next.images.size() || + next.images[resource].indirect_root != root_index) { + if (error != nullptr) { + *error = "indirect image specialization has an invalid candidate"; + } + return false; + } + if (!NullImageDescriptor(next_snapshot.images[resource])) { + exemplar = resource; + break; + } + } + if (exemplar == ImageResource::NoIndirectImage) { + if (error != nullptr) { + *error = "indirect image specialization has no typed candidate"; + } + return false; + } + const auto& image_class = next.images[exemplar]; + for (uint32_t candidate = 0; candidate < next.images.size(); candidate++) { + auto& image = next.images[candidate]; + if (image.indirect_root != root_index) { + continue; + } + if (NullImageDescriptor(next_snapshot.images[candidate])) { + image.kind = image_class.kind; + image.dimension = image_class.dimension; + image.mip_count = image_class.mip_count; + image.storage_swizzle = image_class.storage_swizzle; + image.cube = image_class.cube; + } + if (image.kind != image_class.kind || image.dimension != image_class.dimension || + image.mip_mode != image_class.mip_mode || + image.mip_count != image_class.mip_count || + image.storage_swizzle != image_class.storage_swizzle || + image.depth_compare != image_class.depth_compare || + image.cube != image_class.cube) { + if (error != nullptr) { + *error = fmt::format( + "indirect image table at pc 0x{:08x} has incompatible candidates", + root.first_use_pc); + } + return false; + } + } + } if (program.values == nullptr) { if (error != nullptr) { *error = "typed SSA is not ready"; @@ -709,7 +934,16 @@ bool SpecializeResources(Program& program, const ResourceSnapshot& snapshot, std } return false; } - const auto& image = next.images[memory.resource]; + const auto& image = next.images[memory.resource]; + if (image.indirect_root == memory.resource && + inst.GetOpcode() != ValueOpcode::ImageSampleRaw) { + if (error != nullptr) { + *error = fmt::format( + "indirect image table at pc 0x{:08x} is used by unsupported {}", + inst.Flags().pc, ValueOpcodeName(inst.GetOpcode())); + } + return false; + } memory.kind = image.kind; memory.image_dimension = image.dimension; memory.image_cube = image.cube; @@ -717,6 +951,7 @@ bool SpecializeResources(Program& program, const ResourceSnapshot& snapshot, std } program.info = std::move(next); program.values->memory_info = std::move(memory_info); + snapshot = std::move(next_snapshot); return true; } diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h index af73f31b0..8b89968f3 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h @@ -14,6 +14,13 @@ struct ResourceSnapshot { bool operator==(const Address& other) const = default; }; + struct IndirectImage { + uint32_t resource = 0; + uint32_t capacity = 0; + std::vector keys; + std::vector candidates; + std::vector descriptors; + }; std::vector buffers; std::vector images; @@ -21,6 +28,7 @@ struct ResourceSnapshot { std::vector
addresses; std::vector flattened_srt; std::vector user_data; + std::vector indirect_images; }; bool ValidateResourceSnapshot(const Program& program, const ResourceSnapshot& snapshot, @@ -35,7 +43,7 @@ bool MaterializeResources(const Program& program, const SrtRuntime& runtime, // Applies runtime descriptor shape/format facts to a copied dense topology before layout and // emission. On failure the program is unchanged. -bool SpecializeResources(Program& program, const ResourceSnapshot& snapshot, std::string* error); +bool SpecializeResources(Program& program, ResourceSnapshot& snapshot, std::string* error); } // namespace Libs::Graphics::ShaderRecompiler::IR diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index 8e235c4de..0edb838b9 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -169,8 +169,12 @@ public: } } for (const auto& plan: m_indirect_images) { - for (uint32_t dword = 0; dword < plan.roots.size(); dword++) { - plan.handle->SetArg(dword, plan.roots[dword]); + plan.handle->SetArg(0, plan.key); + for (uint32_t dword = 0; dword < 4u; dword++) { + plan.handle->SetArg(dword + 1u, plan.roots[dword + 4u]); + } + for (uint32_t dword = 5u; dword < plan.roots.size(); dword++) { + plan.handle->SetArg(dword, plan.key); } for (const auto index: plan.memory) { m_values.memory_info[index].planning_only = true; @@ -204,9 +208,10 @@ private: struct IndirectImagePlan { Inst* handle = nullptr; uint32_t source = 0; + Value key; std::array roots {}; - std::array memory {}; - std::array reads {}; + std::array memory {}; + std::array reads {}; }; bool Fail(uint32_t pc, std::string* error, const std::string& reason) const { @@ -474,8 +479,8 @@ private: } else if (!EquivalentValue(m_values, heap_offset, heap_reads[dword]->Arg(1))) { return false; } - plan.memory[dword + 1u] = memory_index; - plan.reads[dword + 1u] = heap_reads[dword]; + plan.memory[dword] = memory_index; + plan.reads[dword] = heap_reads[dword]; } const auto* shift = heap_offset.TryInstruction(); @@ -538,13 +543,12 @@ private: std::copy(heap_source.dwords.begin(), heap_source.dwords.begin() + 4u, image_source.dwords.begin() + 4u); image_source.indirect_image = DescriptorSource::IndirectImage { - material_source_index, heap_source_index, selector_stride, selector_offset}; + material_source_index, heap_source_index, selector_stride, selector_offset, 0u}; - plan.handle = &handle; - plan.source = InternSource(image_source); - plan.roots = image_source.dwords; - plan.memory[0] = material_memory_index; - plan.reads[0] = material_read; + plan.handle = &handle; + plan.source = InternSource(image_source); + plan.key = Value(material_read); + plan.roots = image_source.dwords; return true; } diff --git a/src/graphics/shader/shader.cpp b/src/graphics/shader/shader.cpp index 3789182fe..fbc7aa9e2 100644 --- a/src/graphics/shader/shader.cpp +++ b/src/graphics/shader/shader.cpp @@ -1029,9 +1029,10 @@ static std::string ShaderDescribeSpecialization(const ShaderRecompiler::IR::Prog for (uint32_t i = 0; i < program.info.images.size(); i++) { const auto& image = program.info.images[i]; ret += fmt::format( - " i{}[kind={} dim={} mip={}/{} swizzle=0x{:03x}]", i, static_cast(image.kind), - static_cast(image.dimension), static_cast(image.mip_mode), - image.mip_count, image.storage_swizzle); + " i{}[kind={} dim={} mip={}/{} swizzle=0x{:03x} indirect={}/{}+{}]", i, + static_cast(image.kind), static_cast(image.dimension), + static_cast(image.mip_mode), image.mip_count, image.storage_swizzle, + image.indirect_root, image.indirect_mapping_offset, image.indirect_mapping_capacity); } for (uint32_t i = 0; i < program.info.addresses.size(); i++) { ret += fmt::format(" a{}[base=0x{:x}]", i, program.info.addresses[i].specialized_base); @@ -1068,9 +1069,15 @@ static void ShaderAppendNativeSpecialization(std::vector& for (const auto& image: program.info.images) { ids.push_back(static_cast(image.kind)); ids.push_back(static_cast(image.dimension)); + ids.push_back(static_cast(image.cube)); ids.push_back(static_cast(image.mip_mode)); ids.push_back(image.mip_count); ids.push_back(image.storage_swizzle); + ids.push_back(image.indirect_root); + ids.push_back(image.indirect_mapping_offset); + ids.push_back(image.indirect_mapping_capacity); + ids.push_back(static_cast(image.indirect_resources.size())); + ids.insert(ids.end(), image.indirect_resources.begin(), image.indirect_resources.end()); } ids.push_back(static_cast(program.info.addresses.size())); for (const auto& address: program.info.addresses) { diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 43ddff146..a68a0ea70 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -247,15 +247,24 @@ void TestInvariantIndirectImageMaterialization() { ValidateValueProgram(*fixture->program.values, true, &validation_error), "post-tracking dead-code elimination invalidated descriptor provenance"); - Check(fixture->program.info.buffers.empty() && + Check(fixture->program.info.buffers.size() == 1 && fixture->program.info.images.size() == 1 && - fixture->program.values->dynamic_reads.empty(), - "indirect image planning reads leaked into shader resources"); + fixture->program.values->dynamic_reads.size() == 1, + "indirect image key was not retained as a scalar-buffer read"); const auto source = fixture->program.info.images[0].source; Check(source < fixture->program.values->descriptor_sources.size() && fixture->program.values->descriptor_sources[source] .indirect_image.has_value(), "indirect image source was not retained for runtime proof"); + const auto image_handle = std::ranges::find_if( + *fixture->block, [](const Inst &inst) { + return inst.GetOpcode() == ValueOpcode::GetImageResource; + }); + Check(image_handle != fixture->block->end() && + image_handle->Arg(0).ResolveInstruction() != nullptr && + image_handle->Arg(0).ResolveInstruction()->GetOpcode() == + ValueOpcode::ReadConstBuffer, + "indirect image handle discarded the live material key"); std::array user_data{0x1000u, 224u << 16u, 2u, 0u, 0x2000u, 16u << 16u, 4u, 0u, 7u}; @@ -284,24 +293,114 @@ void TestInvariantIndirectImageMaterialization() { .read_specialization_memory = ReadLinearTestMemory}; ResourceSnapshot snapshot; std::string error; + const auto same_snapshot = [](const ResourceSnapshot &lhs, + const ResourceSnapshot &rhs) { + return lhs.buffers == rhs.buffers && lhs.images == rhs.images && + lhs.samplers == rhs.samplers && lhs.addresses == rhs.addresses && + lhs.flattened_srt == rhs.flattened_srt && + lhs.user_data == rhs.user_data && + lhs.indirect_images.empty() == rhs.indirect_images.empty(); + }; Check(MaterializeResources(fixture->program, runtime, snapshot, &error) && snapshot.images.size() == 1 && std::equal(image_descriptor.begin(), image_descriptor.end(), snapshot.images[0].dwords.begin()), "invariant indirect image table did not materialize"); - const auto prior_image = snapshot.images[0]; + const auto prior_snapshot = snapshot; memory.fail_address = 0x1004u; Check(!MaterializeResources(fixture->program, runtime, snapshot, &error) && error.find("scalar read") != std::string::npos && - snapshot.images[0] == prior_image, + same_snapshot(snapshot, prior_snapshot), "rejected planning memory read mutated the snapshot"); memory.fail_address = UINT64_MAX; + memory.words[(0x1000u - memory.base + 36u) / 4u] = 1u; - Check(!MaterializeResources(fixture->program, runtime, snapshot, &error) && - error.find("not invariant") != std::string::npos && - snapshot.images[0] == prior_image, - "divergent indirect image table was accepted or mutated the snapshot"); + for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) { + memory.words[(0x2000u - memory.base) / 4u + dword] = 0u; + memory.words[(0x2020u - memory.base) / 4u + dword] = 0u; + } + memory.words[(0x2000u - memory.base) / 4u + 1u] = image_descriptor[1]; + memory.words[(0x2000u - memory.base) / 4u + 3u] = image_descriptor[3]; + memory.words[(0x2020u - memory.base) / 4u + 1u] = image_descriptor[1]; + memory.words[(0x2020u - memory.base) / 4u + 3u] = + image_descriptor[3] ^ (1u << 28u); + ResourceSnapshot null_snapshot; + Check(MaterializeResources(fixture->program, runtime, null_snapshot, &error) && + null_snapshot.indirect_images.empty() && + std::ranges::all_of(null_snapshot.images[0].dwords, + [](uint32_t dword) { return dword == 0u; }), + "stale typed null image descriptors were not canonicalized"); + + for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) { + memory.words[(0x2000u - memory.base) / 4u + dword] = + image_descriptor[dword]; + memory.words[(0x2020u - memory.base) / 4u + dword] = + image_descriptor[dword]; + } + memory.words[(0x2020u - memory.base) / 4u] ^= 1u; + memory.words[(0x1000u - memory.base + 36u) / 4u] = 1u; + ResourceSnapshot dynamic_snapshot; + Check(MaterializeResources(fixture->program, runtime, dynamic_snapshot, + &error) && + dynamic_snapshot.images.size() == 1 && + dynamic_snapshot.indirect_images.size() == 1 && + dynamic_snapshot.indirect_images[0].descriptors.size() == 2 && + SpecializeResources(fixture->program, dynamic_snapshot, &error) && + fixture->program.info.images.size() == 2 && + fixture->program.info.images[0].indirect_root == 0 && + fixture->program.info.images[0].indirect_mapping_capacity != 0 && + fixture->program.info.images[0].indirect_resources.size() == 2 && + dynamic_snapshot.images.size() == 2 && + dynamic_snapshot.indirect_images.empty(), + "dynamic indirect image table was not specialized transactionally"); + + for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) { + memory.words[(0x2000u - memory.base) / 4u + dword] = + image_descriptor[dword]; + memory.words[(0x2020u - memory.base) / 4u + dword] = + image_descriptor[dword]; + } + memory.words[(0x2000u - memory.base) / 4u] += 0x100u; + memory.words[(0x2020u - memory.base) / 4u] += 0x101u; + ResourceSnapshot rebound_snapshot; + Check(MaterializeResources(fixture->program, runtime, rebound_snapshot, + &error) && + ValidateResourceSpecialization(fixture->program, rebound_snapshot, + &error), + "stable indirect key mapping did not accept changed image addresses"); + memory.words[(0x2020u - memory.base) / 4u] = + memory.words[(0x2000u - memory.base) / 4u]; + Check(MaterializeResources(fixture->program, runtime, rebound_snapshot, + &error) && + ValidateResourceSpecialization(fixture->program, rebound_snapshot, + &error), + "runtime indirect key mapping did not accept collapsed candidates"); + const auto collapsed_snapshot = rebound_snapshot; + ResourceSnapshot capacity_snapshot; + for (const uint32_t records : {1u, 3u}) { + user_data[2] = records; + Check(MaterializeResources(fixture->program, runtime, capacity_snapshot, + &error) && + ValidateResourceSpecialization(fixture->program, + capacity_snapshot, &error), + "runtime indirect key mapping rejected a fitting material-table size"); + } + user_data[2] = 2u; + memory.words[(0x2020u - memory.base) / 4u] = + memory.words[(0x2000u - memory.base) / 4u] + 1u; + memory.words[(0x2040u - memory.base) / 4u] = + memory.words[(0x2000u - memory.base) / 4u] + 2u; + for (uint32_t dword = 1; dword < image_descriptor.size(); dword++) { + memory.words[(0x2040u - memory.base) / 4u + dword] = + image_descriptor[dword]; + } + memory.words[(0x1000u - memory.base + 68u) / 4u] = 2u; + Check(!MaterializeResources(fixture->program, runtime, rebound_snapshot, + &error) && + error.find("candidate topology") != std::string::npos && + same_snapshot(rebound_snapshot, collapsed_snapshot), + "larger indirect candidate topology reused or mutated a cached snapshot"); auto memory_backed = MakeIndirectImageFixture(false, 0u, true); memory_backed->PlanAndTrack(); @@ -319,7 +418,7 @@ void TestInvariantIndirectImageMaterialization() { Check(!MaterializeResources(memory_backed->program, memory_backed_runtime, snapshot, &error) && error.find("constant read failed") != std::string::npos && - snapshot.images[0] == prior_image, + same_snapshot(snapshot, prior_snapshot), "rejected indirect table descriptor read mutated the snapshot"); memory.fail_address = UINT64_MAX; diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 84cffa41e..e9b5c634b 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -16737,6 +16737,126 @@ TestCase ImageStoreVariants() { return test; } +void CheckIndirectImageKeySwitch() { + constexpr const char *name = "IndirectImageKeySwitch"; + constexpr uint32_t mapping_capacity = 1793u; + using namespace ShaderRecompiler::IR; + + Program program{}; + program.stage = ShaderType::Compute; + program.wave_size = 32; + program.srt_plan_complete = true; + program.resource_tracking_complete = true; + program.shader_info_complete = true; + program.values = std::make_shared(); + program.values->block_storage.push_back(std::make_unique()); + auto *block = program.values->block_storage.back().get(); + program.values->blocks.push_back(block); + program.values->block_info.push_back({.id = 0}); + + auto &key = block->AppendNewInst(ValueOpcode::LaneId); + auto &image = block->AppendNewInst( + ValueOpcode::GetImageResource, + {Value(&key), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), + Value(0u), Value(0u)}); + image.SetFlags(0u); + auto &sampler = block->AppendNewInst( + ValueOpcode::GetSamplerResource, + {Value(0u), Value(0u), Value(0u), Value(0u)}); + sampler.SetFlags(0u); + auto &address = block->AppendNewInst( + ValueOpcode::MakeImageAddress, + {Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), + Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), + Value(0u)}); + MemoryInfo memory{}; + memory.kind = ResourceKind::Image; + memory.resource = 0; + memory.sampler = 0; + memory.dmask = 0xf; + memory.image_sample_flags = ShaderRecompiler::Decoder::ImageSampleFlagLod; + memory.image_dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; + memory.image_address_components = 3; + program.values->memory_info.push_back(memory); + const MemoryFlags memory_flags{0u, 0x10f0u}; + uint64_t memory_flag_bits = 0; + std::memcpy(&memory_flag_bits, &memory_flags, sizeof(memory_flags)); + auto &sample = block->AppendNewInst( + ValueOpcode::ImageSampleRaw, + {Value(&image), Value(&sampler), Value(&address)}, + memory_flag_bits); + auto &sample_x = block->AppendNewInst(ValueOpcode::CompositeExtractU32x4, + {Value(&sample), Value(0u)}); + block->AppendNewInst(ValueOpcode::ReferenceU32, {Value(&sample_x)}); + + program.values->descriptor_sources.resize(2); + program.values->descriptor_sources[0].dword_count = 8; + program.values->descriptor_sources[0].indirect_image = + DescriptorSource::IndirectImage{0u, 0u, 224u, 12u, 0u}; + program.values->descriptor_sources[1].dword_count = 4; + + ImageResource root{}; + root.source = 0; + root.first_use_pc = 0x10f0u; + root.kind = ResourceKind::Image; + root.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; + root.read = true; + root.indirect_root = 0; + root.indirect_mapping_offset = 0; + root.indirect_mapping_capacity = mapping_capacity; + root.indirect_resources = {0u, 1u}; + auto candidate = root; + candidate.indirect_mapping_capacity = 0; + candidate.indirect_resources.clear(); + program.info.images = {root, candidate}; + program.info.samplers.push_back({1u, 0x10f0u}); + program.info.sampled_pairs.push_back({0u, 0u, 0x10f0u}); + + std::string error; + Require(name, "binding layout", + AllocateBindings(program, {}, &error), error.c_str()); + ResourceSnapshot snapshot{}; + DescriptorValue descriptor{}; + descriptor.dword_count = 8; + descriptor.dwords[0] = 0x20u; + descriptor.dwords[1] = + static_cast(Prospero::BufferFormat::k32_32_32_32Float) + << 20u; + descriptor.dwords[2] = 3u | (3u << 14u); + descriptor.dwords[3] = + DstSel(4, 5, 6, 7) | + (static_cast(Prospero::ImageType::kColor2D) << 28u); + snapshot.images = {descriptor, descriptor}; + snapshot.images[1].dwords[0] = 0x40u; + snapshot.flattened_srt.resize(1u + mapping_capacity * 2u); + snapshot.flattened_srt[0] = 2u; + snapshot.flattened_srt[1] = 0u; + snapshot.flattened_srt[2] = 0u; + snapshot.flattened_srt[3] = 1u; + snapshot.flattened_srt[4] = 1u; + DescriptorValue sampler_descriptor{}; + sampler_descriptor.dword_count = 4; + snapshot.samplers.push_back(sampler_descriptor); + + ShaderComputeInputInfo compute{}; + std::vector spirv; + Require(name, "SPIR-V emit", + ShaderRecompiler::Spirv::EmitProgram(program, snapshot, nullptr, + nullptr, &compute, spirv, &error), + error.c_str()); + ValidateSpirv(name, spirv); + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2); + std::string text; + Require(name, "SPIR-V disassembly", tools.Disassemble(spirv, &text), + "failed to disassemble indirect image shader"); + Require(name, "key switch", + text.find("OpSwitch") != std::string::npos && + text.find("OpPhi") != std::string::npos && + CountText(text, "OpImageSampleExplicitLod") == 2 && + CountText(text, "OpIEqual") == 11, + "dynamic image key did not use a compact two-sample switch"); +} + TestCase ImageStoreMipSelectsPpsa01340Descriptor() { using O = ShaderOpcode; @@ -19533,6 +19653,11 @@ void CheckStorageImageSwizzleSpecializationId() { ShaderRecompiler::IR::ImageMipMode::DynamicStorage; auto mip3_program = mip1_program; mip3_program.info.images[0].mip_count = 3; + auto array_program = identity_program; + array_program.info.images[0].dimension = + ShaderRecompiler::Decoder::ImageDimension::Dim2DArray; + auto cube_program = array_program; + cube_program.info.images[0].cube = true; const auto resources = std::make_shared(); @@ -19549,16 +19674,25 @@ void CheckStorageImageSwizzleSpecializationId() { auto mip3_info = identity_info; mip3_info.stage.program = std::make_shared(mip3_program); + auto array_info = identity_info; + array_info.stage.program = + std::make_shared(array_program); + auto cube_info = identity_info; + cube_info.stage.program = + std::make_shared(cube_program); const auto identity_id = ShaderGetIdCS(regs, identity_info, true); const auto rgb1_id = ShaderGetIdCS(regs, rgb1_info, true); const auto mip1_id = ShaderGetIdCS(regs, mip1_info, true); const auto mip3_id = ShaderGetIdCS(regs, mip3_info, true); + const auto array_id = ShaderGetIdCS(regs, array_info, true); + const auto cube_id = ShaderGetIdCS(regs, cube_info, true); Require("StorageImageSwizzleSpecializationId", "pipeline cache key", identity_id != rgb1_id && identity_id.ids.size() == rgb1_id.ids.size() && - mip1_id != mip3_id && mip1_id.ids.size() == mip3_id.ids.size(), - "storage swizzle or mip-count variants share a pipeline ID"); + mip1_id != mip3_id && mip1_id.ids.size() == mip3_id.ids.size() && + array_id != cube_id && array_id.ids.size() == cube_id.ids.size(), + "storage swizzle, mip-count, or cube variants share a pipeline ID"); std::printf("[host] %-32s ok\n", "StorageImageSwizzlePipelineId"); } @@ -20854,6 +20988,11 @@ int main(int argc, char **argv) { RunCase(&vulkan, ImageStoreBgraUsesInverseSwizzle()); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--indirect-image-only") == 0) { + CheckStorageImageSwizzleSpecializationId(); + CheckIndirectImageKeySwitch(); + return 0; + } if (argc == 2 && std::strcmp(argv[1], "--storage-mip-host-only") == 0) { VulkanHarness vulkan; vulkan.CheckRenderExecutorStencilBindingDiscovery(); @@ -20924,6 +21063,7 @@ int main(int argc, char **argv) { CheckEmbeddedFetchVertexOffset(); CheckEmbeddedFetchLaneSpill(); CheckRectListShaders(); + CheckIndirectImageKeySwitch(); CheckPs5GameExampleImageClearRuntimeShape(); vulkan.CheckSchedulerTimeline(); vulkan.CheckGpuMappedRangeLifecycle();