[AGC] Decode VOP3P and emit packed f16 arithmetic (first slice) (#145)

* [AGC] Decode VOP3P and emit packed f16 arithmetic (first slice)

On gfx10 the VOP3P family lives under its own 0b110011000 prefix (word0
top byte 0xCC), which the major-opcode switch currently routes to the
SMEM branch, so packed instructions were decoded as scalar memory ops
and emitted as silent no-ops. Intercept the exact 9-bit prefix ahead of
the switch, decode the five packed-f16 arithmetic opcodes with their
op_sel/op_sel_hi/neg_lo/neg_hi/clamp modifiers, and emit them as
UnpackHalf2x16 -> component-wise f32 vec2 ops -> PackHalf2x16 so no
Float16 capability is needed.

Bit layout and opcode numbers pinned to LLVM MC test encodings
(vop3p.s, gfx10_vop3p_literalv216.txt) and VOP3PInstructions.td.
Unsupported modifiers, packed constants and out-of-scope packed opcodes
fail with a clear error instead of emitting wrong results.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AGC] Make packed f16 exact and drop v_pk_fma_f16 (review response)

Address the FP16 correctness review on the VOP3P slice.

Replace GLSL UnpackHalf2x16/PackHalf2x16 with explicit integer f16<->f32
conversions (EmitHalfToFloat/EmitFloatToHalf): exact widening with subnormal
normalisation, and narrowing with round-to-nearest-even, overflow-to-Inf and
NaN/Inf handling. Their subnormal and rounding behaviour no longer depends on
implementation-defined float-controls modes.

With exact conversions, v_pk_add_f16 and v_pk_mul_f16 are bit-exact to a true
f16 op (f32 result rounds losslessly to f16; a f16 product fits in f32). Emit
v_pk_min_f16/v_pk_max_f16 as fminnum_like/fmaxnum_like (NaN operand returns the
other; ordered numeric compare) instead of GLSL FMin/FMax.

v_pk_fma_f16 now fails emission loudly: a fused f16 FMA rounds once, an f32
multiply-add then pack double-rounds (fma(0x4100,0x7522,0x04EA) is 0x7A6B fused
vs 0x7A6A via f32). Exact fused emulation is a planned follow-up slice.

ShaderDump gains an Expect model (Translates/DecodeFails/EmitFails) and packed
regressions: arith, non-default modifiers, and loud-failure pins for the fma
case above and for clamp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: tensorcrush <tensorcrush@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Antigravity AI <antigravity@gemini.com>
This commit is contained in:
tensorcrush
2026-07-17 17:58:22 +02:00
committed by GitHub
parent fbafd3f429
commit 847371d2de
3 changed files with 308 additions and 0 deletions
@@ -237,6 +237,17 @@ public sealed record Gen5SdwaControl(
bool Clamp,
uint? ScalarDestination) : Gen5InstructionControl;
// Packed (VOP3P) source and destination modifiers. Each mask holds one bit per
// source operand. OpSel/OpSelHi pick which 16-bit half of a source feeds the low
// and high result lanes respectively; NegLo/NegHi negate the value routed to each
// lane. Clamp saturates each output half to [0, 1].
public sealed record Gen5Vop3pControl(
uint OpSelMask,
uint OpSelHiMask,
uint NegLoMask,
uint NegHiMask,
bool Clamp) : Gen5InstructionControl;
public sealed record Gen5DppControl(
uint Control,
bool FetchInactive,
@@ -562,6 +562,22 @@ public static class Gen5ShaderTranslator
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:
@@ -1160,6 +1176,37 @@ public static class Gen5ShaderTranslator
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.
// Unhandled packed opcodes (integer, fma_mix, ...) stay opaque here and
// fail loudly at emission rather than being silently mis-emitted.
name = opcode switch
{
0x0E => "VPkFmaF16",
0x0F => "VPkAddF16",
0x10 => "VPkMulF16",
0x11 => "VPkMinF16",
0x12 => "VPkMaxF16",
_ => $"Vop3pRaw{opcode:X2}",
};
return true;
}
private static bool DecodeDs(
uint word,
out string name,
@@ -1839,6 +1886,28 @@ public static class Gen5ShaderTranslator
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];