From ecf019f23b01eed22bdb642f9810397b86e82519 Mon Sep 17 00:00:00 2001 From: kuba Date: Sat, 3 Oct 2026 20:47:07 +0200 Subject: [PATCH] [SpirvTranslator] Read half-selected V_FMA_MIX literals as f16 op_sel_hi marks a V_FMA_MIX operand as f16 for literals too, but literals were always read as f32 bit patterns. Silent Hill's deferred lighting weights a luminance with the f16 literals 0x34CD/0x38B8/0x2F0A (0.30, 0.59, 0.11); as f32 denormals they made the luminance zero, the shader clamped it to 1e-4 and divided by it, and the title-menu scene came out thousands of times too bright, which auto exposure then turned black. --- .../Gen5SpirvTranslator.Alu.cs | 9 +++-- .../Gpu/Vulkan/FloatSemanticsDeviceTests.cs | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs index fca2dca..ba5c220 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs @@ -1778,8 +1778,11 @@ public static partial class Gen5SpirvTranslator } // 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 + // register or literal operand is taken as an f16 (the half picked by op_sel, + // widened exactly to f32) or as a full f32; inline constants keep their value + // either way. A literal is an f16 bit pattern then: reading 0x34CD as an f32 + // made Silent Hill's luminance weights denormal zeros, and the lighting that + // divides by that luminance came out thousands of times too bright. 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. @@ -1791,7 +1794,7 @@ public static partial class Gen5SpirvTranslator var source = instruction.Sources[index]; var readAsHalf = ((control.OpSelHiMask >> index) & 1) != 0 && - source.Kind is Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister; + source.Kind is Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister or Gen5OperandKind.LiteralConstant; uint value; if (readAsHalf) diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/FloatSemanticsDeviceTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/FloatSemanticsDeviceTests.cs index 9f7e37b..165e049 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/FloatSemanticsDeviceTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/FloatSemanticsDeviceTests.cs @@ -122,5 +122,45 @@ public sealed class FloatSemanticsDeviceTests(HeadlessVulkanFixture fixture) : I } } + // op_sel_hi marks a V_FMA_MIX operand as f16, literals included: 0x34CD is 0.3, not an + // f32 denormal. Silent Hill weights a luminance with such literals and divides by it. + [Fact] + public void FusedMixReadsHalfSelectedLiteralsAsHalfFloats() + { + var vulkan = fixture.Vulkan; + if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return; + var halfLiteral = new Gen5Vop3pControl(0, 0b001, 0, 0, false); + var floatLiteral = new Gen5Vop3pControl(0, 0b000, 0, 0, false); + var program = Program( + Vop1(0, "VCvtF32U32", 1, Gen5Operand.Vector(0)), + Vop3(4, "VFmaMixF32", 12, new Gen5Operand(Gen5OperandKind.LiteralConstant, 0x34CD), Gen5Operand.Vector(1), Operand(0)) with { Control = halfLiteral }, + Vop3(16, "VFmaMixF32", 13, new Gen5Operand(Gen5OperandKind.LiteralConstant, Bits(0.25f)), Gen5Operand.Vector(1), Operand(0)) with { Control = floatLiteral }, + Vop2(28, "VLshlrevB32", 7, Operand(5), Gen5Operand.Vector(0)), + BufferAccess(32, "BufferStoreDword", 4, vectorData: 12, offsetEnabled: true, vectorAddress: 7), + BufferAccess(40, "BufferStoreDword", 4, offset: 4, vectorData: 13, offsetEnabled: true, vectorAddress: 7), + EndProgram(48)); + var (plan, resources, layout) = Prepare(program); + var request = new ShaderCompileRequest(plan, resources, layout) + { + LocalSizeX = ThreadCount, ThreadCountX = ThreadCount, WaveSize = 32, + }; + Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); + using var harness = new ImageTestHarness(vulkan); + using var runner = new LayoutComputeRunner(harness, request, shader.Spirv); + var result = runner.CreateBuffer(ThreadCount * RowBytes); + var registers = new uint[256]; + registers[6] = ThreadCount * RowBytes; + harness.Run(() => runner.Dispatch(registers, + new Dictionary { [DescriptorBindingKind.Buffers] = [result] }, 1)); + var actual = runner.ReadBack(result, 0, ThreadCount * RowBytes); + var weight = (float)BitConverter.UInt16BitsToHalf(0x34CD); + for (var lane = 0u; lane < ThreadCount; lane++) + { + var row = actual.AsSpan((int)(lane * RowBytes)); + Assert.Equal(Bits(MathF.FusedMultiplyAdd(weight, lane, 0f)), BinaryPrimitives.ReadUInt32LittleEndian(row)); + Assert.Equal(Bits(lane * 0.25f), BinaryPrimitives.ReadUInt32LittleEndian(row[4..])); + } + } + private static uint Bits(float value) => BitConverter.SingleToUInt32Bits(value); }