fix(shader): make v_sin and v_cos linear within 2^-12 of a turn from a zero

After #1979 the cycle is exact, but the driver's sine of the radians loses
tiny results to its absolute error, and near the zeros of sine at k + 0.5
and of cosine at k ± 0.25 the f32 radians drop the low bits of the cycle.
Measured on RDNA2 (gfx1036), the hardware returns the input times 6.2831826
for every input below 2^-12 of a turn and stays within 7 ULP of the f32 2π
times the distance at the half and quarter turns.

Within 2^-12 of a turn from a zero the lowering now returns the linear term
of the exact distance to that zero, with the hardware's constant at the
integers and the f32 2π elsewhere; everywhere else it keeps the driver's
sine or cosine of the radians. agc_driver_sin_cos_near_zero pins 32 inputs
per type from the oracle with per-lane tolerances.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_012GLfKH7Sqe51wX254k1XsV
This commit is contained in:
Jan ŠléglandClaude Fable 5.1 authored and Adrià Franch committed 2026-10-10 09:39:01 +02:00
1 parent 4e9d298eac
commit 909281c994
3 files changed
+210 -5

No files matched your search

@@ -0,0 +1,190 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <optional>
#include <span>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Threads = 32;
constexpr std::uint32_t Inputs = 4;
constexpr std::uint32_t Results = 4;
constexpr std::uint32_t Columns = 4;
alignas(256) std::array<std::uint32_t, Threads * Inputs> Input{};
alignas(256) std::array<std::uint32_t, Threads * Results> Output{};
alignas(256) constexpr std::array<std::uint32_t, 15> Code{
0x34020084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x7e160280u, 0x7e180280u, 0x7e1a0280u,
0x7e146b04u, 0x7e166d04u, 0x7e18c105u, 0x7e1ac305u, 0xe0781000u, 0x80010a01u, 0xbf810000u,
};
constexpr std::uint32_t Rows[Threads][4] = {
{0x80800000u, 0x00008001u, 0x00000000u, 0x00000000u},
{0x8d800000u, 0x00008400u, 0x00000000u, 0x00000000u},
{0xb0800000u, 0x00009000u, 0x00000000u, 0x00000000u},
{0xb58637bdu, 0x0000868eu, 0x00000000u, 0x00000000u},
{0x00800000u, 0x00000001u, 0x00000000u, 0x00000000u},
{0x358637bdu, 0x00000400u, 0x00000000u, 0x00000000u},
{0xb87fda40u, 0x000083ffu, 0x00000000u, 0x00000000u},
{0x387fda40u, 0x000003ffu, 0x00000000u, 0x00000000u},
{0xbeffffffu, 0x0000b7ffu, 0x00000000u, 0x00000000u},
{0x3effffffu, 0x000037ffu, 0x00000000u, 0x00000000u},
{0x3f000000u, 0x00003800u, 0x00000000u, 0x00000000u},
{0xbf000000u, 0x0000b800u, 0x00000000u, 0x00000000u},
{0x3e7fffffu, 0x000033ffu, 0x00000000u, 0x00000000u},
{0xbe7fffffu, 0x0000b3ffu, 0x00000000u, 0x00000000u},
{0x3e800000u, 0x00003400u, 0x00000000u, 0x00000000u},
{0x3f400000u, 0x00003a00u, 0x00000000u, 0x00000000u},
{0x437f8000u, 0x00005bfcu, 0x00000000u, 0x00000000u},
{0x43802000u, 0x00005c01u, 0x00000000u, 0x00000000u},
{0xc47a2000u, 0x0000e3d1u, 0x00000000u, 0x00000000u},
{0x7f7fc99eu, 0x00007bffu, 0x00000000u, 0x00000000u},
{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u},
{0x80000000u, 0x00008000u, 0x00000000u, 0x00000000u},
{0x7f800000u, 0x00007c00u, 0x00000000u, 0x00000000u},
{0x7fc00000u, 0x00007e00u, 0x00000000u, 0x00000000u},
{0x3dcccccdu, 0x00002e66u, 0x00000000u, 0x00000000u},
{0xbdcccccdu, 0x0000ae66u, 0x00000000u, 0x00000000u},
{0x3e99999au, 0x000034cdu, 0x00000000u, 0x00000000u},
{0xbf333333u, 0x0000b99au, 0x00000000u, 0x00000000u},
{0x3fa66666u, 0x00003d33u, 0x00000000u, 0x00000000u},
{0xc0266666u, 0x0000c133u, 0x00000000u, 0x00000000u},
{0x41240000u, 0x00004920u, 0x00000000u, 0x00000000u},
{0xc2c84000u, 0x0000d642u, 0x00000000u, 0x00000000u}
};
constexpr std::uint32_t Expected[Threads][Columns] = {
{0x81c90fd5u, 0x3f800000u, 0x00008006u, 0x00003c00u},
{0x8ec90fd5u, 0x3f800000u, 0x00008e48u, 0x00003c00u},
{0xb1c90fd5u, 0x3f800000u, 0x00009a48u, 0x00003c00u},
{0xb6d2d421u, 0x3f800000u, 0x00009126u, 0x00003c00u},
{0x01c90fd5u, 0x3f800000u, 0x00000006u, 0x00003c00u},
{0x36d2d421u, 0x3f800000u, 0x00000e48u, 0x00003c00u},
{0xb9c8f22fu, 0x3f7ffffeu, 0x00008e47u, 0x00003c00u},
{0x39c8f22fu, 0x3f7ffffeu, 0x00000e47u, 0x00003c00u},
{0xb4490fdcu, 0xbf800000u, 0x00009648u, 0x0000bc00u},
{0x34490fdcu, 0xbf800000u, 0x00001648u, 0x0000bc00u},
{0x00000000u, 0xbf800000u, 0x00000000u, 0x0000bc00u},
{0x00000000u, 0xbf800000u, 0x00000000u, 0x0000bc00u},
{0x3f800000u, 0x33c90fd9u, 0x00003c00u, 0x00001248u},
{0xbf800000u, 0x33c90fd9u, 0x0000bc00u, 0x00001248u},
{0x3f800000u, 0x00000000u, 0x00003c00u, 0x00000000u},
{0xbf800000u, 0x00000000u, 0x0000bc00u, 0x00000000u},
{0x00000000u, 0xbf800000u, 0x00000000u, 0x0000bc00u},
{0x3f800000u, 0x00000000u, 0x00003c00u, 0x00000000u},
{0x00000000u, 0xbf800000u, 0x00000000u, 0x0000bc00u},
{0x00000000u, 0x3f800000u, 0x00000000u, 0x00003c00u},
{0x00000000u, 0x3f800000u, 0x00000000u, 0x00003c00u},
{0x80000000u, 0x3f800000u, 0x00008000u, 0x00003c00u},
{0xffc00000u, 0xffc00000u, 0x0000fe00u, 0x0000fe00u},
{0x7fc00000u, 0x7fc00000u, 0x00007e00u, 0x00007e00u},
{0x3f167919u, 0x3f4f1bbdu, 0x000038b4u, 0x00003a79u},
{0xbf167919u, 0x3f4f1bbdu, 0x0000b8b4u, 0x00003a79u},
{0x3f737870u, 0xbe9e377cu, 0x00003b9cu, 0x0000b4f3u},
{0x3f737870u, 0xbe9e377cu, 0x00003b9du, 0x0000b4edu},
{0x3f737872u, 0xbe9e3770u, 0x00003b9du, 0x0000b4edu},
{0x3f167910u, 0xbf4f1bc3u, 0x000038b0u, 0x0000ba7cu},
{0x3f800000u, 0x00000000u, 0x00003c00u, 0x00000000u},
{0xbf3504f4u, 0x3f3504f3u, 0x0000b9a8u, 0x000039a8u}
};
constexpr const char* Names[Columns] = {"v_sin_f32", "v_cos_f32", "v_sin_f16", "v_cos_f16"};
constexpr std::uint32_t Tolerance[Threads] = {0u, 0u, 0u, 0u, 0u, 0u, 1u, 1u, 1u, 1u, 0u, 0u, 2u, 2u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 12u, 12u, 12u, 12u, 12u, 12u, 12u, 12u};
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
}
std::string Hex(std::uint32_t value) {
char text[16];
std::snprintf(text, sizeof(text), "0x%08x", value);
return text;
}
std::uint32_t Ordered(std::uint32_t bits, std::uint32_t width) {
const std::uint32_t sign = 1u << (width - 1u);
return (bits & sign) != 0u ? sign - (bits & ~sign) : bits | sign;
}
std::uint32_t Distance(std::uint32_t actual, std::uint32_t expected, std::uint32_t width) {
const std::uint32_t a = Ordered(actual, width);
const std::uint32_t e = Ordered(expected, width);
return a > e ? a - e : e - a;
}
bool IsPinned(std::uint32_t bits, std::uint32_t width) {
const std::uint32_t magnitude = width == 16u ? bits & 0x7fffu : bits & 0x7fffffffu;
const std::uint32_t infinity = width == 16u ? 0x7c00u : 0x7f800000u;
return magnitude == 0u || magnitude >= infinity;
}
void Run(AgcDriver::VulkanDevice& device, const std::optional<ShaderRecompiler::ShaderFloatMode>& floatMode) {
for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), &Input[tid * Inputs]);
Output.fill(0xdeadbeefu);
std::vector<std::uint32_t> userData(8, 0u);
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u));
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
std::copy(input.begin(), input.end(), userData.begin());
std::copy(output.begin(), output.end(), userData.begin() + 4);
const std::span<const std::uint32_t> code(Code);
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{reinterpret_cast<std::uintptr_t>(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<std::uintptr_t>(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 = floatMode;
const auto result = ShaderRecompiler::Recompile(request);
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
device.WaitIdle();
}
void Check() {
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
const std::uint32_t* out = &Output[tid * Results];
for (std::uint32_t column = 0; column < Columns; ++column) {
const std::uint32_t width = column < 2u ? 32u : 16u;
const std::uint32_t mask = width == 16u ? 0xffffu : 0xffffffffu;
const std::uint32_t actual = out[column] & mask;
const std::uint32_t expected = Expected[tid][column] & mask;
const std::string name = "sin cos near zero: lane " + std::to_string(tid) + " " + Names[column] + " is " + Hex(actual) + ", expected " + Hex(expected);
if (IsPinned(expected, width)) {
Require(actual == expected, name);
continue;
}
Require(Distance(actual, expected, width) <= Tolerance[tid], name + " (tolerance " + std::to_string(Tolerance[tid]) + " ulp)");
}
}
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false});
Check();
std::puts("sin cos near zero tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
@@ -738,16 +738,31 @@ std::uint32_t EmitFPLog2(SpirvEmitterState& state, std::uint32_t arg0) {
return EmitExt(state, TypeF32(state), GLSLstd450Log2, {EmitFlushF32DenormToSignedZero(state, arg0)});
}
std::uint32_t TrigLinearNearZero(SpirvEmitterState& state, std::uint32_t value, std::uint32_t distance, std::uint32_t linearConstant) {
const auto linear = Binary(state, spv::OpFMul, TypeF32(state), distance, linearConstant);
const auto magnitude = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), distance), ConstantU32(state, 0x7fffffffu));
const auto near = Binary(state, spv::OpULessThan, TypeBool(state), magnitude, ConstantU32(state, 0x39800000u));
return Select(state, TypeF32(state), near, linear, value);
}
std::uint32_t EmitFPSin(SpirvEmitterState& state, std::uint32_t arg0) {
const auto cycle = EmitTrigCycleF32(state, arg0, true);
const auto source = Binary(state, spv::OpFMul, TypeF32(state), cycle, ConstantF32(state, 0x40c90fdbu));
return EmitExt(state, TypeF32(state), GLSLstd450Sin, {source});
const auto absolute = EmitExt(state, TypeF32(state), GLSLstd450FAbs, {cycle});
const auto signBits = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), cycle), ConstantU32(state, 0x80000000u));
const auto value = EmitExt(state, TypeF32(state), GLSLstd450Sin, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))});
const auto nearHalf = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3e800000u));
const auto distance = Select(state, TypeF32(state), nearHalf, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3f000000u), absolute), absolute);
const auto linearConstant = Select(state, TypeF32(state), nearHalf, ConstantF32(state, 0x40c90fdbu), ConstantF32(state, 0x40c90fd5u));
const auto result = TrigLinearNearZero(state, value, distance, linearConstant);
return Unary(state, spv::OpBitcast, TypeF32(state), Binary(state, spv::OpBitwiseOr, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), result), signBits));
}
std::uint32_t EmitFPCos(SpirvEmitterState& state, std::uint32_t arg0) {
const auto cycle = EmitTrigCycleF32(state, arg0, false);
const auto source = Binary(state, spv::OpFMul, TypeF32(state), cycle, ConstantF32(state, 0x40c90fdbu));
return EmitExt(state, TypeF32(state), GLSLstd450Cos, {source});
const auto absolute = EmitExt(state, TypeF32(state), GLSLstd450FAbs, {cycle});
const auto value = EmitExt(state, TypeF32(state), GLSLstd450Cos, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))});
const auto distance = Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3e800000u), absolute);
return TrigLinearNearZero(state, value, distance, ConstantF32(state, 0x40c90fdbu));
}
std::uint32_t EmitIdentity(SpirvValueEmitContext&, std::uint32_t value) {
+1 -1
View File
@@ -347,7 +347,7 @@ Throughout the project, every function at every stage either **does exactly what
- [PA_CL_CLIP_CNTL](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented.
- [10_11_11 unorm color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion.
- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. Last mode throws without `VK_EXT_provoking_vertex`, for generated rectangle, mesh or tessellation primitives, explicit per-vertex flat inputs, and triangle strips with per-vertex inputs, whose [ordering depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html).
- [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. v_sin_f32/v_cos_f32 with inputs beyond ±256 cycles were not compared with the console.
- [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0, where its log2 is up to 24 million ULP off. [v_sin and v_cos](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 10 ULP just below non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 5 ULP inside the bands. NVIDIA's sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it.
- `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead.
- [Ordered not-equal compares](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual` with both operands checked for NaN on their bits instead of `OpFOrdNotEqual`; the two agree for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of the compare feeds a ballot on a 32-wide subgroup (#825)
- The length-changing path of the [instruction rewriter](../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length.