diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DivFixupF64DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DivFixupF64DenormalModes.cpp new file mode 100644 index 0000000000..b609850c3f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DivFixupF64DenormalModes.cpp @@ -0,0 +1,149 @@ +#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 +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t ValueCount = 10; +constexpr std::uint32_t Threads = 1024; +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{ + 0x34020085u, 0x34040083u, 0xe0381000u, 0x80000401u, 0xe0341010u, 0x80000801u, 0xbf8c3f70u, 0xd560000au, + 0x04220d04u, 0xe0741000u, 0x80010a02u, 0xbf810000u, +}; + +constexpr std::uint64_t Values[ValueCount] = { + 0x0000000000000000ull, 0x8000000000000000ull, 0x0000000000000001ull, 0x800fffffffffffffull, 0x0010000000000000ull, + 0x3ff0000000000000ull, 0xbff0000000000000ull, 0x7fefffffffffffffull, 0x7ff0000000000000ull, 0x7ff8000000000000ull, +}; + +struct Mode { + const char* name; + std::optional value; + bool flushed; + bool ieee; +}; + +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, 0x31016facu}; +} + +std::string Hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%016llx", static_cast(value)); + return text; +} + +std::array Sources(std::uint32_t tid) { + const std::uint32_t row = tid < ValueCount * ValueCount * ValueCount ? tid : 0u; + return {Values[row / (ValueCount * ValueCount)], Values[row / ValueCount % ValueCount], Values[row % ValueCount]}; +} + +void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sources = Sources(tid); + for (std::uint32_t index = 0; index < sources.size(); ++index) { + Input[tid * Inputs + index * 2u] = static_cast(sources[index]); + Input[tid * Inputs + index * 2u + 1u] = static_cast(sources[index] >> 32u); + } + Input[tid * Inputs + 6u] = 0u; + Input[tid * Inputs + 7u] = 0u; + } + 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; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +std::uint64_t Fixup(std::uint64_t quotient, std::uint64_t denominator, std::uint64_t numerator, bool flushed, bool ieee) { + constexpr std::uint64_t Magnitude = 0x7fffffffffffffffull; + constexpr std::uint64_t Infinity = 0x7ff0000000000000ull; + const auto flush = [&](std::uint64_t value) { return flushed && (value & Infinity) == 0u ? value & ~Magnitude : value; }; + denominator = flush(denominator); + numerator = flush(numerator); + const auto nan = [&](std::uint64_t value) { return (value & Magnitude) > Infinity; }; + const auto infinite = [&](std::uint64_t value) { return (value & Magnitude) == Infinity; }; + const auto zero = [&](std::uint64_t value) { return (value & Magnitude) == 0u; }; + const auto exponent = [](std::uint64_t value) { return static_cast((value >> 52u) & 0x7ffu); }; + const std::uint64_t sign = (denominator ^ numerator) & ~Magnitude; + if (nan(numerator)) return ieee ? numerator | 0x0008000000000000ull : numerator; + if (nan(denominator)) return ieee ? denominator | 0x0008000000000000ull : denominator; + if ((zero(denominator) && zero(numerator)) || (infinite(denominator) && infinite(numerator))) return 0xfff8000000000000ull; + if (zero(denominator) || infinite(numerator)) return sign | Infinity; + if (infinite(denominator) || zero(numerator)) return sign; + if (exponent(numerator) - exponent(denominator) < -1075) return sign; + if (nan(quotient)) return sign | Infinity; + return sign | (quotient & Magnitude); +} + +void Check(const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sources = Sources(tid); + const std::uint64_t expected = Fixup(sources[0], sources[1], sources[2], mode.flushed, mode.ieee); + const std::uint64_t actual = Output[tid * Results] | (static_cast(Output[tid * Results + 1u]) << 32u); + Require(actual == expected, std::string("v_div_fixup_f64 ") + mode.name + " of " + Hex(sources[0]) + ", " + Hex(sources[1]) + ", " + Hex(sources[2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const Mode modes[] = { + {"without a known float mode", std::nullopt, false, false}, + {"f64 denormal mode 0, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, true, true}, + {"f64 denormal mode 0, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}, true, false}, + {"f64 denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x70u, true, true, false}, false, true}, + {"f64 denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x70u, true, false, false}, false, false}, + {"f64 denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xb0u, true, true, false}, true, true}, + {"f64 denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xb0u, true, false, false}, true, false}, + {"f64 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, false, true}, + {"f64 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, false, false}, + }; + for (const Mode& mode : modes) { + Run(*device, mode); + Check(mode); + } + std::puts("v_div_fixup_f64 denormal mode tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/src/Float64Instructions.cpp b/core/shader/recompiler/Translation/src/Float64Instructions.cpp index 00a0bac5b3..2f355fe3bc 100644 --- a/core/shader/recompiler/Translation/src/Float64Instructions.cpp +++ b/core/shader/recompiler/Translation/src/Float64Instructions.cpp @@ -149,9 +149,14 @@ bool TranslationContext::vDivFmasF64(const RdnaInstruction& inst) { } bool TranslationContext::vDivFixupF64(const RdnaInstruction& inst) { + const auto flush = [&](const std::array& word) { + if (!floatMode.has_value() || (floatMode->floatMode & 0x40u) != 0u) return word; + const IrU1 tiny(ir.IEqual(ir.BitwiseAnd(word[1].Value(), ir.Constant(0x7ff00000u)), ir.Constant(0u))); + return std::array{IrU32(ir.Select(tiny.Value(), ir.Constant(0u), word[0].Value())), IrU32(ir.Select(tiny.Value(), ir.BitwiseAnd(word[1].Value(), ir.Constant(0x80000000u)), word[1].Value()))}; + }; const auto quotient = readF64Bits(sourceAt(inst, 0u)); - const auto denominator = readF64Bits(sourceAt(inst, 1u)); - const auto numerator = readF64Bits(sourceAt(inst, 2u)); + const auto denominator = flush(readF64Bits(sourceAt(inst, 1u))); + const auto numerator = flush(readF64Bits(sourceAt(inst, 2u))); const auto magnitude = [&](IrU32 high) { return IrU32(ir.BitwiseAnd(high.Value(), ir.Constant(0x7fffffffu))); }; const auto lowNonzero = [&](const std::array& word) { return IrU1(ir.INotEqual(word[0].Value(), ir.Constant(0u))); }; const auto isNan = [&](const std::array& word) { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index aedf5ebd41..ac0537ac42 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -89,6 +89,7 @@ - [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. A D16 store sign-extends each 16-bit half before that saturation, as measured on RDNA2 (gfx1035) for `image_store d16` to all eight SINT formats with DMASK 0xf, 0x7, 0x5, 0x3, 0x2, 0xa and 0x1 (1792 rows match). - [`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. - [v_ldexp_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f16 denormal field: bit 0 preserves the half input and bit 1 the half result. Otherwise a denormal input is flushed to its signed zero, and a result whose exact scaled value is below the minimum normal (2^-14) is flushed to its signed zero before rounding, so `v_ldexp_f16 0x07ff, -1` gives +0 instead of 0x0400. Measured on RDNA2 (gfx1036) with round to nearest, IEEE 0 and 1 and denorm16 0..3, over every half magnitude 0x0000..0x0fff of both signs scaled by 2^-1 and 2^0 (16384 rows per mode): no mismatch. NaN and infinity handling and destination high-half preservation are unchanged. Shaders without a known float mode keep the previous lowering. +- [v_div_fixup_f64](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) flushes its src1 and src2 to a zero of the same sign when bit 0 of the `FLOAT_MODE` f16/f64 denormal field is clear (0 and 2) and keeps a denormal src0 and result in every mode, as `v_div_fixup_f32` does with the f32 field. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 1728 triples of 12 f64 values (zeros, denormals, the minimum normal, ±1, the largest finite value, infinity and quiet and signaling NaNs) at IEEE=0 and IEEE=1 with denorm16 0..3. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [v_mullit_f32](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted only with IEEE=1, as measured at IEEE=0 too; a positive denormal src2 counts as positive); with f32 denormals flushed it follows the f32 denormals entry below, and denormal modes 1 and 2 throw. - [v_mad_legacy_f32 and v_mac_legacy_f32](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140, VOP2 0x06) round the product to f32 before the add, give a +0 product for a zero factor, and flush their f32 denormal sources, their product and their result to a zero of the same sign in every f32 denormal mode, an exception to the `FLOAT_MODE` model in [f32DenormalFlushFor](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp). Measured on gfx1010 (RX 5700 XT) with `tools/hw-oracle/template.s`, assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll` instead of `oracle.c` and ROCr, over the 32 rows of the five `FmaLegacy.cpp` and the four `MacLegacy.cpp` encodings: identical in wave32 and wave64 at IEEE 0 and 1 with f32 denormal modes 0 to 3, except that the two signaling NaN rows are quieted only with IEEE=1 (#1281, #1290). A sum of two zeros takes its sign on the bits (+0 unless both are -0), as lavapipe returned -0 for +0 + -0. The PS5's gfx1013 is not measured; LLVM decodes the encoding as `v_mad_legacy_f32` there, as on gfx1010. RDNA2 differs: on gfx1031 (RX 6700 XT, measured by pandather in #1281) the encoding is `v_fma_legacy_f32` and fused, with the same zero rule and denormal-source flush, so an RDNA2 measurement is not evidence for the console. - [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the f64 arithmetic of v_interp_p2_f16, so they need the Float64 capability. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. With f16 denormals kept, every f16 rounding mode is translated: the exact product-sum is rounded once in the mode's direction. All four modes match RDNA2 (gfx1036) on the 2,233 rows of the #2631 sweep, including `FLOAT_MODE` 0xcc (toward zero, used by ps5-opengl) and the exact cancellations toward -infinity; the PS5's gfx1013 is not measured. A result beyond 65504 becomes infinity or 65504 as IEEE 754 gives for the mode. Flushed f16 denormals still throw, and packed f16 still throws in every mode but round to nearest even with f16 denormals kept. `FP16_OVFL` set is not measured.