From cf268af83922d28568c53ef960fe3f3558117048 Mon Sep 17 00:00:00 2001 From: nmzik Date: Sat, 26 Sep 2026 14:04:22 +0400 Subject: [PATCH] Shader: implement RDNA2 image atomic float maximum Shader: implement RDNA2 image atomic float minimum Co-authored-by: foufouadi Co-authored-by: frans Co-authored-by: fxpw Co-authored-by: Matthew Hurley <171001439+theantipopau@users.noreply.github.com> --- .../renderer/pipeline/descriptors.cpp | 7 +- .../backend/spirv/spirvEmitterImage.cpp | 11 +- .../backend/spirv/spirvEmitterInstructions.h | 2 + .../recompiler/frontend/decode/ImageOps.cpp | 2 + .../frontend/decode/ShaderDecoder.cpp | 2 + .../frontend/decode/ShaderDecoder.h | 2 + .../recompiler/frontend/translate/Memory.cpp | 6 +- .../recompiler/ir/opcodes/ValueOpcodes.cpp | 2 + .../recompiler/ir/opcodes/ValueOpcodes.inc | 2 + .../ir/passes/ResourceMaterialization.cpp | 5 +- tests/ShaderRecompilerComputeTests.cpp | 126 ++++++++++++++++++ 11 files changed, 161 insertions(+), 6 deletions(-) diff --git a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp index 8a8875bb0..34310409a 100644 --- a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp @@ -335,8 +335,10 @@ void ValidateStorageTexture(const ShaderRecompiler::IR::ImageResource& resource, const bool raw_sint_storage = format == Prospero::BufferFormat::k32SInt && uint_resource && resource.written && !resource.read && !resource.atomic; const auto numeric_class = Prospero::SampledTextureNumericClass(format); + const bool raw_float_atomic = format == Prospero::BufferFormat::k32Float && uint_resource && + resource.atomic; const bool format_ok = - raw_sint_storage || + raw_sint_storage || raw_float_atomic || (numeric_class != Prospero::TextureNumericClass::Unsupported && numeric_class != Prospero::TextureNumericClass::Sint && uint_resource == (numeric_class == Prospero::TextureNumericClass::Uint) && @@ -640,7 +642,8 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR pixel_format = depth_format->depth_attachment_format; } } - const auto storage_view_format = storage && format == Prospero::BufferFormat::k32SInt + const auto storage_view_format = storage && (resource.atomic || + format == Prospero::BufferFormat::k32SInt) ? vk::Format::eR32Uint : SrgbStorageViewFormat(pixel_format); const auto view_format = storage && storage_view_format != vk::Format::eUndefined diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp index 016e5066a..02ac7e8fd 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp @@ -877,7 +877,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { return; } const auto atomic_opcode = ImageAtomicOpcode(op); - if (atomic_opcode != spv::OpNop) { + if (image_info.access == IR::ImageAccess::Atomic) { const auto dimension = image.dimension; ctx.Define(inst, EmitValueOrZeroIfCondition(state, ctx.Arg(inst, 3), [&]() { const auto pointer = state.builder.AllocateId(); @@ -887,6 +887,15 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { StorageImageDescriptorPointer(state, mem.resource), CoordU32(ctx, mem, *address, dimension), ConstantU32(state, 0)); + if (op == IR::ValueOpcode::ImageAtomicFMin32 || + op == IR::ValueOpcode::ImageAtomicFMax32) { + return AtomicUpdate(state, pointer, IR::ResourceKind::Image, + [&](uint32_t old) { + return EmitFloatAtomicReplacement( + state, old, ctx.Arg(inst, 2), + op == IR::ValueOpcode::ImageAtomicFMax32); + }); + } const auto old = state.builder.AllocateId(); state.builder.AddFunction(atomic_opcode, TypeU32(state), old, pointer, ConstantU32(state, spv::ScopeDevice), diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h index 805074430..e87236f51 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h @@ -271,6 +271,8 @@ inline constexpr auto EmitImageAtomicUMax32 = EmitImage; inline constexpr auto EmitImageAtomicAnd32 = EmitImage; inline constexpr auto EmitImageAtomicOr32 = EmitImage; inline constexpr auto EmitImageAtomicXor32 = EmitImage; +inline constexpr auto EmitImageAtomicFMin32 = EmitImage; +inline constexpr auto EmitImageAtomicFMax32 = EmitImage; void EmitUnreachable(ValueEmitContext& ctx, const IR::Inst& inst); inline constexpr auto EmitPhi = EmitUnreachable; inline constexpr auto EmitTessellationBase = EmitUnreachable; diff --git a/src/graphics/shader/recompiler/frontend/decode/ImageOps.cpp b/src/graphics/shader/recompiler/frontend/decode/ImageOps.cpp index 831c677d1..b42248fc4 100644 --- a/src/graphics/shader/recompiler/frontend/decode/ImageOps.cpp +++ b/src/graphics/shader/recompiler/frontend/decode/ImageOps.cpp @@ -198,6 +198,8 @@ constexpr Detail::OpcodeMap MIMG_ATOMIC_OPCODE_LIST[] = { {0x18u, Opcode::IMAGE_ATOMIC_AND}, {0x19u, Opcode::IMAGE_ATOMIC_OR}, {0x1au, Opcode::IMAGE_ATOMIC_XOR}, + {0x1eu, Opcode::IMAGE_ATOMIC_FMIN}, + {0x1fu, Opcode::IMAGE_ATOMIC_FMAX}, }; constexpr auto MIMG_SAMPLE_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_SAMPLE_OPCODE_LIST); diff --git a/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.cpp b/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.cpp index 493ddf068..36b53e443 100644 --- a/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.cpp +++ b/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.cpp @@ -579,6 +579,8 @@ std::string InstructionToString(const Instruction& inst) { case Opcode::IMAGE_ATOMIC_AND: case Opcode::IMAGE_ATOMIC_OR: case Opcode::IMAGE_ATOMIC_XOR: + case Opcode::IMAGE_ATOMIC_FMIN: + case Opcode::IMAGE_ATOMIC_FMAX: case Opcode::IMAGE_LOAD: case Opcode::IMAGE_LOAD_MIP: case Opcode::IMAGE_GET_RESINFO: diff --git a/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.h b/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.h index 9410989c9..79e9e7e4e 100644 --- a/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.h +++ b/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.h @@ -571,6 +571,8 @@ enum class Opcode { IMAGE_ATOMIC_AND, IMAGE_ATOMIC_OR, IMAGE_ATOMIC_XOR, + IMAGE_ATOMIC_FMIN, + IMAGE_ATOMIC_FMAX, IMAGE_SAMPLE, IMAGE_GATHER4_L, IMAGE_GATHER4_LZ, diff --git a/src/graphics/shader/recompiler/frontend/translate/Memory.cpp b/src/graphics/shader/recompiler/frontend/translate/Memory.cpp index 3ba3801ed..95e12c0c0 100644 --- a/src/graphics/shader/recompiler/frontend/translate/Memory.cpp +++ b/src/graphics/shader/recompiler/frontend/translate/Memory.cpp @@ -223,7 +223,7 @@ Decoder::Operand MemorySourceAt(const Decoder::Instruction& decoded, uint32_t in const bool store_or_atomic = decoded.opcode == Decoder::Opcode::IMAGE_STORE || decoded.opcode == Decoder::Opcode::IMAGE_STORE_MIP || (decoded.opcode >= Decoder::Opcode::IMAGE_ATOMIC_SWAP && - decoded.opcode <= Decoder::Opcode::IMAGE_ATOMIC_XOR); + decoded.opcode <= Decoder::Opcode::IMAGE_ATOMIC_FMAX); if (store_or_atomic) { return index == 0u ? decoded.dst : decoded.src0; } @@ -1026,6 +1026,10 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) { return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicOr32); case Decoder::Opcode::IMAGE_ATOMIC_XOR: return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicXor32); + case Decoder::Opcode::IMAGE_ATOMIC_FMIN: + return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicFMin32); + case Decoder::Opcode::IMAGE_ATOMIC_FMAX: + return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicFMax32); case Decoder::Opcode::FLAT_LOAD_UBYTE: case Decoder::Opcode::FLAT_LOAD_SBYTE: diff --git a/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.cpp b/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.cpp index 2e360a8d5..26665b6f7 100644 --- a/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.cpp +++ b/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.cpp @@ -220,6 +220,8 @@ ImageOpcodeInfo ImageOpcodeInfoOf(ValueOpcode opcode) { case ValueOpcode::ImageAtomicAnd32: case ValueOpcode::ImageAtomicOr32: case ValueOpcode::ImageAtomicXor32: + case ValueOpcode::ImageAtomicFMin32: + case ValueOpcode::ImageAtomicFMax32: return {ImageAccess::Atomic, ImageResourceClass::Storage, false}; default: return {}; } diff --git a/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc b/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc index d13cd08d5..e3306a207 100644 --- a/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc +++ b/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc @@ -284,6 +284,8 @@ VALUE_OPCODE(ImageAtomicUMax32, U32, ImageResource, ImageAddress, U32, U1) VALUE_OPCODE(ImageAtomicAnd32, U32, ImageResource, ImageAddress, U32, U1) VALUE_OPCODE(ImageAtomicOr32, U32, ImageResource, ImageAddress, U32, U1) VALUE_OPCODE(ImageAtomicXor32, U32, ImageResource, ImageAddress, U32, U1) +VALUE_OPCODE(ImageAtomicFMin32, U32, ImageResource, ImageAddress, U32, U1) +VALUE_OPCODE(ImageAtomicFMax32, U32, ImageResource, ImageAddress, U32, U1) VALUE_OPCODE(GetAttribute, U32, U32, U32) VALUE_OPCODE(GetInterpolationParameter, U32, U32, U32, U32) VALUE_OPCODE(SetAttribute, Void, U32x4, U1) diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index f428a2ce2..1047c2972 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -485,7 +485,8 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna image.cube = DescriptorIsCube(descriptor); const auto format = static_cast((descriptor.dwords[1] >> 20u) & 0x1ffu); - if (base.atomic && format != Prospero::BufferFormat::k32UInt) { + if (base.atomic && format != Prospero::BufferFormat::k32UInt && + format != Prospero::BufferFormat::k32Float) { return SpecializationFail( fmt::format("atomic image descriptor {} uses unsupported format {}", i, static_cast(format))); @@ -514,7 +515,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna fmt::format("storage image descriptor {} uses unsupported format {}", i, static_cast(format))); } - if (raw_sint_storage) { + if (raw_sint_storage || base.atomic) { image.numeric_class = Prospero::TextureNumericClass::Uint; } } else if (image.numeric_class == Prospero::TextureNumericClass::Unsupported || diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 2ce1d0521..7a6c4b6da 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -16634,6 +16634,8 @@ CoverageClass ClassifyOpcode(ShaderOpcode opcode, case Opcode::IMAGE_ATOMIC_AND: case Opcode::IMAGE_ATOMIC_OR: case Opcode::IMAGE_ATOMIC_XOR: + case Opcode::IMAGE_ATOMIC_FMIN: + case Opcode::IMAGE_ATOMIC_FMAX: case Opcode::IMAGE_SAMPLE: case Opcode::IMAGE_GATHER4_LZ: case Opcode::IMAGE_GATHER4_C: @@ -28139,6 +28141,124 @@ TestCase ImageAtomicVariants() { return test; } +template TestCase ImageAtomicFloatGlcAndExec() { + using O = ShaderOpcode; + std::vector code; + constexpr u32 atomic_word = max_value ? 0xf07c0108u : 0xf0780108u; + code.push_back(EncodeSop1(0x04, 12, 126)); // Preserve active workgroup lanes. + AppendVMovU32(&code, 8, 2); + AppendVMovU32(&code, 9, 1); + AppendVMovLiteral(&code, 5, 0x40000000u); // 2.0 + // FMAX uses the captured GLC=0 instruction word. + code.insert(code.end(), {atomic_word, 0x00010508u}); + AppendStoreVgpr(&code, 5, 0); + AppendVMovLiteral(&code, 5, max_value ? 0x40800000u : 0x3f800000u); + code.insert(code.end(), {atomic_word | 0x2000u, 0x00010508u}); // GLC=1. + AppendStoreVgpr(&code, 5, 1); + AppendVMovLiteral(&code, 5, + max_value ? 0x41000000u : 0x3f000000u); // Inactive lane. + code.push_back(EncodeSop1(0x04, 126, InlineU32(0))); + code.insert(code.end(), {atomic_word | 0x2000u, 0x00010508u}); + code.push_back(EncodeSop1(0x04, 126, 12)); + AppendStoreVgpr(&code, 5, 2); + AppendEnd(&code); + + TestCase test; + test.name = max_value ? "ImageAtomicFMaxCapturedGlcAndExec" + : "ImageAtomicFMinGlcAndExec"; + test.code = std::move(code); + test.expected = {0x40000000u, 0x40000000u, + max_value ? 0x41000000u : 0x3f000000u}; + test.opcodes = {O::V_MOV_B32, O::S_MOV_B64, + max_value ? O::IMAGE_ATOMIC_FMAX : O::IMAGE_ATOMIC_FMIN, + O::BUFFER_STORE_DWORD, O::S_ENDPGM}; + const std::array descriptor = { + 0x504a7c00u, 0xc1600000u, 0x0086c0efu, 0x91b80924u, + 0x00000000u, 0x00700080u, 0x00000000u, 0x00000000u}; + std::copy_n(descriptor.begin(), 8, test.user_data.begin() + 4); + test.has_user_data = true; + test.storage_image_r32ui = std::vector(16, 0); + test.storage_image_r32ui[6] = max_value ? 0x3f800000u : 0x40800000u; + test.expected_storage_image_r32ui = test.storage_image_r32ui; + test.expected_storage_image_r32ui[6] = max_value ? 0x40800000u : 0x3f800000u; + test.required_spirv = {"OpImageTexelPointer", "OpAtomicCompareExchange", "R32ui"}; + return test; +} + +template TestCase ImageAtomicFloatSpecialValues() { + using O = ShaderOpcode; + // source, previous texel, expected minimum, expected maximum. + const u32 cases[][4] = { + {0x40000000u, 0x3f800000u, 0x3f800000u, 0x40000000u}, + {0xc0000000u, 0xbf800000u, 0xc0000000u, 0xbf800000u}, + {0x7f800000u, 0x3f800000u, 0x3f800000u, 0x7f800000u}, + {0xff800000u, 0x3f800000u, 0xff800000u, 0x3f800000u}, + {0x3f800000u, 0x7fc12345u, 0x7fc12345u, 0x7fc12345u}, + {0x7fcabcdeu, 0x3f800000u, 0x3f800000u, 0x3f800000u}, + {0x3f800000u, 0x7fa54321u, 0x7fa54321u, 0x7fa54321u}, + {0x7faabcdeu, 0x3f800000u, 0x3f800000u, 0x3f800000u}, + {0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x80000000u, 0x80000001u, 0x80000001u, 0x80000000u}, + {0x80000001u, 0x80000000u, 0x80000001u, 0x80000000u}, + {0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u}, + {0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u}, + }; + TestCase test; + test.name = max_value ? "ImageAtomicFMaxSpecialValues" + : "ImageAtomicFMinSpecialValues"; + test.user_data = MakeStorageTextureData(Prospero::BufferFormat::k32UInt); + test.has_user_data = true; + test.storage_image_r32ui = std::vector(16, 0); + test.expected_storage_image_r32ui = test.storage_image_r32ui; + for (u32 i = 0; i < std::size(cases); i++) { + AppendVMovU32(&test.code, 20, i & 3u); + AppendVMovU32(&test.code, 21, i >> 2u); + AppendVMovLiteral(&test.code, 0, cases[i][0]); + test.code.push_back(EncodeMimg0(max_value ? 0x1f : 0x1e, 1, 0, true)); + test.code.push_back(EncodeMimg1(0, 20)); + AppendStoreVgpr(&test.code, 0, i); + test.expected.push_back(cases[i][1]); + test.storage_image_r32ui[i] = cases[i][1]; + test.expected_storage_image_r32ui[i] = cases[i][max_value ? 3 : 2]; + } + AppendEnd(&test.code); + test.opcodes = {O::V_MOV_B32, + max_value ? O::IMAGE_ATOMIC_FMAX : O::IMAGE_ATOMIC_FMIN, + O::BUFFER_STORE_DWORD, O::S_ENDPGM}; + return test; +} + +template TestCase ImageAtomicFloatContendedWorkgroup() { + using O = ShaderOpcode; + TestCase test; + test.name = max_value ? "ImageAtomicFMaxContendedWorkgroup" + : "ImageAtomicFMinContendedWorkgroup"; + test.code.push_back(EncodeVop1(0x06, 1, Vgpr(0))); // Float thread_id.x. + AppendVMovU32(&test.code, 20, 0); + AppendVMovU32(&test.code, 21, 0); + test.code.push_back(EncodeMimg0(max_value ? 0x1f : 0x1e, 1)); + test.code.push_back(EncodeMimg1(1, 20)); + AppendEnd(&test.code); + test.opcodes = {O::V_CVT_F32_U32, O::V_MOV_B32, + max_value ? O::IMAGE_ATOMIC_FMAX : O::IMAGE_ATOMIC_FMIN, + O::S_ENDPGM}; + test.user_data = MakeStorageTextureData(Prospero::BufferFormat::k32UInt); + test.has_user_data = true; + test.storage_image_r32ui = std::vector(16, 0); + test.storage_image_r32ui[0] = + max_value ? 0xc2c80000u : 0x42c80000u; // -/+100.0 + test.expected_storage_image_r32ui = test.storage_image_r32ui; + test.expected_storage_image_r32ui[0] = + max_value ? 0x427c0000u : 0u; // 63.0 / 0.0 + test.compute_info.threads_num[0] = 64; + test.compute_info.threads_num[1] = 1; + test.compute_info.threads_num[2] = 1; + test.compute_info.thread_ids_num = 1; + test.has_compute_info = true; + return test; +} + TestCase ImageAtomicGlc0DoesNotReturnOldValue() { using O = ShaderOpcode; @@ -28951,6 +29071,12 @@ std::vector MakeCases() { AddCase(ImageAtomicSwapReturnsPreviousTexel); AddCase(ImageStoreAndAtomicUseSeparateBindings); AddCase(ImageAtomicVariants); + AddCase(ImageAtomicFloatGlcAndExec); + AddCase(ImageAtomicFloatGlcAndExec); + AddCase(ImageAtomicFloatSpecialValues); + AddCase(ImageAtomicFloatSpecialValues); + AddCase(ImageAtomicFloatContendedWorkgroup); + AddCase(ImageAtomicFloatContendedWorkgroup); AddCase(ImageAtomicGlc0DoesNotReturnOldValue); AddCase(MultipleWorkitemsGlobalId); AddCase(DispatcherIrreducibleControlFlow);