From eb8a5504a36eec7a25480f0fdcd1696b2649f7c6 Mon Sep 17 00:00:00 2001 From: nmzik Date: Wed, 23 Sep 2026 11:15:08 +0400 Subject: [PATCH] Emit coordinates for each indirect image candidate Co-authored-by: Techx3 <56268269+Techx3@users.noreply.github.com> --- .../backend/spirv/spirvEmitterImage.cpp | 21 ++-- .../ir/passes/ResourceMaterialization.cpp | 11 +- tests/ResourceTrackingTests.cpp | 34 ++++++ tests/ShaderRecompilerComputeTests.cpp | 112 +++++++++++++++++- 4 files changed, 162 insertions(+), 16 deletions(-) diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp index 52e0c56b5..e875e03ed 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp @@ -63,10 +63,10 @@ uint32_t AddressF32(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR:: : Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), value); } -ImageSampleLayout Layout(const IR::MemoryInfo& mem, ImageDimension dimension) { +ImageSampleLayout Layout(const IR::MemoryInfo& mem) { ImageSampleLayout layout; uint32_t cursor = 0; - const auto& info = ImageDimensionInfoFor(dimension); + const auto& info = ImageDimensionInfoFor(mem.image_dimension); if (HasFlag(mem, Decoder::ImageSampleFlagOffset)) layout.offset = cursor++; if (HasFlag(mem, Decoder::ImageSampleFlagBias)) layout.bias = cursor++; if (HasFlag(mem, Decoder::ImageSampleFlagCompare)) layout.dref = cursor++; @@ -104,8 +104,7 @@ uint32_t CubeLayer(EmitterState& state, uint32_t value) { } uint32_t CoordF32(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::Inst& address, - uint32_t first, uint32_t components) { - const bool cube = ctx.state.program.info.images.at(mem.resource).cube; + uint32_t first, uint32_t components, bool cube = false) { auto x = AddressF32(ctx, mem, address, first); if (components == 1u) return x; auto y = mem.image_address_components > first + 1u ? AddressF32(ctx, mem, address, first + 1u) @@ -576,7 +575,8 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { const auto lod = state.builder.AllocateId(); state.builder.AddFunction( spv::OpImageQueryLod, TypeF32Vector(state, 2), lod, sampled, - CoordF32(ctx, mem, *address, 0, ImageDimensionInfoFor(dimension).spatial_components)); + CoordF32(ctx, mem, *address, 0, ImageDimensionInfoFor(dimension).spatial_components, + image.cube)); uint32_t values[4] = {ConstantU32(state, 0), ConstantU32(state, 0), ConstantU32(state, 0), ConstantU32(state, 0)}; for (uint32_t index = 0; index < 2u; index++) { @@ -637,7 +637,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { if (op == IR::ValueOpcode::ImageSampleRaw || op == IR::ValueOpcode::ImageGatherRaw) { const auto dimension = image.dimension; const auto& dimension_info = ImageDimensionInfoFor(dimension); - const auto layout = Layout(mem, dimension); + const auto layout = Layout(mem); const auto numeric_class = image.numeric_class; const bool dref = HasFlag(mem, Decoder::ImageSampleFlagCompare); if (dref && state.program.info.images[mem.resource].conversion_format != @@ -645,9 +645,9 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { ctx.Fail(inst, "uses depth comparison with a packed integer image"); return; } - const auto coord = - CoordF32(ctx, mem, *address, layout.coord, dimension_info.coordinate_components); if (op == IR::ValueOpcode::ImageGatherRaw) { + const auto coord = CoordF32(ctx, mem, *address, layout.coord, + dimension_info.coordinate_components, image.cube); if (HasFlag(mem, Decoder::ImageSampleFlagLod)) { static std::atomic_flag warned = ATOMIC_FLAG_INIT; if (!warned.test_and_set(std::memory_order_relaxed)) { @@ -752,6 +752,11 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { operands.push_back(AddressF32(ctx, mem, *address, layout.bias)); } const auto EmitSample = [&](uint32_t resource) { + const auto& candidate = state.program.info.images[resource]; + const auto coord = + CoordF32(ctx, mem, *address, layout.coord, + ImageDimensionInfoFor(candidate.dimension).coordinate_components, + candidate.cube); const auto sampled = MakeSampledImage(state, resource, mem.sampler); const auto sample = state.builder.AllocateId(); std::vector sample_operands {result_type, sample, sampled, coord}; diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index 10867632e..480ab6b15 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -453,6 +453,10 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna return SpecializationFail("indirect image specialization has no typed candidate"); } const auto& image_class = specialization.images[exemplar]; + const auto is_2d = [](Decoder::ImageDimension dimension) { + return dimension == Decoder::ImageDimension::Dim2D || + dimension == Decoder::ImageDimension::Dim2DArray; + }; for (uint32_t candidate = 0; candidate < specialization.images.size(); candidate++) { auto& image = specialization.images[candidate]; if (image.indirect_root != root_index) { @@ -466,12 +470,13 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna image.shader_swizzle = image_class.shader_swizzle; image.cube = image_class.cube; } + const bool same_coordinates = image.dimension == image_class.dimension && + image.cube == image_class.cube; if (image.numeric_class != image_class.numeric_class || - image.dimension != image_class.dimension || + (!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension))) || image.mip_count != image_class.mip_count || image.conversion_format != image_class.conversion_format || - image.shader_swizzle != image_class.shader_swizzle || - image.cube != image_class.cube) { + image.shader_swizzle != image_class.shader_swizzle) { return SpecializationFail( fmt::format("indirect image table at pc 0x{:08x} has incompatible candidates", program.info.images[root_index].first_use_pc)); diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index a1a450cb2..ce2f5b44d 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -352,6 +352,40 @@ void TestInvariantIndirectImageMaterialization() { dynamic_snapshot.images.size() == 2 && dynamic_specialization.images.size() == 2, "dynamic indirect image table did not materialize"); + const auto second_image = (0x2020u - memory.base) / 4u; + memory.words[second_image + 1u] |= 3u << 30u; + memory.words[second_image + 2u] = 3u << 14u; + memory.words[second_image + 3u] = + Libs::Graphics::DstSel(4, 5, 6, 7) | + (static_cast(Libs::Graphics::Prospero::ImageType::kCube) << 28u); + memory.words[second_image + 4u] = 11u; + ResourceSnapshot mixed_snapshot; + ResourceSpecialization mixed_specialization; + Check(MaterializeResources(resource_plan, runtime, mixed_snapshot, + mixed_specialization) && + mixed_snapshot.images.size() == 2 && + mixed_specialization.images.size() == 2 && + mixed_specialization.images[0].dimension == + Decoder::ImageDimension::Dim2D && + !mixed_specialization.images[0].cube && + mixed_specialization.images[1].dimension == + Decoder::ImageDimension::Dim2DArray && + mixed_specialization.images[1].cube && + std::equal(mixed_snapshot.images[1].dwords.begin(), + mixed_snapshot.images[1].dwords.end(), + memory.words.begin() + second_image), + "mixed 2D and cube candidates were rejected or discarded"); + memory.words[second_image + 1u] = + static_cast( + Libs::Graphics::Prospero::BufferFormat::k32_32_32_32UInt) + << 20u | + (3u << 30u); + Check(!MaterializeResources(resource_plan, runtime, mixed_snapshot, + mixed_specialization), + "indirect images with different numeric classes were accepted"); + for (const auto dword : {1u, 2u, 3u, 4u}) { + memory.words[second_image + dword] = image_descriptor[dword]; + } ApplyResourceSpecialization(fixture->program, dynamic_specialization); Check(fixture->program.info.images.size() == 2 && fixture->program.info.images[0].indirect_root == 0 && diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 04e5d140d..79ca42eda 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -26174,6 +26174,48 @@ TestCase ImageSampleAndGather() { return test; } +TestCase ImageCubeGradientsPreserveDerivatives() { + using O = ShaderOpcode; + TestCase test; + test.name = "ImageCubeGradientsPreserveDerivatives"; + for (u32 component = 0; component < 7u; component++) { + constexpr std::array values{0.125f, 0.0f, 0.0f, 0.125f, + 1.375f, 1.625f, 10.0f}; + AppendVMovLiteral(&test.code, 20u + component, + std::bit_cast(values[component])); + } + test.code.push_back(EncodeMimg0(0x22, 0x1, 0, false, 3)); + test.code.push_back(EncodeMimg1(0, 20)); + AppendStoreVgpr(&test.code, 0, 0); + AppendEnd(&test.code); + test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD, + O::S_ENDPGM}; + test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k32_32_32_32Float); + test.user_data[1] |= 3u << 30u; + test.user_data[2] = 3u << 14u; + test.user_data[3] = + (static_cast(Prospero::ImageType::kCube) << 28u) | (2u << 16u); + test.user_data[4] = 11u; + test.user_data[5] = 2u << 4u; + test.user_data[50] = sizeof(u32); + test.has_user_data = true; + test.sampled_image_view_type = vk::ImageViewType::e2DArray; + test.sampled_image_layers = 12; + for (u32 mip = 0; mip < 3u; mip++) { + const auto layer_dwords = (4u >> mip) * (4u >> mip) * 4u; + auto &pixels = test.sampled_image_rgba_mips.emplace_back(layer_dwords * 12u); + for (u32 layer = 0; layer < 12u; layer++) { + std::fill_n(pixels.begin() + layer * layer_dwords, layer_dwords, + std::bit_cast(static_cast(1u + layer + 100u * mip))); + } + } + // Guest face 10 is host layer 8. Unmodified gradients select mip 0; + // applying the cube coordinate bias to gradients instead selects mip 2. + test.expected = {std::bit_cast(9.0f)}; + test.required_spirv = {"OpImageSampleExplicitLod", "Grad"}; + return test; +} + TestCase ImageGatherLodApproximatesLevelZero() { using O = ShaderOpcode; std::vector code; @@ -26549,6 +26591,63 @@ void CheckIndirectImageKeySwitch() { CountText(text, "OpImageSampleExplicitLod") == 2 && CountText(text, "OpIEqual") == 11, "dynamic image key did not use a compact two-sample switch"); + + program.memory_info[0].image_dimension = + ShaderRecompiler::Decoder::ImageDimension::Dim2DArray; + program.memory_info[0].image_address_components = 4; + for (u32 component = 0; component < 4u; component++) { + constexpr std::array values{1.375f, 1.625f, 10.0f, 2.0f}; + address.SetArg(component, Value(std::bit_cast(values[component]))); + } + root.indirect_resources = {0u, 1u, 2u}; + program.info.images = {root, candidate, candidate}; + for (const bool cube_first : {true, false}) { + for (u32 resource = 0; resource < 3u; resource++) { + auto &image_resource = program.info.images[resource]; + image_resource.cube = resource == (cube_first ? 0u : 1u); + image_resource.dimension = + resource == (cube_first ? 1u : 0u) + ? ShaderRecompiler::Decoder::ImageDimension::Dim2D + : ShaderRecompiler::Decoder::ImageDimension::Dim2DArray; + } + program.binding_layout_complete = false; + AllocateBindings(program); + spirv = ShaderRecompiler::Spirv::EmitProgram(program, {.compute = &compute}); + ValidateSpirv(name, spirv); + Require(name, "mixed SPIR-V disassembly", tools.Disassemble(spirv, &text), + "failed to disassemble mixed indirect image shader"); + Require(name, "mixed key switch", + CountText(text, "OpImageSampleExplicitLod") == 3 && + CountText(text, "OpFSub") == 2, + "mixed candidates lost a sample or shared cube coordinate conversion"); + + std::vector> definitions(spirv[3]); + u32 samples = 0; + for (size_t offset = 5; offset < spirv.size();) { + const auto words = std::span(spirv).subspan(offset, spirv[offset] >> 16u); + const auto opcode = static_cast(words[0] & 0xffffu); + if (opcode == spv::OpTypeVector) { + definitions[words[1]] = words; + } else if (opcode == spv::OpCompositeConstruct || opcode == spv::OpBitcast || + opcode == spv::OpConstant) { + definitions[words[2]] = words; + } else if (opcode == spv::OpImageSampleExplicitLod) { + const auto coord = definitions[words[4]]; + const auto lod = definitions[words[6]]; + const auto components = samples == (cube_first ? 1u : 0u) ? 2u : 3u; + Require(name, "mixed coordinate and LOD layout", + !coord.empty() && definitions[coord[1]][3] == components && + words[5] == spv::ImageOperandsLodMask && lod.size() == 4u && + definitions[lod[3]].size() == 4u && + definitions[lod[3]][3] == std::bit_cast(2.0f), + "candidate coordinates changed the instruction's LOD operand"); + samples++; + } + offset += words.size(); + } + Require(name, "mixed sample count", samples == 3u, + "mixed candidate switch did not retain every image"); + } } TestCase ImageStoreMipSelectsPpsa01340Descriptor() { @@ -27795,6 +27894,7 @@ std::vector MakeCases() { AddCase(ImageGetResinfoDmaskWidthHeight); AddCase(ImageGetResinfoDmaskMipLevels); AddCase(ImageSampleAndGather); + AddCase(ImageCubeGradientsPreserveDerivatives); AddCase(ImageGatherLodApproximatesLevelZero); AddCase(ImageD16GatherPacksHalfPairs); AddCase(ImageSampleA16SamplerCoordsOnGpu); @@ -32727,6 +32827,13 @@ int main(int argc, char **argv) { RunCase(nullptr, ImageSampleA16CompareBiasRdna2AddressOrder()); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--indirect-image-only") == 0) { + CheckImageSamplerSpecialization(); + CheckIndirectImageKeySwitch(); + VulkanHarness vulkan; + RunCase(&vulkan, ImageCubeGradientsPreserveDerivatives()); + return 0; + } #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS if (argc == 2 && std::strcmp(argv[1], "--reverse-rt-death") == 0) { RunReverseRenderTargetDeathCase(); @@ -32762,11 +32869,6 @@ int main(int argc, char **argv) { RunCase(&vulkan, ImageStoreBgraUsesInverseSwizzle()); return 0; } - if (argc == 2 && std::strcmp(argv[1], "--indirect-image-only") == 0) { - CheckImageSamplerSpecialization(); - CheckIndirectImageKeySwitch(); - return 0; - } if (argc == 2 && std::strcmp(argv[1], "--storage-mip-host-only") == 0) { VulkanHarness vulkan; vulkan.CheckRenderExecutorStencilBindingDiscovery();