[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>
This commit is contained in:
Mees van den Kieboom
2026-07-16 16:28:39 +02:00
committed by GitHub
parent f2d9051358
commit 9883a9445d
5 changed files with 551 additions and 33 deletions
@@ -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<Gen5BufferMemoryControl>(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<Gen5BufferMemoryControl>(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<Gen5ImageControl>(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<uint, uint> { [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<byte> buffer = stackalloc byte[4];
foreach (var word in words.Append(EndPgm))
{
BinaryPrimitives.WriteUInt32LittleEndian(buffer, word);
Assert.True(memory.TryWrite(address, buffer));
address += sizeof(uint);
}
}
}
@@ -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<uint, uint>());
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<uint, uint>
{
// 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<uint, uint> 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<ushort> CompileCompute(
uint[] programWords,
Dictionary<uint, uint> 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<uint, uint>
{
[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<ushort> CollectOpcodes(byte[] spirv)
{
var opcodes = new HashSet<ushort>();
// 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;
}
}