Files
sharpemu/tests/SharpEmu.Libs.Tests/Agc/Gen5FmaMixSpirvTests.cs
T
kuba 3574a3b145 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".
2026-07-20 14:37:40 +03:00

136 lines
5.0 KiB
C#

// 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;
}
}