diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp index c273ae8f1..73dc86b3a 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp @@ -813,6 +813,25 @@ uint32_t EmitImageResult(ValueEmitContext& ctx, const IR::Inst& inst, const Emit return mixed ? value : result_bits(mem.resource, value); } +// Ordinals outside the table select candidates[0]. Past ordinal 0, a consecutive table is an +// offset of ordinal - 1, which wraps at 0 so that one unsigned compare checks both ends. +uint32_t SamplerArrayIndex(EmitterState& state, uint32_t ordinal, + const std::vector& candidates) { + const auto count = static_cast(candidates.size()); + const auto gap = std::adjacent_find(candidates.begin() + 1, candidates.end(), + [](uint32_t lhs, uint32_t rhs) { return rhs != lhs + 1u; }); + if (count == 1u || gap != candidates.end()) { + return EmitIndexSwitch(state, ordinal, count, TypeU32(state), + [&](uint32_t item) { return ConstantU32(state, candidates[item]); }); + } + const auto offset = Binary(state, spv::OpISub, TypeU32(state), ordinal, ConstantU32(state, 1u)); + const auto in_table = Binary(state, spv::OpULessThan, TypeBool(state), offset, + ConstantU32(state, count - 1u)); + const auto index = Binary(state, spv::OpIAdd, TypeU32(state), offset, + ConstantU32(state, candidates[1])); + return Select(state, TypeU32(state), in_table, index, ConstantU32(state, candidates[0])); +} + struct ImageSampler { uint32_t id = 0; bool dynamic = false; @@ -848,9 +867,7 @@ std::array LoadImageSamplers(ValueEmitContext& ctx, const IR::I selected = EmitIndirectResourceIndex(state, selected, samplers[index].indirect_mapping_offset, samplers[index].indirect_search_iterations, 0u); - array_index = EmitIndexSwitch( - state, selected, static_cast(candidates.size()), - TypeU32(state), [&](uint32_t ordinal) { return ConstantU32(state, candidates[ordinal]); }); + array_index = SamplerArrayIndex(state, selected, candidates); } sampler = {LoadSamplerDescriptor(state, index, array_index), array_index != 0u}; }; diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index bfd4bad7a..a151a035a 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -43833,6 +43833,123 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) { } } +void CheckSamplerCandidateTables(VulkanHarness &vulkan) { + constexpr const char *name = "SamplerCandidateTables"; + constexpr u32 wave_size = 32u, sampler_count = 4u; + using namespace ShaderRecompiler::IR; + + CompiledShader compiled; + auto &program = compiled.program; + program.stage = ShaderType::Compute; + program.wave_size = wave_size; + program.srt_plan_complete = true; + program.resource_tracking_complete = true; + program.block_storage.push_back(std::make_unique()); + auto *block = program.block_storage.back().get(); + program.blocks.push_back(block); + + // Lane L selects ordinal L - 1, so lane 0 selects 0xffffffff. + auto &lane = block->AppendNewInst(ValueOpcode::LaneId); + auto &ordinal = block->AppendNewInst(ValueOpcode::ISub32, {Value(&lane), Value(1u)}); + auto &image = block->AppendNewInst( + ValueOpcode::GetImageResource, + {Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u)}); + auto &sampler = block->AppendNewInst(ValueOpcode::GetSamplerResource, + {Value(&ordinal), Value(0u), Value(0u), Value(0u)}); + // LOD 16 clamps to each sampler's maxLod, so sampler N reads mip N. + auto &address = block->AppendNewInst( + ValueOpcode::MakeImageAddress, + {Value(0u), Value(0u), Value(std::bit_cast(16.0f)), 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.dmask = 0xf; + memory.image_sample_flags = ShaderRecompiler::Decoder::ImageSampleFlagLod; + memory.image_dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; + memory.image_address_components = 3; + program.memory_info.push_back(memory); + auto &sample = block->AppendNewInst( + ValueOpcode::ImageSampleRaw, {Value(&image), Value(&sampler), Value(&address), Value(true)}); + sample.SetFlags(MemoryFlags{0u, 0x10f0u}); + auto &sample_x = + block->AppendNewInst(ValueOpcode::CompositeExtractU32x4, {Value(&sample), Value(0u)}); + auto &byte_offset = block->AppendNewInst(ValueOpcode::IMul32, {Value(&lane), Value(4u)}); + auto &output = block->AppendNewInst(ValueOpcode::GetBufferResource, + {Value(0u), Value(0u), Value(0u), Value(0u)}); + program.memory_info.push_back({.kind = ResourceKind::Buffer, .offen = true}); + auto &store = block->AppendNewInst( + ValueOpcode::StoreBufferU32, + {Value(&output), Value(0u), Value(&byte_offset), Value(0u), Value(&sample_x), Value(true)}); + store.SetFlags(MemoryFlags{1u, 0u}); + + program.descriptor_sources.resize(2); + program.descriptor_sources[0].dword_count = 8; + program.descriptor_sources[1].dword_count = 4; + ImageResource texture{}; + texture.first_use_pc = 0x10f0u; + texture.resource_class = ImageResourceClass::Sampled; + texture.numeric_class = Prospero::TextureNumericClass::Float; + texture.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; + texture.read = true; + program.info.images = {texture}; + program.info.samplers.assign(sampler_count, + SamplerResource{.source = 1u, .first_use_pc = 0x10f0u}); + program.info.sampled_pairs.push_back({0u, 0u, 0x10f0u}); + program.info.buffers.push_back({.packed_stride = 1, .written = true}); + + std::vector> mips; + std::vector native_samplers; + for (u32 mip = 0; mip < sampler_count; ++mip) { + const auto size = 32u >> mip; + mips.emplace_back(size * size, std::bit_cast(float(10u + mip))); + native_samplers.push_back(vulkan.CreateSampler(name, mip + 1u)); + } + std::array textures{vulkan.CreateImageMips( + name, 32, 32, vk::Format::eR32Sfloat, vk::ImageUsageFlagBits::eSampled, mips, 1, + vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageType::e2D, vk::ImageViewType::e2D, 1)}; + ShaderComputeInputInfo compute{}; + compute.wave_size = wave_size; + compute.host_subgroup_size = vulkan.SubgroupSize(); + compute.threads_num[0] = wave_size; + compute.threads_num[1] = compute.threads_num[2] = 1u; + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2); + TestCase test; + test.name = name; + // A table consecutive from ordinal 1 selects without a switch; a single candidate and a table + // with a gap keep it. + const std::array, 3> tables{{{3u, 0u, 1u}, {2u}, {0u, 2u, 1u}}}; + for (const auto &table : tables) { + program.info.samplers[0].indirect_resources = table; + program.shader_info_complete = false; + CollectShaderInfo(program, {.compute = &compute}); + compiled.spirv = ShaderRecompiler::Spirv::EmitProgram(program, {.compute = &compute}); + compiled.packed_user_data.resize(program.bindings.ShaderDataDwords()); + ValidateSpirv(name, compiled.spirv); + std::string text; + Require(name, "sampler table disassembly", tools.Disassemble(compiled.spirv, &text), + "failed to disassemble the sampler table shader"); + Require(name, "sampler table selection", + CountText(text, "OpSwitch") == (&table == &tables.front() ? 0u : 1u), + "only a consecutive sampler table selects without a switch"); + test.initial.assign(wave_size, 0xdeadbeefu); + test.expected.resize(wave_size); + for (u32 lane_index = 0; lane_index < wave_size; ++lane_index) { + const auto selected = lane_index - 1u; + test.expected[lane_index] = + std::bit_cast(float(10u + table[selected < table.size() ? selected : 0u])); + } + auto buffer = vulkan.CreateStorageBuffer(name, test.initial, test.initial.size()); + vulkan.Dispatch(test, compiled, buffer, nullptr, nullptr, nullptr, nullptr, nullptr, textures, + native_samplers); + CompareWords(test, "selected sampler per ordinal", test.expected, + vulkan.ReadBuffer(name, buffer, test.expected.size())); + vulkan.DestroyBuffer(&buffer); + } + std::printf("[compute] %-32s ok\n", name); + for (const auto native_sampler : native_samplers) vulkan.Device().destroySampler(native_sampler); + vulkan.DestroyImage(&textures[0]); +} + TestCase ImageStoreMipSelectsPpsa01340Descriptor() { using O = ShaderOpcode; @@ -53210,6 +53327,7 @@ int main(int argc, char **argv) { vulkan.CheckResidentImageHeap(); CheckFiniteFirstLaneImageChoice(vulkan); CheckFiniteInlineSamplerPhi(vulkan); + CheckSamplerCandidateTables(vulkan); RunCase(&vulkan, ImageCubeGradientsPreserveDerivatives()); return 0; } @@ -53398,6 +53516,7 @@ int main(int argc, char **argv) { CheckDynamicSampleOffsets(vulkan, 64u); CheckFiniteFirstLaneImageChoice(vulkan); CheckFiniteInlineSamplerPhi(vulkan); + CheckSamplerCandidateTables(vulkan); CheckWave64WholeWaveResults(); CheckPs5GameExampleImageClearRuntimeShape(); vulkan.CheckSchedulerTimeline();