[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
@@ -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<uint> value,
Func<uint> 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"))
{
@@ -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,
@@ -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 =
@@ -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;
}
}