mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-05 17:39:53 +08:00
Shader: lower VOP3P V_FMA_MIX_F32/LO/HI (was dropping Unity HDR shaders) (#466)
The decoder recognises the VOP3P mix ops (0x20 V_FMA_MIX_F32, 0x21 V_FMA_MIXLO_F16, 0x22 V_FMA_MIXHI_F16) but left them opaque (Vop3pRaw20/21/22), so at SPIR-V emission they fell through the vector-ALU switch to the default and failed with "unsupported vector opcode". A single unhandled instruction fails the whole compile, so any shader using fma_mix was dropped entirely. Unity's built-in-RP / PostProcessing v2 HDR, tone-mapping and auto-exposure shaders emit V_FMA_MIX_F32, so those passes never translated (this is what kept Superliminal's auto-exposure luminance chain from running). Name the three opcodes in DecodeVop3p (like the packed v_pk_* ops) and lower them in the SPIR-V translator. Each mix op computes a single f32 fma(a, b, c) where every source is read *independently* as either a full f32 register/constant or one f16 half widened to f32. Per operand, op_sel_hi selects f16-vs-f32 and op_sel picks which f16 half; the neg_hi field is repurposed as an absolute-value modifier and neg negates, applied abs-then-neg. This reuses the VOP3P op_sel/op_sel_hi/neg/neg_hi bit layout with the mix-specific meaning, not the packed-math meaning. The result is a scalar f32 for V_FMA_MIX_F32; _MIXLO/_MIXHI narrow it back to f16 (exact round-to-nearest-even, via the existing EmitFloatToHalf) and write it into the low/high 16 bits of vdst, preserving the other half. The clamp modifier saturates to [0, 1] consistently with the other VOP3P ops. Per-operand F16/F32 select and the abs/neg modifiers follow shadPS4's GetSrcMix, the authoritative reference for the mix semantics. Adds Gen5FmaMixSpirvTests: assembles V_FMA_MIX_F32 (with a representative op_sel/op_sel_hi/neg/abs) and V_FMA_MIXLO_F16 compute shaders and asserts they translate to GPU SPIR-V without hitting the drop path and emit a GLSL.std.450 Fma (and an FAbs for the neg_hi modifier). Both fail against the pre-fix tree with "unsupported vector opcode Vop3pRaw20/21".
This commit is contained in:
@@ -959,6 +959,15 @@ public static partial class Gen5SpirvTranslator
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "VFmaMixF32":
|
||||||
|
case "VFmaMixloF16":
|
||||||
|
case "VFmaMixhiF16":
|
||||||
|
if (!TryEmitFmaMix(instruction, destination, out result, out error))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
error = $"unsupported vector opcode {instruction.Opcode}";
|
error = $"unsupported vector opcode {instruction.Opcode}";
|
||||||
@@ -1033,6 +1042,103 @@ public static partial class Gen5SpirvTranslator
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// V_FMA_MIX_F32 / _MIXLO_F16 / _MIXHI_F16 (VOP3P opcodes 0x20 / 0x21 /
|
||||||
|
// 0x22). Unlike the packed v_pk_* ops these compute a single f32
|
||||||
|
// fma(a, b, c): each of the three sources is *independently* read as
|
||||||
|
// either a full f32 register/constant or one f16 half widened to f32,
|
||||||
|
// selected per operand by op_sel_hi (read as f16 when set) and op_sel
|
||||||
|
// (which half feeds the f32). For the mix ops the VOP3P neg_hi field is
|
||||||
|
// the absolute-value modifier and neg negates, applied abs-then-neg to
|
||||||
|
// match the hardware and shadPS4's GetSrcMix. _MIXLO / _MIXHI round the
|
||||||
|
// f32 result back to f16 and write it into the low / high 16 bits of
|
||||||
|
// vdst, leaving the other half intact.
|
||||||
|
private bool TryEmitFmaMix(
|
||||||
|
Gen5ShaderInstruction instruction,
|
||||||
|
uint destination,
|
||||||
|
out uint result,
|
||||||
|
out string error)
|
||||||
|
{
|
||||||
|
result = 0;
|
||||||
|
error = string.Empty;
|
||||||
|
if (instruction.Control is not Gen5Vop3pControl control)
|
||||||
|
{
|
||||||
|
error = $"missing vop3p control for {instruction.Opcode}";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var product = Bitcast(
|
||||||
|
_uintType,
|
||||||
|
Ext(
|
||||||
|
50,
|
||||||
|
_floatType,
|
||||||
|
EmitFmaMixOperand(instruction, control, 0),
|
||||||
|
EmitFmaMixOperand(instruction, control, 1),
|
||||||
|
EmitFmaMixOperand(instruction, control, 2)));
|
||||||
|
if (control.Clamp)
|
||||||
|
{
|
||||||
|
product = EmitClampToUnitInterval(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instruction.Opcode == "VFmaMixF32")
|
||||||
|
{
|
||||||
|
result = product;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// _MIXLO / _MIXHI: narrow to f16 and merge into one half of vdst.
|
||||||
|
var half = EmitFloatToHalf(product);
|
||||||
|
var existing = LoadV(destination);
|
||||||
|
result = instruction.Opcode == "VFmaMixloF16"
|
||||||
|
? BitwiseOr(BitwiseAnd(existing, UInt(0xFFFF_0000)), half)
|
||||||
|
: BitwiseOr(
|
||||||
|
BitwiseAnd(existing, UInt(0x0000_FFFF)),
|
||||||
|
ShiftLeftLogical(half, UInt(16)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads one V_FMA_MIX source as an f32. op_sel_hi selects whether a
|
||||||
|
// register operand is taken as an f16 (the half picked by op_sel, widened
|
||||||
|
// exactly to f32) or as a full f32; inline constants are always f32. The
|
||||||
|
// per-operand neg_hi bit takes the absolute value and neg negates, in that
|
||||||
|
// order (abs-then-neg), reusing the VOP3P modifier fields the way the mix
|
||||||
|
// ops define them rather than the packed low/high-lane meaning.
|
||||||
|
private uint EmitFmaMixOperand(
|
||||||
|
Gen5ShaderInstruction instruction,
|
||||||
|
Gen5Vop3pControl control,
|
||||||
|
int index)
|
||||||
|
{
|
||||||
|
var source = instruction.Sources[index];
|
||||||
|
var readAsHalf =
|
||||||
|
((control.OpSelHiMask >> index) & 1) != 0 &&
|
||||||
|
source.Kind is Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister;
|
||||||
|
|
||||||
|
uint value;
|
||||||
|
if (readAsHalf)
|
||||||
|
{
|
||||||
|
var raw = GetRawSource(instruction, index);
|
||||||
|
var half = ((control.OpSelMask >> index) & 1) != 0
|
||||||
|
? ShiftRightLogical(raw, UInt(16))
|
||||||
|
: raw;
|
||||||
|
value = Bitcast(_floatType, EmitHalfToFloat(half));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
value = GetFloatSource(instruction, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (((control.NegHiMask >> index) & 1) != 0)
|
||||||
|
{
|
||||||
|
value = Ext(4, _floatType, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (((control.NegLoMask >> index) & 1) != 0)
|
||||||
|
{
|
||||||
|
value = _module.AddInstruction(SpirvOp.FNegate, _floatType, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
// Computes one result lane (low or high) as a packed 16-bit f16 value.
|
// Computes one result lane (low or high) as a packed 16-bit f16 value.
|
||||||
// The op runs in f32 and its result is narrowed back to f16 exactly (see
|
// The op runs in f32 and its result is narrowed back to f16 exactly (see
|
||||||
// EmitFloatToHalf). When the clamp modifier is set the pre-narrowing f32
|
// EmitFloatToHalf). When the clamp modifier is set the pre-narrowing f32
|
||||||
|
|||||||
@@ -1192,8 +1192,10 @@ public static class Gen5ShaderTranslator
|
|||||||
|
|
||||||
// Opcode numbers taken from LLVM's AMDGPU VOP3PInstructions.td and the
|
// Opcode numbers taken from LLVM's AMDGPU VOP3PInstructions.td and the
|
||||||
// gfx9/gfx10 MC test encodings; they are unchanged across gfx9 and gfx10.
|
// gfx9/gfx10 MC test encodings; they are unchanged across gfx9 and gfx10.
|
||||||
// Unhandled packed opcodes (integer, fma_mix, ...) stay opaque here and
|
// The mix ops (0x20/0x21/0x22) are V_MAD_MIX_* on gfx9 and V_FMA_MIX_*
|
||||||
// fail loudly at emission rather than being silently mis-emitted.
|
// (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
|
name = opcode switch
|
||||||
{
|
{
|
||||||
0x0E => "VPkFmaF16",
|
0x0E => "VPkFmaF16",
|
||||||
@@ -1201,6 +1203,9 @@ public static class Gen5ShaderTranslator
|
|||||||
0x10 => "VPkMulF16",
|
0x10 => "VPkMulF16",
|
||||||
0x11 => "VPkMinF16",
|
0x11 => "VPkMinF16",
|
||||||
0x12 => "VPkMaxF16",
|
0x12 => "VPkMaxF16",
|
||||||
|
0x20 => "VFmaMixF32",
|
||||||
|
0x21 => "VFmaMixloF16",
|
||||||
|
0x22 => "VFmaMixhiF16",
|
||||||
_ => $"Vop3pRaw{opcode:X2}",
|
_ => $"Vop3pRaw{opcode:X2}",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.ShaderCompiler;
|
||||||
|
using SharpEmu.ShaderCompiler.Vulkan;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Agc;
|
||||||
|
|
||||||
|
// Regression tests for the VOP3P mix ops V_FMA_MIX_F32 / _MIXLO_F16 / _MIXHI_F16
|
||||||
|
// (opcodes 0x20 / 0x21 / 0x22). The decoder leaves any unlowered VOP3P opcode
|
||||||
|
// opaque (Vop3pRaw20/21/22); before these were lowered they hit the vector-ALU
|
||||||
|
// switch default and failed emission ("unsupported vector opcode"), which drops
|
||||||
|
// the whole shader. Unity HDR / tone-mapping / auto-exposure shaders use
|
||||||
|
// V_FMA_MIX_F32 and so failed to translate entirely.
|
||||||
|
//
|
||||||
|
// Each mix op computes a single f32 fma(a, b, c) where every source is read
|
||||||
|
// *independently* as either a full f32 register or one f16 half widened to f32,
|
||||||
|
// selected per operand by op_sel_hi (f16 when set) and op_sel (which half). The
|
||||||
|
// mix ops also repurpose the VOP3P neg_hi field as an absolute-value modifier.
|
||||||
|
public sealed class Gen5FmaMixSpirvTests
|
||||||
|
{
|
||||||
|
private const ulong ShaderAddress = 0x1_0000_0000;
|
||||||
|
|
||||||
|
// GLSL.std.450 extended-instruction numbers used by the lowering.
|
||||||
|
private const uint GlslFma = 50;
|
||||||
|
private const uint GlslFAbs = 4;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FmaMixF32_TranslatesToFmaAndDoesNotDropShader()
|
||||||
|
{
|
||||||
|
// V_FMA_MIX_F32 v3, v0, v1, v2
|
||||||
|
// op_sel_hi = 0b011 -> src0/src1 read as f16, src2 as full f32
|
||||||
|
// op_sel = 0b010 -> src1 takes its high f16 half (src0 low half)
|
||||||
|
// neg_hi = 0b001 -> abs(src0)
|
||||||
|
// neg = 0b100 -> -src2
|
||||||
|
// Reaching TryCompileComputeShader == true already proves the shader is no
|
||||||
|
// longer dropped at the VOP3P default error path.
|
||||||
|
var spirv = Compile([0xCC201103u, 0x9C0A0300u]);
|
||||||
|
|
||||||
|
Assert.True(
|
||||||
|
ContainsExtInst(spirv, GlslFma),
|
||||||
|
"V_FMA_MIX_F32 must lower to a GLSL.std.450 Fma");
|
||||||
|
Assert.True(
|
||||||
|
ContainsExtInst(spirv, GlslFAbs),
|
||||||
|
"the neg_hi modifier on a mix source must lower to an FAbs (abs-then-neg)");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FmaMixLoF16_TranslatesWithoutDroppingShader()
|
||||||
|
{
|
||||||
|
// V_FMA_MIXLO_F16 v3, v0, v1, v2 with op_sel_hi = 0b111 (all sources read
|
||||||
|
// as f16 low halves). The f32 fma result is narrowed to f16 and merged
|
||||||
|
// into the low 16 bits of vdst; the fma itself is still emitted.
|
||||||
|
var spirv = Compile([0xCC214003u, 0x1C0A0300u]);
|
||||||
|
|
||||||
|
Assert.True(
|
||||||
|
ContainsExtInst(spirv, GlslFma),
|
||||||
|
"V_FMA_MIXLO_F16 must still lower its multiply-add to a GLSL.std.450 Fma");
|
||||||
|
}
|
||||||
|
|
||||||
|
// True when the module contains an OpExtInst selecting the given GLSL.std.450
|
||||||
|
// instruction number.
|
||||||
|
private static bool ContainsExtInst(byte[] spirv, uint instruction)
|
||||||
|
{
|
||||||
|
foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv))
|
||||||
|
{
|
||||||
|
// OpExtInst = 12: (opcode, resultType, resultId, set, instruction, ...).
|
||||||
|
if (op != 12 || wordCount < 5)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ReadWord(spirv, offset + 16) == instruction)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<(ushort Op, int WordCount, int Offset)> EnumerateInstructions(
|
||||||
|
byte[] spirv)
|
||||||
|
{
|
||||||
|
// 5-word SPIR-V header, then (wordCount << 16 | opcode) packed instructions.
|
||||||
|
for (var offset = 5 * sizeof(uint); offset + sizeof(uint) <= spirv.Length;)
|
||||||
|
{
|
||||||
|
var word = ReadWord(spirv, offset);
|
||||||
|
var wordCount = (int)(word >> 16);
|
||||||
|
if (wordCount <= 0)
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return ((ushort)word, wordCount, offset);
|
||||||
|
offset += wordCount * sizeof(uint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint ReadWord(byte[] spirv, int offset) =>
|
||||||
|
BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset, sizeof(uint)));
|
||||||
|
|
||||||
|
private static byte[] Compile(uint[] programWords)
|
||||||
|
{
|
||||||
|
var memory = new FakeCpuMemory(ShaderAddress, 0x2000);
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
Gen5ShaderAtomicDecodeTests.WriteProgram(memory, ShaderAddress, programWords);
|
||||||
|
var shaderRegisters = new Dictionary<uint, uint>
|
||||||
|
{
|
||||||
|
[Gen5ShaderAtomicDecodeTests.ComputePgmRsrc2Register] = 16u << 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.True(
|
||||||
|
Gen5ShaderTranslator.TryCreateState(
|
||||||
|
ctx,
|
||||||
|
ShaderAddress,
|
||||||
|
0,
|
||||||
|
shaderRegisters,
|
||||||
|
Gen5ShaderAtomicDecodeTests.ComputeUserDataRegister,
|
||||||
|
out var state,
|
||||||
|
out var error),
|
||||||
|
error);
|
||||||
|
Assert.True(
|
||||||
|
Gen5ShaderScalarEvaluator.TryEvaluate(ctx, state, out var evaluation, out error),
|
||||||
|
error);
|
||||||
|
Assert.True(
|
||||||
|
Gen5SpirvTranslator.TryCompileComputeShader(
|
||||||
|
state, evaluation, 1, 1, 1, out var shader, out error),
|
||||||
|
error);
|
||||||
|
return shader.Spirv;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user