From 0a726416785fdb16d9372b5fc4080b6223060371 Mon Sep 17 00:00:00 2001 From: Claxten Date: Tue, 27 Oct 2026 01:43:04 +0100 Subject: [PATCH] shader: evaluate saturating float-to-signed conversions in runtime descriptor values --- .../shader/recompiler/ir/passes/SrtWalker.cpp | 18 ++++++++++++ tests/ScalarProvenanceTests.cpp | 29 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp b/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp index 314bf2e4a..43de7e5c3 100644 --- a/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp +++ b/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp @@ -82,6 +82,7 @@ bool IsRuntimeUniformOp(ValueOpcode op) { case ValueOpcode::BitCastU32F32: case ValueOpcode::BitCastF32U32: case ValueOpcode::ConvertU32F32: + case ValueOpcode::ConvertS32F32: case ValueOpcode::ConvertF32U32: case ValueOpcode::CompositeConstructU64: case ValueOpcode::CompositeExtractU64: @@ -1038,6 +1039,23 @@ void WalkerCompiler::EmitStrict(const Node& node) { c.L(done); break; } + case ValueOpcode::ConvertS32F32: { + // The conversion saturates and maps NaN to zero. cvttss2si's out-of-range result, + // INT32_MIN, is already right for negative overflow. + Xbyak::Label done; + c.movd(c.xmm0, c.eax); + c.xor_(c.eax, c.eax); + c.ucomiss(c.xmm0, c.xmm0); + c.jp(done, Near); + c.mov(c.ecx, 0x4f000000u); + c.movd(c.xmm1, c.ecx); + c.mov(c.eax, INT32_MAX); + c.ucomiss(c.xmm0, c.xmm1); + c.jae(done, Near); + c.cvttss2si(c.eax, c.xmm0); + c.L(done); + break; + } case ValueOpcode::FPMul32: c.movd(c.xmm0, c.eax); c.movd(c.xmm1, c.ecx); diff --git a/tests/ScalarProvenanceTests.cpp b/tests/ScalarProvenanceTests.cpp index de59a8cd6..44f15237a 100644 --- a/tests/ScalarProvenanceTests.cpp +++ b/tests/ScalarProvenanceTests.cpp @@ -515,6 +515,34 @@ void TestFloatComparisonDescriptorInputs() { } } +void TestSignedConversionDescriptorInputs() { + // 3.75 and -3.75 truncate, the largest float below 2^31 converts exactly, 2^31 and +inf + // saturate up, -2^31 and -1e10 saturate down, and -0.0, a denormal and both NaNs read zero. + constexpr std::array, 11> inputs = {{ + {0x40700000u, 3u}, {0xc0700000u, 0xfffffffdu}, {0x4effffffu, 0x7fffff80u}, + {0x4f000000u, 0x7fffffffu}, {0x7f800000u, 0x7fffffffu}, {0xcf000000u, 0x80000000u}, + {0xd01502f9u, 0x80000000u}, {0x80000000u, 0u}, {0x00000001u, 0u}, + {0x7fc00000u, 0u}, {0xffc00000u, 0u}}}; + Fixture fixture; + const auto user = fixture.Emit( + ValueOpcode::GetUserData, {Value(static_cast(2))}); + const auto converted = fixture.Emit( + ValueOpcode::ConvertS32F32, {fixture.Emit(ValueOpcode::BitCastF32U32, {user})}); + Check(ValidateRuntimeValue(fixture.program, converted), + "signed float conversion was rejected as a runtime value"); + fixture.program.descriptor_sources.push_back( + {.dwords = {converted}, .dword_count = 1}); + + auto plan = ExtractResourcePlan(fixture.program); + for (const auto [bits, expected] : inputs) { + const std::array user_data{bits}; + DescriptorValue result; + Check(EvaluateSource(plan, {.user_data = user_data}, 0, result) && + result.dwords[0] == expected, + "descriptor signed conversion disagrees with the native conversion"); + } +} + void TestSharedIntegerRuntimeDependencies() { Fixture fixture; const auto lane = fixture.Emit(ValueOpcode::LaneId); @@ -888,6 +916,7 @@ int main() { TestRuntime64BitDescriptorOps(); TestUniformFirstLaneSamplerLod(); TestFloatComparisonDescriptorInputs(); + TestSignedConversionDescriptorInputs(); TestSharedIntegerRuntimeDependencies(); TestConstantBufferBounds(); TestReadLaneElimination();