sharpemu/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs

3191 lines
119 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.ShaderCompiler.Ir;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
namespace SharpEmu.ShaderCompiler;
public static partial class Gen5ShaderTranslator
{
private static int _dppVectorsValidated;
// Some shipped Gen5 shaders exceed 64 KiB of encoded instructions before
// reaching S_ENDPGM. Keep the malformed-shader guard, but size it to the
// largest declared shader object accepted by the decoder (1 MiB).
private const int MaxInstructions = (int)(MaximumDeclaredShaderSizeBytes / sizeof(uint));
private const ulong FusedContinuationAlignment = 0x100;
private const ulong ShaderSizeOffset = 0x44;
private const uint MaximumDeclaredShaderSizeBytes = 1024 * 1024;
private static readonly ConditionalWeakTable<object, FusedProgramRegistry> _fusedProgramsByMemory = new();
private sealed class FusedProgramRegistry
{
private readonly object _gate = new();
private Dictionary<ulong, FusedShaderParts> _programs = new();
public bool TryGet(ulong entryAddress, out FusedShaderParts? parts) =>
Volatile.Read(ref _programs).TryGetValue(entryAddress, out parts);
public void Set(ulong entryAddress, FusedShaderParts parts)
{
lock (_gate)
{
var next = new Dictionary<ulong, FusedShaderParts>(_programs) { [entryAddress] = parts };
Volatile.Write(ref _programs, next);
}
}
}
private sealed record FusedShaderParts(
ulong EntryHeaderAddress,
ulong ContinuationAddress,
ulong ContinuationHeaderAddress);
private static FusedProgramRegistry GetFusedPrograms(ICpuMemory memory)
{
// Each native guest thread has its own tracking wrapper, but all
// shader creation and render contexts share the underlying memory.
while (memory is ICpuMemoryWrapper wrapper)
{
memory = wrapper.Inner;
}
return _fusedProgramsByMemory.GetValue(memory, static _ => new FusedProgramRegistry());
}
/// <summary>
/// Records the two code objects that AGC joins into one hardware shader.
/// The entry code transfers control to the continuation with S_SETPC_B64.
/// </summary>
public static void RegisterFusedProgram(
CpuContext ctx,
ulong entryAddress,
ulong entryHeaderAddress,
ulong continuationAddress,
ulong continuationHeaderAddress)
{
if (entryAddress == 0 ||
entryHeaderAddress == 0 ||
continuationAddress == 0 ||
continuationHeaderAddress == 0)
{
return;
}
var registry = GetFusedPrograms(ctx.Memory);
registry.Set(entryAddress, new FusedShaderParts(
entryHeaderAddress,
continuationAddress,
continuationHeaderAddress));
}
// The continuation registered for an entry address, when the guest joined two code objects.
public static bool TryGetFusedProgramParts(
CpuContext ctx,
ulong entryAddress,
out ulong continuationAddress,
out ulong continuationHeaderAddress)
{
var registry = GetFusedPrograms(ctx.Memory);
registry.TryGet(entryAddress, out var parts);
continuationAddress = parts?.ContinuationAddress ?? 0;
continuationHeaderAddress = parts?.ContinuationHeaderAddress ?? 0;
return parts is not null;
}
public static bool TryGetFusedProgramParts(
CpuContext ctx,
ulong entryAddress,
out ulong entryHeaderAddress,
out ulong continuationAddress,
out ulong continuationHeaderAddress)
{
var registry = GetFusedPrograms(ctx.Memory);
registry.TryGet(entryAddress, out var parts);
entryHeaderAddress = parts?.EntryHeaderAddress ?? 0;
continuationAddress = parts?.ContinuationAddress ?? 0;
continuationHeaderAddress = parts?.ContinuationHeaderAddress ?? 0;
return parts is not null;
}
public static string Describe(CpuContext ctx, ulong exportShaderAddress, ulong pixelShaderAddress)
{
var es = TryDecodeProgram(ctx, exportShaderAddress, out var esProgram, out var esError)
? ShaderDecodeInfo.Create(esProgram).ToString()
: $"error={esError}";
var ps = TryDecodeProgram(ctx, pixelShaderAddress, out var psProgram, out var psError)
? ShaderDecodeInfo.Create(psProgram).ToString()
: $"error={psError}";
return $"es[{es}] ps[{ps}]";
}
public static string DescribeWords(CpuContext ctx, ulong shaderAddress) =>
TryDecodeProgram(ctx, shaderAddress, out var program, out var error)
? string.Join(',', program.Instructions.SelectMany(instruction => instruction.Words)
.Select(word => $"{word:X8}"))
: $"error={error}";
// Public contract entry: emitter test suites and tools drive the decoder directly
// from raw instruction words.
public static bool TryDecodeProgram(
CpuContext ctx,
ulong address,
out Gen5ShaderProgram program,
out string error)
{
ValidateDppControlVectors();
var registry = GetFusedPrograms(ctx.Memory);
registry.TryGet(address, out var fusedParts);
if (fusedParts is not null)
{
return TryDecodeFusedProgram(ctx, address, fusedParts, out program, out error);
}
return TryDecodeProgramSegment(
ctx,
address,
maximumBytes: null,
// An unregistered S_SETPC transfers control to a separately
// allocated code object. The bytes that follow it in this
// object are padding or unrelated data, so decoding through them
// produces bogus ALU instructions and can reject an otherwise
// valid shader. Registered fused programs take the dedicated
// path above and splice their continuation explicitly.
stopAtSetProgramCounter: true,
out program,
out _,
out error);
}
private enum ProgramTermination
{
None,
EndProgram,
SetProgramCounter,
}
private static bool TryDecodeFusedProgram(
CpuContext ctx,
ulong entryAddress,
FusedShaderParts parts,
out Gen5ShaderProgram program,
out string error)
{
program = new Gen5ShaderProgram(entryAddress, []);
if (!TryReadDeclaredShaderSize(ctx, parts.EntryHeaderAddress, out var entrySize, out error) ||
!TryReadDeclaredShaderSize(
ctx,
parts.ContinuationHeaderAddress,
out var continuationSize,
out error))
{
return false;
}
var continuationDistance = unchecked(parts.ContinuationAddress - entryAddress);
if (parts.ContinuationAddress == entryAddress || (continuationDistance & (sizeof(uint) - 1)) != 0)
{
error = $"invalid-fused-layout entry=0x{entryAddress:X} " +
$"continuation=0x{parts.ContinuationAddress:X}";
return false;
}
if (!TryDecodeProgramSegment(
ctx,
entryAddress,
entrySize,
stopAtSetProgramCounter: true,
out var entryProgram,
out var entryTermination,
out error))
{
error = $"fused-entry: {error}";
return false;
}
if (entryTermination != ProgramTermination.SetProgramCounter ||
entryProgram.Instructions.Count == 0)
{
error = $"fused-entry-missing-setpc entry=0x{entryAddress:X}";
return false;
}
if (!TryDecodeProgramSegment(
ctx,
parts.ContinuationAddress,
continuationSize,
stopAtSetProgramCounter: false,
out var continuationProgram,
out _,
out error))
{
error = $"fused-continuation: {error}";
return false;
}
var lastEntryInstruction = entryProgram.Instructions[^1];
var entryEnd = (ulong)lastEntryInstruction.Pc + (ulong)lastEntryInstruction.Words.Count * sizeof(uint);
var adjacent = parts.ContinuationAddress > entryAddress && continuationDistance <= uint.MaxValue &&
continuationDistance >= entryEnd;
var continuationPc = adjacent
? (uint)continuationDistance
: (uint)((entryEnd + FusedContinuationAlignment - 1) & ~(FusedContinuationAlignment - 1));
var instructions = new List<Gen5ShaderInstruction>(
entryProgram.Instructions.Count + continuationProgram.Instructions.Count);
instructions.AddRange(entryProgram.Instructions.Take(entryProgram.Instructions.Count - 1));
var setProgramCounter = entryProgram.Instructions[^1];
instructions.Add(setProgramCounter with
{
Encoding = Gen5ShaderEncoding.Sopp,
Opcode = "SNop",
Words = [0xBF800000u],
Sources = [],
Destinations = [],
Control = null,
});
foreach (var instruction in continuationProgram.Instructions)
{
var rebasedPc = (ulong)continuationPc + instruction.Pc;
if (rebasedPc > uint.MaxValue)
{
error = $"fused-continuation-pc-overflow pc=0x{instruction.Pc:X} " +
$"base=0x{continuationPc:X}";
return false;
}
instructions.Add(adjacent
? instruction with { Pc = (uint)rebasedPc }
: instruction with { Pc = (uint)rebasedPc, AddressOffset = unchecked(continuationDistance + instruction.ProgramOffset) });
}
program = new Gen5ShaderProgram(entryAddress, instructions) { FusedContinuationPc = continuationPc };
error = string.Empty;
return true;
}
private static bool TryReadDeclaredShaderSize(
CpuContext ctx,
ulong headerAddress,
out uint sizeBytes,
out string error)
{
if (!TryReadUInt32(ctx, headerAddress + ShaderSizeOffset, out sizeBytes) ||
sizeBytes == 0 ||
(sizeBytes & (sizeof(uint) - 1)) != 0 ||
sizeBytes > MaximumDeclaredShaderSizeBytes)
{
error = $"invalid-shader-size header=0x{headerAddress:X} size=0x{sizeBytes:X}";
return false;
}
error = string.Empty;
return true;
}
private static bool TryDecodeProgramSegment(
CpuContext ctx,
ulong address,
uint? maximumBytes,
bool stopAtSetProgramCounter,
out Gen5ShaderProgram program,
out ProgramTermination termination,
out string error)
{
program = new Gen5ShaderProgram(address, []);
termination = ProgramTermination.None;
error = string.Empty;
if (address == 0)
{
error = "missing";
return false;
}
var instructions = new List<Gen5ShaderInstruction>();
var instructionCount = 0;
var lastPc = 0u;
var lastWord = 0u;
uint furthestForwardBranchTarget = 0;
for (uint pc = 0;
instructionCount < MaxInstructions &&
(!maximumBytes.HasValue || pc < maximumBytes.Value);)
{
if (maximumBytes.HasValue && maximumBytes.Value - pc < sizeof(uint))
{
error = $"truncated-word pc=0x{pc:X} limit=0x{maximumBytes.Value:X}";
return false;
}
if (!TryReadUInt32(ctx, address + pc, out var word))
{
error = $"read-failed pc=0x{pc:X}";
return false;
}
lastPc = pc;
lastWord = word;
if (!TryDecodeInstruction(
ctx,
address,
pc,
word,
out var encoding,
out var name,
out var sizeDwords,
out error))
{
return false;
}
// S_CODE_END pads the space after the last instruction. Code placed after the final
// S_ENDPGM that ends in a backward branch runs straight into it.
if (string.Equals(name, "SCodeEnd", StringComparison.Ordinal))
{
if (pc < furthestForwardBranchTarget || instructions.Count == 0)
{
error = $"code-end-inside-program pc=0x{pc:X} branchTarget=0x{furthestForwardBranchTarget:X}";
return false;
}
program = new Gen5ShaderProgram(address, instructions);
termination = ProgramTermination.EndProgram;
return true;
}
var instructionBytes = checked(sizeDwords * sizeof(uint));
if (maximumBytes.HasValue && instructionBytes > maximumBytes.Value - pc)
{
error = $"truncated-instruction pc=0x{pc:X} dwords={sizeDwords} " +
$"limit=0x{maximumBytes.Value:X}";
return false;
}
var words = new uint[sizeDwords];
words[0] = word;
for (uint wordIndex = 1; wordIndex < sizeDwords; wordIndex++)
{
if (!TryReadUInt32(ctx, address + pc + wordIndex * sizeof(uint), out words[wordIndex]))
{
error = $"read-failed pc=0x{pc + wordIndex * sizeof(uint):X}";
return false;
}
}
var instruction = CreateInstruction(pc, encoding, name, words);
if (instruction.Control is Gen5GlobalMemoryControl
{
UsesFlatAddress: true,
})
{
instruction = ResolveFlatAddressBase(instructions, instruction);
}
instructions.Add(instruction);
instructionCount++;
if (Gen5IrBranchResolver.Instance.TryGetBranchTarget(
instruction,
out var branchTargetPc) &&
branchTargetPc > instruction.Pc &&
// S_CBRANCH_CDBG* only fire under an attached GPU debugger, and
// the translators emit them as never taken. Their targets are
// debugger stubs placed after S_ENDPGM that read devkit-only
// addresses, so they must not extend the decoded program.
!instruction.Opcode.StartsWith("SCbranchCdbg", StringComparison.Ordinal))
{
furthestForwardBranchTarget = Math.Max(
furthestForwardBranchTarget,
branchTargetPc);
}
pc += sizeDwords * sizeof(uint);
// An unconditional backward S_BRANCH with no forward target
// pending is the last reachable instruction too: compilers place
// out-of-line loop blocks after S_ENDPGM, and the code object's
// metadata trailer follows them.
if ((string.Equals(name, "SEndpgm", StringComparison.Ordinal) ||
string.Equals(name, "SBranch", StringComparison.Ordinal)) &&
pc > furthestForwardBranchTarget)
{
program = new Gen5ShaderProgram(address, instructions);
termination = ProgramTermination.EndProgram;
return true;
}
// S_SWAPPC_B64 with NULL as its link destination discards the return
// address and is a tail transfer, as used by merged LS/HS programs.
if (stopAtSetProgramCounter &&
(string.Equals(name, "SSetpcB64", StringComparison.Ordinal) ||
(string.Equals(name, "SSwappcB64", StringComparison.Ordinal) && ((word >> 16) & 0x7F) == 125)))
{
program = new Gen5ShaderProgram(address, instructions);
termination = ProgramTermination.SetProgramCounter;
return true;
}
}
error = maximumBytes.HasValue
? $"unterminated limit=0x{maximumBytes.Value:X} pc=0x{lastPc:X} word=0x{lastWord:X8} count={instructionCount}"
: $"unterminated pc=0x{lastPc:X} word=0x{lastWord:X8} count={instructionCount}";
return false;
}
[Conditional("DEBUG")]
private static void ValidateDppControlVectors()
{
if (System.Threading.Interlocked.Exchange(ref _dppVectorsValidated, 1) != 0)
{
return;
}
static (uint Lane, bool InRange) Resolve(uint control, uint lane)
{
var rowBase = lane & ~15u;
var rowLane = lane & 15u;
return control switch
{
>= 0x101 and <= 0x10F => (
rowBase + ((rowLane + (control & 15)) & 15),
rowLane + (control & 15) < 16),
>= 0x111 and <= 0x11F => (
rowBase + ((rowLane - (control & 15)) & 15),
rowLane >= (control & 15)),
>= 0x121 and <= 0x12F => (
rowBase + ((rowLane - (control & 15)) & 15),
true),
0x140 => (rowBase + 15 - rowLane, true),
0x141 => ((lane & ~7u) + 7 - (lane & 7), true),
>= 0x150 and <= 0x15F => (rowBase + (control & 15), true),
>= 0x160 and <= 0x16F => (rowBase + (rowLane ^ (control & 15)), true),
_ => (lane, false),
};
}
Debug.Assert(Resolve(0x101, 0) == (1u, true));
Debug.Assert(Resolve(0x101, 15) == (0u, false));
Debug.Assert(Resolve(0x112, 1) == (15u, false));
Debug.Assert(Resolve(0x123, 0) == (13u, true));
Debug.Assert(Resolve(0x140, 18) == (29u, true));
Debug.Assert(Resolve(0x141, 9) == (14u, true));
Debug.Assert(Resolve(0x153, 20) == (19u, true));
Debug.Assert(Resolve(0x163, 22) == (21u, true));
const uint dpp8 =
(7u << 0) | (6u << 3) | (5u << 6) | (4u << 9) |
(3u << 12) | (2u << 15) | (1u << 18) | (0u << 21);
Debug.Assert(((dpp8 >> (0 * 3)) & 7) == 7);
Debug.Assert(((dpp8 >> (7 * 3)) & 7) == 0);
}
private static bool TryDecodeInstruction(
CpuContext ctx,
ulong baseAddress,
uint pc,
uint word,
out Gen5ShaderEncoding encoding,
out string name,
out uint sizeDwords,
out string error)
{
encoding = Gen5ShaderEncoding.Vop2;
name = string.Empty;
sizeDwords = 1;
error = string.Empty;
if ((word & 0x80000000u) == 0)
{
var vopOpcode = (word >> 25) & 0x3F;
encoding = vopOpcode switch
{
0x3E => Gen5ShaderEncoding.Vopc,
0x3F => Gen5ShaderEncoding.Vop1,
_ => Gen5ShaderEncoding.Vop2,
};
return DecodeVop2(word, out name, out sizeDwords, out error);
}
if ((word & 0xF8000000u) == 0xC0000000u)
{
encoding = Gen5ShaderEncoding.Smrd;
return DecodeSmrd(word, out name, out sizeDwords, out error);
}
if ((word & 0xC0000000u) == 0x80000000u)
{
var sopOpcode = (word >> 23) & 0x7F;
encoding = sopOpcode switch
{
0x7D => Gen5ShaderEncoding.Sop1,
0x7E => Gen5ShaderEncoding.Sopc,
0x7F => Gen5ShaderEncoding.Sopp,
>= 0x60 => Gen5ShaderEncoding.Sopk,
_ => Gen5ShaderEncoding.Sop2,
};
return DecodeSop(word, out name, out sizeDwords, out error);
}
// gfx10 moved VOP3P (packed 16-bit math) to its own 0b110011000 prefix
// (word0 top byte 0xCC), separate from the VOP3 block. Match the full
// 9-bit prefix here, before the coarse major-opcode switch, so packed
// instructions are not misread as one of the neighbouring encodings.
if ((word & 0xFF800000u) == 0xCC000000u)
{
encoding = Gen5ShaderEncoding.Vop3p;
if (!ctx.TryReadUInt32(baseAddress + pc + sizeof(uint), out var vop3pExtra))
{
error = $"vop3p-extra-read-failed pc=0x{pc:X}";
return false;
}
return DecodeVop3p(word, vop3pExtra, out name, out sizeDwords, out error);
}
switch (word >> 26)
{
case 0x33:
encoding = Gen5ShaderEncoding.Smem;
return DecodeSmem(word, out name, out sizeDwords, out error);
case 0x32:
encoding = Gen5ShaderEncoding.Vintrp;
return DecodeVintrp(word, out name, out sizeDwords, out error);
case 0x34:
case 0x35:
encoding = Gen5ShaderEncoding.Vop3;
if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var vop3Extra))
{
error = $"vop3-extra-read-failed pc=0x{pc:X}";
return false;
}
return DecodeVop3(
word,
vop3Extra,
IsVop3BOpcode((word >> 16) & 0x3FF),
out name,
out sizeDwords,
out error);
case 0x36:
encoding = Gen5ShaderEncoding.Ds;
return DecodeDs(word, out name, out sizeDwords, out error);
case 0x37:
encoding = Gen5ShaderEncoding.Flat;
return DecodeFlat(word, out name, out sizeDwords, out error);
case 0x38:
encoding = Gen5ShaderEncoding.Mubuf;
if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var mubufExtra))
{
error = $"mubuf-extra-read-failed pc=0x{pc:X}";
return false;
}
return DecodeMubuf(word, mubufExtra, out name, out sizeDwords, out error);
case 0x3A:
encoding = Gen5ShaderEncoding.Mtbuf;
if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var mtbufExtra))
{
error = $"mtbuf-extra-read-failed pc=0x{pc:X}";
return false;
}
return DecodeMtbuf(word, mtbufExtra, out name, out sizeDwords, out error);
case 0x3C:
encoding = Gen5ShaderEncoding.Mimg;
return DecodeMimg(word, out name, out sizeDwords, out error);
case 0x3D:
encoding = Gen5ShaderEncoding.Smem;
return DecodeSmem(word, out name, out sizeDwords, out error);
case 0x3E:
encoding = Gen5ShaderEncoding.Exp;
name = "Exp";
sizeDwords = 2;
return true;
case 0x3F:
encoding = Gen5ShaderEncoding.Vop3p;
return DecodeRaw2(word, "Vop3p", out name, out sizeDwords, out error);
default:
error = $"unknown-top pc=0x{pc:X} word=0x{word:X8}";
return false;
}
}
// Kept beside the production decoder so offline compatibility tools use
// precisely the same opcode tables and instruction-width rules as runtime
// shader translation.
internal static bool TryDecodeInstructionForPreflight(
CpuContext ctx,
uint pc,
uint word,
out string name,
out uint sizeDwords,
out string error) =>
TryDecodeInstruction(
ctx,
0,
pc,
word,
out _,
out name,
out sizeDwords,
out error);
private static bool DecodeSop(uint word, out string name, out uint sizeDwords, out string error)
{
var opcode = (word >> 23) & 0x7F;
return opcode switch
{
0x7D => DecodeSop1(word, out name, out sizeDwords, out error),
0x7E => DecodeSopc(word, out name, out sizeDwords, out error),
0x7F => DecodeSopp(word, out name, out sizeDwords, out error),
>= 0x60 => DecodeSopk(word, out name, out sizeDwords, out error),
_ => DecodeSop2(word, out name, out sizeDwords, out error),
};
}
private static bool DecodeSop1(uint word, out string name, out uint sizeDwords, out string error)
{
var opcode = (word >> 8) & 0xFF;
var src0 = word & 0xFF;
sizeDwords = 1 + (src0 == 0xFF ? 1u : 0u);
error = string.Empty;
name = opcode switch
{
0x03 => "SMovB32",
0x04 => "SMovB64",
0x06 => "SCmovB64",
0x07 => "SNotB32",
0x08 => "SNotB64",
0x09 => "SWqmB32",
0x0A => "SWqmB64",
0x0B => "SBrevB32",
0x0F => "SBcnt1I32B32",
0x10 => "SBcnt1I32B64",
0x13 => "SFF1I32B32",
0x14 => "SFF1I32B64",
0x15 => "SFlbitI32B32",
0x16 => "SFlbitI32B64",
0x1B => "SBitset0B32",
0x1C => "SBitset0B64",
0x1D => "SBitset1B32",
0x1E => "SBitset1B64",
0x1F => "SGetpcB64",
0x20 => "SSetpcB64",
0x21 => "SSwappcB64",
0x24 => "SAndSaveexecB64",
0x25 => "SOrSaveexecB64",
0x26 => "SXorSaveexecB64",
0x27 => "SAndn2SaveexecB64",
0x28 => "SOrn2SaveexecB64",
0x29 => "SNandSaveexecB64",
0x2A => "SNorSaveexecB64",
0x2B => "SXnorSaveexecB64",
0x2C => "SQuadmaskB32",
0x2D => "SQuadmaskB64",
0x34 => "SAbsI32",
0x37 => "SAndn1SaveexecB64",
0x38 => "SOrn1SaveexecB64",
0x3B => "SBitreplicateB64B32",
0x3C => "SAndSaveexecB32",
0x3D => "SOrSaveexecB32",
0x3E => "SXorSaveexecB32",
0x3F => "SAndn2SaveexecB32",
0x40 => "SOrn2SaveexecB32",
0x41 => "SNandSaveexecB32",
0x42 => "SNorSaveexecB32",
0x43 => "SXnorSaveexecB32",
0x44 => "SAndn1SaveexecB32",
0x45 => "SOrn1SaveexecB32",
_ => string.Empty,
};
return FinishDecode(name, $"unknown-sop1 op=0x{opcode:X2} word=0x{word:X8}", out error);
}
private static bool DecodeSop2(uint word, out string name, out uint sizeDwords, out string error)
{
var opcode = (word >> 23) & 0x7F;
var src0 = word & 0xFF;
var src1 = (word >> 8) & 0xFF;
sizeDwords = src0 == 0xFF || src1 == 0xFF ? 2u : 1u;
error = string.Empty;
name = opcode switch
{
0x00 => "SAddU32",
0x01 => "SSubU32",
0x02 => "SAddI32",
0x03 => "SSubI32",
0x04 => "SAddcU32",
0x05 => "SSubbU32",
0x06 => "SMinI32",
0x07 => "SMinU32",
0x08 => "SMaxI32",
0x09 => "SMaxU32",
0x0A => "SCselectB32",
0x0B => "SCselectB64",
0x0E => "SAndB32",
0x0F => "SAndB64",
0x10 => "SOrB32",
0x11 => "SOrB64",
0x12 => "SXorB32",
0x13 => "SXorB64",
0x14 => "SAndn2B32",
0x15 => "SAndn2B64",
0x16 => "SOrn2B32",
0x17 => "SOrn2B64",
0x18 => "SNandB32",
0x19 => "SNandB64",
0x1A => "SNorB32",
0x1B => "SNorB64",
0x1C => "SXnorB32",
0x1D => "SXnorB64",
0x1E => "SLshlB32",
0x1F => "SLshlB64",
0x20 => "SLshrB32",
0x21 => "SLshrB64",
0x22 => "SAshrI32",
0x23 => "SAshrI64",
0x24 => "SBfmB32",
0x25 => "SBfmB64",
0x26 => "SMulI32",
0x27 => "SBfeU32",
0x28 => "SBfeI32",
0x29 => "SBfeU64",
0x2A => "SBfeI64",
0x2D => "SAbsdiffI32",
0x2E => "SLshl1AddU32",
0x2F => "SLshl2AddU32",
0x30 => "SLshl3AddU32",
0x31 => "SLshl4AddU32",
0x32 => "SPackLlB32B16",
0x33 => "SPackLhB32B16",
0x34 => "SPackHhB32B16",
0x35 => "SMulHiU32",
0x36 => "SMulHiI32",
// RDNA scalar floating-point forms use the extended SOP2 opcode
// range. They keep the raw SGPR bit representation and do not
// alter SCC.
0x40 => "SAddF32",
0x41 => "SSubF32",
_ => string.Empty,
};
return FinishDecode(name, $"unknown-sop2 op=0x{opcode:X2}", out error);
}
private static bool DecodeSopc(uint word, out string name, out uint sizeDwords, out string error)
{
var opcode = (word >> 16) & 0x7F;
var src0 = word & 0xFF;
var src1 = (word >> 8) & 0xFF;
sizeDwords = src0 == 0xFF || src1 == 0xFF ? 2u : 1u;
error = string.Empty;
name = opcode switch
{
0x00 => "SCmpEqI32",
0x01 => "SCmpLgI32",
0x02 => "SCmpGtI32",
0x03 => "SCmpGeI32",
0x04 => "SCmpLtI32",
0x05 => "SCmpLeI32",
0x06 => "SCmpEqU32",
0x07 => "SCmpLgU32",
0x08 => "SCmpGtU32",
0x09 => "SCmpGeU32",
0x0A => "SCmpLtU32",
0x0B => "SCmpLeU32",
0x0C => "SBitcmp0B32",
0x0D => "SBitcmp1B32",
0x0E => "SBitcmp0B64",
0x0F => "SBitcmp1B64",
0x12 => "SCmpEqU64",
0x13 => "SCmpLgU64",
_ => string.Empty,
};
return FinishDecode(name, $"unknown-sopc op=0x{opcode:X2}", out error);
}
private static bool DecodeSopp(uint word, out string name, out uint sizeDwords, out string error)
{
var opcode = (word >> 16) & 0x7F;
sizeDwords = 1;
error = string.Empty;
name = opcode switch
{
0x00 => "SNop",
0x01 => "SEndpgm",
0x02 => "SBranch",
0x04 => "SCbranchScc0",
0x05 => "SCbranchScc1",
0x06 => "SCbranchVccz",
0x07 => "SCbranchVccnz",
0x08 => "SCbranchExecz",
0x09 => "SCbranchExecnz",
0x0A => "SBarrier",
0x0C => "SWaitcnt",
0x0E => "SSleep",
0x0F => "SSetprio",
0x10 => "SSendmsg",
0x12 => "STrap",
0x16 => "STtraceData",
0x17 => "SCbranchCdbgsys",
0x18 => "SCbranchCdbguser",
0x19 => "SCbranchCdbgsysOrUser",
0x1A => "SCbranchCdbgsysAndUser",
0x1F => "SCodeEnd",
0x20 => "SInstPrefetch",
0x21 => "SClause",
0x23 => "SWaitcntDepctr",
_ => string.Empty,
};
return FinishDecode(name, $"unknown-sopp op=0x{opcode:X2}", out error);
}
private static bool DecodeSopk(uint word, out string name, out uint sizeDwords, out string error)
{
var opcode = ((word >> 23) & 0x7F) - 0x60;
sizeDwords = 1;
error = string.Empty;
name = opcode switch
{
0x00 => "SMovkI32",
0x03 => "SCmpkEqI32",
0x04 => "SCmpkLgI32",
0x05 => "SCmpkGtI32",
0x06 => "SCmpkGeI32",
0x07 => "SCmpkLtI32",
0x08 => "SCmpkLeI32",
0x09 => "SCmpkEqU32",
0x0A => "SCmpkLgU32",
0x0B => "SCmpkGtU32",
0x0C => "SCmpkGeU32",
0x0D => "SCmpkLtU32",
0x0E => "SCmpkLeU32",
0x0F => "SAddkI32",
0x10 => "SMulkI32",
0x13 => "SSetregB32",
// RDNA2 uses four SOPK forms to wait for one counter.
// The selected counter does not change the translated operation.
0x17 or 0x18 or 0x19 or 0x1A => "SWaitcnt",
_ => string.Empty,
};
return FinishDecode(name, $"unknown-sopk op=0x{opcode:X2}", out error);
}
private static bool DecodeVop1(uint word, out string name, out uint sizeDwords, out string error)
{
var opcode = (word >> 9) & 0xFF;
var src0 = word & 0x1FF;
sizeDwords = src0 is 0xE9 or 0xEA or 0xF9 or 0xFA or 0xFF ? 2u : 1u;
error = string.Empty;
name = opcode switch
{
0x00 => "VNop",
0x01 => "VMovB32",
0x02 => "VReadfirstlaneB32",
0x04 => "VCvtF64I32",
0x05 => "VCvtF32I32",
0x06 => "VCvtF32U32",
0x07 => "VCvtU32F32",
0x08 => "VCvtI32F32",
0x0A => "VCvtF16F32",
0x0B => "VCvtF32F16",
0x0C => "VCvtRpiI32F32",
0x0D => "VCvtFlrI32F32",
0x0E => "VCvtOffF32I4",
0x0F => "VCvtF32F64",
0x11 => "VCvtF32Ubyte0",
0x12 => "VCvtF32Ubyte1",
0x13 => "VCvtF32Ubyte2",
0x14 => "VCvtF32Ubyte3",
0x16 => "VCvtF64U32",
0x20 => "VFractF32",
0x21 => "VTruncF32",
0x22 => "VCeilF32",
0x23 => "VRndneF32",
0x24 => "VFloorF32",
0x25 => "VExpF32",
0x27 => "VLogF32",
0x2A => "VRcpF32",
0x2B => "VRcpIflagF32",
0x2E => "VRsqF32",
0x2F => "VRcpF64",
0x30 => "VRsqF64",
0x33 => "VSqrtF32",
0x34 => "VSqrtF64",
0x35 => "VSinF32",
0x36 => "VCosF32",
0x37 => "VNotB32",
0x38 => "VBfrevB32",
0x39 => "VFfbhU32",
0x3A => "VFfblB32",
0x3B => "VFfbhI32",
0x42 => "VMovreldB32",
0x43 => "VMovrelsB32",
0x44 => "VMovrelsdB32",
0x48 => "VMovrelsd2B32",
0x52 => "VCvtU16F16",
0x54 => "VRcpF16",
0x55 => "VSqrtF16",
0x56 => "VRsqF16",
0x57 => "VLogF16",
0x58 => "VExpF16",
0x5B => "VFloorF16",
0x5C => "VCeilF16",
0x5D => "VTruncF16",
0x5E => "VRndneF16",
0x5F => "VFractF16",
0x60 => "VSinF16",
0x61 => "VCosF16",
_ => string.Empty,
};
return FinishDecode(name, $"unknown-vop1 op=0x{opcode:X2}", out error);
}
private static bool DecodeVop2(uint word, out string name, out uint sizeDwords, out string error)
{
var opcode = (word >> 25) & 0x3F;
if (opcode == 0x3E)
{
return DecodeVopc(word, out name, out sizeDwords, out error);
}
if (opcode == 0x3F)
{
return DecodeVop1(word, out name, out sizeDwords, out error);
}
var src0 = word & 0x1FF;
sizeDwords = opcode is 0x20 or 0x21 or 0x2C or 0x2D or 0x37 or 0x38 ||
src0 is 0xE9 or 0xEA or 0xF9 or 0xFA or 0xFF ? 2u : 1u;
error = string.Empty;
name = opcode switch
{
// Some Gen5 shader streams use the VOP2 opcode-zero form as a
// padding/no-op instruction. Keep it as VNop so the translator
// can consume the instruction without manufacturing a result.
0x00 => "VNop",
0x01 => "VCndmaskB32",
0x02 => "VDot2cF32F16",
0x03 => "VAddF32",
0x04 => "VSubF32",
0x05 => "VSubrevF32",
0x08 => "VMulF32",
0x09 => "VMulI32I24",
0x0B => "VMulU32U24",
0x0C => "VMulHiU32U24",
0x0F => "VMinF32",
0x10 => "VMaxF32",
0x11 => "VMinI32",
0x12 => "VMaxI32",
0x13 => "VMinU32",
0x14 => "VMaxU32",
0x15 => "VLshrB32",
0x16 => "VLshrrevB32",
0x17 => "VAshrI32",
0x18 => "VAshrrevI32",
0x19 => "VLshlB32",
0x1A => "VLshlrevB32",
0x1B => "VAndB32",
0x1C => "VOrB32",
0x1D => "VXorB32",
0x1E => "VXnorB32",
0x1F => "VMacF32",
0x20 => "VMadMkF32",
0x21 => "VMadAkF32",
0x22 => "VBcntU32B32",
0x23 => "VMbcntLoU32B32",
0x24 => "VMbcntHiU32B32",
0x25 => "VAddI32",
0x26 => "VSubI32",
0x27 => "VSubrevI32",
0x28 => "VAddcU32",
0x29 => "VSubbU32",
0x2A => "VSubbrevU32",
0x2B => "VFmacF32",
0x2C => "VFmaMkF32",
0x2D => "VFmaAkF32",
0x2F => "VCvtPkrtzF16F32",
0x30 => "VCvtPkU16U32",
0x31 => "VCvtPkI16I32",
0x32 => "VAddF16",
0x33 => "VSubF16",
0x34 => "VSubrevF16",
0x35 => "VMulF16",
// RDNA encodes FMAMK/FMAAK as VOP2 instructions with a literal
// dword immediately following the opcode word.
0x37 => "VFmaMkF16",
0x38 => "VFmaAkF16",
0x39 => "VMaxF16",
0x3A => "VMinF16",
0x3C => "VPkFmacF16",
_ => string.Empty,
};
return FinishDecode(
name,
$"unknown-vop2 op=0x{opcode:X2} word=0x{word:X8}",
out error);
}
private static bool DecodeVopc(uint word, out string name, out uint sizeDwords, out string error)
{
var opcode = (word >> 17) & 0xFF;
var src0 = word & 0x1FF;
sizeDwords = src0 is 0xE9 or 0xEA or 0xF9 or 0xFA or 0xFF ? 2u : 1u;
error = string.Empty;
name = VopcOpcodeName(opcode);
return FinishDecode(name, $"unknown-vopc op=0x{opcode:X2}", out error);
}
// VOPC names, shared by the VOP3 encoding of the same compares (VOP3 opcodes 0x000-0x0FF).
private static string VopcOpcodeName(uint opcode) =>
opcode switch
{
0x00 => "VCmpFF32",
0x01 => "VCmpLtF32",
0x02 => "VCmpEqF32",
0x03 => "VCmpLeF32",
0x04 => "VCmpGtF32",
0x05 => "VCmpLgF32",
0x06 => "VCmpGeF32",
0x07 => "VCmpOF32",
0x08 => "VCmpUF32",
0x09 => "VCmpNgeF32",
0x0A => "VCmpNlgF32",
0x0B => "VCmpNgtF32",
0x0C => "VCmpNleF32",
0x0D => "VCmpNeqF32",
0x0E => "VCmpNltF32",
0x0F => "VCmpTruF32",
0x10 => "VCmpxFF32",
0x11 => "VCmpxLtF32",
0x12 => "VCmpxEqF32",
0x13 => "VCmpxLeF32",
0x14 => "VCmpxGtF32",
0x15 => "VCmpxLgF32",
0x16 => "VCmpxGeF32",
0x17 => "VCmpxOF32",
0x18 => "VCmpxUF32",
0x19 => "VCmpxNgeF32",
0x1A => "VCmpxNlgF32",
0x1B => "VCmpxNgtF32",
0x1C => "VCmpxNleF32",
0x1D => "VCmpxNeqF32",
0x1E => "VCmpxNltF32",
0x1F => "VCmpxTruF32",
0x80 => "VCmpFI32",
0x81 => "VCmpLtI32",
0x82 => "VCmpEqI32",
0x83 => "VCmpLeI32",
0x84 => "VCmpGtI32",
0x85 => "VCmpNeI32",
0x86 => "VCmpGeI32",
0x87 => "VCmpTI32",
0x88 => "VCmpClassF32",
0x89 => "VCmpLtI16",
0x8A => "VCmpEqI16",
0x8B => "VCmpLeI16",
0x8C => "VCmpGtI16",
0x8D => "VCmpNeI16",
0x8E => "VCmpGeI16",
0x98 => "VCmpxClassF32",
0xA9 => "VCmpLtU16",
0xAA => "VCmpEqU16",
0xAB => "VCmpLeU16",
0xAC => "VCmpGtU16",
0xAD => "VCmpNeU16",
0xAE => "VCmpGeU16",
0x99 => "VCmpxLtI16",
0x9A => "VCmpxEqI16",
0x9B => "VCmpxLeI16",
0x9C => "VCmpxGtI16",
0x9D => "VCmpxNeI16",
0x9E => "VCmpxGeI16",
0xB9 => "VCmpxLtU16",
0xBA => "VCmpxEqU16",
0xBB => "VCmpxLeU16",
0xBC => "VCmpxGtU16",
0xBD => "VCmpxNeU16",
0xBE => "VCmpxGeU16",
0x90 => "VCmpxFI32",
0x91 => "VCmpxLtI32",
0x92 => "VCmpxEqI32",
0x93 => "VCmpxLeI32",
0x94 => "VCmpxGtI32",
0x95 => "VCmpxNeI32",
0x96 => "VCmpxGeI32",
0x97 => "VCmpxTI32",
0xA0 => "VCmpFI64",
0xA1 => "VCmpLtI64",
0xA2 => "VCmpEqI64",
0xA3 => "VCmpLeI64",
0xA4 => "VCmpGtI64",
0xA5 => "VCmpNeI64",
0xA6 => "VCmpGeI64",
0xA7 => "VCmpTI64",
0xB0 => "VCmpxFI64",
0xB1 => "VCmpxLtI64",
0xB2 => "VCmpxEqI64",
0xB3 => "VCmpxLeI64",
0xB4 => "VCmpxGtI64",
0xB5 => "VCmpxNeI64",
0xB6 => "VCmpxGeI64",
0xB7 => "VCmpxTI64",
0xC0 => "VCmpFU32",
0xC1 => "VCmpLtU32",
0xC2 => "VCmpEqU32",
0xC3 => "VCmpLeU32",
0xC4 => "VCmpGtU32",
0xC5 => "VCmpNeU32",
0xC6 => "VCmpGeU32",
0xC7 => "VCmpTU32",
0xC8 => "VCmpFF16",
0xC9 => "VCmpLtF16",
0xCA => "VCmpEqF16",
0xCB => "VCmpLeF16",
0xCC => "VCmpGtF16",
0xCD => "VCmpLgF16",
0xCE => "VCmpGeF16",
0xCF => "VCmpOF16",
0xD0 => "VCmpxFU32",
0xD1 => "VCmpxLtU32",
0xD2 => "VCmpxEqU32",
0xD3 => "VCmpxLeU32",
0xD4 => "VCmpxGtU32",
0xD5 => "VCmpxNeU32",
0xD6 => "VCmpxGeU32",
0xD7 => "VCmpxTU32",
0xD8 => "VCmpxFF16",
0xD9 => "VCmpxLtF16",
0xDA => "VCmpxEqF16",
0xDB => "VCmpxLeF16",
0xDC => "VCmpxGtF16",
0xDD => "VCmpxLgF16",
0xDE => "VCmpxGeF16",
0xDF => "VCmpxOF16",
0xE0 => "VCmpFU64",
0xE1 => "VCmpLtU64",
0xE2 => "VCmpEqU64",
0xE3 => "VCmpLeU64",
0xE4 => "VCmpGtU64",
0xE5 => "VCmpNeU64",
0xE6 => "VCmpGeU64",
0xE7 => "VCmpTU64",
0xE8 => "VCmpUF16",
0xE9 => "VCmpNgeF16",
0xEA => "VCmpNlgF16",
0xEB => "VCmpNgtF16",
0xEC => "VCmpNleF16",
0xED => "VCmpNeqF16",
0xEE => "VCmpNltF16",
0xEF => "VCmpTruF16",
0xF0 => "VCmpxFU64",
0xF1 => "VCmpxLtU64",
0xF2 => "VCmpxEqU64",
0xF3 => "VCmpxLeU64",
0xF4 => "VCmpxGtU64",
0xF5 => "VCmpxNeU64",
0xF6 => "VCmpxGeU64",
0xF7 => "VCmpxTU64",
0xF8 => "VCmpxUF16",
0xF9 => "VCmpxNgeF16",
0xFA => "VCmpxNlgF16",
0xFB => "VCmpxNgtF16",
0xFC => "VCmpxNleF16",
0xFD => "VCmpxNeqF16",
0xFE => "VCmpxNltF16",
0xFF => "VCmpxTruF16",
_ => string.Empty,
};
private static bool DecodeVop3(
uint word,
uint extra,
bool isVop3B,
out string name,
out uint sizeDwords,
out string error)
{
var opcode = (word >> 16) & 0x3FF;
var src0 = extra & 0x1FF;
var src1 = (extra >> 9) & 0x1FF;
var src2 = (extra >> 18) & 0x1FF;
sizeDwords = src0 == 0xFF || src1 == 0xFF || src2 == 0xFF ? 3u : 2u;
error = string.Empty;
if (!isVop3B && opcode < 0x100 && VopcOpcodeName(opcode) is { Length: > 0 } compare)
{
name = compare;
return true;
}
name = isVop3B
? opcode switch
{
0x128 => "VAddCoCiU32",
0x129 => "VSubCoCiU32",
0x12A => "VSubrevCoCiU32",
0x30F => "VAddCoU32",
0x310 => "VSubCoU32",
0x319 => "VSubrevCoU32",
0x176 => "VMadU64U32",
_ => $"Vop3bRaw{opcode:X3}",
}
: opcode switch
{
0x0B5 => "VCmpxNeI64",
0x101 => "VCndmaskB32",
0x103 => "VAddF32",
0x104 => "VSubF32",
0x108 => "VMulF32",
0x109 => "VMulI32I24",
0x10F => "VMinF32",
0x110 => "VMaxF32",
0x11F => "VMacF32",
0x12B => "VFmacF32",
0x12F => "VCvtPkrtzF16F32",
0x141 => "VMadF32",
0x142 => "VMadI32I24",
0x143 => "VMadU32U24",
0x144 => "VCubeidF32",
0x145 => "VCubescF32",
0x146 => "VCubetcF32",
0x147 => "VCubemaF32",
0x14A => "VBfiB32",
0x14E => "VAlignbitB32",
0x14F => "VAlignbyteB32",
0x14B => "VFmaF32",
0x14C => "VFmaF64",
0x151 => "VMin3F32",
0x152 => "VMin3I32",
0x153 => "VMin3U32",
0x154 => "VMax3F32",
0x155 => "VMax3I32",
0x156 => "VMax3U32",
0x157 => "VMed3F32",
0x158 => "VMed3I32",
0x159 => "VMed3U32",
0x15A => "VSadU8",
0x15B => "VSadHiU8",
0x15C => "VSadU16",
0x15D => "VSadU32",
0x15E => "VCvtPkU8F32",
0x148 => "VBfeU32",
0x149 => "VBfeI32",
0x165 => "VMulF64",
0x169 => "VMulLoU32",
0x16A => "VMulHiU32",
0x16B => "VMulLoI32",
0x16C => "VMulHiI32",
// VOP3-encoded VOP1 (opcode + 0x180) for the double-precision
// conversions and transcendentals.
0x184 => "VCvtF64I32",
0x18F => "VCvtF32F64",
0x196 => "VCvtF64U32",
0x1AF => "VRcpF64",
0x1B0 => "VRsqF64",
0x1B4 => "VSqrtF64",
// VOP3-encoded half-precision transcendentals and rounding.
0x1D4 => "VRcpF16",
0x1D5 => "VSqrtF16",
0x1D7 => "VLogF16",
0x1D8 => "VExpF16",
0x1DB => "VFloorF16",
0x1DC => "VCeilF16",
0x1DD => "VTruncF16",
0x1DE => "VRndneF16",
0x1DF => "VFractF16",
0x1E0 => "VSinF16",
0x1E1 => "VCosF16",
0x1D6 => "VRsqF16",
0x303 => "VAddNcU16",
0x34B => "VFmaF16",
0x351 => "VMin3F16",
0x354 => "VMax3F16",
0x357 => "VMed3F16",
0x360 => "VReadlaneB32",
0x361 => "VWritelaneB32",
0x362 => "VLdexpF32",
0x363 => "VBfmB32",
0x364 => "VBcntU32B32",
0x365 => "VMbcntLoU32B32",
0x366 => "VMbcntHiU32B32",
0x368 => "VCvtPknormI16F32",
0x369 => "VCvtPknormU16F32",
0x36A => "VCvtPkU16U32",
0x36B => "VCvtPkI16I32",
// GFX10 reverse 64-bit shifts.
0x2FF => "VLshlrevB64",
0x300 => "VLshrrevB64",
0x373 => "VMadU32U16",
0x346 => "VLshlAddU32",
0x345 => "VXadU32",
0x347 => "VAddLshlU32",
0x36D => "VAdd3U32",
0x36F => "VLshlOrU32",
0x178 => "VXor3B32",
// VOP1 opcode 0x52 is available through VOP3 as opcode 0x1D2.
0x1D2 => "VCvtU16F16",
0x371 => "VAndOrB32",
0x372 => "VOr3U32",
0x377 => "VPermlane16B32",
0x378 => "VPermlanex16B32",
// GFX10 16-bit integer ALU, VOP3-only.
0x304 => "VSubNcU16",
0x307 => "VLshrrevB16",
0x308 => "VAshrrevI16",
0x309 => "VMaxU16",
0x30A => "VMaxI16",
0x30B => "VMinU16",
0x30C => "VMinI16",
0x30D => "VAddNcI16",
0x30E => "VSubNcI16",
0x314 => "VLshlrevB16",
// VOP3-encoded 64-bit VOPC (opcode < 0x100): V_CMP_*_U64 / V_CMPX_*_U64.
0x0E0 => "VCmpFU64",
0x0E1 => "VCmpLtU64",
0x0E2 => "VCmpEqU64",
0x0E3 => "VCmpLeU64",
0x0E4 => "VCmpGtU64",
0x0E5 => "VCmpNeU64",
0x0E6 => "VCmpGeU64",
0x0E7 => "VCmpTU64",
0x0F0 => "VCmpxFU64",
0x0F1 => "VCmpxLtU64",
0x0F2 => "VCmpxEqU64",
0x0F3 => "VCmpxLeU64",
0x0F4 => "VCmpxGtU64",
0x0F5 => "VCmpxNeU64",
0x0F6 => "VCmpxGeU64",
0x0F7 => "VCmpxTU64",
_ => $"Vop3Raw{opcode:X3}",
};
return FinishDecode(name, $"unknown-vop3 op=0x{opcode:X3}", out error);
}
private static bool IsVop3BOpcode(uint opcode) =>
opcode is 0x128 or 0x129 or 0x12A or 0x16D or 0x16E or 0x176 or 0x177 or 0x30F or 0x310 or 0x319;
private static bool DecodeRaw2(
uint word,
string prefix,
out string name,
out uint sizeDwords,
out string error)
{
name = $"{prefix}Raw{word >> 24:X2}";
sizeDwords = 2;
error = string.Empty;
return true;
}
private static bool DecodeVop3p(
uint word,
uint extra,
out string name,
out uint sizeDwords,
out string error)
{
var opcode = (word >> 16) & 0x7F;
var src0 = extra & 0x1FF;
var src1 = (extra >> 9) & 0x1FF;
var src2 = (extra >> 18) & 0x1FF;
sizeDwords = src0 == 0xFF || src1 == 0xFF || src2 == 0xFF ? 3u : 2u;
error = string.Empty;
// Opcode numbers taken from LLVM's AMDGPU VOP3PInstructions.td and the
// gfx9/gfx10 MC test encodings; they are unchanged across gfx9 and gfx10.
// The mix ops (0x20/0x21/0x22) are V_MAD_MIX_* on gfx9 and V_FMA_MIX_*
// (fused) on the gfx10 the PS5 targets; both share these opcodes. Any
// remaining packed opcode (integer, ...) stays opaque here and fails
// loudly at emission rather than being silently mis-emitted.
name = opcode switch
{
0x02 => "VPkAddI16",
0x03 => "VPkSubI16",
0x0E => "VPkFmaF16",
0x0F => "VPkAddF16",
0x10 => "VPkMulF16",
0x11 => "VPkMinF16",
0x12 => "VPkMaxF16",
0x20 => "VFmaMixF32",
0x21 => "VFmaMixloF16",
0x22 => "VFmaMixhiF16",
_ => $"Vop3pRaw{opcode:X2}",
};
return true;
}
private static bool DecodeDs(
uint word,
out string name,
out uint sizeDwords,
out string error)
{
var opcode = (word >> 18) & 0xFF;
sizeDwords = 2;
error = string.Empty;
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",
0x12 => "DsMinF32",
0x13 => "DsMaxF32",
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",
0x38 => "DsRead2St64B32",
0x39 => "DsReadI8",
0x3D => "DsConsume",
0x3E => "DsAppend",
// gfx10 groups the 64-bit LDS atomics at 0x40..0x4C, directly ahead
// of DS_WRITE_B64 (0x4D). Only the no-return forms are named here:
// the RTN variants cannot be split into two 32-bit atomics without
// losing the atomicity of the returned pair, so they stay unknown
// and keep failing loudly.
0x40 => "DsAddU64",
0x4A => "DsOrB64",
0x4D => "DsWriteB64",
0x4E => "DsWrite2B64",
0x4F => "DsWrite2St64B64",
0x76 => "DsReadB64",
0x77 => "DsRead2B64",
0xB0 => "DsWriteAddtidB32",
0xB1 => "DsReadAddtidB32",
0xB3 => "DsBpermuteB32",
0xDE => "DsWriteB96",
0xDF => "DsWriteB128",
0xFE => "DsReadB96",
0xFF => "DsReadB128",
_ => string.Empty,
};
return FinishDecode(
name,
$"unknown-ds op=0x{opcode:X2} word=0x{word:X8}",
out error);
}
private static bool DecodeBuffer(
uint word,
string prefix,
out string name,
out uint sizeDwords,
out string error)
{
// GFX10 MIMG uses bit 0 as opcode bit 7. Treating the opcode as only
// bits 18..24 makes the 0x80+ family (including sample-adjust forms)
// decode as the corresponding low opcode.
var opcode = ((word >> 18) & 0x7F) | ((word & 1) << 7);
name = $"{prefix}Raw{opcode:X2}";
sizeDwords = 2;
error = string.Empty;
return true;
}
private static bool DecodeMtbuf(
uint word,
uint extra,
out string name,
out uint sizeDwords,
out string error)
{
// The fourth opcode bit lives in the second word and selects the D16 forms.
var opcode = ((word >> 16) & 0x7) | (((extra >> 21) & 1) << 3);
name = opcode switch
{
0x00 => "TBufferLoadFormatX",
0x01 => "TBufferLoadFormatXy",
0x02 => "TBufferLoadFormatXyz",
0x03 => "TBufferLoadFormatXyzw",
0x04 => "TBufferStoreFormatX",
0x05 => "TBufferStoreFormatXy",
0x06 => "TBufferStoreFormatXyz",
0x07 => "TBufferStoreFormatXyzw",
0x08 => "TBufferLoadFormatD16X",
0x09 => "TBufferLoadFormatD16Xy",
0x0A => "TBufferLoadFormatD16Xyz",
0x0B => "TBufferLoadFormatD16Xyzw",
0x0C => "TBufferStoreFormatD16X",
0x0D => "TBufferStoreFormatD16Xy",
0x0E => "TBufferStoreFormatD16Xyz",
0x0F => "TBufferStoreFormatD16Xyzw",
_ => string.Empty,
};
sizeDwords = (extra >> 24) == 0xFF ? 3u : 2u;
error = string.Empty;
return true;
}
private static bool DecodeMubuf(
uint word,
uint extra,
out string name,
out uint sizeDwords,
out string error)
{
// GFX10 MUBUF has eight opcode bits at 18..25; bit zero belongs to OFFSET.
var opcode = (word >> 18) & 0xFF;
name = opcode switch
{
0x00 => "BufferLoadFormatX",
0x01 => "BufferLoadFormatXy",
0x02 => "BufferLoadFormatXyz",
0x03 => "BufferLoadFormatXyzw",
0x04 => "BufferStoreFormatX",
0x05 => "BufferStoreFormatXy",
0x06 => "BufferStoreFormatXyz",
0x07 => "BufferStoreFormatXyzw",
0x08 => "BufferLoadUbyte",
0x09 => "BufferLoadSbyte",
0x0A => "BufferLoadUshort",
0x0B => "BufferLoadSshort",
0x0C => "BufferLoadDword",
0x0D => "BufferLoadDwordx2",
0x0E => "BufferLoadDwordx4",
0x0F => "BufferLoadDwordx3",
0x18 => "BufferStoreByte",
0x19 => "BufferStoreByteD16Hi",
0x1A => "BufferStoreShort",
0x1B => "BufferStoreShortD16Hi",
0x1C => "BufferStoreDword",
0x1D => "BufferStoreDwordx2",
0x1E => "BufferStoreDwordx4",
0x1F => "BufferStoreDwordx3",
0x20 => "BufferLoadUbyteD16",
0x21 => "BufferLoadUbyteD16Hi",
0x22 => "BufferLoadSbyteD16",
0x23 => "BufferLoadSbyteD16Hi",
0x24 => "BufferLoadShortD16",
0x25 => "BufferLoadShortD16Hi",
0x27 => "BufferStoreFormatD16HiX",
0x30 => "BufferAtomicSwap",
0x31 => "BufferAtomicCmpswap",
0x32 => "BufferAtomicAdd",
0x33 => "BufferAtomicSub",
0x35 => "BufferAtomicSmin",
0x36 => "BufferAtomicUmin",
0x37 => "BufferAtomicSmax",
0x38 => "BufferAtomicUmax",
0x39 => "BufferAtomicAnd",
0x3A => "BufferAtomicOr",
0x3B => "BufferAtomicXor",
0x3C => "BufferAtomicInc",
0x3D => "BufferAtomicDec",
0x3E => "BufferWbinvl1",
0x3F => "BufferAtomicFmin",
0x40 => "BufferAtomicFmax",
// GFX9 buffer atomic encodings used by the guest shaders.
0x41 => "BufferAtomicCmpswap",
0x42 => "BufferAtomicAdd",
0x43 => "BufferAtomicSub",
0x45 => "BufferAtomicSmin",
0x46 => "BufferAtomicUmin",
0x47 => "BufferAtomicSmax",
0x48 => "BufferAtomicUmax",
0x49 => "BufferAtomicAnd",
0x4A => "BufferAtomicOr",
0x4B => "BufferAtomicXor",
0x4C => "BufferAtomicInc",
0x50 => "BufferAtomicSwapX2",
0x5A => "BufferAtomicOrX2",
0x80 => "BufferLoadFormatD16X",
0x81 => "BufferLoadFormatD16Xy",
0x82 => "BufferLoadFormatD16Xyz",
0x83 => "BufferLoadFormatD16Xyzw",
0x84 => "BufferStoreFormatD16X",
0x85 => "BufferStoreFormatD16Xy",
0x86 => "BufferStoreFormatD16Xyz",
0x87 => "BufferStoreFormatD16Xyzw",
_ => $"MubufRaw{opcode:X2}",
};
sizeDwords = (extra >> 24) == 0xFF ? 3u : 2u;
error = string.Empty;
return true;
}
private static bool DecodeFlat(
uint word,
out string name,
out uint sizeDwords,
out string error)
{
var segment = (word >> 14) & 0x3;
var opcode = ((word >> 18) & 0x7F) | ((word & 1) << 7);
sizeDwords = 2;
error = string.Empty;
var prefix = segment switch
{
0x0 => "Flat",
0x1 => "Scratch",
0x2 => "Global",
_ => string.Empty,
};
var suffix = opcode switch
{
0x08 => "LoadUbyte",
0x09 => "LoadSbyte",
0x0A => "LoadUshort",
0x0B => "LoadSshort",
0x0C => "LoadDword",
0x0D => "LoadDwordx2",
0x0E => "LoadDwordx4",
0x0F => "LoadDwordx3",
0x18 => "StoreByte",
0x19 => "StoreByteD16Hi",
0x1A => "StoreShort",
0x1B => "StoreShortD16Hi",
0x1C => "StoreDword",
0x1D => "StoreDwordx2",
0x1E => "StoreDwordx4",
0x1F => "StoreDwordx3",
0x20 => "LoadUbyteD16",
0x21 => "LoadUbyteD16Hi",
0x22 => "LoadSbyteD16",
0x23 => "LoadSbyteD16Hi",
0x24 => "LoadShortD16",
0x25 => "LoadShortD16Hi",
0x32 => "AtomicAdd",
0x38 => "AtomicUMax",
_ => string.Empty,
};
name = prefix.Length != 0 && suffix.Length != 0
? prefix + suffix
: string.Empty;
return FinishDecode(
name,
$"unknown-flat segment=0x{segment:X1} op=0x{opcode:X2} word=0x{word:X8}",
out error);
}
private static bool DecodeSmrd(uint word, out string name, out uint sizeDwords, out string error)
{
var opcode = (word >> 22) & 0x1F;
var offset = word & 0xFF;
var immediateOffset = ((word >> 8) & 1) != 0;
sizeDwords = !immediateOffset && offset == 0xFF ? 2u : 1u;
error = string.Empty;
name = opcode switch
{
0x00 => "SLoadDword",
0x01 => "SLoadDwordx2",
0x02 => "SLoadDwordx4",
0x03 => "SLoadDwordx8",
0x04 => "SLoadDwordx16",
0x08 => "SBufferLoadDword",
0x09 => "SBufferLoadDwordx2",
0x0A => "SBufferLoadDwordx4",
0x0B => "SBufferLoadDwordx8",
0x0C => "SBufferLoadDwordx16",
0x15 => "ScratchStoreDword",
0x16 => "ScratchStoreDwordx2",
0x17 => "ScratchStoreDwordx4",
_ => string.Empty,
};
return FinishDecode(name, $"unknown-smrd op=0x{opcode:X2} word=0x{word:X8}", out error);
}
private static bool DecodeSmem(uint word, out string name, out uint sizeDwords, out string error)
{
var opcode = (word >> 18) & 0xFF;
sizeDwords = 2;
error = string.Empty;
name = opcode switch
{
0x00 => "SLoadDword",
0x01 => "SLoadDwordx2",
0x02 => "SLoadDwordx4",
0x03 => "SLoadDwordx8",
0x04 => "SLoadDwordx16",
0x08 => "SBufferLoadDword",
0x09 => "SBufferLoadDwordx2",
0x0A => "SBufferLoadDwordx4",
0x0B => "SBufferLoadDwordx8",
0x0C => "SBufferLoadDwordx16",
_ => string.Empty,
};
return FinishDecode(name, $"unknown-smem op=0x{opcode:X2}", out error);
}
private static bool DecodeMimg(uint word, out string name, out uint sizeDwords, out string error)
{
// RDNA2 MIMG OP[7] is bit 0 while OP[6:0] occupies bits 24:18.
// Ignoring bit 0 aliases 0xE6/0xE7 BVH operations to 0x66/0x67.
var opcode = ((word >> 18) & 0x7F) | ((word & 1) << 7);
sizeDwords = 2 + ((word >> 1) & 0x3);
error = string.Empty;
// Bit 0 (OPM) holds opcode bit 7.
var fullOpcode = opcode | ((word & 1) << 7);
if (fullOpcode is 0xE6 or 0xE7)
{
name = fullOpcode == 0xE6 ? "ImageBvhIntersectRay" : "ImageBvh64IntersectRay";
return true;
}
name = opcode switch
{
0x00 => "ImageLoad",
0x01 => "ImageLoadMip",
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",
// The float image atomics close the gfx10 atomic block. No
// backend emits them yet, so they still fail - but naming them
// lets the decoder finish the program, which is what makes the
// shader's bindings and global-memory usage visible in the trace.
0x1D => "ImageAtomicFcmpswap",
0x1E => "ImageAtomicFmin",
0x1F => "ImageAtomicFmax",
0x20 => "ImageSample",
0x21 => "ImageSampleCl",
0x22 => "ImageSampleD",
0x23 => "ImageSampleDCl",
0x24 => "ImageSampleL",
0x25 => "ImageSampleB",
0x26 => "ImageSampleBCl",
0x27 => "ImageSampleLz",
0x28 => "ImageSampleC",
0x29 => "ImageSampleCCl",
0x2A => "ImageSampleCD",
0x2B => "ImageSampleCDCl",
0x2C => "ImageSampleCL",
0x2D => "ImageSampleCB",
0x2E => "ImageSampleCBCl",
0x2F => "ImageSampleCLz",
0x30 => "ImageSampleO",
0x31 => "ImageSampleClO",
0x32 => "ImageSampleDO",
0x33 => "ImageSampleDClO",
0x34 => "ImageSampleLO",
0x35 => "ImageSampleBO",
0x36 => "ImageSampleBClO",
0x37 => "ImageSampleLzO",
0x38 => "ImageSampleCO",
0x39 => "ImageSampleCClO",
0x3A => "ImageSampleCDO",
0x3B => "ImageSampleCDClO",
0x3C => "ImageSampleCLO",
0x3D => "ImageSampleCBO",
0x3E => "ImageSampleCBClO",
0x3F => "ImageSampleCLzO",
0x40 => "ImageGather4",
0x47 => "ImageGather4Lz",
0x44 => "ImageGather4L",
0x48 => "ImageGather4C",
0x4C => "ImageGather4CL",
0x4E => "ImageGather4CBCl",
0x4F => "ImageGather4CLz",
0x57 => "ImageGather4LzO",
0x58 => "ImageGather4CO",
0x5F => "ImageGather4CLzO",
0x60 => "ImageGetLod",
0x61 => "ImageGather4H",
0x68 => "ImageSampleCd",
0x69 => "ImageSampleCdCl",
0x6A => "ImageSampleCCd",
0x6B => "ImageSampleCCdCl",
0x6C => "ImageSampleCdO",
0x6D => "ImageSampleCdClO",
0x6E => "ImageSampleCCdO",
0x6F => "ImageSampleCCdClO",
0xA0 => "ImageSampleA",
0xA1 => "ImageSampleClA",
0xA5 => "ImageSampleBA",
0xA6 => "ImageSampleBClA",
0xA8 => "ImageSampleCA",
0xA9 => "ImageSampleCClA",
0xAD => "ImageSampleCBA",
0xAE => "ImageSampleCBClA",
0xB0 => "ImageSampleAO",
0xB1 => "ImageSampleClAO",
0xB5 => "ImageSampleBAO",
0xB6 => "ImageSampleBClAO",
0xB8 => "ImageSampleCAO",
0xB9 => "ImageSampleCClAO",
0xBD => "ImageSampleCBAO",
0xBE => "ImageSampleCBClAO",
0xE6 => "ImageBvhIntersectRay",
0xE7 => "ImageBvh64IntersectRay",
_ => string.Empty,
};
return FinishDecode(name, $"unknown-mimg op=0x{opcode:X2}", out error);
}
private static bool DecodeVintrp(uint word, out string name, out uint sizeDwords, out string error)
{
var opcode = (word >> 16) & 0x3;
sizeDwords = 1;
error = string.Empty;
name = opcode switch
{
0x00 => "VInterpP1F32",
0x01 => "VInterpP2F32",
0x02 => "VInterpMovF32",
_ => string.Empty,
};
return FinishDecode(name, $"unknown-vintrp op=0x{opcode:X1}", out error);
}
private static bool HasDoubleVectorResult(string opcode) => opcode is
"VCvtF64I32" or "VCvtF64U32" or "VRcpF64" or "VRsqF64" or
"VSqrtF64" or "VMulF64" or "VFmaF64";
private static bool FinishDecode(string name, string decodeError, out string error)
{
error = string.Empty;
if (name.Length != 0)
{
return true;
}
error = decodeError;
return false;
}
private static string ClassifyInstruction(string name)
{
if (name.StartsWith("Image", StringComparison.Ordinal))
{
return "image";
}
if (name.StartsWith("Global", StringComparison.Ordinal))
{
return "global_memory";
}
if (name.StartsWith("VInterp", StringComparison.Ordinal))
{
return "interp";
}
if (string.Equals(name, "Exp", StringComparison.Ordinal))
{
return "export";
}
if (name.StartsWith("SLoad", StringComparison.Ordinal) ||
name.StartsWith("SBufferLoad", StringComparison.Ordinal))
{
return "scalar_load";
}
if (name.StartsWith('V'))
{
return "valu";
}
if (name.StartsWith('S'))
{
return "salu";
}
return "other";
}
private static bool IsMimgInstruction(string name) =>
name.StartsWith("Image", StringComparison.Ordinal);
public static bool IsImageLoadOperation(string name) =>
name.StartsWith("ImageLoad", StringComparison.Ordinal);
public static bool IsStorageImageOperation(string name) =>
name.StartsWith("ImageStore", StringComparison.Ordinal) ||
name.StartsWith("ImageAtomic", StringComparison.Ordinal);
public const uint IdentityImageDstSelect = 0xFACu;
public static uint GetImageDescriptorDstSelect(
IReadOnlyList<uint> resourceDescriptor) =>
resourceDescriptor.Count > 3
? resourceDescriptor[3] & 0xFFFu
: IdentityImageDstSelect;
/// <summary>
/// Gets the sequential store-source index for one physical image channel.
/// A negative result means that the channel must contain zero.
/// </summary>
public static int GetImageStoreSourceIndex(
uint dstSelect,
uint dmask,
int destinationComponent)
{
ArgumentOutOfRangeException.ThrowIfNegative(destinationComponent);
ArgumentOutOfRangeException.ThrowIfGreaterThan(destinationComponent, 3);
var logicalComponent = -1;
var physicalSelector = 4u + (uint)destinationComponent;
for (var component = 0; component < 4; component++)
{
if (((dstSelect >> (component * 3)) & 0x7u) == physicalSelector)
{
logicalComponent = component;
break;
}
}
if (logicalComponent < 0)
{
return -1;
}
var effectiveDmask = dmask & 0xFu;
if (effectiveDmask == 0)
{
effectiveDmask = 1;
}
if ((effectiveDmask & (1u << logicalComponent)) == 0)
{
return -1;
}
var sourceIndex = 0;
for (var component = 0; component < logicalComponent; component++)
{
sourceIndex += (int)((effectiveDmask >> component) & 1u);
}
return sourceIndex;
}
public static bool IsDataShareAtomic(string name) => name switch
{
"DsAddU32" or "DsSubU32" or "DsIncU32" or "DsDecU32" or
"DsMinI32" or "DsMaxI32" or "DsMinU32" or "DsMaxU32" or
"DsMinF32" or "DsMaxF32" 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" or
"DsAddU64" or "DsOrB64" => true,
_ => false,
};
private static Gen5ShaderInstruction ResolveFlatAddressBase(
IReadOnlyList<Gen5ShaderInstruction> precedingInstructions,
Gen5ShaderInstruction instruction)
{
if (instruction.Control is not Gen5GlobalMemoryControl
{
UsesFlatAddress: true,
} control ||
!TryFindVectorDefinition(
precedingInstructions,
control.VectorAddress,
out var lowDefinition) ||
!TryFindVectorDefinition(
precedingInstructions,
control.VectorAddress + 1,
out var highDefinition))
{
return instruction;
}
foreach (var lowSource in lowDefinition.Sources)
{
if (lowSource.Kind != Gen5OperandKind.ScalarRegister)
{
continue;
}
foreach (var highSource in highDefinition.Sources)
{
if (highSource.Kind != Gen5OperandKind.ScalarRegister ||
highSource.Value != lowSource.Value + 1)
{
continue;
}
return instruction with
{
Sources =
[
.. instruction.Sources,
Gen5Operand.Scalar(lowSource.Value),
],
Control = control with
{
ScalarAddress = lowSource.Value,
},
};
}
}
return instruction;
}
private static bool TryFindVectorDefinition(
IReadOnlyList<Gen5ShaderInstruction> instructions,
uint register,
out Gen5ShaderInstruction definition)
{
for (var index = instructions.Count - 1; index >= 0; index--)
{
var candidate = instructions[index];
foreach (var destination in candidate.Destinations)
{
if (destination.Kind == Gen5OperandKind.VectorRegister &&
destination.Value == register)
{
definition = candidate;
return true;
}
}
}
definition = default!;
return false;
}
private static Gen5ShaderInstruction CreateInstruction(
uint pc,
Gen5ShaderEncoding encoding,
string opcode,
uint[] words)
{
var word = words[0];
var isSdwa =
encoding is Gen5ShaderEncoding.Vop1 or Gen5ShaderEncoding.Vop2 or Gen5ShaderEncoding.Vopc &&
(word & 0x1FF) == 0xF9;
var isDpp =
encoding is Gen5ShaderEncoding.Vop1 or Gen5ShaderEncoding.Vop2 or Gen5ShaderEncoding.Vopc &&
(word & 0x1FF) == 0xFA;
var isDpp8 =
encoding is Gen5ShaderEncoding.Vop1 or Gen5ShaderEncoding.Vop2 or Gen5ShaderEncoding.Vopc &&
(word & 0x1FF) is 0xE9 or 0xEA;
var literal = !isSdwa && !isDpp && !isDpp8 &&
words.Length > MinimumEncodingDwords(encoding)
? words[^1]
: (uint?)null;
IReadOnlyList<Gen5Operand> sources = [];
IReadOnlyList<Gen5Operand> destinations = [];
Gen5InstructionControl? control = null;
switch (encoding)
{
case Gen5ShaderEncoding.Sopp when opcode == "STrap":
sources = [new Gen5Operand(Gen5OperandKind.LiteralConstant, word & 0xFF)];
break;
case Gen5ShaderEncoding.Sop1:
sources = [Gen5Operand.Source(word & 0xFF, literal)];
destinations = [Gen5Operand.Scalar((word >> 16) & 0x7F)];
if (opcode == "SCmovB64")
{
// CMOV is CSELECT with the old destination as the false
// source. Reuse its SCC, register-pair and EXEC handling.
opcode = "SCselectB64";
encoding = Gen5ShaderEncoding.Sop2;
sources = [sources[0], destinations[0]];
}
break;
case Gen5ShaderEncoding.Sop2:
sources =
[
Gen5Operand.Source(word & 0xFF, literal),
Gen5Operand.Source((word >> 8) & 0xFF, literal),
];
destinations = [Gen5Operand.Scalar((word >> 16) & 0x7F)];
break;
case Gen5ShaderEncoding.Sopc:
sources =
[
Gen5Operand.Source(word & 0xFF, literal),
Gen5Operand.Source((word >> 8) & 0xFF, literal),
];
break;
case Gen5ShaderEncoding.Sopk:
sources = [new Gen5Operand(Gen5OperandKind.EncodedConstant, word & 0xFFFF)];
if (opcode == "SWaitcnt")
{
var scalarSource = (word >> 16) & 0x7F;
if (scalarSource != 125)
{
sources = [.. sources, Gen5Operand.Scalar(scalarSource)];
}
}
else if (opcode == "SSetregB32")
{
sources =
[
Gen5Operand.Scalar((word >> 16) & 0x7F),
new Gen5Operand(Gen5OperandKind.EncodedConstant, word & 0xFFFF),
];
}
else
{
destinations = [Gen5Operand.Scalar((word >> 16) & 0x7F)];
}
break;
case Gen5ShaderEncoding.Smrd:
{
var scalarBase = ((word >> 9) & 0x3F) * 2;
var scalarDestination = (word >> 15) & 0x7F;
var immediate = ((word >> 8) & 1) != 0;
var offset = word & 0xFF;
if (opcode.StartsWith("ScratchStore", StringComparison.Ordinal))
{
var storeCount = ScalarMemoryDwordCount(opcode);
uint? storeDynamicOffsetRegister = null;
var storeImmediateOffsetBytes = 0;
if (immediate)
{
storeImmediateOffsetBytes = checked((int)(offset * sizeof(uint)));
}
else if (offset == 0xFF && literal.HasValue)
{
storeImmediateOffsetBytes = unchecked((int)literal.Value);
}
else
{
storeDynamicOffsetRegister = offset;
}
sources = [
Gen5Operand.Scalar(scalarBase),
.. Enumerable.Range(0, checked((int)storeCount)).Select(index =>
Gen5Operand.Scalar(scalarDestination + (uint)index)),
];
if (storeDynamicOffsetRegister.HasValue)
{
sources = [.. sources, Gen5Operand.Scalar(storeDynamicOffsetRegister.Value)];
}
destinations = [];
control = new Gen5GlobalMemoryControl(
storeCount,
0,
scalarDestination,
0,
scalarBase,
storeImmediateOffsetBytes,
false,
false,
false,
storeDynamicOffsetRegister,
true);
break;
}
var count = ScalarLoadDwordCount(opcode);
uint? dynamicOffsetRegister = null;
var immediateOffsetBytes = 0;
if (immediate)
{
immediateOffsetBytes = checked((int)(offset * sizeof(uint)));
}
else if (offset == 0xFF && literal.HasValue)
{
immediateOffsetBytes = unchecked((int)literal.Value);
}
else
{
dynamicOffsetRegister = offset;
}
sources = dynamicOffsetRegister.HasValue
? [Gen5Operand.Scalar(scalarBase), Gen5Operand.Scalar(dynamicOffsetRegister.Value)]
: [Gen5Operand.Scalar(scalarBase)];
destinations = Enumerable
.Range((int)scalarDestination, checked((int)count))
.Select(index => Gen5Operand.Scalar((uint)index))
.ToArray();
control = new Gen5ScalarMemoryControl(
count,
immediateOffsetBytes,
dynamicOffsetRegister);
break;
}
case Gen5ShaderEncoding.Smem:
{
var extra = words[1];
var scalarBase = (word & 0x3F) * 2;
var scalarDestination = (word >> 6) & 0x7F;
var scalarOffset = (extra >> 25) & 0x7F;
var offset = SignExtend(extra & 0x1FFFFF, 21);
var count = ScalarLoadDwordCount(opcode);
var scalarOffsetOperand = Gen5Operand.Source(scalarOffset);
var dynamicOffsetRegister = scalarOffsetOperand.Kind ==
Gen5OperandKind.ScalarRegister
? scalarOffsetOperand.Value
: (uint?)null;
sources =
[
Gen5Operand.Scalar(scalarBase),
scalarOffsetOperand,
];
destinations = Enumerable
.Range((int)scalarDestination, checked((int)count))
.Select(index => Gen5Operand.Scalar((uint)index))
.ToArray();
control = new Gen5ScalarMemoryControl(
count,
offset,
dynamicOffsetRegister);
break;
}
case Gen5ShaderEncoding.Vop1:
if (isDpp8)
{
var extra = words[1];
sources = [Gen5Operand.Vector(extra & 0xFF)];
control = new Gen5Dpp8Control(
extra >> 8,
(word & 0x1FF) == 0xEA);
}
else if (isDpp)
{
var extra = words[1];
sources = [Gen5Operand.Vector(extra & 0xFF)];
control = CreateDppControl(extra);
}
else if (isSdwa)
{
var extra = words[1];
var source0 = (extra & 0xFF) +
((((extra >> 23) & 1) == 0) ? 256u : 0u);
sources = [Gen5Operand.Source(source0)];
control = CreateSdwaControl(extra, isCompare: false, hasSource1: false);
}
else
{
sources = [Gen5Operand.Source(word & 0x1FF, literal)];
}
if (opcode == "VMovrelsB32")
{
sources = [sources[0], Gen5Operand.Scalar(124)];
}
// V_READFIRSTLANE_B32 is encoded as VOP1, but its destination
// field names an SGPR rather than a VGPR. Treating it like an
// ordinary vector destination leaves every invocation with a
// different value and corrupts scalar addresses derived from
// lane data.
var vop1Destination = (word >> 17) & 0xFF;
destinations = opcode == "VReadfirstlaneB32"
? [Gen5Operand.Scalar((word >> 17) & 0x7F)]
: HasDoubleVectorResult(opcode)
? [Gen5Operand.Vector(vop1Destination), Gen5Operand.Vector(vop1Destination + 1)]
: [Gen5Operand.Vector(vop1Destination)];
break;
case Gen5ShaderEncoding.Vop2:
if (isDpp8)
{
var extra = words[1];
sources =
[
Gen5Operand.Vector(extra & 0xFF),
Gen5Operand.Vector((word >> 9) & 0xFF),
];
control = new Gen5Dpp8Control(
extra >> 8,
(word & 0x1FF) == 0xEA);
}
else if (isDpp)
{
var extra = words[1];
sources =
[
Gen5Operand.Vector(extra & 0xFF),
Gen5Operand.Vector((word >> 9) & 0xFF),
];
control = CreateDppControl(extra);
}
else if (isSdwa)
{
var extra = words[1];
var source0 = (extra & 0xFF) + ((((extra >> 23) & 1) == 0) ? 256u : 0u);
var source1 =
((word >> 9) & 0xFF) +
((((extra >> 31) & 1) == 0) ? 256u : 0u);
sources =
[
Gen5Operand.Source(source0),
Gen5Operand.Source(source1),
];
control = CreateSdwaControl(extra, isCompare: false, hasSource1: true);
}
else
{
sources =
[
Gen5Operand.Source(word & 0x1FF, literal),
Gen5Operand.Vector((word >> 9) & 0xFF),
];
if ((opcode is "VMadMkF32" or "VFmaMkF32" or "VFmaMkF16") && literal.HasValue)
{
sources =
[
sources[0],
new Gen5Operand(Gen5OperandKind.LiteralConstant, literal.Value),
sources[1],
];
}
else if ((opcode is "VMadAkF32" or "VFmaAkF32" or "VFmaAkF16") && literal.HasValue)
{
sources =
[
.. sources,
new Gen5Operand(Gen5OperandKind.LiteralConstant, literal.Value),
];
}
}
destinations = [Gen5Operand.Vector((word >> 17) & 0xFF)];
break;
case Gen5ShaderEncoding.Vopc:
if (isDpp8)
{
var extra = words[1];
sources =
[
Gen5Operand.Vector(extra & 0xFF),
Gen5Operand.Vector((word >> 9) & 0xFF),
];
control = new Gen5Dpp8Control(
extra >> 8,
(word & 0x1FF) == 0xEA);
}
else if (isDpp)
{
var extra = words[1];
sources =
[
Gen5Operand.Vector(extra & 0xFF),
Gen5Operand.Vector((word >> 9) & 0xFF),
];
control = CreateDppControl(extra);
}
else if (isSdwa)
{
var extra = words[1];
var source0 = (extra & 0xFF) +
((((extra >> 23) & 1) == 0) ? 256u : 0u);
var source1 =
((word >> 9) & 0xFF) +
((((extra >> 31) & 1) == 0) ? 256u : 0u);
sources =
[
Gen5Operand.Source(source0),
Gen5Operand.Source(source1),
];
var sdwa = CreateSdwaControl(extra, isCompare: true, hasSource1: true);
control = sdwa;
if (sdwa.ScalarDestination is { } scalarDestination &&
scalarDestination != 106)
{
destinations = [Gen5Operand.Scalar(scalarDestination)];
}
}
else
{
sources =
[
Gen5Operand.Source(word & 0x1FF, literal),
Gen5Operand.Vector((word >> 9) & 0xFF),
];
}
break;
case Gen5ShaderEncoding.Vop3:
{
var extra = words[1];
sources =
[
Gen5Operand.Source(extra & 0x1FF, literal),
Gen5Operand.Source((extra >> 9) & 0x1FF, literal),
Gen5Operand.Source((extra >> 18) & 0x1FF, literal),
];
var vop3Destination = word & 0xFF;
destinations = HasDoubleVectorResult(opcode)
? [Gen5Operand.Vector(vop3Destination), Gen5Operand.Vector(vop3Destination + 1)]
: [Gen5Operand.Vector(vop3Destination)];
if (opcode == "VReadlaneB32")
{
// V_READLANE uses the VOP3A vdst byte even though the
// destination register is scalar. Bits 8-14 are the
// distinct sdst field used by VOP3B encodings.
destinations = [Gen5Operand.Scalar(word & 0xFF)];
}
var isVop3B = IsVop3BOpcode((word >> 16) & 0x3FF);
if (opcode.StartsWith("VCmp", StringComparison.Ordinal))
{
// VOP3 compares take two sources and write their mask to the SGPR pair in
// the vdst byte; GFX10 VCMPX writes EXEC only.
var isExecCompare = opcode.StartsWith("VCmpx", StringComparison.Ordinal);
sources = [sources[0], sources[1]];
destinations = isExecCompare ? [] : [Gen5Operand.Scalar(word & 0xFF)];
control = new Gen5Vop3Control(
(word >> 8) & 0x7,
(extra >> 29) & 0x7,
0,
false,
0,
isExecCompare ? null : word & 0xFF);
break;
}
control = new Gen5Vop3Control(
isVop3B ? 0 : (word >> 8) & 0x7,
(extra >> 29) & 0x7,
(extra >> 27) & 0x3,
((word >> 15) & 1) != 0,
isVop3B ? 0 : (word >> 11) & 0xF,
isVop3B ? (word >> 8) & 0x7F : null);
break;
}
case Gen5ShaderEncoding.Vop3p:
{
var extra = words[1];
sources =
[
Gen5Operand.Source(extra & 0x1FF, literal),
Gen5Operand.Source((extra >> 9) & 0x1FF, literal),
Gen5Operand.Source((extra >> 18) & 0x1FF, literal),
];
destinations = [Gen5Operand.Vector(word & 0xFF)];
// op_sel_hi is split across both dwords: bits [1:0] live in word1
// [28:27], bit [2] in word0 [14].
var opSelHi = ((extra >> 27) & 0x3) | (((word >> 14) & 0x1) << 2);
control = new Gen5Vop3pControl(
(word >> 11) & 0x7,
opSelHi,
(extra >> 29) & 0x7,
(word >> 8) & 0x7,
((word >> 15) & 1) != 0);
break;
}
case Gen5ShaderEncoding.Ds:
{
var extra = words[1];
var vectorAddress = extra & 0xFF;
var vectorData0 = (extra >> 8) & 0xFF;
var vectorData1 = (extra >> 16) & 0xFF;
var vectorDestination = (extra >> 24) & 0xFF;
// GFX10 DS: offset0 [7:0], offset1 [15:8], bit 16 reserved, GDS [17], op [25:18].
control = new Gen5DataShareControl(
word & 0xFF,
(word >> 8) & 0xFF,
((word >> 17) & 1) != 0);
sources = opcode switch
{
"DsAppend" or "DsConsume" or "DsReadAddtidB32" => [Gen5Operand.Scalar(124)],
"DsWriteAddtidB32" => [Gen5Operand.Scalar(124), Gen5Operand.Vector(vectorData0)],
"DsWriteB32" => [
Gen5Operand.Vector(vectorAddress),
Gen5Operand.Vector(vectorData0),
],
"DsWriteB64" => [
Gen5Operand.Vector(vectorAddress),
Gen5Operand.Vector(vectorData0),
Gen5Operand.Vector(vectorData0 + 1),
],
"DsWrite2B64" or "DsWrite2St64B64" => [
Gen5Operand.Vector(vectorAddress),
Gen5Operand.Vector(vectorData0),
Gen5Operand.Vector(vectorData0 + 1),
Gen5Operand.Vector(vectorData1),
Gen5Operand.Vector(vectorData1 + 1),
],
"DsWriteB96" => [
Gen5Operand.Vector(vectorAddress),
Gen5Operand.Vector(vectorData0),
Gen5Operand.Vector(vectorData0 + 1),
Gen5Operand.Vector(vectorData0 + 2),
],
"DsWriteB128" => [
Gen5Operand.Vector(vectorAddress),
Gen5Operand.Vector(vectorData0),
Gen5Operand.Vector(vectorData0 + 1),
Gen5Operand.Vector(vectorData0 + 2),
Gen5Operand.Vector(vectorData0 + 3),
],
"DsWrite2B32" or "DsWrite2St64B32" => [
Gen5Operand.Vector(vectorAddress),
Gen5Operand.Vector(vectorData0),
Gen5Operand.Vector(vectorData1),
],
"DsSwizzleB32" => [Gen5Operand.Vector(vectorData0)],
"DsBpermuteB32" => [
Gen5Operand.Vector(vectorAddress),
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),
],
"DsMskorB32" => [
Gen5Operand.Vector(vectorAddress),
Gen5Operand.Vector(vectorData0),
Gen5Operand.Vector(vectorData1),
],
// 64-bit LDS atomics take ADDR plus a DATA0 register pair.
"DsAddU64" or "DsOrB64" => [
Gen5Operand.Vector(vectorAddress),
Gen5Operand.Vector(vectorData0),
Gen5Operand.Vector(vectorData0 + 1),
],
// GFX10 DS_MIN/MAX_F32 use DATA0 as the replacement value and
// DATA1 as the floating-point compare operand.
"DsMinF32" or "DsMaxF32" => [
Gen5Operand.Vector(vectorAddress),
Gen5Operand.Vector(vectorData0),
Gen5Operand.Vector(vectorData1),
],
_ when IsDataShareAtomic(opcode) => [
Gen5Operand.Vector(vectorAddress),
Gen5Operand.Vector(vectorData0),
],
_ => [Gen5Operand.Vector(vectorAddress)],
};
destinations = opcode switch
{
"DsAppend" or "DsConsume" => [
Gen5Operand.Vector(vectorDestination),
],
"DsReadB32" or "DsReadI8" or "DsReadAddtidB32" or
"DsSwizzleB32" or "DsBpermuteB32" => [
Gen5Operand.Vector(vectorDestination),
],
"DsReadB64" or "DsRead2B32" or "DsRead2St64B32" => [
Gen5Operand.Vector(vectorDestination),
Gen5Operand.Vector(vectorDestination + 1),
],
"DsReadB96" => [
Gen5Operand.Vector(vectorDestination),
Gen5Operand.Vector(vectorDestination + 1),
Gen5Operand.Vector(vectorDestination + 2),
],
"DsReadB128" or "DsRead2B64" => [
Gen5Operand.Vector(vectorDestination),
Gen5Operand.Vector(vectorDestination + 1),
Gen5Operand.Vector(vectorDestination + 2),
Gen5Operand.Vector(vectorDestination + 3),
],
_ when IsDataShareAtomic(opcode) &&
opcode.Contains("Rtn", StringComparison.Ordinal) => [
Gen5Operand.Vector(vectorDestination),
],
_ => [],
};
break;
}
case Gen5ShaderEncoding.Vintrp:
sources = [Gen5Operand.Vector(word & 0xFF)];
destinations = [Gen5Operand.Vector((word >> 18) & 0xFF)];
control = new Gen5InterpolationControl(
(word >> 10) & 0x3F,
(word >> 8) & 0x3);
break;
case Gen5ShaderEncoding.Flat:
{
var extra = words[1];
var vectorAddress = extra & 0xFF;
var sourceVectorRegister = (extra >> 8) & 0xFF;
var destinationVectorRegister = (extra >> 24) & 0xFF;
var scalarAddress = (extra >> 16) & 0x7F;
var usesFlatAddress = opcode.StartsWith(
"Flat",
StringComparison.Ordinal);
var usesScratchAddress = opcode.StartsWith(
"Scratch",
StringComparison.Ordinal);
var memoryOpcode = usesFlatAddress
? "Global" + opcode["Flat".Length..]
: usesScratchAddress
? "Global" + opcode["Scratch".Length..]
: opcode;
var dwordCount = memoryOpcode switch
{
"GlobalLoadUbyte" or
"GlobalLoadSbyte" or
"GlobalLoadUshort" or
"GlobalLoadSshort" or
"GlobalLoadUbyteD16" or
"GlobalLoadUbyteD16Hi" or
"GlobalLoadSbyteD16" or
"GlobalLoadSbyteD16Hi" or
"GlobalLoadShortD16" or
"GlobalLoadShortD16Hi" or
"GlobalStoreByte" or
"GlobalStoreByteD16Hi" or
"GlobalStoreShort" or
"GlobalStoreShortD16Hi" or
"GlobalStoreDword" or
"GlobalAtomicAdd" or
"GlobalAtomicUMax" => 1u,
"GlobalLoadDword" => 1u,
"GlobalLoadDwordx2" => 2u,
"GlobalLoadDwordx3" => 3u,
"GlobalLoadDwordx4" => 4u,
"GlobalStoreDwordx2" => 2u,
"GlobalStoreDwordx3" => 3u,
"GlobalStoreDwordx4" => 4u,
_ => 0u,
};
sources = usesFlatAddress
?
[
Gen5Operand.Vector(vectorAddress),
Gen5Operand.Vector(vectorAddress + 1),
]
: usesScratchAddress && scalarAddress < 125
? [Gen5Operand.Scalar(scalarAddress)]
: usesScratchAddress
? [Gen5Operand.Vector(vectorAddress)]
:
[
Gen5Operand.Vector(vectorAddress),
Gen5Operand.Scalar(scalarAddress),
];
var isLoad = memoryOpcode.StartsWith("GlobalLoad", StringComparison.Ordinal);
var isStore = memoryOpcode.StartsWith("GlobalStore", StringComparison.Ordinal);
var isAtomic = memoryOpcode.StartsWith("GlobalAtomic", StringComparison.Ordinal);
var globallyCoherent = ((word >> 16) & 1) != 0;
if (isStore || isAtomic)
{
sources = [.. sources, .. Enumerable
.Range((int)sourceVectorRegister, checked((int)dwordCount))
.Select(index => Gen5Operand.Vector((uint)index))];
}
else if (isLoad && memoryOpcode.Contains("D16", StringComparison.Ordinal))
{
// A partial load preserves the other half of the destination.
sources = [.. sources, Gen5Operand.Vector(destinationVectorRegister)];
}
destinations = isLoad || (isAtomic && globallyCoherent)
? Enumerable
.Range((int)destinationVectorRegister, checked((int)dwordCount))
.Select(index => Gen5Operand.Vector((uint)index))
.ToArray()
: [];
control = new Gen5GlobalMemoryControl(
dwordCount,
vectorAddress,
sourceVectorRegister,
destinationVectorRegister,
usesFlatAddress ? uint.MaxValue : scalarAddress,
SignExtend(word & 0x1FFF, 13),
globallyCoherent,
((word >> 17) & 1) != 0,
usesFlatAddress);
break;
}
case Gen5ShaderEncoding.Mubuf:
{
var extra = words[1];
var vectorAddress = extra & 0xFF;
var vectorData = (extra >> 8) & 0xFF;
var scalarResource = ((extra >> 16) & 0x1F) * 4;
var scalarOffset = (extra >> 24) & 0xFF;
var dwordCount = opcode switch
{
"BufferLoadFormatX" => 1u,
"BufferLoadFormatXy" => 2u,
"BufferLoadFormatXyz" => 3u,
"BufferLoadFormatXyzw" => 4u,
"BufferStoreFormatX" => 1u,
"BufferStoreFormatXy" => 2u,
"BufferStoreFormatXyz" => 3u,
"BufferStoreFormatXyzw" => 4u,
"BufferLoadFormatD16X" or "BufferLoadFormatD16Xy" or
"BufferStoreFormatD16X" or "BufferStoreFormatD16Xy" => 1u,
"BufferLoadFormatD16Xyz" or "BufferLoadFormatD16Xyzw" or
"BufferStoreFormatD16Xyz" or "BufferStoreFormatD16Xyzw" => 2u,
"BufferLoadUbyte" or
"BufferLoadSbyte" or
"BufferLoadUshort" or
"BufferLoadSshort" or
"BufferStoreByte" or
"BufferStoreByteD16Hi" or
"BufferStoreShort" or
"BufferStoreShortD16Hi" or
"BufferLoadUbyteD16" or
"BufferLoadUbyteD16Hi" or
"BufferLoadSbyteD16" or
"BufferLoadSbyteD16Hi" or
"BufferLoadShortD16" or
"BufferLoadShortD16Hi" => 1u,
"BufferLoadDword" => 1u,
"BufferLoadDwordx2" => 2u,
"BufferLoadDwordx3" => 3u,
"BufferLoadDwordx4" => 4u,
"BufferStoreDword" => 1u,
"BufferStoreDwordx2" => 2u,
"BufferStoreDwordx3" => 3u,
"BufferStoreDwordx4" => 4u,
"BufferAtomicCmpswap" or "BufferAtomicSwapX2" or "BufferAtomicOrX2" => 2u,
_ when opcode.StartsWith("BufferAtomic", StringComparison.Ordinal) => 1u,
_ => 0u,
};
sources =
[
Gen5Operand.Vector(vectorAddress),
Gen5Operand.Scalar(scalarResource),
Gen5Operand.Source(scalarOffset, literal),
];
destinations = Enumerable
.Range((int)vectorData, checked((int)dwordCount))
.Select(index => Gen5Operand.Vector((uint)index))
.ToArray();
control = new Gen5BufferMemoryControl(
dwordCount,
vectorAddress,
vectorData,
scalarResource,
(int)(word & 0xFFF),
((word >> 13) & 1) != 0,
((word >> 12) & 1) != 0,
((word >> 14) & 1) != 0,
((extra >> 22) & 1) != 0,
PackedD16: opcode.Contains("FormatD16", StringComparison.Ordinal) && !opcode.Contains("Hi", StringComparison.Ordinal),
FormatComponentCount: opcode.Contains("FormatD16", StringComparison.Ordinal) && !opcode.Contains("Hi", StringComparison.Ordinal)
? opcode.EndsWith("Xyzw", StringComparison.Ordinal) ? 4u
: opcode.EndsWith("Xyz", StringComparison.Ordinal) ? 3u
: opcode.EndsWith("Xy", StringComparison.Ordinal) ? 2u : 1u
: 0u);
break;
}
case Gen5ShaderEncoding.Mtbuf:
{
var extra = words[1];
var vectorAddress = extra & 0xFF;
var vectorData = (extra >> 8) & 0xFF;
var scalarResource = ((extra >> 16) & 0x1F) * 4;
var scalarOffset = (extra >> 24) & 0xFF;
var dwordCount = opcode switch
{
"TBufferLoadFormatX" or "TBufferStoreFormatX" => 1u,
"TBufferLoadFormatXy" or "TBufferStoreFormatXy" => 2u,
"TBufferLoadFormatXyz" or "TBufferStoreFormatXyz" => 3u,
"TBufferLoadFormatXyzw" or "TBufferStoreFormatXyzw" => 4u,
"TBufferLoadFormatD16X" or "TBufferLoadFormatD16Xy" or
"TBufferStoreFormatD16X" or "TBufferStoreFormatD16Xy" => 1u,
"TBufferLoadFormatD16Xyz" or "TBufferLoadFormatD16Xyzw" or
"TBufferStoreFormatD16Xyz" or "TBufferStoreFormatD16Xyzw" => 2u,
_ => 0u,
};
sources =
[
Gen5Operand.Vector(vectorAddress),
Gen5Operand.Scalar(scalarResource),
Gen5Operand.Source(scalarOffset, literal),
];
destinations = Enumerable
.Range((int)vectorData, checked((int)dwordCount))
.Select(index => Gen5Operand.Vector((uint)index))
.ToArray();
control = new Gen5BufferMemoryControl(
dwordCount,
vectorAddress,
vectorData,
scalarResource,
(int)(word & 0xFFF),
((word >> 13) & 1) != 0,
((word >> 12) & 1) != 0,
((word >> 14) & 1) != 0,
((extra >> 22) & 1) != 0,
Typed: true,
TypedFormat: (word >> 19) & 0x7F,
PackedD16: opcode.Contains("D16", StringComparison.Ordinal),
FormatComponentCount: opcode.Contains("D16", StringComparison.Ordinal)
? opcode.EndsWith("Xyzw", StringComparison.Ordinal) ? 4u
: opcode.EndsWith("Xyz", StringComparison.Ordinal) ? 3u
: opcode.EndsWith("Xy", StringComparison.Ordinal) ? 2u : 1u
: 0u);
break;
}
case Gen5ShaderEncoding.Mimg:
{
var extra = words[1];
var vectorAddress = extra & 0xFF;
var vectorData = (extra >> 8) & 0xFF;
var scalarResource = ((extra >> 16) & 0x1F) * 4;
var scalarSampler = ((extra >> 21) & 0x1F) * 4;
var addressRegisters = new List<uint>(1 + Math.Max(0, words.Length - 2) * 4)
{
vectorAddress,
};
for (var wordIndex = 2; wordIndex < words.Length; wordIndex++)
{
for (var shift = 0; shift < 32; shift += 8)
{
addressRegisters.Add((words[wordIndex] >> shift) & 0xFF);
}
}
if (opcode.StartsWith("ImageBvh", StringComparison.Ordinal))
{
var a16 = ((extra >> 30) & 1) != 0;
var rayControl = new Gen5RayIntersectControl(vectorAddress, addressRegisters, vectorData, scalarResource, a16);
// Node pointer (2 dwords for BVH64), extent, origin, then direction and
// inverse direction, which A16 packs as halves into three dwords.
var addressCount = (opcode == "ImageBvh64IntersectRay" ? 2 : 1) + 1 + 3 + (a16 ? 3 : 6);
var raySources = new List<Gen5Operand>(addressCount + 1);
for (var component = 0; component < addressCount; component++)
{
raySources.Add(Gen5Operand.Vector(rayControl.GetAddressRegister(component)));
}
raySources.Add(Gen5Operand.Scalar(scalarResource));
sources = raySources;
destinations = Enumerable
.Range((int)vectorData, (int)Gen5RayIntersectControl.ResultDwords)
.Select(index => Gen5Operand.Vector((uint)index))
.ToArray();
control = rayControl;
break;
}
var imageSources = new List<Gen5Operand>(addressRegisters.Count + 2);
foreach (var addressRegister in addressRegisters)
{
imageSources.Add(Gen5Operand.Vector(addressRegister));
}
imageSources.Add(Gen5Operand.Scalar(scalarResource));
imageSources.Add(Gen5Operand.Scalar(scalarSampler));
sources = imageSources;
destinations = opcode.StartsWith("ImageStore", StringComparison.Ordinal)
? []
: opcode is "ImageBvhIntersectRay" or "ImageBvh64IntersectRay"
? Enumerable.Range((int)vectorData, 4)
.Select(index => Gen5Operand.Vector((uint)index))
.ToArray()
: [Gen5Operand.Vector(vectorData)];
var dimension = (word >> 3) & 0x7;
control = new Gen5ImageControl(
(word >> 8) & 0xF,
vectorAddress,
addressRegisters,
vectorData,
scalarResource,
scalarSampler,
dimension,
dimension is 4 or 5 or 7,
((word >> 13) & 1) != 0,
((word >> 25) & 1) != 0,
((extra >> 30) & 1) != 0,
((extra >> 31) & 1) != 0);
break;
}
case Gen5ShaderEncoding.Exp:
{
var extra = words[1];
sources =
[
Gen5Operand.Vector(extra & 0xFF),
Gen5Operand.Vector((extra >> 8) & 0xFF),
Gen5Operand.Vector((extra >> 16) & 0xFF),
Gen5Operand.Vector((extra >> 24) & 0xFF),
];
control = new Gen5ExportControl(
(word >> 4) & 0x3F,
word & 0xF,
((word >> 10) & 1) != 0,
((word >> 11) & 1) != 0,
((word >> 12) & 1) != 0);
break;
}
}
return new Gen5ShaderInstruction(pc, encoding, opcode, words, sources, destinations, control);
}
private static Gen5DppControl CreateDppControl(uint word) =>
new(
(word >> 8) & 0x1FF,
((word >> 18) & 1) != 0,
((word >> 19) & 1) != 0,
((word >> 21) & 1) | (((word >> 23) & 1) << 1),
((word >> 20) & 1) | (((word >> 22) & 1) << 1),
(word >> 24) & 0xF,
(word >> 28) & 0xF);
private static Gen5SdwaControl CreateSdwaControl(
uint word,
bool isCompare,
bool hasSource1)
{
var scalarDestination = isCompare
? ((word >> 15) & 1) != 0
? (word >> 8) & 0x7Fu
: 106u
: (uint?)null;
return new Gen5SdwaControl(
isCompare ? 6u : (word >> 8) & 0x7u,
isCompare ? 0u : (word >> 11) & 0x3u,
(word >> 16) & 0x7u,
hasSource1 ? (word >> 24) & 0x7u : 6u,
((word >> 19) & 1) != 0,
hasSource1 && ((word >> 27) & 1) != 0,
((word >> 21) & 1) | (hasSource1 ? ((word >> 29) & 1) << 1 : 0),
((word >> 20) & 1) | (hasSource1 ? ((word >> 28) & 1) << 1 : 0),
isCompare ? 0u : (word >> 14) & 0x3u,
!isCompare && ((word >> 13) & 1) != 0,
scalarDestination);
}
private static int MinimumEncodingDwords(Gen5ShaderEncoding encoding) => encoding switch
{
Gen5ShaderEncoding.Vop3 or
Gen5ShaderEncoding.Smem or
Gen5ShaderEncoding.Mubuf or
Gen5ShaderEncoding.Mtbuf or
Gen5ShaderEncoding.Ds or
Gen5ShaderEncoding.Flat or
Gen5ShaderEncoding.Vop3p or
Gen5ShaderEncoding.Mimg or
Gen5ShaderEncoding.Exp => 2,
_ => 1,
};
private static uint ScalarLoadDwordCount(string opcode) => opcode switch
{
"SLoadDword" or "SBufferLoadDword" => 1,
"SLoadDwordx2" or "SBufferLoadDwordx2" => 2,
"SLoadDwordx4" or "SBufferLoadDwordx4" => 4,
"SLoadDwordx8" or "SBufferLoadDwordx8" => 8,
"SLoadDwordx16" or "SBufferLoadDwordx16" => 16,
_ => 0,
};
private static uint ScalarMemoryDwordCount(string opcode) => opcode switch
{
"ScratchStoreDword" => 1,
"ScratchStoreDwordx2" => 2,
"ScratchStoreDwordx4" => 4,
_ => 0,
};
private static int SignExtend(uint value, int bits)
{
var shift = 32 - bits;
return (int)(value << shift) >> shift;
}
private static void AddFeatureCount(Dictionary<string, int> counts, string key)
{
counts.TryGetValue(key, out var count);
counts[key] = count + 1;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
return true;
}
private readonly record struct ShaderDecodeInfo(
int InstructionCount,
Dictionary<string, int> Counts,
Dictionary<string, int> FeatureCounts,
Dictionary<string, int> MimgCounts,
List<string> Details)
{
public static ShaderDecodeInfo Create(Gen5ShaderProgram program)
{
var counts = new Dictionary<string, int>(StringComparer.Ordinal);
var featureCounts = new Dictionary<string, int>(StringComparer.Ordinal);
var mimgCounts = new Dictionary<string, int>(StringComparer.Ordinal);
var details = new List<string>();
foreach (var instruction in program.Instructions)
{
AddFeatureCount(counts, instruction.Opcode);
AddFeatureCount(featureCounts, ClassifyInstruction(instruction.Opcode));
if (instruction.Control is Gen5ImageControl image)
{
AddFeatureCount(mimgCounts, $"{instruction.Opcode}/dmask=0x{image.Dmask:X}");
}
if (details.Count < 16 && DescribeInstruction(instruction) is { } detail)
{
details.Add(detail);
}
}
return new ShaderDecodeInfo(
program.Instructions.Count,
counts,
featureCounts,
mimgCounts,
details);
}
public override string ToString()
{
var builder = new StringBuilder();
builder.Append("ins=");
builder.Append(InstructionCount);
AppendCounts(builder, " features=", FeatureCounts, 16);
builder.Append(" ops=");
AppendCounts(builder, string.Empty, Counts, 128);
AppendCounts(builder, " mimg=", MimgCounts, 12);
AppendDetails(builder, Details, 10);
return builder.ToString();
}
private static string? DescribeInstruction(Gen5ShaderInstruction instruction)
{
if (instruction.Control is Gen5ImageControl image)
{
var addressRegisters = string.Join(
'/',
image.AddressRegisters.Select(register => $"v{register}"));
return
$"{instruction.Opcode}@0x{instruction.Pc:X}:dm=0x{image.Dmask:X}," +
$"va={addressRegisters},vd=v{image.VectorData}," +
$"sr=s{image.ScalarResource},ss=s{image.ScalarSampler}," +
$"dim={image.Dimension},da={(image.IsArray ? 1 : 0)}," +
$"a16={(image.A16 ? 1 : 0)},d16={(image.D16 ? 1 : 0)}," +
$"glc={(image.Glc ? 1 : 0)}," +
$"slc={(image.Slc ? 1 : 0)}";
}
if (instruction.Control is Gen5ExportControl export)
{
return
$"Exp@0x{instruction.Pc:X}:target=0x{export.Target:X}," +
$"en=0x{export.EnableMask:X},compr={(export.Compressed ? 1 : 0)}," +
$"done={(export.Done ? 1 : 0)},vm={(export.ValidMask ? 1 : 0)}," +
$"src={string.Join('/', instruction.Sources)}";
}
if (instruction.Control is Gen5InterpolationControl interpolation)
{
return
$"{instruction.Opcode}@0x{instruction.Pc:X}:" +
$"attr={interpolation.Attribute},chan={interpolation.Channel}," +
$"src={instruction.Sources[0]},dst={instruction.Destinations[0]}";
}
return null;
}
private static void AppendCounts(
StringBuilder builder,
string prefix,
Dictionary<string, int> counts,
int limit)
{
if (counts.Count == 0)
{
return;
}
builder.Append(prefix);
var written = 0;
foreach (var (name, count) in counts)
{
if (written != 0)
{
builder.Append(',');
}
builder.Append(name);
builder.Append(':');
builder.Append(count);
written++;
if (written == limit && counts.Count > written)
{
builder.Append(",...");
break;
}
}
}
private static void AppendDetails(StringBuilder builder, List<string> details, int limit)
{
if (details.Count == 0)
{
return;
}
builder.Append(" detail=");
var written = 0;
foreach (var detail in details)
{
if (written != 0)
{
builder.Append(';');
}
builder.Append(detail);
written++;
if (written == limit && details.Count > written)
{
builder.Append(";...");
break;
}
}
}
}
}