From 59d8256b361759734fce9f5c0891fc866ef9806f Mon Sep 17 00:00:00 2001 From: nmzik Date: Sun, 25 Oct 2026 01:39:01 +0200 Subject: [PATCH] shader: encode 11/10-bit float formatted buffer stores as truncated unsigned floats --- .../backend/spirv/spirvEmitterAlu.cpp | 50 +++++++++---------- .../backend/spirv/spirvEmitterInternal.h | 1 + .../spirv/spirvEmitterMemoryHelpers.cpp | 11 ++++ tests/ShaderRecompilerComputeTests.cpp | 12 +++++ 4 files changed, 49 insertions(+), 25 deletions(-) diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAlu.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAlu.cpp index 5245c6e7c..bfb530681 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAlu.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAlu.cpp @@ -54,9 +54,30 @@ uint32_t EmitExt(EmitterState& state, uint32_t type, uint32_t opcode, return result; } -uint32_t EmitF32ToF16RtzBits(EmitterState& state, uint32_t value) { +uint32_t EmitF32ToU32(EmitterState& state, uint32_t src, bool signed_value) { + // Clamping first keeps the truncating conversion defined; NaN and values past the largest + // float below the integer limit are selected afterwards. + const auto clamped = + EmitExt(state, TypeF32(state), GLSLstd450NClamp, + {src, ConstantF32(state, signed_value ? 0xcf000000u : 0u), + ConstantF32(state, signed_value ? 0x4effffffu : 0x4f7fffffu)}); + const auto converted = signed_value + ? Unary(state, spv::OpBitcast, TypeU32(state), + Unary(state, spv::OpConvertFToS, TypeI32(state), clamped)) + : Unary(state, spv::OpConvertFToU, TypeU32(state), clamped); + const auto above = Binary(state, spv::OpFOrdGreaterThanEqual, TypeBool(state), src, + ConstantF32(state, signed_value ? 0x4f000000u : 0x4f800000u)); + const auto high = + Select(state, TypeU32(state), above, + ConstantU32(state, signed_value ? 0x7fffffffu : 0xffffffffu), converted); + return Select(state, TypeU32(state), Unary(state, spv::OpIsNan, TypeBool(state), src), + ConstantU32(state, 0), high); +} + +} // namespace + +uint32_t EmitF32BitsToF16RtzBits(EmitterState& state, uint32_t bits) { // Native half conversion causes regressions; use compact integer RTZ conversion. - const auto bits = EmitBitcastF32ToU32(state, value); const auto abs = EmitAndConstant(state, bits, 0x7fffffffu); const auto sign = EmitAndConstant( state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), bits, ConstantU32(state, 16)), @@ -92,28 +113,6 @@ uint32_t EmitF32ToF16RtzBits(EmitterState& state, uint32_t value) { return EmitOrU32(state, sign, magnitude); } -uint32_t EmitF32ToU32(EmitterState& state, uint32_t src, bool signed_value) { - // Clamping first keeps the truncating conversion defined; NaN and values past the largest - // float below the integer limit are selected afterwards. - const auto clamped = - EmitExt(state, TypeF32(state), GLSLstd450NClamp, - {src, ConstantF32(state, signed_value ? 0xcf000000u : 0u), - ConstantF32(state, signed_value ? 0x4effffffu : 0x4f7fffffu)}); - const auto converted = signed_value - ? Unary(state, spv::OpBitcast, TypeU32(state), - Unary(state, spv::OpConvertFToS, TypeI32(state), clamped)) - : Unary(state, spv::OpConvertFToU, TypeU32(state), clamped); - const auto above = Binary(state, spv::OpFOrdGreaterThanEqual, TypeBool(state), src, - ConstantF32(state, signed_value ? 0x4f000000u : 0x4f800000u)); - const auto high = - Select(state, TypeU32(state), above, - ConstantU32(state, signed_value ? 0x7fffffffu : 0xffffffffu), converted); - return Select(state, TypeU32(state), Unary(state, spv::OpIsNan, TypeBool(state), src), - ConstantU32(state, 0), high); -} - -} // namespace - uint32_t EmitFPFma32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) { const auto result = EmitGlsl(state, a, b, c); state.builder.AddAnnotation(spv::OpDecorate, result, spv::DecorationNoContraction); @@ -199,7 +198,8 @@ uint32_t EmitCompositeExtractU32x2(EmitterState& state, uint32_t arg0, IR::Value uint32_t EmitPackFloat2x16Rtz(EmitterState& state, uint32_t arg0, uint32_t arg1) { return EmitNative( - state, EmitF32ToF16RtzBits(state, arg0), EmitF32ToF16RtzBits(state, arg1), + state, EmitF32BitsToF16RtzBits(state, EmitBitcastF32ToU32(state, arg0)), + EmitF32BitsToF16RtzBits(state, EmitBitcastF32ToU32(state, arg1)), ConstantU32(state, 16), ConstantU32(state, 16)); } diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h index 87c069bbf..afe403111 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h @@ -531,6 +531,7 @@ inline constexpr auto EmitFNegateValue = EmitNative; uint32_t EmitF16BitsToF32(EmitterState& state, uint32_t bits); +uint32_t EmitF32BitsToF16RtzBits(EmitterState& state, uint32_t bits); void EmitProgram(EmitterState& state); diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemoryHelpers.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemoryHelpers.cpp index 223ff9eaf..282277bcb 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemoryHelpers.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemoryHelpers.cpp @@ -472,6 +472,17 @@ uint32_t EncodeFormattedStoreComponent(EmitterState& state, const Format::Buffer UnormRounding rounding) { EXIT_IF(rounding != UnormRounding::RoundEven && info.type != Format::ComponentType::Unorm); const auto bits = info.component_bits[component]; + if (info.type == Format::ComponentType::Float && bits < 16u) { + // Truncate to f16, whose bias the unsigned 11/10-bit floats share, and drop mantissa + // bits; negatives and NaN, whose f16 bits lie above +inf, store zero. + const auto half = data_bits == 16u ? EmitAndConstant(state, data, 0xffffu) + : EmitF32BitsToF16RtzBits(state, data); + return Select(state, TypeU32(state), + EmitCompareU32Constant(state, spv::OpUGreaterThan, half, 0x7c00u), + ConstantU32(state, 0), + EmitBinaryU32(state, spv::OpShiftRightLogical, half, + ConstantU32(state, 15u - bits))); + } if (data_bits == 16u) { if (info.type == Format::ComponentType::Uint) { data = EmitBitFieldUExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16)); diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 13d56631d..5bbcadb87 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -34198,6 +34198,8 @@ std::vector BufferStoreFormatD16Cases() { {0xc0003c00u, 0xdead3800u}, {0xc0003c00u, 0x00003800u}, 2, 0x86u}, {"BufferStoreFormatD16XyzUint8", F::k8_8_8_8UInt, {0x00020001u, 0xdead0003u}, {0x00030201u}, 1, 0x86u}, + {"BufferStoreFormatD16XyzFloat11_11_10", F::k11_11_10Float, + {0xc0003c7fu, 0xdead7bffu}, {0xf7c003c7u}, 1, 0x86u}, }; constexpr O formats[] = {O::BUFFER_STORE_FORMAT_D16_X, O::BUFFER_STORE_FORMAT_D16_XY, O::BUFFER_STORE_FORMAT_D16_XYZ, O::BUFFER_STORE_FORMAT_D16_XYZW}; @@ -34326,6 +34328,16 @@ std::vector FormattedStoreConversionCases() { F::k8_8_8_8UScaled, 4, {nan, inf, -inf, -0.0f}, 0x0000ff00u}, {{"BufferStoreSscaled8SpecialValues", "TBufferStoreSscaled8SpecialValues"}, F::k8_8_8_8SScaled, 4, {nan, inf, -inf, -0.0f}, 0x00807f00u}, + // Unsigned 11/10-bit floats truncate, clamp finite values and store negatives and NaN as 0. + {{"BufferStoreFloat11_11_10Truncates", "TBufferStoreFloat11_11_10Truncates"}, + F::k11_11_10Float, 3, {1.0f, std::bit_cast(0x3f87ffffu), 65520.0f}, 0xf7de1bc0u}, + {{"BufferStoreFloat11_11_10Clamps", "TBufferStoreFloat11_11_10Clamps"}, + F::k11_11_10Float, 3, {1e10f, -1.0f, inf}, 0xf80007bfu}, + {{"BufferStoreFloat11_11_10Denormals", "TBufferStoreFloat11_11_10Denormals"}, + F::k11_11_10Float, 3, + {nan, std::bit_cast(0x35800000u), std::bit_cast(0x38400000u)}, 0x06000800u}, + {{"BufferStoreFloat10_11_11Truncates", "TBufferStoreFloat10_11_11Truncates"}, + F::k10_11_11Float, 3, {std::bit_cast(0x3f87ffffu), 1.0f, 65520.0f}, 0xf7ef01e1u}, }; constexpr O store_opcodes[2][4] = { {O::BUFFER_STORE_FORMAT_X, O::BUFFER_STORE_FORMAT_XY,