fix(shader): refuse the f32 DS src2 operations until LDS float denormals are modelled

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Adrià Franch 2026-10-08 23:06:14 +02:00
parent c0519cc814
commit be3bd9fbd7
3 changed files with 25 additions and 19 deletions

View File

@ -22,7 +22,7 @@ constexpr std::uint32_t Results = 16;
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, 207> Code{
alignas(256) constexpr std::array<std::uint32_t, 171> Code{
0x34020084, 0xe0381000, 0x80000201, 0x340c0082, 0xbf8c3f70, 0x4a0e0cff, 0x00001000, 0x4a100c80,
0xd8340000, 0x00000207, 0xd8340000, 0x00000308, 0xda007c00, 0x00000007, 0xd8d80000, 0x0a000007,
0x4a0e0cff, 0x00001080, 0x4a100cff, 0x00002080, 0xd8340000, 0x00000207, 0xd8340000, 0x00000308,
@ -43,12 +43,8 @@ alignas(256) constexpr std::array<std::uint32_t, 207> Code{
0x4a0e0cff, 0x00001580, 0x4a100cff, 0x00002580, 0xd8340000, 0x00000207, 0xd8340000, 0x00000308,
0xda2c0400, 0x00000007, 0xd8d80000, 0x15000007, 0x4a0e0cff, 0x00001600, 0x4a100cff, 0x00000600,
0xd8340000, 0x00000207, 0xd8340000, 0x00000308, 0xda347c00, 0x00000007, 0xd8d80000, 0x16000007,
0x4a0e0cff, 0x00001680, 0x4a100cff, 0x00002680, 0xd8340000, 0x00000407, 0xd8340000, 0x00000508,
0xda480400, 0x00000007, 0xd8d80000, 0x17000007, 0x4a0e0cff, 0x00001700, 0x4a100cff, 0x00000700,
0xd8340000, 0x00000407, 0xd8340000, 0x00000508, 0xda4c7c00, 0x00000007, 0xd8d80000, 0x18000007,
0x4a0e0cff, 0x00001780, 0x4a100cff, 0x00002780, 0xd8340000, 0x00000407, 0xd8340000, 0x00000508,
0xda540400, 0x00000007, 0xd8d80000, 0x19000007, 0x34020086, 0xbf8cc07f, 0xe0781000, 0x80010a01,
0xe0781010, 0x80010e01, 0xe0781020, 0x80011201, 0xe0781030, 0x80011601, 0xbf810000,
0x34020086, 0xbf8cc07f, 0xe0781000, 0x80010a01, 0xe0781010, 0x80010e01, 0xe0781020, 0x80011201,
0xe0781030, 0x80011601, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 3> AddressOffsetCode{0xda008000, 0x00000000, 0xbf810000};
@ -64,7 +60,7 @@ void Fill(std::uint32_t tid, std::uint32_t* words) {
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, 0x01016facu};
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
}
std::string Hex(std::uint32_t value) {
@ -100,9 +96,9 @@ void Run(AgcDriver::VulkanDevice& device) {
}
void CheckLanes() {
constexpr std::array<const char*, Results> names{
constexpr std::array<const char*, 13> names{
"ds_add_src2_u32", "ds_sub_src2_u32", "ds_rsub_src2_u32", "ds_inc_src2_u32", "ds_dec_src2_u32", "ds_min_src2_i32", "ds_max_src2_i32", "ds_min_src2_u32",
"ds_max_src2_u32", "ds_and_src2_b32", "ds_or_src2_b32", "ds_xor_src2_b32", "ds_write_src2_b32", "ds_min_src2_f32", "ds_max_src2_f32", "ds_add_src2_f32",
"ds_max_src2_u32", "ds_and_src2_b32", "ds_or_src2_b32", "ds_xor_src2_b32", "ds_write_src2_b32",
};
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
const std::uint32_t* in = &Input[tid * Inputs];
@ -110,21 +106,32 @@ void CheckLanes() {
const std::uint32_t b = in[1];
const auto sa = static_cast<std::int32_t>(a);
const auto sb = static_cast<std::int32_t>(b);
const float fa = std::bit_cast<float>(in[2]);
const float fb = std::bit_cast<float>(in[3]);
const std::array<std::uint32_t, Results> expected{
const std::array<std::uint32_t, 13> expected{
a + b, a - b, b - a, a >= b ? 0u : a + 1u, (a == 0u || a > b) ? b : a - 1u,
static_cast<std::uint32_t>(std::min(sa, sb)), static_cast<std::uint32_t>(std::max(sa, sb)), std::min(a, b), std::max(a, b),
a & b, a | b, a ^ b, b,
std::bit_cast<std::uint32_t>(std::min(fa, fb)), std::bit_cast<std::uint32_t>(std::max(fa, fb)), std::bit_cast<std::uint32_t>(fa + fb),
};
for (std::uint32_t op = 0; op < Results; ++op) {
for (std::uint32_t op = 0; op < expected.size(); ++op) {
const std::uint32_t actual = Output[tid * Results + op];
Require(actual == expected[op], "lds src2: lane " + std::to_string(tid) + " " + names[op] + " is " + Hex(actual) + ", expected " + Hex(expected[op]));
}
}
}
void CheckFloatRejected(AgcDriver::VulkanDevice& device) {
for (const std::uint32_t opcode : {0x92u, 0x93u, 0x95u}) {
alignas(256) static std::array<std::uint32_t, 3> code{};
code = {0xd8000000u | (opcode << 18u) | 1u, 0x00000000u, 0xbf810000u};
bool refused = false;
try {
(void)Compile(device, code);
} catch (const std::exception& error) {
refused = std::string(error.what()).find("f32 denormal mode") != std::string::npos;
}
Require(refused, "lds src2: f32 opcode " + Hex(opcode) + " was not refused");
}
}
void CheckAddressOffsetRejected(AgcDriver::VulkanDevice& device) {
try {
(void)Compile(device, AddressOffsetCode);
@ -143,6 +150,7 @@ int main() {
Run(*device);
CheckLanes();
CheckAddressOffsetRejected(*device);
CheckFloatRejected(*device);
std::puts("lds src2 tests passed");
return 0;
} catch (const std::exception& error) {

View File

@ -366,11 +366,9 @@ bool TranslationContext::emitMemory(const RdnaInstruction& inst) {
case RdnaOpcode::DsWriteSrc2B32:
return dsSrc2(inst, IrOpcode::WriteSharedU32);
case RdnaOpcode::DsMinSrc2F32:
return dsSrc2(inst, IrOpcode::SharedAtomicFMin32);
case RdnaOpcode::DsMaxSrc2F32:
return dsSrc2(inst, IrOpcode::SharedAtomicFMax32);
case RdnaOpcode::DsAddSrc2F32:
return dsSrc2(inst, IrOpcode::SharedAtomicFAdd32);
throw std::runtime_error("f32 DS src2 operations depend on the f32 denormal mode, which is not modelled for LDS float atomics");
case RdnaOpcode::DsWrxchgRtnB32:
return dsAtomic(inst, IrOpcode::SharedAtomicSwap32, true);

View File

@ -328,7 +328,7 @@ Throughout the project, every function at every stage either **does exactly what
- [Min/max S# reduction](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction (the same `SQ_IMG_FILTER_MODE` values RADV programs). Measured on RDNA2 (gfx1035), not on the console: only texels of non-zero weight are reduced, and the weight is taken after rounding to the 8-bit subtexel grid (a sample within 1/512 texel of a centre reduces that texel alone); a driver with other `subTexelPrecisionBits` reduces a different set there. Gathers ignore the mode.
- [Min/max S#](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with anisotropy, a linear mip filter, depth or color-texture compare, or bilinear filtering of an image the recompiler point-samples (sint, converted, depth-bits).
- [Raw `buffer_load_dwordx2`](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`.
- [DS src2 operations](../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) (`ds_*_src2_*`, GFX10.1 only) read `DS[B]`, with `B = A + 4 * sext(offset1[6:0]:offset0)`, and then update `DS[A]` with the matching 32-bit atomic, so the read of `B` is not atomic with the update of `A`. The form that takes the offset from the address (`offset1[7]` set), the 64-bit forms and GDS throw: whether the LDS address keeps `A[31:17]` in that form, and whether the 64-bit forms scale the offset by 4 or 8, is not documented.
- [DS src2 operations](../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) (`ds_*_src2_*`, GFX10.1 only) read `DS[B]`, with `B = A + 4 * sext(offset1[6:0]:offset0)`, and then update `DS[A]` with the matching 32-bit atomic, so the read of `B` is not atomic with the update of `A`. The form that takes the offset from the address (`offset1[7]` set), the 64-bit forms and GDS throw: whether the LDS address keeps `A[31:17]` in that form, and whether the 64-bit forms scale the offset by 4 or 8, is not documented. `ds_min_src2_f32`, `ds_max_src2_f32` and `ds_add_src2_f32` throw: on RDNA2 (gfx1035, the oracle's hardware, which has no src2 opcodes) `ds_min_f32`, `ds_max_f32` and `ds_add_f32` ignore `IEEE_MODE` but depend on whether f32 denormals are flushed (28, 28 and 12 of 196 special rows differ), and that is not modelled for LDS float atomics yet (#981).
- [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached
- [The narrow printf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) passes `%ls` and `%S` to glibc on Linux, which reads them as strings of 32-bit `wchar_t`; the guest's `wchar_t` is 16 bits (UTF-16), so these conversions fail with -1. The Windows path converts them to UTF-8, and `guest_formatting` checks wide conversions on Windows only
- [collate<char>::_Getcat](../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation.