From 52ba493155ecd024a52df14589115b17805cd52f Mon Sep 17 00:00:00 2001 From: kuba Date: Sat, 3 Oct 2026 20:26:22 +0200 Subject: [PATCH] [SpirvTranslator] Saturate V_CVT_PKRTZ_F16_F32 at 65504 instead of overflowing to infinity Rounding toward zero never produces infinity from a finite value: past the f16 range the result is +-65504. We truncated the mantissa and let packHalf2x16 round, so large values became infinite. Silent Hill's deferred lighting packs title-menu values past 65504; its height fog then multiplies scene color by a tiny transmittance, which keeps 65504 finite but leaves an infinity infinite, a later pass turns it into NaN and the whole 3D menu background renders black. Subnormal results now also keep whole 2^-24 steps. A MoltenVK device test covers overflow, truncation, subnormals, NaN and inf. --- .../Gen5SpirvTranslator.Alu.cs | 29 +++++++-- .../Gpu/Vulkan/FloatSemanticsDeviceTests.cs | 61 +++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs index ba5c220..62618d2 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs @@ -5202,12 +5202,33 @@ public static partial class Gen5SpirvTranslator return Bitcast(_uintType, value); } + // Rounds an f32 toward zero onto an f16 value, so packHalf2x16 then encodes it exactly + // (V_CVT_PKRTZ_F16_F32). Toward zero, a finite value beyond the f16 range becomes + // +-65504, never infinity: Silent Hill's deferred lighting packs values past 65504 and + // its fog pass then scales them down, while an infinity stays infinite, turns into NaN + // and blacks out the whole title-menu scene. Subnormal results keep whole 2^-24 steps. private uint TruncateFloat32ForPack(uint value) { - var raw = BitwiseAnd( - Bitcast(_uintType, value), - UInt(0xFFFF_E000)); - return Bitcast(_floatType, raw); + var normal = Bitcast( + _floatType, + BitwiseAnd(Bitcast(_uintType, Ext(43, _floatType, value, Float(-65504f), Float(65504f))), UInt(0xFFFF_E000))); + var subnormal = _module.AddInstruction( + SpirvOp.FMul, + _floatType, + Ext(3, _floatType, _module.AddInstruction(SpirvOp.FMul, _floatType, value, Float(16777216f))), + Float(1f / 16777216f)); + var isSubnormal = _module.AddInstruction( + SpirvOp.FOrdLessThan, + _boolType, + Ext(4, _floatType, value), + Float(6.10351562e-05f)); + var finite = _module.AddInstruction(SpirvOp.Select, _floatType, isSubnormal, subnormal, normal); + var special = _module.AddInstruction( + SpirvOp.LogicalOr, + _boolType, + _module.AddInstruction(SpirvOp.IsNan, _boolType, value), + _module.AddInstruction(SpirvOp.IsInf, _boolType, value)); + return _module.AddInstruction(SpirvOp.Select, _floatType, special, value, finite); } private uint Ext(uint operation, uint resultType, params uint[] operands) diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/FloatSemanticsDeviceTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/FloatSemanticsDeviceTests.cs index 165e049..c2c66b9 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/FloatSemanticsDeviceTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/FloatSemanticsDeviceTests.cs @@ -162,5 +162,66 @@ public sealed class FloatSemanticsDeviceTests(HeadlessVulkanFixture fixture) : I } } + // V_CVT_PKRTZ_F16_F32 rounds toward zero: finite values past the f16 range saturate at + // +-65504 instead of becoming infinity, and subnormal results keep whole 2^-24 steps. + [Fact] + public void PackToHalfRoundsTowardZeroWithoutOverflowingToInfinity() + { + var vulkan = fixture.Vulkan; + if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return; + var program = Program( + Vop1(0, "VCvtF32U32", 1, Gen5Operand.Vector(0)), + MoveScalar(4, 8, Bits(5003.7f)), + MoveScalar(12, 9, Bits(-80000.3f)), + MoveScalar(20, 10, Bits(3.3e-6f)), + MoveScalar(28, 11, Bits(-5e-5f)), + MoveScalar(36, 12, Bits(float.NaN)), + MoveScalar(44, 13, Bits(float.PositiveInfinity)), + Vop3(52, "VMadF32", 2, Gen5Operand.Vector(1), Gen5Operand.Scalar(8), Gen5Operand.Scalar(9)), + Vop3(60, "VMadF32", 3, Gen5Operand.Vector(1), Gen5Operand.Scalar(10), Gen5Operand.Scalar(11)), + Vop2(68, "VCvtPkrtzF16F32", 12, Gen5Operand.Vector(2), Gen5Operand.Vector(3)), + Vop3(72, "VCvtPkrtzF16F32", 13, Gen5Operand.Scalar(12), Gen5Operand.Scalar(13)), + Vop2(80, "VLshlrevB32", 7, Operand(5), Gen5Operand.Vector(0)), + BufferAccess(84, "BufferStoreDword", 4, vectorData: 12, offsetEnabled: true, vectorAddress: 7), + BufferAccess(92, "BufferStoreDword", 4, offset: 4, vectorData: 13, offsetEnabled: true, vectorAddress: 7), + EndProgram(100)); + 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); + for (var lane = 0u; lane < ThreadCount; lane++) + { + var row = actual.AsSpan((int)(lane * RowBytes)); + var large = MathF.FusedMultiplyAdd(lane, 5003.7f, -80000.3f); + var small = MathF.FusedMultiplyAdd(lane, 3.3e-6f, -5e-5f); + var packed = BinaryPrimitives.ReadUInt32LittleEndian(row); + Assert.Equal(HalfTowardZero(large), packed & 0xFFFF); + Assert.Equal(HalfTowardZero(small), packed >> 16); + var special = BinaryPrimitives.ReadUInt32LittleEndian(row[4..]); + Assert.True((special & 0x7C00) == 0x7C00 && (special & 0x3FF) != 0, $"lane {lane}: 0x{special:X8}"); + Assert.Equal(0x7C00u, special >> 16); + } + } + + private static uint HalfTowardZero(float value) + { + var sign = value < 0 || (value == 0 && float.IsNegative(value)) ? 0x8000u : 0u; + var magnitude = MathF.Abs(value); + if (magnitude >= 65504f) return sign | 0x7BFF; + if (magnitude < 6.10351562e-05f) return sign | (uint)Math.Floor(magnitude * 16777216.0); + var bits = BitConverter.SingleToUInt32Bits(magnitude); + return sign | ((((bits >> 23) - 127 + 15) << 10) | ((bits >> 13) & 0x3FF)); + } + private static uint Bits(float value) => BitConverter.SingleToUInt32Bits(value); }