From 4c87dbebb2cd58d402b1eda55c051b4e5fea3aba Mon Sep 17 00:00:00 2001 From: netwarex Date: Sun, 11 Oct 2026 17:55:37 +0200 Subject: [PATCH] shader: select indirect texture descriptors for image gathers --- .../backend/spirv/spirvEmitterImage.cpp | 62 +++++++++------- .../ir/passes/ResourceMaterialization.cpp | 1 + tests/ShaderRecompilerComputeTests.cpp | 71 +++++++++++++++---- 3 files changed, 92 insertions(+), 42 deletions(-) diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp index ceeb4569..419c6600 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp @@ -800,15 +800,16 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { return; } if (op == IR::ValueOpcode::ImageGatherRaw) { - const auto coord = CoordF32(ctx, mem, *address, layout.coord, - dimension_info.coordinate_components, image.cube); if (dimension == ImageDimension::Dim1D) { - if (dref || !HasFlag(mem, Decoder::ImageSampleFlagLevelZero) || + if (image.indirect_root == mem.resource || dref || + !HasFlag(mem, Decoder::ImageSampleFlagLevelZero) || HasFlag(mem, Decoder::ImageSampleFlagOffset) || HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) { ctx.Fail(inst, "has an unsupported 1D gather variant"); return; } + const auto coord = CoordF32(ctx, mem, *address, layout.coord, + dimension_info.coordinate_components, image.cube); const auto sample = EmitOneDimensionalGatherLz(ctx, mem, coord, numeric_class); ctx.Define(inst, ResultVector(ctx, UnpackImageGather(ctx, mem, sample), numeric_class, false, mem, true)); @@ -829,32 +830,39 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { } else { component_or_dref = ConstantU32(state, ImageGatherSource(state, mem)); } - uint32_t operand_mask = 0; - uint32_t offset = 0; - if (HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) { - operand_mask = spv::ImageOperandsConstOffsetsMask; - offset = HorizontalOffsets(state, dimension); - } else if (layout.offset != NoImageComponent) { - operand_mask = spv::ImageOperandsOffsetMask; - offset = PackedOffset(ctx, mem, *address, layout, dimension); - } const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler); - const auto EmitGather = [&](uint32_t mip) { - const auto sampled = MakeSampledImage(state, mem.resource, sampler_id, mip); - const auto sample = state.builder.AllocateId(); - const std::array operands {operand_mask, offset}; - state.builder.AddFunction( - dref ? spv::OpImageDrefGather : spv::OpImageGather, result_type, - sample, sampled, coord, component_or_dref, - std::span(operands).first(operand_mask != 0u ? 2u : 0u)); - return sample; + const auto EmitGather = [&](uint32_t resource, uint32_t array_index) { + const auto& candidate = state.program.info.images[resource]; + const auto dimension = candidate.dimension; + const auto coord = CoordF32( + ctx, mem, *address, layout.coord, + ImageDimensionInfoFor(dimension).coordinate_components, candidate.cube); + uint32_t operand_mask = 0; + uint32_t offset = 0; + if (HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) { + operand_mask = spv::ImageOperandsConstOffsetsMask; + offset = HorizontalOffsets(state, dimension); + } else if (layout.offset != NoImageComponent) { + operand_mask = spv::ImageOperandsOffsetMask; + offset = PackedOffset(ctx, mem, *address, layout, dimension); + } + const auto EmitMip = [&](uint32_t mip) { + const auto sampled = MakeSampledImage(state, resource, sampler_id, mip, array_index); + const auto sample = state.builder.AllocateId(); + const std::array operands {operand_mask, offset}; + state.builder.AddFunction( + dref ? spv::OpImageDrefGather : spv::OpImageGather, result_type, + sample, sampled, coord, component_or_dref, + std::span(operands).first(operand_mask != 0u ? 2u : 0u)); + return sample; + }; + return candidate.mip_mode == IR::ImageMipMode::Dynamic && candidate.mip_count > 1u + ? EmitIndexSwitch( + state, GatherMip(ctx, inst, mem, *address, layout, candidate.mip_count), + candidate.mip_count, result_type, EmitMip) + : EmitMip(0u); }; - const auto sample = image.mip_mode == IR::ImageMipMode::Dynamic && image.mip_count > 1u - ? EmitIndexSwitch( - state, GatherMip(ctx, inst, mem, *address, layout, - image.mip_count), - image.mip_count, result_type, EmitGather) - : EmitGather(0); + const auto sample = EmitImageAccess(ctx, inst, result_type, EmitGather); ctx.Define(inst, ResultVector(ctx, UnpackImageGather(ctx, mem, sample), result_numeric_class, false, mem, true)); return; diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index bd170a0f..66ba146a 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -1406,6 +1406,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization& } EXIT_IF(image.indirect_root == memory.resource && inst.GetOpcode() != ValueOpcode::ImageSampleRaw && + inst.GetOpcode() != ValueOpcode::ImageGatherRaw && inst.GetOpcode() != ValueOpcode::ImageRead); } } diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index f5d60f00..54f07631 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -35749,29 +35749,63 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) { resource == 4u ? vk::ImageViewType::e2DArray : vk::ImageViewType::e2D, layers)); } const auto native_sampler = vulkan.CreateSampler(test.name); - for (const bool fetch : {false, true}) { + enum class Access { Sample, Gather, GatherHomogeneous, Fetch }; + auto *access = &sample; + for (const auto mode : {Access::Sample, Access::Gather, + Access::GatherHomogeneous, Access::Fetch}) { + const bool fetch = mode == Access::Fetch; + const bool gather = mode == Access::Gather || mode == Access::GatherHomogeneous; + const bool homogeneous = mode == Access::GatherHomogeneous; + if (mode == Access::Gather) { + test.name = "IndirectImageGatherContiguousRuns"; + const auto position = std::ranges::find_if(*block, [&](const Inst &inst) { + return &inst == access; + }); + auto &read = *block->PrependNewInst( + position, ValueOpcode::ImageGatherRaw, + {Value(&image), Value(&sampler), Value(&address), Value(0u), Value(0u), + Value(0u), Value(0u)}, memory_flag_bits); + access->ReplaceUsesWith(Value(&read), false); + access = &read; + compiled.program.info.images[4].cube = false; + auto &gather_memory = compiled.program.memory_info[0]; + gather_memory.dmask = 1; + gather_memory.image_sample_flags = ShaderRecompiler::Decoder::ImageSampleFlagLevelZero | + ShaderRecompiler::Decoder::ImageSampleFlagOffset; + // GATHER4_LZ_O places its packed signed offset before the coordinates. + address.SetArg(0, Value(0x3fu)); + address.SetArg(1, Value(std::bit_cast(0.75f))); + address.SetArg(2, Value(std::bit_cast(0.25f))); + address.SetArg(3, Value(0u)); + } + auto &selected_resources = compiled.program.info.images[0].indirect_resources; + selected_resources = homogeneous ? std::vector{0u, 2u, 3u, 5u, 6u} + : root.indirect_resources; + if (homogeneous) test.name = "IndirectImageGatherHomogeneous"; + compiled.resources.flattened_srt[6] = homogeneous ? 4u : ordinals[5]; if (fetch) { test.name = "IndirectImageFetchContiguousRuns"; const auto position = std::ranges::find_if(*block, [&](const Inst &inst) { - return &inst == &sample; + return &inst == access; }); auto &read = *block->PrependNewInst( position, ValueOpcode::ImageRead, {Value(&image), Value(&address), Value(true)}, memory_flag_bits); - sample.ReplaceUsesWith(Value(&read), false); + access->ReplaceUsesWith(Value(&read), false); sampler.Invalidate(); compiled.program.info.samplers.clear(); compiled.program.info.sampled_pairs.clear(); compiled.program.info.images[4].cube = false; + compiled.program.memory_info[0].dmask = 0xf; compiled.program.memory_info[0].image_sample_flags = 0; compiled.program.memory_info[0].image_has_mip = true; for (u32 component = 0; component < 4u; ++component) address.SetArg(component, Value(0u)); // Native DIM=2DArray places LOD after the layer, even for a 2D candidate. address.SetArg(3, Value(1u)); - compiled.program.shader_info_complete = false; - CollectShaderInfo(compiled.program, {.compute = &compute}); } + compiled.program.shader_info_complete = false; + CollectShaderInfo(compiled.program, {.compute = &compute}); for (const u32 wave_size : {32u, 64u}) { compute.wave_size = wave_size; compute.host_subgroup_size = vulkan.SubgroupSize(); @@ -35782,31 +35816,38 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) { compiled.packed_user_data.resize(compiled.program.bindings.ShaderDataDwords()); ValidateSpirv(test.name, compiled.spirv); const auto halves = wave_size > compute.host_subgroup_size ? 2u : 1u; + const auto image_opcode = fetch ? "OpImageFetch" : gather ? "OpImageGather" + : "OpImageSampleExplicitLod"; Require(test.name, "native array image runs", tools.Disassemble(compiled.spirv, &text) && - CountText(text, fetch ? "OpImageFetch" : "OpImageSampleExplicitLod") == 3u * halves, + CountText(text, image_opcode) == (homogeneous ? 1u : 3u) * halves, "mixed runs expanded into per-image accesses"); - if (fetch) { + if (homogeneous) { + Require(test.name, "homogeneous gather array", text.find("OpSwitch") == std::string::npos, + "homogeneous gather emitted a descriptor switch"); + } + if (fetch || gather) { std::vector decorated; - std::vector fetched_images; + std::vector image_operands; 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::OpDecorate && words[2] == spv::DecorationNonUniform) decorated.push_back(words[1]); - if (opcode == spv::OpImageFetch) fetched_images.push_back(words[3]); + if (opcode == (fetch ? spv::OpImageFetch : spv::OpImageGather)) + image_operands.push_back(words[3]); offset += words.size(); } - Require(test.name, "nonuniform fetch operands", - std::ranges::count_if(fetched_images, [&](u32 operand) { + Require(test.name, "nonuniform image operands", + std::ranges::count_if(image_operands, [&](u32 operand) { return std::ranges::find(decorated, operand) != decorated.end(); - }) == 2u * halves, - "a dynamically selected fetch image lacks its nonuniform decoration"); + }) == (homogeneous ? 1u : 2u) * halves, + "a dynamically selected image operand lacks its nonuniform decoration"); } test.initial.assign(wave_size, 0xdeadbeefu); test.expected.assign(wave_size, 0u); for (u32 lane = 0; lane < ordinals.size(); ++lane) { - const auto resource = root.indirect_resources[ordinals[lane]]; + const auto resource = selected_resources[compiled.resources.flattened_srt[1u + lane]]; test.expected[lane] = std::bit_cast(float(resource + (fetch && resource != 0u ? 10u : 0u))); } auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size()); @@ -35814,7 +35855,7 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) { fetch ? vk::Sampler{} : native_sampler, textures); const auto actual = vulkan.ReadBuffer(test.name, buffer, test.expected.size()); vulkan.DestroyBuffer(&buffer); - CompareWords(test, "root, native slots, cube and mapping bounds", test.expected, actual); + CompareWords(test, "root, native slots, array runs and mapping bounds", test.expected, actual); } std::printf("[compute] %-32s ok\n", test.name); }