From a12ddaa905a0bff531a239d43eb346da522b8eba Mon Sep 17 00:00:00 2001 From: Nahuel Genchi <65604505+NahuelGenchi@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:50:41 -0300 Subject: [PATCH 1/4] fix(shader): round v_fma_f64 once for subnormal operands and results A subnormal operand sent v_fma_f64 to a separate multiply and add, and a subnormal result was rounded twice: once to 53 bits in the scaled domain and again by the power-of-two rescale. Both differ from IEEE 754 fusedMultiplyAdd in the last bit. Subnormal operands are now widened by 2^64 and enter the exact path with their exponent lowered by 64. The exact path keeps the sign of the residual between the 53-bit result and the exact sum; when the result lands on a midpoint of the subnormal grid it is moved one ulp towards the exact sum before the single final rounding. Normal results are rebuilt by setting the exponent, so only a subnormal result rounds. v_div_fmas_f64 shares the path. --- .../tests/execution/F64FmaSubnormals.cpp | 124 ++++++++++++++++++ .../src/SpirvAlu/SpirvAluEmitterFloat64.cpp | 85 ++++++++++-- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 196 insertions(+), 15 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp new file mode 100644 index 000000000..7e0dc61ce --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp @@ -0,0 +1,124 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 8; +constexpr std::uint32_t Results = 2; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020085, 0x34060083, 0xe0381000, 0x80000401, 0xe0381010, 0x80000801, 0xbf8c3f70, 0xd54c000c, + 0x04220d04, 0xe0741000, 0x80010c03, 0xbf810000, +}; + +constexpr std::uint64_t Rows[Threads][4] = { + {0xaf909f1570e40fb2ull, 0x10697a78dd1df49bull, 0x8000000000000000ull, 0x800d3be6095b3463ull}, + {0x1e50511fd30f5d67ull, 0x24c92a1b12df5283ull, 0x8329a9b24976c10bull, 0x0006cc7690f01937ull}, + {0x98cd9ffcf3ed94edull, 0x2a51579178c2f7a9ull, 0x03300e10060c0a21ull, 0x000826b2393bc091ull}, + {0x2c67bd24a078a9e3ull, 0x169c3293f5b93146ull, 0x8314eb0558ce14d3ull, 0x8002b781181390c7ull}, + {0xbcca08e86f6c243dull, 0x0647cf3a0cd21b23ull, 0x03235effea3a0f40ull, 0x00053433dc65a9cbull}, + {0x97cb9cf8c8a2fbb6ull, 0xab2cb31ef6264b43ull, 0x8308c3ea76c1ebe5ull, 0x00014c7d2a0da8cbull}, + {0x0946591c0410303cull, 0xba059dbfbeb6d4adull, 0x80099e28c52b6c3dull, 0x835e314afa5d1a07ull}, + {0xcee37832c306ee31ull, 0x80017e95cd7d98dfull, 0x8c168438ac98afa4ull, 0x0ecd18c0555dceefull}, + {0x800e8b91a326e01dull, 0xd020fb9f53a2df2cull, 0x8cb514f5ff6d74b0ull, 0x103ee09fedab559aull}, + {0x401fac71cda29715ull, 0x8008000020000000ull, 0x002fac724c545e4full, 0x0000000000000007ull}, + {0x2e4c000000000000ull, 0x11aeb6db849700ffull, 0x800d70000a021070ull, 0x8000000000000000ull}, + {0x41d0000000000008ull, 0x0000000040000002ull, 0x80761e2b2ef2fce7ull, 0x0084f0ea6986819dull}, + {0xfc21916e447cf802ull, 0x8004eec859174483ull, 0x3be2fe907aa80074ull, 0x3c26da145be5eaacull}, + {0x35d175fd70782c6cull, 0x0c6033e7239c8fe0ull, 0x0005cb65548dabd1ull, 0x0241aea1c6a7627dull}, + {0xb4fb1dc3f1ce6211ull, 0x8b80000000000100ull, 0x800000000b9b5f36ull, 0x008b1dc3f1b72d04ull}, + {0x0008000000000000ull, 0x8004000000000000ull, 0x0000000000000000ull, 0x8000000000000000ull}, + {0x0008000000000000ull, 0x0004000000000000ull, 0x0170000000000000ull, 0x0170000000000000ull}, + {0x4004000000000000ull, 0x0000000000000001ull, 0x0000000000000000ull, 0x0000000000000002ull}, + {0x3fe0000000000000ull, 0x0000000000000001ull, 0x0000000000000000ull, 0x0000000000000000ull}, + {0x3fe0000000000001ull, 0x0000000000000001ull, 0x0000000000000000ull, 0x0000000000000001ull}, + {0x3fefffffffffffffull, 0x0010000000000000ull, 0x0000000000000000ull, 0x0010000000000000ull}, + {0x1e60000000000000ull, 0x1e60000000000000ull, 0x8000000000000001ull, 0x0000000000000000ull}, + {0x3ff0000000000001ull, 0x0018000000000001ull, 0x8010000000000000ull, 0x0008000000000003ull}, + {0x3ff8000000000000ull, 0x4000000000000000ull, 0x3fd0000000000000ull, 0x400a000000000000ull}, + {0x3ff0000000000001ull, 0x3ff0000000000001ull, 0xbff0000000000000ull, 0x3cc0000000000000ull}, + {0xffefffffffffffffull, 0x3ff0000000000000ull, 0x000fffffffffffffull, 0xffefffffffffffffull}, + {0x5fefffffffffffffull, 0x5fefffffffffffffull, 0x0000000000000000ull, 0x7feffffffffffffeull}, + {0x000fffffffffffffull, 0x4000000000000000ull, 0x8000000000000001ull, 0x001ffffffffffffdull}, + {0x8000000000000003ull, 0x7fe8000000000000ull, 0x3ff0000000000000ull, 0x3fefffffffffffeeull}, + {0x1a75555555555555ull, 0x000aaaaaaaaaaaabull, 0x0000000000000000ull, 0x0000000000000000ull}, + {0x0173000000000000ull, 0x3e17000000000000ull, 0x80001b5000000000ull, 0x0000000000000000ull}, + {0xa000000000000001ull, 0x1fffffffffffffffull, 0x0010000000000000ull, 0x8000000000000000ull}, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::string Hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%016llx", static_cast(value)); + return text; +} + +void Run(AgcDriver::VulkanDevice& device) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t operand = 0; operand < 3; ++operand) { + Input[tid * Inputs + operand * 2] = static_cast(Rows[tid][operand]); + Input[tid * Inputs + operand * 2 + 1] = static_cast(Rows[tid][operand] >> 32u); + } + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint64_t actual = Output[tid * Results] | (static_cast(Output[tid * Results + 1]) << 32u); + Require(actual == Rows[tid][3], "v_fma_f64: lane " + std::to_string(tid) + " fma(" + Hex(Rows[tid][0]) + ", " + Hex(Rows[tid][1]) + ", " + Hex(Rows[tid][2]) + ") is " + Hex(actual) + ", expected " + Hex(Rows[tid][3])); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + Check(); + std::puts("v_fma_f64 subnormal tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp index 95c2d7450..1fb812638 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp @@ -306,32 +306,89 @@ std::uint32_t FusedMultiplyAdd(SpirvEmitterState& state, std::uint32_t arg0, std }; const auto signedInt = [&](std::uint32_t value) { return Unary(state, spv::OpBitcast, TypeI32(state), value); }; - const auto product = Binary(state, spv::OpISub, TypeI32(state), signedInt(Binary(state, spv::OpIAdd, TypeU32(state), exponentA, exponentB)), ConstantI32(state, 2046)); - const auto distance = Binary(state, spv::OpISub, TypeI32(state), Binary(state, spv::OpISub, TypeI32(state), signedInt(exponentC), ConstantI32(state, 1023)), product); - const auto far = Binary(state, spv::OpLogicalAnd, TypeBool(state), Binary(state, spv::OpSGreaterThanEqual, TypeBool(state), distance, ConstantI32(state, 60)), Unary(state, spv::OpLogicalNot, TypeBool(state), c.zero)); + const auto both = [&](std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, spv::OpLogicalAnd, TypeBool(state), lhs, rhs); }; + const auto either = [&](std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, spv::OpLogicalOr, TypeBool(state), lhs, rhs); }; + const auto negate = [&](std::uint32_t value) { return Unary(state, spv::OpLogicalNot, TypeBool(state), value); }; + struct Normalized { + F64Bits bits; + std::uint32_t exponent = 0; + }; + const auto normalize = [&](const F64Class& value, std::uint32_t bits, std::uint32_t field) { + const auto subnormal = isField(field, 0u); + const auto widened = Split(state, FromF64(state, Exact(state, spv::OpFMul, ToF64(state, bits), ConstantF64(state, 0x43f0000000000000ull)))); + const auto normal = SelectBits(state, subnormal, widened, value.bits); + const auto bias = Select(state, TypeI32(state), subnormal, ConstantI32(state, 64), ConstantI32(state, 0)); + return Normalized{normal, Binary(state, spv::OpISub, TypeI32(state), signedInt(ExponentField(state, normal.high)), bias)}; + }; + const auto normalA = normalize(a, arg0, exponentA); + const auto normalB = normalize(b, arg1, exponentB); + const auto normalC = normalize(c, arg2, exponentC); + const auto product = Binary(state, spv::OpISub, TypeI32(state), Binary(state, spv::OpIAdd, TypeI32(state), normalA.exponent, normalB.exponent), ConstantI32(state, 2046)); + const auto distance = Binary(state, spv::OpISub, TypeI32(state), Binary(state, spv::OpISub, TypeI32(state), normalC.exponent, ConstantI32(state, 1023)), product); + const auto far = both(Binary(state, spv::OpSGreaterThanEqual, TypeBool(state), distance, ConstantI32(state, 60)), negate(c.zero)); const auto tiny = Binary(state, spv::OpSLessThan, TypeBool(state), distance, ConstantI32(state, -150)); const auto clamped = Glsl(state, TypeI32(state), GLSLstd450SClamp, distance, ConstantI32(state, -150), ConstantI32(state, 60)); const auto sign = Binary(state, spv::OpBitwiseAnd, TypeU32(state), c.bits.high, ConstantU32(state, 0x80000000u)); const F64Bits tinyBits{ConstantU32(state, 0u), Binary(state, spv::OpBitwiseOr, TypeU32(state), sign, ConstantU32(state, (1023u - 150u) << 20u))}; const auto rebiased = Unary(state, spv::OpBitcast, TypeU32(state), Binary(state, spv::OpIAdd, TypeI32(state), clamped, ConstantI32(state, 1023))); - const auto scaledC = SelectBits(state, c.zero, c.bits, SelectBits(state, tiny, tinyBits, WithExponent(state, c.bits, rebiased))); + const auto scaledC = SelectBits(state, c.zero, c.bits, SelectBits(state, tiny, tinyBits, WithExponent(state, normalC.bits, rebiased))); - const auto exact = TwoProduct(state, ToF64(state, Join(state, WithExponent(state, a.bits, ConstantU32(state, 1023u)))), ToF64(state, Join(state, WithExponent(state, b.bits, ConstantU32(state, 1023u))))); + const auto exact = TwoProduct(state, ToF64(state, Join(state, WithExponent(state, normalA.bits, ConstantU32(state, 1023u)))), ToF64(state, Join(state, WithExponent(state, normalB.bits, ConstantU32(state, 1023u))))); const auto sum = TwoSum(state, ToF64(state, Join(state, scaledC)), exact.high); - const auto scaled = Exact(state, spv::OpFAdd, sum.high, AddRoundToOdd(state, sum.low, exact.low)); - const auto power = PowerOfTwo(state, scale); - const auto total = Glsl(state, TypeI32(state), GLSLstd450SClamp, Binary(state, spv::OpIAdd, TypeI32(state), product, scale), ConstantI32(state, -2044), ConstantI32(state, 2046)); - const auto half = Binary(state, spv::OpSDiv, TypeI32(state), total, ConstantI32(state, 2)); - const auto rescaled = Exact(state, spv::OpFMul, Exact(state, spv::OpFMul, scaled, PowerOfTwo(state, half)), PowerOfTwo(state, Binary(state, spv::OpISub, TypeI32(state), total, half))); - const auto farValue = Split(state, FromF64(state, Exact(state, spv::OpFMul, ToF64(state, arg2), power))); - const auto finite = SelectBits(state, far, farValue, Split(state, FromF64(state, rescaled))); + const auto remainder = TwoSum(state, sum.low, exact.low); + const auto odd = AddRoundToOdd(state, sum.low, exact.low); + const auto rounded = TwoSum(state, sum.high, odd); + const auto residual = Exact(state, spv::OpFAdd, rounded.low, Exact(state, spv::OpFAdd, Exact(state, spv::OpFSub, remainder.high, odd), remainder.low)); + const auto productSign = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Binary(state, spv::OpBitwiseXor, TypeU32(state), a.bits.high, b.bits.high), ConstantU32(state, 0x80000000u)); + const auto productNegative = Binary(state, spv::OpINotEqual, TypeBool(state), productSign, ConstantU32(state, 0u)); + const auto above = either(both(far, negate(productNegative)), both(negate(far), Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), residual, ConstantF64(state, 0u)))); + const auto below = either(both(far, productNegative), both(negate(far), Binary(state, spv::OpFOrdLessThan, TypeBool(state), residual, ConstantF64(state, 0u)))); + auto value = SelectBits(state, far, normalC.bits, Split(state, FromF64(state, rounded.high))); + const auto farTotal = Binary(state, spv::OpISub, TypeI32(state), Binary(state, spv::OpIAdd, TypeI32(state), normalC.exponent, scale), signedInt(ExponentField(state, normalC.bits.high))); + const auto total = Select(state, TypeI32(state), far, farTotal, Binary(state, spv::OpIAdd, TypeI32(state), product, scale)); + const auto resultExponent = [&](F64Bits bits) { return Binary(state, spv::OpIAdd, TypeI32(state), signedInt(ExponentField(state, bits.high)), total); }; + const auto magnitude = Binary(state, spv::OpBitwiseOr, TypeU32(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), value.high, ConstantU32(state, 0x7fffffffu)), value.low); + const auto zero = Binary(state, spv::OpIEqual, TypeBool(state), magnitude, ConstantU32(state, 0u)); + const auto unrounded = resultExponent(value); + const auto shift = Binary(state, spv::OpISub, TypeI32(state), ConstantI32(state, 1), unrounded); + const auto dropped = Unary(state, spv::OpBitcast, TypeU32(state), Glsl(state, TypeI32(state), GLSLstd450SClamp, shift, ConstantI32(state, 1), ConstantI32(state, 54))); + const auto isHalf = [&](std::uint32_t word, std::uint32_t bits) { + const auto mask = Binary(state, spv::OpShiftRightLogical, TypeU32(state), ConstantU32(state, 0xffffffffu), Binary(state, spv::OpISub, TypeU32(state), ConstantU32(state, 32u), bits)); + const auto half = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), ConstantU32(state, 1u), Binary(state, spv::OpISub, TypeU32(state), bits, ConstantU32(state, 1u))); + return Binary(state, spv::OpIEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), word, mask), half); + }; + const auto lowHalf = isHalf(value.low, Glsl(state, TypeU32(state), GLSLstd450UMin, dropped, ConstantU32(state, 32u))); + const auto significandHigh = Binary(state, spv::OpBitwiseOr, TypeU32(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), value.high, ConstantU32(state, 0x000fffffu)), ConstantU32(state, 0x00100000u)); + const auto highBits = Binary(state, spv::OpISub, TypeU32(state), Glsl(state, TypeU32(state), GLSLstd450UMax, dropped, ConstantU32(state, 33u)), ConstantU32(state, 32u)); + const auto highHalf = both(Binary(state, spv::OpIEqual, TypeBool(state), value.low, ConstantU32(state, 0u)), isHalf(significandHigh, highBits)); + const auto wide = Binary(state, spv::OpUGreaterThan, TypeBool(state), dropped, ConstantU32(state, 32u)); + const auto onHalf = either(both(wide, highHalf), both(negate(wide), lowHalf)); + const auto inRange = both(Binary(state, spv::OpSLessThanEqual, TypeBool(state), unrounded, ConstantI32(state, 0)), Binary(state, spv::OpSLessThanEqual, TypeBool(state), shift, ConstantI32(state, 54))); + const auto midpoint = both(both(negate(zero), inRange), both(onHalf, either(above, below))); + const auto negative = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), value.high, ConstantU32(state, 0x80000000u)), ConstantU32(state, 0u)); + const auto away = either(both(above, negate(negative)), both(below, negative)); + const auto carry = Select(state, TypeU32(state), Binary(state, spv::OpIEqual, TypeBool(state), value.low, ConstantU32(state, 0xffffffffu)), ConstantU32(state, 1u), ConstantU32(state, 0u)); + const auto borrow = Select(state, TypeU32(state), Binary(state, spv::OpIEqual, TypeBool(state), value.low, ConstantU32(state, 0u)), ConstantU32(state, 1u), ConstantU32(state, 0u)); + const F64Bits increased{Binary(state, spv::OpIAdd, TypeU32(state), value.low, ConstantU32(state, 1u)), Binary(state, spv::OpIAdd, TypeU32(state), value.high, carry)}; + const F64Bits decreased{Binary(state, spv::OpISub, TypeU32(state), value.low, ConstantU32(state, 1u)), Binary(state, spv::OpISub, TypeU32(state), value.high, borrow)}; + value = SelectBits(state, midpoint, SelectBits(state, away, increased, decreased), value); + + const auto exponent = resultExponent(value); + const auto normalResult = WithExponent(state, value, Unary(state, spv::OpBitcast, TypeU32(state), Glsl(state, TypeI32(state), GLSLstd450SClamp, exponent, ConstantI32(state, 1), ConstantI32(state, 2046)))); + const auto lowest = PowerOfTwo(state, Glsl(state, TypeI32(state), GLSLstd450SMax, Binary(state, spv::OpISub, TypeI32(state), exponent, ConstantI32(state, 1)), ConstantI32(state, -60))); + const auto subnormalResult = Split(state, FromF64(state, Exact(state, spv::OpFMul, ToF64(state, Join(state, WithExponent(state, value, ConstantU32(state, 1u)))), lowest))); + const F64Bits infinity{ConstantU32(state, 0u), Binary(state, spv::OpBitwiseOr, TypeU32(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), value.high, ConstantU32(state, 0x80000000u)), ConstantU32(state, 0x7ff00000u))}; + auto finite = SelectBits(state, Binary(state, spv::OpSGreaterThanEqual, TypeBool(state), exponent, ConstantI32(state, 1)), normalResult, subnormalResult); + finite = SelectBits(state, Binary(state, spv::OpSGreaterThanEqual, TypeBool(state), exponent, ConstantI32(state, 2047)), infinity, finite); + finite = SelectBits(state, zero, value, finite); + + const auto power = PowerOfTwo(state, scale); auto fallback = Split(state, FromF64(state, Exact(state, spv::OpFMul, Exact(state, spv::OpFAdd, Exact(state, spv::OpFMul, ToF64(state, arg0), ToF64(state, arg1)), ToF64(state, arg2)), power))); const auto finiteProduct = Binary(state, spv::OpLogicalAnd, TypeBool(state), Unary(state, spv::OpLogicalNot, TypeBool(state), isField(exponentA, 0x7ffu)), Unary(state, spv::OpLogicalNot, TypeBool(state), isField(exponentB, 0x7ffu))); fallback = SelectBits(state, Binary(state, spv::OpLogicalAnd, TypeBool(state), c.infinite, finiteProduct), c.bits, fallback); fallback = SelectBits(state, Classify(state, Join(state, fallback)).nan, {ConstantU32(state, 0u), ConstantU32(state, 0xfff80000u)}, fallback); - const auto subnormalC = Binary(state, spv::OpLogicalAnd, TypeBool(state), isField(exponentC, 0u), Unary(state, spv::OpLogicalNot, TypeBool(state), c.zero)); - const auto special = any({isField(exponentA, 0u), isField(exponentB, 0u), isField(exponentA, 0x7ffu), isField(exponentB, 0x7ffu), isField(exponentC, 0x7ffu), subnormalC}); + const auto special = any({a.zero, b.zero, isField(exponentA, 0x7ffu), isField(exponentB, 0x7ffu), isField(exponentC, 0x7ffu)}); auto result = SelectBits(state, special, fallback, finite); result = SelectBits(state, c.nan, Quiet(state, c.bits), result); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 6d856ef77..a7e09fbd8 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -297,7 +297,7 @@ Throughout the project, every function at every stage either **does exactly what - On a host subgroup narrower than the wave, [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) of a constant lane outside it builds only as lane 63 of a half-wave scan (DPP row_shr 1/2/4/8, `v_permlanex16_b32`, EXEC -1); other such reads throw. - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric` and throw without it, as fixed-function interpolation cannot split a 32-bit attribute. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64, so these instructions need the Float64 capability. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the arithmetic in every mode against the hardware rows on any device with Float64. -- [v_fma_f64](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) is fused exactly only for normal inputs: with a subnormal input it is a separate multiply and add, which rounds twice. +- [v_fma_f64 and v_div_fmas_f64](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept; the MODE register is not read, and these cases were not compared with hardware. - [SINT storage images](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. - [`StoreTexel`](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) writes channel 1 of a two-channel format from the data slot whose DST_SEL is Y, for SINT and UINT alike. With DST_SEL X,Y,Z,W, gfx1035 writes it from the W slot, as adriadam10 measured in #944. - [COND_EXEC](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) reads its condition after the earlier packets of its queue complete. Without PFP_SYNC_ME the console's graphics PFP reads it ahead of the ME, so a title that races the read sees a later value here. From 1dcdca149808f442138e0a8befaedc31750dfb73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 10 Oct 2026 00:16:57 +0200 Subject: [PATCH 2/4] test(agc): pin v_div_fmas_f64 with VCC set on subnormal operands and results Expected values measured on RDNA2 (gfx1035) with the HW oracle at IEEE=0 and IEEE=1, f64 denormals kept. Co-Authored-By: Claude --- .../tests/execution/F64FmaSubnormals.cpp | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp index 7e0dc61ce..c6b2934fa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp @@ -26,6 +26,11 @@ alignas(256) constexpr std::array Code{ 0x04220d04, 0xe0741000, 0x80010c03, 0xbf810000, }; +alignas(256) constexpr std::array FmasCode{ + 0x34020085, 0x34060083, 0xe0381000, 0x80000401, 0xe0381010, 0x80000801, 0xbf8c3f70, 0xbeea03c1, + 0xd570000c, 0x04220d04, 0xe0741000, 0x80010c03, 0xbf810000, +}; + constexpr std::uint64_t Rows[Threads][4] = { {0xaf909f1570e40fb2ull, 0x10697a78dd1df49bull, 0x8000000000000000ull, 0x800d3be6095b3463ull}, {0x1e50511fd30f5d67ull, 0x24c92a1b12df5283ull, 0x8329a9b24976c10bull, 0x0006cc7690f01937ull}, @@ -61,6 +66,29 @@ constexpr std::uint64_t Rows[Threads][4] = { {0xa000000000000001ull, 0x1fffffffffffffffull, 0x0010000000000000ull, 0x8000000000000000ull}, }; +constexpr std::uint64_t FmasRows[][4] = { + {0xcee37832c306ee31ull, 0x80017e95cd7d98dfull, 0x8c168438ac98afa4ull, 0x06cd18c0555dceefull}, + {0x800e8b91a326e01dull, 0xd020fb9f53a2df2cull, 0x8cb514f5ff6d74b0ull, 0x083ee09fedab559aull}, + {0xfc21916e447cf802ull, 0x8004eec859174483ull, 0x3be2fe907aa80074ull, 0x3426da145be5eaacull}, + {0x6456b4d74e778407ull, 0x0000000000000112ull, 0x20280ac49e7f906cull, 0x19b84d8e7e014d9full}, + {0x3b87a9e17b60d8beull, 0x0c5959dcff86b48aull, 0x026239792b5377a2ull, 0x0004afcb04199627ull}, + {0xaf25fa06e8714b4bull, 0x18d2b6bb683e8122ull, 0x02a53f820f7c716bull, 0x800cda229eb9a09dull}, + {0xb99c5fe31c736fbbull, 0x8e5264264dcfc027ull, 0x8022aa8efefbe52cull, 0x0008275eae4b3d41ull}, + {0xc6fdfbc0001ad5dfull, 0x00ffcab17e745bd1ull, 0x82e0e3a0b85211a8ull, 0x800ee4e6bd8a79e5ull}, + {0xc1d1a6cfa76ad3b9ull, 0x862ba38c1492703full, 0x017130645c1851d5ull, 0x000f3ef62c247005ull}, + {0xada41786a4b824bcull, 0x1a554baa4a83fe63ull, 0x0134fb8e603406d4ull, 0x800d5ef26b6f7a29ull}, + {0xb81b70578b6c14b9ull, 0x8fcdd6775e710cc1ull, 0x829c666c28b85110ull, 0x0006656d3ea3b537ull}, + {0x2c256baa0ef1c91bull, 0x9baa7695febd5f53ull, 0x82d3dfc5a191618bull, 0x800236db72e217c3ull}, + {0x355013dab21e2539ull, 0x1296e6732f928ca7ull, 0x82f4e48c5e9c126cull, 0x0005c0b776cad66bull}, + {0xc458817a2d2ea918ull, 0x03a473fd45d66f08ull, 0x03240e0c4f7cfcc4ull, 0x800fa9bfa078e4f1ull}, + {0x33291df738c3f676ull, 0x146a0194f3933c51ull, 0x0120115306c4e7b2ull, 0x000028d32d6f3f07ull}, + {0xb1d797d703ad9014ull, 0x9613cb63b23f1b8bull, 0x0244b2cb24e6fb3aull, 0x00074c0e33ad5c9dull}, + {0x4656b4266997b7f2ull, 0x819765c0b9bfcad0ull, 0x03c9a52d2b9f1094ull, 0x80084cd678e92eefull}, + {0x4513191709caec8cull, 0x02d3b5ddc4ac6b05ull, 0x826384dfaf4b9e81ull, 0x0005e1b7f963c7dbull}, + {0xba6ec9ec81f6efb7ull, 0x8d785fa355446f4eull, 0x0279c121fe09c0aaull, 0x0005dcdd7f8c7cd3ull}, + {0xbe667b0282fcc03cull, 0x897ce8a400290c2cull, 0x0378775e473d7a50ull, 0x000513c44ed72187ull}, +}; + std::array BufferDescriptor(const void* data, std::uint32_t bytes) { const auto address = reinterpret_cast(data); return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; @@ -72,11 +100,13 @@ std::string Hex(std::uint64_t value) { return text; } -void Run(AgcDriver::VulkanDevice& device) { - for (std::uint32_t tid = 0; tid < Threads; ++tid) { +template +void Run(AgcDriver::VulkanDevice& device, const std::array& program, const std::uint64_t (&rows)[Count][4]) { + Input.fill(0u); + for (std::uint32_t tid = 0; tid < Count; ++tid) { for (std::uint32_t operand = 0; operand < 3; ++operand) { - Input[tid * Inputs + operand * 2] = static_cast(Rows[tid][operand]); - Input[tid * Inputs + operand * 2 + 1] = static_cast(Rows[tid][operand] >> 32u); + Input[tid * Inputs + operand * 2] = static_cast(rows[tid][operand]); + Input[tid * Inputs + operand * 2 + 1] = static_cast(rows[tid][operand] >> 32u); } } Output.fill(0xdeadbeefu); @@ -85,7 +115,7 @@ void Run(AgcDriver::VulkanDevice& device) { const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); std::copy(input.begin(), input.end(), userData.begin()); std::copy(output.begin(), output.end(), userData.begin() + 4); - const std::span code(Code); + const std::span code(program); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ @@ -100,10 +130,11 @@ void Run(AgcDriver::VulkanDevice& device) { device.WaitIdle(); } -void Check() { - for (std::uint32_t tid = 0; tid < Threads; ++tid) { +template +void Check(const char* name, const std::uint64_t (&rows)[Count][4]) { + for (std::uint32_t tid = 0; tid < Count; ++tid) { const std::uint64_t actual = Output[tid * Results] | (static_cast(Output[tid * Results + 1]) << 32u); - Require(actual == Rows[tid][3], "v_fma_f64: lane " + std::to_string(tid) + " fma(" + Hex(Rows[tid][0]) + ", " + Hex(Rows[tid][1]) + ", " + Hex(Rows[tid][2]) + ") is " + Hex(actual) + ", expected " + Hex(Rows[tid][3])); + Require(actual == rows[tid][3], std::string(name) + ": lane " + std::to_string(tid) + " (" + Hex(rows[tid][0]) + ", " + Hex(rows[tid][1]) + ", " + Hex(rows[tid][2]) + ") is " + Hex(actual) + ", expected " + Hex(rows[tid][3])); } } @@ -113,8 +144,10 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device); - Check(); + Run(*device, Code, Rows); + Check("v_fma_f64", Rows); + Run(*device, FmasCode, FmasRows); + Check("v_div_fmas_f64 with VCC set", FmasRows); std::puts("v_fma_f64 subnormal tests passed"); return 0; } catch (const std::exception& error) { From b2961b702cb8a9a009d70fe51ed18f10e59d0a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 10 Oct 2026 00:16:57 +0200 Subject: [PATCH 3/4] docs(debt): v_fma_f64 subnormals measured, f64 denormal flush not read Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index a7e09fbd8..386b4febe 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -297,7 +297,7 @@ Throughout the project, every function at every stage either **does exactly what - On a host subgroup narrower than the wave, [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) of a constant lane outside it builds only as lane 63 of a half-wave scan (DPP row_shr 1/2/4/8, `v_permlanex16_b32`, EXEC -1); other such reads throw. - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric` and throw without it, as fixed-function interpolation cannot split a 32-bit attribute. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64, so these instructions need the Float64 capability. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the arithmetic in every mode against the hardware rows on any device with Float64. -- [v_fma_f64 and v_div_fmas_f64](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept; the MODE register is not read, and these cases were not compared with hardware. +- [v_fma_f64 and v_div_fmas_f64](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept, measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1, `v_div_fmas_f64` with VCC clear and set. The f16/f64 denormal field of `FLOAT_MODE` (bits 7:6) is not read: when it flushes, the hardware flushes the operands and the result of `v_fma_f64` (tininess before rounding) and only the result of `v_div_fmas_f64`, while these keep them. - [SINT storage images](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. - [`StoreTexel`](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) writes channel 1 of a two-channel format from the data slot whose DST_SEL is Y, for SINT and UINT alike. With DST_SEL X,Y,Z,W, gfx1035 writes it from the W slot, as adriadam10 measured in #944. - [COND_EXEC](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) reads its condition after the earlier packets of its queue complete. Without PFP_SYNC_ME the console's graphics PFP reads it ahead of the ME, so a title that races the read sees a later value here. From e4a911fcd373ef41a077d50c31131ad9a68aa7b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 10 Oct 2026 00:28:22 +0200 Subject: [PATCH 4/4] test(agc): use raw buffer descriptors in the v_fma_f64 subnormal test With OOB_SELECT 0 and stride 0 every access is out of range now that the buffer bounds follow OOB_SELECT, so the shader never read or wrote. Co-Authored-By: Claude --- .../prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp index c6b2934fa..b77fe2b37 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp @@ -91,7 +91,7 @@ constexpr std::uint64_t FmasRows[][4] = { std::array BufferDescriptor(const void* data, std::uint32_t bytes) { const auto address = reinterpret_cast(data); - return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; } std::string Hex(std::uint64_t value) {