[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.
This commit is contained in:
parent
e65b2e8233
commit
ecf019f23b
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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, GpuBuffer[]> { [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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue