[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 =