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