From 9883a9445d01732e8d4dce2a759f449a54ad1635 Mon Sep 17 00:00:00 2001 From: Mees van den Kieboom Date: Thu, 16 Jul 2026 16:28:39 +0200 Subject: [PATCH] [AGC] Implement RDNA2 buffer/image/DS 32-bit atomic instructions (#222) Adds decode and SPIR-V translation for the missing MUBUF, MIMG and DS atomic instructions in the Gen5 shader translator, generalizing the existing BufferAtomicAdd path. Covers swap, cmpswap, add, sub, smin/smax, umin/umax, and/or/xor, inc and dec, plus the DS RTN variants. Image atomics go through OpImageTexelPointer on the storage image binding. Notable: DS_CMPST operand order (DATA0 = comparator, DATA1 = new value) is reversed relative to buffer/image cmpswap, which a dedicated test locks in. ATOMIC_INC/DEC are approximated with OpAtomicIIncrement/IDecrement, exact for the common 0xFFFFFFFF clamp. Verified with 9 new synthetic decoder and end-to-end SPIR-V tests (part of the #36 test corpus effort); full suite passes 35/35. Signed-off-by: missatjuhvdk1 <177474143+missatjuhvdk1@users.noreply.github.com> Co-authored-by: missatjuhvdk1 <177474143+missatjuhvdk1@users.noreply.github.com> --- .../Gen5SpirvTranslator.cs | 218 +++++++++++++++--- .../SpirvModuleBuilder.cs | 12 + .../Gen5ShaderTranslator.cs | 83 ++++++- .../Agc/Gen5ShaderAtomicDecodeTests.cs | 134 +++++++++++ .../Agc/Gen5SpirvAtomicTranslationTests.cs | 137 +++++++++++ 5 files changed, 551 insertions(+), 33 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/Agc/Gen5ShaderAtomicDecodeTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/Agc/Gen5SpirvAtomicTranslationTests.cs diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index 60722e29..821eb754 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -1802,24 +1802,6 @@ public static partial class Gen5SpirvTranslator switch (instruction.Opcode) { - case "DsAddU32": - { - if (instruction.Sources.Count < 2) - { - error = "missing LDS atomic-add source"; - return false; - } - - var address = GetRawSource(instruction, 0); - _module.AddInstruction( - SpirvOp.AtomicIAdd, - _uintType, - LdsPointer(address, control.Offset0), - UInt(2), // Workgroup scope. - UInt(0x108), // AcquireRelease | WorkgroupMemory. - GetRawSource(instruction, 1)); - return true; - } case "DsWriteB32": { if (instruction.Sources.Count < 2) @@ -1964,6 +1946,11 @@ public static partial class Gen5SpirvTranslator return true; } default: + if (Gen5ShaderTranslator.IsDataShareAtomic(instruction.Opcode)) + { + return TryEmitDataShareAtomic(instruction, control, out error); + } + error = $"unsupported LDS opcode {instruction.Opcode}"; return false; } @@ -2004,6 +1991,126 @@ public static partial class Gen5SpirvTranslator Store(pointer, selected); } + private bool TryEmitDataShareAtomic( + Gen5ShaderInstruction instruction, + Gen5DataShareControl control, + out string error) + { + error = string.Empty; + var atomicOp = instruction.Opcode switch + { + "DsAddU32" or "DsAddRtnU32" => SpirvOp.AtomicIAdd, + "DsSubU32" or "DsSubRtnU32" => SpirvOp.AtomicISub, + "DsIncU32" or "DsIncRtnU32" => SpirvOp.AtomicIIncrement, + "DsDecU32" or "DsDecRtnU32" => SpirvOp.AtomicIDecrement, + "DsMinI32" or "DsMinRtnI32" => SpirvOp.AtomicSMin, + "DsMaxI32" or "DsMaxRtnI32" => SpirvOp.AtomicSMax, + "DsMinU32" or "DsMinRtnU32" => SpirvOp.AtomicUMin, + "DsMaxU32" or "DsMaxRtnU32" => SpirvOp.AtomicUMax, + "DsAndB32" or "DsAndRtnB32" => SpirvOp.AtomicAnd, + "DsOrB32" or "DsOrRtnB32" => SpirvOp.AtomicOr, + "DsXorB32" or "DsXorRtnB32" => SpirvOp.AtomicXor, + "DsWrxchgRtnB32" => SpirvOp.AtomicExchange, + "DsCmpstB32" or "DsCmpstRtnB32" => SpirvOp.AtomicCompareExchange, + _ => SpirvOp.Nop, + }; + if (atomicOp == SpirvOp.Nop) + { + error = $"unsupported LDS opcode {instruction.Opcode}"; + return false; + } + + var address = GetRawSource(instruction, 0); + var pointer = LdsPointer(address, control.Offset0); + EmitExecConditional(() => + { + var original = EmitAtomic( + atomicOp, + _uintType, + pointer, + scope: 2, + semantics: 0x108, + // DS_CMPST sources: DATA0 is the comparator, DATA1 the new value. + value: () => GetRawSource( + instruction, + atomicOp == SpirvOp.AtomicCompareExchange ? 2 : 1), + comparator: () => GetRawSource(instruction, 1)); + if (instruction.Destinations.Count > 0) + { + StoreV(instruction.Destinations[0].Value, original); + } + }); + + return true; + } + + // Maps the AMD atomic-op name suffix shared by buffer/image atomics to a SPIR-V opcode. + // Inc/Dec approximate the AMD wrap-clamp semantics (MEM = tmp >= DATA ? 0 : tmp + 1), + // which is exact for the common 0xFFFFFFFF clamp operand. + private static bool TryGetAtomicOp(string name, out SpirvOp op) + { + op = name switch + { + "Swap" => SpirvOp.AtomicExchange, + "Cmpswap" => SpirvOp.AtomicCompareExchange, + "Add" => SpirvOp.AtomicIAdd, + "Sub" => SpirvOp.AtomicISub, + "Smin" => SpirvOp.AtomicSMin, + "Umin" => SpirvOp.AtomicUMin, + "Smax" => SpirvOp.AtomicSMax, + "Umax" => SpirvOp.AtomicUMax, + "And" => SpirvOp.AtomicAnd, + "Or" => SpirvOp.AtomicOr, + "Xor" => SpirvOp.AtomicXor, + "Inc" => SpirvOp.AtomicIIncrement, + "Dec" => SpirvOp.AtomicIDecrement, + _ => SpirvOp.Nop, + }; + return op != SpirvOp.Nop; + } + + private uint EmitAtomic( + SpirvOp op, + uint type, + uint pointer, + uint scope, + uint semantics, + Func value, + Func comparator) + { + if (op is SpirvOp.AtomicIIncrement or SpirvOp.AtomicIDecrement) + { + return _module.AddInstruction( + op, + type, + pointer, + UInt(scope), + UInt(semantics)); + } + + if (op == SpirvOp.AtomicCompareExchange) + { + // The unequal semantics must not contain Release; downgrade it to Acquire. + return _module.AddInstruction( + op, + type, + pointer, + UInt(scope), + UInt(semantics), + UInt((semantics & ~0x8u) | 0x2u), + value(), + comparator()); + } + + return _module.AddInstruction( + op, + type, + pointer, + UInt(scope), + UInt(semantics), + value()); + } + private bool TryEmitInterpolation( Gen5ShaderInstruction instruction, Gen5InterpolationControl interpolation, @@ -2239,22 +2346,27 @@ public static partial class Gen5SpirvTranslator byteAddress = ApplyGuestBufferByteBias(bindingIndex, byteAddress); var dwordAddress = ShiftRightLogical(byteAddress, UInt(2)); - if (instruction.Opcode is "BufferAtomicAdd" or "BufferAtomicUMax") + if (instruction.Opcode.StartsWith("BufferAtomic", StringComparison.Ordinal)) { + if (!TryGetAtomicOp(instruction.Opcode["BufferAtomic".Length..], out var atomicOp)) + { + error = $"unsupported buffer opcode {instruction.Opcode}"; + return false; + } + EmitExecConditional(() => { var inRange = IsBufferWordInRange(bindingIndex, dwordAddress); EmitConditional(inRange, () => { - var original = _module.AddInstruction( - instruction.Opcode == "BufferAtomicAdd" - ? SpirvOp.AtomicIAdd - : SpirvOp.AtomicUMax, + var original = EmitAtomic( + atomicOp, _uintType, BufferWordPointer(bindingIndex, dwordAddress), - UInt(1), - UInt(0x48), - LoadV(control.VectorData)); + scope: 1, + semantics: 0x48, + value: () => LoadV(control.VectorData), + comparator: () => LoadV(control.VectorData + 1)); if (control.Glc) { StoreV(control.VectorData, original); @@ -3196,6 +3308,60 @@ public static partial class Gen5SpirvTranslator return true; } + if (instruction.Opcode.StartsWith("ImageAtomic", StringComparison.Ordinal)) + { + if (!resource.IsStorage) + { + error = "image atomic is not bound as storage"; + return false; + } + + if (resource.ComponentKind == ImageComponentKind.Float || + !TryGetAtomicOp(instruction.Opcode["ImageAtomic".Length..], out var atomicOp)) + { + error = $"unsupported storage image opcode {instruction.Opcode}"; + return false; + } + + var signed = resource.ComponentKind == ImageComponentKind.Sint; + var atomicImageSize = _module.AddInstruction( + SpirvOp.ImageQuerySize, + _module.TypeVector(_intType, 2), + imageObject); + var coordinates = BuildClampedIntegerCoordinates( + image, + 0, + atomicImageSize); + EmitExecConditional(() => + { + var pointer = _module.AddInstruction( + SpirvOp.ImageTexelPointer, + _module.TypePointer(SpirvStorageClass.Image, resource.ComponentType), + resource.Variable, + coordinates, + UInt(0)); + uint LoadData(uint register) => signed + ? Bitcast(_intType, LoadV(register)) + : LoadV(register); + var original = EmitAtomic( + atomicOp, + resource.ComponentType, + pointer, + scope: 1, + semantics: 0x808, + value: () => LoadData(image.VectorData), + comparator: () => LoadData(image.VectorData + 1)); + if (image.Glc) + { + StoreV( + image.VectorData, + signed ? Bitcast(_uintType, original) : original); + } + }); + + return true; + } + if (resource.IsStorage && instruction.Opcode is not ("ImageLoad" or "ImageLoadMip")) { diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs b/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs index 6295010a..52ff5edc 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs @@ -40,6 +40,7 @@ public enum SpirvOp : ushort FunctionEnd = 56, FunctionCall = 57, Variable = 59, + ImageTexelPointer = 60, Load = 61, Store = 62, AccessChain = 65, @@ -142,8 +143,19 @@ public enum SpirvOp : ushort BitCount = 205, ControlBarrier = 224, MemoryBarrier = 225, + AtomicExchange = 229, + AtomicCompareExchange = 230, + AtomicIIncrement = 232, + AtomicIDecrement = 233, AtomicIAdd = 234, + AtomicISub = 235, + AtomicSMin = 236, + AtomicUMin = 237, + AtomicSMax = 238, AtomicUMax = 239, + AtomicAnd = 240, + AtomicOr = 241, + AtomicXor = 242, Phi = 245, LoopMerge = 246, SelectionMerge = 247, diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs index 8c740da6..3bfa8cab 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs @@ -1172,9 +1172,33 @@ public static class Gen5ShaderTranslator name = opcode switch { 0x00 => "DsAddU32", + 0x01 => "DsSubU32", + 0x03 => "DsIncU32", + 0x04 => "DsDecU32", + 0x05 => "DsMinI32", + 0x06 => "DsMaxI32", + 0x07 => "DsMinU32", + 0x08 => "DsMaxU32", + 0x09 => "DsAndB32", + 0x0A => "DsOrB32", + 0x0B => "DsXorB32", 0x0D => "DsWriteB32", 0x0E => "DsWrite2B32", 0x0F => "DsWrite2St64B32", + 0x10 => "DsCmpstB32", + 0x20 => "DsAddRtnU32", + 0x21 => "DsSubRtnU32", + 0x23 => "DsIncRtnU32", + 0x24 => "DsDecRtnU32", + 0x25 => "DsMinRtnI32", + 0x26 => "DsMaxRtnI32", + 0x27 => "DsMinRtnU32", + 0x28 => "DsMaxRtnU32", + 0x29 => "DsAndRtnB32", + 0x2A => "DsOrRtnB32", + 0x2B => "DsXorRtnB32", + 0x2D => "DsWrxchgRtnB32", + 0x30 => "DsCmpstRtnB32", 0x35 => "DsSwizzleB32", 0x36 => "DsReadB32", 0x37 => "DsRead2B32", @@ -1272,8 +1296,19 @@ public static class Gen5ShaderTranslator 0x23 => "BufferLoadSbyteD16Hi", 0x24 => "BufferLoadShortD16", 0x25 => "BufferLoadShortD16Hi", + 0x30 => "BufferAtomicSwap", + 0x31 => "BufferAtomicCmpswap", 0x32 => "BufferAtomicAdd", - 0x38 => "BufferAtomicUMax", + 0x33 => "BufferAtomicSub", + 0x35 => "BufferAtomicSmin", + 0x36 => "BufferAtomicUmin", + 0x37 => "BufferAtomicSmax", + 0x38 => "BufferAtomicUmax", + 0x39 => "BufferAtomicAnd", + 0x3A => "BufferAtomicOr", + 0x3B => "BufferAtomicXor", + 0x3C => "BufferAtomicInc", + 0x3D => "BufferAtomicDec", _ => $"MubufRaw{opcode:X2}", }; sizeDwords = (extra >> 24) == 0xFF ? 3u : 2u; @@ -1388,7 +1423,18 @@ public static class Gen5ShaderTranslator 0x08 => "ImageStore", 0x09 => "ImageStoreMip", 0x0E => "ImageGetResinfo", + 0x0F => "ImageAtomicSwap", 0x10 => "ImageAtomicCmpswap", + 0x11 => "ImageAtomicAdd", + 0x12 => "ImageAtomicSub", + 0x14 => "ImageAtomicSmin", + 0x15 => "ImageAtomicUmin", + 0x16 => "ImageAtomicSmax", + 0x17 => "ImageAtomicUmax", + 0x18 => "ImageAtomicAnd", + 0x19 => "ImageAtomicOr", + 0x1A => "ImageAtomicXor", + 0x1B => "ImageAtomicInc", 0x1C => "ImageAtomicDec", 0x20 => "ImageSample", 0x22 => "ImageSampleD", @@ -1488,6 +1534,18 @@ public static class Gen5ShaderTranslator name.StartsWith("ImageStore", StringComparison.Ordinal) || name.StartsWith("ImageAtomic", StringComparison.Ordinal); + public static bool IsDataShareAtomic(string name) => name switch + { + "DsAddU32" or "DsSubU32" or "DsIncU32" or "DsDecU32" or + "DsMinI32" or "DsMaxI32" or "DsMinU32" or "DsMaxU32" or + "DsAndB32" or "DsOrB32" or "DsXorB32" or "DsCmpstB32" or + "DsAddRtnU32" or "DsSubRtnU32" or "DsIncRtnU32" or "DsDecRtnU32" or + "DsMinRtnI32" or "DsMaxRtnI32" or "DsMinRtnU32" or "DsMaxRtnU32" or + "DsAndRtnB32" or "DsOrRtnB32" or "DsXorRtnB32" or + "DsWrxchgRtnB32" or "DsCmpstRtnB32" => true, + _ => false, + }; + private static Gen5ShaderInstruction CreateInstruction( uint pc, Gen5ShaderEncoding encoding, @@ -1794,10 +1852,6 @@ public static class Gen5ShaderTranslator ((word >> 17) & 1) != 0); sources = opcode switch { - "DsAddU32" => [ - Gen5Operand.Vector(vectorAddress), - Gen5Operand.Vector(vectorData0), - ], "DsWriteB32" => [ Gen5Operand.Vector(vectorAddress), Gen5Operand.Vector(vectorData0), @@ -1826,6 +1880,17 @@ public static class Gen5ShaderTranslator Gen5Operand.Vector(vectorData1), ], "DsSwizzleB32" => [Gen5Operand.Vector(vectorData0)], + // DS_CMPST operand order is reversed vs buffer/image cmpswap: + // DATA0 holds the comparator, DATA1 holds the new value. + "DsCmpstB32" or "DsCmpstRtnB32" => [ + Gen5Operand.Vector(vectorAddress), + Gen5Operand.Vector(vectorData0), + Gen5Operand.Vector(vectorData1), + ], + _ when IsDataShareAtomic(opcode) => [ + Gen5Operand.Vector(vectorAddress), + Gen5Operand.Vector(vectorData0), + ], _ => [Gen5Operand.Vector(vectorAddress)], }; destinations = opcode switch @@ -1848,6 +1913,10 @@ public static class Gen5ShaderTranslator Gen5Operand.Vector(vectorDestination + 2), Gen5Operand.Vector(vectorDestination + 3), ], + _ when IsDataShareAtomic(opcode) && + opcode.Contains("Rtn", StringComparison.Ordinal) => [ + Gen5Operand.Vector(vectorDestination), + ], _ => [], }; break; @@ -1953,8 +2022,8 @@ public static class Gen5ShaderTranslator "BufferStoreDwordx2" => 2u, "BufferStoreDwordx3" => 3u, "BufferStoreDwordx4" => 4u, - "BufferAtomicAdd" => 1u, - "BufferAtomicUMax" => 1u, + "BufferAtomicCmpswap" => 2u, + _ when opcode.StartsWith("BufferAtomic", StringComparison.Ordinal) => 1u, _ => 0u, }; sources = diff --git a/tests/SharpEmu.Libs.Tests/Agc/Gen5ShaderAtomicDecodeTests.cs b/tests/SharpEmu.Libs.Tests/Agc/Gen5ShaderAtomicDecodeTests.cs new file mode 100644 index 00000000..1c092089 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/Gen5ShaderAtomicDecodeTests.cs @@ -0,0 +1,134 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.ShaderCompiler; +using Xunit; + +namespace SharpEmu.Libs.Tests.Agc; + +// Decodes synthetic GFX10 atomic instructions and checks the opcode names and operand wiring +// that the SPIR-V translator relies on. Word layouts follow the RDNA2 ISA manual: +// MUBUF op=word0[24:18], MIMG op=word0[24:18], DS op=word0[25:18]. +public sealed class Gen5ShaderAtomicDecodeTests +{ + private const ulong ShaderAddress = 0x1_0000_0000; + private const uint EndPgm = 0xBF810000; + + // Compute-stage register block: COMPUTE_USER_DATA_0 and COMPUTE_PGM_RSRC2, + // required since TryCreateState validates the USER_SGPR count. + internal const uint ComputeUserDataRegister = 0x240; + internal const uint ComputePgmRsrc2Register = 0x213; + + [Fact] + public void BufferAtomicUmax_DecodesControlAndDestination() + { + // BUFFER_ATOMIC_UMAX v1, off, s[0:3], 128 offset:8 glc + var instruction = DecodeSingle(0xE0E04008, 0x80000100); + + Assert.Equal("BufferAtomicUmax", instruction.Opcode); + var control = Assert.IsType(instruction.Control); + Assert.Equal(1u, control.DwordCount); + Assert.Equal(1u, control.VectorData); + Assert.Equal(0u, control.ScalarResource); + Assert.Equal(8, control.OffsetBytes); + Assert.True(control.Glc); + Assert.Equal(new[] { Gen5Operand.Vector(1) }, instruction.Destinations); + } + + [Fact] + public void BufferAtomicCmpswap_UsesTwoDataRegisters() + { + // BUFFER_ATOMIC_CMPSWAP v[1:2], off, s[0:3], 128 glc + var instruction = DecodeSingle(0xE0C44000, 0x80000100); + + Assert.Equal("BufferAtomicCmpswap", instruction.Opcode); + var control = Assert.IsType(instruction.Control); + Assert.Equal(2u, control.DwordCount); + Assert.Equal(1u, control.VectorData); + } + + [Fact] + public void ImageAtomicAdd_KeepsDataRegisterAsDestination() + { + // IMAGE_ATOMIC_ADD v2, v[0:1], s[4:11] dmask:0x1 dim:2D glc + var instruction = DecodeSingle(0xF0442100, 0x00010200); + + Assert.Equal("ImageAtomicAdd", instruction.Opcode); + var control = Assert.IsType(instruction.Control); + Assert.Equal(2u, control.VectorData); + Assert.Equal(4u, control.ScalarResource); + Assert.True(control.Glc); + Assert.Equal(new[] { Gen5Operand.Vector(2) }, instruction.Destinations); + } + + [Fact] + public void DsAddU32_HasAddressAndDataSourcesButNoDestination() + { + // DS_ADD_U32 v0, v1 + var instruction = DecodeSingle(0xD8000000, 0x00000100); + + Assert.Equal("DsAddU32", instruction.Opcode); + Assert.Equal( + new[] { Gen5Operand.Vector(0), Gen5Operand.Vector(1) }, + instruction.Sources); + Assert.Empty(instruction.Destinations); + } + + [Fact] + public void DsAddRtnU32_WritesReturnRegister() + { + // DS_ADD_RTN_U32 v3, v0, v1 + var instruction = DecodeSingle(0xD8800000, 0x03000100); + + Assert.Equal("DsAddRtnU32", instruction.Opcode); + Assert.Equal( + new[] { Gen5Operand.Vector(0), Gen5Operand.Vector(1) }, + instruction.Sources); + Assert.Equal(new[] { Gen5Operand.Vector(3) }, instruction.Destinations); + } + + [Fact] + public void DsCmpstRtnB32_OrdersComparatorBeforeNewValue() + { + // DS_CMPST_RTN_B32 v3, v0, v1, v2 - DATA0 (v1) is the comparator, DATA1 (v2) the + // new value, reversed relative to buffer/image cmpswap. + var instruction = DecodeSingle(0xD8C00000, 0x03020100); + + Assert.Equal("DsCmpstRtnB32", instruction.Opcode); + Assert.Equal( + new[] { Gen5Operand.Vector(0), Gen5Operand.Vector(1), Gen5Operand.Vector(2) }, + instruction.Sources); + Assert.Equal(new[] { Gen5Operand.Vector(3) }, instruction.Destinations); + } + + private static Gen5ShaderInstruction DecodeSingle(params uint[] words) + { + var memory = new FakeCpuMemory(ShaderAddress, 0x1000); + var ctx = new CpuContext(memory, Generation.Gen5); + WriteProgram(memory, ShaderAddress, words); + Assert.True( + Gen5ShaderTranslator.TryCreateState( + ctx, + ShaderAddress, + 0, + new Dictionary { [ComputePgmRsrc2Register] = 0 }, + ComputeUserDataRegister, + out var state, + out var error), + error); + return state.Program.Instructions[0]; + } + + internal static void WriteProgram(FakeCpuMemory memory, ulong address, uint[] words) + { + Span buffer = stackalloc byte[4]; + foreach (var word in words.Append(EndPgm)) + { + BinaryPrimitives.WriteUInt32LittleEndian(buffer, word); + Assert.True(memory.TryWrite(address, buffer)); + address += sizeof(uint); + } + } +} diff --git a/tests/SharpEmu.Libs.Tests/Agc/Gen5SpirvAtomicTranslationTests.cs b/tests/SharpEmu.Libs.Tests/Agc/Gen5SpirvAtomicTranslationTests.cs new file mode 100644 index 00000000..18f882f8 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/Gen5SpirvAtomicTranslationTests.cs @@ -0,0 +1,137 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Vulkan; +using Xunit; + +namespace SharpEmu.Libs.Tests.Agc; + +// End-to-end pipeline tests: synthetic GFX10 program -> decode -> scalar evaluation -> SPIR-V. +// Each test asserts the expected OpAtomic* instructions land in the emitted module. +public sealed class Gen5SpirvAtomicTranslationTests +{ + private const ulong ShaderAddress = 0x1_0000_0000; + private const ulong BufferAddress = 0x1_0000_1000; + + [Fact] + public void BufferAtomics_EmitAtomicOpcodes() + { + // BUFFER_ATOMIC_UMAX v1, BUFFER_ATOMIC_CMPSWAP v[1:2], BUFFER_ATOMIC_INC v1, + // all against the V# in s[0:3]. + var opcodes = CompileCompute( + [ + 0xE0E04008, 0x80000100, + 0xE0C44000, 0x80000100, + 0xE0F00000, 0x80000100, + ], + BufferDescriptorRegisters()); + + Assert.Contains((ushort)SpirvOp.AtomicUMax, opcodes); + Assert.Contains((ushort)SpirvOp.AtomicCompareExchange, opcodes); + Assert.Contains((ushort)SpirvOp.AtomicIIncrement, opcodes); + } + + [Fact] + public void DataShareAtomics_EmitAtomicOpcodes() + { + // DS_ADD_RTN_U32 v3, v0, v1; DS_CMPST_RTN_B32 v3, v0, v1, v2; DS_MAX_U32 v0, v1. + var opcodes = CompileCompute( + [ + 0xD8800000, 0x03000100, + 0xD8C00000, 0x03020100, + 0xD8200000, 0x00000100, + ], + new Dictionary()); + + Assert.Contains((ushort)SpirvOp.AtomicIAdd, opcodes); + Assert.Contains((ushort)SpirvOp.AtomicCompareExchange, opcodes); + Assert.Contains((ushort)SpirvOp.AtomicUMax, opcodes); + } + + [Fact] + public void ImageAtomicAdd_EmitsTexelPointerAndAtomicAdd() + { + // IMAGE_ATOMIC_ADD v2, v[0:1], s[4:11] dmask:0x1 dim:2D glc against an R32ui T#. + var opcodes = CompileCompute( + [0xF0442100, 0x00010200], + new Dictionary + { + // Descriptor word1 dataFormat (bits 28:20) = 20 selects R32ui/Uint. + [5] = 20u << 20, + }); + + Assert.Contains((ushort)SpirvOp.ImageTexelPointer, opcodes); + Assert.Contains((ushort)SpirvOp.AtomicIAdd, opcodes); + } + + private static Dictionary BufferDescriptorRegisters() => new() + { + // V# in s[0:3]: base=BufferAddress, stride=0, numRecords=64 bytes, type=0. + [0] = unchecked((uint)BufferAddress), + [1] = (uint)(BufferAddress >> 32), + [2] = 64, + [3] = 0, + }; + + private static HashSet CompileCompute( + uint[] programWords, + Dictionary userDataSgprs) + { + var memory = new FakeCpuMemory(ShaderAddress, 0x2000); + var ctx = new CpuContext(memory, Generation.Gen5); + Gen5ShaderAtomicDecodeTests.WriteProgram(memory, ShaderAddress, programWords); + // COMPUTE_PGM_RSRC2 advertises 16 user SGPRs; the user data words at + // COMPUTE_USER_DATA_0 + index seed s[0..15] for the scalar evaluator. + var shaderRegisters = new Dictionary + { + [Gen5ShaderAtomicDecodeTests.ComputePgmRsrc2Register] = 16u << 1, + }; + foreach (var (sgpr, value) in userDataSgprs) + { + shaderRegisters[Gen5ShaderAtomicDecodeTests.ComputeUserDataRegister + sgpr] = value; + } + + Assert.True( + Gen5ShaderTranslator.TryCreateState( + ctx, + ShaderAddress, + 0, + shaderRegisters, + Gen5ShaderAtomicDecodeTests.ComputeUserDataRegister, + out var state, + out var error), + error); + Assert.True( + Gen5ShaderScalarEvaluator.TryEvaluate(ctx, state, out var evaluation, out error), + error); + Assert.True( + Gen5SpirvTranslator.TryCompileComputeShader( + state, + evaluation, + 1, + 1, + 1, + out var shader, + out error), + error); + return CollectOpcodes(shader.Spirv); + } + + private static HashSet CollectOpcodes(byte[] spirv) + { + var opcodes = new HashSet(); + // 5-word SPIR-V header, then (wordCount << 16 | opcode) packed instructions. + for (var offset = 5 * sizeof(uint); offset + sizeof(uint) <= spirv.Length;) + { + var word = BinaryPrimitives.ReadUInt32LittleEndian( + spirv.AsSpan(offset, sizeof(uint))); + opcodes.Add((ushort)word); + offset += Math.Max((int)(word >> 16), 1) * sizeof(uint); + } + + return opcodes; + } +}