From e32f5e161deded84d8dfec71a497f4e81b41b1dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0l=C3=A9gl?= Date: Sat, 10 Oct 2026 00:48:03 +0200 Subject: [PATCH 001/219] fix(shader): apply cube output modifiers whenever IEEE mode is off v_cubeid_f32, v_cubesc_f32, v_cubetc_f32 and v_cubema_f32 refused any output modifier with "VOP3 source modifiers are not implemented"; PPSA11386 stops there on v_cubeid_f32 v, v, -v, -v div:2. On RDNA2 (gfx1036), over 2048 inputs in every combination of IEEE mode, the four f32 denormal values and f16/f64 denormals flushed or kept, with round to nearest even, the four instructions apply mul:2, mul:4 and div:2 whenever IEEE mode is off, also with f32 output denormals kept, where v_mul_f32_e64 in the same kernels ignores them, and ignore them with IEEE mode on. Where applied, the result is the existing bit-level f32 output modifier result, and clamp is applied after it. The decoder accepts the modifier on the four instructions. floatCube applies the shared f32 scaling, now scaleF32Result, in those modes and writes with the modifier cleared, so clamp is still applied after it. With IEEE mode on the signaling NaN quieting applies and the modifier is ignored; with it off the quieting does nothing and the modifier is applied. agc_driver_vop3_cube_clamp checks each instruction with each modifier against tables captured on gfx1036. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fnrk5Dazjb2tLooVSpdRQq --- .../tests/execution/Vop3CubeClamp.cpp | 177 +++++++++++++++--- .../RdnaDecoder/src/RdnaVectorOpDecoder.cpp | 4 +- .../Translation/TranslationContext.hpp | 1 + .../Translation/src/FloatInstructions.cpp | 8 +- .../src/TranslationContext/OperandAccess.cpp | 30 +-- docs/dev/debt/shader.md | 2 +- 6 files changed, 177 insertions(+), 45 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Vop3CubeClamp.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Vop3CubeClamp.cpp index 10857e6d31..a26753bd63 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Vop3CubeClamp.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Vop3CubeClamp.cpp @@ -155,9 +155,135 @@ constexpr const char* Names[16] = { "v_cubema_f32 v25, -v4, |v5|, v6 clamp", }; -void Fill(std::uint32_t tid, std::uint32_t* words) { - std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), words); -} +alignas(256) constexpr std::array OmodCode{ + 0x34020084, 0x34060086, 0xe0381000, 0x80000401, 0xbf8c3f70, 0xd544000a, 0x0c1a0b04, 0xd544000b, + 0x1c1a0b04, 0xd545000c, 0x0c1a0b04, 0xd545000d, 0x141a0b04, 0xd545000e, 0x1c1a0b04, 0xd546000f, + 0x0c1a0b04, 0xd5460010, 0x141a0b04, 0xd5460011, 0x1c1a0b04, 0xd5470012, 0x0c1a0b04, 0xd5470013, + 0x141a0b04, 0xd5470014, 0x1c1a0b04, 0xd5440015, 0xdc1a0b04, 0xd5458216, 0x341a0b04, 0xd5468117, + 0x5c1a0b04, 0xd5478018, 0x8c1a0b04, 0xd5440019, 0x141a0b04, 0xe0781000, 0x80010a03, 0xe0781010, + 0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xbf810000, +}; + +constexpr std::uint32_t OmodRows[32][4] = { + {0x3f800000u, 0x3f000000u, 0x3e800000u, 0x00000000u}, + {0xbf800000u, 0x3f000000u, 0x3e800000u, 0x00000000u}, + {0x3f000000u, 0x3f800000u, 0x3e800000u, 0x00000000u}, + {0x3f000000u, 0xbf800000u, 0xbe800000u, 0x00000000u}, + {0x3e800000u, 0x3f000000u, 0x3f800000u, 0x00000000u}, + {0x3e800000u, 0xbf000000u, 0xbf800000u, 0x00000000u}, + {0x40400000u, 0xc0e00000u, 0x40000000u, 0x00000000u}, + {0xc0200000u, 0x3fc00000u, 0x40c00000u, 0x00000000u}, + {0x7f7fffffu, 0x7f000000u, 0xff000000u, 0x00000000u}, + {0xff7fffffu, 0x7e800000u, 0x7effffffu, 0x00000000u}, + {0x3f800000u, 0x00800000u, 0x80ffffffu, 0x00000000u}, + {0x3f800000u, 0x01000000u, 0x81000000u, 0x00000000u}, + {0x3f800000u, 0x00000007u, 0x80000007u, 0x00000000u}, + {0x3f800000u, 0x80000000u, 0x00000000u, 0x00000000u}, + {0x00800000u, 0x00000000u, 0x80000000u, 0x00000000u}, + {0x00000007u, 0x00000003u, 0x00000001u, 0x00000000u}, + {0x7f800000u, 0x3f800000u, 0x40000000u, 0x00000000u}, + {0xff800000u, 0x7f800000u, 0x3f800000u, 0x00000000u}, + {0x7fc00000u, 0x3f800000u, 0x3f000000u, 0x00000000u}, + {0x3f800000u, 0xffc00000u, 0x3f000000u, 0x00000000u}, + {0x40000000u, 0x3f800000u, 0x7fc00000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u}, + {0x7149f2cau, 0xf149f2cau, 0x71c9f2cau, 0x00000000u}, + {0x0da24260u, 0x0e224260u, 0x8e736390u, 0x00000000u}, + {0x7e800000u, 0x7e7fffffu, 0x3f800000u, 0x00000000u}, + {0x40000000u, 0x7e800000u, 0x7effffffu, 0x00000000u}, + {0x3f000000u, 0xbf000000u, 0x3f000000u, 0x00000000u}, + {0xc0c5edadu, 0xc08852a3u, 0xc0c18eacu, 0x00000000u}, + {0xbf3d7ad8u, 0xc0737ab3u, 0xc0e5e3c1u, 0x00000000u}, + {0x3f000000u, 0x3e800000u, 0xff812345u, 0x00000000u}, + {0x7f812345u, 0x3f000000u, 0x3e800000u, 0x00000000u} +}; +constexpr std::uint32_t OmodExpected[32][16] = { + {0x00000000u, 0x00000000u, 0xbf000000u, 0xbf800000u, 0xbe000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x3e800000u, 0x3f800000u, 0x00000000u}, + {0x40000000u, 0x3f000000u, 0x3f000000u, 0x3f800000u, 0x3e000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0x3f000000u, 0x00000000u, 0x3e800000u, 0x00000000u, 0x40800000u}, + {0x40800000u, 0x3f800000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0x3f000000u, 0x3f800000u, 0x3e000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x3fc00000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x41000000u}, + {0x40c00000u, 0x3fc00000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0x3f000000u, 0x3f800000u, 0x3e000000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41400000u}, + {0x41000000u, 0x40000000u, 0x3f000000u, 0x3f800000u, 0x3e000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x40200000u, 0x00000000u, 0x3e800000u, 0x00000000u, 0x41800000u}, + {0x41200000u, 0x40200000u, 0xbf000000u, 0xbf800000u, 0xbe000000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0x40000000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x41a00000u}, + {0x40c00000u, 0x3fc00000u, 0x40c00000u, 0x41400000u, 0x3fc00000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0xc1e00000u, 0xc2600000u, 0xc0e00000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x41400000u}, + {0x41000000u, 0x40000000u, 0xc0a00000u, 0xc1200000u, 0xbfa00000u, 0xc0400000u, 0xc0c00000u, 0xbf400000u, 0x41c00000u, 0x42400000u, 0x40c00000u, 0x40200000u, 0x3f800000u, 0x3f400000u, 0x00000000u, 0x41800000u}, + {0x00000000u, 0x00000000u, 0x7f800000u, 0x7f800000u, 0x7e800000u, 0xff800000u, 0xff800000u, 0xfe800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x40000000u, 0x3f000000u, 0x7f7fffffu, 0x7f800000u, 0x7e7fffffu, 0xff000000u, 0xff800000u, 0xfe000000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x3f000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x40800000u}, + {0x00000000u, 0x00000000u, 0x017fffffu, 0x01ffffffu, 0x00000000u, 0x81000000u, 0x81800000u, 0x80000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x01800000u, 0x02000000u, 0x00800000u, 0x81800000u, 0x82000000u, 0x80800000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00800000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01800000u, 0x02000000u, 0x00800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01800000u, 0x00000000u}, + {0x41000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x00000000u, 0x00000000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0xc0000000u, 0xc0800000u, 0xbf000000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x00000000u, 0x3f800000u, 0x3f000000u, 0x3f800000u, 0x00000000u}, + {0x40800000u, 0x3f800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x40000000u, 0x40800000u, 0x3f000000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x3fc00000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x41000000u}, + {0x00000000u, 0x00000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0xc0000000u, 0xc0800000u, 0xbf000000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xc0000000u, 0xc0800000u, 0xbf000000u, 0x41000000u, 0x41800000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x3f000000u, 0x3f800000u, 0x00000000u}, + {0x41000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x41000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x41000000u, 0x40000000u, 0x71c9f2cau, 0x7249f2cau, 0x70c9f2cau, 0x71c9f2cau, 0x7249f2cau, 0x70c9f2cau, 0x72c9f2cau, 0x7349f2cau, 0x71c9f2cau, 0x40200000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x41200000u, 0x40200000u, 0x8e224260u, 0x8ea24260u, 0x8d224260u, 0x8ea24260u, 0x8f224260u, 0x8da24260u, 0x8f736390u, 0x8ff36390u, 0x8e736390u, 0x40000000u, 0x0ea24260u, 0x0da24260u, 0x0f736390u, 0x41a00000u}, + {0x00000000u, 0x00000000u, 0xc0000000u, 0xc0800000u, 0xbf000000u, 0xfeffffffu, 0xff7fffffu, 0xfdffffffu, 0x7f800000u, 0x7f800000u, 0x7e800000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x41000000u, 0x40000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0xff000000u, 0xff800000u, 0xfe000000u, 0x7f800000u, 0x7f800000u, 0x7effffffu, 0x40200000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x41800000u}, + {0x41000000u, 0x40000000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0x40000000u, 0x40800000u, 0x3f000000u, 0x40200000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x40000000u, 0x3f000000u, 0xc1418eacu, 0xc1c18eacu, 0xc0418eacu, 0x410852a3u, 0x418852a3u, 0x400852a3u, 0xc1c5edadu, 0xc245edadu, 0xc0c5edadu, 0x3f000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x41200000u, 0x40200000u, 0x3fbd7ad8u, 0x403d7ad8u, 0x3ebd7ad8u, 0x40f37ab3u, 0x41737ab3u, 0x3ff37ab3u, 0xc1e5e3c1u, 0xc265e3c1u, 0xc0e5e3c1u, 0x40000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x41a00000u}, + {0x00000000u, 0x00000000u, 0x7f812345u, 0x7f812345u, 0x7f812345u, 0xbf000000u, 0xbf800000u, 0xbe000000u, 0x40000000u, 0x40800000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0x3e000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xbf000000u, 0xbf800000u, 0xbe000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0x7f812345u, 0x7f812345u, 0x7f812345u, 0x00000000u, 0x00000000u, 0x3e800000u, 0x00000000u, 0x00000000u} +}; +constexpr std::uint32_t OmodExpectedIeee[32][16] = { + {0x00000000u, 0x00000000u, 0xbe800000u, 0xbe800000u, 0xbe800000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x3e800000u, 0x3f000000u, 0x3f800000u, 0x00000000u}, + {0x3f800000u, 0x3f800000u, 0x3e800000u, 0x3e800000u, 0x3e800000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0x3f800000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x3f800000u}, + {0x40000000u, 0x40000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3e800000u, 0x3e800000u, 0x3e800000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x40400000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x40000000u}, + {0x40400000u, 0x40400000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3e800000u, 0x3e800000u, 0x3e800000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40400000u}, + {0x40800000u, 0x40800000u, 0x3e800000u, 0x3e800000u, 0x3e800000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x40a00000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x40800000u}, + {0x40a00000u, 0x40a00000u, 0xbe800000u, 0xbe800000u, 0xbe800000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0x40800000u, 0x3e800000u, 0x00000000u, 0x3f800000u, 0x40a00000u}, + {0x40400000u, 0x40400000u, 0x40400000u, 0x40400000u, 0x40400000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0xc1600000u, 0xc1600000u, 0xc1600000u, 0x40000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x40400000u}, + {0x40800000u, 0x40800000u, 0xc0200000u, 0xc0200000u, 0xc0200000u, 0xbfc00000u, 0xbfc00000u, 0xbfc00000u, 0x41400000u, 0x41400000u, 0x41400000u, 0x40a00000u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x40800000u}, + {0x00000000u, 0x00000000u, 0x7f000000u, 0x7f000000u, 0x7f000000u, 0xff000000u, 0xff000000u, 0xff000000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x3f800000u, 0x3f800000u, 0x7effffffu, 0x7effffffu, 0x7effffffu, 0xfe800000u, 0xfe800000u, 0xfe800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x3f800000u}, + {0x00000000u, 0x00000000u, 0x00ffffffu, 0x00ffffffu, 0x00ffffffu, 0x80800000u, 0x80800000u, 0x80800000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00800000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x01000000u, 0x01000000u, 0x01000000u, 0x81000000u, 0x81000000u, 0x81000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000007u, 0x00000007u, 0x00000007u, 0x80000007u, 0x80000007u, 0x80000007u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000007u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x01000000u, 0x01000000u, 0x01000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00000000u}, + {0x40800000u, 0x40800000u, 0x00000007u, 0x00000007u, 0x00000007u, 0x80000003u, 0x80000003u, 0x80000003u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x00000000u, 0x00000003u, 0x00000000u, 0x40800000u}, + {0x00000000u, 0x00000000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x40000000u, 0x40000000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x40400000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x40000000u}, + {0x00000000u, 0x00000000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0x40800000u, 0x40800000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x40800000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x40800000u, 0x40800000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x40800000u, 0x40800000u, 0x7149f2cau, 0x7149f2cau, 0x7149f2cau, 0x7149f2cau, 0x7149f2cau, 0x7149f2cau, 0x7249f2cau, 0x7249f2cau, 0x7249f2cau, 0x40a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x40a00000u, 0x40a00000u, 0x8da24260u, 0x8da24260u, 0x8da24260u, 0x8e224260u, 0x8e224260u, 0x8e224260u, 0x8ef36390u, 0x8ef36390u, 0x8ef36390u, 0x40800000u, 0x0da24260u, 0x0e224260u, 0x0ef36390u, 0x40a00000u}, + {0x00000000u, 0x00000000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0xfe7fffffu, 0xfe7fffffu, 0xfe7fffffu, 0x7f000000u, 0x7f000000u, 0x7f000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x40800000u, 0x40800000u, 0x40000000u, 0x40000000u, 0x40000000u, 0xfe800000u, 0xfe800000u, 0xfe800000u, 0x7f7fffffu, 0x7f7fffffu, 0x7f7fffffu, 0x40a00000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x40800000u}, + {0x40800000u, 0x40800000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x40a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x3f800000u, 0x3f800000u, 0xc0c18eacu, 0xc0c18eacu, 0xc0c18eacu, 0x408852a3u, 0x408852a3u, 0x408852a3u, 0xc145edadu, 0xc145edadu, 0xc145edadu, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x3f800000u}, + {0x40a00000u, 0x40a00000u, 0x3f3d7ad8u, 0x3f3d7ad8u, 0x3f3d7ad8u, 0x40737ab3u, 0x40737ab3u, 0x40737ab3u, 0xc165e3c1u, 0xc165e3c1u, 0xc165e3c1u, 0x40800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x40a00000u}, + {0x00000000u, 0x00000000u, 0x7fc12345u, 0x7fc12345u, 0x7fc12345u, 0xbe800000u, 0xbe800000u, 0xbe800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x3e800000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xbe800000u, 0xbe800000u, 0xbe800000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0x7fc12345u, 0x7fc12345u, 0x7fc12345u, 0x00000000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x00000000u} +}; +constexpr const char* OmodNames[16] = { + "v_cubeid_f32 v10, v4, v5, v6 mul:2", + "v_cubeid_f32 v11, v4, v5, v6 div:2", + "v_cubesc_f32 v12, v4, v5, v6 mul:2", + "v_cubesc_f32 v13, v4, v5, v6 mul:4", + "v_cubesc_f32 v14, v4, v5, v6 div:2", + "v_cubetc_f32 v15, v4, v5, v6 mul:2", + "v_cubetc_f32 v16, v4, v5, v6 mul:4", + "v_cubetc_f32 v17, v4, v5, v6 div:2", + "v_cubema_f32 v18, v4, v5, v6 mul:2", + "v_cubema_f32 v19, v4, v5, v6 mul:4", + "v_cubema_f32 v20, v4, v5, v6 div:2", + "v_cubeid_f32 v21, v4, -v5, -v6 div:2", + "v_cubesc_f32 v22, -v4, |v5|, v6 clamp mul:4", + "v_cubetc_f32 v23, |v4|, -v5, v6 clamp div:2", + "v_cubema_f32 v24, v4, v5, -v6 clamp mul:2", + "v_cubeid_f32 v25, v4, v5, v6 mul:4", +}; std::array BufferDescriptor(const void* data, std::uint32_t bytes) { const auto address = reinterpret_cast(data); @@ -193,33 +319,19 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std:: return ShaderRecompiler::Recompile(request); } -void Run(AgcDriver::VulkanDevice& device, const std::optional& floatMode) { - for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::uint32_t (&rows)[32][4], const std::optional& 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); - const std::span code(Code); const auto result = Compile(device, code, floatMode); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); } -void CheckOmodRefused(AgcDriver::VulkanDevice& device) { - for (const std::uint32_t opcode : {0x144u, 0x145u, 0x146u, 0x147u}) { - alignas(256) const std::array code{0xd400000au | (opcode << 16u), 0x0c1a0b04u, 0xbf810000u}; - std::string refusal; - try { - static_cast(Compile(device, code)); - } catch (const std::exception& error) { - refusal = error.what(); - } - Require(refusal.find("VOP3 source modifiers are not implemented") != std::string::npos, "VOP3 opcode " + Hex(opcode) + " with omod was not refused"); - } -} - -void Check(const std::uint32_t (&expected)[32][16], const char* mode) { +void Check(const std::uint32_t (&expected)[32][16], const char* const (&names)[16], const char* mode) { for (std::uint32_t tid = 0; tid < Threads; ++tid) { const std::uint32_t* out = &Output[tid * Results]; for (std::uint32_t i = 0; i < 16; ++i) { - Expect(tid, out[i], expected[tid][i], (std::string(mode) + " " + Names[i]).c_str()); + Expect(tid, out[i], expected[tid][i], (std::string(mode) + " " + names[i]).c_str()); } } } @@ -230,13 +342,22 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device, std::nullopt); - Check(Expected, "no float mode"); - Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); - Check(Expected, "IEEE=0"); - Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); - Check(ExpectedIeee, "IEEE=1"); - CheckOmodRefused(*device); + Run(*device, Code, Rows, std::nullopt); + Check(Expected, Names, "no float mode"); + Run(*device, Code, Rows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); + Check(Expected, Names, "IEEE=0"); + Run(*device, Code, Rows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); + Check(ExpectedIeee, Names, "IEEE=1"); + Run(*device, OmodCode, OmodRows, std::nullopt); + Check(OmodExpected, OmodNames, "no float mode"); + Run(*device, OmodCode, OmodRows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); + Check(OmodExpected, OmodNames, "IEEE=0 f32 denormals flushed"); + Run(*device, OmodCode, OmodRows, ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}); + Check(OmodExpected, OmodNames, "IEEE=0 f32 denormals kept"); + Run(*device, OmodCode, OmodRows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); + Check(OmodExpectedIeee, OmodNames, "IEEE=1 f32 denormals flushed"); + Run(*device, OmodCode, OmodRows, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}); + Check(OmodExpectedIeee, OmodNames, "IEEE=1 f32 denormals kept"); std::puts("vop3 cube clamp tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index dd297de461..02ccb674b8 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -1935,7 +1935,7 @@ bool supportsNativeVop3SourceModifiers(RdnaOpcode opcode) { } bool supportsNativeVop3ResultModifiers(RdnaOpcode opcode) { - if (isVop1FloatResultOpcode(opcode) || isNativeVop3F16TernaryOpcode(opcode)) { + if (isVop1FloatResultOpcode(opcode) || isNativeVop3F16TernaryOpcode(opcode) || isCubeOpcode(opcode)) { return true; } switch (opcode) { @@ -1990,7 +1990,7 @@ bool supportsNativeVop3Clamp(RdnaOpcode opcode) { return true; } return supportsNativeVop3ResultModifiers(opcode) || usesInexactClampControl(opcode) || usesIntegerSaturateClamp(opcode) || - isVop3Integer16AddSubOpcode(opcode) || isF32DivisionStepOpcode(opcode) || isCubeOpcode(opcode); + isVop3Integer16AddSubOpcode(opcode) || isF32DivisionStepOpcode(opcode); } void checkNativeVop3Modifiers(RdnaOpcode opcode, bool permlane, bool carryInOut, bool scalarDst, std::uint32_t abs, std::uint32_t opSel, std::uint32_t clamp, std::uint32_t omod, std::uint32_t neg) { diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 711f219057..9df4e73e82 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -56,6 +56,7 @@ private: IrU1 threadBit(const std::array& mask); void writeRawU32(const RdnaOperand& operand, IrU32 value); IrF32 applyF32ResultModifiers(const RdnaOperand& operand, IrF32 value); + IrF32 scaleF32Result(std::uint32_t omod, IrF32 value); IrF32 applyF16ResultModifiers(const RdnaOperand& operand, IrF32 value); bool outputModifierApplies(std::uint32_t denormalShift) const; bool dx10Clamp() const { return !floatMode.has_value() || floatMode->dx10Clamp; } diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index 8b0ea3b902..74baede53d 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -906,7 +906,13 @@ bool TranslationContext::floatCube(const RdnaInstruction& inst, std::uint32_t re } const IrU32 bits(ir.BitCastU32(result.Value())); const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(0x7f800000u))); - writeOperand(inst.destination, &ir.BitCastF32(ir.Select(nan.Value(), quietNan32(bits).Value(), bits.Value()))); + result = IrF32(ir.BitCastF32(ir.Select(nan.Value(), quietNan32(bits).Value(), bits.Value()))); + RdnaOperand destination = inst.destination; + if (destination.omod != 0u && !ieeeMode) { + result = scaleF32Result(destination.omod, result); + } + destination.omod = 0u; + writeOperand(destination, &result.Value()); return true; } diff --git a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp index f82197547f..aac7437b80 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp @@ -370,21 +370,25 @@ std::array TranslationContext::quietNan64(const std::array& return ieeeMode ? std::array{bits[0], IrU32(ir.BitwiseOr(bits[1].Value(), ir.Constant(0x00080000u)))} : bits; } +IrF32 TranslationContext::scaleF32Result(std::uint32_t omod, IrF32 value) { + IrValue& bits = ir.BitCastU32(value.Value()); + IrValue& magnitude = ir.BitwiseAnd(bits, ir.Constant(0x7fffffffu)); + IrValue& sign = ir.BitwiseAnd(bits, ir.Constant(0x80000000u)); + IrValue* result = nullptr; + if (omod == 3u) { + result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x01000000u)), sign, ir.ISub(bits, ir.Constant(0x00800000u))); + } else { + const std::uint32_t exponentStep = omod == 1u ? 0x00800000u : 0x01000000u; + result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x7f800000u - exponentStep)), ir.IAdd(bits, ir.Constant(exponentStep)), ir.BitwiseOr(sign, ir.Constant(0x7f800000u))); + } + result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x00800000u)), ir.Constant(0u), *result); + result = &ir.Select(ir.UGreaterThan(magnitude, ir.Constant(0x7f7fffffu)), bits, *result); + return IrF32(ir.BitCastF32(*result)); +} + IrF32 TranslationContext::applyF32ResultModifiers(const RdnaOperand& operand, IrF32 value) { if (operand.omod != 0u && outputModifierApplies(4u)) { - IrValue& bits = ir.BitCastU32(value.Value()); - IrValue& magnitude = ir.BitwiseAnd(bits, ir.Constant(0x7fffffffu)); - IrValue& sign = ir.BitwiseAnd(bits, ir.Constant(0x80000000u)); - IrValue* result = nullptr; - if (operand.omod == 3u) { - result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x01000000u)), sign, ir.ISub(bits, ir.Constant(0x00800000u))); - } else { - const std::uint32_t exponentStep = operand.omod == 1u ? 0x00800000u : 0x01000000u; - result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x7f800000u - exponentStep)), ir.IAdd(bits, ir.Constant(exponentStep)), ir.BitwiseOr(sign, ir.Constant(0x7f800000u))); - } - result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x00800000u)), ir.Constant(0u), *result); - result = &ir.Select(ir.UGreaterThan(magnitude, ir.Constant(0x7f7fffffu)), bits, *result); - value = IrF32(ir.BitCastF32(*result)); + value = scaleF32Result(operand.omod, value); } if (operand.clamp) { const IrF32 saturated(ir.Emit(IrOpcode::FPSaturate32, IrType::F32, {&value.Value()})); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 7392cc8399..bf2ac573bd 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -105,7 +105,7 @@ - [Patch constants](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull programs write and domain programs read are placed by their off-chip address modulo the record stride, as the two compile apart; a domain read of a field the hull never writes, or of only a neighbouring patch's record, is not detected. - [Integer partitioning](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (`VGT_TF_PARAM.PARTITIONING` 0) is drawn with Vulkan's equal spacing, which [radeonsi programs as `PART_INTEGER`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L539-540), with factors bounded at 64 instead of fractional-odd's 63; how the console rounds a clamped factor up to a whole number of segments is not measured. Power-of-two (1) and fractional-even (3) partitioning throw. - The [relative patch index](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull and domain programs use as data is (PrimitiveID mod PRIM_GRP_SIZE) mod NUM_PATCHES, with the draw-relative instance times the patches per instance added when GE_CNTL.BREAK_WAVE_AT_EOI is clear and the instances share hull threadgroups; a domain that uses it in such a draw, and a hull without shaderDrawParameters, throw. Unmeasured on hardware; PPSA26344 programs 64 primitives for 63 patches. -- The [output modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the shader's float mode as measured on RDNA2 (gfx1035): the hardware applies it only with IEEE mode off and output denormals of the result's precision flushed (bit 1 of the `FLOAT_MODE` denormal field clear), and ignores it otherwise. On f32 results it is applied as the hardware does with f32 denormals flushed; a shader without a known float mode is translated with IEEE mode off and f32 denormals flushed, the mode titles use. On f16 and f64 results it is ignored, and it throws in the modes where the hardware applies it. +- The [output modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the shader's float mode as measured on RDNA2 (gfx1035): the hardware applies it only with IEEE mode off and output denormals of the result's precision flushed (bit 1 of the `FLOAT_MODE` denormal field clear), and ignores it otherwise. On f32 results it is applied as the hardware does with f32 denormals flushed; a shader without a known float mode is translated with IEEE mode off and f32 denormals flushed, the mode titles use. On f16 and f64 results it is ignored, and it throws in the modes where the hardware applies it. `v_cubeid_f32`, `v_cubesc_f32`, `v_cubetc_f32` and `v_cubema_f32`, measured on RDNA2 (gfx1036) with `DX10_CLAMP`=1, apply it whenever IEEE mode is off, in all four f32 denormal modes (`v_mul_f32_e64` in the same measurement follows the rule above), with the same bit-level result as the f32 path, and ignore it with IEEE mode on; clamp is applied after it. Only round to nearest even is modelled: in the other round modes (measured with IEEE mode off) gfx1036 rounds an overflow as the mode says (to the largest finite value when rounding toward zero), in `v_cubema_f32`'s own result and in the `mul:2` and `mul:4` scaling, where the translation gives infinity, and when rounding toward negative infinity a zero `v_cubema_f32` result with a NaN input is -0, where the translation gives +0. - The [clamp modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows `DX10_CLAMP`, measured on RDNA2 (gfx1035) for f32, f16, packed f16 and f64 results at IEEE=0 and IEEE=1: with `DX10_CLAMP`=1 (and in shaders without a known float mode) a NaN result clamps to +0, including the quieted NaN that `v_pk_min_f16` and `v_pk_max_f16` return for a signaling NaN operand with IEEE=1 (measured on RDNA2 gfx1036 at both `IEEE_MODE` and both `DX10_CLAMP` settings); with `DX10_CLAMP`=0 a NaN result is written unchanged and every other value clamps to [0, 1] as before. f64 results follow the same rule on hardware, but a clamped f64 result with `DX10_CLAMP`=0 throws until it has an execution test. With `DX10_CLAMP`=0 the NaN's bits are those the translation already produces for the instruction without clamp, so where those are left to the driver (f32 and scalar f16 arithmetic) the payload is the driver's, not the hardware's input propagation. `v_dot2_f32_f16` and the integer clamps are unchanged. - [FP16_OVFL](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows the shader's float mode as measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f16 denormals kept and flushed: with `FP16_OVFL`=1 an f16 result that rounds to ±infinity is written as ±0x7bff unless a source operand is infinite, so `v_rcp_f16`, `v_rsq_f16` and `v_log_f16` of ±0 and `v_exp_f16` of 16 or more are clamped while an infinite input passes through, and `v_div_fixup_f16` keeps the infinity it writes for a zero denominator or an infinite numerator and clamps any other infinite quotient, a NaN one included. `v_cvt_pkrtz_f16_f32` rounds toward zero and never overflows. The sources are checked as the f32 values the translation computes with; the rule is applied to `v_fma_mix*_f16` with f32 sources and to `v_interp_p2_f16` without a measurement of infinite f32 sources or parameters. Round modes other than nearest even are not modelled, so the overflow threshold is the nearest-even one (65520). - [Directed f16 rounding](../../../core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp) for `v_cvt_f16_f32`, `v_cvt_f16_i16`, `v_cvt_f16_u16` and `v_ldexp_f16` follows `FLOAT_MODE` bits 3:2 only when f16 denormals are preserved (bits 7:6 = 3), measured on RDNA2 (gfx1036). With f16 denormals flushed (bits 7:6 = 0), or flushed on one side (modes 1 and 2, not measured), the hardware also flushes the f16 result and the directed path does not model that, so those shaders keep round-to-nearest-even. The `clamp` modifier with directed rounding is not measured. From 09f574989e8cf1f84709c7bfff4d87b2fb276477 Mon Sep 17 00:00:00 2001 From: Mehran Date: Thu, 15 Oct 2026 21:37:57 +0300 Subject: [PATCH 002/219] fix(agc): record a multisampled target's commands on its own device --- core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 15da911cfc..df2e758ff4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -43,6 +43,7 @@ VkClearColorValue fastClearColor(const ColorTarget& target) { template void recordCommands(const Context& context, Record&& record) { auto* recorder = Recorder::Active(); + if (recorder != nullptr && recorder->Device() != context.device) recorder = nullptr; std::unique_ptr batch; if (recorder == nullptr) batch = std::make_unique(context); record(recorder != nullptr ? recorder->Commands() : batch->Handle(), recorder); From 82dd2861926694ef44052893ced778a44cacbb74 Mon Sep 17 00:00:00 2001 From: Nicolas Abosch <88494836+nicolasabosch@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:59:32 -0300 Subject: [PATCH 003/219] perf(libkernel): end short sleeps when the GPU or a flip completes A sleep of 1 ms or less returns once the AGC driver sees a GPU batch complete (a thread waits on the recorder's timeline semaphore), runs a batch's completion actions, or VideoOut completes a flip. A thread returns early at most 8 times in a run of back-to-back sleeps; the run restarts when the thread submits AGC work or did not sleep for the requested time. APS5_NO_SLEEP_WAKE=1 keeps every sleep in full. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/Driver/EntryPoints.cpp | 2 + .../Graphics/include/Recorder.hpp | 2 + .../libSceAgcDriver/Graphics/src/Recorder.cpp | 64 ++++++++++ .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 2 + core/libs/prx/libkernel/CMakeLists.txt | 11 ++ core/libs/prx/libkernel/Time/SleepWake.cpp | 57 +++++++++ core/libs/prx/libkernel/Time/Time.cpp | 3 +- core/libs/prx/libkernel/Time/TimedWait.cpp | 87 +++++++++++++ .../prx/libkernel/Time/include/SleepWake.hpp | 22 ++++ .../prx/libkernel/Time/include/TimedWait.hpp | 2 + core/libs/tests/GuestSleepWake.cpp | 114 ++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 12 files changed, 366 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libkernel/Time/SleepWake.cpp create mode 100644 core/libs/prx/libkernel/Time/include/SleepWake.hpp create mode 100644 core/libs/tests/GuestSleepWake.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index 01693be19c..360a39203a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -1,10 +1,12 @@ #include "prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include namespace AgcDriver { void Submit(const Packet* packet, std::uint32_t queue) try { + KernelNoteGpuSubmit_nid_no_patch(); DriverDetail::Driver::Get().Submit(packet, queue); } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 30e547aaa6..dfb2d66507 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -786,6 +786,8 @@ private: Context context; VkSemaphore timeline = VK_NULL_HANDLE; + struct ProgressWatcher; + std::unique_ptr progressWatcher; // Identity for a thread that released the mutex around a wait (syncThroughUnlocked): the // recorder may have been torn down meanwhile, so it is looked up by this id, not by pointer. std::uint64_t id = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index d186a3876d..f59535b1b6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -13,6 +13,7 @@ #include "prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp" #include "prx/libc/include/CpuTopology.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include #include #include @@ -1072,6 +1073,60 @@ void FlushForAccess(std::uint64_t address, std::size_t bytes) { } +struct Recorder::ProgressWatcher { + std::mutex mutex; + std::condition_variable wake; + std::uint64_t submitted = 0; + bool stop = false; + std::thread thread; + + ProgressWatcher(VkDevice device, VkSemaphore timeline, PFN_vkWaitSemaphoresKHR waitSemaphores, PFN_vkGetSemaphoreCounterValueKHR counterValue) { + thread = std::thread([this, device, timeline, waitSemaphores, counterValue] { + CpuTopology::PinHelperThread("GPU progress watcher"); + std::uint64_t seen = 0; + for (;;) { + { + std::unique_lock lock(mutex); + wake.wait(lock, [&] { return stop || submitted > seen; }); + if (stop) return; + } + const std::uint64_t target = seen + 1; + VkSemaphoreWaitInfoKHR info{VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO_KHR}; + info.semaphoreCount = 1; + info.pSemaphores = &timeline; + info.pValues = ⌖ + const auto result = waitSemaphores(device, &info, 50'000'000ull); + if (result == VK_TIMEOUT) continue; + if (result != VK_SUCCESS) { + std::fprintf(stderr, "[gpu] progress watcher: vkWaitSemaphoresKHR failed (%d), watcher stops\n", static_cast(result)); + return; + } + std::uint64_t value = target; + if (counterValue(device, timeline, &value) != VK_SUCCESS) value = target; + seen = std::max(target, value); + KernelWakeSleepers_nid_no_patch(); + } + }); + } + + void Submitted(std::uint64_t serial) { + { + std::lock_guard lock(mutex); + submitted = serial; + } + wake.notify_one(); + } + + ~ProgressWatcher() { + { + std::lock_guard lock(mutex); + stop = true; + } + wake.notify_one(); + thread.join(); + } +}; + Recorder::Recorder(const Context& context, bool timelineSemaphores) : context(context), id(nextRecorderId.fetch_add(1)) { { std::lock_guard lock(liveRecordersMutex); @@ -1118,6 +1173,7 @@ Recorder::~Recorder() { } catch (const std::exception& error) { std::fprintf(stderr, "[gpu] recorder teardown: %s\n", error.what()); } + progressWatcher.reset(); holdKept.clear(); if (activeRecorder == this) { activeRecorder = nullptr; @@ -3359,6 +3415,13 @@ void Recorder::Submit() { newestSubmitted = batch->serial; newestSubmittedAt = batch->submittedAt; } + if (timeline != VK_NULL_HANDLE) { + if (!progressWatcher) { + progressWatcher = std::make_unique(context.device, timeline, context.Function("vkWaitSemaphoresKHR"), + context.Function("vkGetSemaphoreCounterValueKHR")); + } + progressWatcher->Submitted(batch->serial); + } inFlight.push_back(std::move(batch)); inFlightKeptBytes += inFlight.back()->keptBytes; if (activeRecorder == this) pendingLabelSince.store(NoPendingLabel, std::memory_order_release); @@ -3844,6 +3907,7 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source, bool } } } + if (!batch->completions.empty()) KernelWakeSleepers_nid_no_patch(); if (profile) holdCounters.completions += batch->completions.size(); const auto keptStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; // Deferred only from inside a hold: the unlock that releases the list is this thread's own, and diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 96b972a435..2fb7807fc2 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -19,6 +19,7 @@ #include "prx/libScePad/include/PadState.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include "prx/libSceVideoOut/include/VideoOutDriver.hpp" #if APS5_ENABLE_TIMING_LOG #include "prx/libSceVideoOut/include/FrameTimingLog.hpp" @@ -622,6 +623,7 @@ void VideoOutDriver::processFlip(FlipRequest& req) { } req.terminal = true; req.cfg->vblankCond.notify_all(); + KernelWakeSleepers_nid_no_patch(); timing.Mark("notify_game"); } diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 21cf02d628..3d1435ae6d 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -69,6 +69,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SignalSets.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Notification.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/UserContext.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Time/SleepWake.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/Time.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/TimedWait.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/AppMetadata.cpp @@ -576,6 +577,16 @@ if(BUILD_TESTING) set_tests_properties(guest_nanosleep PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_sleep_wake_tests ${LIBS_INCLUDE_DIR}/tests/GuestSleepWake.cpp) + target_include_directories(guest_sleep_wake_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sleep_wake_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sleep_wake_tests) + add_test(NAME guest_sleep_wake COMMAND guest_sleep_wake_tests) + set_tests_properties(guest_sleep_wake PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_sleep_wake PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_sem_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSem.cpp) target_include_directories(guest_pthread_sem_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_sem_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Time/SleepWake.cpp b/core/libs/prx/libkernel/Time/SleepWake.cpp new file mode 100644 index 0000000000..a6a774e8cb --- /dev/null +++ b/core/libs/prx/libkernel/Time/SleepWake.cpp @@ -0,0 +1,57 @@ +#include "prx/libkernel/Time/include/SleepWake.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" + +#include +#include + +namespace SleepWake { + +namespace { + +struct ThreadState { + unsigned earlyWakes = 0; + std::uint64_t lastSleepEndNanos = 0; + bool sleeping = false; +}; + +thread_local ThreadState state; + +std::atomic completions{0}; + +bool Enabled() { + static const bool enabled = std::getenv("APS5_NO_SLEEP_WAKE") == nullptr; + return enabled; +} + +} + +void Sleep(std::uint64_t nanos) { + bool woken = false; + if (nanos != 0 && nanos <= MAX_WAKEABLE_NANOS && !state.sleeping && Enabled()) { + if (TimedWait::NowNanos() - state.lastSleepEndNanos >= nanos) state.earlyWakes = 0; + if (state.earlyWakes < EARLY_WAKES_PER_RUN) { + state.sleeping = true; + const bool early = TimedWait::SleepNanosUnlessChanged(nanos, completions, completions.load(std::memory_order_seq_cst)); + state.sleeping = false; + if (early) ++state.earlyWakes; + woken = true; + } + } + if (!woken) TimedWait::SleepNanos(nanos); + state.lastSleepEndNanos = TimedWait::NowNanos(); +} + +} + +extern "C" { + +void KernelWakeSleepers_nid_no_patch() { + SleepWake::completions.fetch_add(1, std::memory_order_seq_cst); + TimedWait::WakeChangeSleepers(); +} + +void KernelNoteGpuSubmit_nid_no_patch() { + SleepWake::state.earlyWakes = 0; +} + +} diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index e25277e821..f3c5ef190e 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/Time/include/Time.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libc/include/General.hpp" @@ -244,7 +245,7 @@ void KernelTraceWait_nid_postfix(const char* kind, const void* caller, std::uint static void CancellableSleep(std::uint64_t nanos) { ThreadCancel::Check(); - TimedWait::SleepNanos(nanos); + SleepWake::Sleep(nanos); ThreadCancel::Check(); } diff --git a/core/libs/prx/libkernel/Time/TimedWait.cpp b/core/libs/prx/libkernel/Time/TimedWait.cpp index c4ae764f64..15250d04e1 100644 --- a/core/libs/prx/libkernel/Time/TimedWait.cpp +++ b/core/libs/prx/libkernel/Time/TimedWait.cpp @@ -1,9 +1,11 @@ #include "prx/libkernel/Time/include/TimedWait.hpp" #include "prx/libkernel/Pthread/include/WindowsThreadLocal.hpp" +#include #include #include #include +#include #ifdef _WIN32 #include @@ -19,6 +21,28 @@ namespace { constexpr std::uint64_t YIELD_BELOW_NANOS = 50000ULL; constexpr std::uint64_t LEAD_NANOS = 500000ULL; +std::mutex& ChangeSleepersMutex() { + static std::mutex mutex; + return mutex; +} + +std::atomic& ChangeSleeperCount() { + static std::atomic count{0}; + return count; +} + +#ifdef _WIN32 +std::vector& ChangeSleepers() { + static std::vector sleepers; + return sleepers; +} +#else +std::condition_variable& ChangeCondition() { + static std::condition_variable condition; + return condition; +} +#endif + struct ThreadWaitState { std::atomic own{0}; std::atomic* bound = &own; @@ -114,12 +138,14 @@ struct Waiter { Waiter* next = nullptr; Waiter* nested = nullptr; const void* key = nullptr; + HANDLE wake = nullptr; bool queued = false; bool busy = false; ~Waiter() { delete nested; if (event) CloseHandle(event); + if (wake) CloseHandle(wake); if (timer) CloseHandle(timer); for (const auto sleepTimer : sleepTimers) if (sleepTimer) CloseHandle(sleepTimer); } @@ -426,6 +452,67 @@ void SleepUntil(std::uint64_t deadlineNanos) { #endif } +bool SleepNanosUnlessChanged(std::uint64_t nanos, const std::atomic& counter, std::uint64_t observed) { + const std::uint64_t deadlineNanos = NowNanos() + nanos; +#ifdef _WIN32 + Waiter* waiter = ThisThreadWaiter(); + if (!waiter->wake) waiter->wake = CreateEventW(nullptr, FALSE, FALSE, nullptr); + if (!waiter->wake) throw std::runtime_error("TimedWait: CreateEventW failed for a wakeable sleep"); + { + std::lock_guard lock(ChangeSleepersMutex()); + ChangeSleepers().push_back(waiter->wake); + } + ChangeSleeperCount().fetch_add(1, std::memory_order_seq_cst); + struct Unregister { + HANDLE wake; + ~Unregister() { + ChangeSleeperCount().fetch_sub(1, std::memory_order_seq_cst); + std::lock_guard lock(ChangeSleepersMutex()); + auto& sleepers = ChangeSleepers(); + sleepers.erase(std::find(sleepers.begin(), sleepers.end(), wake)); + } + } unregister{waiter->wake}; + std::uint64_t now = NowNanos(); + if (counter.load(std::memory_order_seq_cst) != observed) return true; + if (deadlineNanos > now + LEAD_NANOS) { + const HANDLE handles[2] = {ThreadTimer(1), waiter->wake}; + LARGE_INTEGER due{}; + due.QuadPart = -static_cast((deadlineNanos - now - LEAD_NANOS) / 100ULL); + if (due.QuadPart == 0) due.QuadPart = -1; + if (handles[0] && SetWaitableTimer(handles[0], &due, 0, nullptr, nullptr, FALSE)) { + if (AlertableWait(2, handles, INFINITE) == WAIT_OBJECT_0 + 1) CancelWaitableTimer(handles[0]); + } else { + AlertableWait(1, &handles[1], WholeMilliseconds(deadlineNanos - now - LEAD_NANOS)); + } + if (counter.load(std::memory_order_seq_cst) != observed) return true; + now = NowNanos(); + } + while (now < deadlineNanos) { + if (counter.load(std::memory_order_seq_cst) != observed) return true; + YieldProcessor(); + now = NowNanos(); + } + return false; +#else + std::unique_lock lock(ChangeSleepersMutex()); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::nanoseconds(nanos); + ChangeSleeperCount().fetch_add(1, std::memory_order_seq_cst); + const bool changed = ChangeCondition().wait_until(lock, deadline, [&] { return counter.load(std::memory_order_seq_cst) != observed; }); + ChangeSleeperCount().fetch_sub(1, std::memory_order_seq_cst); + return changed; +#endif +} + +void WakeChangeSleepers() { + if (ChangeSleeperCount().load(std::memory_order_seq_cst) == 0) return; + std::lock_guard lock(ChangeSleepersMutex()); +#ifdef _WIN32 + for (const HANDLE sleeper : ChangeSleepers()) SetEvent(sleeper); +#else + ChangeCondition().notify_all(); +#endif +} + void SleepNanos(std::uint64_t nanos) { if (nanos == 0) return; #ifdef _WIN32 diff --git a/core/libs/prx/libkernel/Time/include/SleepWake.hpp b/core/libs/prx/libkernel/Time/include/SleepWake.hpp new file mode 100644 index 0000000000..47185c0cb1 --- /dev/null +++ b/core/libs/prx/libkernel/Time/include/SleepWake.hpp @@ -0,0 +1,22 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_TIME_INCLUDE_SLEEPWAKE_HPP +#define CORE_LIBS_PRX_LIBKERNEL_TIME_INCLUDE_SLEEPWAKE_HPP + +#include + +namespace SleepWake { + +constexpr std::uint64_t MAX_WAKEABLE_NANOS = 1000000ULL; +constexpr unsigned EARLY_WAKES_PER_RUN = 8; + +void Sleep(std::uint64_t nanos); + +} + +extern "C" { + +void KernelWakeSleepers_nid_no_patch(); +void KernelNoteGpuSubmit_nid_no_patch(); + +} + +#endif diff --git a/core/libs/prx/libkernel/Time/include/TimedWait.hpp b/core/libs/prx/libkernel/Time/include/TimedWait.hpp index 446b0c73f3..2f1725819d 100644 --- a/core/libs/prx/libkernel/Time/include/TimedWait.hpp +++ b/core/libs/prx/libkernel/Time/include/TimedWait.hpp @@ -16,6 +16,8 @@ std::uint64_t DeadlineNanos(std::uint64_t microseconds); std::uint64_t RemainingMicros(std::uint64_t deadlineNanos); void SleepNanos(std::uint64_t nanos); void SleepUntil(std::uint64_t deadlineNanos); +bool SleepNanosUnlessChanged(std::uint64_t nanos, const std::atomic& counter, std::uint64_t observed); +void WakeChangeSleepers(); struct Waiter; diff --git a/core/libs/tests/GuestSleepWake.cpp b/core/libs/tests/GuestSleepWake.cpp new file mode 100644 index 0000000000..edf9fad89d --- /dev/null +++ b/core/libs/tests/GuestSleepWake.cpp @@ -0,0 +1,114 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); +void KernelWakeSleepers_nid_no_patch(); +void KernelNoteGpuSubmit_nid_no_patch(); +} + +static constexpr KernelUseconds WAKEABLE_MICROS = 1000; +static constexpr KernelUseconds LONG_MICROS = 2000; +static constexpr unsigned EARLY_WAKES_PER_RUN = 8; +static constexpr unsigned TRIES = 20; +static constexpr auto EARLY = std::chrono::microseconds(900); + +static void Require(bool value) { if (!value) std::abort(); } + +static std::chrono::microseconds Usleep(KernelUseconds microseconds) { + const auto start = std::chrono::steady_clock::now(); + Require(sceKernelUsleep_nid_postfix(microseconds) == 0); + return std::chrono::duration_cast(std::chrono::steady_clock::now() - start); +} + +static void Idle(std::chrono::microseconds length) { + const auto until = std::chrono::steady_clock::now() + length; + while (std::chrono::steady_clock::now() < until) std::this_thread::yield(); +} + +class Waker { +public: + Waker() : thread([this] { + while (!stop.load()) { + KernelWakeSleepers_nid_no_patch(); + std::this_thread::sleep_for(std::chrono::microseconds(50)); + } + }) {} + ~Waker() { + stop.store(true); + thread.join(); + } + +private: + std::atomic stop{false}; + std::thread thread; +}; + +static void SleepWithoutWakeIsFull() { + for (unsigned i = 0; i < 4; ++i) Require(Usleep(WAKEABLE_MICROS) >= std::chrono::microseconds(WAKEABLE_MICROS)); +} + +static void WakeEndsShortSleep() { + Waker waker; + bool early = false; + for (unsigned i = 0; i < TRIES && !early; ++i) { + KernelNoteGpuSubmit_nid_no_patch(); + early = Usleep(WAKEABLE_MICROS) < EARLY; + } + Require(early); +} + +static void WakeLeavesLongSleep() { + Waker waker; + for (unsigned i = 0; i < 4; ++i) { + KernelNoteGpuSubmit_nid_no_patch(); + Require(Usleep(LONG_MICROS) >= std::chrono::microseconds(LONG_MICROS)); + } +} + +static void RunOfSleepsEndsEarlyAtMostEightTimes() { + Waker waker; + KernelNoteGpuSubmit_nid_no_patch(); + unsigned early = 0; + for (unsigned i = 0; i < EARLY_WAKES_PER_RUN + 8; ++i) { + if (Usleep(WAKEABLE_MICROS) < std::chrono::microseconds(WAKEABLE_MICROS)) ++early; + } + Require(early <= EARLY_WAKES_PER_RUN); + for (unsigned i = 0; i < 4; ++i) Require(Usleep(WAKEABLE_MICROS) >= std::chrono::microseconds(WAKEABLE_MICROS)); +} + +static void SubmitStartsNewRun() { + Waker waker; + for (unsigned i = 0; i < EARLY_WAKES_PER_RUN + 4; ++i) Usleep(WAKEABLE_MICROS); + bool early = false; + for (unsigned i = 0; i < TRIES && !early; ++i) { + KernelNoteGpuSubmit_nid_no_patch(); + early = Usleep(WAKEABLE_MICROS) < EARLY; + } + Require(early); +} + +static void GapStartsNewRun() { + Waker waker; + for (unsigned i = 0; i < EARLY_WAKES_PER_RUN + 4; ++i) Usleep(WAKEABLE_MICROS); + bool early = false; + for (unsigned i = 0; i < TRIES && !early; ++i) { + Idle(std::chrono::microseconds(WAKEABLE_MICROS + 200)); + early = Usleep(WAKEABLE_MICROS) < EARLY; + } + Require(early); +} + +int main() { + SleepWithoutWakeIsFull(); + WakeEndsShortSleep(); + WakeLeavesLongSleep(); + RunOfSleepsEndsEarlyAtMostEightTimes(); + SubmitStartsNewRun(); + GapStartsNewRun(); + return 0; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5396a00c3a..2d9d14419d 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -105,6 +105,7 @@ ### Functional +- [Short sleeps ended by GPU and flip completions](../../../core/libs/prx/libkernel/Time/SleepWake.cpp) (libkernel) - a `sceKernelUsleep`, `usleep`, `nanosleep` or `sceKernelNanosleep` of 1 ms or less returns before the requested time once the AGC driver reports a completed GPU batch or batch completion action, or VideoOut completes a flip, so a title that polls a GPU label or the flip status between sleeps sees it without sleeping the rest. FreeBSD and the console sleep at least the requested time: a title that paces frames with such a sleep can wake up to 1 ms early per sleep. A thread returns early at most 8 times in a run of sleeps; the run restarts when the thread submits AGC work or did not sleep for at least the requested time since its last sleep, so a loop that counts sleeps to time out still sleeps all but 8 of them in full. Polls with longer sleeps, or that need more than 8 early returns per run on a thread that does not submit, are not shortened. `APS5_NO_SLEEP_WAKE=1` keeps every sleep in full - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. - [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, as FreeBSD 11's [`sys_mprotect`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/vm/vm_mmap.c#L606) does with `VM_PROT_ALL`. Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`. Not measured on a console; the map calls and `sceKernelMtypeprotect` still refuse undefined bits - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors is unsupported, as for `shm_open`. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws From 6ce90a72864ec5a08e2101b0f14ecccb3793d7f4 Mon Sep 17 00:00:00 2001 From: Nicolas Abosch <88494836+nicolasabosch@users.noreply.github.com> Date: Thu, 15 Oct 2026 23:42:41 -0300 Subject: [PATCH 004/219] perf(libkernel): keep the sleep wake include and debt entry off lines other PRs change Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp | 2 +- docs/dev/debt/libkernel.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index f59535b1b6..5a54d69c8e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" @@ -13,7 +14,6 @@ #include "prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp" #include "prx/libc/include/CpuTopology.hpp" #include "prx/libc/include/GuestAllocations.hpp" -#include "prx/libkernel/Time/include/SleepWake.hpp" #include #include #include diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 2d9d14419d..3a1df4154c 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -105,7 +105,6 @@ ### Functional -- [Short sleeps ended by GPU and flip completions](../../../core/libs/prx/libkernel/Time/SleepWake.cpp) (libkernel) - a `sceKernelUsleep`, `usleep`, `nanosleep` or `sceKernelNanosleep` of 1 ms or less returns before the requested time once the AGC driver reports a completed GPU batch or batch completion action, or VideoOut completes a flip, so a title that polls a GPU label or the flip status between sleeps sees it without sleeping the rest. FreeBSD and the console sleep at least the requested time: a title that paces frames with such a sleep can wake up to 1 ms early per sleep. A thread returns early at most 8 times in a run of sleeps; the run restarts when the thread submits AGC work or did not sleep for at least the requested time since its last sleep, so a loop that counts sleeps to time out still sleeps all but 8 of them in full. Polls with longer sleeps, or that need more than 8 early returns per run on a thread that does not submit, are not shortened. `APS5_NO_SLEEP_WAKE=1` keeps every sleep in full - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. - [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, as FreeBSD 11's [`sys_mprotect`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/vm/vm_mmap.c#L606) does with `VM_PROT_ALL`. Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`. Not measured on a console; the map calls and `sceKernelMtypeprotect` still refuse undefined bits - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors is unsupported, as for `shm_open`. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws @@ -201,3 +200,4 @@ - [getpriority](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) follows FreeBSD 11.4's [`sys_getpriority`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_resource.c#L93-L165) for the only guest process, which is taken to run at nice 0: `PRIO_PROCESS` and `PRIO_PGRP` with 0 or its own pid (the guest is taken to lead its own process group, see `kill`) and `PRIO_USER` with 0 or its user id 0 return 0, any other id fails with `ESRCH` and any other `which` with `EINVAL`. As in FreeBSD's libc, where `getpriority` is the bare system call, errno is left unchanged on success. The nice value of a title process on the console is unverified, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises is not reported. `setpriority` accepts the same target ids (any other id fails with `ESRCH`, any other `which` with `EINVAL`), clamps the value to FreeBSD's `PRIO_MIN`-`PRIO_MAX` (-20 to 20) and stores it as the guest process's nice value, which `getpriority` returns; the host scheduling class is not changed, and whether a title process may lower the value without privilege is unverified - [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `rtprio_thread` follows FreeBSD 11.4's `rtprio_thread(2)`. Every guest thread here runs under `SCHED_FIFO` or `SCHED_RR` (priority 256-767), whose rtprio class is real-time; the console's mapping from that priority to the rtprio `prio` range (0-31) is unmeasured, so `RTP_LOOKUP` and `RTP_SET` throw for such a thread rather than report an impossible priority. A thread that is not real-time reports `RTP_PRIO_NORMAL` (3) with 0, and `RTP_SET` accepts only that value. Another function fails with `EINVAL`, a null `rtprio` with `EFAULT`, a thread other than the calling one throws, and an id that is not a live guest thread with `ESRCH`. `pthread_setconcurrency` stores 0 or more and returns `EINVAL` for a negative level (the error itself, as FreeBSD's libthr does, with errno untouched); `pthread_getconcurrency` returns the level, which is a hint and changes nothing about the host's concurrency. Morrowind (`mshivam019/morrowind-ps5`) imports all three at start-up - [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. +- [Short sleeps ended by GPU and flip completions](../../../core/libs/prx/libkernel/Time/SleepWake.cpp) (libkernel) - a `sceKernelUsleep`, `usleep`, `nanosleep` or `sceKernelNanosleep` of 1 ms or less returns before the requested time once the AGC driver reports a completed GPU batch or batch completion action, or VideoOut completes a flip, so a title that polls a GPU label or the flip status between sleeps sees it without sleeping the rest. FreeBSD and the console sleep at least the requested time: a title that paces frames with such a sleep can wake up to 1 ms early per sleep. A thread returns early at most 8 times in a run of sleeps; the run restarts when the thread submits AGC work or did not sleep for at least the requested time since its last sleep, so a loop that counts sleeps to time out still sleeps all but 8 of them in full. Polls with longer sleeps, or that need more than 8 early returns per run on a thread that does not submit, are not shortened. `APS5_NO_SLEEP_WAKE=1` keeps every sleep in full From ab9d21afcfbc4b0ebeba3305b06d8d763b0e6c76 Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Fri, 16 Oct 2026 08:47:17 +0530 Subject: [PATCH 005/219] fix(relinker): run a guest DT_INIT of 0 as the module initializer The guest reader stored DT_INIT as a plain address, and every user of it read 0 as "no initializer". The libc.prx and libSceFios2.prx that Until Dawn (CUSA02636) bundles have DT_INIT present with value 0, the start of their text segment. So libc.prx, which also has a preinit array, failed with "Guest PREINIT_ARRAY has no module initializer", and libSceFios2.prx's initializer never ran. GuestImage now records whether the tag is present (HasInit). The reader, the Linux, macOS and Windows writers and the code analysis use it instead of Init != 0, so an initializer at 0 is mapped, started and analysed like any other. Co-Authored-By: Claude Opus 5.5 --- core/relinker/CMakeLists.txt | 4 + .../src/linux/LinuxGuestModuleWriter.cpp | 4 +- .../src/macos/MacOsGuestModuleWriter.cpp | 2 +- .../src/windows/WindowsGuestModuleWriter.cpp | 4 +- .../include/relinker/guest/GuestImage.hpp | 1 + .../src/analysis/CodeInstructionCollector.cpp | 3 +- .../relinker/src/guest/GuestImageReader.cpp | 9 +- .../tests/test_guest_zero_initializer.py | 144 ++++++++++++++++++ 8 files changed, 162 insertions(+), 9 deletions(-) create mode 100644 core/relinker/relinker/tests/test_guest_zero_initializer.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index d227fc017d..20ddc63fd0 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -526,6 +526,10 @@ if(BUILD_TESTING) set_tests_properties(guest_export_lookup PROPERTIES TIMEOUT 30) add_test(NAME guest_needed_nested_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_nested_modules.py $) + add_test(NAME guest_zero_initializer COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_zero_initializer.py + $ $) + set_tests_properties(guest_zero_initializer PROPERTIES TIMEOUT 30) set(windows_stdio_host_arguments) if(WIN32) if(MINGW) diff --git a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp index 76598e35ae..beb3389801 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp @@ -29,7 +29,7 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma const auto extraOffset = bytes.size(); const auto extraAddress = Io::AlignUp64(end, 0x4000); const auto address = [&] { return extraAddress + bytes.size() - extraOffset; }; - const bool passesModuleArgs = image.Init != 0; + const bool passesModuleArgs = image.HasInit; std::uint64_t argsSlot = 0; std::uint64_t resultSlot = 0; if (passesModuleArgs) { @@ -122,7 +122,7 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma return static_cast(static_cast(distance)); }; const auto lifecycle = [&](std::uint64_t target) { - if (target == 0) return std::uint64_t{}; + if (!image.HasInit) return std::uint64_t{}; const auto start = address(); std::vector stub(kStubSize, 0xc3); stub[0x00] = 0x50; diff --git a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp index bd5928a33a..6a1e37fd30 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp @@ -109,7 +109,7 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma input.RunPaths = MachORunPaths(runPath, "@loader_path"); for (const auto slot : guest.InitArray) input.InitArrayRvas.push_back(image.GetRva(slot, 8)); for (const auto slot : guest.FiniArray) input.FiniArrayRvas.push_back(image.GetRva(slot, 8)); - input.InitRva = guest.Init == 0 ? 0 : image.GetRva(guest.Init); + input.InitRva = guest.HasInit ? image.GetRva(guest.Init) : 0; input.FiniRva = guest.Fini == 0 ? 0 : image.GetRva(guest.Fini); input.Trampolines = &guest.Trampolines; try { diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index ded45f6d76..47fc54b091 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -149,7 +149,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI } if (runtime.DeferInitialization) { PeSection lifecycle{".ginit", nextRva, SectionRead | 0x40u, {}}; - Io::AppendU32(lifecycle.Data, guest.Init == 0 ? 0 : image.GetRva(guest.Init)); + Io::AppendU32(lifecycle.Data, guest.HasInit ? image.GetRva(guest.Init) : 0); Io::AppendU32(lifecycle.Data, CheckedRva(guest.InitArray.size())); for (const auto slot : guest.InitArray) Io::AppendU32(lifecycle.Data, image.GetRva(slot, 8)); if (!exports.emplace(Relinker::GuestInitializeExport, nextRva).second) throw Domain::RelinkerException("Duplicate guest export: " + std::string(Relinker::GuestInitializeExport)); @@ -202,7 +202,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI sections.push_back({".dllmain", entry, SectionRead | SectionExecute | 0x20u, {0xb8, 1, 0, 0, 0, 0xc3}}); for (const auto slot : guest.InitArray) runtime.InitArrayRvas.push_back(image.GetRva(slot, 8)); for (const auto slot : guest.FiniArray) runtime.FiniArrayRvas.push_back(image.GetRva(slot, 8)); - runtime.InitRva = guest.Init == 0 ? 0 : image.GetRva(guest.Init); + runtime.InitRva = guest.HasInit ? image.GetRva(guest.Init) : 0; runtime.UnmatchedImportModules.assign(guest.Dynamic.UnmatchedImportModules.begin(), guest.Dynamic.UnmatchedImportModules.end()); runtime.FiniRva = guest.Fini == 0 ? 0 : image.GetRva(guest.Fini); auto result = WindowsPeWriter().Write(sections, entry, directories); diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index 31e5781a54..d20568d202 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -50,6 +50,7 @@ struct GuestImage { std::vector InitArray; std::vector FiniArray; std::uint64_t Init = 0; + bool HasInit = false; std::uint64_t Fini = 0; std::uint64_t Got = 0; bool UsePlatformTlsResolver = true; diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index c5ffda7b73..dfede077e0 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -138,7 +138,8 @@ std::set CodeInstructionCollector::Collect(const std::ve }; range(0, 64); if (const auto entry = Io::ReadU64(bytes, 24); entry != 0) addRoot(entry); - for (const auto tag : {12u, 13u}) if (const auto address = value(tag, 0x60000000 + tag); address && *address != 0) addRoot(*address); + if (const auto init = value(12, 0x6000000c)) addRoot(*init); + if (const auto fini = value(13, 0x6000000d); fini && *fini != 0) addRoot(*fini); const auto symbolBytes = value(0x6100003f, 0xffffffffffffffffull); std::uint64_t symbolCount = 0; if (symbolBytes) { diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index f1734eeba5..bf394990b2 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -323,9 +323,12 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector copyRelocations(7, 0x6100002f, 8, 0x61000031, image.Dynamic.RelaData, false); copyRelocations(23, 0x61000029, 2, 0x6100002d, image.Dynamic.RelaPltData, true); if (tags.contains(3) || tags.contains(0x61000027)) image.Got = get(3, 0x61000027); - if (tags.contains(12)) image.Init = tags.at(12); + if (tags.contains(12)) { + image.Init = tags.at(12); + image.HasInit = true; + } if (tags.contains(13)) image.Fini = tags.at(13); - if (image.Init != 0) mapped(image.Init, 1, 1); + if (image.HasInit) mapped(image.Init, 1, 1); if (image.Fini != 0) mapped(image.Fini, 1, 1); const auto readLifecycleArray = [&](std::uint64_t addressTag, std::uint64_t sizeTag, std::vector& entries) { if (!tags.contains(addressTag) && !tags.contains(sizeTag)) return; @@ -359,7 +362,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector }; readLifecycleArray(25, 27, image.InitArray); readLifecycleArray(26, 28, image.FiniArray); - if (tags.contains(33) && tags.at(33) != 0 && image.Init == 0) fail("Guest PREINIT_ARRAY has no module initializer"); + if (tags.contains(33) && tags.at(33) != 0 && !image.HasInit) fail("Guest PREINIT_ARRAY has no module initializer"); image.Bytes = std::move(bytes); return image; } catch (Domain::RelinkerException& error) { diff --git a/core/relinker/relinker/tests/test_guest_zero_initializer.py b/core/relinker/relinker/tests/test_guest_zero_initializer.py new file mode 100644 index 0000000000..6ee385e50b --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_zero_initializer.py @@ -0,0 +1,144 @@ +from pathlib import Path +import os +import shutil +import struct +import subprocess +import sys +import tempfile + +from guest_import_fixtures import executable +from test_guest_intel_trampolines import main_fixture, pe_sections +from test_guest_module_args import ARGP, ARGS, GETTER, RETURNED, SETTER, macho_binds +from test_guest_module_dir import pe_export_names +from test_guest_symbol_names import dynamic_tags, imported_symbols + +PREINIT_SLOT = 0x2900 +SHARED = 0x100 +STUB = b'\x48\x01\xc8\x31\xff\x31\xf6\x31\xd2\xff\xd0' +THREAD_POINTER = b'\x64\x48\x8b\x04\x25\0\0\0\0' + + +def zero_init_module(preinit, base=0): + image = bytearray(0x3000) + image[:16] = b'\x7fELF\x02\x01\x01' + bytes(9) + struct.pack_into(' Date: Thu, 15 Oct 2026 00:02:53 +0000 Subject: [PATCH 006/219] test(tools): cover unsupported inner ELF encoding --- tools/tests/test_unwrap_self.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 3f2dcb3ee0..32e3ab599d 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -208,6 +208,17 @@ class UnwrapTest(unittest.TestCase): self.assertNotIn("Traceback", result.stderr) self.assertEqual(output.read_bytes(), b"previous output") + def test_unsupported_inner_elf_class_and_byte_order_are_refused(self): + for offset, value in ((4, 1), (5, 2)): + with self.subTest(offset=offset, value=value): + container = bytearray(build_container(default_segments(), default_records(), default_payloads())) + container[ELF_HEADER_OFFSET + offset] = value + result, output = self.convert(bytes(container)) + self.assertEqual(result.returncode, 2) + self.assertIn("inner ELF is not 64-bit little-endian", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertFalse(output.exists()) + def test_a_container_whose_load_segments_look_encrypted_is_refused(self): random_like = bytes(range(256)) * 4 segments = [(PT_LOAD, 1, 0x1234, 0x0, 0x400, PAGE), (PT_LOAD, 4, 0x1500, 0x1000, 0x400, PAGE)] From 0e61224749fb27714cfc87f15cce5f713ccb1c4e Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Fri, 16 Oct 2026 22:01:26 +0000 Subject: [PATCH 007/219] fix(tools): reject unsupported inner ELF encodings --- tools/unwrap_self.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index ad531eb95f..6f8b898f8e 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -97,6 +97,8 @@ def parse_elf(data): if inner + 0x40 > len(data): raise Refused("inner ELF header is truncated") header = bytearray(data[inner:inner + 0x40]) + if header[4] != 2 or header[5] != 1: + raise Refused("inner ELF is not 64-bit little-endian") program_offset = struct.unpack_from(" Date: Sat, 17 Oct 2026 12:14:15 +0200 Subject: [PATCH 008/219] fix(agc): publish host import handles only after creation succeeds --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 12 +- .../Graphics/src/GuestBufferMemory.cpp | 8 +- .../prx/libSceAgcDriver/tests/BdaDevice.cpp | 31 ++- .../tests/HostImportFailure.cpp | 176 ++++++++++++++++++ .../tests/HostImportFailure.hpp | 12 ++ .../tests/HostImportRetireHold.cpp | 2 + 6 files changed, 233 insertions(+), 8 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/HostImportFailure.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/HostImportFailure.hpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index b21af51bda..c572bab6cc 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1217,7 +1217,7 @@ if(BUILD_TESTING) endif() endif() -add_test_executable(agc_host_import_retire_tests EXCLUDE_FROM_ALL tests/HostImportRetireHold.cpp) +add_test_executable(agc_host_import_retire_tests EXCLUDE_FROM_ALL tests/HostImportRetireHold.cpp tests/HostImportFailure.cpp) target_include_directories(agc_host_import_retire_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_host_import_retire_tests PRIVATE libSceAgcDriver libc) set_target_properties(agc_host_import_retire_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -1361,6 +1361,7 @@ add_test_executable(agc_driver_bda_device_tests EXCLUDE_FROM_ALL tests/BdaShader.cpp tests/BdaExecution.cpp tests/BdaContracts.cpp + tests/HostImportFailure.cpp ) target_include_directories(agc_driver_bda_device_tests PRIVATE @@ -1372,6 +1373,15 @@ target_include_directories(agc_driver_bda_device_tests PRIVATE target_link_libraries(agc_driver_bda_device_tests PRIVATE libSceAgcDriver SDL2-static libc) set_target_properties(agc_driver_bda_device_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_bda_device_tests) +if(BUILD_TESTING) + add_test(NAME agc_driver_host_import_failure_handles COMMAND agc_driver_bda_device_tests --host-import-failures) + set_tests_properties(agc_driver_host_import_failure_handles PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_host_import_failure_handles PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() add_test_executable(agc_driver_mesh_tests EXCLUDE_FROM_ALL tests/Mesh.cpp) target_include_directories(agc_driver_mesh_tests PRIVATE diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 3af6b42bd8..80f1df5c6f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -312,7 +312,9 @@ const char* bindImport(const Context& context, HostImport& entry, VkExternalMemo ImportFamilies families; if (handleType == VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT) shareDmaBufImport(context, info, families, false); #endif - if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &entry.buffer); result != VK_SUCCESS) return failed("vkCreateBuffer", result); + VkBuffer buffer = VK_NULL_HANDLE; + if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer); result != VK_SUCCESS) return failed("vkCreateBuffer", result); + entry.buffer = buffer; VkMemoryRequirements requirements{}; context.Function("vkGetBufferMemoryRequirements")(context.device, entry.buffer, &requirements); const auto types = requirements.memoryTypeBits & importTypes; @@ -321,7 +323,9 @@ const char* bindImport(const Context& context, HostImport& entry, VkExternalMemo VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, &flags}; allocation.allocationSize = entry.bytes; allocation.memoryTypeIndex = static_cast(std::countr_zero(types)); - if (const auto result = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &entry.memory); result != VK_SUCCESS) return failed("vkAllocateMemory", result); + VkDeviceMemory memory = VK_NULL_HANDLE; + if (const auto result = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory); result != VK_SUCCESS) return failed("vkAllocateMemory", result); + entry.memory = memory; allocated = true; if (const auto result = context.Function("vkBindBufferMemory")(context.device, entry.buffer, entry.memory, 0); result != VK_SUCCESS) return failed("vkBindBufferMemory", result); const VkBufferDeviceAddressInfo addressInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, entry.buffer}; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp index 860643bd27..7d4ed84dbb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp @@ -1,12 +1,15 @@ #include "BdaShader.hpp" #include "ColorTransferTests.hpp" +#include "HostImportFailure.hpp" #include #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include +#include #include #include +#include #include #include #include @@ -23,7 +26,7 @@ using namespace AgcDriver::Graphics; class Device { public: - Device() { + explicit Device(bool importHostMemory = false) { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); #else @@ -86,11 +89,13 @@ public: VkPhysicalDeviceFeatures enabled{}; enabled.shaderInt64 = VK_TRUE; address.pNext = &bytes; - const std::array extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; + std::vector extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; + const bool externalHost = importHostMemory && std::any_of(available.begin(), available.end(), [](const auto& item) { return std::strcmp(item.extensionName, VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME) == 0; }); + if (externalHost) extensionsEnabled.push_back(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME); VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, &address}; device.queueCreateInfoCount = 1; device.pQueueCreateInfos = &queue; - device.enabledExtensionCount = 2; + device.enabledExtensionCount = static_cast(extensionsEnabled.size()); device.ppEnabledExtensionNames = extensionsEnabled.data(); device.pEnabledFeatures = &enabled; Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); @@ -99,6 +104,13 @@ public: VkPhysicalDeviceProperties properties{}; function("vkGetPhysicalDeviceProperties")(context.physical, &properties); context.limits = properties.limits; + if (externalHost) { + VkPhysicalDeviceExternalMemoryHostPropertiesEXT host{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 extended{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &host}; + function("vkGetPhysicalDeviceProperties2")(context.physical, &extended); + context.hostImportAlignment = host.minImportedHostPointerAlignment; + } + if (importHostMemory) std::printf("host import device %s vendor=0x%04x device=0x%04x driver=0x%08x alignment=%llu\n", properties.deviceName, properties.vendorID, properties.deviceID, properties.driverVersion, static_cast(context.hostImportAlignment)); context.bufferDeviceAddress = true; context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; @@ -142,7 +154,8 @@ private: int main(int argc, char** argv) { try { RunBdaContractTests(); - if (argc == 2) { + const bool importFailures = argc == 2 && std::strcmp(argv[1], "--host-import-failures") == 0; + if (argc == 2 && !importFailures) { const auto shader = MakeBdaTestShader(0x7fff12340000ULL, 32); std::ofstream file(argv[1], std::ios::binary); file.write(reinterpret_cast(shader.data()), static_cast(shader.size() * sizeof(std::uint32_t))); @@ -151,12 +164,20 @@ int main(int argc, char** argv) { } std::unique_ptr device; try { - device = std::make_unique(); + device = std::make_unique(importFailures); } catch (const std::exception& error) { if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; std::cout << "skipped, no usable Vulkan device: " << error.what() << '\n'; return 77; } + if (importFailures) { + if (device->GetContext().hostImportAlignment == 0) { + std::cout << "skipped, no external host memory import support\n"; + return 77; + } + AgcDriver::Tests::RunHostImportFailureTests(device->GetContext(), true); + return 0; + } Buffer buffer(device->GetContext(), 256, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); Require(buffer.DeviceAddress() != 0 && buffer.Bytes().size() == 256, "invalid real BDA buffer"); buffer.Bytes()[255] = std::byte{0x5a}; diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.cpp new file mode 100644 index 0000000000..939c31b830 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.cpp @@ -0,0 +1,176 @@ +#include "HostImportFailure.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#ifdef _WIN32 +#include +#endif +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +enum class Step { None, Create, Allocate, Bind }; + +struct Probe { + PFN_vkGetDeviceProcAddr backend = nullptr; + Step fail = Step::None; + VkResult result = VK_SUCCESS; + unsigned injected = 0; + unsigned invalid = 0; + std::set buffers; + std::set memories; +}; + +Probe probe; + +VKAPI_ATTR VkResult VKAPI_CALL CreateBuffer(VkDevice device, const VkBufferCreateInfo* info, const VkAllocationCallbacks* allocator, VkBuffer* buffer) { + if (probe.fail == Step::Create) { + probe.fail = Step::None; + ++probe.injected; + *buffer = reinterpret_cast(std::uintptr_t{0xdeadbeef}); + return probe.result; + } + const auto result = reinterpret_cast(probe.backend(device, "vkCreateBuffer"))(device, info, allocator, buffer); + if (result == VK_SUCCESS) probe.buffers.insert(*buffer); + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL AllocateMemory(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* allocator, VkDeviceMemory* memory) { + if (probe.fail == Step::Allocate) { + probe.fail = Step::None; + ++probe.injected; + *memory = reinterpret_cast(std::uintptr_t{0xdeadbeef}); + return probe.result; + } + const auto result = reinterpret_cast(probe.backend(device, "vkAllocateMemory"))(device, info, allocator, memory); + if (result == VK_SUCCESS) probe.memories.insert(*memory); + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL BindBufferMemory(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize offset) { + if (probe.fail == Step::Bind) { + probe.fail = Step::None; + ++probe.injected; + return probe.result; + } + return reinterpret_cast(probe.backend(device, "vkBindBufferMemory"))(device, buffer, memory, offset); +} + +VKAPI_ATTR void VKAPI_CALL DestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) { + if (probe.buffers.erase(buffer) != 1) { + ++probe.invalid; + return; + } + reinterpret_cast(probe.backend(device, "vkDestroyBuffer"))(device, buffer, allocator); +} + +VKAPI_ATTR void VKAPI_CALL FreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* allocator) { + if (probe.memories.erase(memory) != 1) { + ++probe.invalid; + return; + } + reinterpret_cast(probe.backend(device, "vkFreeMemory"))(device, memory, allocator); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL DeviceProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateBuffer") == 0) return reinterpret_cast(CreateBuffer); + if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast(AllocateMemory); + if (std::strcmp(name, "vkBindBufferMemory") == 0) return reinterpret_cast(BindBufferMemory); + if (std::strcmp(name, "vkDestroyBuffer") == 0) return reinterpret_cast(DestroyBuffer); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast(FreeMemory); + return probe.backend(device, name); +} + +constexpr std::size_t Alignment = 65536; +constexpr std::size_t Bytes = 4096; + +class Heap { +public: + Heap() { +#ifdef _WIN32 + data = static_cast(VirtualAlloc(nullptr, Alignment, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + data = static_cast(std::aligned_alloc(Alignment, Alignment)); +#endif + Require(data != nullptr, "cannot allocate the host import failure heap"); + std::memset(data, 0x37, Alignment); + GuestAllocations::Mutation().Add(data, Alignment, true, true, true); + } + + ~Heap() { + GuestAllocations::Mutation().Remove(data); +#ifdef _WIN32 + VirtualFree(data, 0, MEM_RELEASE); +#else + std::free(data); +#endif + } + + std::byte* Data() const { return data; } + std::uint64_t Address() const { return reinterpret_cast(data); } + +private: + std::byte* data = nullptr; +}; + +} + +void AgcDriver::Tests::RunHostImportFailureTests(const Graphics::Context& original, bool native) { + unsigned failures = 0; + for (const auto step : {Step::Create, Step::Allocate, Step::Bind}) { + for (const auto result : {VK_ERROR_OUT_OF_HOST_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY}) { + Heap heap; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + ClearHostImports(original.device); + probe = {}; + probe.backend = original.deviceProc; + probe.fail = step; + probe.result = result; + auto context = original; + context.deviceProc = DeviceProc; + struct Cleanup { + VkDevice device; + ~Cleanup() { ClearHostImports(device); } + } cleanup{context.device}; + SetImportWatch(context, ImportWatch::Unwatch); + const HostImport* failed = nullptr; + bool threw = false; + try { + failed = HostImportFor(context, heap.Address(), Bytes); + } catch (const std::runtime_error&) { + threw = true; + } + const bool rejected = failed == nullptr && probe.injected == 1 && ExistingHostImportFor(context, heap.Address(), Bytes) == nullptr && HostImportLiveBytes() == 0; + const bool released = probe.buffers.empty() && probe.memories.empty(); + const auto invalid = probe.invalid; + ClearHostImports(context.device); + probe.fail = Step::None; + const auto* recovered = HostImportFor(context, heap.Address(), Bytes); + Require(recovered != nullptr && recovered->buffer != VK_NULL_HANDLE && recovered->memory != VK_NULL_HANDLE && recovered->address != 0, "host import retry failed"); + if (native) { + Buffer readback(original, Bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::memset(readback.Bytes().data(), 0, Bytes); + CommandBatch batch(original); + RecordMemoryBarrier(original, batch.Handle(), VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(original, batch.Handle(), recovered->buffer, heap.Address() - recovered->base, readback.Handle(), 0, Bytes); + RecordMemoryBarrier(original, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + readback.Invalidate(); + Require(std::memcmp(heap.Data(), readback.Bytes().data(), Bytes) == 0, "host import retry returned incorrect GPU readback data"); + } + ClearHostImports(context.device); + const bool clean = probe.buffers.empty() && probe.memories.empty() && HostImportLiveBytes() == 0; + std::printf("host import step=%u result=%d injected=%u rejected=%u threw=%u invalid releases=%u failure cleaned=%u retry cleaned=%u readbacks=%u\n", static_cast(step), static_cast(result), probe.injected, rejected, threw, invalid, released, clean, native ? 1u : 0u); + if (!rejected || !released || !clean || invalid != 0 || probe.invalid != invalid) ++failures; + } + } + Require(failures == 0, "failed host import creation released an undefined handle or leaked a resource"); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.hpp b/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.hpp new file mode 100644 index 0000000000..70364786ad --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.hpp @@ -0,0 +1,12 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_HOSTIMPORTFAILURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_HOSTIMPORTFAILURE_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" + +namespace AgcDriver::Tests { + +void RunHostImportFailureTests(const Graphics::Context& context, bool native); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp index 92c6fe639a..8d0d9cddc7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp @@ -1,3 +1,4 @@ +#include "HostImportFailure.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -372,6 +373,7 @@ int main() { WindowPromotionOutlivesTheHold(); IdleRetireOutlivesTheHold(); BusyRetireOutlivesALaterBatch(); + AgcDriver::Tests::RunHostImportFailureTests(mockContext(), false); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); return 1; From c5bd6c7b38c2a885924c82ad15d984f26626e775 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:21:55 +0200 Subject: [PATCH 009/219] fix(agc): reuse import allocation status during cleanup --- .../prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 80f1df5c6f..4d691b39cf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -295,7 +295,7 @@ void shareDmaBufImport(const Context& context, VkBufferCreateInfo& info, ImportF const char* bindImport(const Context& context, HostImport& entry, VkExternalMemoryHandleTypeFlagBits handleType, const void* import, std::uint32_t importTypes, bool& allocated, VkResult& failure) { const auto failed = [&](const char* step, VkResult result) -> const char* { if (entry.buffer) context.Function("vkDestroyBuffer")(context.device, entry.buffer, nullptr); - if (entry.memory) context.Function("vkFreeMemory")(context.device, entry.memory, nullptr); + if (allocated && entry.memory) context.Function("vkFreeMemory")(context.device, entry.memory, nullptr); entry.buffer = VK_NULL_HANDLE; entry.memory = VK_NULL_HANDLE; failure = result; @@ -323,9 +323,7 @@ const char* bindImport(const Context& context, HostImport& entry, VkExternalMemo VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, &flags}; allocation.allocationSize = entry.bytes; allocation.memoryTypeIndex = static_cast(std::countr_zero(types)); - VkDeviceMemory memory = VK_NULL_HANDLE; - if (const auto result = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory); result != VK_SUCCESS) return failed("vkAllocateMemory", result); - entry.memory = memory; + if (const auto result = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &entry.memory); result != VK_SUCCESS) return failed("vkAllocateMemory", result); allocated = true; if (const auto result = context.Function("vkBindBufferMemory")(context.device, entry.buffer, entry.memory, 0); result != VK_SUCCESS) return failed("vkBindBufferMemory", result); const VkBufferDeviceAddressInfo addressInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, entry.buffer}; From a963820ce4288db9d2ef56d3c696d232bea26191 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:29:13 +0200 Subject: [PATCH 010/219] test(agc): isolate host import failure fixture sources --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index c572bab6cc..008dcedc5a 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1217,11 +1217,12 @@ if(BUILD_TESTING) endif() endif() -add_test_executable(agc_host_import_retire_tests EXCLUDE_FROM_ALL tests/HostImportRetireHold.cpp tests/HostImportFailure.cpp) +add_test_executable(agc_host_import_retire_tests EXCLUDE_FROM_ALL tests/HostImportRetireHold.cpp) target_include_directories(agc_host_import_retire_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_host_import_retire_tests PRIVATE libSceAgcDriver libc) set_target_properties(agc_host_import_retire_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_host_import_retire_tests) +target_sources(agc_host_import_retire_tests PRIVATE tests/HostImportFailure.cpp) if(BUILD_TESTING) set_target_properties(agc_host_import_retire_tests PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME agc_host_import_retire COMMAND agc_host_import_retire_tests) From 6849eeaec0440cefa3b156e6902da718c8abe32a Mon Sep 17 00:00:00 2001 From: XairRrR Date: Sat, 17 Oct 2026 02:06:48 +0200 Subject: [PATCH 011/219] perf(agc): drop the guest byte snapshot of a large cached texture the write tracker covers Every snapshot texture in the sampled texture cache kept a host copy of its guest bytes, used only when the write tracker reports the surface written, to tell a rewrite of the same bytes from a change. The cache budget counts the GPU allocation alone, so the host copies grew with it: PPSA05684 held 2.7 GB of them in its first level. A snapshot larger than 256 KiB is released after the upload when the write tracker (a write generation or an untracked key) can prove the surface unchanged. A tracked write to such a surface re-uploads it instead of comparing bytes; smaller snapshots, and surfaces the tracker cannot vouch for, keep their copy as before. agc_driver_large_tracked_snapshot samples a 1 MiB surface, committed and partially resident: an unchanged surface is reused without reading guest memory, a CPU write is uploaded, and a driver store of the same bytes is uploaded again because no snapshot is left to compare. AI-assisted: yes --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 ++ .../Graphics/src/ShaderResources.cpp | 9 ++- .../execution/PartiallyResidentSnapshot.cpp | 69 ++++++++++++++++--- 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index c6950a121b..78e949d0d1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1599,6 +1599,11 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_large_tracked_snapshot COMMAND agc_driver_partially_resident_snapshot_tests --large-tracked) + set_tests_properties(agc_driver_large_tracked_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_large_tracked_snapshot PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() if(WIN32) add_test(NAME agc_driver_untracked_snapshot_hint COMMAND agc_driver_partially_resident_snapshot_tests --untracked-hint) set_tests_properties(agc_driver_untracked_snapshot_hint PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30 diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index ab7d9b47ce..cbcf754f28 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -118,6 +118,7 @@ struct CachedTexture { // The untrackedKey the snapshot was last read or compared equal at, for a surface outside the // write watch; zero when none was taken. std::array untracked{}; + std::uint64_t surfaceBytes = 0; }; // Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the @@ -729,18 +730,19 @@ std::shared_ptr cachedTexture(const Context& context, std::spantexture; } - } else if (source == nullptr && it->bytes.size() == guestBytes && it->keys == *keys) { + } else if (source == nullptr && it->surfaceBytes == guestBytes && it->keys == *keys) { // Unwritten pages need no comparison; partially resident textures compare only committed // pages. The compare goes through the flush hook: it waits for recorded work over the // surface (counted, and named for the [hooksync] line). const auto equalsCommitted = [&] { + if (it->bytes.empty()) return false; if (Recorder::SnapshotWriteOverlaps(address, bytes)) counters.pendingReads.fetch_add(1, std::memory_order_relaxed); const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureCompare); return GuestMemory::EqualsCommitted(address, it->bytes); }; const auto unchanged = [&] { if (it->committed != committed) return false; - return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, it->bytes.size()) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation); + return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, static_cast(it->surfaceBytes)) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, static_cast(it->surfaceBytes), it->generation); }; const auto kept = [&] { if (untracked[0] == 0) return false; @@ -764,6 +766,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span(source != nullptr ? 0u : bytes), nullptr, *keys, snapshotGeneration}; entry.committed = std::move(committed); + entry.surfaceBytes = bytes; if (source != nullptr) { entry.source = source; entry.sourceVersion = source->Version(); @@ -792,6 +795,8 @@ std::shared_ptr cachedTexture(const Context& context, std::span(context, *context.detiler, resource, components, entry.bytes, depthCompare); entry.accounted = entry.texture->AllocationBytes(); + constexpr std::size_t SnapshotCompareBytes = 256u << 10u; + if (entry.bytes.size() > SnapshotCompareBytes && (entry.generation != 0 || entry.untracked[0] != 0)) std::vector().swap(entry.bytes); counters.snapshots.fetch_add(1, std::memory_order_relaxed); } const auto budget = sampledBudget(context, cache); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp index 005a1e506f..0477b3face 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp @@ -45,6 +45,8 @@ constexpr std::uint32_t TileR64KBX = 0x1b; constexpr std::uint32_t Type2D = 9; constexpr std::size_t BlockBytes = 65536; constexpr std::size_t SurfaceBytes = std::size_t{Side} * Side * 4u; +constexpr std::uint32_t LargeSide = 512; +constexpr std::size_t LargeSurfaceBytes = std::size_t{LargeSide} * LargeSide * 4u; constexpr int Skipped = 77; class Device { @@ -148,16 +150,16 @@ private: class ReservedSurface { public: - ReservedSurface() { - block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, BlockBytes)); + explicit ReservedSurface(std::size_t bytes) : size(bytes) { + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(size, BlockBytes)); Require(block != nullptr, "partially resident snapshot: cannot reserve the guest surface"); - GuestAllocations::Mutation().Add(block, SurfaceBytes, true, true, true); + GuestAllocations::Mutation().Add(block, size, true, true, true); } ~ReservedSurface() { GuestAllocations::Mutation().Remove(block); - Decommit(0, SurfaceBytes); - GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); + Decommit(0, size); + GuestArena::GuestArenaRelease_nid_postfix(block, size); } ReservedSurface(const ReservedSurface&) = delete; @@ -188,6 +190,7 @@ public: std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } private: + std::size_t size = 0; std::uint8_t* block = nullptr; }; @@ -199,11 +202,11 @@ void CountReads(std::uint64_t address, std::size_t bytes) { if (address < surfaceEnd.load() && address + bytes > surfaceBegin.load()) surfaceReads.fetch_add(1); } -std::array Descriptor(std::uint64_t address) { +std::array Descriptor(std::uint64_t address, std::uint32_t side = Side) { return { static_cast(address >> 8u), - static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), - ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((side - 1u) & 3u) << 30u), + ((side - 1u) >> 2u) | ((side - 1u) << 14u), 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), 0u, 0u, @@ -262,6 +265,48 @@ void Run(const Context& context, ReservedSurface& surface) { AgcDriver::GuestMemory::SetFlushHook(nullptr); } +void LargeTracked(const Context& context, ReservedSurface& surface, std::size_t committedBytes) { + const auto address = surface.Address(); + const auto words = Descriptor(address, LargeSide); + const auto layout = committedBytes == LargeSurfaceBytes ? std::string("committed") : std::string("partially resident"); + Require(DescribeSurface(DecodeTextureResource(words)).guestBytes == LargeSurfaceBytes, "the large test descriptor does not describe the reserved surface"); + surface.Commit(0, committedBytes); + std::memset(surface.Data(), 0x5a, committedBytes); + surfaceBegin = address; + surfaceEnd = address + LargeSurfaceBytes; + std::shared_ptr previous; + const auto sample = [&](const std::string& when, bool uploaded) { + AgcDriver::GuestMemory::BumpCollectEpoch(); + surfaceReads = 0; + auto texture = CachedSampledTexture(context, words); + Require(texture != nullptr && !texture->ViewsStorageImage(), "the large " + layout + " surface was not sampled through a snapshot " + when); + const auto reads = surfaceReads.load(); + if (uploaded && (reads == 0 || texture == previous)) throw std::runtime_error("sampling the large " + layout + " surface " + when + " kept the texture uploaded before it"); + if (!uploaded && (reads != 0 || texture != previous)) throw std::runtime_error("sampling the large " + layout + " surface " + when + " uploaded it again although the write tracker saw no write (" + std::to_string(reads) + " reads)"); + previous = texture; + }; + AgcDriver::GuestMemory::SetFlushHook(&CountReads); + try { + sample("at its first use", true); + sample("again", false); + surface.Data()[0x100] = 0x11; + sample("after a CPU write", true); + sample("after the upload of the written surface", false); + surface.Data()[committedBytes - 0x40] = 0x22; + sample("after a CPU write to its last committed block", true); + sample("after the upload of the written block", false); + AgcDriver::GuestMemory::MarkWritten(address + 0x200, 4); + sample("after a driver store of the bytes already there, which a snapshot it did not keep cannot tell from a change", true); + sample("after the upload that followed the driver store", false); + } catch (...) { + AgcDriver::GuestMemory::SetFlushHook(nullptr); + throw; + } + AgcDriver::GuestMemory::SetFlushHook(nullptr); + ClearCachedTextures(context.device); + surface.Decommit(0, committedBytes); +} + #ifdef _WIN32 void UntrackedHint(const Context& context, ReservedSurface& surface) { const auto address = surface.Address(); @@ -368,9 +413,13 @@ int main(int argc, char** argv) { auto context = device->GetContext(); TextureDetiler detiler(context); context.detiler = &detiler; - ReservedSurface surface; + const bool large = argc == 2 && std::string_view(argv[1]) == "--large-tracked"; + ReservedSurface surface(large ? LargeSurfaceBytes : SurfaceBytes); std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); - if (argc == 2 && std::string_view(argv[1]) == "--pending-stores") PendingStores(context, surface); + if (large) { + LargeTracked(context, surface, LargeSurfaceBytes); + LargeTracked(context, surface, BlockBytes); + } else if (argc == 2 && std::string_view(argv[1]) == "--pending-stores") PendingStores(context, surface); else if (argc == 2 && std::string_view(argv[1]) == "--untracked-hint") { #ifdef _WIN32 UntrackedHint(context, surface); From d2ca227e8f3230cb0b733608ab3fd84cdd9336a9 Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Sun, 18 Oct 2026 05:55:38 +0300 Subject: [PATCH 012/219] perf(libkernel): find direct-memory aliases through the physical index --- core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 4ceb6dae70..5e82161a82 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -616,7 +616,8 @@ void RequireAliasesUnpinned(GuestAllocations::Mutation& mutation, std::uint64_t std::vector> aliases; { std::lock_guard lock(g_directLock); - for (const auto& [base, mapping] : g_directMappings) { + for (const auto base : MappingsSharingPhys(phys, phys + len)) { + const auto& mapping = g_directMappings.at(base); const auto first = std::max(phys, mapping.phys); const auto last = std::min(phys + len, mapping.phys + (mapping.end - base)); if (first < last) aliases.emplace_back(base + (first - mapping.phys), last - first); From 97c11bcb94f90852fcebf7d23dc066dc2feec6a3 Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Sun, 18 Oct 2026 06:03:30 +0300 Subject: [PATCH 013/219] perf(libc): release exact resident mappings without repeated queries --- core/libs/prx/libc/include/WindowsMappings.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 56c8851fbe..4fbf8031ed 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -798,6 +798,11 @@ private: forgetClean(address, end); breakRun(address); breakRun(end); + if (const auto run = residentRuns.find(address); run != residentRuns.end() && run->second.end == end) { + evict(run); + if (!VirtualFree(reinterpret_cast(address), bytes, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory"); + return; + } for (auto cursor = address; cursor < end;) { const auto memory = query(cursor); if (memory.State == MEM_RESERVE) { From 1d9bf1e0299ebf986cef2333bc9ca011ef3276aa Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Sun, 18 Oct 2026 06:10:30 +0300 Subject: [PATCH 014/219] perf(libc): reuse saved direct-memory buffers when unreferenced --- .../libs/prx/libc/include/WindowsMappings.hpp | 49 ++++++++++++++++--- core/libs/tests/GuestMemory.cpp | 45 +++++++++++++++++ 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 56c8851fbe..059e871b1a 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -508,11 +508,24 @@ private: ~Section() { CloseHandle(handle); } }; struct Saved { - unsigned char* bytes; - explicit Saved(unsigned char* bytes) : bytes(bytes) {} + unsigned char* bytes = nullptr; + std::size_t length; + std::array committed{}; + explicit Saved(std::size_t length) : length(length) { + bytes = static_cast(VirtualAlloc(nullptr, length, MEM_RESERVE, PAGE_READWRITE)); + if (bytes == nullptr) fail("reserve unmapped direct memory"); + } Saved(const Saved&) = delete; Saved& operator=(const Saved&) = delete; ~Saved() { VirtualFree(bytes, 0, MEM_RELEASE); } + + void Commit(std::size_t offset) { + const auto page = offset / pageBytes; + const auto bit = std::uint64_t{1} << (page & 63); + if ((committed[page >> 6] & bit) != 0) return; + if (!VirtualAlloc(bytes + offset, pageBytes, MEM_COMMIT, PAGE_READWRITE)) fail("keep unmapped direct memory"); + committed[page >> 6] |= bit; + } }; struct Physical { std::weak_ptr page; @@ -665,12 +678,8 @@ private: const auto found = views.find(page); auto& view = found->second; if (!blank(page)) { - if (!saved) { - auto* bytes = static_cast(VirtualAlloc(nullptr, stop - start, MEM_RESERVE, PAGE_READWRITE)); - if (bytes == nullptr) fail("reserve unmapped direct memory"); - saved = std::make_shared(bytes); - } - if (!VirtualAlloc(saved->bytes + (page - start), pageBytes, MEM_COMMIT, PAGE_READWRITE)) fail("keep unmapped direct memory"); + if (!saved) saved = savedMemory(stop - start); + saved->Commit(page - start); std::memcpy(saved->bytes + (page - start), reinterpret_cast(page), pageBytes); view.slot->saved = saved; view.slot->savedOffset = page - start; @@ -682,6 +691,28 @@ private: residentRuns.erase(run); } + std::shared_ptr savedMemory(std::size_t bytes) { + for (const auto& saved : savedPool) { + if (saved.use_count() == 1 && saved->length == bytes) return saved; + } + constexpr std::size_t limit = 32u * 1024u * 1024u; + const auto reserved = (bytes + granularity - 1) / granularity * granularity; + for (auto it = savedPool.begin(); reserved <= limit && savedPoolBytes > limit - reserved && it != savedPool.end();) { + if (it->use_count() != 1) { + ++it; + continue; + } + savedPoolBytes -= ((*it)->length + granularity - 1) / granularity * granularity; + it = savedPool.erase(it); + } + auto saved = std::make_shared(bytes); + if (reserved <= limit && savedPoolBytes <= limit - reserved) { + savedPool.push_back(saved); + savedPoolBytes += reserved; + } + return saved; + } + static void forget(Ranges& ranges, std::uintptr_t start, std::uintptr_t end) { auto it = ranges.lower_bound(start); if (it != ranges.begin() && std::prev(it)->second > start) --it; @@ -888,6 +919,8 @@ private: } std::map, std::shared_ptr> physical; std::map residentRuns; + std::vector> savedPool; + std::size_t savedPoolBytes = 0; std::map splitAliases; std::size_t granularity = 0x10000; bool watchResident = false; diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 368b25dfd3..4f1c404363 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -428,6 +428,50 @@ static void CheckSparseDirectMemoryBacking() { Require(sceKernelReleaseDirectMemory(physical, size) == 0); } +static void CheckSavedDirectMemorySurvivesCachePressure() { +#ifdef _WIN32 + constexpr std::size_t bytes = 0x200000; + std::array retained{}; + for (std::size_t index = 0; index < retained.size(); ++index) { + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &retained[index]) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, retained[index], bytes) == 0); + std::memset(mapped, static_cast(index + 1), bytes); + Require(sceKernelMunmap(mapped, bytes) == 0); + } + const auto temporary = [] { + constexpr std::size_t small = 0x10000; + for (unsigned char round = 1; round <= 24; ++round) { + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, small, 0, 0, &physical) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, small, 3, 0, physical, 0) == 0); + auto* data = static_cast(mapped); + Require(std::all_of(data, data + small, [](unsigned char value) { return value == 0; })); + data[7] = round; + data[small - 1] = static_cast(round + 0x40); + Require(sceKernelMunmap(mapped, small) == 0); + mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, small, 3, 0, physical, 0) == 0); + data = static_cast(mapped); + for (std::size_t at = 0; at < small; ++at) Require(data[at] == (at == 7 ? round : at == small - 1 ? static_cast(round + 0x40) : 0)); + Require(sceKernelMunmap(mapped, small) == 0); + Require(sceKernelReleaseDirectMemory(physical, small) == 0); + } + }; + temporary(); + for (std::size_t index = 0; index < retained.size(); ++index) { + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, retained[index], bytes) == 0); + const auto* data = static_cast(mapped); + Require(std::all_of(data, data + bytes, [&](unsigned char value) { return value == index + 1; })); + Require(sceKernelMunmap(mapped, bytes) == 0); + Require(sceKernelReleaseDirectMemory(retained[index], bytes) == 0); + } + temporary(); +#endif +} + static void CheckReleaseDirectMemoryClearsMappings() { constexpr std::size_t page = 0x4000; std::int64_t phys = 0; @@ -2657,6 +2701,7 @@ int main(int argc, char** argv) { CheckMprotectIgnoresUndefinedBits(); CheckDirectMemoryFollowsPhysicalPages(); CheckSparseDirectMemoryBacking(); + CheckSavedDirectMemorySurvivesCachePressure(); CheckReleaseDirectMemoryClearsMappings(); CheckReleaseDirectMemoryRejectsInvalidRanges(); CheckFixedMappingReplacesPartialOverlap(); From 9be717d11be7d75630f069dddb493e1720b00851 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 18 Oct 2026 06:07:34 +0000 Subject: [PATCH 015/219] perf(shader): insert patched SRT reads before their use without appending first PlanBuilder::PatchReads emitted GetSrtResource and ReadConst at the end of the block, then removed each with RemoveInstruction, which scans the block from the front to find a value that is always at its end. Create both values directly and insert them before the patched read, as ResourceTracker's EmitBefore does. The SPIR-V is unchanged. verifySrtReadPatching now checks that the flat read sits right before the original read and the planning reference ends the block. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 4 ++++ .../Optimization/src/SrtWalker/SrtPlanBuilder.cpp | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index f55927e50d..f66704020d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5436,6 +5436,10 @@ void verifySrtReadPatching() { const auto* replacement = repeatedUse.Argument(0); require(replacement->Opcode() == IrOpcode::ReadConst, "SRT read patching: the uses do not reference a flat read"); require(repeatedUse.Argument(1) == replacement && secondUser.Argument(0) == replacement, "SRT read patching: not every operand uses the same replacement"); + const std::vector order(block.Instructions().begin(), block.Instructions().end()); + const auto readAt = std::find(order.begin(), order.end(), &read); + require(readAt != order.end() && readAt - order.begin() >= 2 && *(readAt - 1) == replacement && *(readAt - 2) == replacement->Argument(0) && (*(readAt - 2))->Opcode() == IrOpcode::GetSrtResource, "SRT read patching: the flat read is not right before the original read"); + require(order.back()->Opcode() == IrOpcode::ReferenceU32 && order.back()->Argument(0) == &read, "SRT read patching: the planning reference is not at the end of the block"); const auto& remainingUses = read.OperandUses(); require(remainingUses.size() == 1u && remainingUses.front().user->Opcode() == IrOpcode::ReferenceU32, "SRT read patching: the original read retains non-planning uses"); } diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp index 6a1a86eae6..5cae7e7cf5 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp @@ -117,12 +117,12 @@ void PlanBuilder::PatchReads() { throw std::runtime_error("SrtWalker::BuildPlan patched value has no parent block"); } builder.SetInsertionPoint(*block); - IrValue& resource = builder.Emit(IrOpcode::GetSrtResource, IrType::SrtResource, {}); - block->RemoveInstruction(&resource); + IrValue& resource = _program.CreateValue(IrOpcode::GetSrtResource, IrType::SrtResource); block->InsertInstructionBefore(patch.inst, &resource); IrValue& slotConstant = builder.Constant(patch.slot); - IrValue& flat = builder.Emit(IrOpcode::ReadConst, IrType::U32, {&resource, &slotConstant}); - block->RemoveInstruction(&flat); + IrValue& flat = _program.CreateValue(IrOpcode::ReadConst, IrType::U32); + flat.AddArgument(&resource); + flat.AddArgument(&slotConstant); block->InsertInstructionBefore(patch.inst, &flat); patch.inst->ReplaceAllUsesWith(&flat); for (auto& info : _program.Metadata().blockInfo) { From 08156bb30cfa298fddb170f4ca387323d766e1e8 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 18 Oct 2026 10:15:42 +0400 Subject: [PATCH 016/219] feat: filter trace digests by named event threads --- tools/tests/test_trace_digest.py | 30 ++++++++++++++++++++++++++++++ tools/trace_digest.py | 25 ++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index 5864781302..a482d805ce 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -442,6 +442,36 @@ class TraceDigestTests(unittest.TestCase): self.assertEqual(raised.exception.code, 2) self.assertIn("summary file not found", error.getvalue()) + def test_thread_filter_keeps_frame_bounds_and_capture_summary(self): + command = [sys.executable, trace_digest.__file__, str(self.trace)] + result = subprocess.run([*command, "--thread", "queue worker 0x0", "--json"], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + report = json.loads(result.stdout) + self.assertEqual(report["frames"], 3) + self.assertEqual(report["summary"], SUMMARY) + self.assertEqual(report["thread_filter"], ["queue worker 0x0"]) + self.assertEqual([thread["thread"] for thread in report["threads"]], ["queue worker 0x0"]) + self.assertTrue(all("Present" not in entry["name"] for entry in report["top_slices_ms_per_frame"])) + self.assertTrue(all(wait["thread"] == "queue worker 0x0" for wait in report["longest_waits"])) + result = subprocess.run([*command, "--thread", "presenter", "--thread", "queue worker 0x0", "--json"], + capture_output=True, text=True) + unfiltered = trace_digest.digest(trace_digest.load_trace(self.trace), SUMMARY) + report = json.loads(result.stdout) + del report["thread_filter"] + self.assertEqual(report, unfiltered) + result = subprocess.run([*command, "--thread", "presenter"], capture_output=True, text=True) + self.assertIn("Thread filter:", result.stdout) + self.assertIn("capture-wide", result.stdout) + result = subprocess.run([*command, "--thread", "misspelled"], capture_output=True, text=True) + self.assertEqual(result.returncode, 2) + self.assertIn("unknown thread", result.stderr) + self.assertEqual(result.stdout, "") + result = subprocess.run([sys.executable, trace_digest.__file__, str(self.trace.with_suffix(".jsonl")), + "--thread", "presenter"], capture_output=True, text=True) + self.assertEqual(result.returncode, 2) + self.assertIn("event trace", result.stderr) + + def interval_line(start, end, p99, gpu_ms, **changes): line = dict(SUMMARY, window={"spec": "summary_every 30", "first_present": 0, "ms": (end - start) * 1000.0}, interval_start_s=start, interval_end_s=end, diff --git a/tools/trace_digest.py b/tools/trace_digest.py index 08504ca43a..0807a5a55c 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -59,6 +59,17 @@ class Trace: entry[4] = any(is_wait(parent[2]) for parent in stack) stack.append(entry) + def select_threads(self, names): + available = self.threads.keys() | self.slices.keys() | self.folded.keys() + unknown = set(names) - {self.name(tid) for tid in available} + if unknown: + raise ValueError("unknown thread: " + ", ".join(sorted(unknown))) + selected = {tid for tid in available if self.name(tid) in names} + self.threads = {tid: name for tid, name in self.threads.items() if tid in selected} + self.slices = {tid: slices for tid, slices in self.slices.items() if tid in selected} + self.folded = {tid: value for tid, value in self.folded.items() if tid in selected} + self.batched = [entry for entry in self.batched if entry[0] in selected] + def name(self, tid): return self.threads.get(tid, f"tid {tid}") @@ -209,6 +220,9 @@ def table_cell(value): def markdown(result): lines = [] + if result.get("thread_filter"): + lines += ["Thread filter: " + ", ".join(table_cell(name) for name in result["thread_filter"]) + + ". Frame bounds, summary and stack samples remain capture-wide.", ""] summary = result["summary"] if summary: frame = summary["frame_ms"] @@ -327,12 +341,16 @@ def main(arguments=None): parser.add_argument("trace", help="aps5-trace-*.json.gz (or .json), or a .jsonl summary file with APS5_TRACE_SUMMARY_EVERY lines") parser.add_argument("--summary", help="file with the summary line (default: aps5-summary.jsonl next to the trace)") parser.add_argument("--stacks", help="collapsed-stack file of APS5_SAMPLE_WORKER (.collapsed.txt)") + parser.add_argument("--thread", action="append", default=[], metavar="NAME", + help="include event tracks with this exact thread name; repeat to select several tracks") parser.add_argument("--frames", type=nonnegative_integer, default=5, help="slowest frames to list (default 5)") parser.add_argument("--top", type=nonnegative_integer, default=10, help="entries per list (default 10)") parser.add_argument("--json", action="store_true", help="print the digest as JSON") options = parser.parse_args(arguments) if options.summary and not Path(options.summary).is_file(): parser.error(f"summary file not found: {options.summary}") + if options.thread and options.trace.endswith(".jsonl"): + parser.error("--thread requires an event trace, not interval summaries") if options.trace.endswith(".jsonl"): result = digest_intervals(load_intervals(options.trace)) sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else markdown_intervals(result)) @@ -341,8 +359,13 @@ def main(arguments=None): stacks = load_stacks(options.stacks) if options.stacks else None except (OSError, ValueError) as error: parser.error(str(error)) - result = digest(load_trace(options.trace), load_summary(options.summary, options.trace), + trace = load_trace(options.trace) + if options.thread: + trace.select_threads(options.thread) + result = digest(trace, load_summary(options.summary, options.trace), stacks, options.frames, options.top) + if options.thread: + result["thread_filter"] = sorted(set(options.thread)) sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else markdown(result)) return 0 From b9a5d3dfc38641c025d289ebfe9114931201cc5c Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 18 Oct 2026 10:17:59 +0400 Subject: [PATCH 017/219] feat: explain saved test selections and fixture dependencies --- tools/select_tests.py | 34 +++++++++++++++++++++++++++++--- tools/tests/test_select_tests.py | 22 +++++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/tools/select_tests.py b/tools/select_tests.py index 5def841494..0dd3dea3c7 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -166,7 +166,7 @@ def dump(build, out): print(f"{len(tests)} tests, {len(closures)} test executables, {len(deps)} objects with header deps") -def select(graph, changed): +def select(graph, changed, reasons=None): full = [f for f in changed if FULL.search(f)] if full: return None, full @@ -180,15 +180,28 @@ def select(graph, changed): if (test["target"] is None or files & set(test["registered"]) or files & scope or any(f.startswith(dirs) for f in scope | set(test["registered"]))): picked.add(name) + if reasons is not None: + reasons[name] = (["no build executable"] if test["target"] is None else []) + reasons[name] += ["input: " + path for path in sorted(files & scope)] + reasons[name] += ["registration: " + path for path in sorted(files & set(test["registered"]))] + reasons[name] += ["directory: " + directory for directory in dirs + if any(path.startswith(directory) for path in scope | set(test["registered"]))] setups = collections.defaultdict(set) for name, test in tests.items(): for fixture in test["sets"]: setups[fixture].add(name) todo = list(picked) while todo: - test = tests[todo.pop()] + name = todo.pop() + test = tests[name] for extra in set(test["depends"]).union(*(setups[f] for f in test["requires"])): - if extra in tests and extra not in picked: + if extra not in tests: + continue + if reasons is not None: + fixture = set(test["requires"]) & set(tests[extra]["sets"]) + kind = "fixture setup for: " if fixture else "dependency of: " + reasons.setdefault(extra, []).append(kind + name) + if extra not in picked: picked.add(extra) todo.append(extra) return picked, [] @@ -217,6 +230,9 @@ def main(): s.add_argument("build") s.add_argument("graph") s.add_argument("changed", help="file with one changed path per line") + e = sub.add_parser("explain", help="explain saved-graph selections without a configured build; CTest may add fixtures") + e.add_argument("graph", type=Path) + e.add_argument("changed", type=Path, help="file with one changed path per line") c = sub.add_parser("ctest", help="run ctest on a tree, limited to the selected tests if select wrote any") c.add_argument("build") c.add_argument("arguments", nargs=argparse.REMAINDER, help="ctest arguments") @@ -225,6 +241,18 @@ def main(): selected = Path(args.build, "select.tests") extra = ["-I", selected.read_text(encoding="utf-8").strip()] if selected.exists() else [] return subprocess.run(["ctest", "--test-dir", args.build, *args.arguments, *extra]).returncode + if args.command == "explain": + try: + graph = json.loads(args.graph.read_text(encoding="utf-8")) + changed = [line.strip() for line in args.changed.read_text(encoding="utf-8").splitlines() if line.strip()] + reasons = {} + picked, full = select(graph, changed, reasons) + except (OSError, ValueError) as error: + parser.error(str(error)) + print(json.dumps({"tests": sorted(picked) if picked is not None else None, + "full_run_paths": full, "reasons": {name: sorted(set(why)) for name, why in reasons.items()}}, + indent=2, sort_keys=True)) + return 0 build = Build(args.build) if args.command == "dump": dump(build, args.graph) diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index 192a533da4..111086068c 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -119,6 +119,28 @@ class SelectTests(unittest.TestCase): subprocess.run(["cmake", "--build", str(self.build)], check=True, capture_output=True) self.assertEqual(built, {"alpha_tests"}) + def test_explain_reports_input_and_fixture_reasons_without_a_build_cache(self): + changed = self.build / "explain-changed.txt" + cache = self.build / "CMakeCache.txt" + saved = self.build / "CMakeCache.saved" + cache.rename(saved) + try: + for path in ("alpha/alpha.h", "beta/main.c", "CMakeLists.txt"): + with self.subTest(path=path): + changed.write_text(path + "\n", encoding="utf-8") + report = json.loads(self.tool("explain", self.graph, changed)) + picked, full = select_tests.select(json.loads(self.graph.read_text(encoding="utf-8")), [path]) + self.assertEqual(report["tests"], sorted(picked) if picked is not None else None) + self.assertEqual(report["full_run_paths"], full) + if path == "alpha/alpha.h": + self.assertIn("input: alpha/alpha.h", report["reasons"]["alpha"]) + self.assertIn("no build executable", report["reasons"]["script"]) + elif path == "beta/main.c": + self.assertIn("fixture setup for: beta", report["reasons"]["beta_setup"]) + finally: + saved.rename(cache) + + class CaseInsensitivePathTests(unittest.TestCase): def build(self): From 71de464e18cad85f350911d59c4a876100d8d4a3 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 18 Oct 2026 10:19:56 +0400 Subject: [PATCH 018/219] feat: optionally fail import audits on referenced throwing stubs --- tools/import_audit.py | 4 +++- tools/tests/test_import_audit.py | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index cb5f9ac8b4..396c9a4f20 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -693,6 +693,8 @@ def main(argv=None): parser.add_argument("--source", default=str(PRX), help="core/libs/prx tree used to find throwing stubs (default: this repository's)") parser.add_argument("--names", help="aerolib.csv style file (NID, name per line) used to name absent imports") parser.add_argument("--json", type=Path, help="write the full result to this file") + parser.add_argument("--strict-stubs", action="store_true", + help="also fail when a referenced import resolves to a throwing stub") parser.add_argument("--strict-providers", action="store_true", help="fail when a named import is provided only by another library; retains the existing libc and title-module rules") args = parser.parse_args(argv) try: @@ -735,7 +737,7 @@ def main(argv=None): print(f"FAIL: {error.strerror}: {error.filename}" if error.filename else f"FAIL: {error}", file=sys.stderr) return 2 print(render(summary, records, missing, stops, missing_hosts)) - return 1 if summary["unique_by_class"]["absent"] or missing or missing_hosts or (args.strict_providers and summary["library_mismatch"]) else 0 + return 1 if summary["unique_by_class"]["absent"] or missing or missing_hosts or (args.strict_providers and summary["library_mismatch"]) or (args.strict_stubs and summary["unique_by_class"]["stub"]) else 0 if __name__ == "__main__": diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 85d27593e0..4ac02eab7c 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -1668,6 +1668,26 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual((code, out), (2, "")) self.assertIn(f"FAIL: Permission denied: {library}", err) + def test_strict_stubs_fails_only_for_referenced_throwing_providers(self): + name = "sceExamplePending" + nid = nid_names.compute_nid(name) + registry = self.work.root / "game.json" + output = self.work.root / "audit.json" + (self.work.libs / "libSceExample.prx").write_bytes(exporting(nid)) + write_source(self.work.source, "libSceExample", STUB_SOURCE.format(name=name)) + for weak in (False, True): + with self.subTest(weak=weak): + write_registry(registry, [(nid, "libSceExample.prx", weak)]) + self.assertEqual(self.work.run(registry)[0], 0) + code, out, err = self.work.run(registry, "--strict-stubs", "--json", str(output)) + self.assertEqual((code, err), (1, "")) + self.assertIn("stub", out) + self.assertEqual(json.loads(output.read_text(encoding="utf-8"))["unique_by_class"]["stub"], 1) + write_source(self.work.source, "libSceExample", DONE_SOURCE.format(name=name) + + STUB_SOURCE.format(name="sceUnreferencedPending")) + self.assertEqual(self.work.run(registry, "--strict-stubs")[0], 0) + + class StrictProviderTests(unittest.TestCase): def test_named_provider_mismatch_is_optional_failure(self): From 5a35a711165ddfbea78f4cad8d65d8dd1f5c08be Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sun, 18 Oct 2026 05:43:09 -0300 Subject: [PATCH 019/219] feat(videoout): allow opting out of HDR presentation --- .../libSceAgcDriver/Execution/src/HostHdr.cpp | 3 ++- .../prx/libSceAgcDriver/tests/HostHdr.cpp | 25 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 ++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp index d18386b591..63748fa18c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp" #include #include +#include #include namespace AgcDriver { @@ -24,7 +25,7 @@ bool SameDisplay(const HostDisplayHdr& a, const HostDisplayHdr& b) { void Publish(HostHdrStatus& status, const HostDisplayHdr& display, bool surfaceHdr10) { const bool displayChanged = !SameDisplay(status.display, display); - const bool available = display.hdr && surfaceHdr10; + const bool available = display.hdr && surfaceHdr10 && std::getenv("APS5_FORCE_SDR") == nullptr; const bool availabilityChanged = available != status.available; status.display = display; status.surfaceHdr10 = surfaceHdr10; diff --git a/core/libs/prx/libSceAgcDriver/tests/HostHdr.cpp b/core/libs/prx/libSceAgcDriver/tests/HostHdr.cpp index 64b394f21b..6ac8547f1c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HostHdr.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HostHdr.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -125,6 +126,29 @@ void hostState() { Expect(!AgcDriver::CurrentHostHdr().available, "a display switched to SDR leaves HDR output available"); } +void forceSdr() { + resetHost(); +#ifdef _WIN32 + _putenv_s("APS5_FORCE_SDR", "1"); +#else + setenv("APS5_FORCE_SDR", "1", 1); +#endif + AgcDriverSetSurfaceHdr10_nid_postfix(true); + AgcDriverSetHostDisplayHdr_nid_postfix(AgcDriver::DisplayHdrFromOutput(true, 800.0f, 0.01f, 400.0f)); + const auto forced = AgcDriver::CurrentHostHdr(); + Expect(!forced.available && forced.display.hdr && forced.surfaceHdr10, "forcing SDR does not suppress HDR availability while preserving host capabilities"); + Expect(AgcDriver::ToneMapPeakNits(forced) == 100.0f, "forcing SDR does not report SDR reference white"); +#ifdef _WIN32 + _putenv_s("APS5_FORCE_SDR", ""); +#else + unsetenv("APS5_FORCE_SDR"); +#endif + AgcDriverSetSurfaceHdr10_nid_postfix(true); + const auto restored = AgcDriver::CurrentHostHdr(); + Expect(restored.available && restored.generation != forced.generation, "removing the SDR override does not restore HDR availability"); + resetHost(); +} + void reportedWhiteLevel() { resetHost(); constexpr std::uint64_t rgbPq = 0x8100070422000000ull; @@ -158,6 +182,7 @@ int main() { whiteLevels(); hostState(); reportedWhiteLevel(); + forceSdr(); if (failures != 0) return 1; std::puts("host HDR tests passed"); return 0; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 49c7ebc7f3..8df3434077 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -176,3 +176,5 @@ - [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted when blending is disabled (ENABLE clear or BLEND_BYPASS set). Vulkan converts unblended shader values and clamps to the normalized range automatically; blending into an unclamped normalized target remains unmodeled because Vulkan always clamps inputs prior to blending. - [Thick SW_4KB_S_X volumes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) use AMD addrlib's GFX10 non-RB+ 16-pipe `GFX10_SW_4K_S3_X_PATINFO` equations for 1, 2, 4, 8 and 16-byte elements, including packed mip tails. The layout follows [AMD PAL c5e80007](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp); it has not been measured on console hardware. Other thick swizzle modes outside the supported families still throw. Mipmapped 3D color targets remain refused by the color-register decoder. + +- [HDR output override](../../../core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp) - setting `APS5_FORCE_SDR` suppresses HDR availability reported to titles while retaining the detected display and surface capabilities. It selects the existing SDR presentation path; it does not add support for floating-point BT.2100 PQ buffers (`0xC001070722000000` and `0xC001070700000000`), whose presentation remains refused. The override does not change the host display settings. From 851b1688fdda1f0785b611258b88ec30e3714c5f Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Sun, 18 Oct 2026 04:53:14 -0400 Subject: [PATCH 020/219] chore: record performance-series PR metadata Complete the publication checklist and series cross-links for #7470. No source or test changes. From 709520ddabbe872b9fe555c055e64a5648821a93 Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Sun, 18 Oct 2026 04:53:14 -0400 Subject: [PATCH 021/219] chore: record performance-series PR metadata Complete the publication checklist and series cross-links for #7468. No source or test changes. From 4ba87c6b28bef073988f94ae2c09994fb00dcdd2 Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Sun, 18 Oct 2026 04:53:15 -0400 Subject: [PATCH 022/219] chore: record performance-series PR metadata Complete the publication checklist and series cross-links for #7472. No source or test changes. From f543bcaba17276a50450580eedea831ef8e358cb Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sun, 18 Oct 2026 06:01:26 -0300 Subject: [PATCH 023/219] docs(videoout): group the SDR override with HDR presentation --- docs/dev/debt/libSceAgcDriver.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 8df3434077..63457b406b 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -132,6 +132,7 @@ - [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. - [16-bit float display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0xC001000622000000` and `0xC001000600000000` are taken as R16G16B16A16 and B16G16R16A16 float, eight bytes per texel, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps them and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgba16Float`, `Bgra16Float`); PPSA11386 registers 3840x2160 tiled buffers of the first. The tiled layout is assumed to be the 8-byte SW_64KB_R_X equation of `TextureSwizzleEquations.hpp` applied to full coordinates, as addrlib applies it, by analogy with the 32-bit scanout; `ColorTargetLayout` applies the equations to in-block coordinates, which for 8-byte texels differs in every odd 64-row block row (address bit 11 takes y bit 6) and for 16-byte texels also differs, so a float display buffer written through `WriteColorTarget` and presented from guest memory would show those rows misplaced. The values are assumed to be linear light and are encoded with the sRGB curve for the 8-bit output. Bits 32-39 of these formats are `0x06`, as in the 10-bit formats SharpProspero lists without sRGB transfer, while its sRGB formats have `0x00`, its 10-bit PQ formats `0x04` (with `0x07` in bits 40-47) and its float BT.2100 PQ formats `0x07`: they show only that these formats are not the sRGB ones. NaN and negative values give 0, values of 1 and above give 255, alpha is linear. Neither the layout nor the transfer is verified against console or title frames, and HDR output is not modelled. DCC metadata on a float buffer throws. - [BT.2100 PQ display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/include/PqToSdr.hpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0x8100070422000000` and `0x8100070400000000` are taken as 10:10:10:2 texels with BT.2020 primaries and the SMPTE ST 2084 transfer, red or blue in the low bits, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgb10A2Bt2100Pq`, `Bgr10A2Bt2100Pq`) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps the first; PPSA01317 registers display buffers of the first when its output reports HDR. With HDR output they are presented unchanged through an HDR10 swapchain (`VK_COLOR_SPACE_HDR10_ST2084_EXT`, 10-bit, with `VK_EXT_hdr_metadata`); otherwise they are converted to 8-bit sRGB: ST 2084 EOTF, BT.2020 to BT.709 primaries with negative components clipped, a luminance tone map and the sRGB encoding. The tone map is ours: min(203 nits, white level) is SDR white (the BT.2408 HDR reference white), luminance passes unchanged up to 0.75 of it and an extended Reinhard shoulder above reaches 1.0 at the white level, the peak sceSystemServiceGetHdrToneMapLuminance last reported to the title (or the current one when it never asked), since titles read it once. How a console shows such a buffer on an SDR display is unknown. Alpha is opaque. +- [HDR output override](../../../core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp) - setting `APS5_FORCE_SDR` suppresses HDR availability reported to titles while retaining the detected display and surface capabilities. It selects the existing SDR presentation path; it does not add support for floating-point BT.2100 PQ buffers (`0xC001070722000000` and `0xC001070700000000`), whose presentation remains refused. The override does not change the host display settings. - [DCC key stores](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) cover the console's DCC extent only for pipe-aligned DCC of single-level, single-layer 4-byte SW_64KB_R_X surfaces; other surfaces, and every texture and color target key read, cover one byte per 256 bytes. - [DCC key fills over other images](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) - a uniform fill at a live image's `dccAddress`, at least one key per 256 bytes long, is a key fill of that image even when other live images overlap it, but only while it is no longer than the image's key extent (`DccKeyCount`, the pipe-aligned count); a longer one is classified by its overlaps as before. The bound is inferred, not measured on a title: a longer fill also writes the bytes past the keys, which the overlapping image may own. Whether the title's bloom-target key fills stay within the extent was not checked (`APS5_TRACE_DCC_KEYS` prints the length of the fills classified as keys, not of the refused ones). - [Comp-to-single DCC keys](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) (libSceAgcDriver) - key 0x10 (`GFX9_DCC_CLEAR_SINGLE` in Mesa `ac_descriptors.h`) uniform over a surface makes every texel the element at the start of its 256-byte block, the top-left texel of the block where radv's `build_clear_dcc_comp_to_single_shader` stores the clear color. Sampled textures and storage images read that expansion (a storage image's first write stores the keys uncompressed, as under a clear code), and a CB DCC decompress pass stores it over the texels. A fast-clear eliminate over such keys throws, as a metadata pass over mixed keys does; color targets that render over such keys use their stored texels, and textures read mixed keys, and comp-to-single keys of a format whose element does not divide 256 bytes, as stored. "Uniform" is judged over the key bytes the driver reads, one per 256 surface bytes (see DCC key stores above), so a pipe-aligned surface whose keys differ past that range is expanded as if all were comp-to-single, as for the other clear codes. Not verified on hardware @@ -176,5 +177,3 @@ - [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted when blending is disabled (ENABLE clear or BLEND_BYPASS set). Vulkan converts unblended shader values and clamps to the normalized range automatically; blending into an unclamped normalized target remains unmodeled because Vulkan always clamps inputs prior to blending. - [Thick SW_4KB_S_X volumes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) use AMD addrlib's GFX10 non-RB+ 16-pipe `GFX10_SW_4K_S3_X_PATINFO` equations for 1, 2, 4, 8 and 16-byte elements, including packed mip tails. The layout follows [AMD PAL c5e80007](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp); it has not been measured on console hardware. Other thick swizzle modes outside the supported families still throw. Mipmapped 3D color targets remain refused by the color-register decoder. - -- [HDR output override](../../../core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp) - setting `APS5_FORCE_SDR` suppresses HDR availability reported to titles while retaining the detected display and surface capabilities. It selects the existing SDR presentation path; it does not add support for floating-point BT.2100 PQ buffers (`0xC001070722000000` and `0xC001070700000000`), whose presentation remains refused. The override does not change the host display settings. From f099292f8cfde933c0261b6a128dfa19359f5efc Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 18 Oct 2026 10:31:17 +0000 Subject: [PATCH 024/219] perf(shader): leave out the both-zero select of f16 min and max minMaxF16Bits compares the halves as ordered integers (the low 16 bits sign-extended, with the magnitude bits of a negative half inverted), then replaced the result when both halves were zero: max took a & b and min took a | b. The ordered integers already put -0 (-1) below +0 (0), so the compare picks +0 for max and -0 for min, which is what the both-zero select produced. Every caller passes halves with their upper 16 bits clear (readF16Bits masks them, the packed path extracts them), and for those all 2^32 pairs of each give the same bits with and without the select. Each f16 min, max, min3, max3 and med3 now emits five instructions fewer per pair. Co-Authored-By: Claude Opus 5.5 --- core/shader/recompiler/Translation/src/FloatInstructions.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index 61948b2be5..3656e70cf8 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -679,10 +679,7 @@ IrU32 TranslationContext::minMaxF16Bits(bool maxValue, IrU32 lhs, IrU32 rhs) { return ir.BitwiseXor(value, ir.BitwiseAnd(ir.ShiftRightArithmetic(value, ir.Constant(31u)), ir.Constant(0x7fffu))); }; const IrU1 ordered(ir.Emit(maxValue ? IrOpcode::SGreaterThanEqual32 : IrOpcode::SLessThan32, IrType::U1, {&order(lhs), &order(rhs)})); - const IrU32 either(ir.BitwiseOr(lhs.Value(), rhs.Value())); - const IrU1 bothZero(ir.IEqual(ir.BitwiseAnd(either.Value(), ir.Constant(0x7fffu)), ir.Constant(0u))); - const IrU32 zero = maxValue ? IrU32(ir.BitwiseAnd(lhs.Value(), rhs.Value())) : either; - return IrU32(ir.Select(bothZero.Value(), zero.Value(), ir.Select(ordered.Value(), lhs.Value(), rhs.Value()))); + return IrU32(ir.Select(ordered.Value(), lhs.Value(), rhs.Value())); } bool TranslationContext::minMaxF16(const RdnaInstruction& inst, IrOpcode opcode) { From 0a7cb2fca1ae243c196b2b6517f00a7a966b19da Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sun, 18 Oct 2026 08:18:22 -0300 Subject: [PATCH 025/219] docs(videoout): list the SDR environment override --- docs/dev/ENVIRONMENT.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 27bf42846e..96c63092c0 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -134,6 +134,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_FILL_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | waits for every recorded store over the fill's range | | `APS5_FLIP_INFLIGHT` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 1 | number of presentations that may trail on the GPU (0, 1 or 2) | | `APS5_FLIP_READ_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records the in-place reads of completed batches and checks them before the present blit's fence | +| `APS5_FORCE_SDR` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp` | unset | suppresses advertised HDR availability while retaining detected host capabilities; uses the existing SDR presentation path | | `APS5_FORCE_STRUCTURIZER_FALLBACK` | presence | `core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp` | | makes the control-flow test expect the reach-flag fallback instead of no cloning | | `APS5_FULL_BARRIERS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every barrier instead of merging redundant ones | | `APS5_GPU_CHECKPOINTS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | attaches device diagnostic checkpoints through VK_NV_device_diagnostic_checkpoints | From d484281b6112dfb2232e787857601a1ac9e2a257 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 18 Oct 2026 11:21:00 +0000 Subject: [PATCH 026/219] perf(shader): keep one use list per IR value Every IR value kept two lists of its users: uses (the users) and operandUses (the users with their operand index). AddArgument, ReplaceArgument and ReplaceAllUsesWith updated both, and each value allocated and freed both. Only operandUses is kept now. Uses() is a view of the users in operandUses, and HasUses, UseCount and Invalidate read it. sizeof(IrValue) drops from 152 to 128 bytes; the plan value encoder writes no use lists, so only its size check changes. The two lists held the same users in the same order, except after ReplaceArgument on a user that took the value in more than one operand: uses dropped the user's first entry and operandUses the replaced operand's entry. Uses() now follows operandUses there too. shader_ir_use_rewrite counted the allocations of both lists: its allocation failure check now fails the one reserve ReplaceAllUsesWith makes, and its growth check allows the allocations of one list, which main exceeds. Co-Authored-By: Claude Opus 5.5 --- .../IntermediateRepresentation/IrValue.hpp | 8 ++++++-- .../src/IrValue.cpp | 19 +++---------------- .../tests/UseRewrite.cpp | 7 ++++--- .../Optimization/src/SsaBuilder/SsaPass.cpp | 3 ++- core/shader/recompiler/ShaderDiskCache.cpp | 2 +- 5 files changed, 16 insertions(+), 23 deletions(-) diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp index d7adec321f..ccdda16ed9 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -35,7 +36,7 @@ public: [[nodiscard]] IrType Type() const; [[nodiscard]] std::uint32_t Id() const; [[nodiscard]] const std::vector& Arguments() const; - [[nodiscard]] const std::vector& Uses() const; + [[nodiscard]] auto Uses() const; [[nodiscard]] IrBlock* Parent() const; [[nodiscard]] bool HasImmediate() const; [[nodiscard]] std::uint32_t ImmediateU32() const; @@ -103,10 +104,13 @@ private: std::vector operandUses; bool hasImmediate; std::vector arguments; - std::vector uses; IrBlock* parent; }; +inline auto IrValue::Uses() const { + return std::views::transform(operandUses, [](const IrUse& use) { return use.user; }); +} + inline IrOpcode IrValue::Opcode() const { return opcode; } diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp index f5653e80f9..3cc3c041ec 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp @@ -16,10 +16,6 @@ const std::vector& IrValue::Arguments() const { return arguments; } -const std::vector& IrValue::Uses() const { - return uses; -} - IrBlock* IrValue::Parent() const { return parent; } @@ -33,7 +29,6 @@ void IrValue::AddArgument(IrValue* argument) { if (argument == nullptr) { return; } - argument->uses.push_back(this); argument->operandUses.push_back(IrUse{this, arguments.size() - 1}); } @@ -43,10 +38,6 @@ void IrValue::ReplaceArgument(std::size_t index, IrValue* argument) { } IrValue* previous = arguments[index]; if (previous != nullptr) { - const auto useIt = std::find(previous->uses.begin(), previous->uses.end(), this); - if (useIt != previous->uses.end()) { - previous->uses.erase(useIt); - } const auto operandIt = std::find_if(previous->operandUses.begin(), previous->operandUses.end(), [this, index](const IrUse& use) { return use.user == this && use.operand == index; }); @@ -56,7 +47,6 @@ void IrValue::ReplaceArgument(std::size_t index, IrValue* argument) { } arguments[index] = argument; if (argument != nullptr) { - argument->uses.push_back(this); argument->operandUses.push_back(IrUse{this, index}); } } @@ -77,17 +67,14 @@ void IrValue::ReplaceAllUsesWith(IrValue* replacement) { list.reserve(std::max(required, grown)); } }; - reserveAdditional(replacement->uses); reserveAdditional(replacement->operandUses); } for (auto use = operandUses.rbegin(); use != operandUses.rend(); ++use) { use->user->arguments[use->operand] = replacement; if (replacement != nullptr) { - replacement->uses.push_back(use->user); replacement->operandUses.push_back(*use); } } - uses.clear(); operandUses.clear(); } @@ -113,11 +100,11 @@ bool IrValue::MayHaveSideEffects() const { } bool IrValue::HasUses() const { - return !uses.empty(); + return !operandUses.empty(); } std::size_t IrValue::UseCount() const { - return uses.size(); + return operandUses.size(); } std::size_t IrValue::PhiBlockCount() const { @@ -224,7 +211,7 @@ void IrValue::ReplaceOpcode(IrOpcode opcode) { } void IrValue::Invalidate() { - if (!uses.empty()) { + if (!operandUses.empty()) { throw std::runtime_error("IrValue::Invalidate called on a value that still has uses"); } for (std::size_t index = 0; index < arguments.size(); index++) { diff --git a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp index 3308434316..0849790845 100644 --- a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp @@ -1,4 +1,5 @@ #include "IntermediateRepresentation/IrBlock.hpp" +#include #include #include #include @@ -92,7 +93,7 @@ void CheckSimpleReplacement() { Require(second->Argument(0) == target); const std::vector expected{{existing, 0}, {first, 2}, {first, 1}, {second, 0}, {first, 0}}; Require(target->OperandUses() == expected); - Require(target->Uses() == std::vector({existing, first, first, second, first})); + Require(std::ranges::equal(target->Uses(), std::vector({existing, first, first, second, first}))); first->ReplaceArgument(1, source); Require(source->UseCount() == 1); source->ReplaceAllUsesWith(nullptr); @@ -130,7 +131,7 @@ void CheckPhiAndIdentity() { } void CheckAllocationFailure() { - for (std::ptrdiff_t failure = 0; failure < 2; ++failure) { + for (std::ptrdiff_t failure = 0; failure < 1; ++failure) { Graph graph(12); for (std::size_t index = 2; index < 12; ++index) { graph.Value(index)->AddArgument(graph.Value(0)); @@ -169,7 +170,7 @@ void CheckGeometricGrowth() { countAllocations = true; for (auto& source : sources) source->ReplaceAllUsesWith(graph.Value(1)); countAllocations = false; - Require(allocations <= 2 * (std::bit_width(count) + 1)); + Require(allocations <= std::bit_width(count) + 1); Require(graph.Value(1)->UseCount() == count); for (std::size_t index = 2; index < count + 2; ++index) Require(graph.Value(index)->Argument(0) == graph.Value(1)); } diff --git a/core/shader/recompiler/Optimization/src/SsaBuilder/SsaPass.cpp b/core/shader/recompiler/Optimization/src/SsaBuilder/SsaPass.cpp index f3ed83cbed..74181182b1 100644 --- a/core/shader/recompiler/Optimization/src/SsaBuilder/SsaPass.cpp +++ b/core/shader/recompiler/Optimization/src/SsaBuilder/SsaPass.cpp @@ -115,7 +115,8 @@ IrValue* Pass::TryRemoveTrivialPhi(IrValue& phi, Variable variable) { if (same == nullptr) { same = MakeInitialValue(variable); } - std::vector users = phi.Uses(); + std::vector users; + for (IrValue* user : phi.Uses()) users.push_back(user); phi.ReplaceUsesWith(same, true); for (IrValue* user : users) { if (user->IsPhi()) { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 3c9e663e94..373f4cf386 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -105,7 +105,7 @@ static_assert(sizeof(BindingAllocationResult) == 184, "BindingAllocationResult c static_assert(sizeof(CompiledBindingLayout) == 88, "CompiledBindingLayout changed: update the allocation encoder"); static_assert(sizeof(BindingLayout) == 16, "BindingLayout changed: update BuildKey"); static_assert(sizeof(IrResourcePlan) == 728, "IrResourcePlan changed: update EncodePlan and DecodePlan"); -static_assert(sizeof(IrValue) == 152, "IrValue changed: update the plan value encoder"); +static_assert(sizeof(IrValue) == 128, "IrValue changed: update the plan value encoder"); static_assert(sizeof(GuestRegister) == 8, "GuestRegister changed: update the plan value encoder"); static_assert(sizeof(MemoryInfo) == 80, "MemoryInfo changed: update the plan encoder"); static_assert(sizeof(DescriptorSource) == 168, "DescriptorSource changed: update the plan encoder"); From 7c345db5a501518ee9ef29bd1010d4e822ab5172 Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Sun, 18 Oct 2026 09:14:12 -0400 Subject: [PATCH 027/219] fix(libc): validate resident allocations before release --- core/libs/prx/libc/CMakeLists.txt | 9 +++ .../libs/prx/libc/include/WindowsMappings.hpp | 10 +++ core/libs/tests/WindowsMappingsRelease.cpp | 71 +++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 core/libs/tests/WindowsMappingsRelease.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 7344519b44..6c8adf1bff 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -1,4 +1,13 @@ if(BUILD_TESTING) + if(WIN32) + add_test_executable(windows_mappings_release_tests ${LIBS_INCLUDE_DIR}/tests/WindowsMappingsRelease.cpp) + target_include_directories(windows_mappings_release_tests PRIVATE ${LIBS_INCLUDE_DIR}) + add_test(NAME windows_mappings_release COMMAND windows_mappings_release_tests) + foreach(mode 0 1 2) + add_test(NAME windows_mappings_release_mismatch_${mode} COMMAND windows_mappings_release_tests ${mode}) + endforeach() + endif() + add_test_executable(guest_binary_word_tests ${LIBS_INCLUDE_DIR}/tests/GuestBinaryWord.cpp) target_include_directories(guest_binary_word_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_binary_word_tests PRIVATE libc) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 4fbf8031ed..040514ef2b 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -799,6 +799,16 @@ private: breakRun(address); breakRun(end); if (const auto run = residentRuns.find(address); run != residentRuns.end() && run->second.end == end) { + const auto memory = query(address); + if (reinterpret_cast(memory.AllocationBase) != address) throw std::runtime_error("cannot release part of a host allocation"); + if (memory.Type != MEM_PRIVATE) throw std::runtime_error("resident guest mapping is not private memory"); + auto allocationEnd = address; + do { + const auto part = query(allocationEnd); + if (part.AllocationBase != memory.AllocationBase) break; + allocationEnd = reinterpret_cast(part.BaseAddress) + part.RegionSize; + } while (allocationEnd < end); + if (allocationEnd != end || query(end).AllocationBase == memory.AllocationBase) throw std::runtime_error("guest release truncates a host allocation"); evict(run); if (!VirtualFree(reinterpret_cast(address), bytes, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory"); return; diff --git a/core/libs/tests/WindowsMappingsRelease.cpp b/core/libs/tests/WindowsMappingsRelease.cpp new file mode 100644 index 0000000000..4694e926de --- /dev/null +++ b/core/libs/tests/WindowsMappingsRelease.cpp @@ -0,0 +1,71 @@ +#include "prx/libc/include/WindowsMappings.hpp" +#include +#include +#include + +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (!value) throw std::runtime_error("Windows resident release regression at " + std::to_string(location.line())); +} + +static void CheckMetadataMismatch(unsigned mode) { + constexpr std::size_t page = 65536; + auto& mappings = GuestArena::WindowsMappings::Get(); + auto* base = static_cast(mappings.Reserve(nullptr, 3 * page)); + Require(base != nullptr); + const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, 3 * page, nullptr); + Require(section != nullptr); + auto* address = base + page; + mappings.Map(address, page, section, 0, PAGE_READWRITE, true, true); + Require(VirtualFree(address, 0, MEM_RELEASE)); + if (mode == 0) { + Require(VirtualFree(base, 0, MEM_RELEASE)); + Require(VirtualFree(base + 2 * page, 0, MEM_RELEASE)); + Require(VirtualAlloc(base, 3 * page, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) == base); + } else if (mode == 1) { + Require(VirtualFree(base + 2 * page, 0, MEM_RELEASE)); + Require(VirtualAlloc(address, 2 * page, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) == address); + } else { + Require(MapViewOfFileEx(section, FILE_MAP_ALL_ACCESS, 0, 0, page, address) == address); + } + address[0] = 73; + bool rejected = false; + try { mappings.Reset(address, page); } + catch (const std::runtime_error&) { rejected = true; } + std::uint32_t protection = 0; + Require(rejected && address[0] == 73 && mappings.Protection(reinterpret_cast(address), &protection)); + if (mode == 2) Require(UnmapViewOfFile(address)); + else Require(VirtualFree(mode == 0 ? base : address, 0, MEM_RELEASE)); + CloseHandle(section); +} + +static void CheckValidRelease() { + constexpr std::size_t bytes = 2 * 1024 * 1024; + auto& mappings = GuestArena::WindowsMappings::Get(); + auto* base = static_cast(mappings.Reserve(nullptr, bytes)); + Require(base != nullptr); + const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, bytes, nullptr); + Require(section != nullptr); + mappings.Map(base, bytes, section, 0, PAGE_READWRITE, true, true); + base[0] = 31; + base[bytes - 1] = 89; + DWORD previous; + Require(VirtualProtect(base + 65536, 65536, PAGE_READONLY, &previous)); + mappings.Reset(base, bytes); + mappings.Map(base, bytes, section, 0, PAGE_READWRITE, true, true); + Require(base[0] == 31 && base[bytes - 1] == 89); + mappings.Reset(base, bytes); + Require(VirtualFree(base, 0, MEM_RELEASE)); + mappings.ForgetSection(section); + CloseHandle(section); +} + +int main(int argc, char** argv) { + try { + if (argc == 2) CheckMetadataMismatch(static_cast(argv[1][0] - '0')); + else CheckValidRelease(); + } catch (const std::exception& failure) { + std::fprintf(stderr, "%s\n", failure.what()); + return 1; + } + return 0; +} From cabd834bc5448eadb0a6654d56b07d39387876b5 Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Sun, 18 Oct 2026 06:20:03 +0300 Subject: [PATCH 028/219] perf(libc): avoid repeated Windows path component scans --- core/libs/prx/libc/src/General.cpp | 49 +++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index ff92739719..f15ac49ac9 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -85,7 +85,35 @@ struct NameMatch { bool ambiguous = false; }; +#ifdef _WIN32 +std::error_code MatchNamesWindows(const std::filesystem::path& directory, std::span names) { + for (auto& name : names) { + const auto pattern = directory / name.name; + WIN32_FIND_DATAW data{}; + const auto handle = FindFirstFileExW(pattern.c_str(), FindExInfoBasic, &data, FindExSearchNameMatch, nullptr, 0); + if (handle == INVALID_HANDLE_VALUE) { + const auto error = GetLastError(); + if (error == ERROR_FILE_NOT_FOUND || error == ERROR_NO_MORE_FILES) continue; + return {static_cast(error), std::system_category()}; + } + const std::unique_ptr owner(handle, &FindClose); + do { + const auto filename = std::filesystem::path(data.cFileName); + if (!SameName(filename.string(), name.name)) continue; + name.ambiguous = name.ambiguous || name.path.has_value(); + name.path = directory / filename; + } while (FindNextFileW(handle, &data)); + const auto error = GetLastError(); + if (error != ERROR_NO_MORE_FILES) return {static_cast(error), std::system_category()}; + } + return {}; +} +#endif + std::error_code MatchNames(const std::filesystem::path& directory, std::span names) { +#ifdef _WIN32 + return MatchNamesWindows(directory, names); +#else std::error_code error; std::filesystem::directory_iterator entry(directory, error), end; for (; !error && entry != end; entry.increment(error)) { @@ -97,6 +125,7 @@ std::error_code MatchNames(const std::filesystem::path& directory, std::span& relative, bool module = false) { const auto direct = Child(root, JoinNames(relative)); std::error_code error; + auto first = relative.begin(); +#ifdef _WIN32 + const auto attributes = GetFileAttributesW(direct.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return direct; + const auto failure = GetLastError(); + const bool missing = attributes == INVALID_FILE_ATTRIBUTES && (failure == ERROR_FILE_NOT_FOUND || failure == ERROR_PATH_NOT_FOUND); + if (!missing && std::filesystem::exists(direct, error)) return direct; + for (auto last = relative.end(); last != relative.begin();) { + const auto parent = Child(root, JoinNames(std::vector(relative.begin(), std::prev(last)))); + const auto parentAttributes = GetFileAttributesW(parent.c_str()); + const bool present = parentAttributes != INVALID_FILE_ATTRIBUTES && (parentAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0; + if (!present && !std::filesystem::exists(parent, error)) { --last; continue; } + root = parent; + first = last; + break; + } +#else if (std::filesystem::exists(direct, error)) return direct; - for (auto part = relative.begin(); part != relative.end(); ++part) { +#endif + for (auto part = first; part != relative.end(); ++part) { const auto candidate = Child(root, *part); const auto status = std::filesystem::symlink_status(candidate, error); if (std::filesystem::exists(status) || (error && error != std::errc::no_such_file_or_directory)) { From 211af5dc9e69a26ad5cc06e8ccec309f22658eb9 Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Sun, 18 Oct 2026 04:53:14 -0400 Subject: [PATCH 029/219] chore: record performance-series PR metadata Complete the publication checklist and series cross-links for #7467. No source or test changes. From 87a486fb301dc2601becaf30ccdfb2eed3570f47 Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Sun, 18 Oct 2026 09:14:04 -0400 Subject: [PATCH 030/219] fix(libc): preserve lookup in case-sensitive Windows directories --- core/libs/prx/libc/src/General.cpp | 16 ++++++++++--- core/libs/tests/GuestFilesystem.cpp | 37 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index f15ac49ac9..5d48f36f1e 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -86,6 +86,17 @@ struct NameMatch { }; #ifdef _WIN32 +bool CaseInsensitiveDirectoryWindows(const std::filesystem::path& directory) { + const auto handle = CreateFileW(directory.c_str(), FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) return false; + const std::unique_ptr owner(handle, &CloseHandle); + struct { ULONG flags; } information{}; + constexpr auto caseSensitiveInfo = static_cast(23); + return GetFileInformationByHandleEx(handle, caseSensitiveInfo, &information, sizeof(information)) && + (information.flags & FILE_CS_FLAG_CASE_SENSITIVE_DIR) == 0; +} + std::error_code MatchNamesWindows(const std::filesystem::path& directory, std::span names) { for (auto& name : names) { const auto pattern = directory / name.name; @@ -112,8 +123,8 @@ std::error_code MatchNamesWindows(const std::filesystem::path& directory, std::s std::error_code MatchNames(const std::filesystem::path& directory, std::span names) { #ifdef _WIN32 - return MatchNamesWindows(directory, names); -#else + if (CaseInsensitiveDirectoryWindows(directory)) return MatchNamesWindows(directory, names); +#endif std::error_code error; std::filesystem::directory_iterator entry(directory, error), end; for (; !error && entry != end; entry.increment(error)) { @@ -125,7 +136,6 @@ std::error_code MatchNames(const std::filesystem::path& directory, std::span(23); + const bool enabled = SetFileInformationByHandle(handle, caseSensitiveInfo, &information, sizeof(information)); + const auto error = GetLastError(); + CloseHandle(handle); + if (!enabled && (error == ERROR_NOT_SUPPORTED || error == ERROR_INVALID_PARAMETER || error == ERROR_ACCESS_DENIED)) { + std::printf("case-sensitive host directory unavailable: %lu\n", error); + std::filesystem::remove(root); + return; + } + Require(enabled); + { std::ofstream file(root / "File.txt"); file << "payload"; } + Require(std::filesystem::create_directory(root / "Sub")); + Require(std::filesystem::create_directory(root / "Both")); + Require(std::filesystem::create_directory(root / "both")); + { std::ofstream file(root / "libCase.prx.guest.prx"); file << "module"; } + const auto host = root.string(); + AddPathAlias_nid_no_patch("/sensitive-host", host.c_str()); + Require(ResolvePath_nid_no_patch("/sensitive-host/file.txt") == root / "File.txt"); + Require(ResolvePath_nid_no_patch("/sensitive-host/sub") == root / "Sub"); + Require(ResolvePathOrRelinkedModule_nid_no_patch("/sensitive-host/libcase.prx") == root / "libCase.prx.guest.prx"); + bool ambiguous = false; + try { ResolvePath_nid_no_patch("/sensitive-host/BOTH/file.txt"); } + catch (const std::runtime_error& failure) { ambiguous = std::strstr(failure.what(), "Ambiguous case-insensitive guest path") != nullptr; } + Require(ambiguous); + RemovePathAlias_nid_no_patch("/sensitive-host"); + std::filesystem::remove_all(root); +} + static std::filesystem::path ExtendedPath(const std::filesystem::path& path) { const auto& native = path.native(); if (native.starts_with(L"\\\\?\\") || native.starts_with(L"\\\\.\\")) return path; @@ -343,6 +379,7 @@ int main() { TestRelinkedModuleNames(); TestRelinkedModuleNameCase(); #ifdef _WIN32 + TestCaseSensitiveHostDirectory(); TestLongHostPathStat(); #endif #ifdef __linux__ From 5a1590c9f8eefc50d7080f26fae3b8ed07ff99bc Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Sun, 18 Oct 2026 13:15:04 -0400 Subject: [PATCH 031/219] fix(libc): resolve the final Windows path component --- core/libs/prx/libc/src/General.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 5d48f36f1e..90c0cd5088 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -193,7 +193,7 @@ std::filesystem::path ResolveHostPath(std::filesystem::path root, const std::vec const bool present = parentAttributes != INVALID_FILE_ATTRIBUTES && (parentAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0; if (!present && !std::filesystem::exists(parent, error)) { --last; continue; } root = parent; - first = last; + first = std::prev(last); break; } #else From 529dbb04802814a48c1bf21b9762c37a30449315 Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Sun, 18 Oct 2026 04:27:21 -0400 Subject: [PATCH 032/219] fix(agc): preserve pipeline caches on immediate guest exit --- .../Execution/include/VulkanDevice.hpp | 1 + .../Execution/src/Driver/Lifecycle.cpp | 4 ++ .../Execution/src/VulkanDevice.cpp | 4 ++ .../prx/libSceSystemService/CMakeLists.txt | 4 +- core/libs/prx/libc/include/Shutdown.hpp | 1 + core/libs/prx/libc/src/Process.cpp | 28 +++++++++++++ core/libs/tests/GuestSystemExit.cpp | 40 +++++++++++++++++++ 7 files changed, 81 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index cc6d4a0e28..260f773768 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -45,6 +45,7 @@ public: // Starts the journal replay if the device has not (a headless device starts it at its first // draw or dispatch) and joins it (tests; the teardown joins it anyway). void WaitForPipelineJournalReplay(); + void SavePipelineCache(); // Compute pipelines this device built, by dispatches and by the journal replay (tests). std::uint64_t ComputePipelinesCreated() const; ~VulkanDevice(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index b5928fd583..ff17f54fce 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -182,6 +182,10 @@ Driver::Driver() { InstallAsyncLog_nid_no_patch(); static const bool driverMemory = (MapDriverMemory(), true); static_cast(driverMemory); + LibcSetExitDrain_nid_no_patch([] { + const auto current = Driver::Get().device.Load(); + if (current) current->SavePipelineCache(); + }); static_cast(GuestMemory::TrackerGeneration()); ShaderMemory::SetWaitedMsProvider(&Graphics::Recorder::ThreadWaitedMs); try { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 4b0505b3e5..6160bd7754 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2044,6 +2044,10 @@ void VulkanDevice::WaitForPipelineJournalReplay() { if (state->journalReplay != nullptr) state->journalReplay->Wait(); } +void VulkanDevice::SavePipelineCache() { + if (state->pipelineCache) state->pipelineCache->SaveNow(); +} + std::uint64_t VulkanDevice::ComputePipelinesCreated() const { return state->computePipelinesCreated.load(std::memory_order_relaxed); } diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 78aa978dad..f824aa1721 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -38,6 +38,8 @@ if(BUILD_TESTING) add_test(NAME guest_requested_exit COMMAND guest_system_exit_tests --requested-exit-driver) add_test(NAME guest_quick_exit COMMAND guest_system_exit_tests --quick-exit) add_test(NAME guest_immediate_exit COMMAND guest_system_exit_tests --immediate) + add_test(NAME guest_immediate_exit_drain COMMAND guest_system_exit_tests --exit-drain-driver) + set_tests_properties(guest_immediate_exit_drain PROPERTIES TIMEOUT 20) add_test(NAME guest_return_from_main COMMAND guest_system_exit_tests --return-from-main) add_test(NAME guest_system_relaunch COMMAND guest_system_exit_tests --relaunch-driver) add_test(NAME guest_nested_exit COMMAND guest_system_exit_tests --nested-exit-driver) @@ -62,7 +64,7 @@ if(BUILD_TESTING) set_tests_properties(guest_system_load_exec_standard_error PROPERTIES TIMEOUT 20) endif() if(WIN32) - set_tests_properties(guest_system_exit guest_requested_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit guest_system_relaunch guest_nested_exit guest_exit_finalizers_before_shutdown PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_system_exit guest_requested_exit guest_system_exit_parameters guest_immediate_exit guest_immediate_exit_drain guest_return_from_main guest_quick_exit guest_system_relaunch guest_nested_exit guest_exit_finalizers_before_shutdown PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_system_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemService.cpp) diff --git a/core/libs/prx/libc/include/Shutdown.hpp b/core/libs/prx/libc/include/Shutdown.hpp index 8e0f9ec209..a59aa05ced 100644 --- a/core/libs/prx/libc/include/Shutdown.hpp +++ b/core/libs/prx/libc/include/Shutdown.hpp @@ -17,5 +17,6 @@ extern "C" [[noreturn]] void LibcExitWithoutHandlers_nid_no_patch(int code); extern "C" [[noreturn]] void LibcTerminate_nid_no_patch(int code); extern "C" void LibcSetEndFlush_nid_no_patch(void (*flush)()); extern "C" void LibcRunEndFlush_nid_no_patch(); +extern "C" void LibcSetExitDrain_nid_no_patch(void (*drain)()); #endif diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index e6d58a919f..9d2f6909b0 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -100,6 +101,7 @@ extern "C" void LibcRunShutdown_nid_postfix() { namespace { std::atomic endFlush{nullptr}; +std::atomic exitDrain{nullptr}; void (*previousAbortHandler)(int) = SIG_DFL; void FlushOnAbort(int number) { @@ -129,6 +131,12 @@ extern "C" void LibcRunEndFlush_nid_no_patch() { if (const auto flush = endFlush.load(std::memory_order_acquire)) flush(); } +extern "C" void LibcSetExitDrain_nid_no_patch(void (*drain)()) { + if (drain == nullptr) throw std::runtime_error("libc: invalid exit drain registration"); + void (*registered)() = nullptr; + if (!exitDrain.compare_exchange_strong(registered, drain) && registered != drain) throw std::runtime_error("libc: exit drain already registered"); +} + namespace { void StartExit() { @@ -154,6 +162,26 @@ extern "C" { } [[noreturn]] void LibcTerminate_nid_no_patch(int code) { + if (const auto drain = exitDrain.exchange(nullptr)) { + try { + auto completion = std::make_shared>(); + auto finished = completion->get_future(); + std::thread([completion, drain] { + try { + drain(); + completion->set_value(); + } catch (...) { + completion->set_exception(std::current_exception()); + } + }).detach(); + if (finished.wait_for(std::chrono::seconds(8)) == std::future_status::ready) finished.get(); + else std::fputs("[libc] host exit drain exceeded 8 seconds\n", stderr); + } catch (const std::exception& error) { + std::fprintf(stderr, "[libc] host exit drain failed: %s\n", error.what()); + } catch (...) { + std::fputs("[libc] host exit drain failed\n", stderr); + } + } LibcRunEndFlush_nid_no_patch(); #ifdef _WIN32 TerminateProcess(GetCurrentProcess(), static_cast(code)); diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 8fa6982485..c4c81c056e 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -98,6 +98,15 @@ void SecondaryShutdown() { AppendMarker("secondary shutdown"); } void APS5_VABI RemainingHandler(void*) { AppendMarker("remaining handler"); } void APS5_VABI ExitingHandler(void*) { exit_nid_postfix(7); } void GuestExitShutdown() { AppendMarker("shutdown"); } +void DelayedExitDrain() { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + AppendMarker("drain"); +} +void ExitFlushAfterDrain() { AppendMarker("flush"); } +void ThrowingExitDrain() { throw std::runtime_error("exit drain test failure"); } +void BlockedExitDrain() { + for (;;) std::this_thread::sleep_for(std::chrono::seconds(1)); +} void APS5_VABI CheckShutdownStillRunning(void*) { if (LibcShutdownToken_nid_postfix().stop_requested()) std::_Exit(8); AppendMarker("guest finalizer"); @@ -183,9 +192,40 @@ int DriveRelaunch() { std::puts("Guest relaunch completed"); return 0; } + +int DriveExitDrain() { + const auto self = OwnExecutable(); + const auto marker = std::filesystem::temp_directory_path() / ("anyps5-exit-drain-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); + SetVariable(RelaunchMarker, marker.string().c_str()); + Require(RunChild(self, "--exit-drain") == 7); + std::ifstream file(marker); + const std::string text{std::istreambuf_iterator(file), std::istreambuf_iterator()}; + file.close(); + std::filesystem::remove(marker); + Require(text == "drain\nflush\n"); + Require(RunChild(self, "--exit-drain-throws") == 7); + const auto started = std::chrono::steady_clock::now(); + Require(RunChild(self, "--exit-drain-blocked") == 7); + const auto elapsed = std::chrono::steady_clock::now() - started; + Require(elapsed >= std::chrono::seconds(8) && elapsed < std::chrono::seconds(12)); + std::puts("Host exit drain completed before immediate termination and stayed bounded"); + return 0; +} } int main(int argc, char** argv) { const int firstArgument = argc > 0 && std::strncmp(argv[0], "--", 2) == 0 ? 0 : 1; + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--exit-drain-driver") == 0) return DriveExitDrain(); + if (argc > firstArgument && std::strncmp(argv[firstArgument], "--exit-drain", 12) == 0) { + LibcRegisterShutdown_nid_postfix(UnexpectedCleanup); + Require(std::atexit(UnexpectedCleanup) == 0); + if (std::strcmp(argv[firstArgument], "--exit-drain-throws") == 0) LibcSetExitDrain_nid_no_patch(ThrowingExitDrain); + else if (std::strcmp(argv[firstArgument], "--exit-drain-blocked") == 0) LibcSetExitDrain_nid_no_patch(BlockedExitDrain); + else { + LibcSetExitDrain_nid_no_patch(DelayedExitDrain); + LibcSetEndFlush_nid_no_patch(ExitFlushAfterDrain); + } + _Exit_nid_postfix(7); + } if (argc > firstArgument && std::strcmp(argv[firstArgument], "--requested-exit-driver") == 0) { Require(RunChild(OwnExecutable(), "--requested-exit") == 0); return 0; From 2f5282560358c1485c593bdbde7bc2691ca3dde0 Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Sun, 18 Oct 2026 04:53:15 -0400 Subject: [PATCH 033/219] chore: record UI Lab PR metadata Complete the publication checklist and series cross-links for #7474. No source or test changes. From f67e221738d37089475e38e7fd3ef409b17601cd Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:32:14 +0200 Subject: [PATCH 034/219] test(libc): check that the macOS guest arena keeps free space beside host regions The four guest_arena tests still pass with the mach_vm_region walk disabled, because TryReserve marks every 1 GiB chunk it cannot reserve as occupied. guest_arena_gap reads the host map before the arena reserves its range and requires a fixed guest mapping in free space that shares a chunk with a host region to be accepted. The debt entry also records that on macOS 27.0.1 (M4 Pro) the unmappable range starts at 0xF_C000_0000. --- core/libs/tests/GuestArenaHost.cpp | 35 +++++++++++++++++++++- core/libs/tests/guest_arena/CMakeLists.txt | 2 +- docs/dev/debt/libc.md | 2 +- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/core/libs/tests/GuestArenaHost.cpp b/core/libs/tests/GuestArenaHost.cpp index 6a9a907466..e7863563a6 100644 --- a/core/libs/tests/GuestArenaHost.cpp +++ b/core/libs/tests/GuestArenaHost.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/GuestArena.hpp" +#include #include #include #include @@ -20,7 +21,11 @@ using namespace GuestArena; namespace { constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull; +constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull; +constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull; constexpr std::uintptr_t ApplicationAreaEnd = 0x000000FC00000000ull; +constexpr std::uintptr_t ReservationChunk = 1ull << 30; +constexpr std::size_t GapBytes = 0x10000; constexpr std::uint32_t ProtectionNoAccess = 0x01; constexpr std::uint32_t ProtectionReadOnly = 0x02; constexpr std::uint32_t ProtectionReadWrite = 0x04; @@ -51,6 +56,15 @@ std::vector RegionsInArenaRange() { return regions; } +const std::vector RegionsBeforeArena = RegionsInArenaRange(); + +bool Overlaps(const std::vector& regions, std::uintptr_t start, std::uintptr_t end) { + for (const auto& region : regions) { + if (region.start < end && start < region.end) return true; + } + return false; +} + void CheckAvailable() { Require(GuestArenaAvailable_nid_postfix(), "the guest arena is unavailable on macOS"); std::uintptr_t base = 0; @@ -88,6 +102,24 @@ void CheckHostRegionsKept() { std::printf("%zu host regions inside the arena range are protected\n", hostRegions); } +void CheckGapBesideHostRegion() { + Require(GuestArenaAvailable_nid_postfix(), "the guest arena is unavailable on macOS"); + Require(RegionsBeforeArena.size() < RegionsInArenaRange().size(), "the host map was read after the guest arena reserved its range"); + for (const auto& host : RegionsBeforeArena) { + const auto chunk = host.start & ~(ReservationChunk - 1); + for (auto gap = std::max(chunk, ArenaStart); gap + GapBytes <= chunk + ReservationChunk && gap + GapBytes <= ApplicationAreaEnd; gap += GapBytes) { + if (Overlaps(RegionsBeforeArena, gap, gap + GapBytes) || (gap < SystemReservedEnd && SystemReservedStart < gap + GapBytes)) continue; + auto* pointer = reinterpret_cast(gap); + GuestArenaMarkUsed_nid_postfix(pointer, GapBytes); + GuestArenaRelease_nid_postfix(pointer, GapBytes); + std::printf("free range 0x%llx beside host region 0x%llx-0x%llx accepted\n", static_cast(gap), + static_cast(host.start), static_cast(host.end)); + return; + } + } + std::puts("no free range shares a 1 GiB reservation chunk with a host region on this host; nothing to check"); +} + void CheckCommitAndProtect() { constexpr std::size_t bytes = 0x10000; auto* pointer = static_cast(GuestArenaAllocate_nid_postfix(bytes, bytes)); @@ -106,11 +138,12 @@ void CheckCommitAndProtect() { int main(int argc, char** argv) { try { - Require(argc == 2, "expected available, unmappable, host or commit"); + Require(argc == 2, "expected available, unmappable, host, gap or commit"); const std::string_view mode(argv[1]); if (mode == "available") CheckAvailable(); else if (mode == "unmappable") CheckSkipsUnmappable(); else if (mode == "host") CheckHostRegionsKept(); + else if (mode == "gap") CheckGapBesideHostRegion(); else if (mode == "commit") CheckCommitAndProtect(); else throw std::runtime_error("unknown guest arena test"); std::printf("guest arena %s passed\n", argv[1]); diff --git a/core/libs/tests/guest_arena/CMakeLists.txt b/core/libs/tests/guest_arena/CMakeLists.txt index f84f4e6842..e9a10f6a4c 100644 --- a/core/libs/tests/guest_arena/CMakeLists.txt +++ b/core/libs/tests/guest_arena/CMakeLists.txt @@ -15,7 +15,7 @@ set_target_properties(guest_arena_tests PROPERTIES CXX_EXTENSIONS OFF RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") -foreach(mode available unmappable host commit) +foreach(mode available unmappable host gap commit) add_test(NAME guest_arena_${mode} COMMAND guest_arena_tests ${mode}) set_tests_properties(guest_arena_${mode} PROPERTIES TIMEOUT 30) endforeach() diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d787587580..4aae478cfc 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -68,7 +68,7 @@ - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (file reads and socket receives); a host call that writes guest memory without it (`clock_gettime` and the like into a watched page) fails; - code that changes the protection of watched pages, or maps over them, has to call `GuestWriteWatchProtectionChanged`, and code that asks the kernel for a page's protection has to ask `GuestWriteWatchProtection` first, since a watched page is read-only between a collection and the next write. Nothing calls either yet: the macOS memory paths are not in the tree; - the handler is installed when libc loads and passes other faults to the handler installed before it. A handler installed later has to do the same. -- On macOS the [guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) finds host regions with `mach_vm_region` and reserves the rest with a fixed `mach_vm_map`, which fails instead of replacing, as macOS has no `/proc/self/maps` or `MAP_FIXED_NOREPLACE`. The kernel keeps 0x10_0000_0000-0x70_0000_0000 unmappable for every process (max protection none; macOS 26.5, M1 Pro, arm64 and x86_64 under Rosetta), and dyld and the Rosetta runtime sit near 0x2_0200_0000 and 0x3_0400_0000, so a guest mapping fixed inside those ranges fails where the console places it; mappings without a fixed address are placed around them. +- On macOS the [guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) finds host regions with `mach_vm_region` and reserves the rest with a fixed `mach_vm_map`, which fails instead of replacing, as macOS has no `/proc/self/maps` or `MAP_FIXED_NOREPLACE`. The kernel keeps 0x10_0000_0000-0x70_0000_0000 unmappable for every process (max protection none; macOS 26.5, M1 Pro, arm64 and x86_64 under Rosetta; on macOS 27.0.1, M4 Pro, both architectures, it starts at 0xF_C000_0000), and dyld and the Rosetta runtime sit near 0x2_0200_0000 and 0x3_0400_0000, so a guest mapping fixed inside those ranges fails where the console places it; mappings without a fixed address are placed around them. - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync` - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way - [`fopen` streams](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access. On Windows, opening a directory in a read mode fails with `EACCES`, where FreeBSD opens it; directory streams are not emulated From b3068c07ad0c00067205bd63793ffa4238008d2d Mon Sep 17 00:00:00 2001 From: Nicolas Abosch <88494836+nicolasabosch@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:04:19 -0300 Subject: [PATCH 035/219] docs(libkernel): list APS5_NO_SLEEP_WAKE in the environment reference Co-Authored-By: Claude Opus 5.5 --- docs/dev/ENVIRONMENT.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 97411f4b5d..148c1b8017 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -527,6 +527,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_JOB_AFFINITY` | presence | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | pins the title's job worker threads to the efficiency cores of a hybrid CPU | | `APS5_JOB_AFFINITY_MASK` | integer | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | hex core mask that job worker threads are pinned to | | `APS5_NO_JOB_AFFINITY` | presence | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | disables pinning of job worker threads | +| `APS5_NO_SLEEP_WAKE` | presence | `core/libs/prx/libkernel/Time/SleepWake.cpp` | | keeps every sleep of 1 ms or less in full instead of ending it at a GPU batch, batch completion action or flip completion | | `APS5_NO_TIMER_RESOLUTION` | presence | `core/libs/prx/libkernel/Time/Time.cpp` | | keeps the default 15.6 ms scheduler tick instead of raising it to 0.5 ms on Windows | | `APS5_TIME_SCALE` | integer | `core/libs/prx/libkernel/Time/Time.cpp` | 1 | factor applied to the guest monotonic clocks and TSC; sleeps still take real time | | `APS5_TRACE_APR` | presence | `core/libs/prx/libkernel/Apr/src/Apr.cpp` | | logs every APR file read with its path, offset, size and destination | From d1596f2723759f3e7814f58c94f8f597f27d12df Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Sun, 18 Oct 2026 12:01:48 +0200 Subject: [PATCH 036/219] perf(shader): drop window stores no relative read can see --- .../libSceAgcDriver/tests/VectorWindow.cpp | 29 +++++++ core/shader/recompiler/CMakeLists.txt | 1 + .../VectorWindowStoreEliminator.hpp | 20 +++++ .../src/VectorWindowStoreEliminator.cpp | 78 +++++++++++++++++++ core/shader/recompiler/Recompiler.cpp | 6 ++ 5 files changed, 134 insertions(+) create mode 100644 core/shader/recompiler/Optimization/include/Optimization/VectorWindowStoreEliminator.hpp create mode 100644 core/shader/recompiler/Optimization/src/VectorWindowStoreEliminator.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp index 2028dcb2b1..a69e325898 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp @@ -3,6 +3,7 @@ #include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include "Translation/InstructionTranslator.hpp" #include +#include #include #include #include @@ -21,6 +22,7 @@ using namespace ShaderRecompiler; constexpr std::uint32_t MoveM0Two = 0xbefc0382u; constexpr std::uint32_t MoveM0GroupX = 0xbefc0304u; constexpr std::uint32_t MoveV4One = 0x7e0802f2u; +constexpr std::uint32_t MoveV4Two = 0x7e0802f4u; constexpr std::uint32_t MoveV5Two = 0x7e0a02f4u; constexpr std::uint32_t MoveV6Four = 0x7e0c02f6u; constexpr std::uint32_t MovrelsV1V4 = 0x7e028704u; @@ -117,6 +119,24 @@ Shape Scan(std::span words) { return shape; } +std::size_t WindowStores(std::span words) { + std::vector functionPointers; + std::vector pointers; + std::size_t stores = 0; + const auto contains = [](const std::vector& ids, std::uint32_t id) { return std::find(ids.begin(), ids.end(), id) != ids.end(); }; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> spv::WordCountShift; + const auto op = static_cast(words[cursor] & spv::OpCodeMask); + Require(count != 0 && cursor + count <= words.size(), "truncated SPIR-V instruction"); + if (op == spv::OpTypePointer && words[cursor + 2] == spv::StorageClassFunction) functionPointers.push_back(words[cursor + 1]); + if (op == spv::OpVariable && words[cursor + 3] == spv::StorageClassFunction && contains(functionPointers, words[cursor + 1])) pointers.push_back(words[cursor + 2]); + if (op == spv::OpAccessChain && contains(pointers, words[cursor + 3])) pointers.push_back(words[cursor + 2]); + if (op == spv::OpStore && contains(pointers, words[cursor + 1])) ++stores; + cursor += count; + } + return stores; +} + IrProgram TranslateStage(ShaderStageKind stage, std::uint32_t waveSize, std::uint32_t base = 0u, std::uint32_t fetchStart = 0u, std::uint32_t fetchCount = 0u) { ShaderComputeInputInfo compute{}; compute.threadIdsNum = 3; @@ -293,6 +313,15 @@ int main(int argc, char** argv) { return shape.functionArrays == 1; }); + passed &= run("a window register written twice before a relative read keeps only its last window store", [] { + const std::array overwritten{MoveM0GroupX, MoveV4One, MoveV4Two, MoveV5Two, MoveV6Four, MovrelsV1V4, 0xe0700000u, 0x80000100u, EndProgram}; + const std::array once{MoveM0GroupX, MoveV4Two, MoveV5Two, MoveV6Four, MovrelsV1V4, 0xe0700000u, 0x80000100u, EndProgram}; + const auto twice = WindowStores(RecompileProgram(overwritten)); + const auto single = WindowStores(RecompileProgram(once)); + if (twice != single || single < 3u) std::fprintf(stderr, " window stores: written twice %zu, written once %zu\n", twice, single); + return twice == single && single >= 3u; + }); + if (passed) std::puts("Vector window shape and SSA control tests passed"); return passed ? 0 : 1; } diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index e4bcac3e90..f577e50c27 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -112,6 +112,7 @@ function(add_shader_recompiler target runtimeTarget) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/ShaderInfoCollector.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SharedMemoryBarrierInserter.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/DenormalFlushEliminator.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/VectorWindowStoreEliminator.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/RedundantValueEliminator.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtAddressArithmetic.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp diff --git a/core/shader/recompiler/Optimization/include/Optimization/VectorWindowStoreEliminator.hpp b/core/shader/recompiler/Optimization/include/Optimization/VectorWindowStoreEliminator.hpp new file mode 100644 index 0000000000..fd8d5f6ac7 --- /dev/null +++ b/core/shader/recompiler/Optimization/include/Optimization/VectorWindowStoreEliminator.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_VECTORWINDOWSTOREELIMINATOR_HPP +#define CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_VECTORWINDOWSTOREELIMINATOR_HPP + +#include "IntermediateRepresentation/IrProgram.hpp" +#include + +namespace ShaderRecompiler { + +struct VectorWindowStoreEliminationStats { + std::uint32_t removedStores = 0; +}; + +class VectorWindowStoreEliminator { +public: + [[nodiscard]] VectorWindowStoreEliminationStats Eliminate(IrProgram& program) const; +}; + +} + +#endif diff --git a/core/shader/recompiler/Optimization/src/VectorWindowStoreEliminator.cpp b/core/shader/recompiler/Optimization/src/VectorWindowStoreEliminator.cpp new file mode 100644 index 0000000000..32248e72f9 --- /dev/null +++ b/core/shader/recompiler/Optimization/src/VectorWindowStoreEliminator.cpp @@ -0,0 +1,78 @@ +#include "Optimization/VectorWindowStoreEliminator.hpp" +#include +#include + +namespace ShaderRecompiler { + +namespace { + +using LiveSlots = std::vector; + +const IrValue* constantSlot(const IrValue* inst, std::size_t slots) { + const IrValue* index = inst->Argument(0)->Resolve(); + return index->HasImmediate() && index->ImmediateU32() < slots ? index : nullptr; +} + +void transfer(const IrValue* inst, LiveSlots& live) { + if (inst->Opcode() == IrOpcode::LoadVectorWindow) { + if (const IrValue* slot = constantSlot(inst, live.size())) live[slot->ImmediateU32()] = true; + else live.assign(live.size(), true); + } else if (inst->Opcode() == IrOpcode::StoreVectorWindow) { + if (const IrValue* slot = constantSlot(inst, live.size())) live[slot->ImmediateU32()] = false; + } +} + +} + +VectorWindowStoreEliminationStats VectorWindowStoreEliminator::Eliminate(IrProgram& program) const { + VectorWindowStoreEliminationStats stats; + const std::size_t slots = program.Metadata().vectorWindowSize; + if (slots == 0u) return stats; + const auto& order = program.BlockOrder(); + std::unordered_map liveIn; + for (const IrBlock* block : order) liveIn.emplace(block, LiveSlots(slots, false)); + const auto liveOut = [&](const IrBlock* block) { + LiveSlots live(slots, false); + for (const IrBlock* successor : block->Successors()) { + const auto found = liveIn.find(successor); + if (found == liveIn.end()) return LiveSlots(slots, true); + for (std::size_t slot = 0; slot < slots; ++slot) live[slot] = live[slot] || found->second[slot]; + } + return live; + }; + for (bool changed = true; changed;) { + changed = false; + for (auto block = order.rbegin(); block != order.rend(); ++block) { + auto live = liveOut(*block); + const auto& instructions = (*block)->Instructions(); + for (auto inst = instructions.rbegin(); inst != instructions.rend(); ++inst) transfer(*inst, live); + auto& entry = liveIn[*block]; + if (live != entry) { + entry = std::move(live); + changed = true; + } + } + } + for (IrBlock* block : order) { + auto live = liveOut(block); + auto& instructions = block->Instructions(); + for (auto inst = instructions.end(); inst != instructions.begin();) { + --inst; + IrValue* value = *inst; + if (value->Opcode() == IrOpcode::StoreVectorWindow) { + const IrValue* slot = constantSlot(value, slots); + if (slot != nullptr && !live[slot->ImmediateU32()]) { + value->Invalidate(); + inst = instructions.erase(inst); + value->SetParent(nullptr); + ++stats.removedStores; + continue; + } + } + transfer(value, live); + } + } + return stats; +} + +} diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index d08bc8a148..fac339e761 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -38,6 +38,7 @@ #include "Optimization/include/Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" #include "Optimization/include/Optimization/ResourceTracker.hpp" +#include "Optimization/include/Optimization/VectorWindowStoreEliminator.hpp" #include "Optimization/include/Optimization/ShaderInfoCollector.hpp" #include "Optimization/include/Optimization/SrtWalker.hpp" #include "Optimization/include/Optimization/SsaBuilder.hpp" @@ -235,6 +236,7 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat constexpr DeadCodeEliminator deadCodeEliminator; constexpr ReadLaneEliminator readLaneEliminator; constexpr MaskedSelectEliminator maskedSelectEliminator; + constexpr VectorWindowStoreEliminator vectorWindowStoreEliminator; const auto simplify = [&] { dumpIr("translate"); ssaBuilder.Rewrite(program); @@ -254,6 +256,10 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat deadCodeEliminator.Eliminate(program); } + if (vectorWindowStoreEliminator.Eliminate(program).removedStores != 0u) { + deadCodeEliminator.Eliminate(program); + } + if (maskedSelectEliminator.Eliminate(program).removedSelects != 0u) { const bool liveAncillary = std::ranges::any_of(program.Blocks(), [](const auto& block) { return std::ranges::any_of(block->Instructions(), [](const IrValue* value) { From ca01351ff8829a8b20020c15276c9c7471e7e66a Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Sun, 18 Oct 2026 22:00:35 +0200 Subject: [PATCH 037/219] test(shader): check window values across blocks and loop iterations --- .../tests/execution/VectorWindowLiveness.cpp | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowLiveness.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowLiveness.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowLiveness.cpp new file mode 100644 index 0000000000..54193f052e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowLiveness.cpp @@ -0,0 +1,93 @@ +#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 MaxThreads = 64; +constexpr std::uint32_t Results = 4; +alignas(256) std::array Output{}; +alignas(256) std::array Unused{}; + +alignas(256) constexpr std::array Code{ + 0x34060084, 0x4a080081, 0xbf068008, 0xbf850001, 0x4a080890, 0xbefc0380, 0x7e028704, 0xe0701000, + 0x80010103, 0x4a080081, 0xbe890383, 0x7e0a0280, 0xbefc0380, 0x7e0c8704, 0x4a0a0b06, 0x4a080890, + 0x80898109, 0xbf078009, 0xbf85fff9, 0xe0701004, 0x80010503, 0xbf810000, +}; + +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::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::uint32_t threads, std::uint32_t overwrite) { + Output.fill(0xdeadbeefu); + std::vector userData(9, 0u); + const auto input = BufferDescriptor(Unused.data(), static_cast(Unused.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); + userData[8] = overwrite; + 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, {}}, + {waveSize, 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(std::uint32_t waveSize, std::uint32_t threads, std::uint32_t overwrite) { + for (std::uint32_t tid = 0; tid < threads; ++tid) { + const std::uint32_t* out = &Output[tid * Results]; + const std::uint32_t written = tid + 1u; + const std::uint32_t branch = overwrite != 0u ? written + 16u : written; + const std::uint32_t loop = 3u * written + 48u; + const auto where = std::string("vector window liveness: wave") + std::to_string(waveSize) + " lane " + std::to_string(tid) + (overwrite != 0u ? " (overwritten before the read)" : " (overwrite branched over)"); + Require(out[0] == branch, where + ": the window register written before the branch read " + Hex(out[0]) + ", expected " + Hex(branch)); + Require(out[1] == loop, where + ": the window register carried around the loop read " + Hex(out[1]) + ", expected " + Hex(loop)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const std::uint32_t waveSize : {32u, 64u}) { + for (const std::uint32_t overwrite : {0u, 1u}) { + Run(*device, waveSize, waveSize, overwrite); + Check(waveSize, waveSize, overwrite); + } + } + std::puts("vector window liveness tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 3e5499310c596d3cd5ff297759bfdb7813834287 Mon Sep 17 00:00:00 2001 From: thibaut Date: Sun, 18 Oct 2026 23:15:36 +0200 Subject: [PATCH 038/219] fix(shader): round normalized storage image writes to nearest even Vulkan lets a device return either neighbour when it converts a float to a normalized texel, and devices differ: for a product of 123.50003, NVIDIA and Intel both store 123, and on GTA V's FSR1 RCAS compute pass the two vendors stored different bytes for the same inputs. Writes to unorm and snorm storage images whose components share one width now clamp the value (NaN to 0), round value * scale to nearest even in the shader and store level * c, where c is the smallest float not below 1 / scale. The exact product of that float with the scale lies in [level, level + 0.01) for 8-, 10- and 16-bit unorm and snorm, so a device that rounds to nearest and one that truncates both store the level. The format comes from the image descriptor through specialization constants (StoreQuantizeBase, four per written storage image), so other formats keep the plain write. Test: agc_driver_image_store_unorm_rounding stores values next to the midpoints between levels, edge values and NaN/infinities to 8_UNORM, 16_16_UNORM, 8_8_8_8_UNORM, 8_8_8_8_SNORM and 16_16_16_16_UNORM images; it fails on main on an RTX 4090 and on an Intel UHD 770 and passes on both with this change. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XBXM4pgkMHYEtqN1EQni2P --- .../execution/ImageStoreUnormRounding.cpp | 264 ++++++++++++++++++ .../src/DescriptorBindingBuilder.cpp | 18 ++ .../recompiler/PipelineSpecialization.hpp | 2 + .../SpirvBackend/src/SpirvImageEmitter.cpp | 33 ++- 4 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreUnormRounding.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreUnormRounding.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreUnormRounding.cpp new file mode 100644 index 0000000000..e8d05cc432 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreUnormRounding.cpp @@ -0,0 +1,264 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#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 Width = 32; +constexpr std::uint32_t Height = 8; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::uint32_t TwoChannelSwizzle = 0x22cu; +constexpr std::size_t TexelBytes = 8192; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) std::array Input{}; + +alignas(256) constexpr std::array StoreCode{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0201f08, 0x0001141e, + 0x7e3e0281, 0xf0201508, 0x0001141e, 0xbf810000, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "image store unorm rounding: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + + +struct Format { + const char* name; + std::uint32_t format; + std::uint32_t components; + std::uint32_t bits; + bool snorm; +}; + +constexpr std::array Formats{{ + {"8_UNORM", 1u, 1u, 8u, false}, + {"16_16_UNORM", 23u, 2u, 16u, false}, + {"8_8_8_8_UNORM", 56u, 4u, 8u, false}, + {"8_8_8_8_SNORM", 57u, 4u, 8u, true}, + {"16_16_16_16_UNORM", 65u, 4u, 16u, false}, +}}; + +struct Write { + std::uint32_t row; + std::uint32_t dmask; +}; + +constexpr std::array StoreWrites{{{0u, 0xfu}, {1u, 0x5u}}}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +float Scale(const Format& format) { + return static_cast((1u << (format.snorm ? format.bits - 1u : format.bits)) - 1u); +} + +float Value(const Format& format, std::uint32_t tid, std::uint32_t index) { + constexpr std::array Edges{0.0f, -0.0f, 1.0f, 0.5f, -0.25f, 1.5f, -1.5f, 1e-30f, -1e-30f, 0.999f, 0.0019607844f, 0.9980392f, 0.33333334f, -0.6666667f, 2e-5f, 0.75f}; + const auto slot = tid * 4u + index; + if (slot < Edges.size()) return Edges[slot]; + if (slot == Edges.size()) return std::numeric_limits::quiet_NaN(); + if (slot == Edges.size() + 1u) return std::numeric_limits::infinity(); + if (slot == Edges.size() + 2u) return -std::numeric_limits::infinity(); + const auto scale = Scale(format); + const auto level = static_cast((slot * 37u) % static_cast(scale)); + auto value = (level + 0.5f) / scale; + for (std::uint32_t step = 0; step < slot % 5u; ++step) value = std::nextafter(value, slot % 2u == 0u ? 2.0f : -2.0f); + return format.snorm && slot % 3u == 0u ? -value : value; +} + +std::uint32_t Level(const Format& format, float value) { + const auto lower = format.snorm ? -1.0f : 0.0f; + const auto clamped = std::isnan(value) ? 0.0f : std::clamp(value, lower, 1.0f); + const auto product = clamped * Scale(format); + const auto level = static_cast(std::nearbyint(product)); + return static_cast(level) & ((1u << format.bits) - 1u); +} + +std::uint32_t TexelBytesOf(const Format& format) { + return format.components * format.bits / 8u; +} + +std::uint32_t SwizzleOf(const Format& format) { + return format.components == 2u ? TwoChannelSwizzle : IdentitySwizzle; +} + +AgcDriver::Graphics::TileMipLayout Mip(const Format& format) { + return AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format.format, Width, Height, 1u).at(0); +} + +std::uint64_t TexelOffset(const Format& format, std::uint32_t x, std::uint32_t y) { + const auto mip = Mip(format); + return mip.tiledOffset + static_cast(y) * mip.pitchBytes + static_cast(x) * TexelBytesOf(format); +} + +std::vector Initial() { + std::vector bytes(TexelBytes); + for (std::size_t index = 0; index < bytes.size(); ++index) { + bytes[index] = static_cast(0x80u + 37u * index + 11u * (index >> 4u)); + } + return bytes; +} + +std::uint32_t StoredComponent(const Format& format, std::uint32_t tid, std::uint32_t dmask, std::uint32_t component) { + if (((dmask >> component) & 1u) == 0u) return 0u; + std::uint32_t index = 0; + for (std::uint32_t lower = 0; lower < component; ++lower) index += (dmask >> lower) & 1u; + return Level(format, Value(format, tid, index)); +} + +std::vector Expected(const Format& format) { + auto bytes = Initial(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (const auto& write : StoreWrites) { + auto* texel = bytes.data() + TexelOffset(format, tid, write.row); + for (std::uint32_t component = 0; component < format.components; ++component) { + const auto value = StoredComponent(format, tid, write.dmask, component); + std::memcpy(texel + component * format.bits / 8u, &value, format.bits / 8u); + } + } + } + return bytes; +} + +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::array TextureDescriptor(const void* data, std::uint32_t format, std::uint32_t swizzle) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + swizzle | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span code, std::uint32_t format, std::uint32_t swizzle, const std::array& buffer) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels, format, swizzle); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + 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(); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), TexelBytes, nullptr, "test"); + device.WaitIdle(); +} + +void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Format& format) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t index = 0; index < 4u; ++index) Input[tid * 4u + index] = std::bit_cast(Value(format, tid, index)); + } + const auto initial = Initial(); + std::copy(initial.begin(), initial.end(), texels); + Dispatch(device, texels, StoreCode, format.format, SwizzleOf(format), BufferDescriptor(Input.data(), static_cast(Input.size() * 4u))); +} + +void CheckTexels(const std::uint8_t* texels, const Format& format) { + const auto expected = Expected(format); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = TexelOffset(format, x, y); + for (std::uint32_t component = 0; component < format.components; ++component) { + std::uint32_t actual = 0; + std::uint32_t wanted = 0; + std::memcpy(&actual, texels + offset + component * format.bits / 8u, format.bits / 8u); + std::memcpy(&wanted, expected.data() + offset + component * format.bits / 8u, format.bits / 8u); + Require(actual == wanted, std::string("image_store of ") + format.name + ": texel (" + std::to_string(x) + ", " + std::to_string(y) + ") component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(wanted) + " (round to nearest even)"); + } + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + auto* texels = block.Data(); + for (const auto& format : Formats) { + Store(*device, texels, format); + CheckTexels(texels, format); + } + std::puts("image store unorm rounding tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index c7a42f7a98..aea4f015b6 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -355,6 +357,22 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const auto first = PipelineSpecialization::ImageBase + index * PipelineSpecialization::ImageWords; plan.specialization.push_back({first, imageModes[index]}); for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({first + 1u + component, (snapshot.images.at(index).dwords[3] >> (component * 3u)) & 7u}); + const auto& image = info.images[index]; + if (image.resourceClass != ImageResourceClass::Storage || !image.written || image.atomic) continue; + const auto code = (snapshot.images.at(index).dwords[1] >> 20u) & 0x1ffu; + const auto format = code >= 1u && code <= 77u ? GetFormatInfo(static_cast(code)) : SpirvBufferFormatInfo{}; + const bool snorm = format.type == SpirvFormatComponentType::Snorm; + const auto bits = format.componentBits[0]; + const bool uniform = format.componentCount != 0u && !format.packedBitfield && bits >= 2u && bits <= 16u && std::all_of(format.componentBits, format.componentBits + format.componentCount, [&](std::uint32_t width) { return width == bits; }); + const bool quantized = uniform && (format.type == SpirvFormatComponentType::Unorm || snorm); + const auto scale = quantized ? static_cast((1u << (snorm ? bits - 1u : bits)) - 1u) : 1.0f; + auto reciprocal = 1.0f / scale; + while (static_cast(reciprocal) * static_cast(scale) < 1.0) reciprocal = std::nextafter(reciprocal, 2.0f); + const auto quantize = PipelineSpecialization::StoreQuantizeBase + index * PipelineSpecialization::StoreQuantizeWords; + plan.specialization.push_back({quantize, quantized ? 1u : 0u}); + plan.specialization.push_back({quantize + 1u, std::bit_cast(scale)}); + plan.specialization.push_back({quantize + 2u, std::bit_cast(reciprocal)}); + plan.specialization.push_back({quantize + 3u, quantized && snorm ? 1u : 0u}); } for (std::uint32_t target = 0; target < exportMappings.size() && !registrationModules; ++target) { for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({PipelineSpecialization::ExportBase + target * 4u + component, (exportMappings[target] >> (component * 2u)) & 3u}); diff --git a/core/shader/recompiler/PipelineSpecialization.hpp b/core/shader/recompiler/PipelineSpecialization.hpp index e47d9879be..6435d3b012 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -32,6 +32,8 @@ inline constexpr std::uint32_t StencilReferenceOffset = 8235u; inline constexpr std::uint32_t ReversedBlend = 8236u; inline constexpr std::uint32_t HeapCountBase = 8256u; inline constexpr std::uint32_t MipCountBase = 8320u; +inline constexpr std::uint32_t StoreQuantizeBase = 12288u; +inline constexpr std::uint32_t StoreQuantizeWords = 4u; inline constexpr std::uint32_t DescriptorIndexBase = 16384u; inline constexpr std::uint32_t DescriptorIndexStride = 128u; inline constexpr std::uint32_t FoldOffsetBase = 57344u; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 9d60e3f054..64969cc151 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1002,6 +1002,36 @@ std::uint32_t ImageSampleIndex(SpirvValueEmitContext& ctx, const ImageEmitAccess return AddressU32(ctx, access, addressInfo.coordinateComponents); } +std::uint32_t QuantizeStoreTexel(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t texel) { + const auto first = PipelineSpecialization::StoreQuantizeBase + resource * PipelineSpecialization::StoreQuantizeWords; + const auto f32 = TypeF32(state); + const auto vec4 = TypeF32Vector(state, 4u); + const auto bvec4 = state.module.Type(spv::OpTypeVector, TypeBool(state), 4u); + const auto splat = [&](std::uint32_t type, std::uint32_t scalar) { + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, type, result, scalar, scalar, scalar, scalar); + return result; + }; + const auto word = [&](std::uint32_t index) { return state.module.SpecializationConstant(TypeU32(state), first + index, 0u); }; + const auto enabled = Binary(state, spv::OpINotEqual, TypeBool(state), word(0u), ConstantU32(state, 0u)); + const auto scale = splat(vec4, Unary(state, spv::OpBitcast, f32, word(1u))); + const auto reciprocal = splat(vec4, Unary(state, spv::OpBitcast, f32, word(2u))); + const auto snorm = Binary(state, spv::OpINotEqual, TypeBool(state), word(3u), ConstantU32(state, 0u)); + const auto lower = splat(vec4, Select(state, f32, snorm, ConstantF32(state, std::bit_cast(-1.0f)), ConstantF32(state, 0u))); + const auto zero = splat(vec4, ConstantF32(state, 0u)); + const auto one = splat(vec4, ConstantF32(state, std::bit_cast(1.0f))); + const auto finite = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelect, vec4, finite, Unary(state, spv::OpIsNan, bvec4, texel), zero, texel); + const auto clamped = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, vec4, clamped, GlslStd450(state), GLSLstd450FClamp, finite, lower, one); + const auto level = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, vec4, level, GlslStd450(state), GLSLstd450RoundEven, Binary(state, spv::OpFMul, vec4, clamped, scale)); + const auto quantized = Binary(state, spv::OpFMul, vec4, level, reciprocal); + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelect, vec4, result, splat(bvec4, enabled), quantized, texel); + return result; +} + void EmitWriteOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { auto& state = ctx.state; const bool uintImage = access.image.numericClass == IrTextureNumericClass::Uint; @@ -1015,7 +1045,8 @@ void EmitWriteOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { EmitIfCondition(state, condition, [&]() { const auto mipLod = access.image.mipMode == ImageMipMode::DynamicStorage ? LodU32(ctx, access) : 0u; const auto coord = CoordU32(ctx, access); - const auto texel = access.mem.imagePacked ? PackedStoreTexel(ctx, access, ctx.Arg(access.inst, 2)) : StoreTexel(ctx, access, ctx.Arg(access.inst, 2), uintImage); + auto texel = access.mem.imagePacked ? PackedStoreTexel(ctx, access, ctx.Arg(access.inst, 2)) : StoreTexel(ctx, access, ctx.Arg(access.inst, 2), uintImage); + if (!uintImage && !access.mem.imagePacked && !access.image.atomic && access.image.table == NoTable && !state.registrationModules && access.image.conversionFormat == IrBufferFormat::Invalid) texel = QuantizeStoreTexel(state, access.mem.resource, texel); EmitStorageImageWrite(state, access.mem.resource, mipLod, coord, texel, ImageSampleIndex(ctx, access)); }); } From 84e3b73f160c78625424c2bba94bd360e2fcbc94 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Sat, 17 Oct 2026 15:55:24 +0200 Subject: [PATCH 039/219] fix(agc): keep depth planes coherent with buffer reads and writes of their memory A depth surface lives in its Vulkan image, so a buffer read of its depth plane saw guest memory the surface never wrote, and a buffer write into the plane never reached the surface: binding the target again kept the old depth. - A single-sample 32-bit depth surface with the 64 KiB Z swizzle (DB_Z_INFO SW_MODE 24) keeps a storage image in that swizzle over its plane. A bind that writes or fast clears depth marks the plane stale; FlushDepthPlanes copies the depth into the storage image and stores it into guest memory. It runs from StorageTexture::FlushPending, when a buffer upload finishes (over its regions and address space), and before a dispatch for each GuestBuffers V# it binds (a cached dispatch reads them without an upload). A reused build flushes its in-place buffer regions, and an image lookup flushes the range it reads, as do the write-stamp proofs that let a cached image or a reused build skip that lookup: a stale plane's memory looks unchanged to them. - Stale planes are kept as ranges under their own lock with an atomic hull; a flush that overlaps none returns without the GPU and surface locks. - Every bind of such a surface, and every flush of its stale plane, collects the plane's writes. Written by someone else since the surface's previous bind, store or reload, the plane is reloaded from memory instead of stored, so a guest write after a depth draw is not overwritten by that draw's late store. A pending HTILE fast clear wins over the reload. - A color target flushes the stale planes under it before it retires their surfaces, so a target that loads or blends reads the stored depth. A retired surface's plane is no longer stale: a later flush never stores its old depth over what the new owner of the memory wrote. - The depth surfaces lock is recursive: a store reached from a flush under DepthSurfaceTexture re-enters it. - Draws pass whether they write depth, so read-only binds do not mark the plane stale. PPSA05684 copies its main depth plane into two ping-pong planes with a buffer compute, draws the cage's depth into them and fogs its menu from the result; without this the copy carried nothing and the fog covered the cliff and the cage. agc_driver_depth_plane_buffer_memory draws depth 0.5 into such a plane, fetches vertices from it (the plane must hold 0.5), writes 0.75 into the memory and draws at 0.5 under LESS (must pass), then checks the depth written after the reload reaches memory again. On a new plane it writes memory after a depth draw and before any store: a buffer read must keep the guest bytes and the next bind must reload them; a guest write before a depth draw must then lose to that draw. A linear texture at an offset into a plane must read what memory holds after a flush, also when it is viewed through its cached storage image after another depth draw. On a plane with HTILE, an HTILE fast clear after a guest write must win. A color target that blends ZERO/ONE over a plane just drawn at 0.5 must write back the 0.5 it loaded. AI-assisted: yes --- .../Execution/src/VulkanDevice.cpp | 11 + .../Graphics/include/DepthSurface.hpp | 6 +- .../Graphics/src/DepthSurface.cpp | 192 +++++++++++++-- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 5 +- .../Graphics/src/GuestBufferMemory.cpp | 2 + .../Graphics/src/ShaderResources.cpp | 16 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 2 + .../tests/execution/DepthFastClearHarness.hpp | 6 +- .../execution/DepthPlaneBufferMemory.cpp | 220 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 3 +- 10 files changed, 439 insertions(+), 24 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneBufferMemory.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index b4d9106aac..0045ea9931 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4802,6 +4802,17 @@ std::shared_ptr VulkanDevice::computePipeline(const Grap VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipeOut) { startJournalReplay(); + for (const auto& binding : shader.bindings) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; + for (std::size_t at = 0; at + 4u <= binding.guestDescriptor.size(); at += 4u) { + const auto* words = binding.guestDescriptor.data() + at; + const auto base = static_cast(words[0]) | (static_cast(words[1] & 0xffffu) << 32u); + const auto stride = (words[1] >> 16u) & 0x3fffu; + const auto records = static_cast(words[2]); + const auto bytes = stride != 0 ? records * stride : records; + if (base != 0 && bytes != 0) static_cast(Graphics::FlushDepthPlanes(base, static_cast(bytes))); + } + } state->recorder->BoundKeptBytes(); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); PerformanceTimer timing("Vulkan.Dispatch"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index df4e513ba8..54588aec75 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -8,13 +8,17 @@ #include #include #include +#include +#include namespace AgcDriver::Graphics { class Texture; class StorageTexture; -VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); +VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, bool depthWritten = true); +std::vector> FlushDepthPlanes(std::uint64_t address, std::size_t bytes); +bool DepthPlanesStale(); bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples); std::uint64_t DepthSurfacesSerial(); std::uint64_t DepthSurfacesReleased(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index eda844d298..ba8687d686 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -654,6 +654,32 @@ public: else Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); } + std::shared_ptr StoreDepthPlane(const GuestTextureResource& resource) { + ApplyFastClear(); + if (mirrorPlane.storage == nullptr) mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); + copyPlane(mirrorPlane, mirrorPlane.storage, false, true, true); + return mirrorPlane.storage; + } + + bool SyncFromMemory(const GuestTextureResource& resource) { + const auto bytes = static_cast(DescribeSurface(resource).guestBytes); + GuestMemory::CollectWrites(resource.baseAddress, bytes); + if (syncedGeneration == 0 || (pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 || !GuestMemory::WrittenSince(resource.baseAddress, bytes, syncedGeneration)) { + syncedGeneration = GuestMemory::TrackerGeneration(); + return false; + } + memoryStale = false; + if (mirrorPlane.storage == nullptr) mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); + else static_cast(mirrorPlane.storage->Refresh()); + syncedGeneration = GuestMemory::TrackerGeneration(); + copyPlane(mirrorPlane, mirrorPlane.storage, false, false, true); + written = ++depthWrites; + return true; + } + + bool memoryStale = false; + std::uint64_t syncedGeneration = 0; + const Context context; const DepthTarget target; const VkFormat hostFormat; @@ -777,6 +803,7 @@ private: std::uint64_t stencilBytes() const { return DepthSliceBytes(target.extent, 1u); } + PlaneStorage mirrorPlane; bool CoversPlane(const StorageTexture& storage, bool stencil) const { const auto& resource = storage.Descriptor(); @@ -788,15 +815,17 @@ private: return ResolveTextureFormat(resource.format) == (stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT); } - void copyPlane(PlaneStorage& plane, const std::shared_ptr& storage, bool stencil, bool toStorage) { + void copyPlane(PlaneStorage& plane, const std::shared_ptr& storage, bool stencil, bool toStorage, bool ownBatch = false) { auto* recorder = Recorder::Active(); - Require(recorder != nullptr, "depth surface storage copies need an active recorder"); + Require(recorder != nullptr || ownBatch, "depth surface storage copies need an active recorder"); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto bytes = static_cast(target.extent.width) * target.extent.height * (stencil ? 1u : d16 ? 2u : 4u); if (plane.staging == nullptr) plane.staging = std::make_unique(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); - const auto commands = recorder->Commands(); - recorder->Keep(storage); - const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + if (recorder != nullptr) recorder->Keep(storage); + const std::uint32_t timing = recorder != nullptr ? recorder->BeginGpuTiming(Recorder::CommandClass::Copy) : 0u; VkBufferImageCopy planeRegion{0, 0, 0, {stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}}; VkBufferImageCopy colorRegion = planeRegion; colorRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; @@ -807,6 +836,10 @@ private: RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); context.Function("vkCmdCopyBufferToImage")(commands, plane.staging->Handle(), destinationImage, VK_IMAGE_LAYOUT_GENERAL, 1, toStorage ? &colorRegion : &planeRegion); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + if (batch) { + batch->SubmitAndWait(); + return; + } Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); recorder->EndGpuTiming(timing, bytes); } @@ -815,6 +848,7 @@ private: depthPlane = {}; stencilPlane = {}; releaseStencilCopy(); + mirrorPlane = {}; textures.clear(); if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); if (owned != nullptr) owned.reset(); @@ -1107,8 +1141,8 @@ bool sameSurface(const DepthTarget& a, const DepthTarget& b) { return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format && a.samples == b.samples && (a.samples == 1u || (a.htileAddress == b.htileAddress && a.htileStencil == b.htileStencil && a.depthSwizzle == b.depthSwizzle && a.pipeBankXor == b.pipeBankXor && a.firstSlice == b.firstSlice && a.lastSlice == b.lastSlice && a.iterate256 == b.iterate256 && a.htilePipeAligned == b.htilePipeAligned && a.htileVrsEncoding == b.htileVrsEncoding && a.htileDepthOnly == b.htileDepthOnly)); } -std::mutex& surfacesMutex() { - static std::mutex mutex; +std::recursive_mutex& surfacesMutex() { + static std::recursive_mutex mutex; return mutex; } @@ -1180,13 +1214,6 @@ DepthSurface* planeSurface(VkDevice device, std::uint64_t address) { return found == list.rend() ? nullptr : found->get(); } -void retire(DepthSurface& surface) { - surface.retired = true; - for (auto* owners : {&depthOwners(), &stencilOwners()}) { - std::erase_if(*owners, [&](const auto& entry) { return entry.second.surface == &surface; }); - } -} - std::map& clearedHtiles() { static auto* cleared = new std::map(); return *cleared; @@ -1280,6 +1307,84 @@ VkImageAspectFlags storeBackClearedHtile(const Context& context, const DepthTarg htileReads()[target.htileAddress] = generation; return cleared; } + +struct StalePlanes { + std::mutex mutex; + std::vector> ranges; + std::atomic begin{std::numeric_limits::max()}; + std::atomic end{0}; +}; + +StalePlanes& stalePlanes() { + static auto* planes = new StalePlanes(); + return *planes; +} + +void publishStaleHull(StalePlanes& planes) { + auto begin = std::numeric_limits::max(); + std::uint64_t end = 0; + for (const auto& range : planes.ranges) { + begin = std::min(begin, range.first); + end = std::max(end, range.second); + } + planes.begin.store(begin, std::memory_order_release); + planes.end.store(end, std::memory_order_release); +} + +void noteStalePlane(std::uint64_t begin, std::uint64_t end) { + auto& planes = stalePlanes(); + std::lock_guard lock(planes.mutex); + if (std::find(planes.ranges.begin(), planes.ranges.end(), std::pair{begin, end}) != planes.ranges.end()) return; + planes.ranges.emplace_back(begin, end); + publishStaleHull(planes); +} + +bool overlapsStalePlane(std::uint64_t begin, std::uint64_t end) { + auto& planes = stalePlanes(); + if (begin >= planes.end.load(std::memory_order_acquire) || planes.begin.load(std::memory_order_acquire) >= end) return false; + std::lock_guard lock(planes.mutex); + return std::any_of(planes.ranges.begin(), planes.ranges.end(), [&](const auto& range) { return begin < range.second && range.first < end; }); +} + +bool mirrorable(const DepthTarget& target) { + return (target.format == VK_FORMAT_D32_SFLOAT || target.format == VK_FORMAT_D32_SFLOAT_S8_UINT) && target.samples == 1 && target.depthSwizzle == XorSwizzleMode(TextureTileMode::kZ64KBX); +} + +GuestTextureResource mirrorResource(const DepthTarget& target) { + GuestTextureResource resource{}; + resource.baseAddress = target.address; + resource.width = target.extent.width; + resource.height = target.extent.height; + resource.mipCount = 1; + resource.allocatedMipCount = 1; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 22; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + return resource; +} + +std::pair stalePlaneRange(const DepthSurface& surface) { + return {surface.target.address, surface.target.address + DescribeSurface(mirrorResource(surface.target)).guestBytes}; +} + +void retire(DepthSurface& surface) { + surface.retired = true; + for (auto* owners : {&depthOwners(), &stencilOwners()}) { + std::erase_if(*owners, [&](const auto& entry) { return entry.second.surface == &surface; }); + } + if (!surface.memoryStale) return; + surface.memoryStale = false; + const auto plane = stalePlaneRange(surface); + if (std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& other) { return other->memoryStale && stalePlaneRange(*other) == plane; })) return; + auto& planes = stalePlanes(); + std::lock_guard lock(planes.mutex); + std::erase(planes.ranges, plane); + publishStaleHull(planes); +} } std::uint64_t DepthSurfacesSerial() { @@ -1385,7 +1490,7 @@ float HostDepthBiasConstant(const Context& context, VkFormat format, float const return constant * 256.0f; } -VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { +VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, bool depthWritten) { const auto htileCleared = RoutesToNativeDepthOwner(target) ? VkImageAspectFlags{0} : storeBackClearedHtile(context, target); std::lock_guard gpu(GuestMemory::GpuMutex()); std::unique_lock lock(surfacesMutex()); @@ -1442,13 +1547,70 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) if (!created) surface->NoteHtileClear(VK_IMAGE_ASPECT_DEPTH_BIT); } if (!created) surface->NoteHtileClear(htileCleared); + if (mirrorable(target)) static_cast(surface->SyncFromMemory(mirrorResource(target))); surface->ApplyFastClear(); if (!created || inherits) surface->written = ++depthWrites; surface->NoteMemory(); + if ((depthWritten || cleared) && mirrorable(target)) { + surface->memoryStale = true; + const auto plane = target.address; + noteStalePlane(plane, plane + DescribeSurface(mirrorResource(target)).guestBytes); + } drawnDepth = surface; return surface->view; } +bool DepthPlanesStale() { + auto& planes = stalePlanes(); + return planes.begin.load(std::memory_order_acquire) < planes.end.load(std::memory_order_acquire); +} + +std::vector> FlushDepthPlanes(std::uint64_t address, std::size_t bytes) { + static thread_local bool flushing = false; + const auto end = bytes > std::numeric_limits::max() - address ? std::numeric_limits::max() : address + bytes; + if (flushing || !overlapsStalePlane(address, end)) return {}; + flushing = true; + struct Reset { + ~Reset() { flushing = false; } + } reset; + std::vector>> stored; + std::vector> ranges; + { + std::lock_guard gpu(GuestMemory::GpuMutex()); + std::lock_guard lock(surfacesMutex()); + std::vector> remaining; + for (const auto& surface : surfaces()) { + if (!surface->memoryStale) continue; + const auto resource = mirrorResource(surface->target); + const auto plane = surface->target.address; + const auto planeBytes = DescribeSurface(resource).guestBytes; + if (plane >= end || address >= plane + planeBytes) { + remaining.emplace_back(plane, plane + planeBytes); + continue; + } + surface->memoryStale = false; + if (surface->SyncFromMemory(resource)) continue; + stored.emplace_back(surface.get(), surface->StoreDepthPlane(resource)); + ranges.emplace_back(plane, planeBytes); + } + auto& planes = stalePlanes(); + std::lock_guard staleLock(planes.mutex); + planes.ranges = std::move(remaining); + publishStaleHull(planes); + } + for (const auto& entry : stored) entry.second->MarkDirty(); + for (const auto& [plane, planeBytes] : ranges) StorageTexture::FlushPending(plane, static_cast(planeBytes), nullptr, "depth plane store"); + const auto generation = GuestMemory::TrackerGeneration(); + std::lock_guard lock(surfacesMutex()); + for (const auto& entry : stored) { + if (std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& candidate) { return candidate.get() == entry.first; })) { + entry.first->syncedGeneration = generation; + entry.first->NoteMemory(); + } + } + return ranges; +} + std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads) { static constexpr std::array htileMaskKernel{0xd7460000u, 0x04010c06u, 0x34000084u, 0xdc388000u, 0x04020000u, 0xbf8c3f70u, 0xd7710008u, 0x00120805u, 0xd7710009u, 0x00120a05u, 0xd771000au, 0x00120c05u, 0xd771000bu, 0x00120e05u, 0xdc788000u, 0x00020800u, 0xbf810000u}; if (userData.size() < 6 || numThreads[0] != 64 || numThreads[1] != 1 || numThreads[2] != 1) return 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index ce12e8a950..7037c7a2a3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2067,6 +2067,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& } APS5_LOG_OUT_DEBUG("Creating color target %zu address=0x%llx bytes=%llu extent=%ux%u", index, static_cast(color.address), static_cast(color.bytes), color.extent.width, color.extent.height); const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes); + static_cast(FlushDepthPlanes(color.address, colorLayout.Bytes())); RetireDepthSurfaces(context.device, color.address, colorLayout.Bytes()); constexpr VkBufferUsageFlags copies = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; timer.phase(PhaseSetup); @@ -2138,7 +2139,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& binding.target = std::make_unique(context, color, state.blends.at(color.exportIndex).blendEnable != 0); targetViews.push_back(binding.target->View()); } - if (state.depth) targetViews.push_back(DepthSurfaceView(context, *state.depth)); + if (state.depth) targetViews.push_back(DepthSurfaceView(context, *state.depth, state.depthWrite)); timer.phase(PhasePrepare); const bool recordDraws = RecordDraws(); auto* recorder = Recorder::Active(); @@ -2693,7 +2694,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con } if (recipe.hasDepth) { timer.phase(PhaseSetup); - const auto depthView = DepthSurfaceView(context, *state.depth); + const auto depthView = DepthSurfaceView(context, *state.depth, state.depthWrite); timer.phase(PhaseReadTarget); if (recipe.targetViews.back() != depthView) return miss(DrawRecipeMiss::TargetGone); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 1b77cf8faf..617efdb2db 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "ThreadOwned.hpp" #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" @@ -4035,6 +4036,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { for (const auto& region : regions) CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); if (space != nullptr) completeSpaceWrites(); uploaded = true; + for (const auto& region : regions) static_cast(FlushDepthPlanes(region.begin, static_cast(region.end - region.begin))); for (const auto& [begin, end] : writes) StorageTexture::FlushPending(begin, static_cast(end - begin), nullptr, "written buffer range"); if (context.hostImportAlignment != 0) { auto& state = Imports(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 91beeafc12..35a5b01cac 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -646,6 +646,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span(guestBytes); + static_cast(FlushDepthPlanes(address, bytes)); // A storage image whose results for this surface (or for a mip chain containing it) are still on // the GPU supplies the texture by a view of it; anything else needs those results in guest // memory first. @@ -1003,7 +1004,11 @@ std::shared_ptr cachedStorageTexture(const Context& context, std Require(viewed.dimension != TextureDimension::k2DMsaa, "storage image access to a multisampled texture is not implemented"); RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a storage image access"); if (auto depth = DepthSurfaceStorage(context, words, viewed, mip)) return depth; - if (written) RetireDepthSurfaces(context.device, viewed.baseAddress, guestBytes != 0 ? guestBytes : DescribeSurface(viewed).guestBytes); + if (DepthPlanesStale() || written) { + const auto viewedBytes = guestBytes != 0 ? guestBytes : DescribeSurface(viewed).guestBytes; + static_cast(FlushDepthPlanes(viewed.baseAddress, static_cast(viewedBytes))); + if (written) RetireDepthSurfaces(context.device, viewed.baseAddress, viewedBytes); + } static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; if (disabled) return StorageTexture::Create(context, *context.detiler, viewed, mip); static_cast(words); @@ -2782,6 +2787,7 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::span(surface.bytes); + if (!FlushDepthPlanes(address, bytes).empty()) return fail(FastFail::Changed); surface.collected = GuestMemory::CollectWrites(address, bytes); if (surface.collected == 0) return fail(FastFail::Collect); const auto* source = surface.source; @@ -2908,6 +2914,7 @@ bool ShaderResources::fastRevalidateEach() { } const auto address = surface.resource.baseAddress; const auto bytes = static_cast(surface.bytes); + if (!FlushDepthPlanes(address, bytes).empty()) return false; surface.collected = GuestMemory::CollectWrites(address, bytes); if (surface.collected == 0) return false; if (PendingStorageOverlaps(address, bytes, surface.source)) return false; @@ -3081,7 +3088,10 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR if (!reusable || shaders.empty()) return false; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - for (const auto& region : directRegions) CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); + for (const auto& region : directRegions) { + static_cast(FlushDepthPlanes(region.begin, static_cast(region.end - region.begin))); + CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); + } for (const auto& range : describedRanges) { CompletePendingCpuWrites(context, range.address, static_cast(range.bytes)); if (range.dccAddress != 0) CompletePendingCpuWrites(context, range.dccAddress, DccKeyBytes(range.bytes)); @@ -4330,7 +4340,7 @@ std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) // between the stages marks them uncompressed). const auto address = record.resource.baseAddress; const auto bytes = static_cast(record.guestBytes); - if (DepthSurfaceAt(address)) return nullptr; + if (DepthSurfaceAt(address) || !FlushDepthPlanes(address, bytes).empty()) return nullptr; if (GuestMemory::CollectWrites(address, bytes) == 0) return nullptr; if (PendingStorageOverlaps(address, bytes, record.source.get())) return nullptr; auto keys = record.keys; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 7dc464e7c4..e03e2b9454 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/SlimMutex.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" @@ -3506,6 +3507,7 @@ bool StorageTexture::pendingUnitInside(std::uint64_t address, std::size_t bytes) } bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except, const char* reason, PublishScope scope, bool* published) { + static_cast(FlushDepthPlanes(address, bytes)); if (published != nullptr) *published = false; // The images stay alive across the scan: their last owner may be a batch's kept list, which // another thread releases outside the GPU mutex once the batch completed. diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp index a5c5f41b01..e1d74d89e9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp @@ -41,6 +41,7 @@ struct Surface { VkFormat format; std::uint32_t slice = 0; std::uint8_t clearStencil = 0; + std::uint32_t swizzle = 0; }; struct DrawOptions { @@ -71,11 +72,11 @@ struct Shaders { std::uint32_t vertexPush = 0; }; -inline Shaders Compile(AgcDriver::VulkanDevice& device) { +inline Shaders Compile(AgcDriver::VulkanDevice& device, const void* vertices = Triangles().data()) { const auto target = device.Target(); Shaders shaders; std::vector vertexUserData(4, 0u); - const auto vertexBuffer = BufferDescriptor(Triangles().data(), 16u, static_cast(Triangles().size())); + const auto vertexBuffer = BufferDescriptor(vertices, 16u, static_cast(Triangles().size())); std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; ShaderRecompiler::RecompileRequest vertex{ @@ -121,6 +122,7 @@ inline std::uint32_t Draw(AgcDriver::VulkanDevice& device, const Shaders& compil state.hasColorTarget = true; state.depth = AgcDriver::Graphics::DepthTarget{surface.depth, surface.stencil, extent, surface.format, 1.0f, surface.clearStencil, surface.htile, surface.htileStencil}; state.depth->firstSlice = surface.slice; + state.depth->depthSwizzle = surface.swizzle; state.depthTest = options.depthTest; state.depthWrite = options.depthWrite; state.depthCompare = options.depthCompare; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneBufferMemory.cpp new file mode 100644 index 0000000000..d3189a27ba --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneBufferMemory.cpp @@ -0,0 +1,220 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using DepthFastClearHarness::Compile; +using DepthFastClearHarness::Covered; +using DepthFastClearHarness::Draw; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::HtileBytes; +using DepthFastClearHarness::Require; +using DepthFastClearHarness::Surface; +using DepthFastClearHarness::Width; + +constexpr std::size_t Block = 65536; +alignas(256) std::array Htile{}; +alignas(256) std::array ViewedHtile{}; +alignas(256) constexpr std::array LoadCode{ + 0x7e02020du, 0x7e04020cu, 0xf0001108u, 0x00010300u, 0xbf8c3f70u, 0x34080082u, 0xe0701000u, 0x80000304u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +constexpr std::uint32_t LoadThreads = 32; +constexpr std::size_t ColorBytes = static_cast(Width) * Height * 4u; + +void* AllocateWatched(std::size_t bytes) { +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the depth plane block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + return block; +} + +std::array LoadRow(AgcDriver::VulkanDevice& device, std::uint64_t address, std::uint32_t width, std::uint32_t height, std::uint32_t swizzle) { + alignas(256) static std::array output{}; + std::vector userData(16, 0u); + const auto out = reinterpret_cast(output.data()); + const std::array buffer{static_cast(out), static_cast((out >> 32u) & 0xffffu), static_cast(sizeof(output)), 0x31016facu}; + const std::array texture{static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (22u << 20u) | (((width - 1u) & 3u) << 30u), ((width - 1u) >> 2u) | ((height - 1u) << 14u), 0xfacu | (swizzle << 20u) | (9u << 28u), 0u, 0u, 0u, 0u}; + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::span code(LoadCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{LoadThreads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::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; + output.fill(0xdeadbeefu); + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + return output; +} + +bool RowHolds(const std::array& row, float depth) { + return std::all_of(row.begin(), row.end(), [&](std::uint32_t bits) { return bits == std::bit_cast(depth); }); +} + +std::size_t Holding(std::span plane, float depth) { + return static_cast(std::count(plane.begin(), plane.end(), depth)); +} + +void DrawBlendKeepingMemory(AgcDriver::VulkanDevice& device, const DepthFastClearHarness::Shaders& compiled, std::uint64_t address) { + const std::array shaders{{ + {ShaderRecompiler::ShaderStage::Vertex, &compiled.vertex, 0}, + {ShaderRecompiler::ShaderStage::Fragment, &compiled.pixel, PixelPushOffset(compiled.vertexPush, device.Target())} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {address, {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, ColorBytes, 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0.0f, 1.0f}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend = {VK_TRUE, VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, 15}; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(DepthFastClearHarness::Triangles().size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto planeBytes = static_cast(AgcDriver::Graphics::DepthSliceBytes({Width, Height}, 4u)); + Require(planeBytes <= Block, "the depth plane does not fit one block"); + auto* block = static_cast(AllocateWatched(Block)); + const std::span plane(block, planeBytes / sizeof(float)); + const auto address = reinterpret_cast(block); + const auto shaders = Compile(*device); + const auto reader = Compile(*device, block); + const Surface depth{address, 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, AgcDriver::Graphics::XorSwizzleMode(AgcDriver::Graphics::TextureTileMode::kZ64KBX)}; + + Require(Draw(*device, shaders, depth, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 did not cover the target"); + Draw(*device, reader, depth, {.depthTest = false}); + Require(Holding(plane, 0.5f) == static_cast(Width) * Height, "a vertex fetch from the depth plane did not see the depth 0.5 a draw wrote into it"); + + if (!AgcDriver::GuestMemory::WriteWatched() || !AgcDriver::GuestMemory::Watched(address, Block)) { + std::puts("guest memory is not write-watched here: a buffer write into the depth plane is not exercised"); + std::puts("depth plane buffer memory tests passed"); + return 0; + } + std::fill(plane.begin(), plane.end(), 0.75f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(Draw(*device, shaders, depth, {}) == Covered, "a draw at depth 0.5 failed LESS against the 0.75 written into the depth plane's memory"); + Require(Draw(*device, shaders, depth, {.depthWrite = true}) == Covered, "a draw at depth 0.5 writing depth failed LESS against 0.75"); + Draw(*device, reader, depth, {.depthTest = false}); + Require(Holding(plane, 0.5f) == static_cast(Width) * Height, "a vertex fetch from the depth plane did not see the depth 0.5 written after the plane was reloaded"); + + auto* fresh = static_cast(AllocateWatched(Block)); + const std::span freshPlane(fresh, planeBytes / sizeof(float)); + const auto freshReader = Compile(*device, fresh); + const Surface unstored{reinterpret_cast(fresh), 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + Require(Draw(*device, shaders, unstored, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 into a new plane did not cover the target"); + std::fill(freshPlane.begin(), freshPlane.end(), 0.75f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Draw(*device, freshReader, depth, {.depthTest = false}); + Require(Holding(freshPlane, 0.75f) == freshPlane.size(), "the depth 0.5 a draw wrote before a guest write into the plane was stored over that write"); + Require(Draw(*device, shaders, unstored, {}) == Covered, "a draw at depth 0.5 failed LESS against the 0.75 written over depth the surface had not stored"); + std::fill(freshPlane.begin(), freshPlane.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(Draw(*device, shaders, unstored, {}) == 0u, "a draw at depth 0.5 passed LESS against the 0.25 written into the plane before it"); + Require(Draw(*device, shaders, unstored, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 over the reloaded plane did not cover the target"); + Draw(*device, freshReader, depth, {.depthTest = false}); + Require(Holding(freshPlane, 0.5f) == static_cast(Width) * Height, "the depth 0.5 a draw wrote after a guest write into the plane did not reach memory"); + + Htile.fill(0xffffffffu); + auto* cleared = static_cast(AllocateWatched(Block)); + const std::span clearedPlane(cleared, planeBytes / sizeof(float)); + const auto clearedReader = Compile(*device, cleared); + const Surface compressed{reinterpret_cast(cleared), 0, reinterpret_cast(Htile.data()), false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + Require(Draw(*device, shaders, compressed, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 into a plane with HTILE did not cover the target"); + Draw(*device, clearedReader, depth, {.depthTest = false}); + Require(Holding(clearedPlane, 0.5f) == static_cast(Width) * Height, "a vertex fetch from a plane with HTILE did not see the depth 0.5 a draw wrote into it"); + std::fill(clearedPlane.begin(), clearedPlane.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + AgcDriver::Graphics::NoteDepthMetadataFill(compressed.htile, HtileBytes, 0u); + Require(Draw(*device, shaders, compressed, {}) == Covered, "a guest write into the depth plane won over a later HTILE fast clear of it"); + + auto* viewed = static_cast(AllocateWatched(Block)); + const std::span viewedBlock(viewed, Block / sizeof(float)); + std::fill(viewedBlock.begin(), viewedBlock.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + ViewedHtile.fill(0xffffffffu); + const Surface sampled{reinterpret_cast(viewed), 0, reinterpret_cast(ViewedHtile.data()), false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + const auto viewedReader = Compile(*device, viewed); + constexpr std::size_t ViewOffset = 0x100; + const auto view = sampled.depth + ViewOffset; + const auto matchesMemory = [&](const std::array& row) { + Draw(*device, viewedReader, depth, {.depthTest = false}); + return std::memcmp(row.data(), reinterpret_cast(viewed) + ViewOffset, sizeof(row)) == 0; + }; + Require(Draw(*device, shaders, sampled, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 into a viewed plane did not cover the target"); + Require(matchesMemory(LoadRow(*device, view, Width, Height / 2u, 0u)), "a linear texture at an offset into a depth plane did not read the depth drawn into it"); + AgcDriver::Graphics::NoteDepthMetadataFill(sampled.htile, HtileBytes, 0u); + Draw(*device, shaders, sampled, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_NEVER}); + Require(matchesMemory(LoadRow(*device, view, Width, Height / 2u, 0u)), "a cached linear texture at an offset into a depth plane did not read the depth cleared after it was cached"); + Require(Draw(*device, shaders, sampled, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 over the cleared viewed plane did not cover the target"); + Require(matchesMemory(LoadRow(*device, view, Width, Height / 2u, 0u)), "a linear texture at an offset into a depth plane, viewed through its cached storage image, did not read the depth drawn after the image was made"); + + auto* blended = static_cast(AllocateWatched(Block)); + const std::span blendedBlock(blended, Block / sizeof(float)); + std::fill(blendedBlock.begin(), blendedBlock.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const Surface underBlend{reinterpret_cast(blended), 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + Require(Draw(*device, shaders, underBlend, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 under a later blended color target did not cover the target"); + DrawBlendKeepingMemory(*device, shaders, underBlend.depth); + Draw(*device, Compile(*device, blended), depth, {.depthTest = false}); + Require(Holding(blendedBlock, 0.5f) == static_cast(Width) * Height, "a color target blending over a depth plane did not load the depth drawn into the plane"); + std::puts("depth plane buffer memory tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 469ff39817..f09ab3047e 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -59,7 +59,8 @@ - [32-bit depth-bits reads of guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a sampled 32_UINT or 32_SINT T# with a depth swizzle mode that no live depth surface matches reads the guest bytes through an R32_SFLOAT image, as the recompiler's depth-bits read expects, and the shader casts the fetched value back to the stored dword. On RX 6700 XT (gfx1031, AMD Windows driver 32.0.21045.5002) the read keeps float denormals, the sign bit alone and quiet and signaling NaN payloads bit for bit. A host that canonicalizes NaNs or flushes denormals on a 32-bit float fetch would change those dwords, and other drivers are not measured. A live depth surface whose extent or base level differs from the T# is not used; the read takes the guest bytes, as the 32-bit float and 16-bit views already do. - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A dispatch of the 15-word 8x8x1 image clear kernel ([`MatchStorageImageClear`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp): one colour read from the V# in `user[8..11]` stored to every texel of the T# in `user[0..7]`) that covers the whole plane and stores 0 or 1 through a 16-bit unorm view, 0 or 0xffff through a 16-bit uint view, or 0 or a normal float in (0, 1] through a 32-bit float or integer view leaves out the copy-in and replaces the copy-out by a clear of the depth image to that value; the dispatch still writes the storage image. The colour is read again when the copy-out is replaced, and a changed value keeps the copy-out. Indirect dispatches, partial last groups, dispatches that queue on the draw pipeline, other values and formats, a plane the kernel covers only partly and stencil planes keep both copies. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. -- A [depth surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is retired when a color target or a written storage image it does not serve overlaps it, or when it is sampled through a view it cannot serve and guest memory over it was written since it was last bound; lookups then skip it, so sampling reads the memory like any texture. Bound again, it keeps its own depth instead of the bytes written over it. Without write watching, a view it cannot serve still throws. A retired surface also gives up its depth and stencil planes, so another surface sharing them no longer copies from it. +- A [depth surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is retired when a color target or a written storage image it does not serve overlaps it, or when it is sampled through a view it cannot serve and guest memory over it was written since it was last bound; lookups then skip it, so sampling reads the memory like any texture. Bound again, it keeps its own depth instead of the bytes written over it, except for the depth planes the next entry keeps in guest memory. Without write watching, a view it cannot serve still throws. A retired surface also gives up its depth and stencil planes, so another surface sharing them no longer copies from it. +- [Depth planes in guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): only a single-sample `D32_SFLOAT` or `D32_SFLOAT_S8_UINT` depth plane with the 64 KiB Z swizzle (`DB_Z_INFO.SW_MODE` 24) is kept coherent with buffer access to its memory. After a bind that writes or clears its depth, the plane is copied through a storage image in that swizzle into guest memory before a buffer upload, a dispatch's `GuestBuffers` descriptor, a reused build's in-place buffer regions, a sampled or storage image lookup over the range (an image a depth surface does not serve itself), the write-stamp proofs that let a cached image or a reused build skip that lookup, or a pending-store flush reads the range (`FlushDepthPlanes`); a CPU read of the plane is not caught, and neither is a read through the registered heaps an address-based shader maps (only the ranges its descriptors bind are flushed, as for pending storage images). Every bind of such a surface, and every flush of its stale plane, collects the plane's writes; finding it written by someone else since the surface's previous bind, store or reload, it reloads the depth from memory instead of storing, unless an HTILE fast clear of the depth is pending: a tile HTILE marks cleared reads the clear value whatever the plane's bytes hold, so the clear wins over the write. A guest write is so ordered by the bind or flush that collects it: it replaces all depth drawn before, unstored depth included, and depth drawn from a later bind is stored over it. On the console both are memory writes that land per byte in submission order; here the tracker stamps 64 KiB blocks and a store or reload covers the whole plane, so a guest write over part of a plane whose depth was not stored also drops that depth outside the written bytes, and a CPU write is ordered by when it is collected (a bind, a flush, or the next collect epoch), not when it was made. Without write watching nothing is reloaded and the depth is always stored. A plane stays stale until a flush over its range stores it, so a depth target drawn every frame and never read through a buffer stays stale; flushes compare their range with the stale planes' ranges (an atomic hull, then the list under its own lock) and take the GPU and surface locks only on an overlap. A store reached without an active recorder (a CPU-side flush) submits its copies and waits for them. D16 planes, other swizzles, stencil planes and multisampled surfaces keep their depth only in the host image. The guest bytes are the detiler's 64 KiB Z swizzle of the uncompressed depth values. On the console a tile that HTILE marks compressed holds compressed data in memory until a DB decompress writes it uncompressed (HTILE definitions in Mesa's `src/amd/common/ac_descriptors.h`), so a title reading a compressed plane without a decompress sees other bytes there. - [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. A 2D-array T# whose `BASE_ARRAY` is past slice 0 reads slices `BASE_ARRAY` to `LAST_ARRAY` of the copy, which holds slices 0 to `LAST_ARRAY`, and a 2D binding of it reads slice `BASE_ARRAY`; that follows the colour 2D-array semantics and the titles that hit it (PPSA01556, PPSA03912), and was not measured on hardware for depth surfaces. Planes with mips, a cube or a 2D T# with a base slice other than 0, a `BASE_ARRAY` past `LAST_ARRAY`, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). - [Repacked format-43 copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a sampled texture of format 43 (10-bit R, 11-bit G and B unsigned floats from bit 0) is uploaded into a `B10G11R11_UFLOAT` image with its R and B fields swapped (`RepackTextureWord`) and viewed with R and B swapped back. A texture whose results a storage image still holds is copied from it again, whole, whenever the image changed (`RefreshDepthCopy`); a 3D one is flushed to guest memory and uploaded instead. Storage reads and writes of format 43 keep the packed `R32_UINT` path. - [SW_64KB_Z_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the texture equations and 64 KiB block geometry of `SW_64KB_Z_X`, whose addresses are checked against [AMD addrlib GFX10](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) in the texture tiling tests. Any 256-byte pipe/bank XOR below the block size is accepted, as for `SW_64KB_R_X` targets and Z_X textures; which of those bits the console honours, and the render results, are not measured. From 132e69d595be7ac465bcd1a21c573b0652823d30 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Mon, 19 Oct 2026 03:38:58 +0200 Subject: [PATCH 040/219] fix(agc): drop the depth plane images of older surfaces a new plane overlaps Each depth surface stores its plane through a storage image of its own, kept for the surface's life. A title that switches output resolution binds new depth targets over memory where the previous targets' planes were, and the old images stayed live there. Storage images over the same memory lend each other pending results, so a later store of the new plane did not reach guest memory and buffer reads kept seeing the old depth. A bind now drops the images and the pending store of older surfaces whose planes overlap its plane over another range. A same-range surface (another stencil or HTILE address) keeps its image. The scan runs only when a surface or plane image was created since the surface's last scan. AI-assisted: yes --- .../Graphics/src/DepthSurface.cpp | 28 +++++++++++++++++-- docs/dev/debt/libSceAgcDriver.md | 2 +- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index ba8687d686..84c5efb9bc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -32,6 +32,7 @@ namespace AgcDriver::Graphics { namespace { std::uint64_t depthWrites = 0; +std::uint64_t mirrorEpoch = 0; Recorder* RecorderFor(const Context& context) { auto* recorder = Recorder::Active(); @@ -656,7 +657,10 @@ public: std::shared_ptr StoreDepthPlane(const GuestTextureResource& resource) { ApplyFastClear(); - if (mirrorPlane.storage == nullptr) mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); + if (mirrorPlane.storage == nullptr) { + mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); + ++mirrorEpoch; + } copyPlane(mirrorPlane, mirrorPlane.storage, false, true, true); return mirrorPlane.storage; } @@ -669,7 +673,10 @@ public: return false; } memoryStale = false; - if (mirrorPlane.storage == nullptr) mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); + if (mirrorPlane.storage == nullptr) { + mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); + ++mirrorEpoch; + } else static_cast(mirrorPlane.storage->Refresh()); syncedGeneration = GuestMemory::TrackerGeneration(); copyPlane(mirrorPlane, mirrorPlane.storage, false, false, true); @@ -677,7 +684,12 @@ public: return true; } + void DropMirror() { + mirrorPlane = {}; + } + bool memoryStale = false; + std::uint64_t scannedEpoch = 0; std::uint64_t syncedGeneration = 0; const Context context; @@ -1525,6 +1537,7 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, releaseRetired(context); surfacesSerial.fetch_add(1, std::memory_order_release); surfaces().push_back(std::make_unique(context, target, std::move(sampling))); + ++mirrorEpoch; auto& fresh = *surfaces().back(); if (target.samples != 1u && fresh.sampleable && fresh.sampling.refusal.empty()) { std::unique_lock guardLock(depthGuardMutex()); @@ -1551,6 +1564,17 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, surface->ApplyFastClear(); if (!created || inherits) surface->written = ++depthWrites; surface->NoteMemory(); + if (mirrorable(target) && surface->scannedEpoch != mirrorEpoch) { + surface->scannedEpoch = mirrorEpoch; + const auto plane = stalePlaneRange(*surface); + for (const auto& other : surfaces()) { + if (other.get() == surface || other->context.device != context.device || other->written >= surface->written || !mirrorable(other->target)) continue; + const auto otherPlane = stalePlaneRange(*other); + if (otherPlane == plane || otherPlane.first >= plane.second || plane.first >= otherPlane.second) continue; + other->memoryStale = false; + other->DropMirror(); + } + } if ((depthWritten || cleared) && mirrorable(target)) { surface->memoryStale = true; const auto plane = target.address; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index f09ab3047e..504582889f 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -60,7 +60,7 @@ - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A dispatch of the 15-word 8x8x1 image clear kernel ([`MatchStorageImageClear`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp): one colour read from the V# in `user[8..11]` stored to every texel of the T# in `user[0..7]`) that covers the whole plane and stores 0 or 1 through a 16-bit unorm view, 0 or 0xffff through a 16-bit uint view, or 0 or a normal float in (0, 1] through a 32-bit float or integer view leaves out the copy-in and replaces the copy-out by a clear of the depth image to that value; the dispatch still writes the storage image. The colour is read again when the copy-out is replaced, and a changed value keeps the copy-out. Indirect dispatches, partial last groups, dispatches that queue on the draw pipeline, other values and formats, a plane the kernel covers only partly and stencil planes keep both copies. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. - A [depth surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is retired when a color target or a written storage image it does not serve overlaps it, or when it is sampled through a view it cannot serve and guest memory over it was written since it was last bound; lookups then skip it, so sampling reads the memory like any texture. Bound again, it keeps its own depth instead of the bytes written over it, except for the depth planes the next entry keeps in guest memory. Without write watching, a view it cannot serve still throws. A retired surface also gives up its depth and stencil planes, so another surface sharing them no longer copies from it. -- [Depth planes in guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): only a single-sample `D32_SFLOAT` or `D32_SFLOAT_S8_UINT` depth plane with the 64 KiB Z swizzle (`DB_Z_INFO.SW_MODE` 24) is kept coherent with buffer access to its memory. After a bind that writes or clears its depth, the plane is copied through a storage image in that swizzle into guest memory before a buffer upload, a dispatch's `GuestBuffers` descriptor, a reused build's in-place buffer regions, a sampled or storage image lookup over the range (an image a depth surface does not serve itself), the write-stamp proofs that let a cached image or a reused build skip that lookup, or a pending-store flush reads the range (`FlushDepthPlanes`); a CPU read of the plane is not caught, and neither is a read through the registered heaps an address-based shader maps (only the ranges its descriptors bind are flushed, as for pending storage images). Every bind of such a surface, and every flush of its stale plane, collects the plane's writes; finding it written by someone else since the surface's previous bind, store or reload, it reloads the depth from memory instead of storing, unless an HTILE fast clear of the depth is pending: a tile HTILE marks cleared reads the clear value whatever the plane's bytes hold, so the clear wins over the write. A guest write is so ordered by the bind or flush that collects it: it replaces all depth drawn before, unstored depth included, and depth drawn from a later bind is stored over it. On the console both are memory writes that land per byte in submission order; here the tracker stamps 64 KiB blocks and a store or reload covers the whole plane, so a guest write over part of a plane whose depth was not stored also drops that depth outside the written bytes, and a CPU write is ordered by when it is collected (a bind, a flush, or the next collect epoch), not when it was made. Without write watching nothing is reloaded and the depth is always stored. A plane stays stale until a flush over its range stores it, so a depth target drawn every frame and never read through a buffer stays stale; flushes compare their range with the stale planes' ranges (an atomic hull, then the list under its own lock) and take the GPU and surface locks only on an overlap. A store reached without an active recorder (a CPU-side flush) submits its copies and waits for them. D16 planes, other swizzles, stencil planes and multisampled surfaces keep their depth only in the host image. The guest bytes are the detiler's 64 KiB Z swizzle of the uncompressed depth values. On the console a tile that HTILE marks compressed holds compressed data in memory until a DB decompress writes it uncompressed (HTILE definitions in Mesa's `src/amd/common/ac_descriptors.h`), so a title reading a compressed plane without a decompress sees other bytes there. +- [Depth planes in guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): only a single-sample `D32_SFLOAT` or `D32_SFLOAT_S8_UINT` depth plane with the 64 KiB Z swizzle (`DB_Z_INFO.SW_MODE` 24) is kept coherent with buffer access to its memory. After a bind that writes or clears its depth, the plane is copied through a storage image in that swizzle into guest memory before a buffer upload, a dispatch's `GuestBuffers` descriptor, a reused build's in-place buffer regions, a sampled or storage image lookup over the range (an image a depth surface does not serve itself), the write-stamp proofs that let a cached image or a reused build skip that lookup, or a pending-store flush reads the range (`FlushDepthPlanes`); a CPU read of the plane is not caught, and neither is a read through the registered heaps an address-based shader maps (only the ranges its descriptors bind are flushed, as for pending storage images). Every bind of such a surface, and every flush of its stale plane, collects the plane's writes; finding it written by someone else since the surface's previous bind, store or reload, it reloads the depth from memory instead of storing, unless an HTILE fast clear of the depth is pending: a tile HTILE marks cleared reads the clear value whatever the plane's bytes hold, so the clear wins over the write. A guest write is so ordered by the bind or flush that collects it: it replaces all depth drawn before, unstored depth included, and depth drawn from a later bind is stored over it. On the console both are memory writes that land per byte in submission order; here the tracker stamps 64 KiB blocks and a store or reload covers the whole plane, so a guest write over part of a plane whose depth was not stored also drops that depth outside the written bytes, and a CPU write is ordered by when it is collected (a bind, a flush, or the next collect epoch), not when it was made. Without write watching nothing is reloaded and the depth is always stored. A plane stays stale until a flush over its range stores it, so a depth target drawn every frame and never read through a buffer stays stale; flushes compare their range with the stale planes' ranges (an atomic hull, then the list under its own lock) and take the GPU and surface locks only on an overlap. A store reached without an active recorder (a CPU-side flush) submits its copies and waits for them. Each surface stores through a storage image of its own plane. A bind drops the images, and the pending store, of older surfaces whose planes overlap its plane over another range (another extent or base, such as the targets of another output resolution over the same memory), so they neither store nor lend their results over it; a same-range surface (another stencil or HTILE address) keeps its image. D16 planes, other swizzles, stencil planes and multisampled surfaces keep their depth only in the host image. The guest bytes are the detiler's 64 KiB Z swizzle of the uncompressed depth values. On the console a tile that HTILE marks compressed holds compressed data in memory until a DB decompress writes it uncompressed (HTILE definitions in Mesa's `src/amd/common/ac_descriptors.h`), so a title reading a compressed plane without a decompress sees other bytes there. - [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. A 2D-array T# whose `BASE_ARRAY` is past slice 0 reads slices `BASE_ARRAY` to `LAST_ARRAY` of the copy, which holds slices 0 to `LAST_ARRAY`, and a 2D binding of it reads slice `BASE_ARRAY`; that follows the colour 2D-array semantics and the titles that hit it (PPSA01556, PPSA03912), and was not measured on hardware for depth surfaces. Planes with mips, a cube or a 2D T# with a base slice other than 0, a `BASE_ARRAY` past `LAST_ARRAY`, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). - [Repacked format-43 copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a sampled texture of format 43 (10-bit R, 11-bit G and B unsigned floats from bit 0) is uploaded into a `B10G11R11_UFLOAT` image with its R and B fields swapped (`RepackTextureWord`) and viewed with R and B swapped back. A texture whose results a storage image still holds is copied from it again, whole, whenever the image changed (`RefreshDepthCopy`); a 3D one is flushed to guest memory and uploaded instead. Storage reads and writes of format 43 keep the packed `R32_UINT` path. - [SW_64KB_Z_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the texture equations and 64 KiB block geometry of `SW_64KB_Z_X`, whose addresses are checked against [AMD addrlib GFX10](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) in the texture tiling tests. Any 256-byte pipe/bank XOR below the block size is accepted, as for `SW_64KB_R_X` targets and Z_X textures; which of those bits the console honours, and the render results, are not measured. From 0500362ff289ad9e1d6c392ed0d1fe8d814a0bbe Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Sat, 17 Oct 2026 21:26:05 +0530 Subject: [PATCH 041/219] test(relinker): run guest_zero_initializer with the ASan host-loading option #7147 added add_asan_test_options so the CPU-side tests pass in an ANYPS5_ENABLE_ASAN build, and gave the relinker tests that load the instrumented guest_module_args_host.prx into Python verify_asan_link_order=0. guest_zero_initializer's Linux run loads the same host library with ctypes.CDLL in a Python subprocess, so under ASan it failed with "ASan runtime does not come first in initial library list". It now runs with the same option. Co-Authored-By: Claude Opus 5.5 --- core/relinker/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 50039c37aa..963a03a641 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -565,6 +565,7 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_zero_initializer.py $ $) set_tests_properties(guest_zero_initializer PROPERTIES TIMEOUT 30) + add_asan_test_options(guest_zero_initializer verify_asan_link_order=0) set(windows_stdio_host_arguments) if(WIN32) if(MINGW) From f77a9eaa086166d9417b2d31a39a80efd815c2a3 Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Mon, 19 Oct 2026 07:46:52 +0530 Subject: [PATCH 042/219] fix(relinker): give a Linux module with a DT_INIT of 0 its libkernel.prx #6106 made a Linux guest module with an initializer depend on libkernel.prx, which exports the module-argument getter and setter that its DT_INIT stub imports. That check still tested Init != 0, so a module whose DT_INIT is 0 got the stub but not the dependency. With no other libkernel.prx loaded, the executable then stopped with a symbol lookup error, and a --module-dir module failed to load with "undefined symbol: __aps5_get_pending_module_args_nid_no_patch". The check now uses HasInit, like the writers. guest_zero_initializer now checks that the Linux module lists libkernel.prx, and its native load provides one. Co-Authored-By: Claude Opus 5.5 --- core/relinker/relinker/src/guest/GuestModuleBuilder.cpp | 2 +- core/relinker/relinker/tests/test_guest_zero_initializer.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 9c59dbccd1..eb5d1a32fb 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -310,7 +310,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path addHost(dependency, windows || !deferred(index)); if (findGuest(dependency) == guestNames.end()) hosts.push_back(dependency); } - if (!windows && !macos && images[index].Init != 0) { + if (!windows && !macos && images[index].HasInit) { if (std::find(hosts.begin(), hosts.end(), "libkernel.prx") == hosts.end()) hosts.push_back("libkernel.prx"); if (!deferred(index) && uniqueHosts.insert("libkernel.prx").second) hostLibraries.push_back("libkernel.prx"); } diff --git a/core/relinker/relinker/tests/test_guest_zero_initializer.py b/core/relinker/relinker/tests/test_guest_zero_initializer.py index 6ee385e50b..7cc9a40ef7 100644 --- a/core/relinker/relinker/tests/test_guest_zero_initializer.py +++ b/core/relinker/relinker/tests/test_guest_zero_initializer.py @@ -9,6 +9,7 @@ import tempfile from guest_import_fixtures import executable from test_guest_intel_trampolines import main_fixture, pe_sections from test_guest_module_args import ARGP, ARGS, GETTER, RETURNED, SETTER, macho_binds +from test_guest_module_directories import needed_libraries from test_guest_module_dir import pe_export_names from test_guest_symbol_names import dynamic_tags, imported_symbols @@ -103,6 +104,7 @@ def main(): assert GETTER in imported_symbols(image), (preinit, imported_symbols(image)) assert SETTER in imported_symbols(image), (preinit, imported_symbols(image)) assert dynamic_tags(image).get(12, 0) != 0, (preinit, dynamic_tags(image)) + assert 'libkernel.prx' in needed_libraries(image), (preinit, needed_libraries(image)) _, mach_converted = convert(relinker, work / f'macos-{preinit}', preinit, ['--macos']) binds = macho_binds(mach_converted.read_bytes()) assert ('_' + GETTER).encode() in binds, f'the macOS module does not bind {GETTER}' @@ -128,6 +130,7 @@ def main(): libraries.mkdir() provider = libraries / 'libc.prx' shutil.copyfile(host, provider) + shutil.copyfile(host, libraries / 'libkernel.prx') events = case / 'events.txt' run = subprocess.run([sys.executable, __file__, '--load', str(provider), str(converted)], env={**os.environ, 'ANYPS5_GUEST_MODULE_ARGS': str(events)}, From 7fdb261e673d49cd87646fd8997a11c403350c7a Mon Sep 17 00:00:00 2001 From: Leonardo Date: Mon, 19 Oct 2026 05:12:36 +0200 Subject: [PATCH 043/219] feat(libSceVoiceQoS): implement packet and local endpoint attribute functions --- core/libs/prx/libSceVoiceQoS/Export.cpp | 39 +++++++++++++++++++------ core/libs/tests/GuestVoiceQoS.cpp | 26 +++++++++++++++++ docs/dev/debt/libSceVoiceQoS.md | 2 +- 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index d8dc8fe238..e834e9445e 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -68,6 +69,22 @@ int SetAttribute(std::map& endpoints, std::int32_t id, s return 0; } +int GetAttribute(std::map& endpoints, std::int32_t id, std::int32_t attribute, void* value, std::int32_t size) { + const auto endpoint = endpoints.find(id); + if (endpoint == endpoints.end() || !value || size <= 0) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + const auto attr = endpoint->second.find(attribute); + if (attr != endpoint->second.end() && !attr->second.empty()) { + const auto toCopy = std::min(static_cast(size), attr->second.size()); + std::memcpy(value, attr->second.data(), toCopy); + if (static_cast(size) > toCopy) { + std::memset(static_cast(value) + toCopy, 0, static_cast(size) - toCopy); + } + } else { + std::memset(value, 0, static_cast(size)); + } + return 0; +} + template int Locked(Body&& body) { auto& qos = State(); @@ -144,19 +161,23 @@ int APS5_VABI sceVoiceQoSSetRemoteEndpointAttribute(int32_t remoteId, int32_t at return Locked([&](QoS& qos) { return SetAttribute(qos.remotes, remoteId, attributeId, value, size); }); } -int APS5_VABI sceVoiceQoSReadPacket() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSReadPacket(int32_t connectionId, void* buffer, uint32_t* size) { + return Locked([&](QoS& qos) { + if (!buffer || !size || !qos.connections.contains(connectionId)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + *size = 0; + return 0; + }); } -int APS5_VABI sceVoiceQoSWritePacket() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSWritePacket(int32_t connectionId, const void* buffer, uint32_t* size) { + return Locked([&](QoS& qos) { + if (!buffer || !size || !qos.connections.contains(connectionId)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + return 0; + }); } -int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute(int32_t localId, int32_t attributeId, void* value, int32_t size) { + return Locked([&](QoS& qos) { return GetAttribute(qos.locals, localId, attributeId, value, size); }); } } diff --git a/core/libs/tests/GuestVoiceQoS.cpp b/core/libs/tests/GuestVoiceQoS.cpp index cfe4c4f3bb..ec467df0b7 100644 --- a/core/libs/tests/GuestVoiceQoS.cpp +++ b/core/libs/tests/GuestVoiceQoS.cpp @@ -14,6 +14,9 @@ int APS5_VABI sceVoiceQoSConnect(std::int32_t*, std::int32_t, std::int32_t); int APS5_VABI sceVoiceQoSDisconnect(std::int32_t); int APS5_VABI sceVoiceQoSSetLocalEndpointAttribute(std::int32_t, std::int32_t, const void*, std::int32_t); int APS5_VABI sceVoiceQoSSetRemoteEndpointAttribute(std::int32_t, std::int32_t, const void*, std::int32_t); +int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute(std::int32_t, std::int32_t, void*, std::int32_t); +int APS5_VABI sceVoiceQoSReadPacket(std::int32_t, void*, std::uint32_t*); +int APS5_VABI sceVoiceQoSWritePacket(std::int32_t, const void*, std::uint32_t*); } static void Require(bool value) { if (!value) std::abort(); } @@ -58,7 +61,14 @@ int main(int argc, char** argv) { Require(sceVoiceQoSDisconnect(otherConnection) == 0); const std::uint32_t level = 0x1234; + std::uint32_t readLevel = 0; + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, sizeof(readLevel)) == 0 && readLevel == 0); Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, sizeof(level)) == 0); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, sizeof(readLevel)) == 0 && readLevel == level); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, nullptr, sizeof(readLevel)) == argumentInvalid); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, 0) == argumentInvalid); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, -1) == argumentInvalid); + Require(sceVoiceQoSGetLocalEndpointAttribute(local + 100, 3, &readLevel, sizeof(readLevel)) == argumentInvalid); Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, nullptr, sizeof(level)) == argumentInvalid); Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, 0) == 0); Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, -1) == 0); @@ -70,6 +80,19 @@ int main(int argc, char** argv) { Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, -1) == 0); Require(sceVoiceQoSSetRemoteEndpointAttribute(remote + 100, 3, &level, sizeof(level)) == argumentInvalid); + std::vector packet(1024, 0); + std::uint32_t packetSize = static_cast(packet.size()); + Require(sceVoiceQoSReadPacket(connection, packet.data(), &packetSize) == 0 && packetSize == 0); + Require(sceVoiceQoSReadPacket(connection, nullptr, &packetSize) == argumentInvalid); + Require(sceVoiceQoSReadPacket(connection, packet.data(), nullptr) == argumentInvalid); + Require(sceVoiceQoSReadPacket(connection + 100, packet.data(), &packetSize) == argumentInvalid); + + packetSize = 100; + Require(sceVoiceQoSWritePacket(connection, packet.data(), &packetSize) == 0); + Require(sceVoiceQoSWritePacket(connection, nullptr, &packetSize) == argumentInvalid); + Require(sceVoiceQoSWritePacket(connection, packet.data(), nullptr) == argumentInvalid); + Require(sceVoiceQoSWritePacket(connection + 100, packet.data(), &packetSize) == argumentInvalid); + Require(sceVoiceQoSDisconnect(connection) == 0); Require(sceVoiceQoSDisconnect(connection) == argumentInvalid); Require(sceVoiceQoSDeleteLocalEndpoint(local) == 0); @@ -82,6 +105,9 @@ int main(int argc, char** argv) { Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == notInitialized); Require(sceVoiceQoSCreateLocalEndpoint(&local, 1, 0, 0) == notInitialized); Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, sizeof(level)) == notInitialized); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, sizeof(readLevel)) == notInitialized); + Require(sceVoiceQoSReadPacket(connection, packet.data(), &packetSize) == notInitialized); + Require(sceVoiceQoSWritePacket(connection, packet.data(), &packetSize) == notInitialized); Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == argumentInvalid); Require(sceVoiceQoSEnd() == 0); diff --git a/docs/dev/debt/libSceVoiceQoS.md b/docs/dev/debt/libSceVoiceQoS.md index a62a38f2d7..f5347d9010 100644 --- a/docs/dev/debt/libSceVoiceQoS.md +++ b/docs/dev/debt/libSceVoiceQoS.md @@ -3,7 +3,7 @@ ### Unknown function info - [sceVoiceQoSSetConnectionAttribute](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS, NID `KCGOxg8iX7s`) is a throwing stub declared without parameters, like the other endpoint calls of the library: PPSA04159 fails to load without it and its signature is unknown. The name hashes to the imported NID (`tools/nid_names.py`) -- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used. `sceVoiceQoSCreateLocalEndpoint` takes four arguments (`int* id, userId, deviceInId, deviceOutId`) as declared in fpPS4 (`src/ps4_libscevoice.pas`, `ps4_sceVoiceQoSCreateLocalEndpoint`); no public reference exists for the other seven signatures (`DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`, `SetRemoteEndpointAttribute(id, attribute, value, size)`), which are assumed from function naming and call patterns and not confirmed on a PS5 title. Endpoints and connections are tracked by allocated id; duplicate connections and deleting an endpoint while referenced in a connection are permitted as the console is not known to reject them; endpoint attributes are stored as given (including empty or non-positive sizes), as their ids and defaults are unknown; other endpoint and packet calls remain throwing stubs. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all seven implemented calls on its first load path, starting with `sceVoiceQoSCreateLocalEndpoint` when it registers the local talker; it stops with `not implemented` when any of them throws +- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used. `sceVoiceQoSCreateLocalEndpoint` takes four arguments (`int* id, userId, deviceInId, deviceOutId`) as declared in fpPS4 (`src/ps4_libscevoice.pas`, `ps4_sceVoiceQoSCreateLocalEndpoint`); signatures for `DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`, `SetRemoteEndpointAttribute(id, attribute, value, size)`, `GetLocalEndpointAttribute(id, attribute, value, size)`, `ReadPacket(connection, buffer, size)` and `WritePacket(connection, buffer, size)` were verified against call sites in Aliens: Fireteam Elite (PPSA03393, e.g. `0x76D132`, `0x76D16E`, `0x76CECB`). Endpoints and connections are tracked by allocated id; duplicate connections and deleting an endpoint while referenced in a connection are permitted as the console is not known to reject them; endpoint attributes are stored as given (including empty or non-positive sizes), as their ids and defaults are unknown; `ReadPacket` returns 0 with size 0 as network voice packet streaming is not implemented, and `WritePacket` returns 0 for valid connections. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all implemented calls on its load path; it stops with `not implemented` when any unimplemented call throws ### Functional From 6d95f1e3a055d1314a4af7cac4aa6bfa6ec8cdc1 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Mon, 19 Oct 2026 08:01:14 +0200 Subject: [PATCH 044/219] docs(libSceVoiceQoS): classify packet calls as silent stubs --- docs/dev/debt/libSceVoiceQoS.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceVoiceQoS.md b/docs/dev/debt/libSceVoiceQoS.md index f5347d9010..d4b32b59a3 100644 --- a/docs/dev/debt/libSceVoiceQoS.md +++ b/docs/dev/debt/libSceVoiceQoS.md @@ -1,9 +1,14 @@ # libSceVoiceQoS technical debt +### Silent stubs + +- [sceVoiceQoSReadPacket](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - network voice packet streaming is not implemented: it returns 0 and writes size 0 (indicating no packet is available). Aliens: Fireteam Elite (PPSA03393) calls it on its startup path (`0x76D14C`-`0x76D16E`) and stops with `not implemented` if it throws +- [sceVoiceQoSWritePacket](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - network voice packet streaming is not implemented: it returns 0 leaving `*size` untouched (treating all input bytes as accepted). Aliens: Fireteam Elite (PPSA03393) sets `*size` to the packet size before `WritePacket` (`0x76CEB9`-`0x76CEC4`), compares `*size` against that input size upon return (`0x76CF07`-`0x76CF10`) to verify all bytes were accepted, and stops with `not implemented` if it throws + ### Unknown function info - [sceVoiceQoSSetConnectionAttribute](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS, NID `KCGOxg8iX7s`) is a throwing stub declared without parameters, like the other endpoint calls of the library: PPSA04159 fails to load without it and its signature is unknown. The name hashes to the imported NID (`tools/nid_names.py`) -- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used. `sceVoiceQoSCreateLocalEndpoint` takes four arguments (`int* id, userId, deviceInId, deviceOutId`) as declared in fpPS4 (`src/ps4_libscevoice.pas`, `ps4_sceVoiceQoSCreateLocalEndpoint`); signatures for `DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`, `SetRemoteEndpointAttribute(id, attribute, value, size)`, `GetLocalEndpointAttribute(id, attribute, value, size)`, `ReadPacket(connection, buffer, size)` and `WritePacket(connection, buffer, size)` were verified against call sites in Aliens: Fireteam Elite (PPSA03393, e.g. `0x76D132`, `0x76D16E`, `0x76CECB`). Endpoints and connections are tracked by allocated id; duplicate connections and deleting an endpoint while referenced in a connection are permitted as the console is not known to reject them; endpoint attributes are stored as given (including empty or non-positive sizes), as their ids and defaults are unknown; `ReadPacket` returns 0 with size 0 as network voice packet streaming is not implemented, and `WritePacket` returns 0 for valid connections. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all implemented calls on its load path; it stops with `not implemented` when any unimplemented call throws +- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used. `sceVoiceQoSCreateLocalEndpoint` takes four arguments (`int* id, userId, deviceInId, deviceOutId`) as declared in fpPS4 (`src/ps4_libscevoice.pas`, `ps4_sceVoiceQoSCreateLocalEndpoint`); signatures for `DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`, `SetRemoteEndpointAttribute(id, attribute, value, size)`, and `GetLocalEndpointAttribute(id, attribute, value, size)` were verified against call sites in Aliens: Fireteam Elite (PPSA03393, e.g. `0x76D132`, `0x76D16E`, `0x76CECB`). Endpoints and connections are tracked by allocated id; duplicate connections and deleting an endpoint while referenced in a connection are permitted as the console is not known to reject them; endpoint attributes are stored as given (including empty or non-positive sizes), as their ids and defaults are unknown. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all implemented calls on its load path; it stops with `not implemented` when any unimplemented call throws ### Functional From 92063916d2583a958a40fd814f38bd29bf4817a2 Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Sun, 18 Oct 2026 07:50:02 +0200 Subject: [PATCH 045/219] perf(agc): hand pending results over to an image whose DCC keys are a clear code A storage image refreshed over memory where other images still had results pending stored those results first, then uploaded. When the image's DCC keys are a clear code its upload is a GPU clear that reads none of that memory, so those stores were dead for it. Unreal's render graph does this whenever it hands transient memory to a new texture it fast-clears. In write-watched memory the results now stay pending. Anything that reads the memory before the image is written stores them as before. Once the image's own results cover their 64 KiB units they are dropped, as its write-back stores every byte of those units; an image written again since keeps its newer results, and a unit stored after the hand-over no longer counts as a CPU store against the image's write. The path is chosen from the keys read before any flush, and only when no flush is needed. APS5_NO_CLEAR_HAND_OVER=1 stores the results first as before. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 + .../Graphics/include/Texture.hpp | 8 + .../libSceAgcDriver/Graphics/src/Texture.cpp | 105 ++++++++- .../prx/libSceAgcDriver/tests/Recorder.cpp | 212 ++++++++++++++++++ 4 files changed, 327 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 69f134b802..4b882b57eb 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1250,6 +1250,12 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() add_test(NAME agc_storage_baseline_settled COMMAND agc_driver_recorder_tests --settled-baseline-only) + add_test(NAME agc_storage_clear_hand_over COMMAND agc_driver_recorder_tests --clear-hand-over-only) + set_tests_properties(agc_storage_clear_hand_over PROPERTIES SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_storage_clear_hand_over PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_storage_integer_clear COMMAND agc_driver_recorder_tests --integer-clear-only) set_tests_properties(agc_storage_integer_clear PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index b271f40672..c81f4ca6e2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -364,6 +364,9 @@ public: static std::uint64_t SinglePassMoves(); static std::uint64_t SlabRetiles(); static std::uint64_t RefreshesProved(); + static std::uint64_t ClearHandOvers(); + static std::uint64_t HandedOverSuperseded(); + static void SetClearHandOver(bool enabled); static std::uint64_t UntrackedCompares(); std::uint64_t GuestBytes() const; std::uint64_t BudgetBytes() const; @@ -503,6 +506,8 @@ private: // stays responsible for storing them until this image's own results supersede them // (markLayersPending). APS5_NO_ALIAS_BORROW=1 stores and re-reads as before. std::shared_ptr pendingAlias() const; + std::optional clearHandOver(); + void supersedeHandedOver(std::uint32_t first, std::uint32_t count); // The pending images FlushPending(address, bytes) would store (not the one a Refresh validates, // a pending unit inside the range), kept alive across the caller's use. static std::vector> overlappingPending(std::uint64_t address, std::size_t bytes); @@ -583,6 +588,8 @@ private: std::weak_ptr borrowedFrom; std::uint64_t borrowedVersion = 0; std::vector borrowedUnits; + std::vector, std::uint64_t>> handedOver; + std::vector handedUnits; // Units of this image were borrowed since it was last written: the borrower's proofs // (the resource cache's serial memo) hold only while this image's version does, so the next // write here bumps the pending serial although the image was pending already. @@ -608,6 +615,7 @@ private: // Results are on the GPU only (guarded by the pending-write registry lock). bool dirty = false; std::uint64_t version = 0; + std::uint64_t writes = 0; // `original` holds the guest bytes; false after a GPU-side clear, which never read them. std::vector layerOriginal; std::map keptAtUnwatch; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index daab4cad39..5e636f2626 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -88,7 +88,7 @@ thread_local const char* uploadReason = "first"; // (DiscardPendingInside) and units an alias's own results superseded (markLayersPending); relaxed, // reported only. std::atomic singlePassMoves{0}, slabRetiles{0}; -std::atomic partialUploads{0}, partialUploadBytes{0}, partialWriteBacks{0}, partialWriteBackBytes{0}, emptyWriteBacks{0}, coalescedWriteBacks{0}, unitsDropped{0}, unitsSuperseded{0}; +std::atomic partialUploads{0}, partialUploadBytes{0}, partialWriteBacks{0}, partialWriteBackBytes{0}, emptyWriteBacks{0}, coalescedWriteBacks{0}, unitsDropped{0}, unitsSuperseded{0}, clearHandOvers{0}, handedOverSuperseded{0}; // FlushPending listings skipped because no pending unit of the image lay inside the range, images // whose selected pending units were dropped because their memory is no longer registered (see // writeBackLayers), and pending units a DCC clear -> uncompressed key flip kept as the texels @@ -140,7 +140,7 @@ void reportStorageTraffic(StorageTraffic& traffic) { const auto take = [](std::atomic& counter) { return static_cast(counter.exchange(0, std::memory_order_relaxed)); }; const auto partialUploadCount = take(partialUploads), partialWriteBackCount = take(partialWriteBacks); const auto uploadMiB = take(partialUploadBytes) / 1048576.0, writeBackMiB = take(partialWriteBackBytes) / 1048576.0; - AgcDriver::ProfilePrint_nid_no_patch( "[storage] uploads by path (count/MiB, 10 s):%s; %llu write-backs GPU-direct, %llu stored nothing; block units: partial uploads %llu/%.1f, partial write-backs %llu/%.1f, %llu widened to every pending unit, units dropped %llu, superseded %llu, dropped unregistered %llu, kept over a key flip %llu%s, pretest skipped %llu, evicted stale %llu\n", line.c_str(), static_cast(traffic.directWriteBacks), take(emptyWriteBacks), partialUploadCount, uploadMiB, partialWriteBackCount, writeBackMiB, take(coalescedWriteBacks), take(unitsDropped), take(unitsSuperseded), take(unregisteredDropped), take(keyFlipKept), ShadowReport().c_str(), take(pretestSkipped), take(staleEvicted)); + AgcDriver::ProfilePrint_nid_no_patch( "[storage] uploads by path (count/MiB, 10 s):%s; %llu write-backs GPU-direct, %llu stored nothing; block units: partial uploads %llu/%.1f, partial write-backs %llu/%.1f, %llu widened to every pending unit, units dropped %llu, superseded %llu, handed over at %llu clear-key refreshes (%llu units superseded), dropped unregistered %llu, kept over a key flip %llu%s, pretest skipped %llu, evicted stale %llu\n", line.c_str(), static_cast(traffic.directWriteBacks), take(emptyWriteBacks), partialUploadCount, uploadMiB, partialWriteBackCount, writeBackMiB, take(coalescedWriteBacks), take(unitsDropped), take(unitsSuperseded), take(clearHandOvers), take(handedOverSuperseded), take(unregisteredDropped), take(keyFlipKept), ShadowReport().c_str(), take(pretestSkipped), take(staleEvicted)); traffic.writeBacks.clear(); traffic.uploadReasons.clear(); traffic.directWriteBacks = 0; @@ -2049,7 +2049,8 @@ bool StorageTexture::Refresh() { } } // No publish: the upload below reads the stored units from the unit shadow itself. - const bool flushed = FlushPending(descriptor.baseAddress, static_cast(guestBytes), alias.get(), "storage refresh", PublishScope::None); + const auto handOverKeys = alias == nullptr ? clearHandOver() : std::nullopt; + const bool flushed = !handOverKeys.has_value() && FlushPending(descriptor.baseAddress, static_cast(guestBytes), alias.get(), "storage refresh", PublishScope::None); if (profile && flushed) start = LookupOutcomes::Add(LookupOutcomes::PendingFlush, start); // `original` holds the guest bytes the image was last uploaded from or written back as; while the // guest memory and the DCC keys still match, the image content is current. Pages nobody wrote @@ -2094,7 +2095,7 @@ bool StorageTexture::Refresh() { } }; const auto keysStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - keys = ProvedKeys(); + keys = handOverKeys.has_value() ? *handOverKeys : ProvedKeys(); if (profile && descriptor.dccAddress != 0) LookupOutcomes::Add(LookupOutcomes::DccScan, keysStart); if (keys != uploadedKeys) { changed.assign(trackedLayers, true); @@ -3534,6 +3535,7 @@ void StorageTexture::refreshGeneration() { void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) { CaptureTrace::Log("image-write image=%llx first=%u count=%u generation=%llu", static_cast(descriptor.baseAddress), first, count, static_cast(generation)); + ++writes; static const bool eager = std::getenv("APS5_EAGER_WRITEBACK") != nullptr || std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; std::vector unsaved(trackedLayers, false); bool anyUnsaved = false; @@ -3558,6 +3560,7 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) } if (superseded) source->reconcilePending(); } + if (!handedOver.empty() || !handedUnits.empty()) supersedeHandedOver(first, count); if (eager) { WriteBack(); return; @@ -4828,6 +4831,100 @@ void StorageTexture::blockGenerations(std::vector& generations) c } } +namespace { + +std::atomic& ClearHandOverOn() { + static std::atomic on{std::getenv("APS5_NO_CLEAR_HAND_OVER") == nullptr}; + return on; +} + +} + +void StorageTexture::SetClearHandOver(bool enabled) { + ClearHandOverOn().store(enabled, std::memory_order_relaxed); +} + +std::uint64_t StorageTexture::ClearHandOvers() { + return clearHandOvers.load(std::memory_order_relaxed); +} + +std::uint64_t StorageTexture::HandedOverSuperseded() { + return handedOverSuperseded.load(std::memory_order_relaxed); +} + +std::optional StorageTexture::clearHandOver() { + if (!ClearHandOverOn().load(std::memory_order_relaxed) || !blockUnits || descriptor.dccAddress == 0 || anyLayerPending()) return std::nullopt; + if (!GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) return std::nullopt; + const auto others = overlappingPending(descriptor.baseAddress, static_cast(guestBytes)); + if (others.empty()) return std::nullopt; + const auto begin = descriptor.baseAddress; + const auto end = descriptor.baseAddress + guestBytes; + const auto keyBytes = DccKeyBytes(guestBytes); + for (const auto& other : others) { + if (!other->blockUnits || (other->descriptor.dccAddress != 0 && other->uploadedKeys != DccKeys::Uncompressed)) return std::nullopt; + if (other->descriptor.dccAddress != 0 && other->descriptor.dccAddress < descriptor.dccAddress + keyBytes && descriptor.dccAddress < other->descriptor.dccAddress + DccKeyBytes(other->guestBytes)) return std::nullopt; + for (std::uint32_t unit = 0; unit < other->trackedLayers; ++unit) { + if (!other->layerPending[unit]) continue; + const auto from = other->layerBegin(unit); + const auto to = from + other->layerBytes(unit); + if (to <= begin || from >= end) continue; + if (from < begin || to > end || other->layerBytes(unit) != trackedLayerBytes) return std::nullopt; + } + } + const auto keys = ProvedKeys(); + VkClearColorValue clear{}; + if (!IsDccClear(keys) || !ClearColorFor(storageFormat, keys, clear)) return std::nullopt; + if (handedUnits.size() != trackedLayers) handedUnits.assign(trackedLayers, false); + for (const auto& other : others) { + for (std::uint32_t unit = 0; unit < other->trackedLayers; ++unit) { + const auto from = other->layerBegin(unit); + if (other->layerPending[unit] && from >= begin && from < end) handedUnits[static_cast((from - begin) / trackedLayerBytes)] = true; + } + const auto same = std::find_if(handedOver.begin(), handedOver.end(), [&](const auto& entry) { return entry.first.lock() == other; }); + if (same != handedOver.end()) same->second = other->writes; + else handedOver.emplace_back(other, other->writes); + } + clearHandOvers.fetch_add(1, std::memory_order_relaxed); + return keys; +} + +void StorageTexture::supersedeHandedOver(std::uint32_t first, std::uint32_t count) { + const auto begin = layerBegin(first); + const auto end = first + count >= trackedLayers ? descriptor.baseAddress + guestBytes : layerBegin(first + count); + for (auto entry = handedOver.begin(); entry != handedOver.end();) { + const auto source = entry->first.lock(); + if (source == nullptr || source->writes != entry->second) { + entry = handedOver.erase(entry); + continue; + } + bool superseded = false; + for (std::uint32_t unit = 0; unit < source->trackedLayers; ++unit) { + const auto from = source->layerBegin(unit); + if (!source->layerPending[unit] || from < begin || from + source->layerBytes(unit) > end) continue; + source->layerPending[unit] = false; + handedOverSuperseded.fetch_add(1, std::memory_order_relaxed); + superseded = true; + } + if (superseded) source->reconcilePending(); + ++entry; + } + if (first == 0 && count >= trackedLayers) handedOver.clear(); + bool moved = false; + for (std::uint32_t unit = first; unit < first + count && unit < handedUnits.size(); ++unit) { + if (!handedUnits[unit]) continue; + if (!moved) { + const auto current = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast(guestBytes)); + for (std::uint32_t moving = unit; moving < first + count && moving < handedUnits.size(); ++moving) { + if (handedUnits[moving]) layerGeneration[moving] = current; + } + moved = true; + } + handedUnits[unit] = false; + } + if (moved) refreshGeneration(); + if (std::none_of(handedUnits.begin(), handedUnits.end(), [](bool handed) { return handed; })) handedUnits.clear(); +} + std::shared_ptr StorageTexture::pendingAlias() const { static const bool disabled = std::getenv("APS5_NO_ALIAS_BORROW") != nullptr; if (disabled || !blockUnits) return nullptr; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index c6adf9272d..402e936437 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -8623,6 +8623,211 @@ void integerClearTests(const Device& device) { std::cout << "integer storage clears: " << tested << " formats ok\n"; } +void clearHandOverTests(const Device& device, Recorder& recorder, bool watched) { + const auto& base = device.GetContext(); + const std::string what = watched ? "clear-key hand-over (watched)" : "clear-key hand-over"; + if (base.hostImportAlignment == 0) { + std::cout << "host imports unavailable: " << what << " not tested\n"; + return; + } + constexpr std::size_t surfaceA = 256 * 256 * 4; + constexpr std::size_t surfaceB = 128 * 128 * 8; + constexpr std::size_t keyCount = surfaceB / 256; + constexpr std::size_t bytes = surfaceA + 65536; + const auto decided = PrepareImportWatch(base); +#ifndef _WIN32 + if (watched && decided == ImportWatch::Unwatch) { + std::cout << "host imports are compared on this device: " << what << " not tested\n"; + return; + } +#endif + void* block = nullptr; + if (watched) { + block = AllocateWatched(bytes, 65536); + if (block == nullptr) { + std::cout << "no write watching: " << what << " not tested\n"; + return; + } + } else { +#ifdef _WIN32 + block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + block = std::aligned_alloc(65536, bytes); +#endif + } + Require(block != nullptr && reinterpret_cast(block) % 65536 == 0, "cannot allocate the clear-key hand-over block"); + if (watched) SetImportWatch(base, ImportWatch::Watch); + struct RestoreWatch { + const Context& context; + ImportWatch decided; + ~RestoreWatch() { SetImportWatch(context, decided); } + } restoreWatch{base, decided}; + auto* memory = static_cast(block); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + bool watched; + ~Unregister() { + StorageTexture::SetClearHandOver(true); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + if (watched) ReleaseWatched(block, bytes); + } + } unregister{base, block, address, watched}; + if (HostImportFor(base, address, bytes) == nullptr) { + std::cout << "host import of the clear-key hand-over block refused: " << what << " not tested\n"; + return; + } + if (watched && !AgcDriver::GuestMemory::Watched(address, bytes)) { + std::cout << "the imported block left the write watch: " << what << " not tested\n"; + return; + } + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + const auto surface = [&](std::uint32_t width, std::uint32_t height, std::uint32_t format, std::uint64_t dcc) { + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = width; + resource.height = height; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = format; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + resource.dccAddress = dcc; + return resource; + }; + const auto resourceA = surface(256, 256, 56, 0); + const auto resourceB = surface(128, 128, 71, address + surfaceA); + Require(DescribeSurface(resourceA).guestBytes == surfaceA && DescribeSurface(resourceB).guestBytes == surfaceB, "the clear-key hand-over surfaces have unexpected sizes"); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const auto draw = [&](const std::shared_ptr& image, VkClearColorValue value) { + const auto commands = recorder.Commands(); + recorder.Keep(image); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + }; + const auto readImage = [&](const std::shared_ptr& image, std::uint32_t width, std::uint32_t height, std::size_t texelBytes) { + Buffer readback(context, static_cast(width) * height * texelBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {width, height, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, ©); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + const auto pixels = readback.Bytes(); + return std::vector(pixels.begin(), pixels.end()); + }; + const auto readMemory = [&](std::uint64_t from, std::size_t length) { + std::vector read(length); + AgcDriver::GuestMemory::Read(from, read); + recorder.Sync(); + if (!watched) std::memcpy(read.data(), memory + (from - address), length); + return read; + }; + const auto filled = [](std::span read, std::span texel) { + for (std::size_t i = 0; i < read.size(); ++i) { + if (std::to_integer(read[i]) != texel[i % texel.size()]) return false; + } + return true; + }; + constexpr std::array red{255, 0, 0, 255}; + constexpr std::array blue{0, 0, 255, 255}; + constexpr std::array green{0, 0, 0x00, 0x3c, 0, 0, 0x00, 0x3c}; + constexpr std::array cleared{}; + enum class Order { Superseded, ReadFirst, Rewritten }; + struct Outcome { + std::vector bytes; + std::vector imageB; + std::uint64_t handOvers = 0; + std::uint64_t superseded = 0; + }; + const auto run = [&](bool on, Order order) { + const std::string where = what + (order == Order::Superseded ? ", A never read again" : order == Order::ReadFirst ? ", A read before B wrote" : ", A written again after the hand-over") + (on ? ", on" : ", off"); + StorageTexture::SetClearHandOver(on); + recorder.Sync(); + std::memset(memory, 0x55, surfaceA); + std::memset(memory + surfaceA, 0x00, keyCount); + Outcome outcome{}; + const auto handOvers = StorageTexture::ClearHandOvers(); + const auto superseded = StorageTexture::HandedOverSuperseded(); + { + auto imageA = StorageTexture::Create(context, detiler, resourceA, 0); + auto imageB = StorageTexture::Create(context, detiler, resourceB, 0); + draw(imageA, {{1.0f, 0.0f, 0.0f, 1.0f}}); + imageB->Refresh(); + if (on && watched) { + Require(StorageTexture::ClearHandOvers() == handOvers + 1, where + ": B's refresh under clear keys did not hand A's results over"); + Require(StorageTexture::FindPending(address, surfaceA) == imageA, where + ": A's results did not stay pending"); + Require(std::all_of(memory, memory + 2 * 65536, [](std::uint8_t value) { return value == 0x55; }), where + ": B's refresh stored A's results over B"); + } else { + Require(StorageTexture::ClearHandOvers() == handOvers, where + ": the hand-over ran while off"); + } + outcome.imageB = readImage(imageB, 128, 128, 8); + Require(filled(outcome.imageB, cleared), where + ": B does not hold its keys' clear"); + if (order == Order::ReadFirst) { + Require(filled(readMemory(address, 65536), red), where + ": a read of A's memory before B wrote it did not see A's results"); + imageA->Refresh(); + Require(filled(readImage(imageA, 256, 256, 4), red), where + ": A read again as A lost its results"); + } + if (order == Order::Rewritten) { + draw(imageA, {{0.0f, 0.0f, 1.0f, 1.0f}}); + Require(filled(readMemory(address, 65536), blue), where + ": a read of A's memory did not see A's newer results"); + imageB->Refresh(); + Require(filled(readImage(imageB, 128, 128, 8), cleared), where + ": B refreshed again does not hold its keys' clear"); + } + draw(imageB, {{0.0f, 1.0f, 0.0f, 1.0f}}); + Require(StorageTexture::FlushPending(address, bytes, nullptr, "clear-key hand-over test", PublishScope::Whole), where + ": nothing was pending at the end"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + outcome.bytes = readMemory(address, surfaceA); + Require(filled(readImage(imageB, 128, 128, 8), green), where + ": B lost its results"); + } + outcome.handOvers = StorageTexture::ClearHandOvers() - handOvers; + outcome.superseded = StorageTexture::HandedOverSuperseded() - superseded; + recorder.Sync(); + return outcome; + }; + for (const auto order : {Order::Superseded, Order::ReadFirst, Order::Rewritten}) { + const std::string name = what + (order == Order::Superseded ? ", A never read again" : order == Order::ReadFirst ? ", A read before B wrote" : ", A written again after the hand-over"); + const auto off = run(false, order); + const auto on = run(true, order); + const auto& lastA = order == Order::Rewritten ? blue : red; + Require(filled(std::span(off.bytes).first(2 * 65536), green), name + ": B's results did not reach guest memory with the hand-over off"); + Require(filled(std::span(off.bytes).subspan(2 * 65536), lastA), name + ": A's results did not reach guest memory with the hand-over off"); + Require(on.imageB == off.imageB, name + ": B held other texels with the hand-over on than off"); + Require(on.bytes == off.bytes, name + ": the hand-over left other guest bytes than the old order"); + Require(off.handOvers == 0 && off.superseded == 0, name + ": the hand-over ran while off"); + if (!watched) { + Require(on.handOvers == 0, name + ": results were handed over in memory the write tracker does not watch"); + continue; + } + if (order == Order::Superseded) Require(on.superseded == 2, name + ": B's results did not drop A's two units under B (dropped " + std::to_string(on.superseded) + ")"); + if (order == Order::ReadFirst) Require(on.superseded <= 1, name + ": a unit stored by the read was dropped again"); + } + std::cout << what << ": ok\n"; +} + void denormalClearTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { @@ -9132,6 +9337,11 @@ int main(int argc, char** argv) { keptUnitSettleTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--clear-hand-over-only") { + clearHandOverTests(device, recorder, false); + clearHandOverTests(device, recorder, true); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--integer-clear-only") { integerClearTests(device); return 0; @@ -9347,6 +9557,8 @@ int main(int argc, char** argv) { movedMetadataTests(device, recorder); viewPastLastMipTests(device, recorder); keysFillTests(device, recorder); + clearHandOverTests(device, recorder, false); + clearHandOverTests(device, recorder, true); integerClearTests(device); denormalClearTests(device, recorder); unitShadowTests(device, recorder); From 26fecae5b48e2134f7999a0c15361911a3c19411 Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Sun, 18 Oct 2026 08:03:59 +0200 Subject: [PATCH 046/219] test(agc): state the clear-key hand-over fixture's layout --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 402e936437..0b8ce9a563 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -8634,6 +8634,7 @@ void clearHandOverTests(const Device& device, Recorder& recorder, bool watched) constexpr std::size_t surfaceB = 128 * 128 * 8; constexpr std::size_t keyCount = surfaceB / 256; constexpr std::size_t bytes = surfaceA + 65536; + static_assert(surfaceB <= surfaceA && keyCount <= bytes - surfaceA, "B lies over the first units of A, and B's DCC keys follow A inside the block"); const auto decided = PrepareImportWatch(base); #ifndef _WIN32 if (watched && decided == ImportWatch::Unwatch) { From b176274da11563c13a29d7e44ed2b41713d7ef24 Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Sun, 18 Oct 2026 08:42:47 +0200 Subject: [PATCH 047/219] perf(agc): keep a CPU store over a handed-over unit ahead of the receiving image's write --- .../libSceAgcDriver/Graphics/src/Texture.cpp | 26 ++++++++++++------- .../prx/libSceAgcDriver/tests/Recorder.cpp | 13 ++++++---- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 5e636f2626..61bebf1693 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2049,6 +2049,8 @@ bool StorageTexture::Refresh() { } } // No publish: the upload below reads the stored units from the unit shadow itself. + handedOver.clear(); + handedUnits.clear(); const auto handOverKeys = alias == nullptr ? clearHandOver() : std::nullopt; const bool flushed = !handOverKeys.has_value() && FlushPending(descriptor.baseAddress, static_cast(guestBytes), alias.get(), "storage refresh", PublishScope::None); if (profile && flushed) start = LookupOutcomes::Add(LookupOutcomes::PendingFlush, start); @@ -4909,19 +4911,23 @@ void StorageTexture::supersedeHandedOver(std::uint32_t first, std::uint32_t coun ++entry; } if (first == 0 && count >= trackedLayers) handedOver.clear(); - bool moved = false; - for (std::uint32_t unit = first; unit < first + count && unit < handedUnits.size(); ++unit) { - if (!handedUnits[unit]) continue; - if (!moved) { - const auto current = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast(guestBytes)); - for (std::uint32_t moving = unit; moving < first + count && moving < handedUnits.size(); ++moving) { - if (handedUnits[moving]) layerGeneration[moving] = current; - } + if (std::any_of(handedUnits.begin() + first, handedUnits.begin() + std::min(first + count, handedUnits.size()), [](bool handed) { return handed; })) { + const auto current = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast(guestBytes)); + std::vector generations; + blockGenerations(generations); + std::vector stamped(generations.size(), 0); + std::vector cpu(generations.size(), 0); + const bool tracked = GuestMemory::ChangedBlocks(descriptor.baseAddress, static_cast(guestBytes), generations, stamped, cpu); + bool moved = false; + for (std::uint32_t unit = first; unit < first + count && unit < handedUnits.size(); ++unit) { + if (!handedUnits[unit]) continue; + handedUnits[unit] = false; + if (!tracked || unit >= cpu.size() || cpu[unit] != 0) continue; + layerGeneration[unit] = current; moved = true; } - handedUnits[unit] = false; + if (moved) refreshGeneration(); } - if (moved) refreshGeneration(); if (std::none_of(handedUnits.begin(), handedUnits.end(), [](bool handed) { return handed; })) handedUnits.clear(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 0b8ce9a563..b17495c5ba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -8755,7 +8755,7 @@ void clearHandOverTests(const Device& device, Recorder& recorder, bool watched) constexpr std::array blue{0, 0, 255, 255}; constexpr std::array green{0, 0, 0x00, 0x3c, 0, 0, 0x00, 0x3c}; constexpr std::array cleared{}; - enum class Order { Superseded, ReadFirst, Rewritten }; + enum class Order { Superseded, ReadFirst, Rewritten, CpuStore }; struct Outcome { std::vector bytes; std::vector imageB; @@ -8763,7 +8763,7 @@ void clearHandOverTests(const Device& device, Recorder& recorder, bool watched) std::uint64_t superseded = 0; }; const auto run = [&](bool on, Order order) { - const std::string where = what + (order == Order::Superseded ? ", A never read again" : order == Order::ReadFirst ? ", A read before B wrote" : ", A written again after the hand-over") + (on ? ", on" : ", off"); + const std::string where = what + (order == Order::Superseded ? ", A never read again" : order == Order::ReadFirst ? ", A read before B wrote" : order == Order::Rewritten ? ", A written again after the hand-over" : ", a CPU store before B wrote") + (on ? ", on" : ", off"); StorageTexture::SetClearHandOver(on); recorder.Sync(); std::memset(memory, 0x55, surfaceA); @@ -8796,6 +8796,9 @@ void clearHandOverTests(const Device& device, Recorder& recorder, bool watched) imageB->Refresh(); Require(filled(readImage(imageB, 128, 128, 8), cleared), where + ": B refreshed again does not hold its keys' clear"); } + if (order == Order::CpuStore) { + std::memset(memory + 65536, 0x77, 65536); + } draw(imageB, {{0.0f, 1.0f, 0.0f, 1.0f}}); Require(StorageTexture::FlushPending(address, bytes, nullptr, "clear-key hand-over test", PublishScope::Whole), where + ": nothing was pending at the end"); recorder.Submit(); @@ -8809,12 +8812,12 @@ void clearHandOverTests(const Device& device, Recorder& recorder, bool watched) recorder.Sync(); return outcome; }; - for (const auto order : {Order::Superseded, Order::ReadFirst, Order::Rewritten}) { - const std::string name = what + (order == Order::Superseded ? ", A never read again" : order == Order::ReadFirst ? ", A read before B wrote" : ", A written again after the hand-over"); + for (const auto order : {Order::Superseded, Order::ReadFirst, Order::Rewritten, Order::CpuStore}) { + const std::string name = what + (order == Order::Superseded ? ", A never read again" : order == Order::ReadFirst ? ", A read before B wrote" : order == Order::Rewritten ? ", A written again after the hand-over" : ", a CPU store before B wrote"); const auto off = run(false, order); const auto on = run(true, order); const auto& lastA = order == Order::Rewritten ? blue : red; - Require(filled(std::span(off.bytes).first(2 * 65536), green), name + ": B's results did not reach guest memory with the hand-over off"); + if (order != Order::CpuStore) Require(filled(std::span(off.bytes).first(2 * 65536), green), name + ": B's results did not reach guest memory with the hand-over off"); Require(filled(std::span(off.bytes).subspan(2 * 65536), lastA), name + ": A's results did not reach guest memory with the hand-over off"); Require(on.imageB == off.imageB, name + ": B held other texels with the hand-over on than off"); Require(on.bytes == off.bytes, name + ": the hand-over left other guest bytes than the old order"); From 15466d7c53acb1b6c1f6ea49a31ab30db6b367c2 Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Sun, 18 Oct 2026 22:01:46 +0200 Subject: [PATCH 048/219] test(agc): skip the clear-key hand-over test when its watched case cannot run --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index b17495c5ba..eb8fdbce5e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -8623,12 +8623,12 @@ void integerClearTests(const Device& device) { std::cout << "integer storage clears: " << tested << " formats ok\n"; } -void clearHandOverTests(const Device& device, Recorder& recorder, bool watched) { +bool clearHandOverTests(const Device& device, Recorder& recorder, bool watched) { const auto& base = device.GetContext(); const std::string what = watched ? "clear-key hand-over (watched)" : "clear-key hand-over"; if (base.hostImportAlignment == 0) { std::cout << "host imports unavailable: " << what << " not tested\n"; - return; + return false; } constexpr std::size_t surfaceA = 256 * 256 * 4; constexpr std::size_t surfaceB = 128 * 128 * 8; @@ -8639,7 +8639,7 @@ void clearHandOverTests(const Device& device, Recorder& recorder, bool watched) #ifndef _WIN32 if (watched && decided == ImportWatch::Unwatch) { std::cout << "host imports are compared on this device: " << what << " not tested\n"; - return; + return false; } #endif void* block = nullptr; @@ -8647,7 +8647,7 @@ void clearHandOverTests(const Device& device, Recorder& recorder, bool watched) block = AllocateWatched(bytes, 65536); if (block == nullptr) { std::cout << "no write watching: " << what << " not tested\n"; - return; + return false; } } else { #ifdef _WIN32 @@ -8686,11 +8686,11 @@ void clearHandOverTests(const Device& device, Recorder& recorder, bool watched) } unregister{base, block, address, watched}; if (HostImportFor(base, address, bytes) == nullptr) { std::cout << "host import of the clear-key hand-over block refused: " << what << " not tested\n"; - return; + return false; } if (watched && !AgcDriver::GuestMemory::Watched(address, bytes)) { std::cout << "the imported block left the write watch: " << what << " not tested\n"; - return; + return false; } TextureDetiler detiler(base); auto context = base; @@ -8830,6 +8830,7 @@ void clearHandOverTests(const Device& device, Recorder& recorder, bool watched) if (order == Order::ReadFirst) Require(on.superseded <= 1, name + ": a unit stored by the read was dropped again"); } std::cout << what << ": ok\n"; + return true; } void denormalClearTests(const Device& device, Recorder& recorder) { @@ -9343,8 +9344,7 @@ int main(int argc, char** argv) { } if (argc == 2 && std::string_view(argv[1]) == "--clear-hand-over-only") { clearHandOverTests(device, recorder, false); - clearHandOverTests(device, recorder, true); - return 0; + return clearHandOverTests(device, recorder, true) ? 0 : 77; } if (argc == 2 && std::string_view(argv[1]) == "--integer-clear-only") { integerClearTests(device); From 4c8eb6d369204cb02bee9d96e5488016bb83f777 Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Sun, 18 Oct 2026 22:02:13 +0200 Subject: [PATCH 049/219] docs(env): list APS5_NO_CLEAR_HAND_OVER --- docs/dev/ENVIRONMENT.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index d241fa9f5f..f7406ee8b1 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -188,6 +188,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_BLOCK_TRACKING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | tracks array layers, or the whole surface, instead of 64 KiB blocks | | `APS5_NO_BUFFER_CLASSES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | matches exact sizes in the buffer pool instead of rounding up to size classes | | `APS5_NO_CACHED_PRECOLLECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | leaves the write-watch walks of a cached object's Revalidate under the GPU mutex | +| `APS5_NO_CLEAR_HAND_OVER` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | writes other images' pending results back at a clear-key storage refresh instead of handing them over to the refreshed image | | `APS5_NO_CLEARED_VIEW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | keeps fast-cleared surfaces as CPU snapshots instead of viewing the cleared storage image | | `APS5_NO_COLLECT_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | disables the per-thread memo of write-watch collects | | `APS5_NO_CONTEXT_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | rebuilds the Graphics::Context on every call instead of returning the cached one | From 278ea5833758e43a4223c419e0f952ceb320f947 Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Sun, 18 Oct 2026 23:00:49 +0200 Subject: [PATCH 050/219] test(agc): name the device limitation when the clear-key hand-over test skips --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index eb8fdbce5e..e75702d98b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -9344,7 +9344,11 @@ int main(int argc, char** argv) { } if (argc == 2 && std::string_view(argv[1]) == "--clear-hand-over-only") { clearHandOverTests(device, recorder, false); - return clearHandOverTests(device, recorder, true) ? 0 : 77; + if (!clearHandOverTests(device, recorder, true)) { + std::cout << "skipped, the device imports no watched host memory for the clear-key hand-over\n"; + return 77; + } + return 0; } if (argc == 2 && std::string_view(argv[1]) == "--integer-clear-only") { integerClearTests(device); From b687368facbd54c647a56760fa2dd17f89bb5619 Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Mon, 19 Oct 2026 08:55:20 +0200 Subject: [PATCH 051/219] docs(libc): order the macOS unmappable range by macOS version The range starts at 0x10_0000_0000 on macOS 26.5 and at 0xF_C000_0000 on macOS 27. The M2 Max on macOS 27.2 shows the same start as the M4 Pro on 27.0.1, so the line lists the macOS version first and the chips only as the machines it was measured on. --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 4aae478cfc..cdd1e4c19b 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -68,7 +68,7 @@ - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (file reads and socket receives); a host call that writes guest memory without it (`clock_gettime` and the like into a watched page) fails; - code that changes the protection of watched pages, or maps over them, has to call `GuestWriteWatchProtectionChanged`, and code that asks the kernel for a page's protection has to ask `GuestWriteWatchProtection` first, since a watched page is read-only between a collection and the next write. Nothing calls either yet: the macOS memory paths are not in the tree; - the handler is installed when libc loads and passes other faults to the handler installed before it. A handler installed later has to do the same. -- On macOS the [guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) finds host regions with `mach_vm_region` and reserves the rest with a fixed `mach_vm_map`, which fails instead of replacing, as macOS has no `/proc/self/maps` or `MAP_FIXED_NOREPLACE`. The kernel keeps 0x10_0000_0000-0x70_0000_0000 unmappable for every process (max protection none; macOS 26.5, M1 Pro, arm64 and x86_64 under Rosetta; on macOS 27.0.1, M4 Pro, both architectures, it starts at 0xF_C000_0000), and dyld and the Rosetta runtime sit near 0x2_0200_0000 and 0x3_0400_0000, so a guest mapping fixed inside those ranges fails where the console places it; mappings without a fixed address are placed around them. +- On macOS the [guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) finds host regions with `mach_vm_region` and reserves the rest with a fixed `mach_vm_map`, which fails instead of replacing, as macOS has no `/proc/self/maps` or `MAP_FIXED_NOREPLACE`. The kernel keeps the range up to 0x70_0000_0000 unmappable for every process (max protection none). It starts at 0x10_0000_0000 on macOS 26.5 (M1 Pro, arm64 and x86_64 under Rosetta) and at 0xF_C000_0000 on macOS 27 (27.0.1 on an M4 Pro, arm64 and x86_64 under Rosetta; 27.2 on an M2 Max, arm64). With dyld and the Rosetta runtime near 0x2_0200_0000 and 0x3_0400_0000, a guest mapping fixed inside those ranges fails where the console places it; mappings without a fixed address are placed around them. - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync` - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way - [`fopen` streams](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access. On Windows, opening a directory in a read mode fails with `EACCES`, where FreeBSD opens it; directory streams are not emulated From 61340e24afac4f7a43da374d5201c66d0b46a14c Mon Sep 17 00:00:00 2001 From: thibaut Date: Mon, 19 Oct 2026 07:20:08 +0200 Subject: [PATCH 052/219] fix(shader): round normalized storage writes from the exact product RDNA 2 (RX 6700 XT, native image_store) and Apple M2 store the level nearest to the exact product x * scale, not the nearest even level of its f32 rounding. The two differ when fl32(x * scale) lands exactly on a .5: the fma residual of the product now breaks that tie by its sign. The residual is decorated NoContraction, without it the Intel UHD 770 does not evaluate the fma exactly. The test expects the level nearest to the product computed in double, exact for these inputs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XBXM4pgkMHYEtqN1EQni2P --- .../execution/ImageStoreUnormRounding.cpp | 4 ++-- .../SpirvBackend/src/SpirvImageEmitter.cpp | 19 +++++++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreUnormRounding.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreUnormRounding.cpp index e8d05cc432..bb6c6f25cf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreUnormRounding.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreUnormRounding.cpp @@ -127,7 +127,7 @@ float Value(const Format& format, std::uint32_t tid, std::uint32_t index) { std::uint32_t Level(const Format& format, float value) { const auto lower = format.snorm ? -1.0f : 0.0f; const auto clamped = std::isnan(value) ? 0.0f : std::clamp(value, lower, 1.0f); - const auto product = clamped * Scale(format); + const auto product = static_cast(clamped) * static_cast(Scale(format)); const auto level = static_cast(std::nearbyint(product)); return static_cast(level) & ((1u << format.bits) - 1u); } @@ -237,7 +237,7 @@ void CheckTexels(const std::uint8_t* texels, const Format& format) { std::uint32_t wanted = 0; std::memcpy(&actual, texels + offset + component * format.bits / 8u, format.bits / 8u); std::memcpy(&wanted, expected.data() + offset + component * format.bits / 8u, format.bits / 8u); - Require(actual == wanted, std::string("image_store of ") + format.name + ": texel (" + std::to_string(x) + ", " + std::to_string(y) + ") component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(wanted) + " (round to nearest even)"); + Require(actual == wanted, std::string("image_store of ") + format.name + ": texel (" + std::to_string(x) + ", " + std::to_string(y) + ") component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(wanted) + " (level nearest to the exact product)"); } } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 64969cc151..1d6e81da10 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1024,8 +1024,23 @@ std::uint32_t QuantizeStoreTexel(SpirvEmitterState& state, std::uint32_t resourc state.module.AddFunction(spv::OpSelect, vec4, finite, Unary(state, spv::OpIsNan, bvec4, texel), zero, texel); const auto clamped = state.module.AllocateId(); state.module.AddFunction(spv::OpExtInst, vec4, clamped, GlslStd450(state), GLSLstd450FClamp, finite, lower, one); - const auto level = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, vec4, level, GlslStd450(state), GLSLstd450RoundEven, Binary(state, spv::OpFMul, vec4, clamped, scale)); + const auto product = Binary(state, spv::OpFMul, vec4, clamped, scale); + state.module.AddAnnotation(spv::OpDecorate, product, spv::DecorationNoContraction); + const auto ext = [&](GLSLstd450 op, const auto&... operands) { + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, vec4, result, GlslStd450(state), static_cast(op), operands...); + return result; + }; + const auto error = ext(GLSLstd450Fma, clamped, scale, Unary(state, spv::OpFNegate, vec4, product)); + state.module.AddAnnotation(spv::OpDecorate, error, spv::DecorationNoContraction); + const auto below = ext(GLSLstd450Floor, product); + const auto fraction = Binary(state, spv::OpFSub, vec4, product, below); + state.module.AddAnnotation(spv::OpDecorate, fraction, spv::DecorationNoContraction); + const auto tie = Binary(state, spv::OpFOrdEqual, bvec4, fraction, splat(vec4, ConstantF32(state, std::bit_cast(0.5f)))); + const auto above = Binary(state, spv::OpFAdd, vec4, below, one); + const auto even = ext(GLSLstd450RoundEven, product); + const auto broken = Select(state, vec4, Binary(state, spv::OpFOrdGreaterThan, bvec4, error, zero), above, Select(state, vec4, Binary(state, spv::OpFOrdLessThan, bvec4, error, zero), below, even)); + const auto level = Select(state, vec4, tie, broken, even); const auto quantized = Binary(state, spv::OpFMul, vec4, level, reciprocal); const auto result = state.module.AllocateId(); state.module.AddFunction(spv::OpSelect, vec4, result, splat(bvec4, enabled), quantized, texel); From 4a5ce8a094b9ccd671ebbc3ab65b65a8677faa12 Mon Sep 17 00:00:00 2001 From: babyhuey <7751607+babyhuey@users.noreply.github.com> Date: Mon, 19 Oct 2026 07:41:05 -0400 Subject: [PATCH 053/219] fix(libc): adopt the guest SDK allocator only when the title replaces the heap A bundled libc.prx can export the ten allocator NIDs while forwarding them to HLE, so adopting its exports for a title whose replacement table is empty sends HLE allocations back into themselves. Keep the default heap in that case, as before the guest SDK adoption. --- core/libs/prx/libc/CMakeLists.txt | 1 + core/libs/prx/libc/src/ApplicationHeap.cpp | 5 +++-- core/libs/tests/ApplicationHeap.cpp | 12 ++++++++++++ docs/dev/debt/libc.md | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index dba309d6a7..4f214f5ee3 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -751,6 +751,7 @@ if(BUILD_TESTING) add_dependencies(application_heap_tests guest_sdk_heap_fixture guest_sdk_heap_partial_fixture) add_test(NAME application_heap_guest_sdk COMMAND application_heap_tests sdk $) add_test(NAME application_heap_guest_sdk_partial COMMAND application_heap_tests sdk-partial $) + add_test(NAME application_heap_guest_sdk_default COMMAND application_heap_tests sdk-default $) endif() if(WIN32) set_tests_properties(application_heap_replacement application_heap_invalid_metadata application_heap_default application_heap_exit_order application_heap_scripting application_heap_lazy diff --git a/core/libs/prx/libc/src/ApplicationHeap.cpp b/core/libs/prx/libc/src/ApplicationHeap.cpp index 538e038d40..84a8f46eb7 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -156,11 +156,12 @@ void ApplicationHeapInitialize_nid_no_patch(const void* processParameters) { const auto* replacement = read(libcParameters, 0x30); if (read(replacement, 0) != 0x78 || read(replacement, 8) != 2) throw std::runtime_error("application heap: unsupported allocator replacement table"); std::array api; - if (ApplicationGuestHeapApi_nid_no_patch(api.data())) { + std::memcpy(api.data(), static_cast(replacement) + 0x20, sizeof(api)); + const bool replaced = std::any_of(api.begin(), api.end(), [](const void* entry) { return entry != nullptr; }); + if (replaced && ApplicationGuestHeapApi_nid_no_patch(api.data())) { ApplicationHeapRegister_nid_no_patch(api.data()); return; } - std::memcpy(api.data(), static_cast(replacement) + 0x20, sizeof(api)); ApplicationHeapRegister_nid_no_patch(api.data()); const auto initialize = read(replacement, 0x10); if (initialize != nullptr) initialize(); diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 16f537b48f..be99345d50 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -238,6 +238,18 @@ int main(int argc, char** argv) { require(frees == 3 && initializes == 1); return 0; } + if (argc > 1 && std::strcmp(argv[1], "sdk-default") == 0) { + require(argc == 3); + void* sdk = dlopen(argv[2], RTLD_NOW | RTLD_GLOBAL); + require(sdk != nullptr && initializes == 1); + std::memset(replacement.data() + 0x10, 0, replacement.size() - 0x10); + ApplicationHeapInitialize_nid_no_patch(process.data()); + void* pointer = ApplicationHeapAllocate_nid_no_patch(24); + require(pointer != nullptr && pointer != storage.data() && lastSize == 0); + ApplicationHeapFree_nid_no_patch(pointer); + require(frees == 0 && dlclose(sdk) == 0 && finalizes == 1); + return 0; + } #endif if (argc > 1 && std::strcmp(argv[1], "lazy") == 0) { installProcessParameters(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ff94841f67..5331f1a82f 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -3,7 +3,7 @@ ### Unknown function info - [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. Implemented only on Linux; other platforms throw -- [ApplicationGuestHeapApi](../../../core/libs/prx/libc/src/ApplicationProcessParameters.cpp) (libc) - on Linux, HLE application allocation uses a loaded guest SDK's ten allocator exports when they belong to one native module. Its CRT owns application allocator initialization and finalization; HLE retains the module until process exit. Partial or mixed providers throw. This avoids reinitializing an allocator already used by SDK startup; console behavior and other host platforms remain unverified. +- [ApplicationGuestHeapApi](../../../core/libs/prx/libc/src/ApplicationProcessParameters.cpp) (libc) - on Linux, HLE application allocation uses a loaded guest SDK's ten allocator exports when the title's replacement table has an allocator entry and the exports belong to one native module. With an empty table it keeps the default heap, since a bundled `libc.prx` may export the allocator NIDs while forwarding them to HLE. Its CRT owns application allocator initialization and finalization; HLE retains the module until process exit. Partial or mixed providers throw. This avoids reinitializing an allocator already used by SDK startup; console behavior and other host platforms remain unverified. - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is unverified. - [scriptingGetMem, scriptingFreeMem](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - runtime allocator hooks for Unity titles. The allocation signature `(alignment, size)`, minimum alignment 16 and null result for a non-power-of-two alignment follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libKernel.cpp); the free hook's one-pointer signature follows [Raeen's recorded contract](https://github.com/Whoisraeen/Raeen/blob/2db56923204b89719916b95e29c2f3c4e1d9f90b/docs/unity-dlsym-blocker.md). Both use libc's own guest heap, not the title's registered allocator. A title that ships its own `libc.prx` (Metal Gear Solid Master Collection, PPSA18194) has already initialized that allocator, and reaching it from here would run its initializer a second time. That title's IL2CPP runtime looks up only `scriptingGetMem`, by name from `Il2cppUserAssemblies.prx`, and asks it for 256 KiB blocks aligned to 4 KiB. Which console library exports the hooks is unknown, and there is no console measurement. Allocation failure and sizes that overflow return null. From c505661ea9d0a8cf4a1b21819bc4f0a6b48943b7 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Mon, 19 Oct 2026 04:37:06 +0200 Subject: [PATCH 054/219] fix(agc): store a depth plane over the guest writes made before the store A storage image's write-back keeps the 64 KiB blocks written since its layers were last in sync, since the memory may have been reused. A depth plane's image is overwritten whole by every store, but it kept the sync state of its creation or last reload. A plane bound behind an HTILE fast clear never reloads, so once guest memory over it had been written (another render target over the same memory), every later store kept those blocks and the drawn depth never reached guest memory. A store now marks the image as holding the whole plane, as a whole-image copy does (StorageTexture::MarkWholeWritten, split out of CopyFrom). agc_driver_depth_plane_buffer_memory gains the case: store a plane, write its memory, clear its HTILE and draw, then fetch the plane. It fails without the change (the fetch reads the guest write). AI-assisted: yes --- .../Graphics/include/Texture.hpp | 1 + .../Graphics/src/DepthSurface.cpp | 2 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 6 +++++- .../tests/execution/DepthPlaneBufferMemory.cpp | 18 ++++++++++++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index b271f40672..890c8ede57 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -197,6 +197,7 @@ public: void WriteBack(); // Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead). void MarkDirty(); + void MarkWholeWritten(); void MarkSliceDirty(std::uint32_t slice); void Flush(); bool Dirty() const; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 9ac59bffdb..7b4d48574b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -1723,7 +1723,7 @@ std::vector> FlushDepthPlanes(std::uint6 planes.ranges = std::move(remaining); publishStaleHull(planes); } - for (const auto& entry : stored) entry.second->MarkDirty(); + for (const auto& entry : stored) entry.second->MarkWholeWritten(); for (const auto& [plane, planeBytes] : ranges) StorageTexture::FlushPending(plane, static_cast(planeBytes), nullptr, "depth plane store"); const auto generation = GuestMemory::TrackerGeneration(); std::lock_guard lock(surfacesMutex()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 7b46543456..14ec76f87c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -4744,6 +4744,11 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { copied.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &copied); recorder->EndGpuTiming(timing, guestBytes); + MarkWholeWritten(); + return true; +} + +void StorageTexture::MarkWholeWritten() { // As after a clear: the image holds the surface, guest memory follows at the write-back (which // keeps the 64 KiB blocks the CPU writes from now on; the collect is uncached for the reason // given in FillClear). @@ -4758,7 +4763,6 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { refreshGeneration(); forgetBorrowed(0, trackedLayers); MarkDirty(); - return true; } // See AdjacentGenerationEnabled above. diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneBufferMemory.cpp index d3189a27ba..fcaadb3722 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneBufferMemory.cpp @@ -23,6 +23,7 @@ #include #include #include +#include namespace { @@ -211,6 +212,23 @@ int main() { DrawBlendKeepingMemory(*device, shaders, underBlend.depth); Draw(*device, Compile(*device, blended), depth, {.depthTest = false}); Require(Holding(blendedBlock, 0.5f) == static_cast(Width) * Height, "a color target blending over a depth plane did not load the depth drawn into the plane"); + Htile.fill(0xffffffffu); + auto* reused = static_cast(AllocateWatched(Block)); + const std::span reusedPlane(reused, planeBytes / sizeof(float)); + const auto reusedReader = Compile(*device, reused); + const Surface recleared{reinterpret_cast(reused), 0, reinterpret_cast(Htile.data()), false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + Require(Draw(*device, shaders, recleared, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 into a plane that is cleared each frame did not cover the target"); + Draw(*device, reusedReader, depth, {.depthTest = false}); + Require(Holding(reusedPlane, 0.5f) == static_cast(Width) * Height, "a vertex fetch did not see the depth 0.5 drawn into a plane that is cleared each frame"); + std::fill(reusedPlane.begin(), reusedPlane.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + for (int frame = 0; frame < 2; ++frame) { + const float value = 0.75f - 0.125f * static_cast(frame); + AgcDriver::Graphics::NoteDepthMetadataFill(recleared.htile, HtileBytes, 0u); + Require(Draw(*device, shaders, recleared, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS, .minDepth = value, .maxDepth = value}) == Covered, "the draw after the HTILE clear did not cover the target"); + Draw(*device, reusedReader, depth, {.depthTest = false}); + Require(Holding(reusedPlane, value) == static_cast(Width) * Height, "after a guest write into a stored depth plane, the depth drawn behind an HTILE clear did not reach memory (frame " + std::to_string(frame) + ")"); + } std::puts("depth plane buffer memory tests passed"); return 0; } catch (const std::exception& error) { From 3f0d75025ff0cfe67b6436336fbddb3f9c881cb3 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Mon, 19 Oct 2026 14:49:34 +0200 Subject: [PATCH 055/219] docs(agc): keep the depth planes debt entry under the length limit State the gap first and drop the mechanism details the code and the pull request already carry. AI-assisted: yes --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index c10a3d8f8f..f23de80505 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -61,7 +61,7 @@ - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A dispatch of the 15-word 8x8x1 image clear kernel ([`MatchStorageImageClear`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp): one colour read from the V# in `user[8..11]` stored to every texel of the T# in `user[0..7]`) that covers the whole plane and stores 0 or 1 through a 16-bit unorm view, 0 or 0xffff through a 16-bit uint view, or 0 or a normal float in (0, 1] through a 32-bit float or integer view leaves out the copy-in and replaces the copy-out by a clear of the depth image to that value; the dispatch still writes the storage image. The colour is read again when the copy-out is replaced, and a changed value keeps the copy-out. Indirect dispatches, partial last groups, dispatches that queue on the draw pipeline, other values and formats, a plane the kernel covers only partly and stencil planes keep both copies. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. - A [depth surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is retired when a color target or a written storage image it does not serve overlaps it, or when it is sampled through a view it cannot serve and guest memory over it was written since it was last bound; lookups then skip it, so sampling reads the memory like any texture. Bound again, it keeps its own depth instead of the bytes written over it, except for the depth planes the next entry keeps in guest memory. Without write watching, a view it cannot serve still throws. A retired surface also gives up its depth and stencil planes, so another surface sharing them no longer copies from it. -- [Depth planes in guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): only a single-sample `D32_SFLOAT` or `D32_SFLOAT_S8_UINT` depth plane with the 64 KiB Z swizzle (`DB_Z_INFO.SW_MODE` 24) is kept coherent with buffer access to its memory. After a bind that writes or clears its depth, the plane is copied through a storage image in that swizzle into guest memory before a buffer upload, a dispatch's `GuestBuffers` descriptor, a reused build's in-place buffer regions, a sampled or storage image lookup over the range (an image a depth surface does not serve itself), the write-stamp proofs that let a cached image or a reused build skip that lookup, or a pending-store flush reads the range (`FlushDepthPlanes`); a CPU read of the plane is not caught, and neither is a read through the registered heaps an address-based shader maps (only the ranges its descriptors bind are flushed, as for pending storage images). Every bind of such a surface, and every flush of its stale plane, collects the plane's writes; finding it written by someone else since the surface's previous bind, store or reload, it reloads the depth from memory instead of storing, unless an HTILE fast clear of the depth is pending: a tile HTILE marks cleared reads the clear value whatever the plane's bytes hold, so the clear wins over the write. A guest write is so ordered by the bind or flush that collects it: it replaces all depth drawn before, unstored depth included, and depth drawn from a later bind is stored over it. On the console both are memory writes that land per byte in submission order; here the tracker stamps 64 KiB blocks and a store or reload covers the whole plane, so a guest write over part of a plane whose depth was not stored also drops that depth outside the written bytes, and a CPU write is ordered by when it is collected (a bind, a flush, or the next collect epoch), not when it was made. Without write watching nothing is reloaded and the depth is always stored. A plane stays stale until a flush over its range stores it, so a depth target drawn every frame and never read through a buffer stays stale; flushes compare their range with the stale planes' ranges (an atomic hull, then the list under its own lock) and take the GPU and surface locks only on an overlap. A store reached without an active recorder (a CPU-side flush) submits its copies and waits for them. Each surface stores through a storage image of its own plane. A bind drops the images, and the pending store, of older surfaces whose planes overlap its plane over another range (another extent or base, such as the targets of another output resolution over the same memory), so they neither store nor lend their results over it; a same-range surface (another stencil or HTILE address) keeps its image. D16 planes, other swizzles, stencil planes and multisampled surfaces keep their depth only in the host image. The guest bytes are the detiler's 64 KiB Z swizzle of the uncompressed depth values. On the console a tile that HTILE marks compressed holds compressed data in memory until a DB decompress writes it uncompressed (HTILE definitions in Mesa's `src/amd/common/ac_descriptors.h`), so a title reading a compressed plane without a decompress sees other bytes there. +- [Depth planes in guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): only single-sample `D32_SFLOAT` and `D32_SFLOAT_S8_UINT` planes with the 64 KiB Z swizzle (`SW_MODE` 24) reach guest memory; D16, other swizzles, stencil and multisampled planes keep depth in the host image only. A plane drawn since its last store is stored, uncompressed, when a buffer upload, a `GuestBuffers` V#, an image lookup or a pending-store flush overlaps it; CPU reads and reads through address-based heaps are not caught. Guest writes are ordered when collected (bind, flush, collect epoch), not when made, at 64 KiB granularity: a collected write reloads the whole plane unless an HTILE clear is pending. A store outside a recorder waits for the GPU. The console keeps HTILE-compressed tiles compressed in memory until a decompress (Mesa `ac_descriptors.h`). - [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. A 2D-array T# whose `BASE_ARRAY` is past slice 0 reads slices `BASE_ARRAY` to `LAST_ARRAY` of the copy, which holds slices 0 to `LAST_ARRAY`, and a 2D binding of it reads slice `BASE_ARRAY`; that follows the colour 2D-array semantics and the titles that hit it (PPSA01556, PPSA03912), and was not measured on hardware for depth surfaces. Planes with mips, a cube or a 2D T# with a base slice other than 0, a `BASE_ARRAY` past `LAST_ARRAY`, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). - [Repacked format-43 copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a sampled texture of format 43 (10-bit R, 11-bit G and B unsigned floats from bit 0) is uploaded into a `B10G11R11_UFLOAT` image with its R and B fields swapped (`RepackTextureWord`) and viewed with R and B swapped back. A texture whose results a storage image still holds is copied from it again, whole, whenever the image changed (`RefreshDepthCopy`); a 3D one is flushed to guest memory and uploaded instead. Storage reads and writes of format 43 keep the packed `R32_UINT` path. - [SW_64KB_Z_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the texture equations and 64 KiB block geometry of `SW_64KB_Z_X`, whose addresses are checked against [AMD addrlib GFX10](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) in the texture tiling tests. Any 256-byte pipe/bank XOR below the block size is accepted, as for `SW_64KB_R_X` targets and Z_X textures; which of those bits the console honours, and the render results, are not measured. From 09093bfa26970180510ab7bd454b651ead266792 Mon Sep 17 00:00:00 2001 From: Nicolas Abosch <88494836+nicolasabosch@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:31:17 -0300 Subject: [PATCH 056/219] fix(libkernel): reset the wake event before a wakeable sleep and keep coarse and sub-50 us sleeps as before A wake set after the previous wait returned left the per-thread auto-reset event signalled, so the next short sleep returned from its wait at once and spun to its deadline. With APS5_COARSE_TIMED_WAITS or a request under 50 us the sleep now takes TimedWait::SleepNanos as on main. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Time/TimedWait.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/prx/libkernel/Time/TimedWait.cpp b/core/libs/prx/libkernel/Time/TimedWait.cpp index 15250d04e1..7c171db399 100644 --- a/core/libs/prx/libkernel/Time/TimedWait.cpp +++ b/core/libs/prx/libkernel/Time/TimedWait.cpp @@ -455,6 +455,10 @@ void SleepUntil(std::uint64_t deadlineNanos) { bool SleepNanosUnlessChanged(std::uint64_t nanos, const std::atomic& counter, std::uint64_t observed) { const std::uint64_t deadlineNanos = NowNanos() + nanos; #ifdef _WIN32 + if (Coarse() || nanos < YIELD_BELOW_NANOS) { + SleepNanos(nanos); + return false; + } Waiter* waiter = ThisThreadWaiter(); if (!waiter->wake) waiter->wake = CreateEventW(nullptr, FALSE, FALSE, nullptr); if (!waiter->wake) throw std::runtime_error("TimedWait: CreateEventW failed for a wakeable sleep"); @@ -472,6 +476,7 @@ bool SleepNanosUnlessChanged(std::uint64_t nanos, const std::atomicwake}; + ResetEvent(waiter->wake); std::uint64_t now = NowNanos(); if (counter.load(std::memory_order_seq_cst) != observed) return true; if (deadlineNanos > now + LEAD_NANOS) { From 462cbf6499097b1b630156e9d29c5ae0c03680d0 Mon Sep 17 00:00:00 2001 From: Nicolas Abosch <88494836+nicolasabosch@users.noreply.github.com> Date: Mon, 19 Oct 2026 12:09:15 -0300 Subject: [PATCH 057/219] docs(libkernel): shorten the short-sleep debt entry Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 98617203ba..e9264de0c0 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -210,7 +210,7 @@ - [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified; XPSemu currently stops first at the unresolved `mlockall` import (#4386) - [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 8192 entries on each side and counts them as a PS5 (firmware 13.60) counted them for a payload process: one cpu entry for each 2 MiB granule that holds a committed mapping, whatever its protection (none included), and one gpu entry for each 1 GiB region that holds a GPU-accessible mapping, plus one for each 2 MiB granule of such a mapping that is not a whole granule of direct memory at a 2 MiB-aligned physical address. A granule or region shared by several mappings counts once, and reservations do not count. On the console the first GPU mapping of a process also takes three gpu entries and one cpu entry that stay after it is unmapped, which is not modelled, and only the ranges whose protection is recorded here are counted. A null pointer returns `SCE_KERNEL_ERROR_EFAULT`, where the console faults. Resonance: A Plague Tale Legacy (PPSA30601) calls it at start-up (#4067) - [clock_settime](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) validates like FreeBSD 11.4's [`kern_clock_settime`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_time.c#L396-L412) for a root caller: `EFAULT` for a null time, `EINVAL` for any clock but `CLOCK_REALTIME`, a negative second count or a nanosecond count outside 0 to 999999999. A valid `CLOCK_REALTIME` request throws, naming the requested time, since the host clock is never set and no guest clock offset is kept. FreeBSD checks `PRIV_CLOCK_SETTIME` first, which a jailed process fails with `EPERM`; whether a title holds it on the console is unverified. PS5MC (PPSA99420) imports it; no call site is known -- [Short sleeps ended by GPU and flip completions](../../../core/libs/prx/libkernel/Time/SleepWake.cpp) (libkernel) - a `sceKernelUsleep`, `usleep`, `nanosleep` or `sceKernelNanosleep` of 1 ms or less returns before the requested time once the AGC driver reports a completed GPU batch or batch completion action, or VideoOut completes a flip, so a title that polls a GPU label or the flip status between sleeps sees it without sleeping the rest. FreeBSD and the console sleep at least the requested time: a title that paces frames with such a sleep can wake up to 1 ms early per sleep. A thread returns early at most 8 times in a run of sleeps; the run restarts when the thread submits AGC work or did not sleep for at least the requested time since its last sleep, so a loop that counts sleeps to time out still sleeps all but 8 of them in full. Polls with longer sleeps, or that need more than 8 early returns per run on a thread that does not submit, are not shortened. `APS5_NO_SLEEP_WAKE=1` keeps every sleep in full +- [Short sleeps ended by GPU and flip completions](../../../core/libs/prx/libkernel/Time/SleepWake.cpp) (libkernel) - a `sceKernelUsleep`, `usleep`, `nanosleep` or `sceKernelNanosleep` of 1 ms or less returns before the requested time, which FreeBSD and the console do not do: it ends once the AGC driver reports a completed GPU batch or batch completion action, or VideoOut completes a flip, so a title that polls a GPU label or the flip status between sleeps does not sleep the rest. A title that paces frames with such a sleep can wake up to 1 ms early. A thread returns early at most 8 times in a run of sleeps; the run restarts when the thread submits AGC work or did not sleep the requested time since its last sleep, so a loop that counts sleeps to time out still sleeps all but 8 in full. `APS5_NO_SLEEP_WAKE=1` keeps every sleep in full - [chflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) follows FreeBSD 11.4's [`kern_chflagsat`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2366-L2383) and UFS's [`ufs_setattr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/ufs/ufs/ufs_vnops.c#L509-L556) for a root caller: the path lookup reports `EFAULT`, `ENOENT` and `ENOTDIR`, a flag outside the 17 UFS stores (`VNOVAL` included) `EOPNOTSUPP`, and `SF_SNAPSHOT` `EPERM`. Neither host keeps BSD file flags, so every file is taken to have none (`stat` reports `st_flags` 0): flags 0 succeed without touching the file, and any other flag throws, naming the flags. `UF_READONLY`, `UF_HIDDEN`, `UF_SYSTEM` and `UF_ARCHIVE` are FreeBSD's names for the Windows attributes but are not mapped to them: UFS stores them without enforcing them, `FILE_ATTRIBUTE_READONLY` already carries the owner write bit of `chmod` on Windows, and Linux has no equivalent a process can set without `CAP_LINUX_IMMUTABLE`. The console's filesystem types, `EROFS` on read-only mounts and the change-time update are not modelled - [getpriority](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) follows FreeBSD 11.4's [`sys_getpriority`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_resource.c#L93-L165) for the only guest process, which is taken to run at nice 0: `PRIO_PROCESS` and `PRIO_PGRP` with 0 or its own pid (the guest is taken to lead its own process group, see `kill`) and `PRIO_USER` with 0 or its user id 0 return 0, any other id fails with `ESRCH` and any other `which` with `EINVAL`. As in FreeBSD's libc, where `getpriority` is the bare system call, errno is left unchanged on success. The nice value of a title process on the console is unverified, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises is not reported. `setpriority` accepts the same target ids (any other id fails with `ESRCH`, any other `which` with `EINVAL`), clamps the value to FreeBSD's `PRIO_MIN`-`PRIO_MAX` (-20 to 20) and stores it as the guest process's nice value, which `getpriority` returns; the host scheduling class is not changed, and whether a title process may lower the value without privilege is unverified - [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `rtprio_thread` follows FreeBSD 11.4's `rtprio_thread(2)`. Every guest thread here runs under `SCHED_FIFO` or `SCHED_RR` (priority 256-767), whose rtprio class is real-time; the console's mapping from that priority to the rtprio `prio` range (0-31) is unmeasured, so `RTP_LOOKUP` and `RTP_SET` throw for such a thread rather than report an impossible priority. A thread that is not real-time reports `RTP_PRIO_NORMAL` (3) with 0, and `RTP_SET` accepts only that value. Another function fails with `EINVAL`, a null `rtprio` with `EFAULT`, a thread other than the calling one throws, and an id that is not a live guest thread with `ESRCH`. `pthread_setconcurrency` stores 0 or more and returns `EINVAL` for a negative level (the error itself, as FreeBSD's libthr does, with errno untouched); `pthread_getconcurrency` returns the level, which is a hint and changes nothing about the host's concurrency. Morrowind (`mshivam019/morrowind-ps5`) imports all three at start-up From 447b744ddf45ff6978e91ca953dd81699bf7d932 Mon Sep 17 00:00:00 2001 From: Nicolas Abosch <88494836+nicolasabosch@users.noreply.github.com> Date: Mon, 19 Oct 2026 12:44:39 -0300 Subject: [PATCH 058/219] test(libkernel): run guest_sleep_wake with coarse timed waits too With APS5_COARSE_TIMED_WAITS set a sleep is never ended early. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 6 ++++-- core/libs/tests/GuestSleepWake.cpp | 12 ++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 14f89254e1..cc845d9afa 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -727,9 +727,11 @@ if(BUILD_TESTING) target_link_libraries(guest_sleep_wake_tests PRIVATE libkernel libc) configure_windows_unwind(guest_sleep_wake_tests) add_test(NAME guest_sleep_wake COMMAND guest_sleep_wake_tests) - set_tests_properties(guest_sleep_wake PROPERTIES TIMEOUT 20) + add_test(NAME guest_sleep_wake_coarse COMMAND guest_sleep_wake_tests) + set_tests_properties(guest_sleep_wake guest_sleep_wake_coarse PROPERTIES TIMEOUT 20) + set_tests_properties(guest_sleep_wake_coarse PROPERTIES ENVIRONMENT "APS5_COARSE_TIMED_WAITS=1") if(WIN32) - set_tests_properties(guest_sleep_wake PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_sleep_wake guest_sleep_wake_coarse PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_pthread_sem_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSem.cpp) diff --git a/core/libs/tests/GuestSleepWake.cpp b/core/libs/tests/GuestSleepWake.cpp index edf9fad89d..d58b3d9255 100644 --- a/core/libs/tests/GuestSleepWake.cpp +++ b/core/libs/tests/GuestSleepWake.cpp @@ -48,6 +48,14 @@ private: std::thread thread; }; +static void CoarseWaitsKeepEverySleepFull() { + Waker waker; + for (unsigned i = 0; i < 12; ++i) { + KernelNoteGpuSubmit_nid_no_patch(); + Require(Usleep(WAKEABLE_MICROS) >= std::chrono::microseconds(WAKEABLE_MICROS)); + } +} + static void SleepWithoutWakeIsFull() { for (unsigned i = 0; i < 4; ++i) Require(Usleep(WAKEABLE_MICROS) >= std::chrono::microseconds(WAKEABLE_MICROS)); } @@ -104,6 +112,10 @@ static void GapStartsNewRun() { } int main() { + if (std::getenv("APS5_COARSE_TIMED_WAITS") != nullptr) { + CoarseWaitsKeepEverySleepFull(); + return 0; + } SleepWithoutWakeIsFull(); WakeEndsShortSleep(); WakeLeavesLongSleep(); From 2873e9c3617eba642c71618062222ac7f02654ad Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Sun, 18 Oct 2026 22:38:15 +0530 Subject: [PATCH 059/219] fix(ampr): validate null command buffer arguments --- core/libs/prx/libSceAmpr/Export.cpp | 4 ++++ core/libs/tests/GuestAmpr.cpp | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index fd0d7cf527..e79d2d0143 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -347,6 +347,7 @@ std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWaitOnCounter() { return sizeof std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteKernelEventQueueOnCompletion() { return sizeof(Apr::WriteKernelEventQueueCommand); } int APS5_VABI sceAmprAprCommandBufferConstructor(Apr::CommandBufferObject* buffer, uint64_t* gatherState, uint64_t* scatterState) { + if (!buffer || !gatherState || !scatterState) return SCE_KERNEL_ERROR_EINVAL; buffer->type = Apr::BufferType::Apr; *gatherState = 0; *scatterState = 0; @@ -452,6 +453,7 @@ int APS5_VABI sceAmprCommandBufferConstructNop(Apr::CommandBufferObject* buffer, } int APS5_VABI sceAmprCommandBufferConstructor(Apr::CommandBufferObject* buffer) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; *buffer = {Apr::BufferType::Generic, 0, 0, 0, 0, nullptr}; return 0; } @@ -506,12 +508,14 @@ int APS5_VABI sceAmprCommandBufferPushMarkerWithColor(Apr::CommandBufferObject* } int APS5_VABI sceAmprCommandBufferReset(Apr::CommandBufferObject* buffer) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; buffer->offset = 0; buffer->numCommands = 0; return 0; } int APS5_VABI sceAmprCommandBufferSetBuffer(Apr::CommandBufferObject* buffer, void* memory, uint32_t size) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; buffer->base = static_cast(memory); buffer->size = size; buffer->offset = 0; diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 2340688d38..ca17fc2c23 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -243,6 +243,16 @@ void TestRejectedArguments() { Require(sceAmprMeasureCommandSizePushMarkerWithColor(nullptr, color) == rejected); Require(sceAmprMeasureCommandSizeSetMarker(nullptr) == rejected); Require(sceAmprMeasureCommandSizeSetMarkerWithColor(nullptr, color) == rejected); + + std::uint64_t gatherState = 0; + std::uint64_t scatterState = 0; + std::array memory{}; + Require(sceAmprCommandBufferConstructor(nullptr) == invalidArgument); + Require(sceAmprAprCommandBufferConstructor(nullptr, &gatherState, &scatterState) == invalidArgument); + Require(sceAmprAprCommandBufferConstructor(&recorder.buffer, nullptr, &scatterState) == invalidArgument); + Require(sceAmprAprCommandBufferConstructor(&recorder.buffer, &gatherState, nullptr) == invalidArgument); + Require(sceAmprCommandBufferReset(nullptr) == invalidArgument); + Require(sceAmprCommandBufferSetBuffer(nullptr, memory.data(), memory.size()) == invalidArgument); } void TestFullBuffer() { From c84149b0c13fa94fc3f354e8182c2ec3c3735d2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 19 Oct 2026 18:39:25 +0200 Subject: [PATCH 060/219] docs: record unverified null-argument results of libSceAmpr command buffer exports --- docs/dev/debt/libSceAmpr.md | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index a9c31f321e..d1303bca9d 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -2,16 +2,4 @@ ### Unknown function info -- [AMPR WriteAddressFromCounterPairOnCompletion](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order -- [AMPR command buffer offsets](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - recording with a current offset greater than the bound memory size throws before calculating remaining capacity or writing a command. This validates the emulator's own command-buffer encoding; the console's response to a corrupted opaque buffer object is unknown. -- [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_KERNEL_ERROR_EBUSY` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes -- [APR map begin and end](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - as in ampr_emu, `MapBegin`/`MapDirectBegin` map when they run and `MapEnd` only closes the region; while a region is open a second begin and every completion write return `EPERM`, the at-start `_04_00` writes are accepted -- [AMPR markers](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded -- [__cxa_call_unexpected](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - takes the in-flight exception's unwind header and never returns, as [libcxxabi 18.1.8](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_personality.cpp#L1171-L1211) does. No `std::set_unexpected` is exported and the exception header's unexpected handler is never set, so the handler is always the default one, which calls `std::terminate` ([cxa_default_handlers.cpp](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_default_handlers.cpp#L88-L99)); it therefore forwards to libc's `__cxa_call_terminate`, which begins the catch and runs the current terminate handler. That matches libcxxabi except for which terminate handler runs: there `std::terminate` after `__cxa_begin_catch` ([cxa_exception.cpp L470-L477](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L470-L477)) runs the handler saved in the caught exception's header ([cxa_handlers.cpp L72-L93](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_handlers.cpp#L72-L93)), which `__cxa_throw` and `__cxa_init_primary_exception` set to `std::get_terminate()` when the exception is thrown ([cxa_exception.cpp L209-L214](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L209-L214), [L279](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L279)), while here the current global handler runs, as AnyPS5's own `std::terminate` does. The two differ only if `std::set_terminate` was called between the throw and this call. A null header terminates because this emulator's libc `__cxa_begin_catch` terminates on null (libcxxabi does not define that case). Dynamic exception specifications that would let a replaced unexpected handler throw a permitted type are not modelled - -### Functional - -- [sceAmprAmmGetUsageStatsData](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) fills the caller-sized 24-byte statistics structure with 512 page-table pool entries, no allocated entries, a zero high watermark and ring idle flags 0x7, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libAmpr.cpp); they are not measured on a console, and the real call is a pass-through to `sceKernelMapperGetUsageStatsData` per [drakmor/ampr_emu](https://github.com/drakmor/ampr_emu/blob/main/src/ampr_emu_amm.cpp), which this port has no kernel-side counterpart for. -- [Dinkumware thread exports](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning. -- [_Cnd_signal, _Cnd_init_with_name](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - take a pointer to the handle slot like the libc `_Cnd_*` exports from [#3079](https://github.com/boykopovar/AnyPS5/pull/3079) and act on the same conditions. `_Cnd_signal` wakes one waiter and returns `_Thrd_success` (0), as `_Cnd_broadcast` does; whether the console maps a failing internal signal to `_Thrd_error` is not known. The signature `_Cnd_init_with_name(_Cnd_t*, const char* name)` is inferred from [fpPS4 `ps4_mtx_internal.pas`](https://github.com/red-prig/fpPS4/blob/trunk/src/libcinternal/ps4_mtx_internal.pas), where the PS4 `_Mtx_init` is `_Mtx_init_with_name` with the name `SceLibcI_Mtx` and the name only reaches the pthread object; here it is `_Cnd_init` and the name, which may be null, is ignored. Neither is confirmed on a PS5 `libc.prx`. `_Thrd_id` still throws: microsoft/STL returns an `unsigned int` thread id, but whether the PS5 returns the `pthread_t` or a kernel thread id, and at what width, is not known -- [std::_Pad](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follows Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` of Visual C++ 2013 ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. The pad is taken to be that header's class with the console's pointer handles: a vtable pointer whose first slot is the pure virtual `_Go()`, then `_Cnd_t` at 0x8, `_Mtx_t` at 0x10 and `bool _Started` at 0x18, 32 bytes, the derived `_LaunchPad` target following it; the console's `_Pad(const char*)` and `_Launch(const char*, pthread**)`/`_Launch(pthread_attr**, pthread**)` overloads suggest named primitives and could mean another layout or vtable. `_Launch` starts the thread with `pthread_create` and default attributes (the console's `_Thrd_start` name, stack size and priority are unknown), its entry calls `_Go()` and returns the result as the exit code, and it does not run `_Cnd_do_broadcast_at_thread_exit`, which is absent here. A failing primitive throws the guest `std::system_error` that `_Throw_C_error` picks for its code, and a failing thread start the `resource_unavailable_try_again` one of `_Thrd_startX`; any `pthread_create` error counts, since the console's `_Thrd_start` result is unknown. A null pad or thread slot throws +- [sceAmprCommandBufferConstructor](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the result for a null command buffer is unknown for `sceAmprCommandBufferConstructor`, `sceAmprAprCommandBufferConstructor`, `sceAmprCommandBufferReset` and `sceAmprCommandBufferSetBuffer`; they return `SCE_KERNEL_ERROR_EINVAL` like the other command buffer exports, while KytyPS5 returns OK for the constructors and EPERM for Reset From c9731df4c9aeb699198057b687e5eb746a3c9f55 Mon Sep 17 00:00:00 2001 From: mpratsaki <110930793+mpratsaki@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:54:53 +0300 Subject: [PATCH 061/219] fix(relinker): compare Windows file identities by handle MinGW's std::filesystem::equivalent returns false for hard links whose names give different permissions, such as .elf and .exe. The temporary hard-link probe that worked around it failed on read-only files and on directories that refuse the link. Compare the volume serial number and file index from GetFileInformationByHandle instead. Co-Authored-By: Claude Opus 5.5 --- core/relinker/main.cpp | 40 +++++++-------- .../relinker/tests/test_input_preservation.py | 51 +++++++++++-------- .../relinker/tests/test_output_collisions.py | 2 +- docs/dev/debt/relinker.md | 2 +- 4 files changed, 51 insertions(+), 44 deletions(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index dee892bb9b..fe626ea38d 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include #ifdef _WIN32 @@ -104,11 +106,9 @@ public: } } - std::filesystem::path GetPath() const { return directory / "file"; } const std::filesystem::path& GetDirectory() const { return directory; } void Remove() { - std::filesystem::remove(GetPath()); std::filesystem::remove(directory); directory.clear(); } @@ -117,26 +117,26 @@ private: std::filesystem::path directory; }; -bool SameOutputFile(const std::filesystem::path& first, const std::filesystem::path& second) { - if (std::filesystem::equivalent(first, second)) return true; #ifdef _WIN32 - if (std::filesystem::status(first).permissions() != std::filesystem::status(second).permissions() && - std::filesystem::hard_link_count(first) > 1 && std::filesystem::hard_link_count(second) > 1) { - for (const auto& path : {first, second}) { - if ((std::filesystem::status(path).permissions() & std::filesystem::perms::owner_write) == std::filesystem::perms::none) - throw Domain::RelinkerException("Cannot compare read-only output identities: " + Io::Utf8Path(path)); - } - OutputIdentityDirectory firstDirectory(first.parent_path()); - OutputIdentityDirectory secondDirectory(second.parent_path()); - std::filesystem::create_hard_link(first, firstDirectory.GetPath()); - std::filesystem::create_hard_link(second, secondDirectory.GetPath()); - const bool same = std::filesystem::equivalent(firstDirectory.GetPath(), secondDirectory.GetPath()); - secondDirectory.Remove(); - firstDirectory.Remove(); - return same; - } +std::tuple FileIdentity(const std::filesystem::path& path) { + const auto handle = CreateFileW(path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) + throw std::filesystem::filesystem_error("Cannot open file identity", path, std::error_code(static_cast(GetLastError()), std::system_category())); + BY_HANDLE_FILE_INFORMATION information{}; + const bool read = GetFileInformationByHandle(handle, &information) != 0; + const auto error = GetLastError(); + CloseHandle(handle); + if (!read) throw std::filesystem::filesystem_error("Cannot read file identity", path, std::error_code(static_cast(error), std::system_category())); + return {information.dwVolumeSerialNumber, information.nFileIndexHigh, information.nFileIndexLow}; +} +#endif + +bool SameOutputFile(const std::filesystem::path& first, const std::filesystem::path& second) { +#ifdef _WIN32 + return FileIdentity(first) == FileIdentity(second); +#else + return std::filesystem::equivalent(first, second); #endif - return false; } bool SameOutputDestination(const std::filesystem::path& first, const std::filesystem::path& second) { diff --git a/core/relinker/relinker/tests/test_input_preservation.py b/core/relinker/relinker/tests/test_input_preservation.py index 6384b54147..241dd1f4bc 100644 --- a/core/relinker/relinker/tests/test_input_preservation.py +++ b/core/relinker/relinker/tests/test_input_preservation.py @@ -31,10 +31,8 @@ def check_executable_hardlinks(relinker, work): try: result = subprocess.run([str(relinker), *command], cwd=case, capture_output=True, text=True, timeout=20) - assert result.returncode == 2 and ( - "output would overwrite" in result.stderr or - readonly and "Cannot compare read-only output identities" in result.stderr - ), (windows, output_name, input_name, readonly, result.stdout, result.stderr) + assert result.returncode == 2 and "output would overwrite" in result.stderr, ( + windows, output_name, input_name, readonly, result.stdout, result.stderr) for path, original in inputs.items(): assert path.read_bytes() == original, path for path in outputs.values(): @@ -47,24 +45,33 @@ def check_executable_hardlinks(relinker, work): if readonly: protected.chmod(stat.S_IWRITE) - case = work / "distinct-input-hardlinks" - command, outputs, inputs = prepare_secondary(case, True) - links = case / "separate-links" - links.mkdir() - for index, path in enumerate(inputs): - os.link(path, links / f"input-{index}") - original = next(iter(inputs.values())) - for name, path in outputs.items(): - path.write_bytes(original) - os.link(path, links / name) - result = subprocess.run([str(relinker), *command], cwd=case, - capture_output=True, text=True, timeout=20) - assert result.returncode == 0, (result.stdout, result.stderr) - for name in ("executable", "secondary-one", "secondary-two"): - assert outputs[name].read_bytes().startswith(b"MZ") - for path, original in inputs.items(): - assert path.read_bytes() == original and path.stat().st_nlink == 2, path - assert not list(case.rglob(".anyps5-output-identity-*")) + for readonly in ((False, True) if os.name == "nt" else (False,)): + case = work / f"distinct-input-hardlinks-{readonly}" + command, outputs, inputs = prepare_secondary(case, True) + links = case / "separate-links" + links.mkdir() + for index, path in enumerate(inputs): + os.link(path, links / f"input-{index}") + original = next(iter(inputs.values())) + for name, path in outputs.items(): + path.write_bytes(original) + os.link(path, links / name) + if readonly: + for path in inputs: + path.chmod(stat.S_IREAD) + try: + result = subprocess.run([str(relinker), *command], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (readonly, result.stdout, result.stderr) + for name in ("executable", "secondary-one", "secondary-two"): + assert outputs[name].read_bytes().startswith(b"MZ") + for path, original in inputs.items(): + assert path.read_bytes() == original and path.stat().st_nlink == 2, path + assert not list(case.rglob(".anyps5-output-identity-*")) + finally: + if readonly: + for path in inputs: + path.chmod(stat.S_IWRITE) def check_diagnostic_inputs(relinker, work): diff --git a/core/relinker/relinker/tests/test_output_collisions.py b/core/relinker/relinker/tests/test_output_collisions.py index 9af3a826a2..4213674c46 100644 --- a/core/relinker/relinker/tests/test_output_collisions.py +++ b/core/relinker/relinker/tests/test_output_collisions.py @@ -205,7 +205,7 @@ def main(): try: result = subprocess.run([str(relinker), *command], cwd=case, capture_output=True, text=True, timeout=20) - assert result.returncode == 2 and "Cannot compare read-only output identities" in result.stderr, ( + assert result.returncode == 2 and "Output paths collide" in result.stderr, ( result.stdout, result.stderr) assert output.read_bytes() == b"previous output" assert paths["module"].read_bytes() == b"previous output" diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index d49cedbfcb..af4fb966cc 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -4,7 +4,7 @@ - [Executable guest imports](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - on Linux and macOS, a relocated executable import resolved to one guest provider requires that module and its dependencies at startup, even when it was discovered through `--module-dir` without a DT_NEEDED entry. Unreferenced symbols and no-op relocations do not require an otherwise deferred module. Console module-start ordering is unmeasured. -- Windows [input and output collision checks](../../../core/relinker/main.cpp) use temporary hard links when MinGW reports different permissions for two multiply linked files, as with `.elf` and `.exe` names. The comparison fails if either file is read-only or either parent directory refuses the temporary link, even when the files are distinct. +- Windows [input and output collision checks](../../../core/relinker/main.cpp) compare the volume serial number and the 64-bit file index from `GetFileInformationByHandle`. ReFS file IDs are 128-bit, so two distinct files on ReFS can in principle compare equal and the conversion is refused; `FILE_ID_INFO` is not used. - [Runtime-loaded modules](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) that another module needs are initialized at startup with no start arguments, before the module that needs them is loaded; the console is assumed to start them when that module is loaded. - The executable file that [relinker](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens a console when launched, unless it is relinked with `--windows-gui`, which selects the GUI subsystem. A GUI build started from Explorer has no standard output or error handle; the [entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) gives each missing one `anyps5.log` beside the executable, replacing the previous run's file (#3282). Errors about the executable path or about waiting for the previous LoadExec process are written before the log is opened and are lost when the standard error handle is missing. If the log cannot be created, output to the missing handles is lost. - [Guest import registries](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - declared host imports of bundled modules are recorded with their module name and relocation target, even when another guest module exports the same stripped NID. No-op relocations are omitted. Registries describe import bindings, not implementation correctness or runtime reachability. From b336f0c4dce3285e8e8ffa3b3d81842247f994f1 Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Mon, 19 Oct 2026 16:55:42 +0200 Subject: [PATCH 062/219] fix(relinker): validate Windows trampoline section placement and sites --- core/relinker/CMakeLists.txt | 4 + .../src/windows/WindowsTrampolineBuilder.cpp | 37 +++++- .../tests/WindowsTrampolineBuilderTests.cpp | 118 ++++++++++++++++++ 3 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 core/relinker/elfpatcher/tests/WindowsTrampolineBuilderTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index a34e1d6552..33bf45ca0c 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -717,6 +717,10 @@ if(BUILD_TESTING) ) add_test(NAME windows_icon_resource COMMAND windows_icon_resource_tests) + add_test_executable(windows_trampoline_builder_tests ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsTrampolineBuilderTests.cpp) + target_link_libraries(windows_trampoline_builder_tests PRIVATE relinker_core) + add_test(NAME windows_trampoline_builder COMMAND windows_trampoline_builder_tests) + add_test_executable(entry_stub_tests ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/EntryStubTests.cpp) target_link_libraries(entry_stub_tests PRIVATE relinker_core) add_test(NAME entry_stub COMMAND entry_stub_tests) diff --git a/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp index f8d06aa53d..1fcf78fad7 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp @@ -5,18 +5,28 @@ #include #include #include +#include #include namespace Elfpatcher::Windows { namespace { +struct SitePatch { + PeSection* Section = nullptr; + std::ptrdiff_t Offset = 0; + std::size_t Length = 0; + std::vector JumpBytes; +}; + PeSection& findSection(std::vector& sections, const std::uint32_t rva, const std::size_t length) { for (auto& section : sections) { if (rva < section.Rva || rva - section.Rva >= section.Data.size()) continue; if (length > section.Data.size() - (rva - section.Rva)) throw Domain::RelinkerException("AMD-only instruction crosses a PE section boundary", rva); + if ((section.Characteristics & SectionExecute) == 0) + throw Domain::RelinkerException("AMD-only instruction is in a non-executable PE section", rva); return section; } throw Domain::RelinkerException("AMD-only instruction is outside the PE image", rva); @@ -28,12 +38,26 @@ void WindowsTrampolineBuilder::Build(const std::vector& using namespace Codegen::Amd64OnlySubstitutionTable; if (sites.empty()) return; + if (nextRva < LoadRva || nextRva % SectionAlignment != 0) + throw Domain::RelinkerException("Invalid Windows trampoline section RVA", nextRva); + if (std::any_of(sections.begin(), sections.end(), [](const auto& section) { return section.Name == ".amdstub"; })) + throw Domain::RelinkerException("Duplicate Windows trampoline section"); + if (!sections.empty() && nextRva < AlignRva(static_cast(sections.back().Rva) + sections.back().MemorySize())) + throw Domain::RelinkerException("Windows trampoline section overlaps preceding section", nextRva); const auto sectionRva = nextRva; std::vector bytes; + std::map ranges; + std::vector patches; + patches.reserve(sites.size()); for (const auto& site : sites) { const auto siteRva = image.GetRva(site.Address, site.Length); if (site.Length < kJmpRel32.Size || site.OriginalBytes.size() != site.Length || site.Body.size() < kJmpRel32.Size || site.ReturnBranchOffset > site.Body.size() - kJmpRel32.Size || site.Body[site.ReturnBranchOffset] != kJmpRel32.Bytes[0]) throw Domain::RelinkerException("Invalid AMD-only trampoline site", siteRva); + const auto siteEndRva = static_cast(siteRva) + site.Length; + const auto next = ranges.lower_bound(siteRva); + if ((next != ranges.end() && next->first < siteEndRva) || (next != ranges.begin() && std::prev(next)->second > siteRva)) + throw Domain::RelinkerException("Overlapping AMD-only trampoline sites", siteRva); + ranges.emplace(siteRva, siteEndRva); auto& section = findSection(sections, siteRva, site.Length); const auto offset = static_cast(siteRva - section.Rva); if (!std::equal(site.OriginalBytes.begin(), site.OriginalBytes.end(), section.Data.begin() + offset)) @@ -43,19 +67,22 @@ void WindowsTrampolineBuilder::Build(const std::vector& const auto bodyOffset = bytes.size(); bytes.insert(bytes.end(), site.Body.begin(), site.Body.end()); Codegen::ApplyStubRelocations(std::span(bytes.data() + bodyOffset, site.ReturnBranchOffset), site.Relocations, siteRva, stubRva, site.Offset); - const auto returnRva = CheckedRva(siteRva + site.Length); + const auto returnRva = CheckedRva(siteEndRva); const auto displacement = static_cast(returnRva) - (static_cast(stubRva) + static_cast(site.ReturnBranchOffset + kJmpRel32.Size)); if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) throw Domain::RelinkerException("AMD-only stub return exceeds rel32 range", returnRva); Io::WriteU32(bytes, bodyOffset + site.ReturnBranchOffset + 1, static_cast(displacement)); WindowsStubEmitter jump(siteRva); jump.Rip({0xe9}, stubRva); - const auto jumpBytes = jump.TakeBytes(); - std::fill_n(section.Data.begin() + offset, site.Length, kNop1.Bytes[0]); - std::copy(jumpBytes.begin(), jumpBytes.end(), section.Data.begin() + offset); + patches.push_back({§ion, offset, site.Length, jump.TakeBytes()}); + } + const auto endRva = AlignRva(sectionRva + bytes.size()); + for (const auto& patch : patches) { + std::fill_n(patch.Section->Data.begin() + patch.Offset, patch.Length, kNop1.Bytes[0]); + std::copy(patch.JumpBytes.begin(), patch.JumpBytes.end(), patch.Section->Data.begin() + patch.Offset); } sections.push_back({".amdstub", sectionRva, SectionRead | SectionExecute | 0x20u, std::move(bytes)}); - nextRva = AlignRva(sectionRva + sections.back().Data.size()); + nextRva = endRva; } } diff --git a/core/relinker/elfpatcher/tests/WindowsTrampolineBuilderTests.cpp b/core/relinker/elfpatcher/tests/WindowsTrampolineBuilderTests.cpp new file mode 100644 index 0000000000..b710c96f05 --- /dev/null +++ b/core/relinker/elfpatcher/tests/WindowsTrampolineBuilderTests.cpp @@ -0,0 +1,118 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace Elfpatcher::Windows; +using Bytes = std::vector; + +void require(const bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +template +void write(Bytes& bytes, const std::size_t offset, const TValue value) { + std::memcpy(bytes.data() + offset, &value, sizeof(value)); +} + +const Bytes SiteBytes{0xF2, 0x0F, 0x78, 0xDB, 0x08, 0x08}; +const Bytes StubBody{0x90, 0xE9, 0x00, 0x00, 0x00, 0x00}; + +Bytes elfFixture() { + Bytes bytes(0x400); + bytes[0] = 0x7F; + bytes[1] = 'E'; + bytes[2] = 'L'; + bytes[3] = 'F'; + bytes[4] = 2; + bytes[5] = 1; + bytes[6] = 1; + write(bytes, 16, 3); + write(bytes, 18, 62); + write(bytes, 24, 0x1000); + std::fill(bytes.begin() + 0x200, bytes.begin() + 0x300, 0xCC); + std::copy(SiteBytes.begin(), SiteBytes.end(), bytes.begin() + 0x202); + std::copy(SiteBytes.begin(), SiteBytes.end(), bytes.begin() + 0x210); + std::copy(SiteBytes.begin(), SiteBytes.end(), bytes.begin() + 0x302); + return bytes; +} + +std::vector elfHeaders() { + return {{1, 5, 0x200, 0x1000, 0, 0x100, 0x100, 0x1000}, {1, 6, 0x300, 0x2000, 0, 0x100, 0x100, 0x1000}}; +} + +const Bytes Source = elfFixture(); + +Codegen::TrampolineSite site(const std::uint64_t address) { + const auto offset = static_cast(address & 0xFFF) + (address < 0x2000 ? 0x200 : 0x300); + return {static_cast(offset), address, SiteBytes.size(), {Source.begin() + offset, Source.begin() + offset + static_cast(SiteBytes.size())}, StubBody, 1}; +} + +void requireRejected(const WindowsLoadImage& image, const std::vector& sites, std::vector sections, std::uint32_t nextRva, const std::string& expected) { + const auto original = sections; + const auto originalRva = nextRva; + try { + WindowsTrampolineBuilder().Build(sites, image, sections, nextRva); + } catch (const Domain::RelinkerException& error) { + require(std::string(error.what()).find(expected) != std::string::npos, "Unexpected error '" + std::string(error.what()) + "', expected '" + expected + "'"); + require(sections.size() == original.size() && nextRva == originalRva, "Rejected build changed the section list for '" + expected + "'"); + for (std::size_t index = 0; index < sections.size(); ++index) + require(sections[index].Data == original[index].Data, "Rejected build patched section " + sections[index].Name + " for '" + expected + "'"); + return; + } + throw std::runtime_error("Build succeeded, expected '" + expected + "'"); +} + +void run() { + const WindowsLoadImage image(Source, elfHeaders()); + const auto endRva = image.GetEndRva(); + + auto sections = image.BuildSections(); + auto nextRva = endRva; + WindowsTrampolineBuilder().Build({site(0x1002), site(0x1010)}, image, sections, nextRva); + require(sections.size() == 3 && sections.back().Name == ".amdstub" && sections.back().Rva == endRva, "Stub section was not appended"); + require(nextRva == AlignRva(static_cast(endRva) + sections.back().Data.size()), "Next RVA does not follow the stub section"); + require(sections[0].Data[2] == 0xE9 && sections[0].Data[7] == 0x90 && sections[0].Data[0x10] == 0xE9 && sections[0].Data[0x15] == 0x90, "Sites were not replaced by jumps"); + + const auto fresh = image.BuildSections(); + requireRejected(image, {site(0x1002), site(0x1004)}, fresh, endRva, "Overlapping AMD-only trampoline sites"); + requireRejected(image, {site(0x1004), site(0x1002)}, fresh, endRva, "Overlapping AMD-only trampoline sites"); + requireRejected(image, {site(0x1002), site(0x1002)}, fresh, endRva, "Overlapping AMD-only trampoline sites"); + requireRejected(image, {site(0x2002)}, fresh, endRva, "AMD-only instruction is in a non-executable PE section"); + + auto changed = site(0x1010); + changed.OriginalBytes[3] = 0xDC; + requireRejected(image, {site(0x1002), changed}, fresh, endRva, "AMD-only site bytes changed before patching"); + + requireRejected(image, {site(0x1002)}, fresh, endRva + 0x10, "Invalid Windows trampoline section RVA"); + requireRejected(image, {site(0x1002)}, fresh, LoadRva - SectionAlignment, "Invalid Windows trampoline section RVA"); + requireRejected(image, {site(0x1002)}, fresh, endRva - SectionAlignment, "Windows trampoline section overlaps preceding section"); + requireRejected(image, {site(0x1002)}, sections, nextRva, "Duplicate Windows trampoline section"); + + auto untouched = image.BuildSections(); + auto untouchedRva = endRva; + WindowsTrampolineBuilder().Build({}, image, untouched, untouchedRva); + require(untouched.size() == fresh.size() && untouchedRva == endRva, "Empty site list changed the image"); +} + +} + +int main() { + try { + run(); + } catch (const std::exception& e) { + std::cerr << e.what() << '\n'; + return 1; + } + return 0; +} From d37221a00ac191ba3eb90bb8f091336767139ea1 Mon Sep 17 00:00:00 2001 From: mopgobopot Date: Mon, 19 Oct 2026 20:01:41 +0300 Subject: [PATCH 063/219] fix(relinker): move in-place AMD-only replacements into a short instruction's stub A short AMD-only instruction lowered through a stub takes the following instructions with it. When one of them is an AMD-only instruction that is replaced in place (MOVNTSS, MOVNTSD, MONITORX, MWAITX, and RDSEED and CLWB for Rosetta), MatchSequence found no out-of-line lowering for it and the conversion failed with 'AMD-only instruction sequence has no out-of-line lowering', for example on extrq xmm0,xmm1 followed by movntss [rdi],xmm0. A following instruction with an in-place replacement now has that replacement moved into the stub, with its RIP-relative displacement relocated. Out-of-line emission for 256-bit store windows is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../src/x86/Amd64OnlyInstructionMatcher.cpp | 33 ++++++++++++++++++- .../codegen/tests/Amd64OnlyConverterTests.cpp | 24 ++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index c9128ecbaf..6809460347 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -182,6 +183,7 @@ private: [[nodiscard]] Amd64OnlyMatch _matchClzero(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] std::optional _matchRosetta(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] const char* _emitOutOfLine(StubBodyBuilder& body, const DecodedInstruction& instr) const; + [[nodiscard]] const char* _emitInPlace(StubBodyBuilder& body, const DecodedInstruction& instr) const; }; Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchMovnts(const DecodedInstruction& instr, const Entry& entry) const { @@ -292,6 +294,33 @@ const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, c return nullptr; } +const char* Amd64OnlyInstructionMatcher::_emitInPlace(StubBodyBuilder& body, const DecodedInstruction& instr) const { + if (const auto inPlace = Match(instr.Data, instr.Length); inPlace && inPlace->Lowering == Amd64OnlyLowering::InPlace) { + for (const auto* entry : {&kMovntss, &kMovntsd, &kMonitorx, &kMwaitx, &kRdseed, &kClwb}) { + if (inPlace->InstructionName != entry->Name) + continue; + const X64InstructionDecoder decoder; + const std::span replacement(inPlace->ReplacementBytes); + for (std::size_t offset = 0; offset < replacement.size();) { + const auto info = decoder.DecodeInstruction(replacement.data() + offset, replacement.size() - offset); + if (info.Length == 0 || info.Length > replacement.size() - offset) + throw CodegenException("In-place replacement does not split into whole instructions"); + const auto part = replacement.subspan(offset, info.Length); + if (info.HasRipRelativeDisp) { + std::int32_t displacement = 0; + std::memcpy(&displacement, part.data() + info.RipRelativeDispOffset, sizeof(displacement)); + body.RipRelative(part, info.RipRelativeDispOffset, static_cast(offset + info.Length) + displacement); + } else { + body.Raw(part); + } + offset += info.Length; + } + return entry->Name; + } + } + return nullptr; +} + void Amd64OnlyInstructionMatcher::EmitOutOfLine(StubBodyBuilder& body, const std::span instruction) const { if (_emitOutOfLine(body, DecodedInstruction{instruction.data(), instruction.size()}) == nullptr) throw CodegenException("AMD-only instruction has no out-of-line lowering"); @@ -308,7 +337,9 @@ std::optional Amd64OnlyInstructionMatcher::MatchSequence( const char* name = nullptr; for (const auto& instruction : instructions) { const DecodedInstruction instr{instruction.data(), instruction.size()}; - const auto emitted = _emitOutOfLine(body, instr); + auto emitted = _emitOutOfLine(body, instr); + if (emitted == nullptr && name != nullptr) + emitted = _emitInPlace(body, instr); if (emitted == nullptr) return std::nullopt; if (name == nullptr) diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index e1a802b28c..342fb2ea2d 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -623,6 +623,29 @@ void converterMonitorWait() { require(result.Bytes == expected, "MONITORX/MWAITX were replaced with the wrong bytes"); } +void converterInPlaceFollower() { + const auto converter = Codegen::MakeAmd64OnlyConverter(); + const auto convert = [&](const Bytes& following) { + Bytes file(0x300, 0xCC); + Bytes text = {0x66, 0x0F, 0x79, 0xCA}; + text.insert(text.end(), following.begin(), following.end()); + text.push_back(0xC3); + std::copy(text.begin(), text.end(), file.begin() + 0x200); + return converter->Convert(file, {segmentHeader(text.size())}); + }; + const auto tail = [](const Codegen::TrampolineSite& site, const std::size_t length) { + return Bytes(site.Body.begin() + static_cast(site.ReturnBranchOffset - length), site.Body.begin() + static_cast(site.ReturnBranchOffset)); + }; + const auto store = convert({0xF3, 0x0F, 0x2B, 0x07}); + require(store.Trampolines.size() == 1 && store.Trampolines[0].Length == 8 && tail(store.Trampolines[0], 4) == Bytes{0xF3, 0x0F, 0x11, 0x07}, "MOVNTSS after a short EXTRQ was not moved into its stub as MOVSS"); + const auto ripStore = convert({0xF2, 0x0F, 0x2B, 0x05, 0x40, 0x00, 0x00, 0x00}); + require(ripStore.Trampolines.size() == 1 && ripStore.Trampolines[0].Length == 12, "RIP-relative MOVNTSD after a short EXTRQ was not moved into its stub"); + const auto& ripSite = ripStore.Trampolines[0]; + require(tail(ripSite, 8) == Bytes{0xF2, 0x0F, 0x11, 0x05, 0x00, 0x00, 0x00, 0x00} && ripSite.Relocations.size() == 1 && ripSite.Relocations[0].SiteTarget == 12 + 0x40 && ripSite.Relocations[0].InstructionEnd == ripSite.ReturnBranchOffset, "Moved MOVNTSD has the wrong bytes or relocation"); + const auto monitor = convert({0x0F, 0x01, 0xFA}); + require(monitor.Trampolines.size() == 1 && monitor.Trampolines[0].Length == 7 && tail(monitor.Trampolines[0], 3) == Bytes{0x0F, 0x1F, 0x00}, "MONITORX after a short EXTRQ was not moved into its stub as a NOP"); +} + void converterClzero() { const auto converter = Codegen::MakeAmd64OnlyConverter(); Bytes file(0x300, 0xCC); @@ -1872,6 +1895,7 @@ int main() { converterSha256(); converterSha1(); converterMonitorWait(); + converterInPlaceFollower(); converterClzero(); rosettaSubstitutions(); converterRosetta(); From e3e05d917190528b9a919e489e86f1b108a09384 Mon Sep 17 00:00:00 2001 From: mopgobopot Date: Mon, 19 Oct 2026 20:07:03 +0300 Subject: [PATCH 064/219] fix(relinker): do not take instructions another AMD-only stub moved as a short instruction's followers A short AMD-only instruction takes the following instructions into its stub until its site fits a jump. The loop did not check which instructions an earlier 256-bit store island had already claimed, so with --to-rosetta two trampolines covered the same bytes: for je; jmp; vmovups [rdi],ymm0; rcpps xmm1,xmm2 followed by adds, the RCPPS site at +8 overlapped the island at +11, the patched code crashed, and the real patchers failed with 'AMD-only site bytes changed before patching'. A claimed follower now ends the attempt: an optional instruction such as RCPPS or RDPID is kept, and any other instruction fails with an error naming the moved follower. Co-Authored-By: Claude Opus 5.5 --- .../codegen/src/Amd64OnlyConverter.cpp | 2 ++ .../codegen/tests/Amd64OnlyConverterTests.cpp | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/core/relinker/codegen/src/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index a2ea235db6..a972a2eceb 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -386,6 +386,8 @@ void Amd64OnlyConverter::_convertSegment( for (auto next = item.Index + 1; siteLength < Amd64OnlySubstitutionTable::kJmpRel32.Size; ++next) { if (next >= matches.size() || matches[next].Offset != match.Offset + siteLength) throw CodegenException("AMD-only instruction too short for a jump and not followed by an instruction", fileOffset); + if (claimed.contains(next)) + throw CodegenException("AMD-only instruction too short for a jump is followed by an instruction another stub already moved", ph.Offset + matches[next].Offset); const auto& following = matches[next]; const std::span bytes(seg.data() + following.Offset, following.Length); const auto info = decoder.DecodeInstruction(bytes.data(), bytes.size()); diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index e1a802b28c..4557a4ee4a 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -905,6 +905,32 @@ void converterWideStoreWindows() { requireFailure([&] { (void)convert(withStore(far, tail)); }, "A 256-bit store whose movable neighbours are all beyond a short jump was kept"); } +void converterClaimedFollower() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + for (const auto& [shortInstruction, name] : {std::pair{Bytes{0x0F, 0x53, 0xCA}, "RCPPS"}, std::pair{Bytes{0xF3, 0x0F, 0xC7, 0xF8}, "RDPID"}}) { + Bytes file(0x300, 0xCC); + Bytes text = {0x74, 0x06, 0xEB, 0x00, 0xC5, 0xFC, 0x11, 0x07}; + text.insert(text.end(), shortInstruction.begin(), shortInstruction.end()); + for (int index = 0; index < 4; ++index) + text.insert(text.end(), {0x48, 0x01, 0xC8}); + text.insert(text.end(), 8, 0x90); + text.push_back(0xC3); + std::copy(text.begin(), text.end(), file.begin() + 0x200); + const auto result = converter->Convert(file, {segmentHeader(text.size())}); + for (std::size_t left = 0; left < result.Trampolines.size(); ++left) { + for (std::size_t right = left + 1; right < result.Trampolines.size(); ++right) { + const auto& a = result.Trampolines[left]; + const auto& b = result.Trampolines[right]; + require(a.Offset + a.Length <= b.Offset || b.Offset + b.Length <= a.Offset, "A short instruction's site overlaps instructions another stub already moved"); + } + } + bool kept = false; + for (const auto& report : result.Reports) + kept = kept || (report.InstructionName == name && report.Lowering == Codegen::Amd64OnlyLowering::Kept); + require(result.KeptCount == 1 && kept, "An optional short instruction followed by moved instructions was not kept"); + } +} + void converterJumpTableTargets() { const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); const Bytes leaTable = {0x48, 0x8D, 0x0D, 0xF9, 0x1F, 0x00, 0x00}; @@ -1882,6 +1908,7 @@ int main() { rdtscpExecution(); wideStoreSplit(); converterWideStoreWindows(); + converterClaimedFollower(); converterJumpTableTargets(); converterReciprocal(); converterStrayRex(); From 612b41e4f34a8782dd2aacfee852fdfc852ecabc Mon Sep 17 00:00:00 2001 From: HQasper <52220032+HQasper@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:36:01 +0200 Subject: [PATCH 065/219] perf(agc): resolve DISPATCH_INDIRECT thread dimensions on the GPU An indirect dispatch with USE_THREAD_DIMENSIONS read its thread counts on the CPU, which waits for the recorded work that stores them. The argument kernel now turns them into group counts on the GPU and copies the counts into the ShaderData thread limit, so the dispatch stays on the GPU path. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 + .../Execution/include/VulkanDevice.hpp | 7 +- .../src/Driver/Dispatch/Dispatch.cpp | 11 +- .../src/Driver/Dispatch/IndirectDispatch.cpp | 3 +- .../Execution/src/VulkanDevice.cpp | 48 +++- .../Graphics/include/Recorder.hpp | 2 +- .../Graphics/include/ShaderResources.hpp | 3 + .../Graphics/shaders/DispatchArguments.comp | 16 ++ .../libSceAgcDriver/Graphics/src/Recorder.cpp | 12 +- .../Graphics/src/ShaderResources.cpp | 27 ++- .../execution/IndirectThreadDimensions.cpp | 214 ++++++++++++++++++ docs/dev/ENVIRONMENT.md | 1 + 12 files changed, 331 insertions(+), 19 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/IndirectThreadDimensions.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 65f1aff6c3..82a6f94fbd 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1776,6 +1776,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_indirect_thread_dimensions_cpu COMMAND agc_driver_indirect_thread_dimensions_tests) + set_tests_properties(agc_driver_indirect_thread_dimensions_cpu PROPERTIES ENVIRONMENT "APS5_NO_GPU_THREAD_DIMENSIONS=1" SKIP_RETURN_CODE 77 TIMEOUT 60) + if(WIN32) + set_tests_properties(agc_driver_indirect_thread_dimensions_cpu PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS) if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/${agcExecutionTestName}.cpp) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 4b8f170a2e..9fbaa47e89 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -11,6 +11,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include #include #include #include @@ -272,7 +273,7 @@ public: int cpuReason; double argumentReadMs; }; - IndirectOutcome DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr prepared = nullptr, std::shared_ptr* recipe = nullptr); + IndirectOutcome DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr prepared = nullptr, std::shared_ptr* recipe = nullptr, const std::array& threadGroup = {}); // A value-equal dispatch-cache hit with a recipe (design_cpu_final M4), in two steps. // PrepareRecipe, WITHOUT GuestMemory::GpuMutex: the pre-checks (recipes on, the recipe's // device is this one, its template and pipeline objects still alive), the template's @@ -294,7 +295,7 @@ public: // with the recipe's, else the process aborts. `refreshByWords`: the driver's data-only hit // (`shader` carries live flat-SRT words the recipe's hash does not name), so the template's // data refresh is decided by the per-word compare instead of the hash. - RecipeOutcome DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr& hit, IndirectOutcome& outcome, const std::shared_ptr& verify = nullptr, bool refreshByWords = false); + RecipeOutcome DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr& hit, IndirectOutcome& outcome, const std::shared_ptr& verify = nullptr, bool refreshByWords = false, const std::array& threadGroup = {}); // Whether dispatch-cache hits use recipes (APS5_NO_DISPATCH_RECIPE unset) and whether every // hit is verified (APS5_VERIFY_RECIPE=1). static bool DispatchRecipes(); @@ -347,7 +348,7 @@ private: ShaderRecompiler::SpirvTarget buildTarget() const; Graphics::Context buildContext() const; // Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups. - IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe); + IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe, const std::array& threadGroup = {}); // The stage-A pre-sync over `surfaces` (see PrepareDispatch): the serial waited for, 0 none. std::uint64_t presync(std::span surfaces); std::shared_ptr computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index 565ec80e25..5b323662c9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -145,11 +145,16 @@ void Driver::dispatch(QueueState& queue, std::span packet, } } if (partialLastGroup) compute.partialThreads = partialGroups; + std::array threadGroup{}; + if (indirectArguments != 0 && (packet[4] & 0x20u) != 0) { + for (std::uint32_t axis = 0; axis < 3; ++axis) threadGroup[axis] = std::max(readRegister(queue.shader, 0x207 + axis) & 0xffffu, 1u); + compute.partialThreads = {~0u, ~0u, ~0u}; + } ShaderRecompiler::RecompileRequest request{ {ShaderRecompiler::ShaderStage::Compute, address, std::span(snapshot.code).subspan(codeOffset), snapshot.headerAddress, snapshot.header}, {(packet[4] & 0x8000u) != 0 ? 32u : 64u, 0, userData, compute, std::nullopt, std::nullopt, memory, DispatchFloatMode(snapshot, queue.shader)}, localDevice->ComputeTarget((packet[4] & 0x8000u) != 0 ? 32u : 64u), - {0, 0, 0, 128} + {0, 0, 0, threadGroup != std::array{} ? 0u : 128u} }; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; static double captureMs = 0, keyMs = 0, recompileMs = 0, deviceMs = 0; @@ -560,7 +565,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, try { if (recipeHit != nullptr) { VulkanDevice::IndirectOutcome outcome{0, 0}; - const auto result = localDevice->DispatchRecipe(compiled, groups[0], groups[1], groups[2], indirectArguments, address, recipeHit, outcome, VulkanDevice::VerifyRecipes() ? prepared : nullptr, dataHit); + const auto result = localDevice->DispatchRecipe(compiled, groups[0], groups[1], groups[2], indirectArguments, address, recipeHit, outcome, VulkanDevice::VerifyRecipes() ? prepared : nullptr, dataHit, threadGroup); if (result == RecipeOutcome::Rebuild) { recipeHit = nullptr; @@ -569,7 +574,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, } if (indirectArguments != 0) countIndirect(outcome.cpuReason, outcome.argumentReadMs); } else if (indirectArguments != 0) { - const auto outcome = localDevice->DispatchIndirect(compiled, indirectArguments, snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr); + const auto outcome = localDevice->DispatchIndirect(compiled, indirectArguments, snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr, threadGroup); countIndirect(outcome.cpuReason, outcome.argumentReadMs); } else { localDevice->Dispatch(compiled, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp index cd6ee2d743..65057babfa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp @@ -48,7 +48,8 @@ void Driver::countIndirect(int path, double readMs) { int Driver::indirectArgumentsPath(std::span packet, std::uint64_t arguments) { static const bool gpuIndirect = std::getenv("APS5_NO_GPU_INDIRECT") == nullptr; if (!gpuIndirect) return IndirectDisabled; - if ((packet.back() & 0x20u) != 0) return IndirectThreadDimensions; + static const bool gpuThreadDimensions = std::getenv("APS5_NO_GPU_THREAD_DIMENSIONS") == nullptr; + if ((packet.back() & 0x20u) != 0 && !gpuThreadDimensions) return IndirectThreadDimensions; if (arguments % 4 != 0) return IndirectMisaligned; return IndirectGpu; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 1ebaafcc12..83545714f3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4276,6 +4276,8 @@ struct RecordedDispatch { std::shared_ptr bindings{}; std::uint64_t cpuArguments = 0; int cpuReason = 0; + std::array threadGroup{}; + std::array threadLimit{}; }; VkDevice VulkanDevice::Device() const { @@ -4532,8 +4534,8 @@ void VulkanDevice::Dispatch(const ShaderRecompiler::RecompileResult& shader, std static_cast(dispatch(shader, x, y, z, 0, snapshots, programAddress, std::move(prepared), recipe)); } -VulkanDevice::IndirectOutcome VulkanDevice::DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe) { - return dispatch(shader, 0, 0, 0, arguments, snapshots, programAddress, std::move(prepared), recipe); +VulkanDevice::IndirectOutcome VulkanDevice::DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe, const std::array& threadGroup) { + return dispatch(shader, 0, 0, 0, arguments, snapshots, programAddress, std::move(prepared), recipe, threadGroup); } void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& outcome, char* groupsText) { @@ -4541,6 +4543,7 @@ void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& out auto& timer = *record.timer; const auto& context = *record.context; const auto arguments = record.arguments; + const bool threadDimensions = record.threadGroup != std::array{}; // Decided here, after the resource build (which may retire imports and wait for recorded // work) and before the dispatch itself is recorded, so the import cannot be dropped before // the record. The build already recorded the copy-ins of its misaligned and staged regions @@ -4567,7 +4570,8 @@ void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& out outcome.cpuReason = 8; } else if (labelPending() || state->CopiedWriterOverlaps(arguments, 12)) { outcome.cpuReason = 2; - } else if ((record.argumentImport = Graphics::HostImportFor(context, arguments, 12)) == nullptr) { + } else if ((record.argumentImport = Graphics::HostImportFor(context, arguments, 12)) == nullptr || (threadDimensions && record.argumentImport->address == 0)) { + record.argumentImport = nullptr; // Valid until the next refreshImports, which only runs under GuestMemory::GpuMutex (held // here, by every Dispatch caller) and Keeps a retired VkBuffer while the recorder is busy. outcome.cpuReason = 3; @@ -4584,6 +4588,10 @@ void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& out Graphics::FinishLeaseWork(); std::array groups{}; GuestMemory::Read(arguments, std::as_writable_bytes(std::span(groups)), 4); + if (threadDimensions) { + record.threadLimit = groups; + for (std::size_t axis = 0; axis < groups.size(); ++axis) groups[axis] = groups[axis] / record.threadGroup[axis] + (groups[axis] % record.threadGroup[axis] != 0 ? 1u : 0u); + } outcome.argumentReadMs = std::chrono::duration(std::chrono::steady_clock::now() - readStart).count(); record.x = groups[0]; record.y = groups[1]; @@ -4660,6 +4668,24 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { } recordStep(PhaseRecordDataRefresh); } + std::shared_ptr threadArguments; + if (record.threadGroup != std::array{}) { + const auto data = resources.shaderDataBuffer(*record.shader); + const auto limitDword = record.shader->program->imageMetadataDword; + Graphics::Require(data != VK_NULL_HANDLE && limitDword >= 3, "Vulkan dispatch: a thread-dimension dispatch has no ShaderData thread limit"); + const VkDeviceSize limitByte = static_cast(limitDword - 3) * sizeof(std::uint32_t); + Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + if (argumentImport != nullptr) { + threadArguments = std::make_shared(context, 24, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + Graphics::Require(recorder.RecordDispatchArguments(commands, argumentImport->address + (arguments - argumentImport->base), threadArguments->DeviceAddress(), &record.threadGroup), "Vulkan dispatch: thread-dimension indirect arguments need the argument kernel"); + const VkBufferCopy limit{12, limitByte, 12}; + context.Function("vkCmdCopyBuffer")(commands, threadArguments->Handle(), data, 1, &limit); + } else { + context.Resolved(&Graphics::DeviceFunctions::cmdUpdateBuffer, "vkCmdUpdateBuffer")(commands, data, limitByte, sizeof(record.threadLimit), record.threadLimit.data()); + } + resources.forgetShaderDataWords(*record.shader); + covered = 0; + } using CommandClass = Graphics::Recorder::CommandClass; if (Graphics::Recorder::BarrierValidate()) { if (argumentImport != nullptr) { @@ -4693,7 +4719,11 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { } VkBuffer indirectBuffer = argumentImport != nullptr ? argumentImport->buffer : VK_NULL_HANDLE; VkDeviceSize indirectOffset = argumentImport != nullptr ? Graphics::HostImportOffset(*argumentImport, arguments) : 0; - if (argumentImport != nullptr && argumentImport->address != 0) { + if (threadArguments != nullptr) { + indirectBuffer = threadArguments->Handle(); + indirectOffset = 0; + recorder.Keep(std::move(threadArguments)); + } else if (argumentImport != nullptr && argumentImport->address != 0) { auto checked = std::make_shared(context, 12, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); if (recorder.RecordDispatchArguments(commands, argumentImport->address + (arguments - argumentImport->base), checked->DeviceAddress())) { indirectBuffer = checked->Handle(); @@ -4911,7 +4941,7 @@ std::shared_ptr VulkanDevice::computePipeline(const Grap return finish(std::move(objects), nullptr); } -VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipeOut) { +VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipeOut, const std::array& threadGroup) { startJournalReplay(); state->recorder->BoundKeptBytes(); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); @@ -5049,6 +5079,10 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec Graphics::ShaderResources::MoveProof proof; auto moved = cached->MovedDispatchBuffers(shaders[0], state->recorder.get(), DispatchMoveBytes(), proof); moves.checked.fetch_add(1, std::memory_order_relaxed); + if (moved.has_value() && threadGroup != std::array{}) { + const auto data = cached->shaderDataAllocation(shaders[0]); + if (data.has_value() && std::ranges::any_of(*moved, [&](const Graphics::ShaderResources::MovedBuffer& entry) { return entry.allocation == *data; })) moved.reset(); + } if (moved.has_value() && !moved->empty()) { overlay = cached->PrepareDrawBindings(*state->recorder, *moved, true); if (overlay != nullptr && overlay->allocation.set == VK_NULL_HANDLE) overlay.reset(); @@ -5151,6 +5185,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec RecordedDispatch record{&context, &shaders[0], resources, objects, pushStages, &pushBytes, x, y, z, arguments, nullptr, programAddress, fromCache && TemplateDataRefresh() ? RecordedDispatch::DataRefresh::Words : RecordedDispatch::DataRefresh::None, 0, revalidateMs, &timer}; record.resourceKeyMatched = fromCache; record.bindings = overlay; + record.threadGroup = threadGroup; if (arguments != 0) decideIndirect(record, outcome, groupsText); // The indirect hold total below still covers the whole call of a CPU-resolved one. const bool indirect = record.arguments != 0 || outcome.cpuReason != 0; @@ -5225,7 +5260,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec return outcome; } -RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr& hit, IndirectOutcome& outcome, const std::shared_ptr& verify, bool refreshByWords) { +RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr& hit, IndirectOutcome& outcome, const std::shared_ptr& verify, bool refreshByWords, const std::array& threadGroup) { state->recorder->BoundKeptBytes(); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); PerformanceTimer timing("Vulkan.DispatchRecipe"); @@ -5321,6 +5356,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu if (dataRefresh == RecordedDispatch::DataRefresh::Words) counters.refreshByWords.fetch_add(1, std::memory_order_relaxed); RecordedDispatch record{&context, &shaders[0], hit->resources, hit->objects, recipe.pushes ? VkShaderStageFlags{VK_SHADER_STAGE_COMPUTE_BIT} : VkShaderStageFlags{0}, &recipe.pushBytes, x, y, z, arguments, nullptr, programAddress, dataRefresh, recipe.dataWordsHash, 0, &timer}; record.resourceKeyMatched = verify != nullptr; + record.threadGroup = threadGroup; if (arguments != 0) decideIndirect(record, outcome, groupsText); const bool indirect = record.arguments != 0 || outcome.cpuReason != 0; const auto recordStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index f4d473d7a5..706c1127c0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -200,7 +200,7 @@ public: // ever submitted); WaitSerial(serial) then covers all recorded work. std::uint64_t SubmitAndEpoch(); bool HasTimeline() const { return timeline != VK_NULL_HANDLE; } - bool RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments); + bool RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments, const std::array* threadGroup = nullptr); // Waits until every batch up to `serial` completed on the GPU. Called WITHOUT GuestMemory::GpuMutex: // it touches only the immutable timeline semaphore (the caller keeps the device alive). Requires // HasTimeline(); the debug switch APS5_NO_TIMELINE makes the device create the recorder without one. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 7b4aaa4858..53e3b2ed2e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -334,6 +334,9 @@ public: private: friend class AgcDriver::VulkanDevice; bool refreshData(VkCommandBuffer commands, const CompiledShader& shader, Recorder* recorder); + std::optional shaderDataAllocation(const CompiledShader& shader) const; + VkBuffer shaderDataBuffer(const CompiledShader& shader) const; + void forgetShaderDataWords(const CompiledShader& shader); struct DescribedRange { const char* kind; std::uint64_t address; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/DispatchArguments.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/DispatchArguments.comp index ed0b252561..2ebd51c40e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/shaders/DispatchArguments.comp +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/DispatchArguments.comp @@ -9,13 +9,29 @@ layout(push_constant) uniform Parameters { uint maxX; uint maxY; uint maxZ; + uint threadDimensions; + uint groupX; + uint groupY; + uint groupZ; uint unused; } parameters; +uint groups(uint threads, uint group) { + return threads / group + (threads % group != 0u ? 1u : 0u); +} + void main() { uint x = parameters.source.words[0]; uint y = parameters.source.words[1]; uint z = parameters.source.words[2]; + if (parameters.threadDimensions != 0u) { + parameters.arguments.words[3] = x; + parameters.arguments.words[4] = y; + parameters.arguments.words[5] = z; + x = groups(x, parameters.groupX); + y = groups(y, parameters.groupY); + z = groups(z, parameters.groupZ); + } bool valid = x <= parameters.maxX && y <= parameters.maxY && z <= parameters.maxZ; parameters.arguments.words[0] = valid ? x : 0u; parameters.arguments.words[1] = valid ? y : 0u; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 6655a45e82..289f6ff1cc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1976,8 +1976,8 @@ void Recorder::EndGpuTiming(std::uint32_t index, std::uint64_t bytes) { context.Function("vkCmdWriteTimestamp")(open->commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, open->queries, index * 2 + 1); } -bool Recorder::RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments) { - constexpr std::uint32_t PushBytes = 32; +bool Recorder::RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments, const std::array* threadGroup) { + constexpr std::uint32_t PushBytes = 48; if (dispatchArgumentState == 0) { dispatchArgumentState = -1; VkShaderModule module = VK_NULL_HANDLE; @@ -2013,11 +2013,15 @@ bool Recorder::RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress VkDeviceAddress source; VkDeviceAddress arguments; std::array limits; - } parameters{source, arguments, {limit[0], limit[1], limit[2], 0u}}; + std::array group; + } parameters{source, arguments, {limit[0], limit[1], limit[2], threadGroup != nullptr ? 1u : 0u}, {1u, 1u, 1u, 0u}}; + if (threadGroup != nullptr) std::copy(threadGroup->begin(), threadGroup->end(), parameters.group.begin()); static_assert(sizeof(parameters) == PushBytes); function(cmdPushConstants, "vkCmdPushConstants")(commands, dispatchArgumentLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, PushBytes, ¶meters); function(cmdDispatch, "vkCmdDispatch")(commands, 1, 1, 1); - recordBarrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_INDIRECT_COMMAND_READ_BIT); + const VkPipelineStageFlags readers = VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT | (threadGroup != nullptr ? VK_PIPELINE_STAGE_TRANSFER_BIT : 0u); + const VkAccessFlags reads = VK_ACCESS_INDIRECT_COMMAND_READ_BIT | (threadGroup != nullptr ? VK_ACCESS_TRANSFER_READ_BIT : 0u); + recordBarrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, readers, VK_ACCESS_SHADER_WRITE_BIT, reads); CountBarriers(CommandClass::DispatchLeading, 2); return true; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 56ba82e241..80fe7fde64 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4143,7 +4143,7 @@ std::size_t ShaderResources::addDataBuffer(std::span words) const auto size = words.size() * sizeof(std::uint32_t); Require(size <= context.limits.maxStorageBufferRange, "shader data buffer exceeds descriptor range limit"); const bool refreshable = TemplateDataRefresh() && size <= MaxRefreshBytes; - auto buffer = std::make_unique(context, size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | (refreshable ? VK_BUFFER_USAGE_TRANSFER_DST_BIT : 0u)); + auto buffer = std::make_unique(context, size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); std::memcpy(buffer->Bytes().data(), words.data(), size); Allocation allocation{0, size, false, std::move(buffer)}; if (refreshable) allocation.dataWords.assign(words.begin(), words.end()); @@ -4219,6 +4219,31 @@ bool ShaderResources::refreshData(VkCommandBuffer commands, const CompiledShader return recorded; } +std::optional ShaderResources::shaderDataAllocation(const CompiledShader& shader) const { + Require(shader.program != nullptr, "missing compiled shader"); + const auto& program = *shader.program; + if (program.bindings.size() != bindings.size()) return std::nullopt; + for (std::size_t index = 0; index < program.bindings.size(); ++index) { + if (program.bindings[index].role != ShaderRecompiler::DescriptorRole::ShaderData || bindings[index].allocations.size() != 1) continue; + const auto allocation = bindings[index].allocations.front(); + if (allocations[allocation].guest || allocations[allocation].buffer == nullptr) return std::nullopt; + return allocation; + } + return std::nullopt; +} + +VkBuffer ShaderResources::shaderDataBuffer(const CompiledShader& shader) const { + const auto allocation = shaderDataAllocation(shader); + return allocation.has_value() ? allocations[*allocation].buffer->Handle() : VK_NULL_HANDLE; +} + +void ShaderResources::forgetShaderDataWords(const CompiledShader& shader) { + if (const auto allocation = shaderDataAllocation(shader)) { + allocations[*allocation].dataWords.clear(); + rehashDataWords(); + } +} + void ShaderResources::writeDataWords(VkCommandBuffer commands, std::size_t allocation, std::span words) const { const auto& buffer = *allocations[allocation].buffer; const auto size = words.size() * sizeof(std::uint32_t); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/IndirectThreadDimensions.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectThreadDimensions.cpp new file mode 100644 index 0000000000..37573087cf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectThreadDimensions.cpp @@ -0,0 +1,214 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array AddressWriterCode{0x7e000202u, 0x7e020203u, 0x7e040204u, 0x7e060280u, 0xdc7c8000u, 0x00000003u, 0xbf810000u}; +alignas(256) std::array CounterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000281u, 0xe0c80000u, 0x80020000u, 0xbf810000u}; + +constexpr std::uint32_t ThreadDimensions = 0x8061u; +constexpr std::uint32_t GroupCounts = 0x8041u; +constexpr std::uint32_t Compute = 0x20u; +constexpr std::size_t GuestBytes = 1u << 20u; + +struct Slot { + alignas(256) std::array arguments{}; + alignas(256) std::array counter{}; + alignas(256) std::array counterDescriptor{}; + alignas(256) std::uint32_t done = 0; +}; + +struct Shape { + const char* name; + std::array threads; + std::array group; +}; + +constexpr std::array Shapes{{ + {"100x1x1 threads in groups of 64x1x1", {100u, 1u, 1u}, {64u, 1u, 1u}}, + {"5x3x2 threads in groups of 4x2x2", {5u, 3u, 2u}, {4u, 2u, 2u}}, + {"64x2x1 threads in groups of 32x1x1", {64u, 2u, 1u}, {32u, 1u, 1u}}, + {"0x7x1 threads in groups of 8x1x1", {0u, 7u, 1u}, {8u, 1u, 1u}}, +}}; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +std::array Descriptor(const void* pointer, std::uint32_t bytes) { + return {Low(pointer), High(pointer) & 0xffffu, bytes, 0x31016facu}; +} + +std::uint32_t Load(const std::uint32_t& value) { + return reinterpret_cast(value); +} + +std::uint32_t Expected(const Shape& shape) { + return shape.threads[0] * shape.threads[1] * shape.threads[2]; +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Program(std::vector& words, const void* code, std::array group, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (std::uint32_t axis = 0; axis < 3; ++axis) SetShaderRegister(words, 0x207u + axis, group[axis]); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); +} + +void Reset(Slot& slot) { + slot.arguments = {0xffffffffu, 0xffffffffu, 0xffffffffu, 0}; + slot.counter = {}; + slot.counterDescriptor = Descriptor(slot.counter.data(), sizeof(slot.counter)); + slot.done = 0; +} + +void Check(const Slot& slot, const Shape& shape, const std::string& name) { + const auto start = std::chrono::steady_clock::now(); + while (Load(slot.done) == 0) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(10), name + ": the completion label did not land"); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + Require(Load(slot.counter[0]) == Expected(shape), name + ": " + std::to_string(Load(slot.counter[0])) + " threads ran, expected " + std::to_string(Expected(shape))); +} + +void RunSubmitted(Slot& slot, const Shape& shape, bool producedOnGpu) { + Reset(slot); + if (!producedOnGpu) { + for (std::size_t axis = 0; axis < 3; ++axis) slot.arguments[axis] = shape.threads[axis]; + } + std::vector words; + if (producedOnGpu) { + Program(words, AddressWriterCode.data(), {1u, 1u, 1u}, {Low(slot.arguments.data()), High(slot.arguments.data()), shape.threads[0], shape.threads[1], shape.threads[2]}); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, GroupCounts}); + } + Program(words, CounterCode.data(), shape.group, {Low(slot.counterDescriptor.data()), High(slot.counterDescriptor.data())}); + words.insert(words.end(), {0xc0021102u, 1u, Low(slot.arguments.data()), High(slot.arguments.data())}); + words.insert(words.end(), {0xc0011600u, 0u, ThreadDimensions}); + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(&slot.done), High(&slot.done), 1u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(Compute, &packet) == 0, "compute submit failed"); + AgcDriverWaitIdle_nid_postfix(); + Check(slot, shape, std::string(shape.name) + (producedOnGpu ? ", arguments stored by the GPU" : ", arguments stored by the CPU")); +} + +ShaderRecompiler::RecompileResult Recompile(AgcDriver::VulkanDevice& device, std::span code, std::span userData, std::span memory, std::array group, std::array partialThreads, std::uint32_t pushBytes) { + const ShaderRecompiler::ShaderComputeStageInfo compute{group, 0, {false, false, false}, false, 1, partialThreads}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.ComputeTarget(32), + {0, 0, 0, pushBytes} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void RunCpuFallback(Slot& slot, const Shape& shape) { + AgcDriverShutdown_nid_postfix(); + Reset(slot); + const auto device = OpenVulkanTestDevice(); + Require(device != nullptr, "cannot open the CPU fallback test device"); + const std::array producerData{Low(slot.arguments.data()), High(slot.arguments.data()), shape.threads[0], shape.threads[1], shape.threads[2]}; + const std::array producerMemory{{{reinterpret_cast(AddressWriterCode.data()), std::as_bytes(std::span(AddressWriterCode))}}}; + const auto producer = Recompile(*device, AddressWriterCode, producerData, producerMemory, {1u, 1u, 1u}, {}, 128u); + const std::array consumerData{Low(slot.counterDescriptor.data()), High(slot.counterDescriptor.data())}; + const std::array consumerMemory{{ + {reinterpret_cast(CounterCode.data()), std::as_bytes(std::span(CounterCode))}, + {reinterpret_cast(slot.counterDescriptor.data()), std::as_bytes(std::span(slot.counterDescriptor))}, + }}; + const auto consumer = Recompile(*device, CounterCode, consumerData, consumerMemory, shape.group, {~0u, ~0u, ~0u}, 0u); + const auto preparedProducer = device->PrepareDispatch(producer, {}, 1, reinterpret_cast(AddressWriterCode.data())); + const auto preparedConsumer = device->PrepareDispatch(consumer, {}, 0, reinterpret_cast(CounterCode.data())); + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "the CPU fallback test has no active recorder"); + device->Dispatch(producer, 1, 1, 1, {}, reinterpret_cast(AddressWriterCode.data()), preparedProducer); + const auto labelAddress = reinterpret_cast(&slot.arguments[2]); + Require(device->WriteLabelOnGpu(labelAddress, std::as_bytes(std::span(&shape.threads[2], 1)), 1, Compute, false) == 0, "the argument label was not recorded on the GPU"); + const auto outcome = device->DispatchIndirect(consumer, reinterpret_cast(slot.arguments.data()), {}, reinterpret_cast(CounterCode.data()), preparedConsumer, nullptr, shape.group); + Require(outcome.cpuReason == 2, std::string(shape.name) + ": the indirect dispatch did not take the pending-label CPU fallback (reason " + std::to_string(outcome.cpuReason) + ")"); + recorder->Sync(); + slot.done = 1; + Check(slot, shape, std::string(shape.name) + ", read on the CPU behind a pending argument label"); +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, GuestBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* block = static_cast(std::aligned_alloc(GuestBytes, GuestBytes)); +#endif + Require(block != nullptr, "cannot allocate guest memory"); + GuestAllocations::Mutation().Add(block, GuestBytes, true, true, true); + auto* slot = new (block) Slot{}; + const bool gpuThreadDimensions = std::getenv("APS5_NO_GPU_THREAD_DIMENSIONS") == nullptr; + for (const auto& shape : Shapes) { + if (gpuThreadDimensions) RunSubmitted(*slot, shape, true); + RunSubmitted(*slot, shape, false); + std::cout << shape.name << ": passed\n"; + } + if (gpuThreadDimensions) { + for (const auto& shape : Shapes) { + RunCpuFallback(*slot, shape); + std::cout << shape.name << ", CPU fallback: passed\n"; + } + } + std::cout << "indirect thread dimension tests passed\n"; + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 222ee12c79..62fdf4e536 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -230,6 +230,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_GPU_INDIRECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp` | | reads indirect dispatch arguments on the CPU instead of letting the GPU count them | | `APS5_NO_GPU_INDIRECT_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | reads indirect draw arguments through the flush hook on the CPU instead of from the GPU | | `APS5_NO_GPU_SAMPLE_DUMPS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | dumps occlusion-counter samples on the CPU instead of through the GPU copy path | +| `APS5_NO_GPU_THREAD_DIMENSIONS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp` | | reads the thread counts of an indirect dispatch with USE_THREAD_DIMENSIONS on the CPU instead of turning them into group counts and a thread limit on the GPU | | `APS5_NO_HOLD_PROFILE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | turns off GpuMutex hold-time profiling, keeping only the wait timing | | `APS5_NO_HOOKSYNC_PROFILE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | leaves only the plain hook-sync counts under APS5_PROFILE_DRAW | | `APS5_NO_HOOK_COMPLETION_GUARD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets the flush hook record the accessing worker's queued labels even from inside a completion action | From 6dc7c9fbb2febea719d8f2b55f4cf791443b6e6a Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Mon, 19 Oct 2026 19:39:24 +0200 Subject: [PATCH 066/219] feat(libSceRazorCpu): run the data tag and logical file access calls without a capture --- core/libs/prx/libSceRazorCpu/Export.cpp | 17 +++++---- core/libs/tests/GuestRazorCpu.cpp | 48 +++++++++++++++++++++++++ docs/dev/debt/libSceRazorCpu.md | 3 ++ 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceRazorCpu/Export.cpp b/core/libs/prx/libSceRazorCpu/Export.cpp index 0714155d1b..af49b0fa3c 100644 --- a/core/libs/prx/libSceRazorCpu/Export.cpp +++ b/core/libs/prx/libSceRazorCpu/Export.cpp @@ -69,17 +69,16 @@ int APS5_VABI sceRazorCpuSync(void) { } void APS5_VABI sceRazorCpuDisableFiberUserMarkers(void) { - NotImplemented_nid_no_patch(__func__); } int APS5_VABI sceRazorCpuInitDataTags(void* buffer, size_t size) { (void)buffer; (void)size; - NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceRazorCpuShutdownDataTags(void) { - NotImplemented_nid_no_patch(__func__); + return 0; } size_t APS5_VABI sceRazorCpuGetDataTagStorageSize(uint32_t tagCount) { @@ -93,7 +92,7 @@ int APS5_VABI sceRazorCpuTagArray(const char* label, uint32_t category, void* ar (void)array; (void)bufferSize; (void)elementSize; - NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceRazorCpuTagBuffer(const char* label, uint32_t category, void* buffer, size_t bufferSize) { @@ -101,20 +100,20 @@ int APS5_VABI sceRazorCpuTagBuffer(const char* label, uint32_t category, void* b (void)category; (void)buffer; (void)bufferSize; - NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceRazorCpuResizeTaggedBuffer(uint32_t category, void* buffer, size_t newBufferSize) { (void)category; (void)buffer; (void)newBufferSize; - NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceRazorCpuUnTagBuffer(uint32_t category, void* buffer) { (void)category; (void)buffer; - NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceRazorCpuBeginLogicalFileAccess(const char* filename, uint64_t tag, uint64_t size, uint8_t operation) { @@ -122,11 +121,11 @@ int APS5_VABI sceRazorCpuBeginLogicalFileAccess(const char* filename, uint64_t t (void)tag; (void)size; (void)operation; - NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceRazorCpuEndLogicalFileAccess(void) { - NotImplemented_nid_no_patch(__func__); + return 0; } } diff --git a/core/libs/tests/GuestRazorCpu.cpp b/core/libs/tests/GuestRazorCpu.cpp index 19ac107f19..13572d5feb 100644 --- a/core/libs/tests/GuestRazorCpu.cpp +++ b/core/libs/tests/GuestRazorCpu.cpp @@ -1,6 +1,9 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include +#include +#include extern "C" { std::uint32_t APS5_VABI sceRazorCpuIsCapturing(void); @@ -15,6 +18,16 @@ int APS5_VABI sceRazorCpuFlushOccurred(std::uint64_t* timeSpentInFlush); int APS5_VABI sceRazorCpuPlotValue(const char* series, float value); int APS5_VABI sceRazorCpuWriteBookmark(const char* label, const char* description); int APS5_VABI sceRazorCpuSync(void); +void APS5_VABI sceRazorCpuDisableFiberUserMarkers(void); +int APS5_VABI sceRazorCpuInitDataTags(void* buffer, std::size_t size); +int APS5_VABI sceRazorCpuShutdownDataTags(void); +std::size_t APS5_VABI sceRazorCpuGetDataTagStorageSize(std::uint32_t tagCount); +int APS5_VABI sceRazorCpuTagArray(const char* label, std::uint32_t category, void* array, std::size_t bufferSize, std::size_t elementSize); +int APS5_VABI sceRazorCpuTagBuffer(const char* label, std::uint32_t category, void* buffer, std::size_t bufferSize); +int APS5_VABI sceRazorCpuResizeTaggedBuffer(std::uint32_t category, void* buffer, std::size_t newBufferSize); +int APS5_VABI sceRazorCpuUnTagBuffer(std::uint32_t category, void* buffer); +int APS5_VABI sceRazorCpuBeginLogicalFileAccess(const char* filename, std::uint64_t tag, std::uint64_t size, std::uint8_t operation); +int APS5_VABI sceRazorCpuEndLogicalFileAccess(void); } namespace { @@ -47,4 +60,39 @@ int main() { Require(sceRazorCpuSync() == 0); Require(sceRazorCpuSync() == 0); Require(sceRazorCpuIsCapturing() == 0); + + sceRazorCpuDisableFiberUserMarkers(); + sceRazorCpuDisableFiberUserMarkers(); + Require(sceRazorCpuPushMarker("thread", 0x80ffffffu, 0) == 0); + Require(sceRazorCpuPopMarker() == 0); + + Require(sceRazorCpuBeginLogicalFileAccess("data/level.pak", 0x0123456789abcdefull, 4096, 0) == 0); + Require(sceRazorCpuEndLogicalFileAccess() == 0); + Require(sceRazorCpuBeginLogicalFileAccess("save/slot0.bin", 0, 512, 1) == 0); + Require(sceRazorCpuEndLogicalFileAccess() == 0); + Require(sceRazorCpuEndLogicalFileAccess() == 0); + + unsigned char storage[256]; + std::memset(storage, 0x5a, sizeof(storage)); + int elements[16]{}; + unsigned char buffer[64]{}; + Require(sceRazorCpuInitDataTags(storage, sizeof(storage)) == 0); + Require(sceRazorCpuTagArray("elements", 7, elements, sizeof(elements), sizeof(elements[0])) == 0); + Require(sceRazorCpuTagBuffer("buffer", 7, buffer, sizeof(buffer)) == 0); + Require(sceRazorCpuResizeTaggedBuffer(7, buffer, sizeof(buffer) / 2) == 0); + Require(sceRazorCpuUnTagBuffer(7, buffer) == 0); + Require(sceRazorCpuUnTagBuffer(7, elements) == 0); + Require(sceRazorCpuShutdownDataTags() == 0); + for (const unsigned char byte : storage) Require(byte == 0x5a); + Require(sceRazorCpuTagBuffer("untracked", 3, buffer, sizeof(buffer)) == 0); + Require(sceRazorCpuUnTagBuffer(3, buffer) == 0); + Require(sceRazorCpuIsCapturing() == 0); + + bool threw = false; + try { + sceRazorCpuGetDataTagStorageSize(16); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); } diff --git a/docs/dev/debt/libSceRazorCpu.md b/docs/dev/debt/libSceRazorCpu.md index 30115eb282..3a4d1b15d4 100644 --- a/docs/dev/debt/libSceRazorCpu.md +++ b/docs/dev/debt/libSceRazorCpu.md @@ -4,6 +4,8 @@ - [sceRazorCpuPushMarker](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature (label, color, flags) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), the same as sceRazorCpuPushMarkerStatic's except that the label is copied; it does nothing without a capture and returns 0 - [sceRazorCpuSync](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - the `int(void)` signature and its meaning, an unnamed frame-boundary event, come from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L119-L122). Without a capture there is no event to record, so it does nothing and returns 0, like the other `sceRazorCpu*` markers here and the [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/razor_cpu/razor_cpu.cpp#L143-L146) stub. PRAGMATA (PPSA02530) continues on 0; the console's result with no capture attached is unverified +- [sceRazorCpuDisableFiberUserMarkers, sceRazorCpuBeginLogicalFileAccess and sceRazorCpuEndLogicalFileAccess](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures and meanings from SharpProspero ([fiber markers](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L73-L79), [logical file access](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L189-L209)): the first makes later markers carry the thread instead of the fiber, the other two attribute the thread's file reads and writes to a named logical file. Both only change what a capture records, so without one they do nothing, and the two file-access calls return 0 for any arguments. Whether the console checks the operation (0 read, 1 write) or an unmatched end without a capture is unverified +- [sceRazorCpuInitDataTags, sceRazorCpuShutdownDataTags, sceRazorCpuTagBuffer, sceRazorCpuTagArray, sceRazorCpuResizeTaggedBuffer and sceRazorCpuUnTagBuffer](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L130-L187), which documents them as the data sampler's buffer-tag table and the tags in it. Without a capture there is no data sampler, so they neither read the caller's storage nor keep the tags, and return 0 for any arguments; the console's errors for a tag before `sceRazorCpuInitDataTags`, an unknown buffer or a full table are unverified ### Unknown function info @@ -11,3 +13,4 @@ - [sceRazorCpuPushMarkerStatic, sceRazorCpuPopMarker](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from PPSA26344's calls; (name, color, flags) assumed for the push; both do nothing without a capture - [sceRazorCpuPlotValue, sceRazorCpuWriteBookmark](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures (series, float value) and (label, optional description) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), matching PPSA06320's calls; both do nothing without a capture and return 0 - [sceRazorCpuNamedSync](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - NID `XCuZoBSVFG8` is named by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/razor_cpu/razor_cpu.cpp); the NID and a stub returning 0 come from there, but its stub declares no arguments; the `const char* label` parameter (NUL-terminated, up to 16384 bytes) comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero), which documents it as the frame-boundary label. Without a capture it does nothing and returns 0; the label is not read, and the capture behavior and the real return values are unverified +- [sceRazorCpuGetDataTagStorageSize](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - throws: [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L142-L144) documents it as the byte size a tag table needs for a count of entries, but not the size of an entry From f50326165e0056faf614d3407eb35382c388e935 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:31:27 +0200 Subject: [PATCH 067/219] feat(shader): decode MIMG opcode 0xb8 as image_sample_c_o_a --- core/libs/tests/ImageSampleGather.cpp | 17 +++++++++++++++++ .../RdnaDecoder/src/RdnaImageOpDecoder.cpp | 1 + 2 files changed, 18 insertions(+) diff --git a/core/libs/tests/ImageSampleGather.cpp b/core/libs/tests/ImageSampleGather.cpp index 6fa633e3a7..3d3b40d608 100644 --- a/core/libs/tests/ImageSampleGather.cpp +++ b/core/libs/tests/ImageSampleGather.cpp @@ -72,8 +72,25 @@ static void CheckUnsupportedOpcodeDiagnostic(std::uint32_t encoding, std::string } Require(false); } +static void CheckSampleCompareOffsetAdjust() { + const std::array code{0xf0e00109u, 0x00801e33u}; + const RdnaInstruction instruction = DecodeRdnaMimg(0u, code, 0u); + Require(instruction.op == RdnaOpcode::ImageSample); + Require(instruction.imageOpcodeId == 0xb8u); + Require(instruction.imageSampleFlags == (RdnaImageSampleFlagCompare | RdnaImageSampleFlagOffset | RdnaImageSampleFlagAdjust)); + Require(instruction.imageAddressComponents == 4u); + Require(GetRdnaImageAddressComponentLayout(instruction.imageSampleFlags, 1u).bitOffset == 32u); + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + TranslationContext context(program, block, 256); + context.TranslateInstruction(instruction); + Require(program.Resources().memoryInfo.size() == 1u); + Require(program.Resources().memoryInfo.back().imageSampleFlags == instruction.imageSampleFlags); +} int main() { CheckSamplerVccTuple(); + CheckSampleCompareOffsetAdjust(); CheckSamplerVccTupleOverflow(); Check(0x26u, RdnaOpcode::ImageSampleBCl, 0xfu); Check(0x36u, RdnaOpcode::ImageSampleBClO, 0xfu); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp index f64c3c6bd7..e3be618395 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp @@ -70,6 +70,7 @@ constexpr ImageOpcodeInfo imageOpcodes[] = { {0xa5u, RdnaOpcode::ImageSample, "image_sample_b_a", RdnaImageSampleFlagBias | RdnaImageSampleFlagAdjust, true, false, false}, {0xa8u, RdnaOpcode::ImageSample, "image_sample_c_a", RdnaImageSampleFlagCompare | RdnaImageSampleFlagAdjust, true, false, false}, {0xb0u, RdnaOpcode::ImageSample, "image_sample_o_a", RdnaImageSampleFlagOffset | RdnaImageSampleFlagAdjust, true, false, false}, + {0xb8u, RdnaOpcode::ImageSample, "image_sample_c_o_a", RdnaImageSampleFlagCompare | RdnaImageSampleFlagOffset | RdnaImageSampleFlagAdjust, true, false, false}, {0x3cu, RdnaOpcode::ImageSampleCLO, nullptr, RdnaImageSampleFlagCompare | RdnaImageSampleFlagLod | RdnaImageSampleFlagOffset, true, false, false}, {0x3fu, RdnaOpcode::ImageSampleCLzO, nullptr, RdnaImageSampleFlagCompare | RdnaImageSampleFlagLevelZero | RdnaImageSampleFlagOffset, true, false, false}, {0x3au, RdnaOpcode::ImageSampleCDO, nullptr, RdnaImageSampleFlagCompare | RdnaImageSampleFlagDerivative | RdnaImageSampleFlagOffset, true, false, false}, From ce81a77d1a21ff534a0ee9db24fecb2805b6c0ed Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:40:53 +0200 Subject: [PATCH 068/219] fix(libc): avoid premature scaled hyperbolic overflow --- core/libs/prx/libc/src/Math.cpp | 36 ++++++++++++- core/libs/tests/GuestMath.cpp | 95 +++++++++++++++++++++++++++++++++ docs/dev/debt/libc.md | 2 + 3 files changed, 131 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 5ae2fdf949..73284ae488 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -210,6 +210,38 @@ float GuestBesselSecondKindZero() { return -std::numeric_limits::infinity(); } +float ScaledHyperbolic(float x, float y, bool sine) { + const auto xBits = std::bit_cast(x); + const auto xMagnitude = xBits & 0x7fffffffu; + const auto yMagnitude = std::bit_cast(y) & 0x7fffffffu; + constexpr std::uint32_t infinity = 0x7f800000u; + if (xMagnitude > infinity) return x; + if (yMagnitude > infinity) return y; + if ((xMagnitude == infinity && yMagnitude == 0) || (sine && xMagnitude == 0 && yMagnitude == infinity)) { + errno = EDOM; + std::feraiseexcept(FE_INVALID); + return std::numeric_limits::quiet_NaN(); + } + if (xMagnitude == infinity) return sine ? x * y : std::copysign(std::numeric_limits::infinity(), y); + if (xMagnitude == 0 || yMagnitude == 0 || yMagnitude == infinity) return sine ? x * y : y; + constexpr double limit = 2.0 * std::numeric_limits::max_exponent; + const float magnitude = std::bit_cast(xMagnitude); + const double argument = magnitude < limit ? static_cast(magnitude) : limit; + const double value = static_cast(y) * (sine ? std::sinh(argument) : std::cosh(argument)); + const bool exponential = magnitude >= (sine ? 7.7f : 8.675f); + float result; + if (exponential && std::fabs(value) >= 0x1p128) { + result = std::copysign(std::numeric_limits::infinity(), y); + } else { + result = static_cast(value); + } + if (exponential && std::isinf(result)) { + errno = ERANGE; + std::feraiseexcept(FE_OVERFLOW | FE_INEXACT); + } + return sine && (xBits & 0x80000000u) != 0 ? -result : result; +} + } extern "C" { @@ -581,8 +613,8 @@ int APS5_VABI ilogbf_nid_postfix(float x) { return ilogb_nid_postfix(static_cast double APS5_VABI remainder_nid_postfix(double x, double y) { return std::remainder(x, y); } double APS5_VABI remquo_nid_postfix(double x, double y, int* quo) { return std::remquo(x, y, quo); } float APS5_VABI remquof_nid_postfix(float x, float y, int* quo) { return std::remquo(x, y, quo); } -float APS5_VABI _FSinh_nid_postfix(float x, float y) { return y * std::sinh(x); } -float APS5_VABI _FCosh_nid_postfix(float x, float y) { return y * std::cosh(x); } +float APS5_VABI _FSinh_nid_postfix(float x, float y) { return ScaledHyperbolic(x, y, true); } +float APS5_VABI _FCosh_nid_postfix(float x, float y) { return ScaledHyperbolic(x, y, false); } struct alignas(16) LibcFloatConstant { std::uint32_t bits[4]; }; LibcFloatConstant _FInf_nid_postfix {{0x7f800000u, 0, 0, 0}}; diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 98eb2d3d10..acafbab1f1 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -116,6 +116,8 @@ double APS5_VABI sinh_nid_postfix(double); double APS5_VABI cosh_nid_postfix(double); float APS5_VABI coshf_nid_postfix(float); float APS5_VABI sinhf_nid_postfix(float); +float APS5_VABI _FSinh_nid_postfix(float, float); +float APS5_VABI _FCosh_nid_postfix(float, float); double APS5_VABI asinh_nid_postfix(double); float APS5_VABI asinhf_nid_postfix(float); double APS5_VABI acosh_nid_postfix(double); @@ -696,6 +698,98 @@ static void CheckHyperbolic() { Require(Near(acosh_nid_postfix(1e300), 691.4686750787736, 1e-15)); } +static void CheckScaledHyperbolic() { + const int rounding = std::fegetround(); + Require(std::fesetround(FE_TONEAREST) == 0); + unsigned checks = 0; + for (bool sine : {true, false}) { + const auto function = sine ? _FSinh_nid_postfix : _FCosh_nid_postfix; + const char* name = sine ? "_FSinh" : "_FCosh"; + const auto checkValue = [&](float x, float y) { + const long double value = sine ? std::sinh(static_cast(x)) : std::cosh(static_cast(x)); + const float expected = static_cast(value * static_cast(y)); + std::feclearexcept(FE_ALL_EXCEPT); + errno = 12345; + const float result = function(x, y); + const auto actualBits = std::bit_cast(result); + const auto expectedBits = std::bit_cast(expected); + const auto distance = actualBits > expectedBits ? actualBits - expectedBits : expectedBits - actualBits; + const bool exact = expected == 0.0f || std::isinf(expected); + if ((exact ? actualBits != expectedBits : !std::isfinite(result) || distance > 2) || + (std::isfinite(expected) && (std::fetestexcept(FE_OVERFLOW | FE_INVALID) != 0 || errno != 12345))) { + std::fprintf(stderr, "%s(%a, %a): got %08x, expected %08x, errno %d, flags %x\n", + name, static_cast(x), static_cast(y), actualBits, expectedBits, errno, std::fetestexcept(FE_ALL_EXCEPT)); + std::abort(); + } + ++checks; + }; + checkValue(90.0f, 1e-20f); + checkValue(100.0f, 1e-40f); + const float magnitudes[] = {0.0f, 0x1p-126f, 0.001f, 0.5f, 1.0f, 4.0f, 7.0f, 8.0f, 10.0f, 20.0f, 50.0f, 88.0f, 90.0f, 100.0f, 150.0f, 180.0f, 190.0f, 193.0f}; + const float scales[] = {0.0f, 0x1p-149f, 0x1p-140f, 0x1p-126f, 1e-20f, 0.5f, 1.0f, 1e20f, std::numeric_limits::max()}; + for (float magnitude : magnitudes) { + for (float scale : scales) { + for (float xSign : {1.0f, -1.0f}) { + for (float ySign : {1.0f, -1.0f}) checkValue(magnitude * xSign, scale * ySign); + } + } + } + for (float scale : scales) { + if (scale == 0.0f) continue; + for (long double target : {static_cast(std::numeric_limits::max()), 0x1p-150L}) { + if (!sine && target < scale) continue; + const long double ratio = target / static_cast(scale); + const float boundary = static_cast(sine ? std::asinh(ratio) : std::acosh(ratio)); + float magnitude = std::nextafter(std::nextafter(boundary, -INFINITY), -INFINITY); + for (unsigned neighbor = 0; neighbor != 5; ++neighbor) { + for (float xSign : {1.0f, -1.0f}) { + for (float ySign : {1.0f, -1.0f}) checkValue(magnitude * xSign, scale * ySign); + } + magnitude = std::nextafter(magnitude, INFINITY); + } + } + } + const auto checkSpecial = [&](float x, float y, std::uint32_t expectedBits, int error, int flags) { + std::feclearexcept(FE_ALL_EXCEPT); + std::feraiseexcept(FE_DIVBYZERO); + errno = 12345; + const float result = function(x, y); + const int raised = std::fetestexcept(FE_ALL_EXCEPT); + if (std::bit_cast(result) != expectedBits || errno != error || raised != (FE_DIVBYZERO | flags)) { + std::fprintf(stderr, "%s special (%a, %a): got %08x, expected %08x, errno %d, flags %x\n", + name, static_cast(x), static_cast(y), std::bit_cast(result), expectedBits, errno, raised); + std::abort(); + } + ++checks; + }; + const float infinity = std::numeric_limits::infinity(); + for (float sign : {1.0f, -1.0f}) { + checkSpecial(sign * infinity, 0.0f, 0x7fc00000u, EDOM, FE_INVALID); + checkSpecial(sign * 100.0f, 0.0f, sine && sign < 0 ? 0x80000000u : 0u, 12345, 0); + checkSpecial(sign * 100.0f, -0.0f, sine && sign < 0 ? 0u : 0x80000000u, 12345, 0); + checkSpecial(sign * 1.0f, infinity, sine && sign < 0 ? 0xff800000u : 0x7f800000u, 12345, 0); + } + for (std::uint32_t bits : {0x7fc12345u, 0xffc54321u, 0x7fa12345u, 0xffa54321u}) { + const float nan = std::bit_cast(bits); + checkSpecial(nan, 0.0f, bits, 12345, 0); + checkSpecial(infinity, nan, bits, 12345, 0); + checkSpecial(nan, std::bit_cast(0x7fc99999u), bits, 12345, 0); + } + checkSpecial(0.0f, infinity, sine ? 0x7fc00000u : 0x7f800000u, sine ? EDOM : 12345, sine ? FE_INVALID : 0); + checkSpecial(1.0f, std::numeric_limits::max(), 0x7f800000u, 12345, FE_OVERFLOW | FE_INEXACT); + checkSpecial(100.0f, 1.0f, 0x7f800000u, ERANGE, FE_OVERFLOW | FE_INEXACT); + for (int mode : {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}) { + Require(std::fesetround(mode) == 0); + checkSpecial(std::numeric_limits::max(), 0x1p-149f, 0x7f800000u, ERANGE, FE_OVERFLOW | FE_INEXACT); + } + Require(std::fesetround(FE_TONEAREST) == 0); + if (sine) checkSpecial(0x1p-126f, 0x1p-149f, 0u, 12345, FE_UNDERFLOW | FE_INEXACT); + } + std::feclearexcept(FE_ALL_EXCEPT); + Require(std::fesetround(rounding) == 0); + std::printf("Scaled hyperbolic checks: %u\n", checks); +} + static void CheckSinhf() { const float infinity = std::numeric_limits::infinity(); const struct { std::uint32_t input; std::uint32_t output; } cases[] = { @@ -1257,6 +1351,7 @@ int main() { Require(std::isinf(__powidf2_nid_postfix(-0.0, -3)) && std::signbit(__powidf2_nid_postfix(-0.0, -3))); Require(__powidf2_nid_postfix(2.0, -127) == 0x1p-127); CheckHyperbolic(); + CheckScaledHyperbolic(); CheckSinhf(); CheckAtanhf(); CheckHypotSpecialValue(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 17eb3d7260..c58294d3d0 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -52,6 +52,8 @@ ### Functional +- [_FSinh and _FCosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) finite results are not verified bit for bit against the console: the scaled product uses the host's binary64 hyperbolic function before rounding to binary32, rather than the float rational and `_FExp` sequence in PRAGMATA's bundled `libc.prx`. NaN preference and payloads, signed zeros, infinite operands and the range-error thresholds follow that binary's instructions; the system `libSceLibcInternal` implementation and nondefault flush-to-zero modes are unverified. + - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. - [Guest time conversions](../../../core/libs/prx/libc/src/Time.cpp) rewrite the host process `TZ` when a guest changes its own `TZ`, so other host consumers of that variable (including libSceRtc local time) shift too; code outside libc does not take the zone lock. Zone syntax follows the host C runtime, not FreeBSD: Windows only accepts the `std offset [dst]` form, treats an empty guest `TZ` as `UTC0` and reads `:Europe/Berlin` as an abbreviation (for example `rop`); only Linux honours zone file names. If the guest accepts a `TZ` value that the host rejects (on Windows, for example, one longer than the host environment limit), conversions throw `std::runtime_error`. A `tm_zone` saved before a `TZ` change can refer to storage now holding the new zone name, changing `%Z` output. From 4513d471049478e3264419135617db4f5b94a3fe Mon Sep 17 00:00:00 2001 From: ionut27001 <194206737+ionut27001@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:40:23 +0300 Subject: [PATCH 069/219] perf(agc): keep released image memory in the buffer pool for reuse by type and size An image's own VkDeviceMemory was allocated and freed one at a time, and in a scene that re-creates its storage images at every dynamic-resolution step the dispatching queue worker spent half of its image-table resolution freeing evicted images and most of the rest allocating new ones. The pool now keeps the device memory of a released storage or sampled image under its own budget (APS5_IMAGE_POOL_MIB, a quarter of the device tier's budget by default) and hands it to the next image of the same memory type and size. --- .../Graphics/include/BufferPool.hpp | 29 +++- .../Graphics/include/Texture.hpp | 9 +- .../Graphics/src/BufferPool.cpp | 137 +++++++++++++++++- .../libSceAgcDriver/Graphics/src/Texture.cpp | 25 +++- .../prx/libSceAgcDriver/tests/BufferPool.cpp | 109 ++++++++++++++ docs/dev/ENVIRONMENT.md | 3 +- 6 files changed, 290 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index beec495e48..4aa1ecd08a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_BUFFERPOOL_HPP #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include #include #include #include @@ -24,6 +25,7 @@ struct BufferAllocation { VkDeviceSize offset = 0; bool slab = false; std::size_t bufferBytes = 0; + std::uint32_t memoryType = 0; }; struct SlabSlot { @@ -63,6 +65,10 @@ public: static std::size_t Capacity(std::size_t bytes, VkMemoryPropertyFlags properties); static VkBufferUsageFlags Usage(VkBufferUsageFlags usage, VkMemoryPropertyFlags properties); VkDeviceMemory AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType = std::nullopt, std::uint32_t* allocatedType = nullptr); + static VkDeviceSize ImageBudget(VkDeviceSize deviceBudget); + VkDeviceMemory TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType = std::nullopt, std::uint32_t* allocatedType = nullptr); + void PutImageMemory(VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept; + std::size_t RetainedImages(); std::optional Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); void Put(const BufferAllocation& allocation) noexcept; static bool SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom); @@ -95,11 +101,12 @@ private: std::size_t bytes; VkBufferUsageFlags usage; VkMemoryPropertyFlags properties; + std::uint32_t memoryType = 0; bool operator==(const SlotKey&) const = default; }; struct SlotKeyHash { std::size_t operator()(const SlotKey& key) const noexcept { - return std::hash{}(key.bytes) ^ (static_cast(key.usage) << 32u) ^ (static_cast(key.properties) << 48u); + return std::hash{}(key.bytes) ^ (static_cast(key.usage) << 32u) ^ (static_cast(key.properties) << 48u) ^ (static_cast(key.memoryType) << 56u); } }; // One retention tier: its slots by key (each list oldest first), their count and bytes, the @@ -127,6 +134,9 @@ private: // caller destroys those after releasing the mutex, so builds taking buffers on other threads // do not wait behind the Vulkan destroy calls. Nothing changes when the vector cannot grow. void evictOldest(Tier& tier, std::vector& evicted); + bool evictOldestImage(std::vector& evicted); + bool imagePooled(VkDeviceSize bytes, std::uint32_t memoryType) const; + bool heapOverBudget(); // The retained-slot bound of each tier (APS5_BUFFER_POOL_SLOTS, default `defaultSlots`), read once. static std::size_t MaxSlots(); std::size_t maxSlots; @@ -137,14 +147,22 @@ private: PFN_vkFreeMemory freeMemory; PFN_vkAllocateMemory allocateMemory; PFN_vkMapMemory mapMemory; - VkPhysicalDevice physical; - VkPhysicalDeviceMemoryProperties memoryProperties; - PFN_vkGetPhysicalDeviceMemoryProperties2 memoryProperties2; + VkPhysicalDevice physical = VK_NULL_HANDLE; + VkPhysicalDeviceMemoryProperties memoryProperties{}; + PFN_vkGetPhysicalDeviceMemoryProperties2 memoryProperties2 = nullptr; std::mutex mutex; // Not `small`/`large`: (via ) defines `small` as a macro. Tier smallTier; Tier largeTier; Tier deviceTier; + std::chrono::steady_clock::time_point heapRead{}; + bool heapOver = false; + VkDeviceSize imageBudget = 0; + VkDeviceSize imageBytes = 0; + std::size_t imageSlots = 0; + std::uint64_t imageHits = 0; + std::uint64_t imageMisses = 0; + std::uint64_t imageEvictions = 0; std::uint64_t clock = 0; std::mutex slabMutex; std::unordered_map slabs; @@ -160,7 +178,8 @@ private: }; std::shared_ptr GetBufferPool(const Context& context); -VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation); +VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, std::uint32_t* allocatedType = nullptr); +void ReleaseImageMemory(const Context& context, VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 60e1af575d..dcbeaccadd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -46,13 +46,13 @@ struct OwnedImage { OwnedImage(const Context& context, VkImage image, VkDeviceMemory memory) : context(context), image(image), memory(memory) {} OwnedImage(const OwnedImage&) = delete; OwnedImage& operator=(const OwnedImage&) = delete; - ~OwnedImage() { - if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); - if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); - } + ~OwnedImage(); + void Allocate(const Context& device, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation); Context context; VkImage image; VkDeviceMemory memory; + VkDeviceSize memoryBytes = 0; + std::uint32_t memoryType = 0; }; class Texture { @@ -591,6 +591,7 @@ private: VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; VkDeviceSize memoryBytes = 0; + std::uint32_t memoryType = 0; VkImageView view = VK_NULL_HANDLE; std::uint32_t defaultMip = 0; std::map extraViews; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index 0fe017487d..54f7104c56 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -42,6 +42,10 @@ BufferPool::BufferPool(const Context& context) : maxSlots(std::max( smallTier.budget = smallBudget; largeTier.budget = budget; deviceTier.budget = deviceBudget(context); + memoryProperties = context.memory; + physical = context.physical; + memoryProperties2 = context.memoryProperties2; + imageBudget = ImageBudget(deviceTier.budget); } BufferPool::~BufferPool() { @@ -61,7 +65,7 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation unsigned attempts = 1; VkDeviceSize reclaimed = 0; VkDeviceSize released = 0; - const auto exhausted = [](VkResult value) { return value == VK_ERROR_OUT_OF_DEVICE_MEMORY || value == VK_ERROR_OUT_OF_HOST_MEMORY; }; + const auto exhausted = [](VkResult value) { return value == VK_ERROR_OUT_OF_DEVICE_MEMORY || value == VK_ERROR_OUT_OF_HOST_MEMORY || value == VK_ERROR_TOO_MANY_OBJECTS; }; if (exhausted(result)) { reclaimed = trim(); if (reclaimed != 0) { @@ -73,6 +77,7 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation if (exhausted(result)) { if (cacheReclaim != nullptr) released = cacheReclaim(ReclaimTarget(allocation.allocationSize)); if (released != 0) { + reclaimed += trim(); memory = VK_NULL_HANDLE; result = allocateMemory(device, &allocation, nullptr, &memory); ++attempts; @@ -115,6 +120,91 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation return VK_NULL_HANDLE; } +VkDeviceSize BufferPool::ImageBudget(VkDeviceSize deviceBudget) { + static const char* value = std::getenv("APS5_IMAGE_POOL_MIB"); + if (value != nullptr) return static_cast(std::strtoull(value, nullptr, 10)) << 20u; + return deviceBudget / 4u; +} + +bool BufferPool::imagePooled(VkDeviceSize bytes, std::uint32_t memoryType) const { + return imageBudget != 0 && bytes != 0 && bytes <= imageBudget && bytes <= deviceTier.budget && memoryType < memoryProperties.memoryTypeCount && DeviceTiered(memoryProperties.memoryTypes[memoryType].propertyFlags); +} + +bool BufferPool::heapOverBudget() { + if (memoryProperties2 == nullptr || physical == VK_NULL_HANDLE) return false; + const auto now = std::chrono::steady_clock::now(); + if (heapRead != std::chrono::steady_clock::time_point{} && now - heapRead < std::chrono::milliseconds(100)) return heapOver; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2}; + properties.pNext = &reported; + memoryProperties2(physical, &properties); + heapRead = now; + heapOver = false; + for (std::uint32_t heap = 0; heap < memoryProperties.memoryHeapCount && heap < VK_MAX_MEMORY_HEAPS; ++heap) { + if ((memoryProperties.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && reported.heapBudget[heap] != 0 && reported.heapUsage[heap] > reported.heapBudget[heap]) heapOver = true; + } + return heapOver; +} + +VkDeviceMemory BufferPool::TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType, std::uint32_t* allocatedType) { + const auto type = allocation.memoryTypeIndex; + if (imagePooled(allocation.allocationSize, type)) { + std::lock_guard lock(mutex); + ++clock; + const auto found = deviceTier.free.find({static_cast(allocation.allocationSize), 0, memoryProperties.memoryTypes[type].propertyFlags, type}); + if (found != deviceTier.free.end() && !found->second.empty()) { + const auto memory = found->second.back().allocation.memory; + found->second.pop_back(); + if (found->second.empty()) deviceTier.free.erase(found); + --deviceTier.slots; + deviceTier.retainedBytes -= allocation.allocationSize; + --imageSlots; + imageBytes -= allocation.allocationSize; + ++imageHits; + if (allocatedType != nullptr) *allocatedType = type; + return memory; + } + ++imageMisses; + } + return AllocateMemory(allocation, operation, hostType, allocatedType); +} + +void BufferPool::PutImageMemory(VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept { + if (memory == VK_NULL_HANDLE) return; + if (!imagePooled(bytes, memoryType)) { + freeMemory(device, memory, nullptr); + return; + } + BufferAllocation allocation{VK_NULL_HANDLE, memory, nullptr, 0, bytes, static_cast(bytes), 0, memoryProperties.memoryTypes[memoryType].propertyFlags}; + allocation.memoryType = memoryType; + std::vector evicted; + try { + std::lock_guard lock(mutex); + if (heapOverBudget()) { + while (imageSlots != 0 && evictOldestImage(evicted)) { + } + evicted.push_back(allocation); + } else { + while (imageSlots != 0 && imageBytes + bytes > imageBudget && evictOldestImage(evicted)) { + } + while (deviceTier.slots != 0 && (deviceTier.retainedBytes + bytes > deviceTier.budget || deviceTier.slots >= maxSlots)) evictOldest(deviceTier, evicted); + deviceTier.free[{allocation.bytes, 0, allocation.properties, memoryType}].push_back({allocation, ++clock}); + ++deviceTier.slots; + deviceTier.retainedBytes += bytes; + ++imageSlots; + imageBytes += bytes; + } + } catch (...) { + destroy(allocation); + } + for (const auto& gone : evicted) destroy(gone); +} + +std::size_t BufferPool::RetainedImages() { + std::lock_guard lock(mutex); + return imageSlots; +} + VkDeviceSize BufferPool::ReclaimTarget(VkDeviceSize bytes) { return bytes + std::max(bytes, VkDeviceSize{256} << 20u); } @@ -256,7 +346,7 @@ VkDeviceSize BufferPool::destroy(const BufferAllocation& allocation) noexcept { } // Device-local allocations (see DeviceBuffer) are never mapped. if (allocation.mapping != nullptr) unmap(device, allocation.memory); - destroyBuffer(device, allocation.buffer, nullptr); + if (allocation.buffer != VK_NULL_HANDLE) destroyBuffer(device, allocation.buffer, nullptr); freeMemory(device, allocation.memory, nullptr); return allocation.allocationBytes; } @@ -297,7 +387,7 @@ std::optional BufferPool::Take(std::size_t bytes, VkBufferUsag const auto now = std::chrono::steady_clock::now(); if (now - lastReport > std::chrono::seconds(10)) { lastReport = now; - AgcDriver::ProfilePrint_nid_no_patch("[bufferpool] small: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB); large: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB)%s; device: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB of %llu)\n", static_cast(smallTier.hits), static_cast(smallTier.misses), static_cast(smallTier.evictions), smallTier.slots, smallTier.retainedBytes / 1048576.0, static_cast(largeTier.hits), static_cast(largeTier.misses), static_cast(largeTier.evictions), largeTier.slots, largeTier.retainedBytes / 1048576.0, sharedTiers() ? " (shared)" : "", static_cast(deviceTier.hits), static_cast(deviceTier.misses), static_cast(deviceTier.evictions), deviceTier.slots, deviceTier.retainedBytes / 1048576.0, static_cast(deviceTier.budget >> 20u)); + AgcDriver::ProfilePrint_nid_no_patch("[bufferpool] small: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB); large: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB)%s; device: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB of %llu); images: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB of %llu)\n", static_cast(smallTier.hits), static_cast(smallTier.misses), static_cast(smallTier.evictions), smallTier.slots, smallTier.retainedBytes / 1048576.0, static_cast(largeTier.hits), static_cast(largeTier.misses), static_cast(largeTier.evictions), largeTier.slots, largeTier.retainedBytes / 1048576.0, sharedTiers() ? " (shared)" : "", static_cast(deviceTier.hits), static_cast(deviceTier.misses), static_cast(deviceTier.evictions), deviceTier.slots, deviceTier.retainedBytes / 1048576.0, static_cast(deviceTier.budget >> 20u), static_cast(imageHits), static_cast(imageMisses), static_cast(imageEvictions), imageSlots, imageBytes / 1048576.0, static_cast(imageBudget >> 20u)); } } auto& tier = tierFor(capacity, properties); @@ -322,12 +412,37 @@ void BufferPool::evictOldest(Tier& tier, std::vector& evicted) // First: a throw here leaves the slot retained and counted. evicted.push_back(oldest->second.front().allocation); tier.retainedBytes -= oldest->second.front().allocation.allocationBytes; + if (oldest->second.front().allocation.buffer == VK_NULL_HANDLE) { + imageBytes -= oldest->second.front().allocation.allocationBytes; + --imageSlots; + ++imageEvictions; + } oldest->second.pop_front(); if (oldest->second.empty()) tier.free.erase(oldest); --tier.slots; ++tier.evictions; } +bool BufferPool::evictOldestImage(std::vector& evicted) { + auto oldest = deviceTier.free.end(); + for (auto it = deviceTier.free.begin(); it != deviceTier.free.end(); ++it) { + if (it->second.front().allocation.buffer != VK_NULL_HANDLE) continue; + if (oldest == deviceTier.free.end() || it->second.front().lastUse < oldest->second.front().lastUse) oldest = it; + } + if (oldest == deviceTier.free.end()) return false; + evicted.push_back(oldest->second.front().allocation); + const auto bytes = oldest->second.front().allocation.allocationBytes; + deviceTier.retainedBytes -= bytes; + imageBytes -= bytes; + oldest->second.pop_front(); + if (oldest->second.empty()) deviceTier.free.erase(oldest); + --deviceTier.slots; + --imageSlots; + ++deviceTier.evictions; + ++imageEvictions; + return true; +} + std::size_t BufferPool::MaxSlots() { // APS5_BUFFER_POOL_SLOTS= bounds the retained allocations; 64 is the capacity the pool had // before least-recently-used retention, for A/B runs. @@ -351,6 +466,9 @@ VkDeviceSize BufferPool::trim() { tier->retainedBytes = 0; tier->slots = 0; } + imageEvictions += imageSlots; + imageBytes = 0; + imageSlots = 0; } VkDeviceSize bytes = 0; for (const auto* tier : {&retiredSmall, &retiredLarge, &retiredDevice}) { @@ -403,8 +521,17 @@ std::shared_ptr GetBufferPool(const Context& context) { return context.bufferPool; } -VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation) { - return GetBufferPool(context)->AllocateMemory(allocation, operation, context.RefusedDeviceMemoryFallback(mask, allocation.memoryTypeIndex)); +VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, std::uint32_t* allocatedType) { + return GetBufferPool(context)->TakeImageMemory(allocation, operation, context.RefusedDeviceMemoryFallback(mask, allocation.memoryTypeIndex), allocatedType); +} + +void ReleaseImageMemory(const Context& context, VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept { + if (memory == VK_NULL_HANDLE) return; + if (context.bufferPool) { + context.bufferPool->PutImageMemory(memory, bytes, memoryType); + return; + } + context.Function("vkFreeMemory")(context.device, memory, nullptr); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index d7a68e3704..add15d9dd0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -40,6 +40,17 @@ namespace AgcDriver::Graphics { +OwnedImage::~OwnedImage() { + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + ReleaseImageMemory(context, memory, memoryBytes, memoryType); +} + +void OwnedImage::Allocate(const Context& device, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation) { + memory = AllocateImageMemory(device, allocation, mask, operation, &memoryType); + memoryBytes = allocation.allocationSize; + context.bufferPool = device.bufferPool; +} + namespace { // APS5_PROFILE_DRAW: accumulate texture setup phases and report every 200 textures. @@ -268,7 +279,7 @@ ExpandImage CreateExpandImage(const Context& context, const GuestTextureResource VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture"); - result.owned->memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory sRGB copy"); + result.owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory sRGB copy"); Check(context.Function("vkBindImageMemory")(context.device, result.image, result.owned->memory, 0), "vkBindImageMemory sRGB copy"); result.bytes = requirements.size; return result; @@ -350,7 +361,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture"); - owned->memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory texture"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory texture"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory"); const VkImage uploadImage = image; @@ -626,7 +637,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - owned->memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory repacked copy"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory repacked copy"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory repacked copy"); const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; @@ -684,7 +695,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - owned->memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory block copy"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory block copy"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory block copy"); const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; @@ -741,7 +752,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - owned->memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth copy"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth copy"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory depth copy"); const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; @@ -1409,7 +1420,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, allocation.allocationSize = requirements.size; memoryBytes = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "storage texture"); - memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory storage texture"); + memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory storage texture", &memoryType); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory storage"); } catch (...) { release(); @@ -5487,7 +5498,7 @@ void StorageTexture::release() noexcept { if (proxyMemory) context.Function("vkFreeMemory")(context.device, proxyMemory, nullptr); if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); - if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + ReleaseImageMemory(context, memory, memoryBytes, memoryType); } std::uint64_t StorageTexture::GuestBytes() const { diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index e089fa2a6a..abdf8f0166 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -3,6 +3,7 @@ #include "AllocationFailure.hpp" #include "SlabReturn.hpp" #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #include #include +#include #include using namespace AgcDriver::Graphics; @@ -895,6 +897,110 @@ void DeviceBindingFailures() { } +VkDeviceMemory reclaimImage = VK_NULL_HANDLE; +const Context* reclaimImageContext = nullptr; + +VkDeviceSize mockImageReclaim(VkDeviceSize) { + if (reclaimImage == VK_NULL_HANDLE) return 0; + ReleaseImageMemory(*reclaimImageContext, reclaimImage, 10 * MiB, 0); + reclaimImage = VK_NULL_HANDLE; + return 10 * MiB; +} + +void ImageMemoryReclaim() { + mock = MockDevice{}; + auto context = mockContext(); + context.cacheReclaim = mockImageReclaim; + reclaimImageContext = &context; + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = 10 * MiB; + allocation.memoryTypeIndex = 0; + std::uint32_t type = 0; + reclaimImage = AllocateImageMemory(context, allocation, 1u, "test image", &type); + mock.limit = 12 * MiB; + auto request = allocation; + request.allocationSize = 4 * MiB; + const auto before = mock.attempts; + try { + const auto recovered = AllocateImageMemory(context, request, 1u, "test image", &type); + Expect(recovered != VK_NULL_HANDLE && mock.attempts == before + 2 && mock.liveBytes == 4 * MiB && context.bufferPool->RetainedImages() == 0, "the image memory a cache release returns to the pool is freed before the retry"); + ReleaseImageMemory(context, recovered, 4 * MiB, 0); + } catch (const std::exception& error) { + Expect(false, std::string("the cache release of an image did not permit recovery: ") + error.what()); + } + reclaimImageContext = nullptr; +} + +void ImageMemoryOverBudget() { + mock = MockDevice{}; + auto context = budgetContext(); + reportedBudget = 1000 * MiB; + reportedUsage = 500 * MiB; + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = 4 * MiB; + allocation.memoryTypeIndex = 0; + std::uint32_t type = 0; + const auto first = AllocateImageMemory(context, allocation, 1u, "test image", &type); + ReleaseImageMemory(context, first, 4 * MiB, 0); + Expect(mock.frees == 0 && context.bufferPool->RetainedImages() == 1, "image memory is retained while the heap is under its reported budget"); + reportedUsage = 1100 * MiB; + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + const auto second = AllocateImageMemory(context, allocation, 1u, "test image", &type); + Expect(second == first && mock.allocations == 1, "a retained allocation is still handed out over the budget"); + ReleaseImageMemory(context, second, 4 * MiB, 0); + Expect(mock.frees == 1 && context.bufferPool->RetainedImages() == 0, "image memory released while the heap is over its reported budget is freed"); + const auto third = AllocateImageMemory(context, allocation, 1u, "test image", &type); + ReleaseImageMemory(context, third, 4 * MiB, 0); + reportedUsage = 500 * MiB; + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + const auto fourth = AllocateImageMemory(context, allocation, 1u, "test image", &type); + ReleaseImageMemory(context, fourth, 4 * MiB, 0); + Expect(mock.frees == 2 && context.bufferPool->RetainedImages() == 1, "the pool retains image memory again once the heap is under its budget"); +} + +void ImageMemoryReuse() { + mock = MockDevice{}; + auto context = mockContext(); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = 4 * MiB; + allocation.memoryTypeIndex = 0; + std::uint32_t type = 99; + const auto first = AllocateImageMemory(context, allocation, 1u, "test image", &type); + Expect(type == 0 && mock.allocations == 1, "the first image allocates device memory of its type"); + ReleaseImageMemory(context, first, 4 * MiB, 0); + Expect(mock.frees == 0 && context.bufferPool->RetainedImages() == 1, "a released device-local image allocation is retained"); + const auto again = AllocateImageMemory(context, allocation, 1u, "test image", &type); + Expect(again == first && type == 0 && mock.allocations == 1 && context.bufferPool->RetainedImages() == 0, "the next image of the same type and size takes the retained allocation"); + auto smaller = allocation; + smaller.allocationSize = 2 * MiB; + const auto other = AllocateImageMemory(context, smaller, 1u, "test image", &type); + Expect(other != again && mock.allocations == 2, "an image of another size allocates"); + ReleaseImageMemory(context, other, 2 * MiB, 0); + ReleaseImageMemory(context, again, 4 * MiB, 0); + auto host = allocation; + host.memoryTypeIndex = 1; + const auto placed = AllocateImageMemory(context, host, 2u, "test image", &type); + Expect(type == 1 && mock.allocations == 3, "a host-visible image allocation is made"); + ReleaseImageMemory(context, placed, 4 * MiB, 1); + Expect(mock.frees == 1 && context.bufferPool->RetainedImages() == 2, "a host-visible image allocation is freed when released"); + const auto unsized = AllocateImageMemory(context, smaller, 1u, "test image", &type); + Expect(unsized == other && mock.allocations == 3 && context.bufferPool->RetainedImages() == 1, "an image of a retained size takes that allocation"); + ReleaseImageMemory(context, unsized, 0, 0); + Expect(mock.frees == 2 && context.bufferPool->RetainedImages() == 1, "an allocation of unknown size is freed"); + std::vector many; + for (int i = 0; i < 33; ++i) many.push_back(AllocateImageMemory(context, allocation, 1u, "test image", &type)); + Expect(many.front() == again && mock.allocations == 35, "a burst takes the retained allocation first and allocates the rest"); + for (const auto memory : many) ReleaseImageMemory(context, memory, 4 * MiB, 0); + Expect(context.bufferPool->RetainedImages() == 32 && mock.frees == 3 && !mock.memoryBytes.contains(many.front()) && mock.memoryBytes.contains(many.back()), "retained image memory is evicted least recently used under its budget"); + mock.results.push_back(VK_ERROR_OUT_OF_DEVICE_MEMORY); + auto third = allocation; + third.allocationSize = 3 * MiB; + const auto recovered = AllocateImageMemory(context, third, 1u, "test image", &type); + Expect(recovered != VK_NULL_HANDLE && mock.allocations == 36 && mock.frees == 35 && context.bufferPool->RetainedImages() == 0, "an allocation failure frees the retained image memory"); + ReleaseImageMemory(context, recovered, 3 * MiB, 0); + Expect(mock.frees == 35 && context.bufferPool->RetainedImages() == 1, "the pool retains image memory again after a trim"); +} + int main(int argc, char** argv) { try { if (argc == 2 && std::string_view(argv[1]) == "--small-device-retention") { @@ -937,6 +1043,9 @@ int main(int argc, char** argv) { PartialSlabDoesNotRetry(); CacheReclaimRecovery(); ImageCacheReclaim(); + ImageMemoryReuse(); + ImageMemoryReclaim(); + ImageMemoryOverBudget(); HostPlacement(); RefusedDevicePlacement(); AttachmentReclaim(); diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 719ded84dd..9e7aa2668d 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -154,6 +154,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_HOOK_LOCKED_WAIT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | keeps the flush hook's pending-write wait under the GpuMutex | | `APS5_HOST_IMPORT_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 0 | budget in MiB for pinned host imports; `0` selects the larger of 6144 MiB and half the physical memory, at most the largest host heap minus the larger of 2 GiB and half that heap (4055 MiB on a 16 GB Windows PC with an RTX 4060) | | `APS5_IGNORE_DEPTH_TEST` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp` | | renders depth- and stencil-tested draws without a depth target as if the tests always passed | +| `APS5_IMAGE_POOL_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | size in MiB of the released image memory the buffer pool keeps for reuse by memory type and size while the device-local heap is under its reported budget, a quarter of the device tier's budget by default; `0` frees image memory at once | | `APS5_INDIRECT_VERTEX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp` | 64 | cap in MiB on a vertex attribute extent, above which a GPU-side indirect draw resolves on the CPU | | `APS5_INSERT_COMPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | compares a fresh capture's regions against the recorder before inserting it into the cache | | `APS5_KEPT_DESCRIPTOR_SETS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 8 | number of freed dedicated image table descriptor sets kept for a later build with the same layout and sizes; 0 keeps none | @@ -239,7 +240,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_IMAGE_PAGE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | leaves the main guest image's page-protection table uncached | | `APS5_NO_INDIRECT_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp` | | rejects indirect draw packets at submission validation | | `APS5_NO_JOIN_WAW_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets a contiguous store join the pending one without the write-after-write check | -| `APS5_NO_KEEP_NEW_STORAGE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | does not keep a newly created storage image alive with the open batch | +| `APS5_NO_KEEP_NEW_STORAGE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | does not keep a newly created storage image alive with the open batch; such an image evicted before the batch completes can hand its memory to another image through the buffer pool while the batch still writes it | | `APS5_NO_KEYS_FILL_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp` | | records no memo of GPU-side DCC key fills | | `APS5_NO_KEYS_REFILL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | keeps a covered image's uploaded keys as they were instead of marking them uncompressed after a matching key fill | | `APS5_NO_KEYS_RESCAN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | reuses the DCC keys scanned before the lookup's refresh instead of discarding them for a rescan | From bafc97bb1e7a0078e0a836dcacf36f7b1eef84e7 Mon Sep 17 00:00:00 2001 From: ionut27001 <194206737+ionut27001@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:30:33 +0300 Subject: [PATCH 070/219] perf(agc): evict the oldest retained image from a list in constant time A release scanned every key of the device tier for the oldest retained image, on the queue worker, once per retained image the budget does not hold. Retained images are now also kept in one list in release order, and each retained slot holds its own node, so a release picks and evicts the oldest image in constant time and a take removes its node in constant time. The device-tier eviction that a release can also trigger when the buffer budget or the slot count is reached still scans the tier's keys; that path predates this change and is untouched. --- .../Graphics/include/BufferPool.hpp | 3 ++ .../Graphics/src/BufferPool.cpp | 42 ++++++++++++------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index 4aa1ecd08a..657c5351c4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -96,6 +97,7 @@ private: struct Slot { BufferAllocation allocation; std::uint64_t lastUse; + std::list::iterator order; }; struct SlotKey { std::size_t bytes; @@ -163,6 +165,7 @@ private: std::uint64_t imageHits = 0; std::uint64_t imageMisses = 0; std::uint64_t imageEvictions = 0; + std::list imageOrder; std::uint64_t clock = 0; std::mutex slabMutex; std::unordered_map slabs; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index 54f7104c56..128970a898 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -153,9 +153,11 @@ VkDeviceMemory BufferPool::TakeImageMemory(const VkMemoryAllocateInfo& allocatio ++clock; const auto found = deviceTier.free.find({static_cast(allocation.allocationSize), 0, memoryProperties.memoryTypes[type].propertyFlags, type}); if (found != deviceTier.free.end() && !found->second.empty()) { - const auto memory = found->second.back().allocation.memory; - found->second.pop_back(); - if (found->second.empty()) deviceTier.free.erase(found); + auto& slots = found->second; + const auto memory = slots.back().allocation.memory; + imageOrder.erase(slots.back().order); + slots.pop_back(); + if (slots.empty()) deviceTier.free.erase(found); --deviceTier.slots; deviceTier.retainedBytes -= allocation.allocationSize; --imageSlots; @@ -188,7 +190,16 @@ void BufferPool::PutImageMemory(VkDeviceMemory memory, VkDeviceSize bytes, std:: while (imageSlots != 0 && imageBytes + bytes > imageBudget && evictOldestImage(evicted)) { } while (deviceTier.slots != 0 && (deviceTier.retainedBytes + bytes > deviceTier.budget || deviceTier.slots >= maxSlots)) evictOldest(deviceTier, evicted); - deviceTier.free[{allocation.bytes, 0, allocation.properties, memoryType}].push_back({allocation, ++clock}); + const SlotKey imageKey{allocation.bytes, 0, allocation.properties, memoryType}; + auto& slots = deviceTier.free[imageKey]; + slots.push_back({allocation, ++clock, {}}); + try { + slots.back().order = imageOrder.insert(imageOrder.end(), &slots.back()); + } catch (...) { + slots.pop_back(); + if (slots.empty()) deviceTier.free.erase(imageKey); + throw; + } ++deviceTier.slots; deviceTier.retainedBytes += bytes; ++imageSlots; @@ -413,6 +424,7 @@ void BufferPool::evictOldest(Tier& tier, std::vector& evicted) evicted.push_back(oldest->second.front().allocation); tier.retainedBytes -= oldest->second.front().allocation.allocationBytes; if (oldest->second.front().allocation.buffer == VK_NULL_HANDLE) { + imageOrder.erase(oldest->second.front().order); imageBytes -= oldest->second.front().allocation.allocationBytes; --imageSlots; ++imageEvictions; @@ -424,18 +436,17 @@ void BufferPool::evictOldest(Tier& tier, std::vector& evicted) } bool BufferPool::evictOldestImage(std::vector& evicted) { - auto oldest = deviceTier.free.end(); - for (auto it = deviceTier.free.begin(); it != deviceTier.free.end(); ++it) { - if (it->second.front().allocation.buffer != VK_NULL_HANDLE) continue; - if (oldest == deviceTier.free.end() || it->second.front().lastUse < oldest->second.front().lastUse) oldest = it; - } - if (oldest == deviceTier.free.end()) return false; - evicted.push_back(oldest->second.front().allocation); - const auto bytes = oldest->second.front().allocation.allocationBytes; + if (imageOrder.empty()) return false; + const Slot* oldest = imageOrder.front(); + const SlotKey key{oldest->allocation.bytes, 0, oldest->allocation.properties, oldest->allocation.memoryType}; + const auto found = deviceTier.free.find(key); + evicted.push_back(oldest->allocation); + imageOrder.pop_front(); + const auto bytes = oldest->allocation.allocationBytes; deviceTier.retainedBytes -= bytes; imageBytes -= bytes; - oldest->second.pop_front(); - if (oldest->second.empty()) deviceTier.free.erase(oldest); + found->second.pop_front(); + if (found->second.empty()) deviceTier.free.erase(found); --deviceTier.slots; --imageSlots; ++deviceTier.evictions; @@ -469,6 +480,7 @@ VkDeviceSize BufferPool::trim() { imageEvictions += imageSlots; imageBytes = 0; imageSlots = 0; + imageOrder.clear(); } VkDeviceSize bytes = 0; for (const auto* tier : {&retiredSmall, &retiredLarge, &retiredDevice}) { @@ -506,7 +518,7 @@ void BufferPool::Put(const BufferAllocation& allocation) noexcept { evicted.push_back(allocation); } else { while (tier.slots != 0 && (tier.retainedBytes + allocation.allocationBytes > tier.budget || tier.slots >= maxSlots)) evictOldest(tier, evicted); - tier.free[{allocation.bytes, allocation.usage, allocation.properties}].push_back({allocation, ++clock}); + tier.free[{allocation.bytes, allocation.usage, allocation.properties}].push_back({allocation, ++clock, {}}); ++tier.slots; tier.retainedBytes += allocation.allocationBytes; } From bff194647a888e8ce3c94e248b0d94b2c7470b0a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 18:29:09 +0000 Subject: [PATCH 071/219] feat(libSceFont): open the other script files of a system font set Co-Authored-By: Claude Opus 5.5 (1M context) --- .../prx/libSceFont/include/FontInternal.hpp | 11 +++ core/libs/prx/libSceFont/src/Layout.cpp | 42 +++++----- core/libs/prx/libSceFont/src/Open.cpp | 6 ++ core/libs/prx/libSceFont/src/Render.cpp | 13 +-- core/libs/prx/libSceFont/src/State.cpp | 38 ++++++++- core/libs/prx/libSceFont/src/SystemFont.cpp | 64 ++++++++++++--- core/libs/tests/GuestSystemFont.cpp | 79 ++++++++++++++++++- docs/dev/debt/libSceFont.md | 3 +- docs/user/USAGE.md | 2 +- 9 files changed, 216 insertions(+), 42 deletions(-) diff --git a/core/libs/prx/libSceFont/include/FontInternal.hpp b/core/libs/prx/libSceFont/include/FontInternal.hpp index b2a742c291..4c0e9c391f 100644 --- a/core/libs/prx/libSceFont/include/FontInternal.hpp +++ b/core/libs/prx/libSceFont/include/FontInternal.hpp @@ -24,9 +24,16 @@ constexpr std::uint8_t STYLE_FRAME_FLAG_SLANT = 0x02; constexpr std::uint8_t STYLE_FRAME_FLAG_WEIGHT = 0x04; constexpr float POINTS_PER_INCH = 72.0f; +struct FallbackFace { + std::shared_ptr> data; + FT_Face face = nullptr; + std::uint32_t subFontIndex = 0; +}; + struct FontState { std::shared_ptr> faceData; FT_Face face = nullptr; + std::vector fallbacks; float scaleW = 16.0f; float scaleH = 16.0f; std::map scriptLanguages; @@ -88,11 +95,15 @@ FontState* TryGetState(FontHandle handle); FontState& ResetState(FontHandle handle); void RemoveState(FontHandle handle); void LoadStateFace(FontState& state, std::shared_ptr> bytes, std::uint32_t subFontIndex); +void AddStateFallbackFace(FontState& state, std::shared_ptr> bytes, std::uint32_t subFontIndex); +FT_Face FallbackFaceForCode(const FontState& state, std::uint32_t code, FT_UInt* glyphIndex); +FT_Face FaceForCode(const FontState& state, std::uint32_t code, FT_UInt* glyphIndex); bool IsSystemFontSet(std::uint32_t fontSetType); std::vector SystemFontCandidates(std::uint32_t fontSetType); std::filesystem::path SystemFontDirectory(); std::optional FindSystemFontFile(std::uint32_t fontSetType); +std::vector FindSystemFontFallbackFiles(std::uint32_t fontSetType); void TrackGeneratedGlyph(FontGlyph glyph); bool ForgetGeneratedGlyph(FontGlyph glyph); diff --git a/core/libs/prx/libSceFont/src/Layout.cpp b/core/libs/prx/libSceFont/src/Layout.cpp index 8a067968b8..a59a3b5f2f 100644 --- a/core/libs/prx/libSceFont/src/Layout.cpp +++ b/core/libs/prx/libSceFont/src/Layout.cpp @@ -404,10 +404,10 @@ int Font::GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMe float scaleH = 0.0f; const int scaleRc = StyleStateGetScalePixel(style, &scaleW, &scaleH); if (scaleRc != SCE_FONT_OK) return finish(scaleRc); - const FT_Face face = state->face; - if (!face) return finish(SCE_FONT_ERROR_NO_SUPPORT_FUNCTION); - const FT_UInt glyphIndex = ResolveGlyphIndexWithFallback(face, code); - if (glyphIndex == 0) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + if (!state->face) return finish(SCE_FONT_ERROR_NO_SUPPORT_FUNCTION); + FT_UInt glyphIndex = 0; + const FT_Face face = FaceForCode(*state, code, &glyphIndex); + if (!face) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); const auto charW = static_cast(static_cast(scaleW * 64.0f)); const auto charH = static_cast(static_cast(scaleH * 64.0f)); FT_F26Dot6 usedW = charW; @@ -475,19 +475,23 @@ int Font::RenderCharGlyphImageCore(FontHandle handle, std::uint32_t code, FontRe obj->shift_units_y = 0; obj->layout_seed_pair = 0; StyleStateGetScalePixel(style, &obj->scale_x_0x50, &obj->scale_y_0x54); - float outScaleX = 0.0f; - float outScaleY = 0.0f; - int rc; - if (style->scale_unit == 0) { - rc = driver->set_char_default_dpi(obj, style->scale_w, style->scale_h, &outScaleX, &outScaleY); - } else { - rc = driver->set_char_with_dpi(obj, style->dpi_x ? style->dpi_x : 0x48, style->dpi_y ? style->dpi_y : 0x48, style->scale_w, style->scale_h, &outScaleX, &outScaleY); - } - if (rc != SCE_FONT_OK) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + const auto setCharSize = [&](FontObj* target) { + float outScaleX = 0.0f; + float outScaleY = 0.0f; + if (style->scale_unit == 0) return driver->set_char_default_dpi(target, style->scale_w, style->scale_h, &outScaleX, &outScaleY); + return driver->set_char_with_dpi(target, style->dpi_x ? style->dpi_x : 0x48, style->dpi_y ? style->dpi_y : 0x48, style->scale_w, style->scale_h, &outScaleX, &outScaleY); + }; + if (setCharSize(obj) != SCE_FONT_OK) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); std::uint32_t glyphIndex = 0; - rc = driver->glyph_index(obj, code, &glyphIndex); - if (rc != SCE_FONT_OK || glyphIndex == 0) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); - return finish(RenderGlyphIndexToSurface(*obj, glyphIndex, surface, x, y, metrics, result)); + if (driver->glyph_index(obj, code, &glyphIndex) == SCE_FONT_OK && glyphIndex != 0) return finish(RenderGlyphIndexToSurface(*obj, glyphIndex, surface, x, y, metrics, result)); + const FontState* state = TryGetState(handle); + FT_UInt fallbackIndex = 0; + const FT_Face fallbackFace = state ? FallbackFaceForCode(*state, code, &fallbackIndex) : nullptr; + if (!fallbackFace) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + FontObj fallback = *obj; + fallback.ft_face = fallbackFace; + if (setCharSize(&fallback) != SCE_FONT_OK) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + return finish(RenderGlyphIndexToSurface(fallback, fallbackIndex, surface, x, y, metrics, result)); } bool Font::BuildTrueOutline(GeneratedGlyph& glyph) { @@ -495,9 +499,9 @@ bool Font::BuildTrueOutline(GeneratedGlyph& glyph) { if (!state || !state->face) return false; const auto* font = GetNativeFont(glyph.owner); if (font->magic != HANDLE_MAGIC) return false; - const FT_Face face = state->face; - const FT_UInt glyphIndex = FT_Get_Char_Index(face, glyph.codepoint); - if (glyphIndex == 0) return false; + FT_UInt glyphIndex = 0; + const FT_Face face = FaceForCode(*state, glyph.codepoint, &glyphIndex); + if (!face) return false; const auto charW = static_cast(static_cast(glyph.glyph.scale_x * 64.0f)); const auto charH = static_cast(static_cast(glyph.glyph.base_scale * 64.0f)); if (FT_Set_Char_Size(face, charW, charH, 72, 72) != 0) return false; diff --git a/core/libs/prx/libSceFont/src/Open.cpp b/core/libs/prx/libSceFont/src/Open.cpp index ef118bd83e..4e288f2b44 100644 --- a/core/libs/prx/libSceFont/src/Open.cpp +++ b/core/libs/prx/libSceFont/src/Open.cpp @@ -392,6 +392,7 @@ int APS5_VABI sceFontOpenFontInstance(FontHandle fontHandle, FontHandle setupFon targetState.scaleW = sourceState->scaleW; targetState.scaleH = sourceState->scaleH; if (sourceState->faceData && !sourceState->faceData->empty()) LoadStateFace(targetState, sourceState->faceData, subFontIndex); + for (const FallbackFace& fallback : sourceState->fallbacks) AddStateFallbackFace(targetState, fallback.data, fallback.subFontIndex); } if (pFontHandle) *pFontHandle = target; return SCE_FONT_OK; @@ -432,8 +433,13 @@ int APS5_VABI sceFontOpenFontSet(FontLibrary library, std::uint32_t fontSetType, if (openRc == SCE_FONT_ERROR_FS_OPEN_FAILED || openRc == SCE_FONT_ERROR_NO_SUPPORT_FORMAT) return SCE_FONT_ERROR_FONT_OPEN_FAILED; if (openRc != SCE_FONT_OK) return openRc; GetNativeFont(*pFontHandle)->open_info.fontset_flags = fontSetType; + const std::vector fallbackFiles = FindSystemFontFallbackFiles(fontSetType); + for (const SystemFontFile& fallback : fallbackFiles) APS5_LOG_OUT("fontSetType=0x%08X fallback=%s", fontSetType, fallback.path.string().c_str()); FontState& state = ResetState(*pFontHandle); if (auto bytes = SystemFontBytes(file->path)) LoadStateFace(state, std::move(bytes), file->subFontIndex); + for (const SystemFontFile& fallback : fallbackFiles) { + if (auto bytes = SystemFontBytes(fallback.path)) AddStateFallbackFace(state, std::move(bytes), fallback.subFontIndex); + } return SCE_FONT_OK; } ReleaseLibraryLock(lib, libraryLock); diff --git a/core/libs/prx/libSceFont/src/Render.cpp b/core/libs/prx/libSceFont/src/Render.cpp index 55d3de834d..ec5558c4f6 100644 --- a/core/libs/prx/libSceFont/src/Render.cpp +++ b/core/libs/prx/libSceFont/src/Render.cpp @@ -81,10 +81,11 @@ int CachedColumnOffset(FontHandle handle, FontHandleNative* font, float& offset) return rc; } -void LoadKerning(FT_Face face, float scaleW, float scaleH, std::uint32_t preCode, std::uint32_t code, FontKerning* kerning) { - const FT_UInt previousGlyph = face ? FT_Get_Char_Index(face, preCode) : 0; - const FT_UInt glyph = face ? FT_Get_Char_Index(face, code) : 0; - if (!face || previousGlyph == 0 || glyph == 0) return; +void LoadKerning(const FontState& state, float scaleW, float scaleH, std::uint32_t preCode, std::uint32_t code, FontKerning* kerning) { + FT_UInt previousGlyph = 0; + FT_UInt glyph = 0; + const FT_Face face = FaceForCode(state, preCode, &previousGlyph); + if (!face || FaceForCode(state, code, &glyph) != face) return; const auto charW = static_cast(static_cast(scaleW * 64.0f)); const auto charH = static_cast(static_cast(scaleH * 64.0f)); FT_Set_Char_Size(face, charW, charH, 72, 72); @@ -187,7 +188,7 @@ int APS5_VABI sceFontGetKerning(FontHandle fontHandle, std::uint32_t preCode, st if (!font || font->magic != HANDLE_MAGIC || !AcquireFontLock(font, fontLock)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; *kerning = {}; const FontState* state = TryGetState(fontHandle); - if (state) LoadKerning(state->face, state->scaleW, state->scaleH, preCode, code, kerning); + if (state) LoadKerning(*state, state->scaleW, state->scaleH, preCode, code, kerning); ReleaseFontLock(font, fontLock); return SCE_FONT_OK; } @@ -209,7 +210,7 @@ int APS5_VABI sceFontGetRenderScaledKerning(FontHandle fontHandle, std::uint32_t float scaleH = 0.0f; rc = StyleStateGetScalePixel(&font->cached_style.state, &scaleW, &scaleH); const FontState* state = TryGetState(fontHandle); - if (rc == SCE_FONT_OK && state) LoadKerning(state->face, scaleW, scaleH, preCode, code, kerning); + if (rc == SCE_FONT_OK && state) LoadKerning(*state, scaleW, scaleH, preCode, code, kerning); } ReleaseCachedStyleLock(font, cachedLock); ReleaseFontLock(font, fontLock); diff --git a/core/libs/prx/libSceFont/src/State.cpp b/core/libs/prx/libSceFont/src/State.cpp index ee86c2dac0..4ebd92348b 100644 --- a/core/libs/prx/libSceFont/src/State.cpp +++ b/core/libs/prx/libSceFont/src/State.cpp @@ -13,6 +13,7 @@ #include #include "prx/libc/include/PreciseWait.hpp" +#include "prx/libSceFont/include/FontFreeType.hpp" #include "prx/libSceFont/include/FontInternal.hpp" namespace { @@ -54,10 +55,9 @@ void ReleaseWord(std::uint32_t& word, std::uint32_t previous) { } Font::FontState::~FontState() { - if (face) { - std::lock_guard lock(faceMutex); - FT_Done_Face(face); - } + std::lock_guard lock(faceMutex); + if (face) FT_Done_Face(face); + for (const FallbackFace& fallback : fallbacks) FT_Done_Face(fallback.face); } void Font::Backoff() { @@ -267,6 +267,36 @@ void Font::LoadStateFace(FontState& state, std::shared_ptr> bytes, std::uint32_t subFontIndex) { + std::lock_guard lock(faceMutex); + const FT_Library library = HostFreeTypeLibrary(); + if (!library || !bytes || bytes->empty()) return; + FT_Face face = nullptr; + if (FT_New_Memory_Face(library, bytes->data(), static_cast(bytes->size()), static_cast(subFontIndex), &face) != 0) return; + FT_Select_Charmap(face, FT_ENCODING_UNICODE); + state.fallbacks.push_back({std::move(bytes), face, subFontIndex}); +} + +FT_Face Font::FallbackFaceForCode(const FontState& state, std::uint32_t code, FT_UInt* glyphIndex) { + for (const FallbackFace& fallback : state.fallbacks) { + const FT_UInt index = ResolveGlyphIndexWithFallback(fallback.face, code); + if (index == 0) continue; + *glyphIndex = index; + return fallback.face; + } + *glyphIndex = 0; + return nullptr; +} + +FT_Face Font::FaceForCode(const FontState& state, std::uint32_t code, FT_UInt* glyphIndex) { + const FT_UInt index = ResolveGlyphIndexWithFallback(state.face, code); + if (index != 0) { + *glyphIndex = index; + return state.face; + } + return FallbackFaceForCode(state, code, glyphIndex); +} + void Font::TrackGeneratedGlyph(FontGlyph glyph) { std::lock_guard lock(glyphMutex); generatedGlyphs.insert(glyph); diff --git a/core/libs/prx/libSceFont/src/SystemFont.cpp b/core/libs/prx/libSceFont/src/SystemFont.cpp index f164d86462..5bcfb0789e 100644 --- a/core/libs/prx/libSceFont/src/SystemFont.cpp +++ b/core/libs/prx/libSceFont/src/SystemFont.cpp @@ -37,7 +37,7 @@ constexpr std::array SYSTEM_FONT_SET_TYPES = { 0x1A0825C3u, 0x1A0825C7u, 0x1A083553u, 0x1A083557u, 0x1A0835D3u, 0x1A0835D7u, }; -enum class Family { Latin, LatinItalic, Typewriter, Vietnamese, Thai, Japanese, ChineseGb }; +enum class Family { Latin, LatinItalic, Typewriter, Vietnamese, Thai, Japanese, ChineseGb, Hangul, Arabic }; struct FamilyFiles { std::array system; @@ -45,7 +45,7 @@ struct FamilyFiles { std::uint32_t substituteSubFont; }; -constexpr std::array FAMILY_FILES = {{ +constexpr std::array FAMILY_FILES = {{ {{"SST-Light.otf", "SST-Roman.otf", "SST-Medium.otf", "SST-Bold.otf"}, {"NotoSans-Light.ttf", "NotoSans-Regular.ttf", "NotoSans-Medium.ttf", "NotoSans-Bold.ttf"}, 0}, {{"SST-LightItalic.otf", "SST-Italic.otf", "SST-MediumItalic.otf", "SST-BoldItalic.otf"}, @@ -60,6 +60,10 @@ constexpr std::array FAMILY_FILES = {{ {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 0}, {{"DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf"}, {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 2}, + {{"SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Bold.otf"}, + {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 1}, + {{"SSTArabic-Light.otf", "SSTArabic-Roman.otf", "SSTArabic-Medium.otf", "SSTArabic-Bold.otf"}, + {"NotoSansArabic-Light.ttf", "NotoSansArabic-Regular.ttf", "NotoSansArabic-Medium.ttf", "NotoSansArabic-Bold.ttf"}, 0}, }}; Family FamilyOf(std::uint32_t fontSetType) { @@ -87,6 +91,37 @@ std::size_t WeightOf(std::uint32_t fontSetType) { } } +std::vector FallbackFamilies(std::uint32_t fontSetType) { + const std::uint32_t scripts = (fontSetType >> 8) & 0xFFu; + const Family primary = FamilyOf(fontSetType); + std::vector families; + const auto add = [&](bool wanted, Family family) { + if (wanted && family != primary && std::find(families.begin(), families.end(), family) == families.end()) families.push_back(family); + }; + add((fontSetType & 0x10u) != 0, Family::Vietnamese); + add((scripts & 0x10u) != 0, Family::Thai); + add((scripts & 0x88u) == 0x88u, Family::ChineseGb); + add((scripts & 0x04u) != 0, Family::Japanese); + add((scripts & 0x80u) != 0, Family::ChineseGb); + add((scripts & 0x20u) != 0, Family::Hangul); + add((fontSetType & 0x80u) != 0, Family::Arabic); + return families; +} + +std::vector FamilyCandidates(Family family, std::size_t weight) { + const FamilyFiles& files = FAMILY_FILES[static_cast(family)]; + return {{std::filesystem::path(files.system[weight]), 0}, {std::filesystem::path(files.substitute[weight]), files.substituteSubFont}}; +} + +std::optional FirstExisting(const std::filesystem::path& directory, const std::vector& candidates) { + for (const Font::SystemFontFile& candidate : candidates) { + std::error_code error; + const std::filesystem::path path = directory / candidate.path; + if (std::filesystem::is_regular_file(path, error)) return Font::SystemFontFile{path, candidate.subFontIndex}; + } + return std::nullopt; +} + std::filesystem::path ExecutableDirectory() { #ifdef _WIN32 wchar_t module[MAX_PATH]; @@ -115,9 +150,7 @@ bool Font::IsSystemFontSet(std::uint32_t fontSetType) { std::vector Font::SystemFontCandidates(std::uint32_t fontSetType) { if (!IsSystemFontSet(fontSetType)) return {}; - const FamilyFiles& files = FAMILY_FILES[static_cast(FamilyOf(fontSetType))]; - const std::size_t weight = WeightOf(fontSetType); - return {{std::filesystem::path(files.system[weight]), 0}, {std::filesystem::path(files.substitute[weight]), files.substituteSubFont}}; + return FamilyCandidates(FamilyOf(fontSetType), WeightOf(fontSetType)); } std::filesystem::path Font::SystemFontDirectory() { @@ -132,11 +165,22 @@ std::filesystem::path Font::SystemFontDirectory() { std::optional Font::FindSystemFontFile(std::uint32_t fontSetType) { const std::vector candidates = SystemFontCandidates(fontSetType); if (candidates.empty()) return std::nullopt; + return FirstExisting(SystemFontDirectory(), candidates); +} + +std::vector Font::FindSystemFontFallbackFiles(std::uint32_t fontSetType) { + if (!IsSystemFontSet(fontSetType)) return {}; const std::filesystem::path directory = SystemFontDirectory(); - for (const SystemFontFile& candidate : candidates) { - std::error_code error; - const std::filesystem::path path = directory / candidate.path; - if (std::filesystem::is_regular_file(path, error)) return SystemFontFile{path, candidate.subFontIndex}; + const std::size_t weight = WeightOf(fontSetType); + std::vector files; + const std::optional primary = FirstExisting(directory, SystemFontCandidates(fontSetType)); + const auto opened = [&](const SystemFontFile& file) { + const auto same = [&](const SystemFontFile& other) { return other.path == file.path && other.subFontIndex == file.subFontIndex; }; + return (primary && same(*primary)) || std::any_of(files.begin(), files.end(), same); + }; + for (const Family family : FallbackFamilies(fontSetType)) { + auto file = FirstExisting(directory, FamilyCandidates(family, weight)); + if (file && !opened(*file)) files.push_back(std::move(*file)); } - return std::nullopt; + return files; } diff --git a/core/libs/tests/GuestSystemFont.cpp b/core/libs/tests/GuestSystemFont.cpp index ec2f807855..4740649257 100644 --- a/core/libs/tests/GuestSystemFont.cpp +++ b/core/libs/tests/GuestSystemFont.cpp @@ -24,7 +24,16 @@ int APS5_VABI sceFontCloseFont(FontHandle); int APS5_VABI sceFontSetScalePixel(FontHandle, float, float); int APS5_VABI sceFontGetCharGlyphMetrics(FontHandle, std::uint32_t, FontGlyphMetrics*); int APS5_VABI sceFontGetHorizontalLayout(FontHandle, FontHorizontalLayout*); +int APS5_VABI sceFontGetKerning(FontHandle, std::uint32_t, std::uint32_t, FontKerning*); +int APS5_VABI sceFontCreateRenderer(const FontMemory*, const void*, FontRenderer*); +int APS5_VABI sceFontDestroyRenderer(FontRenderer*); +int APS5_VABI sceFontBindRenderer(FontHandle, FontRenderer); +int APS5_VABI sceFontUnbindRenderer(FontHandle); +int APS5_VABI sceFontSetupRenderScalePixel(FontHandle, float, float); +void APS5_VABI sceFontRenderSurfaceInit(FontRenderSurface*, void*, int, int, int, int); +int APS5_VABI sceFontRenderCharGlyphImageHorizontal(FontHandle, std::uint32_t, FontRenderSurface*, float, float, FontGlyphMetrics*, FontRenderOutput*); const void* APS5_VABI sceFontSelectLibraryFt(int); +const void* APS5_VABI sceFontSelectRendererFt(int); } static void Check(bool value, int line) { @@ -124,6 +133,29 @@ static float AdvanceOf(FontHandle font, std::uint32_t code) { return metrics.Horizontal.advance; } +static float SetAdvanceOf(FontLibrary library, std::uint32_t fontSetType, std::uint32_t code) { + FontHandle set = nullptr; + Require(sceFontOpenFontSet(library, fontSetType, 1, nullptr, &set) == SCE_FONT_OK); + Require(sceFontSetScalePixel(set, 100.0f, 100.0f) == SCE_FONT_OK); + const float advance = AdvanceOf(set, code); + Require(sceFontCloseFont(set) == SCE_FONT_OK); + return advance; +} + +static int RenderedPixels(FontHandle font, std::uint32_t code, float& advance) { + std::vector pixels(128 * 128); + FontRenderSurface surface{}; + sceFontRenderSurfaceInit(&surface, pixels.data(), 128, 1, 128, 128); + FontGlyphMetrics metrics{}; + FontRenderOutput result{}; + const int rc = sceFontRenderCharGlyphImageHorizontal(font, code, &surface, 10.0f, 100.0f, &metrics, &result); + advance = metrics.Horizontal.advance; + if (rc != SCE_FONT_OK) return rc; + int lit = 0; + for (const std::uint8_t pixel : pixels) lit += pixel != 0 ? 1 : 0; + return lit; +} + int main() { constexpr std::uint32_t EuropeanLight = 0x180700C3u; constexpr std::uint32_t EuropeanBold = 0x180700C7u; @@ -218,7 +250,52 @@ int main() { Require(AdvanceOf(memoryFont, 'A') == 90.0f); Require(sceFontGetHorizontalLayout(memoryFont, &layout) == SCE_FONT_OK); - for (FontHandle handle : {font, instance, substitute, memoryFont}) Require(sceFontCloseFont(handle) == SCE_FONT_OK); + constexpr std::uint32_t EuropeanJpAr = 0x180704C4u; + constexpr std::uint32_t EuropeanJpCjk = 0x1807A444u; + constexpr std::uint32_t EuropeanGbCjk = 0x1807AC44u; + constexpr std::uint32_t European = 0x18070044u; + const auto fallbackFonts = root / "fallback-fonts"; + std::filesystem::create_directories(fallbackFonts); + WriteFile(fallbackFonts / "SST-Roman.otf", SquareGlyphFont('A', 600)); + WriteFile(fallbackFonts / "SSTJpPro-Regular.otf", SquareGlyphFont(0x4E00, 630)); + WriteFile(fallbackFonts / "DFHEI5-SONY.ttf", SquareGlyphFont(0x4E00, 640)); + WriteFile(fallbackFonts / "SCEPS4Yoongd-Medium.otf", SquareGlyphFont(0xAC00, 650)); + WriteFile(fallbackFonts / "NotoSansArabic-Regular.ttf", SquareGlyphFont(0x0627, 660)); + SetFontDirectory(fallbackFonts); + Require(SetAdvanceOf(library, EuropeanJpAr, 'A') == 60.0f); + Require(SetAdvanceOf(library, EuropeanJpAr, 0x4E00) == 63.0f); + Require(SetAdvanceOf(library, EuropeanJpAr, 0x0627) == 66.0f); + Require(SetAdvanceOf(library, EuropeanJpCjk, 0x4E00) == 63.0f); + Require(SetAdvanceOf(library, EuropeanJpCjk, 0xAC00) == 65.0f); + Require(SetAdvanceOf(library, EuropeanGbCjk, 0x4E00) == 64.0f); + WriteFile(fallbackFonts / "NotoSans-Regular.ttf", SquareGlyphFont(0x1EA0, 670)); + Require(SetAdvanceOf(library, 0x18070454u, 0x1EA0) == 67.0f); + FontHandle plain = nullptr; + Require(sceFontOpenFontSet(library, European, 1, nullptr, &plain) == SCE_FONT_OK); + Require(sceFontGetCharGlyphMetrics(plain, 0x4E00, &metrics) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + Require(sceFontCloseFont(plain) == SCE_FONT_OK); + + FontHandle mixed = nullptr; + Require(sceFontOpenFontSet(library, EuropeanJpAr, 1, nullptr, &mixed) == SCE_FONT_OK); + FontHandle mixedInstance = nullptr; + Require(sceFontOpenFontInstance(mixed, nullptr, &mixedInstance) == SCE_FONT_OK); + Require(sceFontSetScalePixel(mixedInstance, 100.0f, 100.0f) == SCE_FONT_OK); + Require(AdvanceOf(mixedInstance, 0x0627) == 66.0f); + FontKerning kerning{1.0f, 1.0f, 1.0f, 1.0f}; + Require(sceFontGetKerning(mixed, 'A', 0x4E00, &kerning) == SCE_FONT_OK && kerning.offsetX == 0.0f); + FontRenderer renderer = nullptr; + Require(sceFontCreateRenderer(&memory, sceFontSelectRendererFt(0), &renderer) == SCE_FONT_OK); + Require(sceFontBindRenderer(mixed, renderer) == SCE_FONT_OK); + Require(sceFontSetupRenderScalePixel(mixed, 100.0f, 100.0f) == SCE_FONT_OK); + float advance = 0.0f; + Require(RenderedPixels(mixed, 'A', advance) > 0 && advance == 60.0f); + Require(RenderedPixels(mixed, 0x4E00, advance) > 0 && advance == 63.0f); + Require(RenderedPixels(mixed, 0x0627, advance) > 0 && advance == 66.0f); + Require(RenderedPixels(mixed, 0xAC00, advance) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + Require(sceFontUnbindRenderer(mixed) == SCE_FONT_OK); + Require(sceFontDestroyRenderer(&renderer) == SCE_FONT_OK); + + for (FontHandle handle : {font, instance, substitute, memoryFont, mixed, mixedInstance}) Require(sceFontCloseFont(handle) == SCE_FONT_OK); Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); Require(allocations == 0); std::filesystem::remove_all(root); diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 15c10ccaf8..7c36d156c2 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -20,6 +20,7 @@ ### Functional -- [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) opens only the primary file of a system font set. Characters the console takes from the set's other files (Hangul, Thai and Arabic in Japanese and Chinese sets, Arabic in European sets) are reported as unsupported glyphs. The console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans CJK) is loaded instead. It is not the system font: its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. +- [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the other script files of a set are chosen from its type, not measured on a console: Vietnamese for low-byte bit 0x10, Thai for script bit 0x10, Japanese for 0x04, simplified Chinese for 0x80 (before Japanese with 0x08), Hangul for 0x20 and Arabic for low-byte bit 0x80, as the SDK set names (`..._JP_AR`, `..._GBCJK_TH`) listed by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/font/font_internal.cpp) suggest; the file order and the Hangul file name (`SCEPS4Yoongd-Medium.otf`, `SCEPS4Yoongd-Bold.otf`) also come from shadPS4. A character the primary file lacks is taken from the first of them that has it; kerning between two files is 0, and `sceFontGetCharGlyphCode` and `sceFontGetFontGlyphsCount` answer for the primary file only +- [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans Arabic, Noto Sans CJK with its Korean face for Hangul) is loaded instead; its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. - [sceFontGetCharGlyphMetrics, sceFontGetRenderCharGlyphMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) report the vertical metrics the way the [FreeType driver](../../../core/libs/prx/libSceFontFt/src/Driver.cpp) reports them for a rendered glyph, not confirmed on a title: FreeType's vertical advance, and for a face without vertical metrics a bearing of half the horizontal advance left of the horizontal bearing and of half the advance left over by the glyph height. - [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - `sceFontWritingLineWritesOrder`, `sceFontWritingLineRefersRenderStep`, `sceFontWritingLineGetRenderMetrics`, `sceFontWritingLineGetOrderingSpace` and `sceFontWritingLineClear` follow the PS4 11.00 `libSceFont.sprx` (firmware module, read from its disassembly and compared call for call on random sequences): runs are linked in visual order by embedding level, the space at each spaced join is summed for the line, steps walk the visual order from the left (form `0x12`) or from the right (form `0x11`) and the render metrics grow as steps are consumed. The PS5 library was not measured. Not from that module: form `0x10` is accepted and treated as left to right and a detail with id `0x0FD5` is accepted (the PS4 library rejects both); the memory argument of `sceFontCreateWritingLine` and the rest of the detail are not read (the PS4 library validates its library object there); an unknown writing form, another detail id, a null line pointer on creation, unknown and null handles (except in `sceFontWritingLineRefersRenderStep`, which returns null for a null line) and a null output of `sceFontWritingLineGetRenderMetrics` throw instead of returning the PS4 error codes, which are not confirmed for the PS5; `sceFontDestroyWritingLine` was not compared. Lines are allocated on the host, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 3b3f4f6bd3..136f62870f 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -222,7 +222,7 @@ Run the helper in a second terminal. Commands are polled at most every 100 ms du ### System fonts -Games that open the console's system font sets (`sceFontOpenFontSet`) need font files in an `anyps5-fonts/` directory beside the output executable; set `ANYPS5_SYSTEM_FONTS` to use another directory. Files dumped from the console are used under their own names (`SST-Roman.otf`, `SST-Bold.otf`, `SSTJpPro-Regular.otf`, ...). Without them, these openly licensed substitutes are used when present: `NotoSans-{Light,Regular,Medium,Bold}.ttf` and `NotoSans-{LightItalic,Italic,MediumItalic,BoldItalic}.ttf` (Latin and Vietnamese), `NotoSansMono-{Light,Regular,Medium,Bold}.ttf` (typewriter), `NotoSansThai-{Light,Regular,Medium,Bold}.ttf` (Thai) and `NotoSansCJK-{Light,Regular,Medium,Bold}.ttc` (Japanese and Chinese). Without either, opening a system font set fails (the log shows `sceFontOpenFontSet` with `file=(none)`), and what happens next depends on the game: some show no text in those fonts, and others stop at start-up because they assert that the open succeeded. The substitutes come from the [Noto project](https://github.com/notofonts/notofonts.github.io) under the SIL Open Font License (the Noto Sans files are in `fonts/NotoSans/full/ttf`); copy the files named above into `anyps5-fonts/`. +Games that open the console's system font sets (`sceFontOpenFontSet`) need font files in an `anyps5-fonts/` directory beside the output executable; set `ANYPS5_SYSTEM_FONTS` to use another directory. Files dumped from the console are used under their own names (`SST-Roman.otf`, `SST-Bold.otf`, `SSTJpPro-Regular.otf`, ...). Without them, these openly licensed substitutes are used when present: `NotoSans-{Light,Regular,Medium,Bold}.ttf` and `NotoSans-{LightItalic,Italic,MediumItalic,BoldItalic}.ttf` (Latin and Vietnamese), `NotoSansMono-{Light,Regular,Medium,Bold}.ttf` (typewriter), `NotoSansThai-{Light,Regular,Medium,Bold}.ttf` (Thai), `NotoSansArabic-{Light,Regular,Medium,Bold}.ttf` (Arabic) and `NotoSansCJK-{Light,Regular,Medium,Bold}.ttc` (Japanese, Chinese and Korean). A set that covers several scripts also opens the files of its other scripts (`SSTThai-*.otf`, `SSTArabic-*.otf`, `SSTJpPro-*.otf`, `DFHEI5-SONY.ttf`, `SCEPS4Yoongd-*.otf` or their substitutes) for the characters its main file lacks; a missing one leaves those characters without a glyph. Without either, opening a system font set fails (the log shows `sceFontOpenFontSet` with `file=(none)`), and what happens next depends on the game: some show no text in those fonts, and others stop at start-up because they assert that the open succeeded. The substitutes come from the [Noto project](https://github.com/notofonts/notofonts.github.io) under the SIL Open Font License (the Noto Sans files are in `fonts/NotoSans/full/ttf`, the Arabic ones in `fonts/NotoSansArabic/full/ttf`); copy the files named above into `anyps5-fonts/`. ### Console language From e4898783b603cd9915452783d564772d61569160 Mon Sep 17 00:00:00 2001 From: Johan David Date: Mon, 19 Oct 2026 14:45:14 -0500 Subject: [PATCH 072/219] feat(agc): sample the draw committer with APS5_SAMPLE_COMMITTER Queue 0's draw committer records the draws the packet worker hands over. When the worker's APS5_SAMPLE_WORKER stacks wait in DrawPipeline::Enqueue, the committer is the thread that bounds the frame, and only APS5_SAMPLE_THREADS sees it: 40 addresses, 12 frames deep, without whole stacks. APS5_SAMPLE_COMMITTER= attaches the worker's sampler to the committer when its thread starts and writes the same two files. Both use one start function, and StopWorkerSampler stops both. --- .../Execution/include/WorkerSampler.hpp | 1 + .../src/Driver/Draw/DrawPipeline.cpp | 2 ++ .../Execution/src/WorkerSampler.cpp | 23 ++++++++++++++----- docs/dev/BUILD.md | 4 +++- docs/dev/ENVIRONMENT.md | 1 + 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp index ec282a171f..4aa7625ba9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp @@ -5,6 +5,7 @@ namespace AgcDriver { // Starts sampling the calling thread when APS5_SAMPLE_WORKER names an output file. void StartWorkerSampler(); +void StartCommitterSampler(); void StopWorkerSampler(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp index 78de291737..9ba7b4a1db 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp" #include #include #include @@ -142,6 +143,7 @@ void DrawPipeline::report(std::chrono::steady_clock::time_point now) { void DrawPipeline::run() { PerformanceTrace::BindThread("draw committer"); + StartCommitterSampler(); for (;;) { Commit* commit = nullptr; SpinUntil(HandoffSpinWindow(), [&] { return outstanding.load(std::memory_order_acquire) != 0; }); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp index 8b3ad92f49..7bbca06017 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp @@ -27,6 +27,7 @@ namespace { std::jthread processSamplerThread; std::jthread workerSamplerThread; +std::jthread committerSamplerThread; // Debug aid: APS5_SAMPLE_WORKER= samples the calling thread every millisecond and, every 20 s, // writes "module+offset self inclusive" lines to for offline symbolization with nm. @@ -285,11 +286,7 @@ void StartProcessSampler() { }); } -} - -void StartWorkerSampler() { - StartProcessSampler(); - const char* path = std::getenv("APS5_SAMPLE_WORKER"); +void StartCallingThreadSampler(const char* path, std::jthread& slot) { if (path == nullptr) return; auto sampler = std::make_unique(); sampler->path = path; @@ -302,7 +299,7 @@ void StartWorkerSampler() { sampler->stackHigh = reinterpret_cast(block->StackBase); } if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &sampler->target, THREAD_ALL_ACCESS, FALSE, 0)) throw std::system_error(GetLastError(), std::system_category(), "Duplicating sampler thread handle"); - workerSamplerThread = std::jthread([sampler = std::move(sampler)](std::stop_token token) { + slot = std::jthread([sampler = std::move(sampler)](std::stop_token token) { auto flushed = std::chrono::steady_clock::now(); while (!token.stop_requested()) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); @@ -316,14 +313,28 @@ void StartWorkerSampler() { }); } +} + +void StartWorkerSampler() { + StartProcessSampler(); + StartCallingThreadSampler(std::getenv("APS5_SAMPLE_WORKER"), workerSamplerThread); +} + +void StartCommitterSampler() { + StartCallingThreadSampler(std::getenv("APS5_SAMPLE_COMMITTER"), committerSamplerThread); +} + void StopWorkerSampler() { processSamplerThread.request_stop(); workerSamplerThread.request_stop(); + committerSamplerThread.request_stop(); if (processSamplerThread.joinable()) processSamplerThread.join(); if (workerSamplerThread.joinable()) workerSamplerThread.join(); + if (committerSamplerThread.joinable()) committerSamplerThread.join(); } #else void StartWorkerSampler() {} +void StartCommitterSampler() {} void StopWorkerSampler() {} #endif diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 3d1e5fd416..285587a47a 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -174,7 +174,9 @@ On a device that supports `VK_EXT_device_fault`, the first `VK_ERROR_DEVICE_LOST Set `APS5_SAMPLE_WORKER=` to sample queue worker 0 every millisecond (in practice every timer tick). Every 20 s and at exit it writes ``, one `module offset self inclusive` line per sampled address, and `.collapsed.txt`, the worker's whole stacks in the collapsed format that speedscope.app and flamegraph.pl read: one `frame;...;frame count` line per distinct stack, root first, each frame as `module+0xRVA`. A walk that ends before the thread's root starts with `[truncated]`. The `#` lines give the samples, the walk depth (64 frames), the truncated walks, the mean and longest time the worker was suspended, and the size and PE timestamp of each module, to check the binary a file is symbolized against. A frame with `.pdata` is stepped by `RtlVirtualUnwind`; any other frame by its DWARF record in `.ehfram`, the unwind data the libraries are built with. Those steps read only inside the worker's stack, and a module is indexed after the worker resumes, the first time a walk meets it. -`APS5_SAMPLE_THREADS=` samples every thread of the process every 2 ms and writes the 40 hottest addresses of each busy thread to ``, 12 frames deep and through `.pdata` only. Both samplers exist only on Windows; the addresses are symbolized offline, for example with `nm` on the `.prx`. +`APS5_SAMPLE_COMMITTER=` samples queue 0's draw committer the same way and writes the same two files. The committer records the draws the worker hands over (`APS5_PIPELINED_DRAWS`), so when the worker's stacks wait in `DrawPipeline::Enqueue`, its stacks show what bounds the frame. It starts with the first pipelined draw; with `APS5_PIPELINED_DRAWS=0` nothing is sampled. + +`APS5_SAMPLE_THREADS=` samples every thread of the process every 2 ms and writes the 40 hottest addresses of each busy thread to ``, 12 frames deep and through `.pdata` only. The samplers exist only on Windows; the addresses are symbolized offline, for example with `nm` on the `.prx`. ## Performance trace diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index a309403888..e16040281b 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -345,6 +345,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_RELEASE_UNDER_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | destroys deferred batches in finish() under the lock instead of on a release thread | | `APS5_RESOURCE_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 1024 | maximum entries in the per-device shader-resource cache | | `APS5_RESOURCE_CACHE_TABLE_ELEMENTS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 65536 | number of image-table elements the shader-resource cache keeps (0 keeps the default) | +| `APS5_SAMPLE_COMMITTER` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples queue 0's draw committer every millisecond and writes its stacks to a file (Windows only) | | `APS5_SAMPLE_THREADS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples every thread of the process every 2 ms and writes each busy thread's hottest frames to a file (Windows only) | | `APS5_SAMPLE_WORKER` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples the calling queue worker every millisecond and writes its stacks to a file (Windows only) | | `APS5_SHADOW_VERIFY` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | | verifies that the shadow-published bytes equal the guest bytes | From 0d24f581f4073fdd6e088e1463d7341ce3cee08c Mon Sep 17 00:00:00 2001 From: voidlakhan <61951686+voidlakhan@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:47:08 +0000 Subject: [PATCH 073/219] docs(ampr): preserve existing debt entries --- docs/dev/debt/libSceAmpr.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index d1303bca9d..c67c8ebe10 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -2,4 +2,17 @@ ### Unknown function info -- [sceAmprCommandBufferConstructor](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the result for a null command buffer is unknown for `sceAmprCommandBufferConstructor`, `sceAmprAprCommandBufferConstructor`, `sceAmprCommandBufferReset` and `sceAmprCommandBufferSetBuffer`; they return `SCE_KERNEL_ERROR_EINVAL` like the other command buffer exports, while KytyPS5 returns OK for the constructors and EPERM for Reset +- [AMPR command buffer null arguments](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the result for a null command buffer is unknown for `sceAmprCommandBufferConstructor`, `sceAmprAprCommandBufferConstructor`, `sceAmprCommandBufferReset` and `sceAmprCommandBufferSetBuffer`; these return `SCE_KERNEL_ERROR_EINVAL` here, but the behavior has not been confirmed on a PS5. +- [AMPR WriteAddressFromCounterPairOnCompletion](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order +- [AMPR command buffer offsets](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - recording with a current offset greater than the bound memory size throws before calculating remaining capacity or writing a command. This validates the emulator's own command-buffer encoding; the console's response to a corrupted opaque buffer object is unknown. +- [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_KERNEL_ERROR_EBUSY` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes +- [APR map begin and end](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - as in ampr_emu, `MapBegin`/`MapDirectBegin` map when they run and `MapEnd` only closes the region; while a region is open a second begin and every completion write return `EPERM`, the at-start `_04_00` writes are accepted +- [AMPR markers](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded +- [__cxa_call_unexpected](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - takes the in-flight exception's unwind header and never returns, as [libcxxabi 18.1.8](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_personality.cpp#L1171-L1211) does. No `std::set_unexpected` is exported and the exception header's unexpected handler is never set, so the handler is always the default one, which calls `std::terminate` ([cxa_default_handlers.cpp](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_default_handlers.cpp#L88-L99)); it therefore forwards to libc's `__cxa_call_terminate`, which begins the catch and runs the current terminate handler. That matches libcxxabi except for which terminate handler runs: there `std::terminate` after `__cxa_begin_catch` ([cxa_exception.cpp L470-L477](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L470-L477)) runs the handler saved in the caught exception's header ([cxa_handlers.cpp L72-L93](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_handlers.cpp#L72-L93)), which `__cxa_throw` and `__cxa_init_primary_exception` set to `std::get_terminate()` when the exception is thrown ([cxa_exception.cpp L209-L214](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L209-L214), [L279](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L279)), while here the current global handler runs, as AnyPS5's own `std::terminate` does. The two differ only if `std::set_terminate` was called between the throw and this call. A null header terminates because this emulator's libc `__cxa_begin_catch` terminates on null (libcxxabi does not define that case). Dynamic exception specifications that would let a replaced unexpected handler throw a permitted type are not modelled + +### Functional + +- [sceAmprAmmGetUsageStatsData](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) fills the caller-sized 24-byte statistics structure with 512 page-table pool entries, no allocated entries, a zero high watermark and ring idle flags 0x7, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libAmpr.cpp); they are not measured on a console, and the real call is a pass-through to `sceKernelMapperGetUsageStatsData` per [drakmor/ampr_emu](https://github.com/drakmor/ampr_emu/blob/main/src/ampr_emu_amm.cpp), which this port has no kernel-side counterpart for. +- [Dinkumware thread exports](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning. +- [_Cnd_signal, _Cnd_init_with_name](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - take a pointer to the handle slot like the libc `_Cnd_*` exports from [#3079](https://github.com/boykopovar/AnyPS5/pull/3079) and act on the same conditions. `_Cnd_signal` wakes one waiter and returns `_Thrd_success` (0), as `_Cnd_broadcast` does; whether the console maps a failing internal signal to `_Thrd_error` is not known. The signature `_Cnd_init_with_name(_Cnd_t*, const char* name)` is inferred from [fpPS4 `ps4_mtx_internal.pas`](https://github.com/red-prig/fpPS4/blob/trunk/src/libcinternal/ps4_mtx_internal.pas), where the PS4 `_Mtx_init` is `_Mtx_init_with_name` with the name `SceLibcI_Mtx` and the name only reaches the pthread object; here it is `_Cnd_init` and the name, which may be null, is ignored. Neither is confirmed on a PS5 `libc.prx`. `_Thrd_id` still throws: microsoft/STL returns an `unsigned int` thread id, but whether the PS5 returns the `pthread_t` or a kernel thread id, and at what width, is not known +- [std::_Pad](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follows Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` of Visual C++ 2013 ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. The pad is taken to be that header's class with the console's pointer handles: a vtable pointer whose first slot is the pure virtual `_Go()`, then `_Cnd_t` at 0x8, `_Mtx_t` at 0x10 and `bool _Started` at 0x18, 32 bytes, the derived `_LaunchPad` target following it; the console's `_Pad(const char*)` and `_Launch(const char*, pthread**)`/`_Launch(pthread_attr**, pthread**)` overloads suggest named primitives and could mean another layout or vtable. `_Launch` starts the thread with `pthread_create` and default attributes (the console's `_Thrd_start` name, stack size and priority are unknown), its entry calls `_Go()` and returns the result as the exit code, and it does not run `_Cnd_do_broadcast_at_thread_exit`, which is absent here. A failing primitive throws the guest `std::system_error` that `_Throw_C_error` picks for its code, and a failing thread start the `resource_unavailable_try_again` one of `_Thrd_startX`; any `pthread_create` error counts, since the console's `_Thrd_start` result is unknown. A null pad or thread slot throws From 1231265e5037356120bc9c8ccdc6eb09a92559a9 Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Mon, 19 Oct 2026 23:01:29 +0300 Subject: [PATCH 074/219] fix(agc): accept a draw without a pixel program while CB_COLOR_CONTROL disables color A draw with no pixel program was rejected as writing color whenever CB_TARGET_MASK and CB_SHADER_MASK overlap, even with CB_COLOR_CONTROL mode 0 (CB_DISABLE). DecodeState already drops every color export in that mode, so such a draw writes no color: a depth or stencil decompress pass draws this way, leaving the masks of the previous draw. An unwritten CB_COLOR_CONTROL still leaves the decision to the masks. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 4 +++- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index eba588931f..3f3b97d45b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1967,7 +1967,9 @@ std::string NullPixelProgramRejection(const QueueState& queue) { const auto targetMask = find(queue.context, 0x8e); const auto shaderMask = find(queue.context, 0x8f); if (targetMask == queue.context.end() || shaderMask == queue.context.end()) return "AGC graphics: a draw without a pixel program needs CB_TARGET_MASK and CB_SHADER_MASK"; - if (ColorWriteMask(queue.context) != 0) return "AGC graphics: a draw without a pixel program writes color"; + const auto colorControl = find(queue.context, 0x202); + const bool colorDisabled = colorControl != queue.context.end() && ((colorControl->second >> 4u) & 7u) == 0u; + if (!colorDisabled && ColorWriteMask(queue.context) != 0) return "AGC graphics: a draw without a pixel program writes color"; return {}; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index aa15f4523f..c58c4a81ff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -519,6 +519,12 @@ void stateTests() { queue.shader[0x008] = 0; Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "a zero pixel program address was not read as unset"); Require(AgcDriver::Graphics::DrawRejection(queue, true).find("writes color") != std::string::npos, "a draw without a pixel program that writes color was accepted"); + auto colorDisabled = queue; + colorDisabled.context[0x202] = colorDisabled.context.at(0x202) & ~0x70u; + Require(AgcDriver::Graphics::DrawRejection(colorDisabled, true).empty(), "a draw without a pixel program was rejected while CB_COLOR_CONTROL disables color"); + Require(!AgcDriver::Graphics::DecodeState(colorDisabled).hasColorTarget, "a draw with CB_COLOR_CONTROL disabling color decoded a color target"); + colorDisabled.context.erase(0x202); + Require(AgcDriver::Graphics::DrawRejection(colorDisabled, true).find("writes color") != std::string::npos, "a draw without a pixel program and an unwritten CB_COLOR_CONTROL was accepted"); queue.shader.erase(0x008); queue.shader.erase(0x009); Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "an unwritten pixel program address was not read as unset"); From 84a6fb0fe5956ab6a1cc29d3529cdcef4052ad9c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 21:04:35 +0000 Subject: [PATCH 075/219] test(agc): register the CPU thread-dimension test in its own block The agc_driver_indirect_thread_dimensions_cpu registration sat at the end of the address-passes test block, where other open changes add their tests too. It now has its own BUILD_TESTING block after the execution test name check, with the same properties. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 82a6f94fbd..78f2018ab7 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1776,6 +1776,13 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() +endif() +foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS) + if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/${agcExecutionTestName}.cpp) + message(FATAL_ERROR "ANYPS5_AGC_EXECUTION_TESTS names ${agcExecutionTestName}, which has no source in tests/execution") + endif() +endforeach() +if(BUILD_TESTING) add_test(NAME agc_driver_indirect_thread_dimensions_cpu COMMAND agc_driver_indirect_thread_dimensions_tests) set_tests_properties(agc_driver_indirect_thread_dimensions_cpu PROPERTIES ENVIRONMENT "APS5_NO_GPU_THREAD_DIMENSIONS=1" SKIP_RETURN_CODE 77 TIMEOUT 60) if(WIN32) @@ -1783,11 +1790,6 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() -foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS) - if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/${agcExecutionTestName}.cpp) - message(FATAL_ERROR "ANYPS5_AGC_EXECUTION_TESTS names ${agcExecutionTestName}, which has no source in tests/execution") - endif() -endforeach() if(ANYPS5_ENABLE_SPIRV_TOOLS) target_compile_definitions(agc_driver_stencil_null_feedback_tests PRIVATE ANYPS5_ENABLE_SPIRV_TOOLS=1) target_link_libraries(agc_driver_stencil_null_feedback_tests PRIVATE SPIRV-Tools-static) From d128ce7bb833f8e5d5cfb10c21baf73cba73077d Mon Sep 17 00:00:00 2001 From: Ishmam Ahmed <89687266+IshmamDC217@users.noreply.github.com> Date: Sun, 18 Oct 2026 06:09:40 +0100 Subject: [PATCH 076/219] feat(libc): record traced system library calls on Linux Build the call tracer for Linux as well as Windows. The thunk area is a memfd mapped twice, writable and executable, and threads are named by gettid. Linux has no crash report, so the first traced import installs a SIGABRT handler that prints the last calls and the most called imports, then passes the signal on to the previous handler. guest_call_trace and guest_call_trace_abort now run on Linux too. --- core/libs/prx/libc/CMakeLists.txt | 12 +- .../{windows => x86_64}/CallTrace.cpp | 110 +++++++++++++++++- core/libs/tests/GuestCallTrace.cpp | 45 ++++++- docs/dev/ENVIRONMENT.md | 2 +- docs/dev/debt/libc.md | 2 +- 5 files changed, 156 insertions(+), 15 deletions(-) rename core/libs/prx/libc/src/specifics/{windows => x86_64}/CallTrace.cpp (63%) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index db26a5c925..b8674dd62c 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -30,7 +30,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/FailureReport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/gcc/StackGuard.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/CrashReport.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/CallTrace.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/x86_64/CallTrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/linux/DlIteratePhdr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/macos/GuestImages.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Strings.cpp @@ -572,17 +572,19 @@ if(BUILD_TESTING) set_tests_properties(guest_errors PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - if(WIN32) + if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux") add_test_executable(guest_call_trace_tests ${LIBS_INCLUDE_DIR}/tests/GuestCallTrace.cpp) target_include_directories(guest_call_trace_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_call_trace_tests PRIVATE libc) configure_windows_unwind(guest_call_trace_tests) add_test(NAME guest_call_trace COMMAND guest_call_trace_tests) add_test(NAME guest_call_trace_abort COMMAND guest_call_trace_tests abort) - set_tests_properties(guest_call_trace guest_call_trace_abort PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") + if(WIN32) + set_tests_properties(guest_call_trace guest_call_trace_abort PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() set_tests_properties(guest_call_trace_abort PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION - "last traced system library calls[^~]*#1 thread [0-9]+ libTraced.prx Integers from [^~]*guest_call_trace_tests.exe[^~]*#2 thread [0-9]+ libOther.prx Blocks from[^~]*most called traced imports \\(2 calls in total\\):[^~]* 1 libTraced.prx Integers[^~]* 1 libOther.prx Blocks") + "last traced system library calls[^~]*#1 thread [0-9]+ libTraced.prx Integers from [^~]*guest_call_trace_tests${CMAKE_EXECUTABLE_SUFFIX}[^~]*#2 thread [0-9]+ libOther.prx Blocks from[^~]*most called traced imports \\(2 calls in total\\):[^~]* 1 libTraced.prx Integers[^~]* 1 libOther.prx Blocks") endif() add_test_executable(guest_syslog_tests ${LIBS_INCLUDE_DIR}/tests/GuestSyslog.cpp) target_include_directories(guest_syslog_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libc/src/specifics/windows/CallTrace.cpp b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp similarity index 63% rename from core/libs/prx/libc/src/specifics/windows/CallTrace.cpp rename to core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp index 7e30971457..789f029ac2 100644 --- a/core/libs/prx/libc/src/specifics/windows/CallTrace.cpp +++ b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp @@ -1,5 +1,14 @@ +#if defined(_WIN32) || defined(__linux__) #ifdef _WIN32 #include +#else +#include +#include +#include +#include +#include +#include +#endif #include "prx/libc/include/CallTrace.hpp" #include "prx/libc/include/general/AsmMacros.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -62,6 +71,15 @@ bool Selected(std::string_view library) { return false; } +std::uint64_t CurrentThread() { +#ifdef _WIN32 + return GetCurrentThreadId(); +#else + return static_cast(gettid()); +#endif +} + +#ifdef _WIN32 void MapThunkArea() { const HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, static_cast(ThunkAreaBytes), nullptr); if (section == nullptr) throw std::system_error(static_cast(GetLastError()), std::system_category(), "call trace: CreateFileMappingW"); @@ -71,6 +89,86 @@ void MapThunkArea() { CloseHandle(section); if (g_writable == nullptr || g_executable == nullptr) throw std::system_error(static_cast(code), std::system_category(), "call trace: MapViewOfFile"); } +#else +void MapThunkArea() { + const int file = memfd_create("aps5-call-trace", MFD_CLOEXEC); + if (file < 0) throw std::system_error(errno, std::generic_category(), "call trace: memfd_create"); + void* writable = MAP_FAILED; + void* executable = MAP_FAILED; + if (ftruncate(file, ThunkAreaBytes) == 0) { + writable = mmap(nullptr, ThunkAreaBytes, PROT_READ | PROT_WRITE, MAP_SHARED, file, 0); + if (writable != MAP_FAILED) executable = mmap(nullptr, ThunkAreaBytes, PROT_READ | PROT_EXEC, MAP_SHARED, file, 0); + } + const int code = errno; + close(file); + if (writable == MAP_FAILED || executable == MAP_FAILED) { + if (writable != MAP_FAILED) munmap(writable, ThunkAreaBytes); + throw std::system_error(code, std::generic_category(), "call trace: mmap"); + } + g_writable = static_cast(writable); + g_executable = static_cast(executable); +} + +struct sigaction g_previousAbort{}; + +void DescribeAddress(std::uint64_t address, char* buffer, std::size_t size) { + Dl_info info{}; + if (dladdr(reinterpret_cast(address), &info) != 0 && info.dli_fname != nullptr && info.dli_fbase != nullptr) { + const char* slash = std::strrchr(info.dli_fname, '/'); + const char* name = slash == nullptr ? info.dli_fname : slash + 1; + std::snprintf(buffer, size, "0x%016llx %s+0x%llx", static_cast(address), *name == '\0' ? "?" : name, + static_cast(address - reinterpret_cast(info.dli_fbase))); + return; + } + std::snprintf(buffer, size, "0x%016llx", static_cast(address)); +} + +void ReportCallTrace() { + static CallTraceEntry entries[64]; + const std::size_t count = CopyCallTrace_nid_no_patch(entries, std::size(entries)); + if (count == 0) return; + char line[4096 + 64]; + std::fprintf(stderr, " last traced system library calls (APS5_TRACE_CALLS):\n"); + for (std::size_t index = 0; index < count; ++index) { + DescribeAddress(entries[index].returnAddress, line, sizeof(line)); + std::fprintf(stderr, " #%llu thread %llu %s %s from %s\n", static_cast(entries[index].sequence), + static_cast(entries[index].thread), entries[index].library, entries[index].name, line); + } + static CallTraceCount counts[32]; + std::uint64_t total = 0; + const std::size_t counted = CopyCallCounts_nid_no_patch(counts, std::size(counts), &total); + if (counted == 0) return; + std::fprintf(stderr, " most called traced imports (%llu calls in total):\n", static_cast(total)); + for (std::size_t index = 0; index < counted; ++index) { + std::fprintf(stderr, " %12llu %s %s\n", static_cast(counts[index].calls), counts[index].library, counts[index].name); + } +} + +void ReportOnAbort(int number, siginfo_t* info, void* context) { + std::fprintf(stderr, "[libc] SIGABRT\n"); + ReportCallTrace(); + std::fflush(stderr); + if ((g_previousAbort.sa_flags & SA_SIGINFO) != 0) { + if (g_previousAbort.sa_sigaction != nullptr) g_previousAbort.sa_sigaction(number, info, context); + return; + } + if (g_previousAbort.sa_handler == SIG_IGN) return; + if (g_previousAbort.sa_handler != SIG_DFL) { + g_previousAbort.sa_handler(number); + return; + } + std::signal(number, SIG_DFL); + std::raise(number); +} + +void InstallAbortReport() { + struct sigaction action{}; + action.sa_sigaction = ReportOnAbort; + action.sa_flags = SA_SIGINFO; + sigemptyset(&action.sa_mask); + if (sigaction(SIGABRT, &action, &g_previousAbort) != 0) throw std::system_error(errno, std::generic_category(), "call trace: sigaction"); +} +#endif void* WriteThunk(const ImportRecord& record) { if ((g_thunks + 1) * ThunkBytes > ThunkAreaBytes) throw std::runtime_error("call trace: thunk area exhausted"); @@ -84,7 +182,11 @@ void* WriteThunk(const ImportRecord& record) { std::memcpy(thunk + 2, &recordAddress, sizeof(recordAddress)); std::memcpy(thunk + 10, jump, sizeof(jump)); std::memcpy(thunk + 16, &handler, sizeof(handler)); +#ifdef _WIN32 FlushInstructionCache(GetCurrentProcess(), g_executable + offset, ThunkBytes); +#else + __builtin___clear_cache(reinterpret_cast(g_executable + offset), reinterpret_cast(g_executable + offset + ThunkBytes)); +#endif ++g_thunks; return g_executable + offset; } @@ -100,7 +202,7 @@ void APS5_VABI CallTraceRecord_nid_no_patch(const void* record, std::uint64_t re const std::uint64_t sequence = g_sequence.fetch_add(1, std::memory_order_relaxed) + 1; RingSlot& slot = g_ring[sequence % RingCapacity]; slot.sequence.store(0, std::memory_order_release); - slot.thread.store(GetCurrentThreadId(), std::memory_order_relaxed); + slot.thread.store(CurrentThread(), std::memory_order_relaxed); slot.returnAddress.store(returnAddress, std::memory_order_relaxed); slot.record.store(static_cast(record), std::memory_order_relaxed); slot.sequence.store(sequence, std::memory_order_release); @@ -112,6 +214,9 @@ void* TraceImport_nid_no_patch(void* target, const char* name, const char* libra if (g_ring == nullptr) { MapThunkArea(); g_ring = std::make_unique(RingCapacity); +#ifdef __linux__ + InstallAbortReport(); +#endif } if (const auto existing = g_thunkByTarget.find(target); existing != g_thunkByTarget.end()) return existing->second; g_records.emplace_back(target, name, library); @@ -198,5 +303,6 @@ asm(".text\n" " pop %rdx\n" " pop %rsi\n" " pop %rdi\n" - " jmp *(%r11)\n"); + " jmp *(%r11)\n" + APS5_ASM_FUNCTION_END("CallTraceEnter_nid_no_patch")); #endif diff --git a/core/libs/tests/GuestCallTrace.cpp b/core/libs/tests/GuestCallTrace.cpp index 4be35302dc..57a54c6fa7 100644 --- a/core/libs/tests/GuestCallTrace.cpp +++ b/core/libs/tests/GuestCallTrace.cpp @@ -1,6 +1,12 @@ #include "prx/libc/include/CallTrace.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#ifdef _WIN32 #include +#else +#include +#include +#endif +#include #include #include #include @@ -49,18 +55,47 @@ static Block APS5_VABI Blocks(std::uint64_t seed) { return {{seed, seed + 1, seed + 2, seed + 3}}; } +static std::uint64_t CurrentThread() { +#ifdef _WIN32 + return GetCurrentThreadId(); +#else + return static_cast(gettid()); +#endif +} + +static const void* ImageOf(const void* address) { +#ifdef _WIN32 + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(address, &memory, sizeof(memory)) != sizeof(memory)) return nullptr; + return memory.AllocationBase; +#else + Dl_info info{}; + if (dladdr(address, &info) == 0) return nullptr; + return info.dli_fbase; +#endif +} + template static TFunction* Traced(TFunction* function, const char* name, const char* library) { return reinterpret_cast(TraceImport_nid_no_patch(reinterpret_cast(function), name, library)); } int main(int argc, char** argv) { +#ifdef _WIN32 Require(_putenv_s("APS5_TRACE_CALLS", "libTraced,libOther.prx") == 0); +#else + Require(setenv("APS5_TRACE_CALLS", "libTraced,libOther.prx", 1) == 0); +#endif if (argc > 1 && std::strcmp(argv[1], "abort") == 0) { +#ifndef _WIN32 + Require(std::signal(SIGABRT, [](int) { _exit(3); }) != SIG_ERR); +#endif const auto first = Traced(&Integers, "Integers", "libTraced.prx"); const auto second = Traced(&Blocks, "Blocks", "libOther.prx"); Require(first(1, 2, 3, 4, 5, 6, 7, 8) == 204 && second(7).words[3] == 10); +#ifdef _WIN32 _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); +#endif std::abort(); } CallTraceEntry entries[16]; @@ -90,15 +125,13 @@ int main(int argc, char** argv) { const char* const names[] = {"Integers", "Doubles", "Mixed", "Variadic", "Blocks"}; const char* const libraries[] = {"libTraced.prx", "libTraced", "libOther.prx", "libOther", "libTraced.prx"}; - MEMORY_BASIC_INFORMATION image{}; - Require(VirtualQuery(reinterpret_cast(&main), &image, sizeof(image)) == sizeof(image)); + const void* image = ImageOf(reinterpret_cast(&main)); + Require(image != nullptr); Require(CopyCallTrace_nid_no_patch(entries, 16) == 5); for (std::uint64_t index = 0; index < 5; ++index) { - Require(entries[index].sequence == index + 1 && entries[index].thread == GetCurrentThreadId()); + Require(entries[index].sequence == index + 1 && entries[index].thread == CurrentThread()); Require(std::strcmp(entries[index].name, names[index]) == 0 && std::strcmp(entries[index].library, libraries[index]) == 0); - MEMORY_BASIC_INFORMATION caller{}; - Require(VirtualQuery(reinterpret_cast(entries[index].returnAddress), &caller, sizeof(caller)) == sizeof(caller)); - Require(caller.AllocationBase == image.AllocationBase); + Require(ImageOf(reinterpret_cast(entries[index].returnAddress)) == image); } Require(CopyCallTrace_nid_no_patch(entries, 2) == 2 && entries[0].sequence == 4 && entries[1].sequence == 5); Require(CopyCallCounts_nid_no_patch(counts, 8, &total) == 5 && total == 5); diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index a309403888..fca7db5904 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -528,7 +528,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_WRITE_WATCH` | presence | `core/libs/prx/libc/src/GuestArena.cpp` | | disables write watching so guest buffers are fully re-read on every use | | `APS5_PIN_WAIT_MS` | integer | `core/libs/prx/libc/src/GuestAllocations.cpp` | 60000 | milliseconds to wait for a pinned allocation lease before failing | | `APS5_TRACE_AFFINITY` | presence | `core/libs/prx/libc/include/CpuTopology.hpp` | | prints a line naming the role, tid and mask of every thread pinning | -| `APS5_TRACE_CALLS` | string | `core/libs/prx/libc/src/specifics/windows/CallTrace.cpp` | | comma-separated libraries (or all) whose guest calls are traced | +| `APS5_TRACE_CALLS` | string | `core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp` | | comma-separated libraries (or all) whose guest calls are traced | | `APS5_TRACE_EXIT` | presence | `core/libs/prx/libc/src/Process.cpp` | | prints a backtrace of the caller of exit, _Exit and the process-exit path | | `APS5_TRACE_SSE4A` | presence | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | prints each emulated SSE4a instruction once per rip | | `APS5_WATCH_ADDR` | integer | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | absolute hex address whose writes are reported after its page is committed | diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d22c98aa20..b41c529cf9 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -113,7 +113,7 @@ - [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The lock is a host [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) lock on one byte at offset 2^63 - 2, which no file reaches, so it excludes other `flock` calls and leaves reads, writes and mapped views alone, like FreeBSD's advisory locks. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, selected by the first matching errno entry in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects::_Generic_object`, and the new `generic_category()` export returns that object; a title's inline category object in a bundled libc can still have a different identity. Its `message` and direct message export construct the existing [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) model: a reserved eight-byte word, 16-byte inline buffer, length and capacity, with guest `operator new` storage above 15 bytes. This 40-byte string layout and virtual return ABI have not been verified against a console library. The deleting destructor still throws. `_Syserror_map` covers the 72 distinct Dinkumware `errc` values present in FreeBSD 11.4 [errno.h](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h), returning null for other values; category messages use `unknown error` for those. First-match aliases are `EAGAIN`/`EWOULDBLOCK` -> `operation would block` and `ENOTSUP`/`EOPNOTSUPP` -> `not supported`. STREAMS-only errno assignments absent from that header (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are not inferred -- [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested +- [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) exists on Windows and Linux, not macOS, and records the entry of a call, not its arguments or its result. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested. On Linux the trace is printed only on `SIGABRT`, which covers guest and host `abort()`; Linux has no crash report, so a fatal signal such as `SIGSEGV` ends the process without it - [_Dtest](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows Dinkumware double classification matching _FDtest: finite values return -1, subnormals -2, zeros 0, infinities 1, and NaNs 2. - [app0.zar](../../../core/libs/prx/libc/src/AppArchive.cpp) (libc) - a [ZArchive](https://github.com/Exzap/ZArchive) beside `app0/` is a read-only layer under it: a path missing from `app0/` is looked up in the archive, case-insensitively. It is not a console format; titles ship their files in a PFS image instead. Resolving an archive-only path through any call other than the archive-aware libkernel ones throws, so `fopen`, `stat`, `access`, `opendir` and the rest do not read the archive yet, and `opendir` of a directory that exists in both places, `app0/` itself included, and `fdopen` of an archived descriptor throw instead of missing its entries or reading the null device. A damaged compressed block throws instead of returning `EIO` - [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified From 04547267440334ca128467df53328610a5599c29 Mon Sep 17 00:00:00 2001 From: Ishmam Ahmed <89687266+IshmamDC217@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:09:16 +0100 Subject: [PATCH 077/219] fix(libc): write the Linux call trace report with signal-safe calls only The SIGABRT handler used stdio and dladdr, so an abort raised while another thread held the stderr lock or the loader lock hung instead of ending. The report is now formatted into a stack buffer and written with write(2). Callers are named from a module list taken with dl_iterate_phdr when imports are traced, and import counts are read without a lock from a fixed record array. The abort test now holds the stderr lock in another thread, which timed out before this change. --- .../libc/src/specifics/x86_64/CallTrace.cpp | 193 ++++++++++++++---- core/libs/tests/GuestCallTrace.cpp | 9 + docs/dev/debt/libc.md | 2 +- 3 files changed, 159 insertions(+), 45 deletions(-) diff --git a/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp index 789f029ac2..a1033c1099 100644 --- a/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp +++ b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp @@ -4,8 +4,7 @@ #else #include #include -#include -#include +#include #include #include #endif @@ -16,8 +15,6 @@ #include #include #include -#include -#include #include #include #include @@ -30,7 +27,7 @@ extern "C" void CallTraceEnter_nid_no_patch(); namespace { struct ImportRecord { - void* target; + void* target = nullptr; std::string name; std::string library; mutable std::atomic calls{0}; @@ -46,15 +43,16 @@ struct RingSlot { constexpr std::size_t RingCapacity = 65536; constexpr std::size_t ThunkBytes = 24; constexpr std::size_t ThunkAreaBytes = 1024 * 1024; +constexpr std::size_t RecordCapacity = ThunkAreaBytes / ThunkBytes; std::mutex g_mutex; -std::deque g_records; std::unordered_map g_thunkByTarget; -std::unique_ptr g_ring; +std::atomic g_records{nullptr}; +std::atomic g_recordCount{0}; +std::atomic g_ring{nullptr}; std::atomic g_sequence{0}; std::uint8_t* g_writable = nullptr; std::uint8_t* g_executable = nullptr; -std::size_t g_thunks = 0; bool Selected(std::string_view library) { const char* value = std::getenv("APS5_TRACE_CALLS"); @@ -109,45 +107,139 @@ void MapThunkArea() { g_executable = static_cast(executable); } -struct sigaction g_previousAbort{}; +struct ModuleSegment { + std::uint64_t start; + std::uint64_t end; + std::uint64_t base; + char name[64]; +}; -void DescribeAddress(std::uint64_t address, char* buffer, std::size_t size) { - Dl_info info{}; - if (dladdr(reinterpret_cast(address), &info) != 0 && info.dli_fname != nullptr && info.dli_fbase != nullptr) { - const char* slash = std::strrchr(info.dli_fname, '/'); - const char* name = slash == nullptr ? info.dli_fname : slash + 1; - std::snprintf(buffer, size, "0x%016llx %s+0x%llx", static_cast(address), *name == '\0' ? "?" : name, - static_cast(address - reinterpret_cast(info.dli_fbase))); +struct ModuleMap { + std::size_t count; + ModuleSegment segments[256]; +}; + +struct sigaction g_previousAbort{}; +std::atomic g_modules{nullptr}; +std::atomic g_reporting{false}; +unsigned long long g_moduleChanges = ~0ull; + +void SnapshotModules() { + unsigned long long changes = 0; + dl_iterate_phdr([](dl_phdr_info* info, std::size_t, void* data) { + *static_cast(data) = info->dlpi_adds + info->dlpi_subs; + return 1; + }, &changes); + if (changes == g_moduleChanges) return; + g_moduleChanges = changes; + auto* map = new ModuleMap{}; + dl_iterate_phdr([](dl_phdr_info* info, std::size_t, void* data) { + auto& modules = *static_cast(data); + const char* path = info->dlpi_name != nullptr && *info->dlpi_name != '\0' ? info->dlpi_name : program_invocation_short_name; + const char* slash = std::strrchr(path, '/'); + const char* name = slash == nullptr ? path : slash + 1; + for (int index = 0; index < info->dlpi_phnum && modules.count < std::size(modules.segments); ++index) { + const auto& header = info->dlpi_phdr[index]; + if (header.p_type != PT_LOAD || header.p_memsz == 0) continue; + auto& segment = modules.segments[modules.count++]; + segment.base = info->dlpi_addr; + segment.start = info->dlpi_addr + header.p_vaddr; + segment.end = segment.start + header.p_memsz; + std::strncpy(segment.name, name, sizeof(segment.name) - 1); + } + return 0; + }, map); + g_modules.store(map, std::memory_order_release); +} + +class ReportLine { +public: + ReportLine& Text(const char* text) { + while (*text != '\0' && _length < sizeof(_buffer)) _buffer[_length++] = *text++; + return *this; + } + + ReportLine& Decimal(std::uint64_t value, std::size_t width = 0) { + char digits[20]; + std::size_t count = 0; + do { + digits[count++] = static_cast('0' + value % 10); + value /= 10; + } while (value != 0); + for (std::size_t pad = count; pad < width; ++pad) Text(" "); + while (count > 0 && _length < sizeof(_buffer)) _buffer[_length++] = digits[--count]; + return *this; + } + + ReportLine& Hex(std::uint64_t value, std::size_t digits) { + while (digits < 16 && (value >> (digits * 4)) != 0) ++digits; + Text("0x"); + for (std::size_t index = digits; index > 0 && _length < sizeof(_buffer); --index) { + _buffer[_length++] = "0123456789abcdef"[(value >> ((index - 1) * 4)) & 0xf]; + } + return *this; + } + + void Write() { + Text("\n"); + std::size_t written = 0; + while (written < _length) { + const ssize_t result = ::write(STDERR_FILENO, _buffer + written, _length - written); + if (result < 0 && errno == EINTR) continue; + if (result <= 0) break; + written += static_cast(result); + } + _length = 0; + } + +private: + char _buffer[512]; + std::size_t _length = 0; +}; + +void DescribeAddress(ReportLine& line, std::uint64_t address) { + line.Hex(address, 16); + const ModuleMap* modules = g_modules.load(std::memory_order_acquire); + if (modules == nullptr) return; + for (std::size_t index = 0; index < modules->count; ++index) { + const auto& segment = modules->segments[index]; + if (address < segment.start || address >= segment.end) continue; + line.Text(" ").Text(segment.name).Text("+"); + line.Hex(address - segment.base, 1); return; } - std::snprintf(buffer, size, "0x%016llx", static_cast(address)); } void ReportCallTrace() { static CallTraceEntry entries[64]; + static CallTraceCount counts[32]; + ReportLine line; const std::size_t count = CopyCallTrace_nid_no_patch(entries, std::size(entries)); if (count == 0) return; - char line[4096 + 64]; - std::fprintf(stderr, " last traced system library calls (APS5_TRACE_CALLS):\n"); + line.Text(" last traced system library calls (APS5_TRACE_CALLS):").Write(); for (std::size_t index = 0; index < count; ++index) { - DescribeAddress(entries[index].returnAddress, line, sizeof(line)); - std::fprintf(stderr, " #%llu thread %llu %s %s from %s\n", static_cast(entries[index].sequence), - static_cast(entries[index].thread), entries[index].library, entries[index].name, line); + line.Text(" #").Decimal(entries[index].sequence).Text(" thread ").Decimal(entries[index].thread); + line.Text(" ").Text(entries[index].library).Text(" ").Text(entries[index].name).Text(" from "); + DescribeAddress(line, entries[index].returnAddress); + line.Write(); } - static CallTraceCount counts[32]; std::uint64_t total = 0; const std::size_t counted = CopyCallCounts_nid_no_patch(counts, std::size(counts), &total); if (counted == 0) return; - std::fprintf(stderr, " most called traced imports (%llu calls in total):\n", static_cast(total)); + line.Text(" most called traced imports (").Decimal(total).Text(" calls in total):").Write(); for (std::size_t index = 0; index < counted; ++index) { - std::fprintf(stderr, " %12llu %s %s\n", static_cast(counts[index].calls), counts[index].library, counts[index].name); + line.Text(" ").Decimal(counts[index].calls, 12).Text(" ").Text(counts[index].library).Text(" ").Text(counts[index].name).Write(); } } void ReportOnAbort(int number, siginfo_t* info, void* context) { - std::fprintf(stderr, "[libc] SIGABRT\n"); - ReportCallTrace(); - std::fflush(stderr); + const int savedErrno = errno; + if (!g_reporting.exchange(true, std::memory_order_acq_rel)) { + ReportLine line; + line.Text("[libc] SIGABRT").Write(); + ReportCallTrace(); + } + errno = savedErrno; if ((g_previousAbort.sa_flags & SA_SIGINFO) != 0) { if (g_previousAbort.sa_sigaction != nullptr) g_previousAbort.sa_sigaction(number, info, context); return; @@ -157,8 +249,11 @@ void ReportOnAbort(int number, siginfo_t* info, void* context) { g_previousAbort.sa_handler(number); return; } - std::signal(number, SIG_DFL); - std::raise(number); + struct sigaction fallback{}; + fallback.sa_handler = SIG_DFL; + sigemptyset(&fallback.sa_mask); + sigaction(number, &fallback, nullptr); + raise(number); } void InstallAbortReport() { @@ -170,9 +265,8 @@ void InstallAbortReport() { } #endif -void* WriteThunk(const ImportRecord& record) { - if ((g_thunks + 1) * ThunkBytes > ThunkAreaBytes) throw std::runtime_error("call trace: thunk area exhausted"); - const std::size_t offset = g_thunks * ThunkBytes; +void* WriteThunk(const ImportRecord& record, const std::size_t index) { + const std::size_t offset = index * ThunkBytes; const std::uint8_t prefix[] = {0x49, 0xbb}; const std::uint8_t jump[] = {0xff, 0x25, 0, 0, 0, 0}; const auto recordAddress = reinterpret_cast(&record); @@ -187,7 +281,6 @@ void* WriteThunk(const ImportRecord& record) { #else __builtin___clear_cache(reinterpret_cast(g_executable + offset), reinterpret_cast(g_executable + offset + ThunkBytes)); #endif - ++g_thunks; return g_executable + offset; } @@ -200,7 +293,7 @@ extern "C" { void APS5_VABI CallTraceRecord_nid_no_patch(const void* record, std::uint64_t returnAddress) { static_cast(record)->calls.fetch_add(1, std::memory_order_relaxed); const std::uint64_t sequence = g_sequence.fetch_add(1, std::memory_order_relaxed) + 1; - RingSlot& slot = g_ring[sequence % RingCapacity]; + RingSlot& slot = g_ring.load(std::memory_order_relaxed)[sequence % RingCapacity]; slot.sequence.store(0, std::memory_order_release); slot.thread.store(CurrentThread(), std::memory_order_relaxed); slot.returnAddress.store(returnAddress, std::memory_order_relaxed); @@ -211,27 +304,38 @@ void APS5_VABI CallTraceRecord_nid_no_patch(const void* record, std::uint64_t re void* TraceImport_nid_no_patch(void* target, const char* name, const char* library) { if (target == nullptr || name == nullptr || library == nullptr || !Selected(library)) return target; std::lock_guard lock(g_mutex); - if (g_ring == nullptr) { + if (g_ring.load(std::memory_order_relaxed) == nullptr) { MapThunkArea(); - g_ring = std::make_unique(RingCapacity); + g_records.store(new ImportRecord[RecordCapacity], std::memory_order_release); + g_ring.store(new RingSlot[RingCapacity], std::memory_order_release); #ifdef __linux__ InstallAbortReport(); #endif } +#ifdef __linux__ + SnapshotModules(); +#endif if (const auto existing = g_thunkByTarget.find(target); existing != g_thunkByTarget.end()) return existing->second; - g_records.emplace_back(target, name, library); - void* thunk = WriteThunk(g_records.back()); + const std::size_t index = g_recordCount.load(std::memory_order_relaxed); + if (index == RecordCapacity) throw std::runtime_error("call trace: thunk area exhausted"); + ImportRecord& record = g_records.load(std::memory_order_relaxed)[index]; + record.target = target; + record.name = name; + record.library = library; + void* thunk = WriteThunk(record, index); g_thunkByTarget.emplace(target, thunk); + g_recordCount.store(index + 1, std::memory_order_release); return thunk; } std::size_t CopyCallTrace_nid_no_patch(CallTraceEntry* entries, std::size_t capacity) { - if (entries == nullptr || capacity == 0 || g_ring == nullptr) return 0; + const RingSlot* ring = g_ring.load(std::memory_order_acquire); + if (entries == nullptr || capacity == 0 || ring == nullptr) return 0; const std::uint64_t last = g_sequence.load(std::memory_order_acquire); const std::uint64_t span = std::min({last, capacity, RingCapacity}); std::size_t count = 0; for (std::uint64_t sequence = last - span + 1; sequence <= last; ++sequence) { - const RingSlot& slot = g_ring[sequence % RingCapacity]; + const RingSlot& slot = ring[sequence % RingCapacity]; if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; const auto* record = slot.record.load(std::memory_order_relaxed); const CallTraceEntry entry{sequence, slot.thread.load(std::memory_order_relaxed), slot.returnAddress.load(std::memory_order_relaxed), record->name.c_str(), record->library.c_str()}; @@ -244,10 +348,11 @@ std::size_t CopyCallTrace_nid_no_patch(CallTraceEntry* entries, std::size_t capa std::size_t CopyCallCounts_nid_no_patch(CallTraceCount* entries, std::size_t capacity, std::uint64_t* total) { if (total != nullptr) *total = g_sequence.load(std::memory_order_acquire); if (entries == nullptr || capacity == 0) return 0; - std::unique_lock lock(g_mutex, std::try_to_lock); - if (!lock.owns_lock()) return 0; + const std::size_t recorded = g_recordCount.load(std::memory_order_acquire); + const ImportRecord* records = g_records.load(std::memory_order_acquire); std::size_t count = 0; - for (const auto& record : g_records) { + for (std::size_t index = 0; index < recorded; ++index) { + const ImportRecord& record = records[index]; const std::uint64_t calls = record.calls.load(std::memory_order_relaxed); if (calls == 0 || (count == capacity && calls <= entries[count - 1].calls)) continue; std::size_t position = count < capacity ? count++ : count - 1; diff --git a/core/libs/tests/GuestCallTrace.cpp b/core/libs/tests/GuestCallTrace.cpp index 57a54c6fa7..ff67cf832d 100644 --- a/core/libs/tests/GuestCallTrace.cpp +++ b/core/libs/tests/GuestCallTrace.cpp @@ -6,6 +6,7 @@ #include #include #endif +#include #include #include #include @@ -95,6 +96,14 @@ int main(int argc, char** argv) { Require(first(1, 2, 3, 4, 5, 6, 7, 8) == 204 && second(7).words[3] == 10); #ifdef _WIN32 _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); +#else + std::atomic locked{false}; + std::thread([&locked] { + flockfile(stderr); + locked.store(true); + for (;;) pause(); + }).detach(); + while (!locked.load()) std::this_thread::yield(); #endif std::abort(); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index b41c529cf9..1b39ef30d2 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -113,7 +113,7 @@ - [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The lock is a host [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) lock on one byte at offset 2^63 - 2, which no file reaches, so it excludes other `flock` calls and leaves reads, writes and mapped views alone, like FreeBSD's advisory locks. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, selected by the first matching errno entry in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects::_Generic_object`, and the new `generic_category()` export returns that object; a title's inline category object in a bundled libc can still have a different identity. Its `message` and direct message export construct the existing [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) model: a reserved eight-byte word, 16-byte inline buffer, length and capacity, with guest `operator new` storage above 15 bytes. This 40-byte string layout and virtual return ABI have not been verified against a console library. The deleting destructor still throws. `_Syserror_map` covers the 72 distinct Dinkumware `errc` values present in FreeBSD 11.4 [errno.h](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h), returning null for other values; category messages use `unknown error` for those. First-match aliases are `EAGAIN`/`EWOULDBLOCK` -> `operation would block` and `ENOTSUP`/`EOPNOTSUPP` -> `not supported`. STREAMS-only errno assignments absent from that header (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are not inferred -- [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) exists on Windows and Linux, not macOS, and records the entry of a call, not its arguments or its result. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested. On Linux the trace is printed only on `SIGABRT`, which covers guest and host `abort()`; Linux has no crash report, so a fatal signal such as `SIGSEGV` ends the process without it +- [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) exists on Windows and Linux, not macOS, and records the entry of a call, not its arguments or its result. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested. On Linux the trace is printed only on `SIGABRT`, which covers guest and host `abort()`; Linux has no crash report, so a fatal signal such as `SIGSEGV` ends the process without it. The Linux report is written from the signal handler with `write` only; callers are named from a list of loaded modules taken when imports are traced, so an address in a module loaded later is printed without a module name - [_Dtest](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows Dinkumware double classification matching _FDtest: finite values return -1, subnormals -2, zeros 0, infinities 1, and NaNs 2. - [app0.zar](../../../core/libs/prx/libc/src/AppArchive.cpp) (libc) - a [ZArchive](https://github.com/Exzap/ZArchive) beside `app0/` is a read-only layer under it: a path missing from `app0/` is looked up in the archive, case-insensitively. It is not a console format; titles ship their files in a PFS image instead. Resolving an archive-only path through any call other than the archive-aware libkernel ones throws, so `fopen`, `stat`, `access`, `opendir` and the rest do not read the archive yet, and `opendir` of a directory that exists in both places, `app0/` itself included, and `fdopen` of an archived descriptor throw instead of missing its entries or reading the null device. A damaged compressed block throws instead of returning `EIO` - [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified From 0bad015e21be5f4d48bc17d8e57d42c7255adcc2 Mon Sep 17 00:00:00 2001 From: Ishmam Ahmed <89687266+IshmamDC217@users.noreply.github.com> Date: Mon, 19 Oct 2026 08:47:25 +0100 Subject: [PATCH 078/219] fix(libc): keep the Linux call trace report when two threads abort A thread that lost the race to write the report passed SIGABRT on straight away, and the default action ended the process while the first thread was still writing. It now waits for the report to finish, polling with nanosleep for at most 2 seconds, before it chains. guest_call_trace_abort_race aborts from two threads at once and checks the whole report; it failed 35 times in 50 without the wait. Reported by jotitadev. --- core/libs/prx/libc/CMakeLists.txt | 5 +++++ .../libs/prx/libc/src/specifics/x86_64/CallTrace.cpp | 6 ++++++ core/libs/tests/GuestCallTrace.cpp | 12 +++++++++++- docs/dev/debt/libc.md | 2 +- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index b8674dd62c..a1f1950d50 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -585,6 +585,11 @@ if(BUILD_TESTING) endif() set_tests_properties(guest_call_trace_abort PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION "last traced system library calls[^~]*#1 thread [0-9]+ libTraced.prx Integers from [^~]*guest_call_trace_tests${CMAKE_EXECUTABLE_SUFFIX}[^~]*#2 thread [0-9]+ libOther.prx Blocks from[^~]*most called traced imports \\(2 calls in total\\):[^~]* 1 libTraced.prx Integers[^~]* 1 libOther.prx Blocks") + if(NOT WIN32) + add_test(NAME guest_call_trace_abort_race COMMAND guest_call_trace_tests abort-race) + set_tests_properties(guest_call_trace_abort_race PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION + "last traced system library calls[^~]*#102 thread [0-9]+ libTraced.prx Integers from[^~]*most called traced imports \\(102 calls in total\\):[^~]* 101 libTraced.prx Integers[^~]* 1 libOther.prx Blocks") + endif() endif() add_test_executable(guest_syslog_tests ${LIBS_INCLUDE_DIR}/tests/GuestSyslog.cpp) target_include_directories(guest_syslog_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp index a1033c1099..f5e0920719 100644 --- a/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp +++ b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp @@ -4,6 +4,7 @@ #else #include #include +#include #include #include #include @@ -122,6 +123,7 @@ struct ModuleMap { struct sigaction g_previousAbort{}; std::atomic g_modules{nullptr}; std::atomic g_reporting{false}; +std::atomic g_reported{false}; unsigned long long g_moduleChanges = ~0ull; void SnapshotModules() { @@ -238,6 +240,10 @@ void ReportOnAbort(int number, siginfo_t* info, void* context) { ReportLine line; line.Text("[libc] SIGABRT").Write(); ReportCallTrace(); + g_reported.store(true, std::memory_order_release); + } else { + const timespec pause{0, 1000000}; + for (int waited = 0; waited < 2000 && !g_reported.load(std::memory_order_acquire); ++waited) nanosleep(&pause, nullptr); } errno = savedErrno; if ((g_previousAbort.sa_flags & SA_SIGINFO) != 0) { diff --git a/core/libs/tests/GuestCallTrace.cpp b/core/libs/tests/GuestCallTrace.cpp index ff67cf832d..11d3999982 100644 --- a/core/libs/tests/GuestCallTrace.cpp +++ b/core/libs/tests/GuestCallTrace.cpp @@ -87,7 +87,7 @@ int main(int argc, char** argv) { #else Require(setenv("APS5_TRACE_CALLS", "libTraced,libOther.prx", 1) == 0); #endif - if (argc > 1 && std::strcmp(argv[1], "abort") == 0) { + if (argc > 1 && (std::strcmp(argv[1], "abort") == 0 || std::strcmp(argv[1], "abort-race") == 0)) { #ifndef _WIN32 Require(std::signal(SIGABRT, [](int) { _exit(3); }) != SIG_ERR); #endif @@ -104,6 +104,16 @@ int main(int argc, char** argv) { for (;;) pause(); }).detach(); while (!locked.load()) std::this_thread::yield(); + if (std::strcmp(argv[1], "abort-race") == 0) { + for (int call = 0; call < 100; ++call) Require(first(1, 2, 3, 4, 5, 6, 7, 8) == 204); + std::atomic go{false}; + std::thread([&go] { + while (!go.load()) { + } + std::abort(); + }).detach(); + go.store(true); + } #endif std::abort(); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 1b39ef30d2..bcf57d7466 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -113,7 +113,7 @@ - [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The lock is a host [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) lock on one byte at offset 2^63 - 2, which no file reaches, so it excludes other `flock` calls and leaves reads, writes and mapped views alone, like FreeBSD's advisory locks. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, selected by the first matching errno entry in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects::_Generic_object`, and the new `generic_category()` export returns that object; a title's inline category object in a bundled libc can still have a different identity. Its `message` and direct message export construct the existing [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) model: a reserved eight-byte word, 16-byte inline buffer, length and capacity, with guest `operator new` storage above 15 bytes. This 40-byte string layout and virtual return ABI have not been verified against a console library. The deleting destructor still throws. `_Syserror_map` covers the 72 distinct Dinkumware `errc` values present in FreeBSD 11.4 [errno.h](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h), returning null for other values; category messages use `unknown error` for those. First-match aliases are `EAGAIN`/`EWOULDBLOCK` -> `operation would block` and `ENOTSUP`/`EOPNOTSUPP` -> `not supported`. STREAMS-only errno assignments absent from that header (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are not inferred -- [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) exists on Windows and Linux, not macOS, and records the entry of a call, not its arguments or its result. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested. On Linux the trace is printed only on `SIGABRT`, which covers guest and host `abort()`; Linux has no crash report, so a fatal signal such as `SIGSEGV` ends the process without it. The Linux report is written from the signal handler with `write` only; callers are named from a list of loaded modules taken when imports are traced, so an address in a module loaded later is printed without a module name +- [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) is missing on macOS and records only the entry of a call, not its arguments or result. One process-wide ring keeps the last 65536 calls, so a busy thread pushes the others out. Calls are counted per traced target; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; wider vector arguments survive only while the recording code avoids AVX, which is untested. Linux has no crash report, so it prints the trace only on `SIGABRT` (guest or host `abort()`), not on `SIGSEGV`; a caller in a module loaded after the last traced import prints without a module name, and a thread aborting while another writes the report waits for it at most 2 seconds - [_Dtest](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows Dinkumware double classification matching _FDtest: finite values return -1, subnormals -2, zeros 0, infinities 1, and NaNs 2. - [app0.zar](../../../core/libs/prx/libc/src/AppArchive.cpp) (libc) - a [ZArchive](https://github.com/Exzap/ZArchive) beside `app0/` is a read-only layer under it: a path missing from `app0/` is looked up in the archive, case-insensitively. It is not a console format; titles ship their files in a PFS image instead. Resolving an archive-only path through any call other than the archive-aware libkernel ones throws, so `fopen`, `stat`, `access`, `opendir` and the rest do not read the archive yet, and `opendir` of a directory that exists in both places, `app0/` itself included, and `fdopen` of an archived descriptor throw instead of missing its entries or reading the null device. A damaged compressed block throws instead of returning `EIO` - [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified From 78634fb0a9681b6d40330c09653ac24ee28228fa Mon Sep 17 00:00:00 2001 From: Ishmam Ahmed <89687266+IshmamDC217@users.noreply.github.com> Date: Sun, 18 Oct 2026 06:20:05 +0100 Subject: [PATCH 079/219] feat(relinker): support --trace-imports for Linux output The Linux entry stub now passes each traced import through libc.prx's TraceImport_nid_no_patch before it calls the guest entry, and stores the returned thunk in the import's slot. The same imports are traced as on Windows: functions bound through a PLT slot or a GOT slot, without an addend, and not weak. TraceImport_nid_no_patch is bound eagerly through an added GLOB_DAT relocation, so a libc.prx without it fails at load. Each slot's page is made writable with mprotect first, so slots inside a PT_GNU_RELRO range can be replaced. --trace-imports now requires libc.prx among the libraries on Linux too, and is refused with --macos. --- core/relinker/CMakeLists.txt | 2 + core/relinker/cli/src/CliArgs.cpp | 4 +- .../elfpatcher/linux/LinuxElfPatcher.hpp | 4 +- .../elfpatcher/src/linux/LinuxElfPatcher.cpp | 159 ++++++++++++++++-- .../relinker/src/pipeline/Conversion.cpp | 3 +- .../tests/test_linux_trace_imports.py | 89 ++++++++++ .../tests/test_windows_trace_imports.py | 3 - docs/dev/TRACING.md | 2 +- docs/dev/debt/relinker.md | 2 +- docs/user/USAGE.md | 4 +- 10 files changed, 251 insertions(+), 21 deletions(-) create mode 100644 core/relinker/relinker/tests/test_linux_trace_imports.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index d2a3387683..32dc870d3d 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -516,6 +516,8 @@ if(BUILD_TESTING) set_tests_properties(windows_runpath PROPERTIES TIMEOUT 30) add_test(NAME windows_trace_imports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_trace_imports.py $) + add_test(NAME linux_trace_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_trace_imports.py $) add_test(NAME windows_address_space COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_address_space.py $) add_test(NAME windows_tls_thread_pointer COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index 7cfe452ea7..3fa915c6d2 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -197,8 +197,8 @@ Args ParseArgs(int argc, char* argv[]) { if (args.windowsFixedBase && !args.toWindows) throw std::runtime_error("--windows-fixed-base requires --windows"); - if (args.traceImports && !args.toWindows) - throw std::runtime_error("--trace-imports requires --windows"); + if (args.traceImports && args.toMacos) + throw std::runtime_error("--trace-imports cannot be used with --macos"); if (args.traceImports && args.lazyBinding) throw std::runtime_error("--trace-imports conflicts with --lazy-binding"); if (args.linuxFixedBase && (args.toWindows || args.toMacos)) diff --git a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp index 17f12e8fc6..c24b3114f0 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp @@ -21,7 +21,8 @@ public: std::shared_ptr programHeaderLayoutBuilder, std::shared_ptr sectionHeaderTableBuilder, std::shared_ptr byteWriter, - std::optional fixedBase = std::nullopt + std::optional fixedBase = std::nullopt, + bool traceImports = false ); std::vector Patch( @@ -41,6 +42,7 @@ private: std::shared_ptr _sectionHeaderTableBuilder; std::shared_ptr _byteWriter; std::optional _fixedBase; + bool _traceImports; void _appendDynEntry(std::vector& buf, std::int64_t tag, std::uint64_t val) const; void _appendTrampoline(std::vector& buf, const Codegen::TrampolineSite& site, std::uint64_t extraBlockOffset, const std::function& vaddrOfExtraBlockOffset) const; diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 88e0279e25..df116a307c 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -7,9 +7,11 @@ #include #include #include +#include #include #include #include +#include #include namespace Elfpatcher::Linux { @@ -143,6 +145,75 @@ void SeparateSharedLoadPages(std::vector& buf, std::vector& strings, const std::uint32_t offset) { + if (offset >= strings.size()) + throw Domain::RelinkerException("Import symbol name lies outside the dynamic string table"); + std::string value; + for (std::size_t index = offset; index < strings.size() && strings[index] != 0; ++index) + value.push_back(static_cast(strings[index])); + return value; +} + +std::vector TracedImports(const Domain::SysVDynamicSection& dynSection) { + std::vector imports; + const auto collect = [&](const std::vector& table) { + RequireWholeEntries(table, kRelaEntrySize, "Relocation table"); + for (std::size_t entry = 0; entry < table.size(); entry += kRelaEntrySize) { + const auto info = Io::ReadU64(table, entry + 8); + const auto type = static_cast(info); + const auto symbol = info >> 32; + const auto target = Io::ReadU64(table, entry); + if ((type != R_X86_64_GLOB_DAT && type != R_X86_64_JUMP_SLOT) || symbol == 0 || Io::ReadU64(table, entry + 16) != 0) + continue; + if (type == R_X86_64_GLOB_DAT && !dynSection.FunctionImports.contains(target)) + continue; + if (symbol >= dynSection.DynSymData.size() / kSymEntrySize) + throw Domain::RelinkerException("Invalid import symbol index", target); + const auto symbolOffset = static_cast(symbol * kSymEntrySize); + if (Io::ReadU16(dynSection.DynSymData, symbolOffset + 6) != SHN_UNDEF || dynSection.DynSymData[symbolOffset + 4] >> 4 == STB_WEAK) + continue; + auto name = ReadDynamicString(dynSection.DynStrData, Io::ReadU32(dynSection.DynSymData, symbolOffset)); + if (name.empty()) + continue; + const auto library = dynSection.ImportModules.find(target); + imports.push_back({target, std::move(name), library == dynSection.ImportModules.end() ? std::string{} : library->second}); + } + }; + collect(dynSection.RelaData); + collect(dynSection.RelaPltData); + return imports; +} + +std::vector TracePrologue(const std::uint64_t vaddr, const std::uint64_t table, const std::uint64_t tableEnd, const std::uint64_t function) { + std::vector code = {0x52, 0x53, 0x41, 0x54, 0x48, 0x83, 0xec, 0x08}; + const auto rip = [&](std::initializer_list opcode, const std::uint64_t target) { + code.insert(code.end(), opcode); + const auto displacement = static_cast(target) - static_cast(vaddr + code.size() + 4); + if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) + throw Domain::RelinkerException("Import trace table exceeds rel32 range"); + Io::AppendU32(code, static_cast(displacement)); + }; + rip({0x48, 0x8d, 0x1d}, table); + rip({0x4c, 0x8d, 0x25}, tableEnd); + const auto loop = code.size(); + code.insert(code.end(), {0x4c, 0x39, 0xe3, 0x73, 0x00}); + const auto exit = code.size(); + code.insert(code.end(), {0x48, 0x8b, 0x03, 0x48, 0x8b, 0x3c, 0x03, 0x48, 0x8b, 0x73, 0x08, 0x48, 0x01, 0xde, 0x48, 0x8b, 0x53, 0x10, 0x48, 0x01, 0xda}); + rip({0xff, 0x15}, function); + code.insert(code.end(), {0x48, 0x8b, 0x3b, 0x48, 0x01, 0xdf, 0x48, 0x81, 0xe7, 0x00, 0xf0, 0xff, 0xff, 0xbe, 0x00, 0x10, 0x00, 0x00, 0xba, 0x07, 0x00, 0x00, 0x00, 0x50, 0xb8, 0x0a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x58}); + code.insert(code.end(), {0x48, 0x8b, 0x0b, 0x48, 0x89, 0x04, 0x0b, 0x48, 0x83, 0xc3, 0x18, 0xeb}); + code.push_back(static_cast(static_cast(static_cast(loop) - static_cast(code.size() + 1)))); + code[exit - 1] = static_cast(code.size() - exit); + code.insert(code.end(), {0x48, 0x83, 0xc4, 0x08, 0x41, 0x5c, 0x5b, 0x5a}); + return code; +} + void RebaseProgramHeaders(std::vector& buf, const std::uint64_t tableOffset, const std::uint16_t entrySize, const std::uint16_t count, const std::uint64_t base) { for (std::uint16_t index = 0; index < count; ++index) { const auto header = static_cast(tableOffset + static_cast(index) * entrySize); @@ -170,13 +241,15 @@ LinuxElfPatcher::LinuxElfPatcher( std::shared_ptr programHeaderLayoutBuilder, std::shared_ptr sectionHeaderTableBuilder, std::shared_ptr byteWriter, - std::optional fixedBase + std::optional fixedBase, + bool traceImports ) : _entryStubBuilder(std::move(entryStubBuilder)) , _programHeaderLayoutBuilder(std::move(programHeaderLayoutBuilder)) , _sectionHeaderTableBuilder(std::move(sectionHeaderTableBuilder)) , _byteWriter(std::move(byteWriter)) , _fixedBase(fixedBase) + , _traceImports(traceImports) { } @@ -235,8 +308,13 @@ std::vector LinuxElfPatcher::Patch( throw Domain::RelinkerException("Linux target does not support --windows-diagnostics"); if (_fixedBase && lazyBinding) throw Domain::RelinkerException("A fixed Linux base requires eager binding; --lazy-binding is incompatible"); + if (_traceImports && lazyBinding) + throw Domain::RelinkerException("--trace-imports conflicts with --lazy-binding"); + if (_traceImports && std::find(dynSection.NeededLibraries.begin(), dynSection.NeededLibraries.end(), "libc.prx") == dynSection.NeededLibraries.end()) + throw Domain::RelinkerException("--trace-imports requires libc.prx among the libraries"); if (_fixedBase) RequireOnlyDependencies(dynSection.DynamicSegmentData); + const auto traced = _traceImports ? TracedImports(dynSection) : std::vector{}; const std::uint64_t base = _fixedBase.value_or(0); const auto address = [this, base](const std::uint64_t vaddr) { return _fixedBase ? Rebased(base, vaddr, "Address") : vaddr; @@ -273,17 +351,32 @@ std::vector LinuxElfPatcher::Patch( for (char c : runPath) buf.push_back(static_cast(c)); buf.push_back(0); + const std::uint64_t traceSymbolStrOff = static_cast(buf.size()) - dynStrOff; + if (_traceImports) { + for (char c : std::string_view("TraceImport_nid_no_patch")) + buf.push_back(static_cast(c)); + buf.push_back(0); + } const std::uint64_t dynStrSize = static_cast(buf.size()) - dynStrOff; alignBuf(buf, kDynStrAlignment); const auto dynSymOff = static_cast(buf.size()); - const auto symbols = table(dynSection.DynSymData, false); + auto symbols = table(dynSection.DynSymData, false); + const auto traceSymbol = symbols.size() / kSymEntrySize; + if (_traceImports) { + Io::AppendU32(symbols, static_cast(traceSymbolStrOff)); + symbols.push_back(static_cast((STB_GLOBAL << 4) | STT_FUNC)); + symbols.push_back(0); + Io::AppendU16(symbols, SHN_UNDEF); + Io::AppendU64(symbols, 0); + Io::AppendU64(symbols, 0); + } for (std::uint8_t b : symbols) buf.push_back(b); alignBuf(buf, kDynSymAlignment); const auto hashOff = static_cast(buf.size()); - const auto symbolCount = static_cast(dynSection.DynSymData.size() / kSymEntrySize); + const auto symbolCount = static_cast(symbols.size() / kSymEntrySize); Io::AppendU32(buf, 1); Io::AppendU32(buf, symbolCount); Io::AppendU32(buf, symbolCount > 1 ? 1 : 0); @@ -292,7 +385,13 @@ std::vector LinuxElfPatcher::Patch( alignBuf(buf, kDynSymAlignment); const auto relaOff = static_cast(buf.size()); - const auto relocations = table(dynSection.RelaData, true); + auto relocations = table(dynSection.RelaData, true); + const auto traceRelocation = relocations.size(); + if (_traceImports) { + Io::AppendU64(relocations, 0); + Io::AppendU64(relocations, (static_cast(traceSymbol) << 32) | R_X86_64_GLOB_DAT); + Io::AppendU64(relocations, 0); + } for (std::uint8_t b : relocations) buf.push_back(b); alignBuf(buf, kRelaAlignment); @@ -321,9 +420,9 @@ std::vector LinuxElfPatcher::Patch( _appendDynEntry(dynSegBuf, DT_SYMENT, kSymEntrySize); _appendDynEntry(dynSegBuf, DT_HASH, address(vaddrOfExtraBlockOffset(hashOff))); _appendDynEntry(dynSegBuf, DT_DEBUG, 0); - if (!dynSection.RelaData.empty()) { + if (!relocations.empty()) { _appendDynEntry(dynSegBuf, DT_RELA, address(vaddrOfExtraBlockOffset(relaOff))); - _appendDynEntry(dynSegBuf, DT_RELASZ, dynSection.RelaData.size()); + _appendDynEntry(dynSegBuf, DT_RELASZ, relocations.size()); _appendDynEntry(dynSegBuf, DT_RELAENT, kRelaEntrySize); } if (!dynSection.RelaPltData.empty()) { @@ -341,12 +440,52 @@ std::vector LinuxElfPatcher::Patch( for (std::uint8_t b : dynSegBuf) buf.push_back(b); + std::uint64_t traceFunctionVaddr = 0; + std::uint64_t traceTableVaddr = 0; + std::uint64_t traceTableEndVaddr = 0; + std::vector traceEntries; + if (_traceImports) { + alignBuf(buf, 8); + const auto functionOff = static_cast(buf.size()); + traceFunctionVaddr = vaddrOfExtraBlockOffset(functionOff); + Io::AppendU64(buf, 0); + Io::WriteU64(buf, static_cast(relaOff + traceRelocation), address(traceFunctionVaddr)); + traceTableVaddr = vaddrOfExtraBlockOffset(buf.size()); + for (std::size_t index = 0; index < traced.size(); ++index) { + traceEntries.push_back(buf.size()); + buf.resize(buf.size() + 24); + } + traceTableEndVaddr = vaddrOfExtraBlockOffset(buf.size()); + std::map strings; + const auto addString = [&](const std::string& value) { + if (const auto existing = strings.find(value); existing != strings.end()) return existing->second; + const auto vaddr = vaddrOfExtraBlockOffset(buf.size()); + for (char c : value) + buf.push_back(static_cast(c)); + buf.push_back(0); + strings.emplace(value, vaddr); + return vaddr; + }; + for (std::size_t index = 0; index < traced.size(); ++index) { + const auto entryVaddr = vaddrOfExtraBlockOffset(traceEntries[index]); + const auto relative = [entryVaddr](const std::uint64_t vaddr) { return vaddr - entryVaddr; }; + Io::WriteU64(buf, traceEntries[index], relative(traced[index].Slot)); + Io::WriteU64(buf, traceEntries[index] + 8, relative(addString(traced[index].Name))); + Io::WriteU64(buf, traceEntries[index] + 16, relative(addString(traced[index].Library))); + } + } + const std::uint64_t realEntryVaddr = *reinterpret_cast(buf.data() + kEhdrEntryOffset); const auto stubOff = static_cast(buf.size()); const auto stubVaddr = vaddrOfExtraBlockOffset(stubOff); - const auto stubBytes = _entryStubBuilder->BuildEntryStub(address(stubVaddr), address(realEntryVaddr)); - for (std::uint8_t b : stubBytes) + if (_traceImports) { + for (std::uint8_t b : TracePrologue(stubVaddr, traceTableVaddr, traceTableEndVaddr, traceFunctionVaddr)) + buf.push_back(b); + } + const auto entryStubVaddr = vaddrOfExtraBlockOffset(buf.size()); + for (std::uint8_t b : _entryStubBuilder->BuildEntryStub(address(entryStubVaddr), address(realEntryVaddr))) buf.push_back(b); + const auto stubSize = static_cast(buf.size()) - stubOff; _byteWriter->WriteU64(buf, kEhdrEntryOffset, address(stubVaddr)); for (const auto& site : trampolines) @@ -395,11 +534,11 @@ std::vector LinuxElfPatcher::Patch( sectionRequest.DynStrOffset = dynStrOff; sectionRequest.DynStrSize = dynStrSize; sectionRequest.DynSymOffset = dynSymOff; - sectionRequest.DynSymSize = dynSection.DynSymData.size(); + sectionRequest.DynSymSize = symbols.size(); sectionRequest.DynamicSegmentOffset = dynSegOff; sectionRequest.DynamicSegmentSize = dynSegBuf.size(); sectionRequest.StubOffset = stubOff; - sectionRequest.StubSize = stubBytes.size(); + sectionRequest.StubSize = stubSize; _sectionHeaderTableBuilder->WriteTable(buf, sectionRequest); diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index 9b283de919..6a8cf4bf70 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -139,7 +139,8 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti ), std::make_shared(byteWriter), byteWriter, - options.LinuxFixedBase + options.LinuxFixedBase, + options.TraceImports ); } diff --git a/core/relinker/relinker/tests/test_linux_trace_imports.py b/core/relinker/relinker/tests/test_linux_trace_imports.py new file mode 100644 index 0000000000..f8febde7c2 --- /dev/null +++ b/core/relinker/relinker/tests/test_linux_trace_imports.py @@ -0,0 +1,89 @@ +"""--trace-imports passes each Linux function import through libc.prx's TraceImport_nid_no_patch before the guest entry.""" + +import os +from pathlib import Path +import platform +import subprocess +import sys +import tempfile + +from guest_import_fixtures import executable + + +PROVIDER = "#include \nint shared(void) { _exit(11); }\n" +HOOK = ("#include \n" + "#include \n" + "static int replacement(void) { _exit(77); }\n" + "void* TraceImport_nid_no_patch(void* target, const char* name, const char* library) {\n" + " return strcmp(name, \"shared\") == 0 && strcmp(library, \"a.prx\") == 0 ? (void*)replacement : target;\n" + "}\n") +PLAIN = "int Unrelated(void) { return 0; }\n" + + +def relink(relinker, source, output, *options): + return subprocess.run([str(relinker), "--skip-sce-module", *options, str(source), str(output)], + capture_output=True, text=True, timeout=30) + + +def library(work, name, code): + source = work / (name + ".c") + source.write_text(code) + output = work / (name + ".so") + subprocess.run([os.environ.get("CC", "cc"), "-shared", "-fPIC", str(source), "-o", str(output)], check=True) + return output.read_bytes() + + +def main(): + relinker = Path(sys.argv[1]).resolve() + runnable = sys.platform.startswith("linux") and platform.machine() in ("x86_64", "AMD64") + with tempfile.TemporaryDirectory(prefix="anyps5-linux-trace-imports-") as directory: + work = Path(directory) + libraries = {} + if runnable: + libraries = {name: library(work, name, code) for name, code in (("a", PROVIDER), ("hook", HOOK), ("plain", PLAIN))} + + for name, options, libc, symbol_info, expected in ( + ("untraced", (), "hook", 0x12, 11), + ("traced", ("--trace-imports",), "hook", 0x12, 77), + ("fixed-base", ("--trace-imports", "--linux-fixed-base", "0x400000"), "hook", 0x12, 77), + ("data", ("--trace-imports",), "hook", 0x11, 11), + ("weak", ("--trace-imports",), "hook", 0x22, 11), + ("unsupported", ("--trace-imports",), "plain", 0x12, None)): + case = work / name + case.mkdir() + image = executable("a.prx", extra_dependencies=("libc.prx",)) + image[0x4898 + 4] = symbol_info + source = case / "input.elf" + source.write_bytes(image) + output = case / "output.elf" + result = relink(relinker, source, output, *options) + assert result.returncode == 0, (name, result.stdout, result.stderr) + if not runnable: + continue + (case / "libs").mkdir() + (case / "libs" / "a.prx").write_bytes(libraries["a"]) + (case / "libs" / "libc.prx").write_bytes(libraries[libc]) + output.chmod(0o755) + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + if expected is None: + assert run.returncode != 0 and "TraceImport_nid_no_patch" in run.stderr, (name, run.returncode, run.stdout, run.stderr) + else: + assert run.returncode == expected, (name, run.returncode, run.stdout, run.stderr) + + case = work / "without-libc" + case.mkdir() + source = case / "input.elf" + source.write_bytes(executable("a.prx")) + result = relink(relinker, source, case / "output.elf", "--trace-imports") + assert result.returncode == 2 and "--trace-imports requires libc.prx" in result.stderr, (result.returncode, result.stderr) + assert not (case / "output.elf").exists() + result = relink(relinker, source, case / "output.elf", "--trace-imports", "--lazy-binding") + assert result.returncode != 0 and "--trace-imports conflicts with --lazy-binding" in result.stderr, (result.returncode, result.stderr) + result = relink(relinker, source, case / "output", "--trace-imports", "--macos") + assert result.returncode != 0 and "--trace-imports cannot be used with --macos" in result.stderr, (result.returncode, result.stderr) + + print("Linux import tracing tests passed") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_windows_trace_imports.py b/core/relinker/relinker/tests/test_windows_trace_imports.py index 41a3e42c62..697b50afa7 100644 --- a/core/relinker/relinker/tests/test_windows_trace_imports.py +++ b/core/relinker/relinker/tests/test_windows_trace_imports.py @@ -80,9 +80,6 @@ def main(): assert result.returncode == 2 and '--trace-imports requires libc.prx' in result.stderr, (result.returncode, result.stderr) assert not (case / 'output.exe').exists() - result = subprocess.run([str(relinker), '--trace-imports', str(source), str(case / 'output.elf')], - capture_output=True, text=True, timeout=30) - assert result.returncode != 0 and '--trace-imports requires --windows' in result.stderr, (result.returncode, result.stderr) result = relink(relinker, source, case / 'output.exe', '--trace-imports', '--lazy-binding') assert result.returncode != 0 and '--trace-imports conflicts with --lazy-binding' in result.stderr, (result.returncode, result.stderr) diff --git a/docs/dev/TRACING.md b/docs/dev/TRACING.md index a4c7f10a6a..8cb84bb4a4 100644 --- a/docs/dev/TRACING.md +++ b/docs/dev/TRACING.md @@ -11,7 +11,7 @@ Set a switch before starting the emulator. Unless noted, any value enables it. M | `APS5_TRACE_APR` | APR file reads, including path, offset, size and destination. | | `APS5_TRACE_AUDIOOUT` | Audio output port parameters when a port is opened. | | `APS5_TRACE_AUDIOOUT2` | AudioOut2 context and port parameters, attributes, pushes, queue polls and periodic queue summaries. | -| `APS5_TRACE_CALLS` | On Windows, records calls to the imports selected by this comma-separated library list (or `all`); crash and abort reports include the last 64 calls and most-called imports. The executable must be converted with `--trace-imports`; see [Usage](../user/USAGE.md#tracing-system-library-calls). | +| `APS5_TRACE_CALLS` | On Windows and Linux, records calls to the imports selected by this comma-separated library list (or `all`); crash and abort reports (abort only on Linux) include the last 64 calls and most-called imports. The executable must be converted with `--trace-imports`; see [Usage](../user/USAGE.md#tracing-system-library-calls). | | `APS5_TRACE_EXIT` | Guest `exit` and `_Exit` calls and process-exit backtraces. | | `APS5_TRACE_FIBER` | Fiber initialization, switches and returns. | | `APS5_TRACE_MEMORY` | Direct-memory allocation, mapping and protection operations. | diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index d49cedbfcb..693654d350 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -24,6 +24,6 @@ - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. - `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1 and execute on an M1 running macOS 26.6.2, so on the M1 the difference follows the macOS version; on that host BEXTR, BLSI, BLSMSK, BLSR, TZCNT, LZCNT, POPCNT, BZHI, MULX, PDEP, PEXT, RORX, SARX, SHLX, SHRX, MOVBE, FMA, AVX2 and F16C execute as well, its CPUID leaf 1 reports none of MOVBE, FMA, AVX or F16C, and RDSEED, RDPID, CLWB, ADCX and ADOX raise SIGILL. They also execute on an M5 running macOS 26.4, where the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE execute as well although CPUID leaf 7 reads zero and leaves 1 and 0x80000001 report none of them, and where RDSEED, CLWB, RDPID, ADCX and ADOX raise an illegal instruction. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, and RDTSCP runs unchanged with ECX set to 0 after it, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB (also measured on an M5, macOS 27.0.1). An RDPID or RDTSCP site with no movable instruction after it for the jump is kept; a kept RDTSCP reports 0xAAAAAAAB. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. - `--to-intel` replaces the approximate legacy SSE and VEX.128 register forms of RSQRTPS/RCPPS and VRSQRTPS/VRCPPS with [correctly rounded](../../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). Legacy SSE forms preserve the destination's upper YMM bits; VEX forms clear them. The 256-bit, scalar and memory forms keep the native instruction, as does a site shorter than 5 bytes with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted). -- `--trace-imports` ([entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp)) exists for Windows output only. Only imports bound through a PLT slot, or through a GOT slot whose symbol is a function, are traced; a function reached through any other relocation, or looked up at run time with `sceKernelDlsym`, is not, and neither are weak imports or the imports of modules loaded at run time +- `--trace-imports` ([Windows entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp), [Linux entry stub](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp)) exists for Windows and Linux output, not macOS. Only imports bound through a PLT slot, or through a GOT slot whose symbol is a function, are traced; a function reached through any other relocation, or looked up at run time with `sceKernelDlsym`, is not, and neither are weak imports or the imports of modules loaded at run time. On Linux the entry stub makes the page of every traced slot readable, writable and executable with `mprotect` before it stores the thunk, so a slot inside `PT_GNU_RELRO` can be replaced; those pages stay writable, and data pages become executable, while tracing is on - [ANDN lowering](../../../core/relinker/codegen/src/x86/AndnLowering.cpp) uses scalar NOT and AND for 32- and 64-bit register and memory operands under `--to-rosetta`, following the ANDN entry in [Intel SDM volume 2A](https://cdrdv2-public.intel.com/812383/253666-sdm-vol-2a.pdf). SF and ZF follow the result, CF and OF clear, and AF and PF are undefined. The scratch register is saved below the 128-byte red zone. A 64-bit stack-relative displacement that exceeds signed disp32 after reserving the spill frame throws; other BMI instructions are not lowered. The M1 test covers register overlap, RSP operands, memory addressing, RIP relocation, flags and red-zone preservation; precise fault delivery inside a spill sequence has not been verified. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 8cfd7a1f84..78a89922a8 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -63,7 +63,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--windows-diagnostics` | Include startup dependency diagnostics. Requires `--windows`. | | `--windows-gui` | Select the Windows GUI subsystem instead of the console subsystem. Requires `--windows`. | | `--windows-fixed-base
` | Map the lowest ELF load page at this fixed Windows address, given in decimal or hexadecimal (`0x`). Requires `--windows`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and a complete image fitting in the user address space. The main executable has no ASLR or base relocations; an occupied address fails to load. Bundled modules keep their normal placement. | -| `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Requires `--windows`; conflicts with `--lazy-binding`. | +| `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Windows and Linux output only; conflicts with `--lazy-binding`. | | `--linux-fixed-base
` | Load the Linux executable at this fixed address, given in decimal or hexadecimal (`0x`): every virtual address of the image is offset by it, so virtual address 0 is placed at the address, as the console places an executable. Requires a 64 KiB-aligned address from `0x10000` through `0x7fffffff0000` that is a multiple of every `PT_LOAD` alignment, and an image that ends in the user address space. The executable is written as `ET_EXEC`, without ASLR. Bundled modules keep their normal placement. Conflicts with `--windows`, `--macos` and `--lazy-binding`. | | `--macos-fixed-base
` | Map the lowest ELF load page of the macOS executable at this fixed address, given in decimal or hexadecimal (`0x`). Requires `--macos`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and an image that ends in the macOS user address space. `__PAGEZERO` ends 64 KiB below the address and the Mach-O header occupies those 64 KiB. The executable has no ASLR; an address the process cannot map fails to load. Bundled modules keep their normal placement. | | `--to-intel` | Convert supported AMD-only instructions in the executable and bundled modules. Unsupported instructions or unreachable conversion stubs cause an error. | @@ -298,7 +298,7 @@ A title's own CPU reads of that memory are not seen, so a title that waits for a ### Tracing system library calls -Windows only. An executable converted with `--trace-imports` records the calls it makes to the libraries named in `APS5_TRACE_CALLS`, a comma-separated list such as `libkernel,libSceSaveData`, or `all`. Without the variable nothing is recorded. The crash and abort reports then end with the last 64 calls: sequence number, thread, library, import name as the executable imports it (a NID) and the caller's address. They are followed by the 32 most called imports since start-up, with their number of calls. +Windows and Linux. An executable converted with `--trace-imports` records the calls it makes to the libraries named in `APS5_TRACE_CALLS`, a comma-separated list such as `libkernel,libSceSaveData`, or `all`. Without the variable nothing is recorded. The crash and abort reports then end with the last 64 calls (on Linux, which has no crash report, only an abort prints them): sequence number, thread, library, import name as the executable imports it (a NID) and the caller's address. They are followed by the 32 most called imports since start-up, with their number of calls. ``` last traced system library calls (APS5_TRACE_CALLS): From b4cd843324bc82f7d9806848da66015dab45ce7f Mon Sep 17 00:00:00 2001 From: Joey Fisher Date: Mon, 19 Oct 2026 12:06:03 -0600 Subject: [PATCH 080/219] fix(agc): fail a template's proof on a released texture and audit it with the full walk The fast proof of a cached ShaderResources proves each sampled texture from write stamps, keys and the pending registry. It never asks whether the sampled lookup would still return the bound Texture, so a template whose surface memory is unchanged keeps binding an old object: - After the sampled cache drops or replaces the entry (#6492 marks the texture released), Find keeps serving the template until the next Insert, and its fast proof passes. - When the lookup's answer changes without any lookup running (a snapshot whose surface became host-imported is viewed through its storage image from then on), nothing replaces the entry and the template keeps the snapshot until it leaves the resource cache. The fast proof now fails with 'replaced texture' when a bound texture is released. A template whose last full walk is older than APS5_TEMPLATE_AUDIT_MS (default 1000, 0 turns it off) takes the full walk instead of the fast proof ('audit'), so a different lookup answer fails the proof and the template is rebuilt. --- .../Graphics/include/ShaderResources.hpp | 3 ++- .../Graphics/src/ShaderResources.cpp | 25 +++++++++++++++++-- docs/dev/ENVIRONMENT.md | 1 + 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 7b4aaa4858..677ec98419 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -305,7 +305,7 @@ public: bool DataWordsDiffer(const CompiledShader& shader) const; // Why the fast proof of a Revalidate left the object to the full walk (the [rescache] // revalidate line's reasons); Count: it did not. - enum class FastFail : std::size_t { NoRecord, Collect, Pending, Evicted, Changed, Keys, ClearedView, StorageKeys, Depth, Count }; + enum class FastFail : std::size_t { NoRecord, Collect, Pending, Evicted, Changed, Keys, ClearedView, StorageKeys, Depth, Replaced, Audit, Count }; // Why a Pending failure was left to the full walk instead of the own-object refresh (T1, see // refreshOwnObjects); Count: it was not. enum class OwnRefreshFallback : std::size_t { Disabled, Snapshot, Keys, ForeignView, SurfaceKey, NotImported, Uncached, Rerun, Count }; @@ -588,6 +588,7 @@ private: // Guest buffer elements bound read-only: each use of this object skips that many pending-write // notes (counted in MarkGpuWrites for the [buffers] line). std::size_t readOnlyBuffers = 0; + std::chrono::steady_clock::time_point lastFullWalk = std::chrono::steady_clock::now(); bool drawBuild = false; std::uint64_t dispatchThreads = 0; std::vector> textures; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index ad59adeada..897b02ce5f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2697,7 +2697,7 @@ namespace { // of the reused ones, fast = every image proved current from stamps, full = the lookups were repeated. // Why the fast path left an object to the full walk (the "fast-fail by reason" counts). using FastFail = ShaderResources::FastFail; -constexpr std::array(FastFail::Count)> FastFailNames{"no record", "collect", "pending image", "evicted image", "memory changed", "keys", "cleared view", "storage keys", "depth surface"}; +constexpr std::array(FastFail::Count)> FastFailNames{"no record", "collect", "pending image", "evicted image", "memory changed", "keys", "cleared view", "storage keys", "depth surface", "replaced texture", "audit"}; using OwnRefreshFallback = ShaderResources::OwnRefreshFallback; constexpr std::array(OwnRefreshFallback::Count)> OwnRefreshFallbackNames{"disabled", "snapshot texture", "cleared view", "foreign view", "surface key", "not imported", "uncached", "re-run failed"}; @@ -2835,6 +2835,19 @@ bool EpochRevalidate() { return enabled; } +std::chrono::steady_clock::duration TemplateAuditInterval() { + static const auto interval = [] { + const char* text = std::getenv("APS5_TEMPLATE_AUDIT_MS"); + return std::chrono::milliseconds(text != nullptr ? std::strtoull(text, nullptr, 10) : 1000ull); + }(); + return interval; +} + +bool TemplateAuditDue(std::chrono::steady_clock::time_point last) { + const auto interval = TemplateAuditInterval(); + return interval.count() != 0 && std::chrono::steady_clock::now() - last >= interval; +} + } // Turns the lookups' records into this object's per-texture validation records (see @@ -2913,6 +2926,7 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::spanReleased()) return fail(FastFail::Replaced); const auto address = surface.resource.baseAddress; const auto bytes = static_cast(surface.bytes); surface.collected = GuestMemory::CollectWrites(address, bytes); @@ -3039,6 +3053,7 @@ bool ShaderResources::fastRevalidateEach() { if (!DepthSurfaceHolds(context, surface.resource, textures[i].get())) return false; continue; } + if (textures[i]->Released()) return false; const auto address = surface.resource.baseAddress; const auto bytes = static_cast(surface.bytes); surface.collected = GuestMemory::CollectWrites(address, bytes); @@ -3238,6 +3253,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR // they must hand back the very objects the set's views belong to. The build appended them stage // by stage, binding by binding, so the walk repeats that order. const auto fullWalk = [&] { + lastFullWalk = std::chrono::steady_clock::now(); lookupLog().clear(); BuildLookups lookups; std::size_t textureIndex = 0; @@ -3343,6 +3359,11 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR } if (fallback != OwnRefreshFallback::Count) countOwnRefreshFallback(fallback); } + if (fast && TemplateAuditDue(lastFullWalk)) { + fast = false; + ownRefreshed = false; + reason = FastFail::Audit; + } if (report != nullptr) report->path = !fast ? ProofPath::Full : ownRefreshed ? ProofPath::OwnRefreshed : ProofPath::Fast; if (!fast) { countFullWalk(reason); @@ -3350,7 +3371,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR const auto failure = [&] { switch (reason) { case FastFail::Pending: return ProofFailure::Pending; - case FastFail::Evicted: return ProofFailure::Evicted; + case FastFail::Evicted: case FastFail::Replaced: return ProofFailure::Evicted; case FastFail::Changed: return ProofFailure::Changed; case FastFail::Keys: case FastFail::ClearedView: case FastFail::StorageKeys: return ProofFailure::Keys; default: return ProofFailure::Other; diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index a309403888..5930d06e82 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -373,6 +373,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SYNC_SAMPLE_DUMPS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | takes the CPU path for occlusion-counter sample dumps, as `APS5_NO_GPU_SAMPLE_DUMPS` does | | `APS5_SYNC_SHADER_PREPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | prepares registered shaders on the registering thread instead of in the background | | `APS5_SYNC_TEXTURE_UPLOAD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | restores the waiting batch for every texture upload | +| `APS5_TEMPLATE_AUDIT_MS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 1000 | milliseconds after which a cached shader-resource object takes the full walk instead of the fast proof; `0` turns the audit off | | `APS5_TEXTURE_CACHE_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 0 | forced byte budget in MiB for the storage and sampled texture caches; `0` selects the budget automatically | | `APS5_THREAD_NICE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | -10 | nice value the raised queue workers and presenter ask rtkit for (clamped to -20..19) | | `APS5_THREAD_PRIORITY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | off | raises the queue workers and presenter above the title's threads: `off`, `high` or `rt` | From 3c43ce33a074c1c6df0579e458a777f6c0224426 Mon Sep 17 00:00:00 2001 From: Joey Fisher Date: Mon, 19 Oct 2026 12:06:03 -0600 Subject: [PATCH 081/219] test(agc): templates over a released or changed sampled lookup must rebuild agc_driver_resource_cache_replaced_texture clears the sampled cache under a template that takes the fast proof and requires the next proof to fail. agc_driver_resource_cache_lookup_changed checks that a proof past the audit interval takes the full walk and keeps an unchanged template, then host-imports the surface (no lookup runs) and requires the template to stop proving current. Both run over a write-watched surface so the template takes the fast proof. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +- .../tests/execution/SampledCacheBudget.cpp | 162 ++++++++++++++++++ 2 files changed, 166 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index d4b9a576e1..b4a5a13931 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2316,9 +2316,11 @@ if(BUILD_TESTING) add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission) add_test(NAME agc_driver_resource_cache_oversized_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-oversized) add_test(NAME agc_driver_resource_cache_storage_clear COMMAND agc_driver_sampled_cache_budget_tests resource-clear) - set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_driver_resource_cache_replaced_texture COMMAND agc_driver_sampled_cache_budget_tests resource-replaced) + add_test(NAME agc_driver_resource_cache_lookup_changed COMMAND agc_driver_sampled_cache_budget_tests resource-lookup-changed) + set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear agc_driver_resource_cache_replaced_texture agc_driver_resource_cache_lookup_changed PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_sampled_cache_budget_without_storage_image_multisample agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_sampled_cache_budget_without_storage_image_multisample agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear agc_driver_resource_cache_replaced_texture agc_driver_resource_cache_lookup_changed PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() # The driver draw test again with the draw key off: the draw cache then keys on the decoded vertex diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 881867d747..c6623ffe3e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -9,6 +9,8 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include "VulkanTestDevice.hpp" #include #ifdef _WIN32 @@ -16,6 +18,8 @@ #define NOMINMAX #endif #include +#else +#include #endif #include #include @@ -194,6 +198,53 @@ private: void* memory = nullptr; }; +class WatchedSurface { +public: + explicit WatchedSurface(const Context& context) : context(context) { +#ifdef _WIN32 + memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, 65536)); + if (memory != nullptr) GuestArena::GuestArenaCommit_nid_postfix(memory, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, SurfaceBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + SurfaceBytes != begin + SurfaceBytes + alignment) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + alignment - aligned); + memory = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory, SurfaceBytes); + } +#endif + Require(memory != nullptr, "cannot allocate the watched surface"); + Require(AgcDriver::GuestMemory::Watched(Address(), SurfaceBytes), "the watched surface is not write-watched"); + std::memset(memory, 0x5a, SurfaceBytes); + GuestAllocations::Mutation().Add(memory, SurfaceBytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(Address(), SurfaceBytes); + } + + ~WatchedSurface() { + ClearCachedTextures(context.device); + GuestAllocations::Mutation().Remove(memory); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, SurfaceBytes); +#else + munmap(memory, SurfaceBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, SurfaceBytes); +#endif + } + + WatchedSurface(const WatchedSurface&) = delete; + WatchedSurface& operator=(const WatchedSurface&) = delete; + + std::uint64_t Address() const { return reinterpret_cast(memory); } + +private: + const Context& context; + std::uint8_t* memory = nullptr; +}; + std::array Descriptor(std::uint64_t address, std::uint32_t side = Side) { return { static_cast(address >> 8u), @@ -399,6 +450,103 @@ void viewTests(const Context& context) { Require(heldView.expired() && heldStorage.expired(), "the evicted image outlived its last caller without a sampled lookup"); } +ShaderRecompiler::RecompileResult SampledProgram(std::uint64_t address) { + ShaderRecompiler::DescriptorBinding binding{}; + binding.kind = ShaderRecompiler::DescriptorKind::SampledImage; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.descriptorSet = 0; + binding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding + PlaceholderDimensionsPerClass + 2u; + binding.count = 1; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + binding.imageWritten.assign(1, false); + binding.imageSamplers.assign(1, 0u); + const auto words = Descriptor(address); + binding.guestDescriptor.assign(words.begin(), words.end()); + ShaderRecompiler::RecompileResult program; + program.bindings.push_back(std::move(binding)); + return program; +} + +void replacedEntryTests(Context context) { + PlaceholderImages placeholders(context); + context.placeholderImages = &placeholders; + TextureCache textures(context); + context.textureCache = &textures; + WatchedSurface surface(context); + ClearCachedTextures(context.device); + const auto program = SampledProgram(surface.Address()); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + auto first = std::make_shared(context, shader); + Require(first->Reusable(), "the sampled snapshot's descriptor set is not eligible for resource caching"); + Require(first->SampledTextures().size() == 1, "the sampled descriptor set binds no texture"); + const auto bound = first->SampledTextures().front(); + Require(!bound->ViewsStorageImage(), "the replaced-entry fixture is not a snapshot"); + Require(Sampled(context, surface.Address()) == bound, "the descriptor set does not bind the cache's texture"); + ShaderResources::ProofReport report{}; + Require(first->Revalidate(shader), "a template over an unchanged, still cached texture did not prove current"); + Require(first->Revalidate(shader, &report), "a template over an unchanged, still cached texture did not prove current twice"); + Require(report.path == ShaderResources::ProofPath::Fast, "the fixture's template did not take the fast proof"); + Require(first->SampledTextures().front() == bound, "the proof changed the bound texture"); + ClearCachedTextures(context.device); + Require(TextureCacheUsage().sampledEntries == 0, "clearing the sampled cache left entries"); + Require(!first->Revalidate(shader, &report), "a template binding a texture whose cache entry left still proved current"); + const auto replacement = Sampled(context, surface.Address()); + Require(replacement != bound, "the lookup after the clear handed back the old texture"); + Require(!first->Revalidate(shader), "a template binding the replaced texture proved current against the new entry"); + auto rebuilt = std::make_shared(context, shader); + Require(rebuilt->SampledTextures().front() == replacement, "the rebuilt descriptor set does not bind the cache's new texture"); + Require(rebuilt->Revalidate(shader), "the rebuilt template did not prove current over its cached texture"); + first.reset(); + rebuilt.reset(); + ClearCachedTextures(context.device); + std::puts("template over a replaced sampled cache entry passed"); +} + +bool lookupChangedTests(Context context) { + PlaceholderImages placeholders(context); + context.placeholderImages = &placeholders; + TextureCache textures(context); + context.textureCache = &textures; + if (context.hostImportAlignment == 0) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "the lookup-changed test requires host imports"); + std::puts("skipped, the lookup-changed test requires host imports"); + return false; + } + WatchedSurface surface(context); + ClearCachedTextures(context.device); + const auto program = SampledProgram(surface.Address()); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + auto first = std::make_shared(context, shader); + Require(first->Reusable() && first->SampledTextures().size() == 1, "the lookup-changed template is not reusable"); + const auto bound = first->SampledTextures().front(); + Require(!bound->ViewsStorageImage(), "the lookup-changed fixture is not a snapshot before the import"); + ShaderResources::ProofReport report{}; + Require(first->Revalidate(shader), "the lookup-changed template did not prove current"); + Require(first->Revalidate(shader, &report) && report.path == ShaderResources::ProofPath::Fast, "the lookup-changed template did not take the fast proof"); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + Require(first->Revalidate(shader, &report), "the audit refused an unchanged template"); + Require(report.path == ShaderResources::ProofPath::Full, "a proof past the audit interval did not take the full walk"); + Require(first->SampledTextures().front() == bound, "the audit changed the bound texture"); + Require(first->Revalidate(shader, &report) && report.path == ShaderResources::ProofPath::Fast, "the proof right after an audit did not take the fast proof"); + if (HostImportFor(context, surface.Address(), SurfaceBytes) == nullptr) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "the host import of the watched surface was refused"); + std::puts("skipped, the host import of the watched surface was refused"); + return false; + } + Require(!bound->Released(), "the snapshot left the cache without a lookup"); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + Require(!first->Revalidate(shader, &report), "a template binding a snapshot of a surface its lookup now views through a storage image still proved current past the audit interval"); + const auto view = Sampled(context, surface.Address()); + Require(view != bound && view->ViewsStorageImage(), "the lookup of the imported surface does not view its storage image"); + auto rebuilt = std::make_shared(context, shader); + Require(rebuilt->SampledTextures().front() == view, "the rebuilt template does not bind the storage view"); + first.reset(); + rebuilt.reset(); + ClearCachedTextures(context.device); + std::puts("template over a changed sampled lookup passed"); + return true; +} + void reportedBudgetTests(const Context& context) { if (context.memoryProperties2 == nullptr) { std::puts("VK_EXT_memory_budget unavailable: the sampled texture cache keeps a quarter of the device-local heap"); @@ -722,8 +870,10 @@ int main(int argc, char** argv) { const bool disableStorageImageMultisample = mode == "without-storage-image-multisample"; #ifdef _WIN32 if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", "1"); + if (std::getenv("APS5_TEMPLATE_AUDIT_MS") == nullptr) _putenv_s("APS5_TEMPLATE_AUDIT_MS", "250"); #else if (!memory) setenv("APS5_TEXTURE_CACHE_MIB", "1", 1); + setenv("APS5_TEMPLATE_AUDIT_MS", "250", 0); #endif std::unique_ptr device; try { @@ -757,6 +907,18 @@ int main(int argc, char** argv) { std::puts("resource cache tests passed"); return 0; } + if (mode == "resource-replaced") { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + replacedEntryTests(context); + std::puts("resource cache replaced entry tests passed"); + return 0; + } + if (mode == "resource-lookup-changed") { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + if (!lookupChangedTests(context)) return VulkanTestSkipped; + std::puts("resource cache lookup-changed tests passed"); + return 0; + } if (memory) { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); memoryTests(context); From 1027bfbce16453574754e1d6aa2fbbe3cd7d36bd Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Tue, 20 Oct 2026 01:12:02 +0300 Subject: [PATCH 082/219] feat(libs): declare sceGameLiveStreamingEnableLiveStreaming and sceKernelPrintBacktraceWithModuleInfo PS4 titles import both and do not load without them; every call throws and the debt files list them. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceGameLiveStreaming/Export.cpp | 5 +++++ core/libs/prx/libkernel/Unimplemented.cpp | 6 ++++++ docs/dev/debt/libSceGameLiveStreaming.md | 1 + docs/dev/debt/libkernel.md | 1 + 4 files changed, 13 insertions(+) diff --git a/core/libs/prx/libSceGameLiveStreaming/Export.cpp b/core/libs/prx/libSceGameLiveStreaming/Export.cpp index b28b535c8e..aa280dcad4 100644 --- a/core/libs/prx/libSceGameLiveStreaming/Export.cpp +++ b/core/libs/prx/libSceGameLiveStreaming/Export.cpp @@ -40,4 +40,9 @@ int APS5_VABI sceGameLiveStreamingGetProgramInfo() { return 0; } +int APS5_VABI sceGameLiveStreamingEnableLiveStreaming(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index 090d314539..b0cef12689 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -121,4 +121,10 @@ int APS5_VABI sceKernelUnknown19(void) { NotImplemented_nid_no_patch("uDGXwUKqi8s"); return 0; } + +int APS5_VABI sceKernelPrintBacktraceWithModuleInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/docs/dev/debt/libSceGameLiveStreaming.md b/docs/dev/debt/libSceGameLiveStreaming.md index b30576336c..24dfaa238f 100644 --- a/docs/dev/debt/libSceGameLiveStreaming.md +++ b/docs/dev/debt/libSceGameLiveStreaming.md @@ -2,4 +2,5 @@ ### Unknown function info +- [sceGameLiveStreamingEnableLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws - [sceGameLiveStreamingGetCurrentStatus2](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - the status layout (user id, on-air flag, spectator, text and command message counts, broadcast resolution, 48 reserved bytes) is taken from shadPS4's `OrbisGameLiveStreamingStatus2`; the call reports that no stream is on air, with an invalid user id diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b59532d68f..31acb389aa 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -107,6 +107,7 @@ - [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) (libkernel, NID `LzoM-wVLJDE`) - no source gives the signature; `int (void)` is assumed from `sceKernelIsAddressSanitizerEnabled` and from PPSA23226's bundled `libSceJobManager_nosubmission.prx`, which passes no arguments and tests only the 32-bit result - [getdtablesize](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - returns the smaller of the `RLIMIT_NOFILE` soft limit of `getrlimit` and the host's `getdtablesize()`, capped at `INT_MAX`: FreeBSD returns the smaller of the `RLIMIT_NOFILE` soft limit and `maxfilesperproc` ([`sys_getdtablesize`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L338-L355)), and the host call supplies that bound on macOS, where the soft limit alone is not one (`setrlimit` accepts soft limits above `maxfilesperproc`, `RLIM_INFINITY` included); `maxfilesperproc` is not measured on the console. The guest limit is the host's soft limit, raised to the hard limit when libkernel loads only on Linux: `core/libs/prx/libkernel/File/src/OpenFileLimit.cpp` is `__linux__`-only, so on macOS the guest gets whatever soft limit the host process inherited. VLC (PPSA85300) imports it for a `closefrom` loop in the child of its spawn helper ([libc_stubs.c](https://github.com/ZiZc3/VLC/blob/d46be6a68f4244466b041c2dc1b640915ac1a983/ps5/compat/libc_stubs.c#L66-L72)), which never runs because its `fork` fails with `ENOSYS` - [close, sceKernelClose](../../../core/libs/prx/libkernel/File/src/HostStreams.cpp) (libkernel) - descriptors 0 to 2 that are open when libkernel loads belong to the host process: closing one fails with `EBADF` and leaves it open. The console's result is unknown. PPSA21703 calls `sceKernelClose(0)` repeatedly; closing the host stream let the host C runtime give descriptor 0 to the next file opened, which the next call then closed +- [sceKernelPrintBacktraceWithModuleInfo](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws ### Functional From 60d5149efa98c4b744f879ebf0aa80479a831ceb Mon Sep 17 00:00:00 2001 From: Nicolas Abosch <88494836+nicolasabosch@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:15:28 -0300 Subject: [PATCH 083/219] test(libkernel): run guest_sleep_wake_coarse on Windows only APS5_COARSE_TIMED_WAITS is read under _WIN32 only; on Linux the condition variable path always ends a sleep when a completion arrives. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 6 +++--- core/libs/tests/GuestSleepWake.cpp | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index cc845d9afa..25261562d5 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -727,10 +727,10 @@ if(BUILD_TESTING) target_link_libraries(guest_sleep_wake_tests PRIVATE libkernel libc) configure_windows_unwind(guest_sleep_wake_tests) add_test(NAME guest_sleep_wake COMMAND guest_sleep_wake_tests) - add_test(NAME guest_sleep_wake_coarse COMMAND guest_sleep_wake_tests) - set_tests_properties(guest_sleep_wake guest_sleep_wake_coarse PROPERTIES TIMEOUT 20) - set_tests_properties(guest_sleep_wake_coarse PROPERTIES ENVIRONMENT "APS5_COARSE_TIMED_WAITS=1") + set_tests_properties(guest_sleep_wake PROPERTIES TIMEOUT 20) if(WIN32) + add_test(NAME guest_sleep_wake_coarse COMMAND guest_sleep_wake_tests) + set_tests_properties(guest_sleep_wake_coarse PROPERTIES TIMEOUT 20 ENVIRONMENT "APS5_COARSE_TIMED_WAITS=1") set_tests_properties(guest_sleep_wake guest_sleep_wake_coarse PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/tests/GuestSleepWake.cpp b/core/libs/tests/GuestSleepWake.cpp index d58b3d9255..177ef4b990 100644 --- a/core/libs/tests/GuestSleepWake.cpp +++ b/core/libs/tests/GuestSleepWake.cpp @@ -48,6 +48,7 @@ private: std::thread thread; }; +#ifdef _WIN32 static void CoarseWaitsKeepEverySleepFull() { Waker waker; for (unsigned i = 0; i < 12; ++i) { @@ -55,6 +56,7 @@ static void CoarseWaitsKeepEverySleepFull() { Require(Usleep(WAKEABLE_MICROS) >= std::chrono::microseconds(WAKEABLE_MICROS)); } } +#endif static void SleepWithoutWakeIsFull() { for (unsigned i = 0; i < 4; ++i) Require(Usleep(WAKEABLE_MICROS) >= std::chrono::microseconds(WAKEABLE_MICROS)); @@ -112,10 +114,12 @@ static void GapStartsNewRun() { } int main() { +#ifdef _WIN32 if (std::getenv("APS5_COARSE_TIMED_WAITS") != nullptr) { CoarseWaitsKeepEverySleepFull(); return 0; } +#endif SleepWithoutWakeIsFull(); WakeEndsShortSleep(); WakeLeavesLongSleep(); From 6bfb4c5a63d49c683b4d5bb395de946422d97114 Mon Sep 17 00:00:00 2001 From: raghav314 Date: Wed, 14 Oct 2026 09:43:14 +0530 Subject: [PATCH 084/219] perf(agc): data-only draw hits compare only the user words a stage reads A data-only hit (#494) reused a cached stage only when every user data word of the stage was unchanged. A stage records the user words it depends on when it is captured: the words its SRT walk reads as values or bases (SrtUserDataUse), the words its binding layout forwards into shader data (userDataRegisterReads), and the vertex fetch table words of its vertex stage info. A cached stage is now a candidate when only words outside that set changed; its captured memory is validated as before. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../include/Driver/Dispatch/DispatchCache.hpp | 1 + .../include/Driver/Draw/DrawCache.hpp | 1 + .../Execution/src/Driver/Draw/DrawCapture.cpp | 9 + .../Execution/src/Driver/Draw/DrawLookup.cpp | 9 +- .../Graphics/src/PipelineJournal.cpp | 2 +- .../tests/execution/DrawDataHitUserWords.cpp | 188 ++++++++++++++++++ core/shader/recompiler/Recompiler.cpp | 4 + core/shader/recompiler/Recompiler.hpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 6 +- 10 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index b570ae72ad..ba8d85ec82 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2386,6 +2386,8 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_draw_cache_reuse_inline PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=0;APS5_VERIFY_DRAW_RECIPE=1;APS5_VERIFY_REGISTER_KEYS=1" SKIP_RETURN_CODE 77) set_tests_properties(agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline PROPERTIES FAIL_REGULAR_EXPRESSION "FATAL:") set_property(TEST agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off APPEND PROPERTY ENVIRONMENT "APS5_VERIFY_DRAW_RECIPE=1" "APS5_VERIFY_REGISTER_KEYS=1") + add_test(NAME agc_driver_draw_data_hit_user_words_verify COMMAND agc_driver_draw_data_hit_user_words_tests) + set_tests_properties(agc_driver_draw_data_hit_user_words_verify PROPERTIES ENVIRONMENT "APS5_VERIFY_DRAW_DATA_HITS=1" SKIP_RETURN_CODE 77) if(WIN32) set_tests_properties(agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp index 75efb72f56..5f3df6c538 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp @@ -45,6 +45,7 @@ struct DispatchVariant { std::vector runBases; std::uint64_t relocatableWords = 0; std::vector> shaderDataPatches; + std::uint64_t userDataMask = ~std::uint64_t{0}; std::shared_ptr vertexInfo; std::shared_ptr memory; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp index ec149e3d6c..1f625eb0ba 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -122,6 +122,7 @@ struct StageCapture { std::uint64_t forgetSerial = 0; std::uint32_t pushOffset = 0; ShaderRecompiler::SrtUserDataUse userDataUse; + std::uint64_t userDataMask = ~std::uint64_t{0}; }; struct RelocatableUserWord { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index 429d4bff59..902edf3eda 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -103,6 +103,14 @@ std::shared_ptr Driver::materializeDraw } stageCapture.capture = capture; timing.Mark("materialize"); + const auto walked = capture->userDataUse.asValue | capture->userDataUse.asBase; + auto mask = walked == 0 ? ~std::uint64_t{0} : walked | stageCapture.compiled->userDataRegisterReads; + if (vertexInfos[i]) { + for (const auto reg : {static_cast(vertexInfos[i]->fetchAttribReg), static_cast(vertexInfos[i]->fetchBufferReg)}) { + mask |= reg + 1u < 64u ? std::uint64_t{3} << reg : ~std::uint64_t{0}; + } + } + stageCapture.userDataMask = mask; phaseTiming.Phase(DrawRowRecompile); return stageCapture.compiled; } @@ -157,6 +165,7 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP variant->shader = programs[i].snapshot; variant->forgetSerial = stageCapture.forgetSerial; variant->pushOffset = stageCapture.pushOffset; + variant->userDataMask = stageCapture.userDataMask; if (vertexInfos[i]) variant->vertexInfo = std::make_shared(*vertexInfos[i]); std::vector regions(stageCapture.regions.begin(), stageCapture.regions.end()); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index da118192c4..030cf5c1de 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -214,9 +214,16 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr sampling; for (std::size_t i = 0; i < programs.size(); ++i) { - if (roles[i] == Role::GeometryBack || programs[i].userData != dataEntry->decode->programs[i].userData) continue; + if (roles[i] == Role::GeometryBack || programs[i].userData.size() != dataEntry->decode->programs[i].userData.size()) continue; + const auto& now = programs[i].userData; + const auto& was = dataEntry->decode->programs[i].userData; auto& candidates = dataCandidates[i]; for (const auto& variant : dataEntry->stages[i]) { + bool usedEqual = true; + for (std::size_t w = 0; w < now.size() && usedEqual; ++w) { + if (now[w] != was[w] && (w >= 64u || ((variant->userDataMask >> w) & 1u) != 0)) usedEqual = false; + } + if (!usedEqual) continue; if ((i == 0 && variant->pushOffset != 0) || std::any_of(candidates.begin(), candidates.end(), [&](const auto& candidate) { return candidate.first->pushOffset == variant->pushOffset; })) continue; std::vector regions; appendEntryRegions(*variant, regions); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 45481c3c5f..bef4b524ba 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -44,7 +44,7 @@ static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "Tessel constexpr std::uint32_t FileMagic = 0x4c4a5041u; // Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the // layout guards above do not catch (a field renumbered or reinterpreted at the same size). -constexpr std::uint32_t FileFormat = 4u; +constexpr std::uint32_t FileFormat = 5u; constexpr std::uint32_t ModuleRecord = 1u; constexpr std::uint32_t GraphicsRecord = 2u; constexpr std::uint32_t ComputeRecord = 3u; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp new file mode 100644 index 0000000000..106e14522d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp @@ -0,0 +1,188 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array PixelCode{0xf4080100u, 0xfa000000u, 0xbf8cc07fu, 0xe0300000u, 0x80010000u, 0xbf8c3f70u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u}; +alignas(256) std::array ColorMemory{}; + +constexpr std::size_t BlockBytes = 65536; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +std::uint32_t* GuestBlock() { +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "data-only hit: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + return block; +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +void PixelUserData(std::vector& commands, const std::uint32_t* pointer, std::uint32_t unread) { + const auto address = reinterpret_cast(pointer); + Append(commands, 0x76, {0x00c, static_cast(address)}); + Append(commands, 0x76, {0x00d, static_cast(address >> 32u)}); + Append(commands, 0x76, {0x00e, unread}); +} + +std::vector Setup() { + const auto color = reinterpret_cast(ColorMemory.data()); + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 3u << 1u}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + return commands; +} + +AgcDriver::DriverDetail::DrawEntryCounters Submit(std::vector commands) { + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + return AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); +} + +std::string Counts(const AgcDriver::DriverDetail::DrawEntryCounters& counters) { + return std::to_string(counters.lookups) + " lookups, " + std::to_string(counters.dataHits) + " data-only hits, stages " + std::to_string(counters.dataStagesReused) + " reused / " + std::to_string(counters.dataStagesCompiled) + " compiled / " + std::to_string(counters.dataVerified) + " verified"; +} + +void UnreadUserWordsKeepTheStage(bool verify) { + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + auto* first = GuestBlock(); + auto* second = GuestBlock(); + const auto describe = [](std::uint32_t* table, std::uint32_t* data, float value) { + const auto address = reinterpret_cast(data); + data[0] = std::bit_cast(value); + table[0] = static_cast(address); + table[1] = static_cast(address >> 32u) & 0xffffu; + table[2] = 16; + table[3] = 0x4bfacu; + }; + describe(first, first + 1024, 1.0f); + describe(second, second + 1024, 0.5f); + + auto commands = Setup(); + PixelUserData(commands, first, 0x11); + Append(commands, 0x2d, {3, 2}); + auto counters = Submit(std::move(commands)); + Require(counters.lookups == 1 && counters.dataHits == 0, "the first draw: " + Counts(counters)); + + commands.clear(); + PixelUserData(commands, first, 0x22); + Append(commands, 0x2d, {3, 2}); + const auto before = counters; + counters = Submit(std::move(commands)); + const auto reused = counters.dataStagesReused - before.dataStagesReused; + const auto compiled = counters.dataStagesCompiled - before.dataStagesCompiled; + Require(counters.dataHits == before.dataHits + 1, "a draw that changed only a user word no stage reads was not a data-only hit: " + Counts(counters)); + if (verify) Require(compiled == 2 && counters.dataVerified - before.dataVerified == 2, "verify mode: a draw that changed only an unread user word did not build and compare both stages: " + Counts(counters)); + else Require(reused == 2 && compiled == 0, "a draw that changed only a user word no stage reads did not reuse both stages: " + Counts(counters)); + + commands.clear(); + PixelUserData(commands, second, 0x22); + Append(commands, 0x2d, {3, 2}); + const auto moved = counters; + counters = Submit(std::move(commands)); + Require(counters.dataHits == moved.dataHits + 1, "a draw that pointed the pixel stage at another buffer descriptor was not a data-only hit: " + Counts(counters)); + Require(counters.dataStagesCompiled - moved.dataStagesCompiled >= 1, "a draw that pointed the pixel stage at another buffer descriptor reused the pixel stage: " + Counts(counters)); + if (!verify) Require(counters.dataStagesReused - moved.dataStagesReused == 1, "a draw that changed only the pixel stage's descriptor pointer did not reuse the vertex stage: " + Counts(counters)); +} + +} + +int main() { + try { + bool libraries = false; + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + libraries = device->GraphicsPipelineLibraries(); + } + if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped; + const bool verify = std::getenv("APS5_VERIFY_DRAW_DATA_HITS") != nullptr; + UnreadUserWordsKeepTheStage(verify); + AgcDriverShutdown_nid_postfix(); + std::cout << "Data-only hits " << (verify ? "verified" : "reused") << " the stages whose read user words did not change\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 48c22f5a9f..b3f95a193b 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -975,6 +975,10 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .stencilReferenceDword = artifact.stencilReferenceDword, .variantId = artifact.variantId }; + for (const auto reg : variant.bindings.layout.userDataRegisters) { + const auto index = reg - request.context.userDataBaseRegister; + result.userDataRegisterReads |= reg >= request.context.userDataBaseRegister && index < 64u ? std::uint64_t{1} << index : ~std::uint64_t{0}; + } const auto plan = preparedBindingPlan(variant, snapshot, request.context.pixel ? std::span(request.context.pixel->targetExportMapping) : std::span{}, request.target.registrationModules); const DescriptorBindingPlan* bindingPlan = &plan->bindings; std::shared_ptr entry; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 5b1e075939..b7ecc9d0d2 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -676,6 +676,7 @@ struct ShaderInvocation { std::vector pushConstants; bool readsDepthRemapPush = false; std::vector vertexAttributes; + std::uint64_t userDataRegisterReads = 0; std::uint32_t poisonedSrtReads = 0; std::vector imageTablePoison; std::vector> imageTableRanges; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 31815e3fb6..4f5ce7b8b9 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -85,8 +85,8 @@ namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) static_assert(sizeof(CompiledShaderArtifact) == 280, "CompiledShaderArtifact changed: update the artifact encoder"); -static_assert(sizeof(ShaderInvocation) == 208, "ShaderInvocation changed: update the invocation encoder"); -static_assert(sizeof(RecompileResult) == 520, "RecompileResult changed: update EncodeResult and DecodeResult"); +static_assert(sizeof(ShaderInvocation) == 216, "ShaderInvocation changed: update the invocation encoder"); +static_assert(sizeof(RecompileResult) == 528, "RecompileResult changed: update EncodeResult and DecodeResult"); static_assert(sizeof(DescriptorBinding) == 656, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder"); @@ -599,6 +599,7 @@ void encodeInvocation(Writer& writer, const ShaderInvocation& invocation) { out.Value(attribute.fetchIndex); out.Value(attribute.formatComponents); }); + writer.Value(invocation.userDataRegisterReads); writer.Value(invocation.poisonedSrtReads); writer.List(invocation.imageTablePoison, [](Writer& out, const ImageTableEntryPoison& poison) { for (const auto word : poison.words) out.Value(word); @@ -639,6 +640,7 @@ void decodeInvocation(Reader& reader, ShaderInvocation& invocation) { in.Value(attribute.fetchIndex); in.Value(attribute.formatComponents); }); + reader.Value(invocation.userDataRegisterReads); reader.Value(invocation.poisonedSrtReads); reader.List(invocation.imageTablePoison, 44, [](Reader& in, ImageTableEntryPoison& poison) { for (auto& word : poison.words) in.Value(word); From 5bfd1b43a67ed68049a41a2bcb23c761efd1787b Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 15 Oct 2026 13:08:14 -0400 Subject: [PATCH 085/219] test(agc): admit the serial draw key before testing data-only hits --- .../libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp index 106e14522d..f7db841597 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp @@ -142,6 +142,11 @@ void UnreadUserWordsKeepTheStage(bool verify) { Append(commands, 0x2d, {3, 2}); auto counters = Submit(std::move(commands)); Require(counters.lookups == 1 && counters.dataHits == 0, "the first draw: " + Counts(counters)); + commands.clear(); + PixelUserData(commands, first, 0x11); + Append(commands, 0x2d, {3, 2}); + counters = Submit(std::move(commands)); + Require(counters.inserts != 0, "the repeated register key was not admitted: " + Counts(counters)); commands.clear(); PixelUserData(commands, first, 0x22); From 16acce08df0e4bc73d35b76a6f7436312e009046 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 15 Oct 2026 14:49:02 -0400 Subject: [PATCH 086/219] test(shader): round-trip full user-word dependency masks --- core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 1796f83acb..e04fa1c149 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -132,6 +132,7 @@ void requireSameResult(const RecompileResult& left, const RecompileResult& right require(left.shaderDataUserWords == right.shaderDataUserWords, prefix + "shader-data user words differ"); require(left.imageTableRanges == right.imageTableRanges && left.imageTableShader == right.imageTableShader && left.imageTableFaults == right.imageTableFaults && left.imageTablePoison.size() == right.imageTablePoison.size(), prefix + "image table invocation data differs"); require(left.imageTableReadRanges == right.imageTableReadRanges, prefix + "image table read ranges differ"); + require(left.userDataRegisterReads == right.userDataRegisterReads, prefix + "user data register read mask differs"); for (std::size_t i = 0; i < left.imageTablePoison.size(); ++i) { const auto& a = left.imageTablePoison[i]; const auto& b = right.imageTablePoison[i]; @@ -232,6 +233,7 @@ RecompileResult sampleResult() { result.imageTableReadRanges = {{{0x1000u, 32u}, {0x1040u, 32u}}, {}, {{0x5000000000ull, 16u}}}; result.imageTableShader = 0x80003aa700ull; result.imageTableFaults = 3u; + result.userDataRegisterReads = (std::uint64_t{1} << 63u) | (std::uint64_t{1} << 32u) | 0x25u; return result; } From 5892c846c75bb17c10e1c5ebe96bea0d2db30c5a Mon Sep 17 00:00:00 2001 From: raghav314 Date: Fri, 16 Oct 2026 20:43:22 +0530 Subject: [PATCH 087/219] test(agc): a pixel stage that adds a user word read straight from its SGPR follows a change of that word The word reaches the data-only mask through userDataRegisterReads (the stage layout's user-data registers). Dropping those bits from the mask makes the draw reuse the old stage (it writes 127, expected 191) and verify mode abort. LLM assistance used. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 +- .../tests/execution/DrawDataHitUserWords.cpp | 50 +++++++++++++++++-- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index ba8d85ec82..24e05c116e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2389,7 +2389,7 @@ if(BUILD_TESTING) add_test(NAME agc_driver_draw_data_hit_user_words_verify COMMAND agc_driver_draw_data_hit_user_words_tests) set_tests_properties(agc_driver_draw_data_hit_user_words_verify PROPERTIES ENVIRONMENT "APS5_VERIFY_DRAW_DATA_HITS=1" SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline agc_driver_draw_data_hit_user_words_verify PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp index f7db841597..13d67731a9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "VulkanTestDevice.hpp" @@ -8,6 +9,7 @@ #endif #include #endif +#include #include #include #include @@ -27,6 +29,12 @@ using AgcDriver::Graphics::Require; alignas(256) constexpr std::array VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u}; alignas(256) constexpr std::array PixelCode{0xf4080100u, 0xfa000000u, 0xbf8cc07fu, 0xe0300000u, 0x80010000u, 0xbf8c3f70u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array TriangleVertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, +}; +alignas(256) constexpr std::array ArithmeticPixelCode{0xf4000100u, 0xfa000000u, 0xbf8cc07fu, 0x7e000204u, 0x06000002u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u}; +alignas(256) std::array PixelConstant{}; alignas(256) std::array ColorMemory{}; constexpr std::size_t BlockBytes = 65536; @@ -38,6 +46,7 @@ struct Header { alignas(8) Header VertexHeader{}; alignas(8) Header PixelHeader{}; +alignas(8) Header ArithmeticPixelHeader{}; void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { header.shader.file_header = 0x34333231u; @@ -74,11 +83,11 @@ void PixelUserData(std::vector& commands, const std::uint32_t* po Append(commands, 0x76, {0x00e, unread}); } -std::vector Setup() { +std::vector Setup(bool triangle = false) { const auto color = reinterpret_cast(ColorMemory.data()); - const auto vertex = reinterpret_cast(VertexCode.data()); - const auto pixel = reinterpret_cast(PixelCode.data()); - const std::vector> context{ + const auto vertex = reinterpret_cast(triangle ? TriangleVertexCode.data() : VertexCode.data()); + const auto pixel = reinterpret_cast(triangle ? ArithmeticPixelCode.data() : PixelCode.data()); + std::vector> context{ {0x2d5, 0x2000}, {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, @@ -100,9 +109,10 @@ std::vector Setup() { {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, }; const std::vector> shader{ - {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0}, + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, triangle ? 0u : 1u << 1u}, {0x8c, 0}, {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 3u << 1u}, }; + if (triangle) context.insert(context.end(), {{0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}}); std::vector commands; Append(commands, 0x79, {0x242, 4}); for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); @@ -169,6 +179,35 @@ void UnreadUserWordsKeepTheStage(bool verify) { if (!verify) Require(counters.dataStagesReused - moved.dataStagesReused == 1, "a draw that changed only the pixel stage's descriptor pointer did not reuse the vertex stage: " + Counts(counters)); } +unsigned BrightestColorByte() { + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(ColorMemory.data()), ColorMemory.size()); + unsigned brightest = 0; + for (const auto value : ColorMemory) brightest = std::max(brightest, static_cast(std::to_integer(value))); + return brightest; +} + +void ReadUserWordsChangeTheOutput() { + Registered(VertexHeader, TriangleVertexCode.data(), sizeof(TriangleVertexCode), 2); + Registered(ArithmeticPixelHeader, ArithmeticPixelCode.data(), sizeof(ArithmeticPixelCode), 1); + PixelConstant[0] = 0.25f; + auto* table = reinterpret_cast(PixelConstant.data()); + const auto draw = [&](float word, bool setup) { + auto commands = setup ? Setup(true) : std::vector{}; + PixelUserData(commands, table, std::bit_cast(word)); + Append(commands, 0x2d, {3, 2}); + Submit(std::move(commands)); + return BrightestColorByte(); + }; + draw(0.25f, true); + const auto first = draw(0.25f, false); + const auto before = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + const auto second = draw(0.5f, false); + const auto after = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + const auto closeTo = [](unsigned value, unsigned expected) { return value + 2 >= expected && value <= expected + 2; }; + Require(closeTo(first, 128), "a pixel stage adding user word 2 (0.25) to a loaded 0.25 wrote " + std::to_string(first) + ", expected 128"); + Require(closeTo(second, 191), "a pixel stage adding user word 2 changed to 0.5 wrote " + std::to_string(second) + ", expected 191 (" + Counts(after) + ", before: " + Counts(before) + ")"); +} + } int main() { @@ -182,6 +221,7 @@ int main() { if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped; const bool verify = std::getenv("APS5_VERIFY_DRAW_DATA_HITS") != nullptr; UnreadUserWordsKeepTheStage(verify); + ReadUserWordsChangeTheOutput(); AgcDriverShutdown_nid_postfix(); std::cout << "Data-only hits " << (verify ? "verified" : "reused") << " the stages whose read user words did not change\n"; return 0; From f57b79ef51b866d7da820a9592a093b6d5e90e87 Mon Sep 17 00:00:00 2001 From: bl4nkTHEBUNKER Date: Sun, 18 Oct 2026 17:31:09 +0200 Subject: [PATCH 088/219] fix(libScePad): report the DualSense touchpad resolution The controller information reported 1920 by 943, the DualShock 4 height with the DualSense pixel density, while the host controller's fingers were scaled to 1920 by 942 and the keyboard touch contacts sat on the 942 centre line. A title that maps touch coordinates through the reported resolution lost the lower part of the touchpad. The width and height now live beside the pad state and all three places use them. --- core/libs/prx/libScePad/Export.cpp | 8 +-- core/libs/prx/libScePad/include/PadState.hpp | 3 ++ core/libs/prx/libScePad/src/PadState.cpp | 4 +- core/libs/prx/libSceVideoOut/src/PadInput.cpp | 6 +-- core/libs/tests/GuestPad.cpp | 50 ++++++++++++++++++- docs/dev/debt/libScePad.md | 4 ++ 6 files changed, 64 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 2bd8a89989..4473f9e76d 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -78,8 +78,8 @@ int APS5_VABI scePadGetControllerInformation(int handle, PadControllerInformatio } std::memset(info, 0, sizeof(*info)); info->touchPadInfo.pixelDensity = 44.86f; - info->touchPadInfo.resolution.x = 1920; - info->touchPadInfo.resolution.y = 943; + info->touchPadInfo.resolution.x = kTouchPadWidth; + info->touchPadInfo.resolution.y = kTouchPadHeight; info->stickInfo.deadZoneLeft = 2; info->stickInfo.deadZoneRight = 2; info->connectionType = PAD_CONNECTION_TYPE_LOCAL; @@ -122,8 +122,8 @@ int APS5_VABI scePadGetExtControllerInformation(int handle, void* info) { std::memset(info, 0, 0x2c); PadControllerInformation* base = static_cast(info); base->touchPadInfo.pixelDensity = 44.86f; - base->touchPadInfo.resolution.x = 1920; - base->touchPadInfo.resolution.y = 943; + base->touchPadInfo.resolution.x = kTouchPadWidth; + base->touchPadInfo.resolution.y = kTouchPadHeight; base->stickInfo.deadZoneLeft = 2; base->stickInfo.deadZoneRight = 2; base->connectionType = PAD_CONNECTION_TYPE_LOCAL; diff --git a/core/libs/prx/libScePad/include/PadState.hpp b/core/libs/prx/libScePad/include/PadState.hpp index 8186ee492a..fb621171ee 100644 --- a/core/libs/prx/libScePad/include/PadState.hpp +++ b/core/libs/prx/libScePad/include/PadState.hpp @@ -6,6 +6,9 @@ #include #include "SceTypes.hpp" +constexpr std::uint16_t kTouchPadWidth = 1920; +constexpr std::uint16_t kTouchPadHeight = 1080; + struct PadTouchPoint { bool active = false; std::uint16_t x = 0; diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index a5a82256eb..5a6795f32e 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -202,8 +202,8 @@ PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uin if (touchNum == 0 && (state.touchLeft || state.touchRight || injected.touchLeft || injected.touchRight)) { data.buttons |= 0x100000; touchNum = 1; - data.touch_data_touch0_x = state.touchRight || injected.touchRight ? 1440 : 480; - data.touch_data_touch0_y = 471; + data.touch_data_touch0_x = state.touchRight || injected.touchRight ? kTouchPadWidth * 3 / 4 : kTouchPadWidth / 4; + data.touch_data_touch0_y = kTouchPadHeight / 2; data.touch_data_touch0_id = 0; } data.touch_data_touch_num = touchNum; diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index a48381cf06..fe24a62fc0 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -185,12 +185,12 @@ PadInputState PadInput::sampleController() const { float pressure = 0.0f; if (SDL_GameControllerGetTouchpadFinger(controller, 0, finger, &down, &x, &y, &pressure) != 0 || down == 0) continue; result.touch[finger].active = true; - result.touch[finger].x = static_cast(std::clamp(x, 0.0f, 1.0f) * 1919.0f); - result.touch[finger].y = static_cast(std::clamp(y, 0.0f, 1.0f) * 942.0f); + result.touch[finger].x = static_cast(std::clamp(x, 0.0f, 1.0f) * (kTouchPadWidth - 1)); + result.touch[finger].y = static_cast(std::clamp(y, 0.0f, 1.0f) * (kTouchPadHeight - 1)); } } if (viewPressed && !result.touch[0].active && !result.touch[1].active) { - result.touch[0] = {true, 960, 471, 0}; + result.touch[0] = {true, kTouchPadWidth / 2, kTouchPadHeight / 2, 0}; } return result; } diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index e17b0521d1..bb034206c1 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -3,8 +3,10 @@ #include "prx/libScePad/include/PadState.hpp" #include "prx/libScePad/include/InputCommandFile.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include +#include #include #include #include @@ -28,6 +30,8 @@ int APS5_VABI scePadSetAngularVelocityDeadbandState(int, bool); int APS5_VABI scePadSetAngularVelocityBiasCorrectionState(int, bool); int APS5_VABI scePadIsRemoteController(int, bool*); int APS5_VABI scePadGetAccessibilityControllerState(int, std::uint32_t*); +int APS5_VABI scePadGetControllerInformation(int, PadControllerInformation*); +int APS5_VABI scePadGetExtControllerInformation(int, void*); } static void Require(bool value) { if (!value) std::abort(); } @@ -156,6 +160,46 @@ static void CheckTouchContact() { Pad::ReadState(); } +static void CheckTouchPadInformation(int handle) { + PadControllerInformation info; + std::memset(&info, 0x5a, sizeof(info)); + Require(scePadGetControllerInformation(handle, &info) == PAD_OK); + Require(info.touchPadInfo.resolution.x == kTouchPadWidth); + Require(info.touchPadInfo.resolution.y == kTouchPadHeight); + Require(info.touchPadInfo.pixelDensity == 44.86f); + Require(scePadGetControllerInformation(handle + 1, &info) == PAD_ERROR_INVALID_HANDLE); + + std::array extended{}; + extended.fill(0x5a); + Require(scePadGetExtControllerInformation(handle, extended.data()) == PAD_OK); + PadControllerInformation base{}; + std::memcpy(&base, extended.data(), sizeof(base)); + Require(base.touchPadInfo.resolution.x == kTouchPadWidth); + Require(base.touchPadInfo.resolution.y == kTouchPadHeight); + Require(base.touchPadInfo.pixelDensity == 44.86f); +} + +static void CheckVirtualTouchContact() { + PadInputState left; + left.touchLeft = true; + PadPublishInput_nid_postfix(left); + const auto leftData = Pad::ReadState(); + Require(leftData.touch_data_touch_num == 1); + Require(leftData.touch_data_touch0_x == kTouchPadWidth / 4); + Require(leftData.touch_data_touch0_y == kTouchPadHeight / 2); + + PadInputState right; + right.touchRight = true; + PadPublishInput_nid_postfix(right); + const auto rightData = Pad::ReadState(); + Require(rightData.touch_data_touch_num == 1); + Require(rightData.touch_data_touch0_x == kTouchPadWidth * 3 / 4); + Require(rightData.touch_data_touch0_y == kTouchPadHeight / 2); + + PadPublishInput_nid_postfix(PadInputState{}); + Pad::ReadState(); +} + static void CheckPartialTrigger() { PadInputState partial; partial.buttons = static_cast(Pad::PadButton::L2) | static_cast(Pad::PadButton::R2); @@ -303,7 +347,7 @@ static void CheckInjectedState(const std::filesystem::path& path) { const auto centred = Pad::ReadState(); Require(centred.left_stick_x == 42 && centred.right_stick_y == 17); Require(centred.timestamp > moved.timestamp); - for (const auto& [command, x] : {std::pair{"3 2097152 300\n", 480}, std::pair{"4 4194304 300\n", 1440}}) { + for (const auto& [command, x] : {std::pair{"3 2097152 300\n", kTouchPadWidth / 4}, std::pair{"4 4194304 300\n", kTouchPadWidth * 3 / 4}}) { std::ofstream(path) << command; PadData touched{}; const auto touchDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); @@ -312,7 +356,7 @@ static void CheckInjectedState(const std::filesystem::path& path) { if (touched.touch_data_touch_num != 0) break; std::this_thread::sleep_for(std::chrono::milliseconds(10)); } while (std::chrono::steady_clock::now() < touchDeadline); - Require(touched.touch_data_touch_num == 1 && touched.touch_data_touch0_x == x && touched.touch_data_touch0_y == 471); + Require(touched.touch_data_touch_num == 1 && touched.touch_data_touch0_x == x && touched.touch_data_touch0_y == kTouchPadHeight / 2); Require(touched.buttons == (0x2000 | 0x100000)); std::this_thread::sleep_for(std::chrono::milliseconds(310)); const auto lifted = Pad::ReadState(); @@ -389,6 +433,8 @@ int main() { CheckTiltCorrection(handle); CheckAngularVelocityDeadband(handle); CheckTouchContact(); + CheckVirtualTouchContact(); + CheckTouchPadInformation(handle); CheckPartialTrigger(); CheckReadQueue(handle); CheckTapBetweenReads(); diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 7b1ac780bb..c52607d8ee 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -160,3 +160,7 @@ - [scePadControllerUpdateResetController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadControllerUpdateGetReconnectStatus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadControllerUpdateTerm](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature + +### Functional + +- [Touchpad resolution](../../../core/libs/prx/libScePad/include/PadState.hpp) (libScePad) - the library reports a DualSense to every title, so the controller information reports its touchpad, 1920 by 1080 at 44.86 pixels per cm, and the host controller's fingers and the keyboard touch contacts are placed in that range. [duaLib](https://github.com/WujekFoliarz/duaLib/blob/0b39f92261e2ff0e77eb0dac00943be6970e0dbf/src/source/duaLib.cpp#L847-L849) reports these values for a DualSense and 1920 by 943 at 44 for a DualShock 4, and the Linux kernel's [hid-playstation](https://github.com/torvalds/linux/blob/cd4f494ba535e848e2d299707d363de84e61a518/drivers/hid/hid-playstation.c#L177-L178) holds 1920 by 1080 against 1920 by 942 for the DualShock 4. A DualShock 4 on the host reports through the DualSense range too, so its last touchpad rows are never reached. The console's values are not measured From f95471ec6aabda37a3e26cbe0677e6688f52961d Mon Sep 17 00:00:00 2001 From: battleblob1 Date: Sun, 18 Oct 2026 05:35:14 +0300 Subject: [PATCH 089/219] feat(shader): read domain inputs at a control-point index computed from data A domain program that picks one of its patch's control points from data (Onimusha: Way of the Sword, PPSA27836, selects a corner with v_cmp and v_cndmask before a buffer_load from the off-chip ring) was refused with "the tessellation ring or LDS address is not affine in the control point and relative patch". Computed indices were accepted only for hull LDS reads. - The address evaluator gives off-chip EvaluationInput reads the same dynamic term as hull LDS reads, and a value read from the patch's own ring or LDS no longer counts as depending on the relative patch index, since the lowered read takes the patch implicitly. - When the program also uses its relative patch index as data, the index is replaced by the grouped patch instead of 0, which left a computed read's address pointing into another patch. Such reads now subtract their relative patch term, for hull and domain reads alike; reads in programs that do not use the index as data keep their address. - The computed offset must be a whole number of control points, and the backend clamps the domain gl_in index to the last output control point as it already does for hull reads. Co-Authored-By: Claude Opus 5.5 --- .../tests/TessellationLowering.cpp | 17 +++++++- .../tests/execution/Tessellation.cpp | 13 +++++- .../Optimization/src/TessellationLowering.cpp | 40 +++++++++++++++++-- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 5 ++- docs/dev/debt/shader.md | 2 +- 5 files changed, 67 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp b/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp index 287ca9bfd8..1b1ec2d0da 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp @@ -102,11 +102,12 @@ struct Outcome { }; const ShaderTessellationInputInfo Tessellation{.inputControlPoints = InputControlPoints, .outputControlPoints = InputControlPoints, .patchesPerGroup = 1u}; +const ShaderTessellationInputInfo GroupedTessellation{.inputControlPoints = InputControlPoints, .outputControlPoints = InputControlPoints, .patchesPerGroup = 1u, .primitiveGroupSize = 1u}; -Outcome Lower(IrProgram& program) { +Outcome Lower(IrProgram& program, const ShaderTessellationInputInfo& tessellation = Tessellation) { Outcome outcome; try { - TessellationLowering{}.Lower(program, Tessellation); + TessellationLowering{}.Lower(program, tessellation); } catch (const std::exception& error) { outcome.reason = error.what(); return outcome; @@ -193,5 +194,17 @@ int main() { return RefusedBecause(hull.program, "not a whole number of control points"); }); + passed &= run("a hull read at a computed control-point index stays in its patch when the relative patch is also data", [] { + Hull hull; + auto& address = hull.inPatch(hull.computed(2u, LocalStride)); + hull.read(address); + static_cast(hull.builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&hull.relativePatch()})); + const Outcome outcome = Lower(hull.program, GroupedTessellation); + if (!outcome.lowered) std::fprintf(stderr, "refused: %s\n", outcome.reason.c_str()); + if (!outcome.lowered || outcome.emitted->Opcode() != IrOpcode::ISub32 || outcome.emitted->Argument(0)->Resolve() != address.Resolve()) return false; + const IrValue* patch = outcome.emitted->Argument(1)->Resolve(); + return patch->Opcode() == IrOpcode::IMul32 && patch->Argument(1)->Resolve()->ImmediateU32() == PatchStride; + }); + return passed ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index 9ed2c5c32c..eac5670afb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -118,6 +118,14 @@ alignas(256) constexpr std::array DomainCode{ 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, }; +alignas(256) constexpr std::array DomainComputedPoint{ + 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xbf8c3f70, 0x7c0214ff, + 0x7f7fffff, 0xd5010002, 0x01a90280, 0xd7460002, 0x04050902, 0xe0341010, 0x04030c02, 0xe0341020, + 0x04030e02, 0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, 0x10221705, + 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, 0x40400000, 0x7e260280, 0x7e2802f2, + 0xf80008cf, 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, +}; + alignas(256) constexpr std::array DomainLoweredInterior{ 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, @@ -504,12 +512,12 @@ std::uint32_t CoveredPixels(std::uint32_t patch, bool interior) { return count; } -void RequirePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target, std::span hull, const std::string& program, const PatchDraw& patches = Triangles, std::span local = LocalCode) { +void RequirePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target, std::span hull, const std::string& program, const PatchDraw& patches = Triangles, std::span local = LocalCode, std::span domain = DomainCode) { std::uint32_t cursor = 0; const Stages stages{ Compile(target, ShaderStage::Local, local, cursor, patches), Compile(target, ShaderStage::TessellationControl, hull, cursor, patches), - Compile(target, ShaderStage::TessellationEvaluation, DomainCode, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, domain, cursor, patches), Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), }; const auto center = Row(-0.5f + 1.0f / 3.0f); @@ -874,6 +882,7 @@ int main() { RequirePatches(*device, target, HullRestoredExecSaved, "hull program saving its restored exec"); RequirePatches(*device, target, HullNextInput, "hull program reading the next of 4 input control points of 24 bytes", NextInputTriangles, LocalCodeStride24); RequirePatches(*device, target, HullDynamicIndex, "hull program reading a control point at a computed index"); + RequirePatches(*device, target, HullCode, "domain program reading control points at a computed index", Triangles, LocalCode, DomainComputedPoint); RequirePatches(*device, target, HullAndSaveexec, "hull program narrowing exec with s_and_saveexec_b64"); RequirePatches(*device, target, HullSaveexecLoop, "hull program restoring exec through loop phis"); RequirePatches(*device, target, HullDescendingRecords, "hull program with descending patch records in 128-patch groups", {{3u, 3u, 1u, 2u, 2u, 128u}, Indices}); diff --git a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp index 3304659e91..fff671e247 100644 --- a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp +++ b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp @@ -62,6 +62,11 @@ bool IsBase(const IrValue* raw, TessellationBaseKind kind) { return value->Opcode() == IrOpcode::TessellationBase && IsImmediate(value->Argument(0), static_cast(kind)); } +bool OwnPatchRead(const IrValue* value) { + if (SharedAccessOf(value->Opcode()) == SharedAccess::Read) return true; + return BufferAccessOf(value->Opcode()) == BufferAccess::Read && value->ArgumentCount() > 3u && IsBase(value->Argument(3), TessellationBaseKind::OffChip); +} + std::int64_t FloorDiv(std::int64_t value, std::int64_t divisor) { const auto quotient = value / divisor; return quotient * divisor > value ? quotient - 1 : quotient; @@ -351,6 +356,7 @@ private: pending.push_back(value->Argument(1)->Resolve()); continue; } + if (value != start && OwnPatchRead(value)) continue; for (std::size_t index = 0; index < value->ArgumentCount(); ++index) { if (IrValue* argument = value->Argument(index)) pending.push_back(argument->Resolve()); } @@ -585,6 +591,7 @@ public: } const bool data = usedAsData(relative); if (data) requireGroupedPatches(); + if (data) keepComputedReadsInPatch(*relative.front()); for (IrValue* base : relative) base->ReplaceAllUsesWith(data ? &groupedPatch(*base) : &constant(0u)); deadCodeEliminator.Eliminate(program); if (stage == IrShaderStage::Local && tessellation.drawParameters) writeInstance(); @@ -647,7 +654,7 @@ private: address = memory.offen ? inst->Argument(2) : nullptr; } if (address != nullptr) { - const auto evaluated = evaluator.Evaluate(address, access.predicate, access.kind == TessellationAttribute::ControlInput); + const auto evaluated = evaluator.Evaluate(address, access.predicate, access.kind == TessellationAttribute::ControlInput || access.kind == TessellationAttribute::EvaluationInput); if (!evaluated) Fail("the tessellation ring or LDS address is not affine in the control point and relative patch", flags.pc); access.address = *evaluated; access.addressValue = address; @@ -682,6 +689,8 @@ private: const auto end = access.address.constant + 4 * static_cast(access.components); if (access.address.relative > 0 && access.address.relative % outputs == 0 && access.address.constant >= 0 && end <= access.address.relative) { agree(controlStride, access.address.relative / outputs, access.pc); + } else if (access.address.dynamic) { + Fail("the domain shader reads off-chip memory at a computed offset outside its patch's control points", access.pc); } else { access.kind = TessellationAttribute::PatchInput; } @@ -720,6 +729,7 @@ private: break; case TessellationAttribute::EvaluationInput: if (address.constant < 0 || end > outputs * controlStride) Fail("the domain shader reads off-chip memory outside its patch's control points", access.pc); + if (address.dynamic && (address.dynamicScale == 0 || address.dynamicScale % controlStride != 0)) Fail("the domain shader reads off-chip memory at a computed offset that is not a whole number of control points", access.pc); break; case TessellationAttribute::Factor: { const bool quads = tessellation.domain == 2u; @@ -745,9 +755,12 @@ private: bool dynamicInputAddress(IrValue* user, IrValue* value) { if (user->Opcode() != IrOpcode::GetTessellationAttribute || user->Argument(1)->Resolve() != value->Resolve()) return false; - if (!IsImmediate(user->Argument(0), static_cast(TessellationAttribute::ControlInput))) return false; + const bool control = IsImmediate(user->Argument(0), static_cast(TessellationAttribute::ControlInput)); + const bool evaluation = IsImmediate(user->Argument(0), static_cast(TessellationAttribute::EvaluationInput)); + if (!control && !evaluation) return false; const auto address = evaluator.Evaluate(value, user->Argument(2), true); - return address && address->dynamic && address->relative == static_cast(tessellation.inputControlPoints) * localStride; + const auto stride = control ? static_cast(tessellation.inputControlPoints) * localStride : static_cast(tessellation.outputControlPoints) * controlStride; + return address && address->dynamic && address->relative == stride; } bool usedAsData(const std::vector& bases) { @@ -901,6 +914,27 @@ private: return insert(access, IrOpcode::IAdd32, IrType::U32, {&scaled, &offset}); } + void keepComputedReadsInPatch(IrValue& base) { + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + if (inst->Opcode() != IrOpcode::GetTessellationAttribute) continue; + const bool control = IsImmediate(inst->Argument(0), static_cast(TessellationAttribute::ControlInput)); + if (!control && !IsImmediate(inst->Argument(0), static_cast(TessellationAttribute::EvaluationInput))) continue; + const auto address = evaluator.Evaluate(inst->Argument(1), inst->Argument(2), true); + if (!address || !address->dynamic || address->relative == 0) continue; + IrValue& patch = program.CreateValue(IrOpcode::IMul32, IrType::U32); + patch.AddArgument(&base); + patch.AddArgument(&constant(static_cast(address->relative))); + block->InsertInstructionBefore(inst, &patch); + IrValue& inPatch = program.CreateValue(IrOpcode::ISub32, IrType::U32); + inPatch.AddArgument(inst->Argument(1)); + inPatch.AddArgument(&patch); + block->InsertInstructionBefore(inst, &inPatch); + inst->ReplaceArgument(1, &inPatch); + } + } + } + void rewrite(const Access& access) { IrValue& kind = constant(static_cast(access.kind)); const bool shared = SharedAccessOf(access.inst->Opcode()) != SharedAccess::None; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 390c82cb30..3e1f78c096 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -796,9 +796,10 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins const auto stride = local ? state.program.Metadata().tessellationLocalStride : state.program.Metadata().tessellationControlStride; const auto type = TypePointer(state, storage, TypeU32(state)); const auto inPatch = [&](std::uint32_t vertex) { - if (kind != TessellationAttribute::ControlInput) return vertex; + if (kind != TessellationAttribute::ControlInput && kind != TessellationAttribute::EvaluationInput) return vertex; + const auto& tess = state.inputInfo.vertex->tess; const auto clamped = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, TypeU32(state), clamped, GlslStd450(state), GLSLstd450UMin, vertex, ConstantU32(state, state.inputInfo.vertex->tess.inputControlPoints - 1u)); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), clamped, GlslStd450(state), GLSLstd450UMin, vertex, ConstantU32(state, (kind == TessellationAttribute::ControlInput ? tess.inputControlPoints : tess.outputControlPoints) - 1u)); return clamped; }; const auto* sum = inst.Argument(1); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index c2b4084d5f..4b4229fd82 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -122,7 +122,7 @@ - [POPS collision SGPRs](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the collision wave ID and intrawave collision mask read 0, so a title skips its POPS wait and runs its per-collision loop once. - [Image loads](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) (shader recompiler) - an image_load reads a sampled binding, also of an image the shader stores to, so in a primitive-ordered pixel shader it misses an earlier overlapping primitive's image_store in the same draw. - [Tessellation rings](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) of local, hull and domain programs become Vulkan tessellation interfaces and are never written to guest memory; a title that reads the off-chip or factor ring back sees no data. -- [Hull input control points](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a hull LDS read at a control-point index computed from data reads `gl_in` at that index clamped to the patch's last input control point. On the console an index past the patch reads the LDS slot of a neighbouring patch or of no patch; the host has no such memory, and Vulkan leaves an out-of-range `gl_in` index undefined. The lowering refuses a computed offset that is not a whole number of control points, but the index itself is only known when the program runs. +- [Hull input control points](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a hull LDS read at a control-point index computed from data reads `gl_in` at that index clamped to the patch's last input control point, and a domain off-chip read at a computed index reads `gl_in` clamped to the patch's last output control point. On the console an index past the patch reads the LDS slot or off-chip record of a neighbouring patch or of no patch; the host has no such memory, and Vulkan leaves an out-of-range `gl_in` index undefined. The lowering refuses a computed offset that is not a whole number of control points, but the index itself is only known when the program runs. - [Inner tessellation factors](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) of integer, fractional-even or fractional-odd triangle patches that are NaN tessellate as 1 (fractional-even then raises them to 2, as Vulkan does), as in the D3D11 reference tessellator; the console is not measured. - [Hull programs](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) start from the hull launch registers (user data, ring bases, v0, v1); on the console they start with what the local program leaves, and that it leaves them unchanged is not checked. - [Quad tessellation factors](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) are taken as outer[0..3] then inner[0..1], 24 bytes per patch, as [Mesa stores them](https://github.com/mihawk-99/PS5_Mesa/blob/b3588f78dd7fd46fbadc7ecd487e5f14d4213e4b/src/amd/common/nir/ac_nir_lower_tess_io_to_mem.c); unmeasured on the console. From 6ce8bc037357228db576d5c36230cdb5cd686402 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:21:53 +0300 Subject: [PATCH 090/219] feat(libc, libkernel): register pthread_create and pthread_join callbacks with libc --- core/libs/prx/libc/include/General.hpp | 5 +++++ core/libs/prx/libc/src/Threads.cpp | 10 ++++++++++ core/libs/prx/libkernel/Pthread/Posix/Thread.cpp | 10 ++++++++++ 3 files changed, 25 insertions(+) diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 2db5852780..86b9205ecb 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -45,6 +45,11 @@ extern "C" void* HostStandardHandle_nid_no_patch(int descriptor); extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle); #endif +using PthreadCreateFn_nid_no_patch = int (APS5_VABI*)(void**, const void*, void* (APS5_VABI*)(void*), void*); +using PthreadJoinFn_nid_no_patch = int (APS5_VABI*)(void*, void**); +extern "C" void RegisterPthreadCreate_nid_no_patch(PthreadCreateFn_nid_no_patch fn); +extern "C" void RegisterPthreadJoin_nid_no_patch(PthreadJoinFn_nid_no_patch fn); + #define APS5_INVALID_ARG_EX throw std::invalid_argument(std::string(__func__) + ": invalid argument") #define APS5_DUMMY_FUN \ diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 167360e72c..1a1d07d4da 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -304,5 +304,15 @@ int APS5_VABI _Cnd_signal_nid_postfix(void** handle) { return SignalCondition_nid_no_patch(handle); } +static std::atomic registeredPthreadCreate{nullptr}; +static std::atomic registeredPthreadJoin{nullptr}; + +void RegisterPthreadCreate_nid_no_patch(PthreadCreateFn_nid_no_patch fn) { + registeredPthreadCreate.store(fn, std::memory_order_release); } +void RegisterPthreadJoin_nid_no_patch(PthreadJoinFn_nid_no_patch fn) { + registeredPthreadJoin.store(fn, std::memory_order_release); +} + +} diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index 4cd5a94a3f..959790db4a 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -225,6 +225,16 @@ int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value) { return PosixThread::ToErrno(scePthreadJoin(thread, value)); } +namespace { +struct RegisterPthreadOnLoad { + RegisterPthreadOnLoad() { + RegisterPthreadCreate_nid_no_patch(reinterpret_cast(pthread_create_nid_postfix)); + RegisterPthreadJoin_nid_no_patch(reinterpret_cast(pthread_join_nid_postfix)); + } +}; +const RegisterPthreadOnLoad gRegisterPthread; +} + int APS5_VABI pthread_rename_np_nid_postfix(Pthread thread, const char* name) { if (thread && name && std::strlen(name) >= 32) return PosixThread::ToErrno(SCE_KERNEL_ERROR_ENAMETOOLONG); return PosixThread::ToErrno(scePthreadRename(thread, name)); From 3524b04740b4b061960c3ea3483a1e8f9c35dbf8 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:22:00 +0300 Subject: [PATCH 091/219] feat(libc): move Dinkumware thread exports from libSceAmpr to libc --- core/libs/prx/libSceAmpr/Export.cpp | 110 ---------------------------- core/libs/prx/libc/src/Threads.cpp | 92 +++++++++++++++++++++++ 2 files changed, 92 insertions(+), 110 deletions(-) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index fd0d7cf527..4ea956190e 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -226,18 +226,6 @@ static int RecordWriteAddressFromCounterPair(Apr::CommandBufferObject* buffer, v return AppendCommand(buffer, Apr::Opcode::WriteAddressFromCounterPair, Apr::WriteAddressFromCounterCommand{{}, reinterpret_cast(address), counter, counter + 1u}); } -static constexpr int ThrdSuccess = 0; -static constexpr int ThrdNomem = 1; -static constexpr int ThrdTimedout = 2; -static constexpr int ThrdBusy = 3; -static constexpr int ThrdError = 4; - -static constexpr int CppDeviceOrResourceBusy = 0; -static constexpr int CppInvalidArgument = 1; -static constexpr int CppResourceUnavailableTryAgain = 6; - -static constexpr int MtxPlain = 0x1; - static std::atomic_flag sharedPtrSpinLock; struct AmmUsageStatsData { @@ -252,55 +240,10 @@ static_assert(sizeof(AmmUsageStatsData) == 0x18); static constexpr std::uint16_t AmmPageTablePoolEntries = 512; static constexpr std::uint32_t AmmRingIdleFlags = 0x7; -struct GuestPad { - unsigned (APS5_VABI* const* vtable)(GuestPad*); - void* condition; - void* mutex; - bool started; -}; - -static_assert(offsetof(GuestPad, condition) == 0x8 && offsetof(GuestPad, mutex) == 0x10 && offsetof(GuestPad, started) == 0x18 && sizeof(GuestPad) == 0x20); - extern "C" { -int APS5_VABI pthread_create_nid_postfix(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg); -int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value); -int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type); -void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); -int APS5_VABI _Mtx_lock_nid_postfix(void** handle); -int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); int APS5_VABI _Cnd_init_nid_postfix(void** handle); -void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); -int APS5_VABI _Cnd_wait_nid_postfix(void** condition, void** mutex); -int APS5_VABI _Cnd_signal_nid_postfix(void** handle); int APS5_VABI wcsrtombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t** source, std::size_t limit, void* state); [[noreturn]] void APS5_VABI __cxa_call_terminate_nid_postfix(void* exception); -int APS5_VABI _Cnd_init_nid_postfix(void** handle); -int SignalCondition_nid_no_patch(void** handle); -[[noreturn]] void ThrowCppError_nid_no_patch(int code); -} - -static void RequireThrd(int result, const char* what) { - switch (result) { - case ThrdSuccess: - return; - case ThrdNomem: - case ThrdTimedout: - ThrowCppError_nid_no_patch(CppResourceUnavailableTryAgain); - case ThrdBusy: - ThrowCppError_nid_no_patch(CppDeviceOrResourceBusy); - case ThrdError: - ThrowCppError_nid_no_patch(CppInvalidArgument); - } - throw std::runtime_error(std::string(what) + " failed with " + std::to_string(result)); -} - -static void* APS5_VABI PadEntry_nid_no_patch(void* argument) { - auto* pad = static_cast(argument); - const auto go = pad->vtable[0]; - return reinterpret_cast(static_cast(go(pad))); -} - -extern "C" { int APS5_VABI sceAmprCommandBufferWriteAddressOnCompletion(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint64_t value) { if (!ValidWriteAddress(address)) return SCE_KERNEL_ERROR_EINVAL; @@ -901,59 +844,6 @@ int APS5_VABI sceAmprAmmWaitCommandBufferCompletion(std::uint32_t id) { return AmmSubmitted_nid_no_patch(id) ? 0 : SCE_KERNEL_ERROR_ESRCH; } -[[noreturn]] void APS5_VABI _ZSt16_Throw_Cpp_errori_nid_postfix(int code) { - ThrowCppError_nid_no_patch(code); -} -int APS5_VABI _Thrd_join_nid_postfix(Pthread thread, int* code) { - void* result = nullptr; - if (pthread_join_nid_postfix(thread, &result) != 0) return ThrdError; - if (code) *code = static_cast(reinterpret_cast(result)); - return ThrdSuccess; -} - -void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad) { - if (!pad) APS5_INVALID_ARG_EX; - RequireThrd(_Mtx_lock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_lock"); - pad->started = true; - RequireThrd(_Cnd_signal_nid_postfix(&pad->condition), "_Pad::_Release: _Cnd_signal"); - RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_unlock"); -} - -void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(GuestPad* pad) { - if (!pad) APS5_INVALID_ARG_EX; - RequireThrd(_Cnd_init_nid_postfix(&pad->condition), "_Pad::_Pad: _Cnd_init"); - const int created = _Mtx_init_nid_postfix(&pad->mutex, MtxPlain); - if (created != ThrdSuccess) { - _Cnd_destroy_nid_postfix(&pad->condition); - RequireThrd(created, "_Pad::_Pad: _Mtx_init"); - } - pad->started = false; - const int locked = _Mtx_lock_nid_postfix(&pad->mutex); - if (locked != ThrdSuccess) { - _Mtx_destroy_nid_postfix(&pad->mutex); - _Cnd_destroy_nid_postfix(&pad->condition); - RequireThrd(locked, "_Pad::_Pad: _Mtx_lock"); - } -} - -void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad) { - if (!pad) APS5_INVALID_ARG_EX; - RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::~_Pad: _Mtx_unlock"); - _Mtx_destroy_nid_postfix(&pad->mutex); - _Cnd_destroy_nid_postfix(&pad->condition); -} - -int APS5_VABI _Thrd_id_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, Pthread* thread) { - if (!pad || !thread) APS5_INVALID_ARG_EX; - const int created = pthread_create_nid_postfix(thread, nullptr, PadEntry_nid_no_patch, pad); - if (created != 0) ThrowCppError_nid_no_patch(CppResourceUnavailableTryAgain); - while (!pad->started) RequireThrd(_Cnd_wait_nid_postfix(&pad->condition, &pad->mutex), "_Pad::_Launch: _Cnd_wait"); -} int APS5_VABI _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 1a1d07d4da..f1f48ad8fa 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -304,6 +304,32 @@ int APS5_VABI _Cnd_signal_nid_postfix(void** handle) { return SignalCondition_nid_no_patch(handle); } +struct GuestPad { + unsigned (APS5_VABI* const* vtable)(GuestPad*); + void* condition; + void* mutex; + bool started; +}; + +static_assert(offsetof(GuestPad, condition) == 0x8 && offsetof(GuestPad, mutex) == 0x10 && offsetof(GuestPad, started) == 0x18 && sizeof(GuestPad) == 0x20); + +[[noreturn]] void ThrowCppError_nid_no_patch(int code); + +static void RequireThrd(int result, const char* what) { + switch (result) { + case threadSuccess: + return; + case threadNomem: + case threadTimedout: + ThrowCppError_nid_no_patch(6); + case threadBusy: + ThrowCppError_nid_no_patch(0); + case threadError: + ThrowCppError_nid_no_patch(1); + } + throw std::runtime_error(std::string(what) + " failed with " + std::to_string(result)); +} + static std::atomic registeredPthreadCreate{nullptr}; static std::atomic registeredPthreadJoin{nullptr}; @@ -315,4 +341,70 @@ void RegisterPthreadJoin_nid_no_patch(PthreadJoinFn_nid_no_patch fn) { registeredPthreadJoin.store(fn, std::memory_order_release); } +static void* APS5_VABI PadEntry_nid_no_patch(void* argument) { + auto* pad = static_cast(argument); + const auto go = pad->vtable[0]; + return reinterpret_cast(static_cast(go(pad))); } + +void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(GuestPad* pad) { + if (!pad) APS5_INVALID_ARG_EX; + RequireThrd(_Cnd_init_nid_postfix(&pad->condition), "_Pad::_Pad: _Cnd_init"); + const int created = _Mtx_init_nid_postfix(&pad->mutex, 1); + if (created != threadSuccess) { + _Cnd_destroy_nid_postfix(&pad->condition); + RequireThrd(created, "_Pad::_Pad: _Mtx_init"); + } + pad->started = false; + const int locked = _Mtx_lock_nid_postfix(&pad->mutex); + if (locked != threadSuccess) { + _Mtx_destroy_nid_postfix(&pad->mutex); + _Cnd_destroy_nid_postfix(&pad->condition); + RequireThrd(locked, "_Pad::_Pad: _Mtx_lock"); + } +} + +void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad) { + if (!pad) APS5_INVALID_ARG_EX; + RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::~_Pad: _Mtx_unlock"); + _Mtx_destroy_nid_postfix(&pad->mutex); + _Cnd_destroy_nid_postfix(&pad->condition); +} + +void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad) { + if (!pad) APS5_INVALID_ARG_EX; + RequireThrd(_Mtx_lock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_lock"); + pad->started = true; + RequireThrd(_Cnd_signal_nid_postfix(&pad->condition), "_Pad::_Release: _Cnd_signal"); + RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_unlock"); +} + +void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, void** thread) { + if (!pad || !thread) APS5_INVALID_ARG_EX; + const auto create = registeredPthreadCreate.load(std::memory_order_acquire); + if (!create) throw std::runtime_error("_Pad::_Launch: pthread_create is not registered"); + const int created = create(thread, nullptr, PadEntry_nid_no_patch, pad); + if (created != 0) ThrowCppError_nid_no_patch(6); + while (!pad->started) RequireThrd(_Cnd_wait_nid_postfix(&pad->condition, &pad->mutex), "_Pad::_Launch: _Cnd_wait"); +} + +int APS5_VABI _Thrd_join_nid_postfix(void* thread, int* code) { + const auto join = registeredPthreadJoin.load(std::memory_order_acquire); + if (!join) throw std::runtime_error("_Thrd_join: pthread_join is not registered"); + void* result = nullptr; + if (join(thread, &result) != 0) return threadError; + if (code) *code = static_cast(reinterpret_cast(result)); + return threadSuccess; +} + +int APS5_VABI _Thrd_id_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +[[noreturn]] void APS5_VABI _ZSt16_Throw_Cpp_errori_nid_postfix(int code) { + ThrowCppError_nid_no_patch(code); +} + +} + From 3845df2f1eb77c9d3d1e16050806fa6ea2870599 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:22:07 +0300 Subject: [PATCH 092/219] test(libc): cover Dinkumware _Pad and _Thrd_join in guest_libc_threads --- core/libs/tests/GuestLibcThreads.cpp | 87 ++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/core/libs/tests/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp index 0aabfe21ce..1312aed59f 100644 --- a/core/libs/tests/GuestLibcThreads.cpp +++ b/core/libs/tests/GuestLibcThreads.cpp @@ -17,6 +17,21 @@ void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); int APS5_VABI _Cnd_wait_nid_postfix(void** condition, void** mutex); int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle); int APS5_VABI _Cnd_signal_nid_postfix(void** handle); + +struct GuestPad { + unsigned (APS5_VABI* const* vtable)(GuestPad*); + void* condition; + void* mutex; + bool started; +}; + +void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(GuestPad* pad); +void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad); +void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad); +void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, void** thread); +int APS5_VABI _Thrd_join_nid_postfix(void* thread, int* code); +void RegisterPthreadCreate_nid_no_patch(int (APS5_VABI* fn)(void**, const void*, void* (APS5_VABI*)(void*), void*)); +void RegisterPthreadJoin_nid_no_patch(int (APS5_VABI* fn)(void*, void**)); } static void Require(bool value) { if (!value) std::abort(); } @@ -183,10 +198,82 @@ static void ConditionSignal() { _Cnd_destroy_nid_postfix(&condition); } +struct TestPad { + unsigned (APS5_VABI* const* vtable)(GuestPad*); + void* condition; + void* mutex; + bool started; + std::atomic ran{false}; +}; + +struct HostPadThread { + std::thread worker; + void* result = nullptr; +}; + +static int APS5_VABI HostPthreadCreate(void** thread, const void*, void* (APS5_VABI* entry)(void*), void* arg) { + auto* handle = new HostPadThread{}; + handle->worker = std::thread([handle, entry, arg] { handle->result = entry(arg); }); + *thread = handle; + return 0; +} + +static int APS5_VABI HostPthreadJoin(void* thread, void** value) { + if (!thread) return 22; + auto* handle = static_cast(thread); + handle->worker.join(); + if (value) *value = handle->result; + delete handle; + return 0; +} + +static unsigned APS5_VABI TestPadGo(GuestPad* base) { + auto* pad = reinterpret_cast(base); + pad->ran.store(true); + _ZNSt4_Pad8_ReleaseEv_nid_postfix(base); + return 42; +} + +static void PadLaunchAndJoin() { + unsigned (APS5_VABI* vtable[1])(GuestPad*) = {TestPadGo}; + TestPad unregPad{}; + unregPad.vtable = vtable; + _ZNSt4_PadC2Ev_nid_postfix(reinterpret_cast(&unregPad)); + RegisterPthreadCreate_nid_no_patch(nullptr); + RegisterPthreadJoin_nid_no_patch(nullptr); + void* unregThread = nullptr; + RequireThrows([&] { + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(reinterpret_cast(&unregPad), &unregThread); + }); + RequireThrows([&] { + int code = 0; + _Thrd_join_nid_postfix(&unregPad, &code); + }); + _ZNSt4_PadD2Ev_nid_postfix(reinterpret_cast(&unregPad)); + + RegisterPthreadCreate_nid_no_patch(HostPthreadCreate); + RegisterPthreadJoin_nid_no_patch(HostPthreadJoin); + + TestPad pad{}; + pad.vtable = vtable; + _ZNSt4_PadC2Ev_nid_postfix(reinterpret_cast(&pad)); + void* thread = nullptr; + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(reinterpret_cast(&pad), &thread); + Require(thread != nullptr && pad.started && pad.ran.load()); + _ZNSt4_PadD2Ev_nid_postfix(reinterpret_cast(&pad)); + int code = 0; + Require(_Thrd_join_nid_postfix(thread, &code) == success && code == 42); + Require(_Thrd_join_nid_postfix(nullptr, &code) == 4); + + RegisterPthreadCreate_nid_no_patch(nullptr); + RegisterPthreadJoin_nid_no_patch(nullptr); +} + int main() { MutexBasics(); RecursiveMutex(); MutualExclusion(); ConditionBroadcast(); ConditionSignal(); + PadLaunchAndJoin(); } From 387be86004f8ce3e84dab9420613f04dfe7ca249 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:22:13 +0300 Subject: [PATCH 093/219] docs(debt): move Dinkumware thread debt entries from libSceAmpr to libc --- docs/dev/debt/libSceAmpr.md | 5 ++--- docs/dev/debt/libc.md | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index a9c31f321e..82fe396a6b 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -12,6 +12,5 @@ ### Functional - [sceAmprAmmGetUsageStatsData](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) fills the caller-sized 24-byte statistics structure with 512 page-table pool entries, no allocated entries, a zero high watermark and ring idle flags 0x7, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libAmpr.cpp); they are not measured on a console, and the real call is a pass-through to `sceKernelMapperGetUsageStatsData` per [drakmor/ampr_emu](https://github.com/drakmor/ampr_emu/blob/main/src/ampr_emu_amm.cpp), which this port has no kernel-side counterpart for. -- [Dinkumware thread exports](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning. -- [_Cnd_signal, _Cnd_init_with_name](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - take a pointer to the handle slot like the libc `_Cnd_*` exports from [#3079](https://github.com/boykopovar/AnyPS5/pull/3079) and act on the same conditions. `_Cnd_signal` wakes one waiter and returns `_Thrd_success` (0), as `_Cnd_broadcast` does; whether the console maps a failing internal signal to `_Thrd_error` is not known. The signature `_Cnd_init_with_name(_Cnd_t*, const char* name)` is inferred from [fpPS4 `ps4_mtx_internal.pas`](https://github.com/red-prig/fpPS4/blob/trunk/src/libcinternal/ps4_mtx_internal.pas), where the PS4 `_Mtx_init` is `_Mtx_init_with_name` with the name `SceLibcI_Mtx` and the name only reaches the pthread object; here it is `_Cnd_init` and the name, which may be null, is ignored. Neither is confirmed on a PS5 `libc.prx`. `_Thrd_id` still throws: microsoft/STL returns an `unsigned int` thread id, but whether the PS5 returns the `pthread_t` or a kernel thread id, and at what width, is not known -- [std::_Pad](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follows Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` of Visual C++ 2013 ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. The pad is taken to be that header's class with the console's pointer handles: a vtable pointer whose first slot is the pure virtual `_Go()`, then `_Cnd_t` at 0x8, `_Mtx_t` at 0x10 and `bool _Started` at 0x18, 32 bytes, the derived `_LaunchPad` target following it; the console's `_Pad(const char*)` and `_Launch(const char*, pthread**)`/`_Launch(pthread_attr**, pthread**)` overloads suggest named primitives and could mean another layout or vtable. `_Launch` starts the thread with `pthread_create` and default attributes (the console's `_Thrd_start` name, stack size and priority are unknown), its entry calls `_Go()` and returns the result as the exit code, and it does not run `_Cnd_do_broadcast_at_thread_exit`, which is absent here. A failing primitive throws the guest `std::system_error` that `_Throw_C_error` picks for its code, and a failing thread start the `resource_unavailable_try_again` one of `_Thrd_startX`; any `pthread_create` error counts, since the console's `_Thrd_start` result is unknown. A null pad or thread slot throws +- [Dinkumware thread exports](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. The spin lock (byte exchange loop, no yield) matches that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning. +- [_Cnd_init_with_name](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - takes a pointer to the handle slot like the libc `_Cnd_*` exports from [#3079](https://github.com/boykopovar/AnyPS5/pull/3079) and acts on the same conditions. The signature `_Cnd_init_with_name(_Cnd_t*, const char* name)` is inferred from [fpPS4 `ps4_mtx_internal.pas`](https://github.com/red-prig/fpPS4/blob/trunk/src/libcinternal/ps4_mtx_internal.pas), where the PS4 `_Mtx_init` is `_Mtx_init_with_name` with the name `SceLibcI_Mtx` and the name only reaches the pthread object; here it is `_Cnd_init` and the name, which may be null, is ignored. This is not confirmed on a PS5 `libc.prx`. diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ebf631971a..93c9d917a0 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -113,9 +113,10 @@ - [daemon](../../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981)) - [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The lock is a host [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) lock on one byte at offset 2^63 - 2, which no file reaches, so it excludes other `flock` calls and leaves reads, writes and mapped views alone, like FreeBSD's advisory locks. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. -- [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, selected by the first matching errno entry in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects::_Generic_object`, and the new `generic_category()` export returns that object; a title's inline category object in a bundled libc can still have a different identity. Its `message` and direct message export construct the existing [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) model: a reserved eight-byte word, 16-byte inline buffer, length and capacity, with guest `operator new` storage above 15 bytes. This 40-byte string layout and virtual return ABI have not been verified against a console library. The deleting destructor still throws. `_Syserror_map` covers the 72 distinct Dinkumware `errc` values present in FreeBSD 11.4 [errno.h](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h), returning null for other values; category messages use `unknown error` for those. First-match aliases are `EAGAIN`/`EWOULDBLOCK` -> `operation would block` and `ENOTSUP`/`EOPNOTSUPP` -> `not supported`. STREAMS-only errno assignments absent from that header (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are not inferred +- [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) follows Dinkumware `thread0.cpp` ([VC12](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/thread0.cpp), [STL](https://github.com/microsoft/STL/blob/da52dc0f/stl/src/thread0.cpp)), without console verification. Codes outside 0..6 throw `std::out_of_range`, the deleting destructor throws, and STREAMS errnos absent from FreeBSD 11.4 [`errno.h`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h) (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are omitted. Codes 0..6 throw a 32-byte [VC12 `system_error`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/system_error) with FreeBSD errnos (16, 22, 3, 12, 1, 11, 35), a static `generic_category` (not `_Generic_object`), and unverified 40-byte [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) messages from [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/syserror.cpp) - [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested - [_Dtest](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows Dinkumware double classification matching _FDtest: finite values return -1, subnormals -2, zeros 0, infinities 1, and NaNs 2. - [app0.zar](../../../core/libs/prx/libc/src/AppArchive.cpp) (libc) - a [ZArchive](https://github.com/Exzap/ZArchive) beside `app0/` is a read-only layer under it: a path missing from `app0/` is looked up in the archive, case-insensitively. It is not a console format; titles ship their files in a PFS image instead. Resolving an archive-only path through any call other than the archive-aware libkernel ones throws, so `fopen`, `stat`, `access`, `opendir` and the rest do not read the archive yet, and `opendir` of a directory that exists in both places, `app0/` itself included, and `fdopen` of an archived descriptor throw instead of missing its entries or reading the null device. A damaged compressed block throws instead of returning `EIO` - [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified - [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. +- [std::_Pad, _Thrd_join, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. `_Thrd_id` throws because whether the console returns `pthread_t` or a kernel thread ID and at what width is unknown; `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes (`_Thrd_start` name, stack size and priority are unknown). The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12; named overloads on the console could differ. Failing primitives or thread start throw the guest `std::system_error` from `_Throw_C_error`/`_Thrd_startX`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int` From ba1a85ebb39c471daf4818c43bb6af34b3809160 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:28:17 +0300 Subject: [PATCH 094/219] feat(libc): add Windows module path helper for backtrace resolution --- core/libs/prx/libc/src/Backtrace.cpp | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/Backtrace.cpp b/core/libs/prx/libc/src/Backtrace.cpp index 29c7dae111..59113988ab 100644 --- a/core/libs/prx/libc/src/Backtrace.cpp +++ b/core/libs/prx/libc/src/Backtrace.cpp @@ -7,10 +7,33 @@ #include "prx/libc/include/General.hpp" -#ifdef __linux__ +#if defined(__linux__) #include #include - +#elif defined(_WIN32) +#include +#include "prx/libc/include/exceptions/Unwind.hpp" +extern "C" { +_Unwind_Reason_Code APS5_VABI _Unwind_Backtrace_nid_postfix(_Unwind_Trace_Fn, void*); +_Unwind_Ptr APS5_VABI _Unwind_GetIP_nid_postfix(_Unwind_Context*); +std::string BacktraceModulePath_nid_no_patch(HMODULE handle, std::size_t initialCapacity) { + std::string module; + std::size_t capacity = initialCapacity == 0 ? 1 : initialCapacity; + while (true) { + module.resize(capacity); + SetLastError(ERROR_SUCCESS); + const DWORD length = GetModuleFileNameA(handle, module.data(), static_cast(module.size())); + if (length == 0) return {}; + if (length < capacity && GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + module.resize(length); + return module; + } + if (capacity >= 32768) return {}; + capacity *= 2; + } +} +} +#endif namespace { constexpr int MaxFrames = 256; From 6fb1b56526e0326fb0432cbcdc4afc5132de92ae Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:28:24 +0300 Subject: [PATCH 095/219] feat(libc): implement sceLibcBacktraceSelf and GetBufferSize on Windows --- core/libs/prx/libc/src/Backtrace.cpp | 38 +++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Backtrace.cpp b/core/libs/prx/libc/src/Backtrace.cpp index 59113988ab..53662a77d0 100644 --- a/core/libs/prx/libc/src/Backtrace.cpp +++ b/core/libs/prx/libc/src/Backtrace.cpp @@ -34,6 +34,9 @@ std::string BacktraceModulePath_nid_no_patch(HMODULE handle, std::size_t initial } } #endif + +#if defined(__linux__) || defined(_WIN32) + namespace { constexpr int MaxFrames = 256; @@ -54,15 +57,48 @@ static_assert(sizeof(FrameRecord) == NameOffset); [[gnu::noinline]] std::vector CaptureFrames(int limit) { void* addresses[MaxFrames]; +#if defined(__linux__) const int captured = backtrace(addresses, MaxFrames); +#elif defined(_WIN32) + struct TraceState { + void** addrs; + int max; + int count; + }; + static const auto traceCallback = [](_Unwind_Context* ctx, void* arg) APS5_VABI -> _Unwind_Reason_Code { + auto* s = static_cast(arg); + if (s->count < s->max) { + s->addrs[s->count++] = reinterpret_cast(_Unwind_GetIP_nid_postfix(ctx)); + } + return (s->count >= s->max) ? _URC_NORMAL_STOP : _URC_NO_REASON; + }; + TraceState state{addresses, MaxFrames, 0}; + _Unwind_Backtrace_nid_postfix(traceCallback, &state); + int captured = state.count; + if (captured == 0) { + captured = static_cast(RtlCaptureStackBackTrace(0, MaxFrames, addresses, nullptr)); + } +#endif std::vector frames; for (int index = 2; index < captured && (limit < 0 || static_cast(frames.size()) < limit); ++index) { +#if defined(__linux__) Dl_info info{}; if (dladdr(addresses[index], &info) == 0 || info.dli_fbase == nullptr || info.dli_fname == nullptr) continue; const auto offset = reinterpret_cast(addresses[index]) - reinterpret_cast(info.dli_fbase); if (offset > UINT32_MAX) continue; std::string module = info.dli_fname; - if (const auto slash = module.find_last_of('/'); slash != std::string::npos) module.erase(0, slash + 1); +#elif defined(_WIN32) + HMODULE handle = nullptr; + if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + static_cast(addresses[index]), &handle) || handle == nullptr) { + continue; + } + const auto offset = reinterpret_cast(addresses[index]) - reinterpret_cast(handle); + if (offset > UINT32_MAX) continue; + std::string module = BacktraceModulePath_nid_no_patch(handle, MAX_PATH); + if (module.empty()) continue; +#endif + if (const auto slash = module.find_last_of("/\\"); slash != std::string::npos) module.erase(0, slash + 1); if (module.size() > sizeof(GuestSuffix) - 1 && module.compare(module.size() - (sizeof(GuestSuffix) - 1), std::string::npos, GuestSuffix) == 0) module.resize(module.size() - (sizeof(GuestSuffix) - 1)); frames.push_back({static_cast(offset), std::move(module)}); From 1819d3965e68a2d81259ff9d9552d3f9e03a78a2 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:28:30 +0300 Subject: [PATCH 096/219] test(libc): enable guest_backtrace on Windows and cover path buffer growth --- core/libs/prx/libc/CMakeLists.txt | 9 +++++++++ core/libs/tests/GuestBacktrace.cpp | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index db26a5c925..756e9c4f5e 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -88,6 +88,8 @@ target_sources(libc PRIVATE target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/BinaryWordIo.cpp) + + target_include_directories(libc PRIVATE ${LIBS_INCLUDE_DIR} ) @@ -805,11 +807,18 @@ if(BUILD_TESTING) add_test_executable(cpu_topology_tests ${LIBS_INCLUDE_DIR}/tests/CpuTopology.cpp) target_include_directories(cpu_topology_tests PRIVATE ${LIBS_INCLUDE_DIR}) add_test(NAME cpu_topology COMMAND cpu_topology_tests) + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" OR WIN32) add_test_executable(guest_backtrace_tests ${LIBS_INCLUDE_DIR}/tests/GuestBacktrace.cpp) target_include_directories(guest_backtrace_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_backtrace_tests PRIVATE libc) + configure_windows_unwind(guest_backtrace_tests) set_target_properties(guest_backtrace_tests PROPERTIES ENABLE_EXPORTS ON) add_test(NAME guest_backtrace COMMAND guest_backtrace_tests) + if(WIN32) + set_tests_properties(guest_backtrace PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() add_test_executable(log_macros_tests ${LIBS_INCLUDE_DIR}/tests/LogMacros.cpp) target_include_directories(log_macros_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/tests/GuestBacktrace.cpp b/core/libs/tests/GuestBacktrace.cpp index 25e958cdb9..e2a29db117 100644 --- a/core/libs/tests/GuestBacktrace.cpp +++ b/core/libs/tests/GuestBacktrace.cpp @@ -4,13 +4,20 @@ #include #include #include +#if defined(_WIN32) +#include +#else #include +#endif #include #include extern "C" { int APS5_VABI sceLibcBacktraceGetBufferSize_nid_postfix(int, int*, std::size_t*); int APS5_VABI sceLibcBacktraceSelf_nid_postfix(int, void*, std::size_t, int*); +#if defined(_WIN32) +std::string BacktraceModulePath_nid_no_patch(HMODULE, std::size_t); +#endif } namespace { @@ -66,10 +73,21 @@ std::size_t Used(const std::vector& records) { } [[gnu::noinline]] void CheckBacktrace() { +#if defined(_WIN32) + HMODULE handle = nullptr; + Require(GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&CheckBacktrace), &handle) != 0 && handle != nullptr, + "GetModuleHandleExA of the test failed"); + std::string executable = BacktraceModulePath_nid_no_patch(handle, 1); + const auto slash = executable.find_last_of("/\\"); + Require(!executable.empty() && slash != std::string::npos, "BacktraceModulePath of the test failed"); + executable.erase(0, slash + 1); +#else Dl_info self{}; Require(dladdr(reinterpret_cast(&CheckBacktrace), &self) != 0 && self.dli_fname != nullptr && self.dli_saddr != nullptr, "dladdr of the test failed"); std::string executable = self.dli_fname; executable.erase(0, executable.find_last_of('/') + 1); +#endif int count = -1; std::size_t size = 0; @@ -85,9 +103,17 @@ std::size_t Used(const std::vector& records) { for (std::size_t index = size; index < buffer.size(); ++index) Require(buffer[index] == Fill, "Self wrote past the buffer size"); for (const auto& record : records) Require(record.reserved == 0 && !record.module.empty(), "record has a nonzero reserved word or no module"); Require(records.front().module == executable, "first frame is not in the calling module"); +#if defined(_WIN32) + HMODULE callerHandle = nullptr; + const auto address = reinterpret_cast(handle) + records.front().offset; + Require(GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(address), &callerHandle) != 0 && callerHandle == handle, + "first frame is not the caller"); +#else Dl_info caller{}; const auto address = reinterpret_cast(self.dli_fbase) + records.front().offset; Require(dladdr(reinterpret_cast(address), &caller) != 0 && caller.dli_saddr == self.dli_saddr, "first frame is not the caller"); +#endif for (const int limit : {0, 1, 2}) { Require(sceLibcBacktraceGetBufferSize_nid_postfix(limit, &count, &size) == 0 && count == limit, "GetBufferSize ignored the frame limit"); From 47591f2e92c3b1803b0e961b1be4ba638f11a954 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:28:37 +0300 Subject: [PATCH 097/219] docs(debt): update sceLibcBacktrace technical debt entry for Windows --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ebf631971a..060e290209 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. Implemented only on Linux; other platforms throw +- [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. Implemented only on Linux and Windows; other platforms throw - [ApplicationGuestHeapApi](../../../core/libs/prx/libc/src/ApplicationProcessParameters.cpp) (libc) - on Linux, HLE application allocation uses a loaded guest SDK's ten allocator exports when they belong to one native module. Its CRT owns application allocator initialization and finalization; HLE retains the module until process exit. Partial or mixed providers throw. This avoids reinitializing an allocator already used by SDK startup; console behavior and other host platforms remain unverified. - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is unverified. - [scriptingGetMem, scriptingFreeMem](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - runtime allocator hooks for Unity titles. The allocation signature `(alignment, size)`, minimum alignment 16 and null result for a non-power-of-two alignment follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libKernel.cpp); the free hook's one-pointer signature follows [Raeen's recorded contract](https://github.com/Whoisraeen/Raeen/blob/2db56923204b89719916b95e29c2f3c4e1d9f90b/docs/unity-dlsym-blocker.md). Both use libc's own guest heap, not the title's registered allocator. A title that ships its own `libc.prx` (Metal Gear Solid Master Collection, PPSA18194) has already initialized that allocator, and reaching it from here would run its initializer a second time. That title's IL2CPP runtime looks up only `scriptingGetMem`, by name from `Il2cppUserAssemblies.prx`, and asks it for 256 KiB blocks aligned to 4 KiB. Which console library exports the hooks is unknown, and there is no console measurement. Allocation failure and sizes that overflow return null. From b28198008c64d580a07a05062d75f3e6ffbc162b Mon Sep 17 00:00:00 2001 From: babyhuey <7751607+babyhuey@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:52:47 -0400 Subject: [PATCH 098/219] feat(libkernel): implement sceKernelOpenEventFlag Return the handle of an event flag this process created under the given name, or ENOENT. Twispero opens the shell's SceShellCoreUtilAppFocus flag at start-up and treats a failed open as unknown focus. --- .../libs/prx/libkernel/EventFlag/src/EventFlag.cpp | 13 +++++++++++++ core/libs/tests/GuestKernelErrors.cpp | 14 ++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 28 insertions(+) diff --git a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp index c2d2dab315..a6403ce990 100644 --- a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp +++ b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp @@ -18,6 +18,7 @@ static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; +static constexpr int SCE_KERNEL_ERROR_ENOENT = 0x80020002; static constexpr int SCE_KERNEL_ERROR_ESRCH = 0x80020003; static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; static constexpr int SCE_KERNEL_ERROR_EACCES = 0x8002000D; @@ -146,6 +147,18 @@ int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, ui return SCE_OK; } +int APS5_VABI sceKernelOpenEventFlag(KernelEventFlag* ef, const char* name) { + if (!ef || !name) return SCE_KERNEL_ERROR_EINVAL; + std::shared_lock lock(eventFlagRegistryMutex); + for (const auto& [handle, flag] : eventFlagRegistry) { + std::lock_guard flagLock(flag->_mutex); + if (flag->_deleted || flag->_name != name) continue; + *ef = handle; + return SCE_OK; + } + return SCE_KERNEL_ERROR_ENOENT; +} + int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef) { if (!ef) return SCE_KERNEL_ERROR_ESRCH; const auto keepAlive = ResolveEventFlag(ef); diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index b1981c7ee0..1932625356 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -34,6 +34,7 @@ int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, std::uint32_t attr, std::uint64_t init_pattern, const void* param); int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef); +int APS5_VABI sceKernelOpenEventFlag(KernelEventFlag* ef, const char* name); int APS5_VABI sceKernelSetEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); int APS5_VABI sceKernelClearEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, std::uint64_t set_pattern, int* num_wait_threads); @@ -204,6 +205,19 @@ int main() { Require(sceKernelWaitEventFlag(nullptr, 1, EVF_WAITMODE_OR, nullptr, nullptr) == SCE_KERNEL_ERROR_ESRCH); Require(sceKernelCancelEventFlag(nullptr, 0, nullptr) == SCE_KERNEL_ERROR_ESRCH); + KernelEventFlag opened = nullptr; + Require(sceKernelOpenEventFlag(nullptr, "test") == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelOpenEventFlag(&opened, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelOpenEventFlag(&opened, "SceShellCoreUtilAppFocus") == SCE_KERNEL_ERROR_ENOENT && opened == nullptr); + Require(sceKernelCreateEventFlag(&ef, "test_open", EVF_ATTR_MULTI, 0x05, nullptr) == SCE_OK); + Require(sceKernelOpenEventFlag(&opened, "test_open") == SCE_OK && opened == ef); + Require(sceKernelSetEventFlag(opened, 0x08) == SCE_OK); + std::uint64_t openedPattern = 0; + Require(sceKernelPollEventFlag(ef, 0x08, EVF_WAITMODE_OR, &openedPattern) == SCE_OK && openedPattern == 0x0d); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + opened = nullptr; + Require(sceKernelOpenEventFlag(&opened, "test_open") == SCE_KERNEL_ERROR_ENOENT && opened == nullptr); + Require(sceKernelCreateEventFlag(&ef, "flag", EVF_ATTR_MULTI, 0x15, nullptr) == SCE_OK); std::uint64_t pattern = 0; Require(sceKernelPollEventFlag(ef, 0, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EINVAL); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index e6bdd3c6fc..192dece673 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -58,6 +58,7 @@ - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console - [sceKernelSetEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - multi-waiter satisfaction follows fpPS4 in-order evaluation: registered waiters are evaluated in FIFO order and each satisfied waiter's clear mode is applied immediately to the pattern before evaluating subsequent waiters; this ordering is unverified on PS5 console hardware +- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - returns the handle of an undeleted event flag this process created with the same name, and `SCE_KERNEL_ERROR_ENOENT` otherwise. Flags owned by other processes, such as the shell's `SceShellCoreUtilAppFocus`, do not exist here, and which attribute makes a flag openable on the console is not modelled. The error code for a missing name and the choice between flags sharing a name are unverified. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked - [sceKernelGetAvailableCpumask](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed From 4aadd1965957f9f415e7a7cf5706055883977509 Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Tue, 20 Oct 2026 04:07:56 +0300 Subject: [PATCH 099/219] fix(relinker): accept a relative jump table padded to 16 bytes before more code FindTrailingRelativeJumpTable accepted a jump table after a function's code only when padding followed it up to the end of the inspected range. A PS4 function's table can end past its FDE, be padded to the next 16-byte boundary and be followed by more code before the next FDE. The table is now accepted when padding reaches that boundary; it then ends there, so the code after it is still analysed as code. Co-Authored-By: Claude Opus 5.5 --- .../relinker/analysis/RelativeJumpTables.hpp | 2 +- .../src/analysis/RelativeJumpTables.cpp | 21 ++++++++++++------- .../relinker/tests/ControlFlowGraphTests.cpp | 10 +++++++++ 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp b/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp index 3e8bc10152..8a4caf08d0 100644 --- a/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp +++ b/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp @@ -16,7 +16,7 @@ struct RelativeJumpTable { VirtualAddress End; }; -std::size_t FindTrailingRelativeJumpTable(std::span code); +std::size_t FindTrailingRelativeJumpTable(std::span code, VirtualAddress codeAddress = 0, std::size_t* tableEnd = nullptr); std::vector FindRelativeJumpTables(std::span segment, VirtualAddress segmentVAddr, const std::vector>& functions); } diff --git a/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp b/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp index 0ccbc28717..ebe272bdad 100644 --- a/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp +++ b/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp @@ -24,15 +24,15 @@ bool IsNop(std::span code, std::size_t at, std::size_t lengt return code[position] == 0x0f && position + 1 < end && code[position + 1] == 0x1f; } -bool OnlyPadding(std::span code, std::size_t from, const Codegen::X64InstructionDecoder& decoder) { - for (auto position = from; position < code.size();) { +bool OnlyPadding(std::span code, std::size_t from, std::size_t to, const Codegen::X64InstructionDecoder& decoder) { + for (auto position = from; position < to;) { if (code[position] == 0x00 || code[position] == 0xcc || code[position] == 0x90) { ++position; continue; } try { - const auto length = decoder.DecodeInstruction(code.data() + position, code.size() - position).Length; - if (length == 0 || length > code.size() - position || !IsNop(code, position, length)) return false; + const auto length = decoder.DecodeInstruction(code.data() + position, to - position).Length; + if (length == 0 || length > to - position || !IsNop(code, position, length)) return false; position += length; } catch (const Codegen::CodegenException&) { return false; @@ -69,7 +69,7 @@ std::size_t TablesEnd(std::span code, std::size_t firstTable } -std::size_t FindTrailingRelativeJumpTable(std::span code) { +std::size_t FindTrailingRelativeJumpTable(std::span code, VirtualAddress codeAddress, std::size_t* tableEnd) { const Codegen::X64InstructionDecoder decoder; std::vector loads; std::vector instructionStarts(code.size() + 1, false); @@ -82,7 +82,11 @@ std::size_t FindTrailingRelativeJumpTable(std::span code) { for (const auto& load : loads) { if (load.Target != position || load.At >= lastIndirectJump) continue; const auto end = TablesEnd(code, position, loads, instructionStarts); - if (end != 0 && OnlyPadding(code, end, decoder)) return position; + if (end == 0) continue; + const auto aligned = std::min(code.size(), static_cast(((codeAddress + end + 15) & ~VirtualAddress{15}) - codeAddress)); + if (!OnlyPadding(code, end, aligned, decoder)) continue; + if (tableEnd) *tableEnd = OnlyPadding(code, aligned, code.size(), decoder) ? code.size() : aligned; + return position; } } Codegen::DecodedInstructionInfo info; @@ -122,8 +126,9 @@ std::vector FindRelativeJumpTables(std::span inspectEnd && functions[index + 1].first - inspectEnd <= MaxTableGapAfterFunction) inspectEnd = std::min(functions[index + 1].first, segmentEnd); const auto code = segment.subspan(begin - segmentVAddr, inspectEnd - begin); - if (const auto tail = FindTrailingRelativeJumpTable(code); tail != 0 && tail < code.size()) - tables.push_back({begin, begin + tail, inspectEnd}); + std::size_t tableEnd = code.size(); + if (const auto tail = FindTrailingRelativeJumpTable(code, begin, &tableEnd); tail != 0 && tail < code.size()) + tables.push_back({begin, begin + tail, begin + tableEnd}); } return tables; } diff --git a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp index 718e5ddb07..a7cfe4f88d 100644 --- a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp +++ b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp @@ -249,6 +249,16 @@ void CheckRelativeJumpTables() { nonPadding.back() = 0x55; Require(Relinker::FindTrailingRelativeJumpTable(nonPadding) == nonPadding.size(), "Table followed by non-padding bytes was accepted"); + Bytes codeAfter = text; + codeAfter.resize(48, 0xcc); + codeAfter[32] = 0x55; + codeAfter[33] = 0x5d; + codeAfter[34] = 0xc3; + std::size_t tableEnd = 0; + Require(Relinker::FindTrailingRelativeJumpTable(codeAfter, TextAddress, &tableEnd) == tableOffset && tableEnd == 32, "Table padded to 16 bytes before more code was not found"); + const std::vector> codeAfterFunctions{{TextAddress, TextAddress + 9}, {TextAddress + codeAfter.size(), TextAddress + codeAfter.size() + 8}}; + const auto codeAfterTables = Relinker::FindRelativeJumpTables(codeAfter, TextAddress, codeAfterFunctions); + Require(codeAfterTables.size() == 1 && codeAfterTables[0].Begin == TextAddress + tableOffset && codeAfterTables[0].End == TextAddress + 32, "Table before more code did not end at its padding"); } } From 1b33f14c8c9b0042c1e341b265d1f4dee4531400 Mon Sep 17 00:00:00 2001 From: Luis Gerardo Aguilar <100291529+LuisAguilarDev@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:45:45 -0500 Subject: [PATCH 100/219] feat(netctl): add sceNetCtlGetInfoV6 argument and connection checks Titles import sceNetCtlGetInfoV6, and a missing import stops them at load. The checks that need no answer layout are implemented; every code throws when the host has IPv6, since the layout of the answer is not verified. --- core/libs/prx/libSceNetCtl/Export.cpp | 14 +++++++++----- core/libs/tests/GuestNetCtl.cpp | 7 +++++++ docs/dev/debt/libSceNetCtl.md | 2 +- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index dbc3d4c09a..fa0b264627 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -64,6 +64,8 @@ static constexpr int NET_CTL_INFO_SECONDARY_DNS = 18; static constexpr int NET_CTL_INFO_HTTP_PROXY_CONFIG = 19; static constexpr int NET_CTL_INFO_HTTP_PROXY_SERVER = 20; static constexpr int NET_CTL_INFO_HTTP_PROXY_PORT = 21; +static constexpr int NET_CTL_INFO_V6_IP_ADDRESS = 1; +static constexpr int NET_CTL_INFO_V6_SECONDARY_DNS = 5; static constexpr std::uint32_t NET_CTL_DEVICE_WIRED = 0; static constexpr std::uint32_t NET_CTL_LINK_CONNECTED = 1; static constexpr std::uint32_t NET_CTL_IP_DHCP = 0; @@ -456,11 +458,6 @@ int APS5_VABI sceNetCtlGetInfo(int code, NetCtlInfo* info) { return 0; } -int APS5_VABI sceNetCtlGetInfoV6() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo* nat_info) { if (!nat_info) return SCE_NET_CTL_ERROR_INVALID_ADDR; return SCE_NET_CTL_ERROR_NOT_CONNECTED; @@ -497,6 +494,13 @@ int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback func, void* arg, int* c return RegisterCallback(g_callbacksV6, func, arg, cid); } +int APS5_VABI sceNetCtlGetInfoV6(int code, void* info) { + if (!info) return SCE_NET_CTL_ERROR_INVALID_ADDR; + if (code < NET_CTL_INFO_V6_IP_ADDRESS || code > NET_CTL_INFO_V6_SECONDARY_DNS) return SCE_NET_CTL_ERROR_INVALID_CODE; + if (!HostHasAddress(AF_INET6)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; + throw std::runtime_error("sceNetCtlGetInfoV6: info code " + std::to_string(code) + " is not implemented"); +} + void APS5_VABI sceNetCtlTerm(void) { std::lock_guard lock(g_callbackLock); for (auto& slot : g_callbacks) slot = {}; diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index 5ee4c2a871..a74159ef72 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -37,6 +37,7 @@ int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo*); int APS5_VABI sceNetCtlGetState(int*); int APS5_VABI sceNetCtlGetStateV6(int*); +int APS5_VABI sceNetCtlGetInfoV6(int, void*); int APS5_VABI sceNetCtlGetResult(int, int*); #ifdef __APPLE__ std::size_t NetCtlDefaultRoutes_nid_no_patch(const char*, std::size_t, NetCtlDefaultRoute*, std::size_t); @@ -275,6 +276,12 @@ int main() { Require(sceNetCtlGetInfo(22, &info) == kErrInvalidCode); Require(sceNetCtlGetInfo(14, nullptr) == kErrInvalidAddr); + std::uint8_t infoV6[256] = {}; + Require(sceNetCtlGetInfoV6(0, infoV6) == kErrInvalidCode); + Require(sceNetCtlGetInfoV6(6, infoV6) == kErrInvalidCode); + Require(sceNetCtlGetInfoV6(1, nullptr) == kErrInvalidAddr); + if (stateV6 != kStateIpObtained) Require(sceNetCtlGetInfoV6(1, infoV6) == kErrNotConnected); + const int result = sceNetCtlGetInfo(14, &info); if (result == kErrNotConnected) { std::puts("skipped, the host has no IPv4 address"); diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 0df9af2742..ca13becdd0 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -6,7 +6,7 @@ ### Unknown function info -- [sceNetCtlGetInfoV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `Jy1EO5GdlcM`) - imported by PPSA08587, which fails to load without it, and not called before its loading screen; declared without parameters as a throwing stub, signature unknown. Name from the aerolib NID list +- [sceNetCtlGetInfoV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `Jy1EO5GdlcM`) - imported by PPSA08587. `(int code, SceNetCtlInfoV6* info)` with codes 1-5 (IP address, link-local address, default route, primary and secondary DNS) comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero), whose source is unknown; shadPS4 and fpPS4 only stub it. A null `info` answers `INVALID_ADDR`, a code outside 1-5 `INVALID_CODE`, and a host without an IPv6 address `NOT_CONNECTED`, as `sceNetCtlGetStateV6` reports it. The layout of each answer is unverified, so every code throws on a host with an IPv6 address - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The DHCP host name (12), the PPPoE user name (13), the HTTP proxy server (20) and the Wi-Fi BSSID and SSID (5, 6) are empty strings, as that configuration sets none of them; the remaining Wi-Fi codes (7-10) and the HTTP proxy port (21) report 0; this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/network.cpp) and is not measured on hardware ### Functional From c9e38be25838d92631ec0c66965214db2ae32f34 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Fri, 16 Oct 2026 10:28:02 +0000 Subject: [PATCH 101/219] perf(shader): name a poisoned guarded SRT read in the fault buffer A ReadConst of a guarded flattened-SRT slot called record_bda_fault with the poison record's pc and address when the slot's flag was set. NVIDIA's compiler pays for every call site in a large function: Crimson Desert's 1,916,980-word compute module has 2,769 guarded reads, and its pipeline took 99 s and 5.9 GiB to create. The read now stores the record's flattened-SRT word, tagged with its stage, to a new fault-buffer word after the written-page list, and stops as before. The host reads the pc and address from that stage's flattened SRT and raises the same Unmapped fault. Poisoned and clean captures still share one variant, specialization and pipeline, and the existing words keep their offsets, so cached modules from before still report through record_bda_fault. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/BdaResources.hpp | 7 +++++ .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/src/BdaResources.cpp | 23 ++++++++++++++++ .../Graphics/src/ShaderResources.cpp | 6 +++++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 26 +++++++++++++++++++ core/shader/recompiler/BdaAbi.hpp | 4 ++- .../SpirvMemory/SpirvMemoryInstructions.cpp | 12 ++++----- 7 files changed, 72 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp index 1c8ee60301..580236b120 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp @@ -25,6 +25,11 @@ public: std::vector messages; }; void SetImageTables(std::vector tables, std::vector> ranges); + struct SrtGuardFaults { + std::uint32_t stage = 0; + std::vector flattenedSrt; + }; + void SetSrtGuards(std::vector guards); // APS5_PROFILE_DRAW: page tables served from the per-device cache and built anew (cumulative), // and how many recent tables the cache holds right now. struct TableCacheStats { @@ -52,11 +57,13 @@ private: // same device addresses share one buffer while one of them is alive (see the table cache in // BdaResources.cpp, which refers to it weakly). void markWrittenPages() const; + void checkSrtGuard() const; std::shared_ptr table; std::unique_ptr fault; std::size_t tableBytes = 0; std::vector imageTables; std::vector> imageTableRanges; + std::vector srtGuards; }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 5569cd738a..e73f5d7e02 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -540,6 +540,7 @@ private: GuestBufferMemory guestMemory; std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes(); std::unique_ptr bda; + std::vector srtGuardFaults; bool usesBda = false; bool bdaReadOnly = true; std::vector multisampleSources; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 6e2537248b..1da043f87f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -127,6 +127,7 @@ BdaResources::BdaResources(const Context& context) { Require(ShaderRecompiler::BdaAbi::FaultBufferBytes <= context.limits.maxStorageBufferRange, "BDA fault buffer exceeds storage buffer range limit"); fault = std::make_unique(context, ShaderRecompiler::BdaAbi::FaultBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); std::memset(fault->Bytes().data(), 0, ShaderRecompiler::BdaAbi::WrittenListWord * sizeof(std::uint32_t)); + std::memset(fault->Bytes().data() + ShaderRecompiler::BdaAbi::SrtGuardWord * sizeof(std::uint32_t), 0, sizeof(std::uint32_t)); } BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memory) : BdaResources(context) { @@ -245,6 +246,7 @@ void BdaResources::CheckFault() const { std::memcpy(&report, fault->Bytes().data(), sizeof(report)); if (report.state == ShaderRecompiler::BdaAbi::FaultState::Empty) { Require(static_cast(report.reason) == 0 && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.otherFaults == 0, "BDA fault record has data without publication"); + checkSrtGuard(); return; } Require(report.state == ShaderRecompiler::BdaAbi::FaultState::Ready && report.otherFaults <= 1, "incomplete or invalid BDA fault record"); @@ -309,6 +311,27 @@ void BdaResources::SetImageTables(std::vector tables, std::vec imageTableRanges = std::move(ranges); } +void BdaResources::SetSrtGuards(std::vector guards) { + srtGuards = std::move(guards); +} + +void BdaResources::checkSrtGuard() const { + namespace Abi = ShaderRecompiler::BdaAbi; + std::uint32_t guard = 0; + std::memcpy(&guard, fault->Bytes().data() + Abi::SrtGuardWord * sizeof(std::uint32_t), sizeof(guard)); + if (guard == 0) return; + const auto stage = guard >> Abi::SrtGuardStageShift; + const auto record = guard & ((1u << Abi::SrtGuardStageShift) - 1u); + const auto found = std::find_if(srtGuards.begin(), srtGuards.end(), [&](const SrtGuardFaults& candidate) { return candidate.stage == stage; }); + const auto* guards = found != srtGuards.end() ? &*found : srtGuards.size() == 1 ? &srtGuards.front() : nullptr; + Require(guards != nullptr && record + 2u < guards->flattenedSrt.size(), "a guarded SRT read faulted without its poison record"); + const auto& words = guards->flattenedSrt; + const auto address = static_cast(words[record + 1u]) | (static_cast(words[record + 2u]) << 32u); + std::ostringstream message; + message << "BDA access failed: address=0x" << std::hex << address << " instruction=0x" << words[record] << std::dec << " bytes=4 stage=" << stage << " reason=" << static_cast(Abi::FaultReason::Unmapped); + throw std::runtime_error(message.str()); +} + void BdaResources::markWrittenPages() const { namespace Abi = ShaderRecompiler::BdaAbi; auto* words = reinterpret_cast(fault->Bytes().data()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 5bafd790ea..a55e7c5f5a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1881,6 +1881,11 @@ void ShaderResources::buildPrepare(std::span shaders, cons for (const auto& shader : shaders) { Require(shader.program != nullptr, "missing compiled shader"); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.program->runtimeAbiVersion); + if (shader.program->poisonedSrtReads != 0) { + const auto& programBindings = shader.program->bindings; + const auto flat = std::find_if(programBindings.begin(), programBindings.end(), [](const ShaderRecompiler::DescriptorBinding& binding) { return binding.role == ShaderRecompiler::DescriptorRole::FlattenedSrt; }); + if (flat != programBindings.end()) srtGuardFaults.push_back({IrStage(shader.stage), flat->guestDescriptor}); + } const VkShaderStageFlags flags = VulkanStage(shader.stage); std::uint64_t stageStorageBuffers = 0; std::uint64_t stageResources = shader.stage == ShaderRecompiler::ShaderStage::Fragment ? colorAttachments : 0; @@ -2270,6 +2275,7 @@ void ShaderResources::buildComplete() { timing.uploadMs += phase(BuildPhase::Upload); if (usesBda) bda = std::make_unique(context, guestMemory); else if (usesFaultBuffer) bda = std::make_unique(context); + if (bda) bda->SetSrtGuards(std::move(srtGuardFaults)); finishImageTables(); phase(BuildPhase::Bda); if (_set != VK_NULL_HANDLE || fragment.set != VK_NULL_HANDLE) { diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index eef662d3fd..0eacba5cd3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -6931,6 +6931,28 @@ void verifyGuardedNullPointers() { const auto samePipeline = [](const RecompileResult& left, const RecompileResult& right) { return left.variantId == right.variantId && left.specializationId == right.specializationId && left.PipelineVariantId() == right.PipelineVariantId() && left.spirv == right.spirv; }; + const auto guardSites = [](const RecompileResult& result) { + const auto words = result.spirv.Words(); + std::map names; + std::map constants; + std::vector guardWords; + std::string function; + std::size_t calls = 0; + std::size_t stores = 0; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + const auto opcode = words[cursor] & 0xffffu; + require(count != 0 && count <= words.size() - cursor, "guarded pointer: truncated SPIR-V instruction"); + if (opcode == spv::OpName) names[words[cursor + 1]] = reinterpret_cast(&words[cursor + 2]); + if (opcode == spv::OpConstant && count == 4u) constants[words[cursor + 2]] = words[cursor + 3]; + if (opcode == spv::OpFunction) function = names[words[cursor + 2]]; + if (opcode == spv::OpAccessChain && count == 6u && names[words[cursor + 3]] == "fault_buffer" && constants.contains(words[cursor + 5]) && constants[words[cursor + 5]] == BdaAbi::SrtGuardWord) guardWords.push_back(words[cursor + 2]); + if (function == "main" && opcode == spv::OpFunctionCall && names[words[cursor + 3]] == "record_bda_fault") ++calls; + if (function == "main" && opcode == spv::OpStore && std::find(guardWords.begin(), guardWords.end(), words[cursor + 1]) != guardWords.end()) ++stores; + cursor += count; + } + return std::pair{calls, stores}; + }; NestedRequest branch(branchCode, root.data()); point(payloadAddress); @@ -6941,6 +6963,8 @@ void verifyGuardedNullPointers() { branch.request.context.memory = validMemory.Regions(); const auto validResult = Recompile(branch.request, *valid); require(validResult->poisonedSrtReads == 0u && faultBinding(*validResult) && validResult->bdaAbiVersion == BdaAbi::Version, "guarded pointer: a guarded program has no fault buffer"); + const auto [branchCalls, branchStores] = guardSites(*validResult); + require(branchCalls == 0u && branchStores == 1u, ("guarded pointer: a guarded read calls record_bda_fault " + std::to_string(branchCalls) + " times in main and stores its record to the fault buffer's guard word " + std::to_string(branchStores) + " times, expected one store and no call").c_str()); for (const auto pointer : {std::uint64_t{0}, unmapped}) { for (const auto condition : {1u, 0u}) { @@ -6996,6 +7020,8 @@ void verifyGuardedNullPointers() { chain.request.context.memory = mappedChainMemory.Regions(); const auto mappedChainResult = Recompile(chain.request, *mappedChain); require(chainResult->poisonedSrtReads == 3u && nullChainResult->poisonedSrtReads == 1u && mappedChainResult->poisonedSrtReads == 0u, "guarded pointer: a chain result does not count its poisoned reads"); + const auto [chainCalls, chainStores] = guardSites(*mappedChainResult); + require(chainCalls == 0u && chainStores == mappedChain->plan->guardedSrtSlots.size(), ("guarded pointer: a chain's guarded reads call record_bda_fault " + std::to_string(chainCalls) + " times in main and store to the guard word " + std::to_string(chainStores) + " times, expected one store per guard and no call").c_str()); require(samePipeline(*chainResult, *mappedChainResult) && samePipeline(*nullChainResult, *mappedChainResult), "guarded pointer: a chain's poison changed the variant, the specialization or the module"); const auto poisonedWords = [](const ResourceCapture& capture, std::uint32_t pc, std::uint64_t base) { diff --git a/core/shader/recompiler/BdaAbi.hpp b/core/shader/recompiler/BdaAbi.hpp index b1d2bf578f..0c84230b80 100644 --- a/core/shader/recompiler/BdaAbi.hpp +++ b/core/shader/recompiler/BdaAbi.hpp @@ -63,7 +63,9 @@ inline constexpr std::uint32_t WrittenOverflowWord = sizeof(Fault) / sizeof(std: inline constexpr std::uint32_t WrittenCountWord = WrittenOverflowWord + 1; inline constexpr std::uint32_t WrittenSlotsWord = WrittenCountWord + 1; inline constexpr std::uint32_t WrittenListWord = WrittenSlotsWord + 2 * WrittenPageSlots; -inline constexpr std::size_t FaultBufferBytes = (WrittenListWord + WrittenPageSlots) * sizeof(std::uint32_t); +inline constexpr std::uint32_t SrtGuardWord = WrittenListWord + WrittenPageSlots; +inline constexpr std::uint32_t SrtGuardStageShift = 24; +inline constexpr std::size_t FaultBufferBytes = (SrtGuardWord + 1) * sizeof(std::uint32_t); static_assert((WrittenPageSlots & (WrittenPageSlots - 1)) == 0); static_assert(WrittenChunkPages == 32); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index 42eb7dd81b..aa029426d2 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -1559,14 +1559,14 @@ std::uint32_t EmitReadConst(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto guard = static_cast(found - guarded.begin()); const auto flag = load(ConstantU32(state, resources.srtGuardOffset + guard)); const auto poisoned = Binary(state, spv::OpINotEqual, TypeBool(state), flag, ConstantU32(state, 0u)); + const auto records = resources.srtGuardOffset + static_cast(guarded.size()); + if (records + 3u * guarded.size() >= (1u << BdaAbi::SrtGuardStageShift)) { + ctx.Fail(inst, "has more guarded SRT records than the fault buffer's guard word can name"); + } EmitIfCondition(state, poisoned, [&] { - const auto records = resources.srtGuardOffset + static_cast(guarded.size()); const auto record = Binary(state, spv::OpIAdd, u32, ConstantU32(state, records), Binary(state, spv::OpIMul, u32, Binary(state, spv::OpISub, u32, flag, ConstantU32(state, 1u)), ConstantU32(state, 3u))); - const auto word = [&](std::uint32_t offset) { return load(Binary(state, spv::OpIAdd, u32, record, ConstantU32(state, offset))); }; - const auto pc = word(0u); - const auto low = word(1u); - const auto high = word(2u); - RecordBdaFaultWords(state, low, high, ConstantU32(state, 4u), pc, BdaAbi::FaultReason::Unmapped); + const auto stage = ConstantU32(state, static_cast(resources.stage) << BdaAbi::SrtGuardStageShift); + state.module.AddFunction(spv::OpStore, BdaWord(state, state.faultBufferVariable, ConstantU32(state, BdaAbi::SrtGuardWord)), Binary(state, spv::OpBitwiseOr, u32, record, stage)); }); StopBdaInvocationIf(state, poisoned); } From c76299f21725701d86fb99e034863a7212d97d91 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Fri, 16 Oct 2026 11:58:51 +0000 Subject: [PATCH 102/219] fix(shader): write a poisoned guarded read's record with an atomic store Invocations of one dispatch that fault through different guards wrote the guard word with plain stores, which the Vulkan memory model treats as a data race. The write is now a relaxed device-scope OpAtomicStore, so one of the records is reported whole. The guarded-pointer test now also has two workgroups fault through a null and an unmapped pointer. MarcZX100 pointed out the race. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 2 +- .../tests/execution/GuardedNullPointer.cpp | 19 ++++++++++++++++--- .../SpirvMemory/SpirvMemoryInstructions.cpp | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 0eacba5cd3..c4ee6b7cc9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -6948,7 +6948,7 @@ void verifyGuardedNullPointers() { if (opcode == spv::OpFunction) function = names[words[cursor + 2]]; if (opcode == spv::OpAccessChain && count == 6u && names[words[cursor + 3]] == "fault_buffer" && constants.contains(words[cursor + 5]) && constants[words[cursor + 5]] == BdaAbi::SrtGuardWord) guardWords.push_back(words[cursor + 2]); if (function == "main" && opcode == spv::OpFunctionCall && names[words[cursor + 3]] == "record_bda_fault") ++calls; - if (function == "main" && opcode == spv::OpStore && std::find(guardWords.begin(), guardWords.end(), words[cursor + 1]) != guardWords.end()) ++stores; + if (function == "main" && opcode == spv::OpAtomicStore && std::find(guardWords.begin(), guardWords.end(), words[cursor + 1]) != guardWords.end()) ++stores; cursor += count; } return std::pair{calls, stores}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GuardedNullPointer.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GuardedNullPointer.cpp index d9ad01a30a..eddf6735b9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GuardedNullPointer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GuardedNullPointer.cpp @@ -64,6 +64,12 @@ alignas(256) constexpr std::array ChainCode{ 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u, }; +alignas(256) constexpr std::array TwoGuardCode{ + 0xf4080100u, 0xfa000000u, 0xf4040280u, 0xfa000018u, 0xf4040380u, 0xfa000020u, 0xbe8c03ffu, 0x11111111u, + 0xbf8cc07fu, 0xbf068002u, 0xbf840003u, 0xf4000305u, 0xfa000000u, 0xbf820002u, 0xf4000307u, 0xfa000000u, + 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u, +}; + alignas(256) constexpr std::array DescriptorCode{ 0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0x7e0202ffu, 0x11111111u, 0xbf8cc07fu, 0xbf068008u, 0xbf850006u, 0xf4080405u, 0xfa000010u, 0xbf8cc07fu, 0xe0300000u, 0x80040100u, @@ -117,7 +123,7 @@ struct Outcome { }; template -Outcome Run(AgcDriver::VulkanDevice& device, const std::array& code, std::uint64_t pointer, std::uint32_t guard) { +Outcome Run(AgcDriver::VulkanDevice& device, const std::array& code, std::uint64_t pointer, std::uint32_t guard, std::uint32_t groups = 1u, std::uint64_t second = 0u) { const auto output = static_cast(reinterpret_cast(Output.data())); Root = {}; Root[0] = static_cast(output); @@ -127,11 +133,13 @@ Outcome Run(AgcDriver::VulkanDevice& device, const std::array(pointer); Root[7] = static_cast(pointer >> 32u); + Root[8] = static_cast(second); + Root[9] = static_cast(second >> 32u); std::fill(Output.begin(), Output.end(), 0xdeadbeefu); const auto root = static_cast(reinterpret_cast(Root.data())); const std::array userData{static_cast(root), static_cast(root >> 32u)}; const std::span span(code); - const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {false, false, false}, false, 1}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {groups > 1u, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ {ShaderStage::Compute, reinterpret_cast(span.data()), span, 0, {}}, {64, 0, userData, compute, std::nullopt, std::nullopt, {}}, @@ -150,7 +158,7 @@ Outcome Run(AgcDriver::VulkanDevice& device, const std::array(span.data())); + device.Dispatch(*result, groups, 1, 1, {}, reinterpret_cast(span.data())); device.WaitIdle(); } catch (...) { static_cast(log.Finish()); @@ -204,6 +212,11 @@ void RunTests(AgcDriver::VulkanDevice& device) { Require(Faulted(takenUnmapped) && takenUnmapped.log.find(Fault(unmapped, 0x2cu)) != std::string::npos, "guarded pointer: a read through an unmapped pointer that runs did not fault at its address:\n" + takenUnmapped.log); Require(takenUnmapped.pipelineVariantId == taken.pipelineVariantId, "guarded pointer: an unmapped pointer changed the pipeline variant"); + const auto twoGuards = Run(device, TwoGuardCode, 0u, 0u, 2u, unmapped); + const bool nullRecord = twoGuards.log.find(Fault(0u, 0x2cu)) != std::string::npos; + const bool unmappedRecord = twoGuards.log.find(Fault(unmapped, 0x38u)) != std::string::npos; + Require(twoGuards.poisoned == 2u && Faulted(twoGuards) && nullRecord != unmappedRecord, "guarded pointer: two workgroups faulting through different guards did not report exactly one of their two records:\n" + twoGuards.log); + const auto someLanes = Run(device, ExecCode, valid, 5u); Require(!Faulted(someLanes), "guarded pointer: a mapped pointer under EXEC faulted:\n" + someLanes.log); RequireWords(someLanes, 5u, Payload, "guarded pointer: a mapped pointer read under EXEC"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index aa029426d2..d997a174eb 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -1566,7 +1566,7 @@ std::uint32_t EmitReadConst(SpirvValueEmitContext& ctx, const IrValue& inst) { EmitIfCondition(state, poisoned, [&] { const auto record = Binary(state, spv::OpIAdd, u32, ConstantU32(state, records), Binary(state, spv::OpIMul, u32, Binary(state, spv::OpISub, u32, flag, ConstantU32(state, 1u)), ConstantU32(state, 3u))); const auto stage = ConstantU32(state, static_cast(resources.stage) << BdaAbi::SrtGuardStageShift); - state.module.AddFunction(spv::OpStore, BdaWord(state, state.faultBufferVariable, ConstantU32(state, BdaAbi::SrtGuardWord)), Binary(state, spv::OpBitwiseOr, u32, record, stage)); + state.module.AddFunction(spv::OpAtomicStore, BdaWord(state, state.faultBufferVariable, ConstantU32(state, BdaAbi::SrtGuardWord)), ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsMaskNone), Binary(state, spv::OpBitwiseOr, u32, record, stage)); }); StopBdaInvocationIf(state, poisoned); } From 213327fedeb023b765f221a22c7a690d4b226792 Mon Sep 17 00:00:00 2001 From: blu <131330772+ycbuzi@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:00:40 -0700 Subject: [PATCH 103/219] fix(libc): keep fmin and fmax quiet on a signalling NaN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fmin, fmax and their f and l forms tested their operands with std::isnan, which GCC compiles to a compare that raises FE_INVALID for a signalling NaN. FreeBSD's s_fmax.c and s_fmin.c read the exponent and significand bits instead, "to avoid raising spurious exceptions", and return the other operand. The NaN test now reads the bits of float, double and the 80-bit long double. GuestFminf checks that a signalling NaN on either side returns the other operand and leaves FE_INVALID clear, for all three types. The failure was first caught by the numeric helper test of boykopovar/AnyPS5#2280 (Привалов Александр Владимирович). Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Math.cpp | 28 ++++++++++++++++++++++++---- core/libs/tests/GuestFminf.cpp | 7 +++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 063eca4f01..369a61803a 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -183,11 +183,31 @@ long double AsinDenominator(long double t) { return 1.0L + t * (q[0] + t * (q[1] + t * (q[2] + t * (q[3] + t * q[4])))); } +template +bool NanWithoutException(T value) { + if constexpr (sizeof(T) == sizeof(std::uint32_t)) { + std::uint32_t bits; + std::memcpy(&bits, &value, sizeof(bits)); + return (bits & UINT32_C(0x7fffffff)) > UINT32_C(0x7f800000); + } else if constexpr (sizeof(T) == sizeof(std::uint64_t)) { + std::uint64_t bits; + std::memcpy(&bits, &value, sizeof(bits)); + return (bits & UINT64_C(0x7fffffffffffffff)) > UINT64_C(0x7ff0000000000000); + } else { + static_assert(std::numeric_limits::digits == 64); + std::uint64_t significand; + std::uint16_t signAndExponent; + std::memcpy(&significand, &value, sizeof(significand)); + std::memcpy(&signAndExponent, reinterpret_cast(&value) + sizeof(significand), sizeof(signAndExponent)); + return (signAndExponent & 0x7fff) == 0x7fff && (significand & UINT64_C(0x7fffffffffffffff)) != 0; + } +} + template T GuestFmin(T x, T y) { - if (std::isnan(x)) + if (NanWithoutException(x)) return y; - if (std::isnan(y)) + if (NanWithoutException(y)) return x; if (std::signbit(x) != std::signbit(y)) return std::signbit(x) ? x : y; @@ -196,9 +216,9 @@ T GuestFmin(T x, T y) { template T GuestFmax(T x, T y) { - if (std::isnan(x)) + if (NanWithoutException(x)) return y; - if (std::isnan(y)) + if (NanWithoutException(y)) return x; if (std::signbit(x) != std::signbit(y)) return std::signbit(x) ? y : x; diff --git a/core/libs/tests/GuestFminf.cpp b/core/libs/tests/GuestFminf.cpp index 742fad398f..fcf48171a6 100644 --- a/core/libs/tests/GuestFminf.cpp +++ b/core/libs/tests/GuestFminf.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -40,6 +41,12 @@ static void CheckMinAndMax(T (APS5_VABI *minimum)(T, T), T (APS5_VABI *maximum)( Require(maximum(T(4.0), nan) == T(4.0)); Require(std::isnan(maximum(nan, nan))); + const auto signaling = std::numeric_limits::signaling_NaN(); + std::feclearexcept(FE_ALL_EXCEPT); + Require(minimum(signaling, T(4.0)) == T(4.0) && minimum(T(4.0), signaling) == T(4.0)); + Require(maximum(signaling, T(4.0)) == T(4.0) && maximum(T(4.0), signaling) == T(4.0)); + Require(std::fetestexcept(FE_INVALID) == 0); + Require(std::signbit(minimum(T(-0.0), T(+0.0)))); Require(std::signbit(minimum(T(+0.0), T(-0.0)))); Require(std::signbit(minimum(T(-0.0), T(-0.0)))); From 971d8ba89174a1d36e1d023ee534243fb4946322 Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Tue, 20 Oct 2026 11:48:16 +0530 Subject: [PATCH 104/219] feat(libSceNpTus): delete title contexts and their requests --- core/libs/prx/libSceNpTus/Export.cpp | 10 +++++- core/libs/tests/GuestNpTus.cpp | 53 ++++++++++++++++++++++++++++ docs/dev/debt/libSceNpTus.md | 1 + 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNpTus/Export.cpp b/core/libs/prx/libSceNpTus/Export.cpp index 11b8a99218..f8d8c89991 100644 --- a/core/libs/prx/libSceNpTus/Export.cpp +++ b/core/libs/prx/libSceNpTus/Export.cpp @@ -20,6 +20,7 @@ static constexpr int MAX_SLOTS = 64; namespace { struct Request { + int titleCtxId = 0; bool started = false; bool aborted = false; int result = 0; @@ -68,10 +69,17 @@ int APS5_VABI sceNpTusCreateRequest(int titleCtxId) { if (!titleContexts.contains(titleCtxId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; if (requests.size() >= MAX_REQUESTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS; const int reqId = nextRequest++; - requests.emplace(reqId, Request{}); + requests.emplace(reqId, Request{titleCtxId}); return reqId; } +int APS5_VABI sceNpTusDeleteNpTitleCtx(int titleCtxId) { + std::lock_guard lock(tusMutex); + if (!titleContexts.erase(titleCtxId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + std::erase_if(requests, [titleCtxId](const auto& entry) { return entry.second.titleCtxId == titleCtxId; }); + return 0; +} + int APS5_VABI sceNpTusDeleteRequest(int reqId) { std::lock_guard lock(tusMutex); return requests.erase(reqId) ? 0 : SCE_NP_COMMUNITY_ERROR_INVALID_ID; diff --git a/core/libs/tests/GuestNpTus.cpp b/core/libs/tests/GuestNpTus.cpp index f2376a4113..4f8afcc04c 100644 --- a/core/libs/tests/GuestNpTus.cpp +++ b/core/libs/tests/GuestNpTus.cpp @@ -1,9 +1,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include +#include extern "C" { int APS5_VABI sceNpTusCreateNpTitleCtxA(std::uint32_t serviceLabel, int userId); +int APS5_VABI sceNpTusDeleteNpTitleCtx(int titleCtxId); int APS5_VABI sceNpTusCreateRequest(int titleCtxId); int APS5_VABI sceNpTusDeleteRequest(int reqId); int APS5_VABI sceNpTusAbortRequest(int reqId); @@ -16,6 +19,7 @@ int APS5_VABI sceNpTusAddAndGetVariableAAsync(int reqId, std::uint64_t targetAcc static constexpr int SIGNED_OUT = static_cast(0x80550006); static constexpr int INVALID_ARGUMENT = static_cast(0x80550704); +static constexpr int TOO_MANY_OBJECTS = static_cast(0x80550706); static constexpr int ABORTED = static_cast(0x80550707); static constexpr int INSUFFICIENT_ARGUMENT = static_cast(0x8055070C); static constexpr int INVALID_ID = static_cast(0x8055070E); @@ -70,4 +74,53 @@ int main() { Require(sceNpTusAbortRequest(request) == INVALID_ID); Require(sceNpTusPollAsync(request, &result) == INVALID_ID); Require(sceNpTusSetMultiSlotVariableAAsync(request, 0, slots, values, 2, nullptr) == INVALID_ID); + + Require(sceNpTusDeleteNpTitleCtx(0) == INVALID_ID); + Require(sceNpTusDeleteNpTitleCtx(-1) == INVALID_ID); + Require(sceNpTusDeleteNpTitleCtx(context + 1000) == INVALID_ID); + const int siblingContext = sceNpTusCreateNpTitleCtxA(0, 1); + const int siblingRequest = sceNpTusCreateRequest(siblingContext); + Require(siblingContext > 0 && siblingRequest > 0); + Require(sceNpTusSetMultiSlotVariableAAsync(siblingRequest, 0, slots, values, 2, nullptr) == 0); + const int idle = sceNpTusCreateRequest(context); + const int finished = sceNpTusCreateRequest(context); + const int aborted = sceNpTusCreateRequest(context); + Require(idle > 0 && finished > 0 && aborted > 0); + Require(sceNpTusSetMultiSlotVariableAAsync(finished, 0, slots, values, 2, nullptr) == 0); + Require(sceNpTusAbortRequest(aborted) == 0); + Require(sceNpTusDeleteNpTitleCtx(context) == 0); + Require(sceNpTusDeleteNpTitleCtx(context) == INVALID_ID); + Require(sceNpTusCreateRequest(context) == INVALID_ID); + for (const int child : {idle, finished, aborted}) { + Require(sceNpTusPollAsync(child, &result) == INVALID_ID); + Require(sceNpTusDeleteRequest(child) == INVALID_ID); + Require(sceNpTusAbortRequest(child) == INVALID_ID); + Require(sceNpTusSetMultiSlotVariableAAsync(child, 0, slots, values, 2, nullptr) == INVALID_ID); + } + RequireFinished(siblingRequest, SIGNED_OUT); + Require(sceNpTusDeleteNpTitleCtx(siblingContext) == 0); + + std::array contexts{}; + for (int& titleContext : contexts) { + titleContext = sceNpTusCreateNpTitleCtxA(0, 1); + Require(titleContext > 0); + } + Require(sceNpTusCreateNpTitleCtxA(0, 1) == TOO_MANY_OBJECTS); + for (int& titleContext : contexts) { + const int previous = titleContext; + Require(sceNpTusDeleteNpTitleCtx(previous) == 0); + titleContext = sceNpTusCreateNpTitleCtxA(0, 1); + Require(titleContext > 0 && titleContext != previous); + Require(sceNpTusCreateRequest(previous) == INVALID_ID); + } + std::array children{}; + for (int& child : children) { + child = sceNpTusCreateRequest(contexts[0]); + Require(child > 0); + } + Require(sceNpTusCreateRequest(contexts[1]) == TOO_MANY_OBJECTS); + Require(sceNpTusDeleteNpTitleCtx(contexts[0]) == 0); + Require(sceNpTusCreateRequest(contexts[1]) > 0); + for (const int child : children) Require(sceNpTusPollAsync(child, &result) == INVALID_ID); + for (std::size_t index = 1; index < contexts.size(); ++index) Require(sceNpTusDeleteNpTitleCtx(contexts[index]) == 0); } diff --git a/docs/dev/debt/libSceNpTus.md b/docs/dev/debt/libSceNpTus.md index 3ccc29383a..8f7fca86ed 100644 --- a/docs/dev/debt/libSceNpTus.md +++ b/docs/dev/debt/libSceNpTus.md @@ -3,6 +3,7 @@ ### Unknown function info - [SceNpTusVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the layout of the output array of `sceNpTusGetMultiSlotVariableAAsync` and of `outVariable` in `sceNpTusAddAndGetVariableAAsync` is not verified, so the output is never written and `variableArraySize` / `outVariableSize` are not checked. The signatures and the community error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/tree/main/src/core/libraries/np/np_tus), which fpPS4 agrees with for the context and request calls; no console measurement. +- [sceNpTusDeleteNpTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the PS5 signature, error codes and child-request lifetime have not been measured on a console. Deletion invalidates the context and all its requests, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e7626e3c7de8d0f0fda6a91f620b42f6c762/src/core/libraries/np/np_tus/np_tus.cpp#L158-L180). ### Functional From 43b1f49f7cb1d3a3924336faf3a3374608c5b9b9 Mon Sep 17 00:00:00 2001 From: Agnibha Mukherjee Date: Sun, 18 Oct 2026 11:47:41 +0530 Subject: [PATCH 105/219] fix(tools): keep the blank line between a staying entry and a paragraph render_index emitted a blank line only after a heading or text line, so a paragraph following an entry that stays in the index was written directly under the bullet, and Markdown read it as a lazy continuation of the list. --- tools/debt_split.py | 2 +- tools/tests/test_debt_split.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/debt_split.py b/tools/debt_split.py index 728521a76a..1460f46bca 100644 --- a/tools/debt_split.py +++ b/tools/debt_split.py @@ -115,7 +115,7 @@ def split(text): def render_index(index): out = [] for kind, item in index: - if kind == "heading" and out and out[-1] != "": + if kind in ("heading", "text") and out and out[-1] != "": out.append("") out.append(item) if kind == "heading" or kind == "text": diff --git a/tools/tests/test_debt_split.py b/tools/tests/test_debt_split.py index d2d25e96be..1f2cf31775 100644 --- a/tools/tests/test_debt_split.py +++ b/tools/tests/test_debt_split.py @@ -234,6 +234,13 @@ class DebtSplitTests(unittest.TestCase): self.assertTrue((self.root / "docs/dev/debt/libSceA.md").is_file()) self.assertIn("libSceA technical debt", self.read("docs/dev/debt/libSceA.md")) + def test_split_keeps_blank_line_between_staying_entry_and_paragraph(self): + paragraph = "A paragraph that belongs to no library." + self.write(INDEX, "# Project technical debt\n\n### Silent stubs\n\n" + A_STUB + + "\n- A toolchain limitation\n\n" + paragraph + "\n") + self.split() + self.assertIn("- A toolchain limitation\n\n" + paragraph + "\n", self.read(INDEX)) + if __name__ == "__main__": unittest.main() From 8ab6ecb17a1cbad665f61fe91f707a22b6c06a17 Mon Sep 17 00:00:00 2001 From: Allan John Date: Tue, 20 Oct 2026 12:55:34 +0530 Subject: [PATCH 106/219] feat(libSceNet): implement sceNetGetDnsInfo Report the host IPv4 DNS servers of the interface sceNetCtlGetInfo uses, through a helper libSceNetCtl exports, and return their count. The signature and layout follow the OpenOrbis musl resolver and Net.h. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/CMakeLists.txt | 1 + core/libs/prx/libSceNet/Export.cpp | 11 +++++++--- core/libs/prx/libSceNetCtl/Export.cpp | 7 ++++++ core/libs/tests/GuestSceNet.cpp | 30 ++++++++++++++++++++++++++ docs/dev/debt/libSceNet.md | 2 +- 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index cdab277419..4f8e132a18 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceNet SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceNet PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceNet PRIVATE libSceNetCtl) configure_windows_unwind(libSceNet) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 7ac322dcb9..1e8beed556 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -349,6 +349,7 @@ int* errno_slot() { } extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" std::size_t NetCtlHostDnsServers_nid_no_patch(std::uint32_t* servers, std::size_t capacity); void set_errno(int err) { *errno_slot() = err; @@ -2118,9 +2119,13 @@ int APS5_VABI sceNetGetAppMemInfo(void) { return 0; } -int APS5_VABI sceNetGetDnsInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNetGetDnsInfo(std::uint32_t* info, int flags) { + if (flags != 0) throw std::runtime_error("sceNetGetDnsInfo: flags " + std::to_string(flags) + " are not supported"); + if (!info) throw std::runtime_error("sceNetGetDnsInfo: a null info is not supported"); + std::array servers{}; + const std::size_t count = std::min(NetCtlHostDnsServers_nid_no_patch(servers.data(), servers.size()), servers.size()); + std::memcpy(info, servers.data(), sizeof(servers)); + return static_cast(count); } int APS5_VABI sceNetGetIfList(void) { diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 1e11aadff5..696bcc55c3 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -350,6 +350,13 @@ static bool FindHostIpv4Interface(HostIpv4Interface& found) { #endif } +extern "C" std::size_t NetCtlHostDnsServers_nid_no_patch(std::uint32_t* servers, std::size_t capacity) { + HostIpv4Interface host; + if (!FindHostIpv4Interface(host)) return 0; + for (std::size_t index = 0; index < host.dns.size() && index < capacity; ++index) servers[index] = host.dns[index].s_addr; + return host.dns.size(); +} + static void WriteInfoWord(NetCtlInfo* info, std::uint32_t value) { std::memcpy(info->opaque, &value, sizeof(value)); } diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 6e2b621d33..7807e27564 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -72,7 +72,9 @@ int APS5_VABI sceNetResolverAbort(int, int); int APS5_VABI sceNetResolverDestroy(int); int APS5_VABI sceNetResolverGetError(int, int*); int APS5_VABI sceNetClearDnsCache(int); +int APS5_VABI sceNetGetDnsInfo(std::uint32_t*, int); int APS5_VABI sceNetCtlGetState(int*); +int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat*); int APS5_VABI sceNetCtlRegisterCallback(NetCtlCallback, void*, int*); int APS5_VABI sceNetCtlUnregisterCallback(int); @@ -1856,6 +1858,34 @@ int main() { } Require(missing); } + { + std::array dns{}; + dns.fill(0xA5A5A5A5u); + const int count = sceNetGetDnsInfo(dns.data(), 0); + Require(count >= 0 && count <= 2 && dns[2] == 0xA5A5A5A5u); + Require(count == 0 || state == 3); + for (int index = count; index < 2; ++index) Require(dns[index] == 0); + for (int index = 0; index < 2; ++index) { + NetCtlInfo info{}; + const int result = sceNetCtlGetInfo(17 + index, &info); + if (result != 0) { + Require(result == static_cast(0x80412108) && count == 0); + continue; + } + const auto* bytes = reinterpret_cast(&dns[index]); + char text[16] = {}; + std::snprintf(text, sizeof(text), "%u.%u.%u.%u", static_cast(bytes[0]), static_cast(bytes[1]), static_cast(bytes[2]), static_cast(bytes[3])); + Require(std::strcmp(text, reinterpret_cast(info.opaque)) == 0); + } + for (const int flags : {0x1, -1}) { + bool threw = false; + try { sceNetGetDnsInfo(dns.data(), flags); } catch (const std::runtime_error&) { threw = true; } + Require(threw); + } + bool threw = false; + try { sceNetGetDnsInfo(nullptr, 0); } catch (const std::runtime_error&) { threw = true; } + Require(threw); + } NetCtlIfStat first{}; first.device = 7; first.reserved[7] = 7; diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 221bc7c877..3497d53429 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -35,7 +35,7 @@ - [sceNetFreeAllRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetArpInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetAppMemInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(info, int flags)` signature, the two IPv4 server slots in network byte order and the server count as the return value follow the PS4 [OpenOrbis musl resolver](https://github.com/OpenOrbis/musl/blob/2b0bb8ce2fa64ac255cb656785729d159a5f074c/src/network/resolvconf.c) and [OpenOrbis Net.h](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/Net.h); unverified on PS5 hardware. It reports the host DNS servers of the interface `sceNetCtlGetInfo` reports, never servers set through `sceNetSetDnsInfo`. Zeroing the unused slots and returning 0 without a host IPv4 interface are unverified. Nonzero flags and a null info throw because their console results are unknown - [sceNetGetIfList](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetIfListEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetIfListOnce](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From c22ed3634754e89024fdabdde0de02b07c84b0f8 Mon Sep 17 00:00:00 2001 From: Zenodeon Date: Thu, 15 Oct 2026 20:36:19 +0400 Subject: [PATCH 107/219] fix(agc): restore write-watch coverage for remapped pages --- .../Execution/include/WriteWatchCoverage.hpp | 35 +++++++++++----- .../Execution/src/GuestMemory.cpp | 5 ++- .../tests/WriteWatchCoverage.cpp | 41 ++++++++++++++++++- 3 files changed, 68 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp index 124d638998..22865df487 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp @@ -23,7 +23,11 @@ public: if (bytes == 0 || bytes > size || address < base || address - base > size - bytes) return false; const auto first = (address - base) / BlockBytes; const auto last = (address - base + bytes - 1) / BlockBytes; - return std::none_of(excluded.begin() + first, excluded.begin() + last + 1, [](auto generation) { return generation != 0; }); + const auto firstPage = (address - base) % BlockBytes / PageBytes; + const auto lastPage = (address - base + bytes - 1) % BlockBytes / PageBytes; + if (first == last) return (excluded[first] & pageMask(firstPage, lastPage + 1)) == 0; + if ((excluded[first] & pageMask(firstPage, PagesPerBlock)) != 0 || (excluded[last] & pageMask(0, lastPage + 1)) != 0) return false; + return std::none_of(excluded.begin() + first + 1, excluded.begin() + last, [](auto mask) { return mask != 0; }); } bool ExcludedSpan(std::uint64_t address, std::size_t bytes, std::uint64_t* begin, std::uint64_t* end) const { @@ -40,10 +44,12 @@ public: if (!excludedBlocks(address, bytes, first, last)) return false; bool changed = false; for (auto block = first; block <= last; ++block) { - changed = changed || excluded[block] == 0; + const auto pages = (std::min(BlockBytes, size - block * BlockBytes) + PageBytes - 1) / PageBytes; + const auto mask = pageMask(0, pages); + changed = changed || excluded[block] != mask; auto& stamp = excludedGenerations[block]; stamp = std::max(stamp, std::max(generation, 1)); - excluded[block] = 1; + excluded[block] = mask; } return changed; } @@ -58,21 +64,28 @@ public: const auto offset = address - base; if (offset >= size) return false; bytes = static_cast(std::min(bytes, size - offset)); - const auto first = offset / BlockBytes + (offset % BlockBytes != 0); + const auto firstPage = offset / PageBytes + (offset % PageBytes != 0); const auto stop = offset + bytes; - const auto end = stop / BlockBytes + (stop == size && stop % BlockBytes != 0); + const auto endPage = stop / PageBytes + (stop == size && stop % PageBytes != 0); + if (firstPage >= endPage) return false; + const auto first = firstPage / PagesPerBlock; + const auto last = (endPage - 1) / PagesPerBlock; bool changed = false; - for (auto block = first; block < end; ++block) { - if (excluded[block] != 0 && excludedGenerations.at(block) < generation) { + for (auto block = first; block <= last; ++block) { + const auto mask = pageMask(block == first ? firstPage % PagesPerBlock : 0, block == last ? (endPage - 1) % PagesPerBlock + 1 : PagesPerBlock); + if ((excluded[block] & mask) != 0 && excludedGenerations.at(block) < generation) { changed = true; - excluded[block] = 0; - excludedGenerations.erase(block); + excluded[block] &= static_cast(~mask); + if (excluded[block] == 0) excludedGenerations.erase(block); } } return changed; } private: + static std::uint16_t pageMask(std::size_t first, std::size_t end) { + return static_cast(((1u << end) - 1u) & ~((1u << first) - 1u)); + } bool excludedBlocks(std::uint64_t address, std::size_t bytes, std::size_t& first, std::size_t& last) const { if (bytes == 0 || size == 0) return false; if (address < base) { @@ -88,9 +101,11 @@ private: return true; } static constexpr std::size_t BlockBytes = 65536; + static constexpr std::size_t PageBytes = 4096; + static constexpr std::size_t PagesPerBlock = BlockBytes / PageBytes; std::uint64_t base = 0; std::size_t size = 0; - std::vector excluded; + std::vector excluded; std::unordered_map excludedGenerations; }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 36d57da9c9..6e7a43a575 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -925,7 +925,7 @@ struct WriteTracker { if (first >= stop) return; bool started = false; for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) { - if (!coverage.Covers(blockBegin(block), 1)) continue; + if (!coverage.Covers(blockBegin(block), static_cast(std::min(WriteBlockBytes, base + size - blockBegin(block))))) continue; if (!started) ++generation; started = true; stampHint(block); @@ -935,7 +935,7 @@ struct WriteTracker { bool hinted(std::uint64_t first, std::uint64_t stop) const { if (first >= stop || first < base || stop > base + size) return false; for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) { - if (hintOf(block) == 0 || coverage.Covers(blockBegin(block), 1)) return false; + if (hintOf(block) == 0 || coverage.Covers(blockBegin(block), static_cast(std::min(WriteBlockBytes, base + size - blockBegin(block))))) return false; } return !GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(static_cast(first), static_cast(stop - first)); } @@ -1204,6 +1204,7 @@ void watchPrivateMapping(std::uintptr_t address, std::size_t bytes, std::uint64_ ++tracker.generation; tracker.noteCpuStore(address, address + bytes, tracker.generation); for (auto block = tracker.blockOf(address); block <= tracker.blockOf(address + bytes - 1); ++block) { + if (const auto& leaf = tracker.leaves[block / WriteTracker::LeafBlocks]; leaf != nullptr) leaf->hintBlocks[block % WriteTracker::LeafBlocks] = 0; if (const auto found = tracker.driverPieces.find(block); found != tracker.driverPieces.end()) found->second.dropped = std::max(found->second.dropped, tracker.generation); tracker.stamp(block, tracker.generation, StampKind::Cpu); } diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp index 806c3334e7..62e56cfe52 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp @@ -41,7 +41,8 @@ int main() { require(coverage.Covers(base + block, block), "clipping an import excluded the rest of the arena"); coverage.Initialize(base, 4 * block + 1); coverage.Exclude(base, 4 * block + 1); - require(!coverage.Restore(base + 1, block - 1) && !coverage.Covers(base, block), "a partial replacement restored an excluded block"); + require(coverage.Restore(base + 1, block - 1) && coverage.Covers(base + 4096, block - 4096), "a partial replacement did not restore its complete pages"); + require(!coverage.Covers(base, 4096) && !coverage.Covers(base, block), "a partial replacement restored an incompletely remapped page"); require(coverage.Restore(base + block, block) && coverage.Covers(base + block, block), "fresh private backing stayed excluded"); require(!coverage.Restore(base + block, block), "restoring an already watched block reports a change"); require(!coverage.Covers(base, block) && !coverage.Covers(base + 2 * block, block), "restoring one block restored its neighbours"); @@ -60,6 +61,44 @@ int main() { coverage.Exclude(base, block, 7); require(!coverage.Restore(base, block, 6) && !coverage.Covers(base, block), "a delayed mapping notification erased a repeated import"); require(coverage.Restore(base, block, 8), "a fresh replacement did not restore the latest import exclusion"); + coverage.Initialize(base, 2 * block); + coverage.Exclude(base, block, 10); + require(coverage.Restore(base + 16384, 16384, 11), "a fresh partial-block private mapping stayed excluded"); + require(coverage.Covers(base + 16384, 16384), "the restored private pages are not watched"); + require(!coverage.Covers(base, 16384) && !coverage.Covers(base + 32768, 32768), "restoring private pages watched an imported neighbour"); + require(!coverage.Covers(base + 16383, 2) && !coverage.Covers(base + 32767, 2), "a boundary query included excluded pages"); + require(!coverage.Restore(base + 32768, 4096, 10), "a stale partial mapping restored an exclusion"); + coverage.Exclude(base + 4096, 1, 12); + require(!coverage.Covers(base + 16384, 16384), "a repeated import kept partial coverage from an older mapping"); + require(!coverage.Restore(base + 16384, 16384, 11), "a delayed partial mapping restored a newer import"); + require(coverage.Restore(base + 16384, 16384, 13), "fresh partial coverage was not restored after re-import"); + constexpr std::size_t page = 4096; + for (std::size_t first = 0; first < block / page; ++first) { + for (std::size_t count = 1; count <= block / page - first; ++count) { + coverage.Initialize(base, 3 * block); + coverage.Exclude(base + block, block, 20); + require(coverage.Restore(base + block + first * page, count * page, 21), "a complete-page replacement was not restored"); + for (std::size_t index = 0; index < 3 * block / page; ++index) { + const bool expected = index < block / page || index >= 2 * block / page || (index >= block / page + first && index < block / page + first + count); + require(coverage.Covers(base + index * page, page) == expected, "partial restoration changed an unrelated page"); + if (index != 0) { + const bool previous = index - 1 < block / page || index - 1 >= 2 * block / page || (index - 1 >= block / page + first && index - 1 < block / page + first + count); + require(coverage.Covers(base + index * page - 1, 2) == (previous && expected), "a page boundary proof ignored an excluded page"); + } + } + require(coverage.Covers(base + block, block) == (count == block / page), "partial page coverage proved a whole block"); + require(!coverage.Restore(base + block + first * page, count * page, 21), "restoring watched pages again reports a change"); + } + } + coverage.Initialize(base, 2 * block + 17); + coverage.Exclude(base + 2 * block, 17, 30); + require(!coverage.Restore(base + 2 * block, 16, 31), "an incomplete terminal page was restored"); + require(coverage.Restore(base + 2 * block, 17, 31) && coverage.Covers(base + 2 * block, 17), "a complete terminal arena page stayed excluded"); + coverage.Initialize(base, 2 * block); + coverage.Exclude(base, block, 40); + coverage.Exclude(base + block, block, 43); + require(coverage.Restore(base + block - page, 2 * page, 42), "a newer boundary page was not restored"); + require(coverage.Covers(base + block - page, page) && !coverage.Covers(base + block, page), "a stale boundary mapping erased a newer neighbouring exclusion"); std::cout << "write watch coverage passed\n"; return 0; } catch (const std::exception& error) { From 823c6a55e148aeaa1e2444eb37a0d324ce0b78c5 Mon Sep 17 00:00:00 2001 From: Zenodeon Date: Sat, 17 Oct 2026 23:53:18 +0400 Subject: [PATCH 108/219] test(agc): verify private remap coverage and remaining hints --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 ++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 53 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index fbd9b9d9c6..84e5750a3d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1337,6 +1337,11 @@ if(BUILD_TESTING) set_tests_properties(agc_unchanged_cpu_stamps PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(WIN32) + add_test(NAME agc_partial_watch_restoration COMMAND agc_driver_recorder_tests --partial-watch-restoration-only) + set_tests_properties(agc_partial_watch_restoration PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_storage_kept_unit_settle COMMAND agc_driver_recorder_tests --kept-unit-settle-only) set_tests_properties(agc_storage_kept_unit_settle PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index f3b0ad345c..d45f9b5124 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -116,6 +116,54 @@ void ReleaseWatched(void* block, std::size_t bytes) { #endif } +bool partialWatchRestorationTests() { +#ifdef _WIN32 + constexpr std::size_t bytes = 131072; + constexpr std::size_t page = 4096; + constexpr std::size_t restoredOffset = 4 * page; + constexpr std::size_t restoredBytes = 4 * page; + if (!AgcDriver::GuestMemory::WriteWatched()) return false; + auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536)); + if (memory == nullptr) return false; + GuestArena::GuestArenaCommit_nid_postfix(memory, bytes, PAGE_READWRITE, restoredBytes); + struct Cleanup { + std::uint8_t* memory; + ~Cleanup() { ReleaseWatched(memory, bytes); } + } cleanup{memory}; + const auto address = reinterpret_cast(memory); + AgcDriver::GuestMemory::Unwatch(address, 65536); + Require(!AgcDriver::GuestMemory::Watched(address, page), "the partial remap fixture was not excluded"); + memory[page] = 0x22; + AgcDriver::GuestMemory::MarkWritten(address + page, 1); + Require(AgcDriver::GuestMemory::UntrackedHint(address + page, 1) != 0, "the excluded remap fixture has no driver hint"); + GuestArena::GuestArenaReset_nid_postfix(memory + restoredOffset, restoredBytes); + GuestArena::GuestArenaCommit_nid_postfix(memory + restoredOffset, restoredBytes, PAGE_READWRITE, restoredBytes); + Require(AgcDriver::GuestMemory::Watched(address + restoredOffset, restoredBytes), "a partial private remap did not restore its write watch"); + Require(!AgcDriver::GuestMemory::Watched(address, restoredOffset), "a partial remap restored an excluded prefix"); + Require(!AgcDriver::GuestMemory::Watched(address + restoredOffset + restoredBytes, page), "a partial remap restored an excluded suffix"); + Require(AgcDriver::GuestMemory::UntrackedHint(address + page, 1) == 0, "a partial remap retained a stale driver hint"); + std::memset(memory + restoredOffset, 0x33, restoredBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto generation = AgcDriver::GuestMemory::CollectWrites(address + restoredOffset, restoredBytes); + Require(generation != 0, "the private remap has no collected CPU write history"); + memory[restoredOffset + page] = 0x44; + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(AgcDriver::GuestMemory::CollectWrites(address + restoredOffset, restoredBytes) != 0, "the private remap stopped collecting CPU writes"); + Require(!AgcDriver::GuestMemory::UnchangedSince(address + restoredOffset, restoredBytes, generation), "the private remap hid a genuine CPU write"); + GuestArena::GuestArenaReset_nid_postfix(memory, restoredBytes); + GuestArena::GuestArenaCommit_nid_postfix(memory, restoredBytes, PAGE_READWRITE, restoredBytes); + Require(AgcDriver::GuestMemory::Watched(address, restoredBytes), "the first private pages did not regain coverage"); + Require(!AgcDriver::GuestMemory::Watched(address + 12 * page, page), "restoring the first pages restored an excluded tail"); + memory[12 * page] = 0x55; + AgcDriver::GuestMemory::MarkWritten(address + 12 * page, 1); + AgcDriver::GuestMemory::Unwatch(address + 12 * page, page); + Require(AgcDriver::GuestMemory::UntrackedHint(address + 12 * page, 1) == 0, "excluding a partially watched block invented a write-watch hint"); + return true; +#else + return false; +#endif +} + std::optional selectDevice(std::span devices, std::string_view request) { const auto rankDeviceType = [](VkPhysicalDeviceType type) { switch (type) { @@ -9571,6 +9619,11 @@ void Run(Context context) { int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--partial-watch-restoration-only") { + if (!partialWatchRestorationTests()) return 77; + std::cout << "partial private write-watch restoration tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--device-selection-only") { deviceSelectionTests(); std::cout << "recorder device selection tests passed\n"; From c97e5e033821e5e3b6ebcefe7ae901f6992bda4e Mon Sep 17 00:00:00 2001 From: karthikeyaAnna Date: Mon, 19 Oct 2026 21:50:56 +0530 Subject: [PATCH 109/219] fix(agc): back 1D depth comparison textures with 2D images Back 1D and 1D-array depth comparison textures with 2D Vulkan images and views of height 1. Drivers such as AMD RADV reject 1D depth images (VK_ERROR_FORMAT_NOT_SUPPORTED). The shader recompiler maps 1D depth comparison samplers to 2D image descriptors, and pins the normalized vertical coordinate to 0.5 to sample the center of row 0 without hitting borders. --- .../Graphics/src/ShaderResources.cpp | 13 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 23 +- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 1 + .../execution/ImageOneDimensionalCompare.cpp | 365 ++++++++++++++++++ .../IrMetadata/Resources.hpp | 1 + .../Optimization/src/ResourceMaterializer.cpp | 11 +- .../Optimization/src/ResourceTracker.cpp | 9 +- core/shader/recompiler/ShaderDiskCache.cpp | 4 +- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 15 +- 10 files changed, 420 insertions(+), 24 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageOneDimensionalCompare.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 5bafd790ea..25d2251b21 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4555,14 +4555,15 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words); const bool forcedSrgb = ForcesDegammaOn(resource.format, binding.imageSamplers[element], shaderSamplers); RequireNonSeamlessCube(resource.dimension == TextureDimension::kCube, binding.imageSamplers[element], shaderSamplers); - const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray; - if (!firstLayer && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); + const bool depthCompare = !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element); + const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && (resource.dimension == TextureDimension::k2DArray || (depthCompare && resource.dimension == TextureDimension::k1DArray)); + const bool depth1D = depthCompare && ((binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k1D) || (binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2DArray && resource.dimension == TextureDimension::k1DArray)); + if (!firstLayer && !depth1D && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); const VkComponentMapping components = ViewComponents(resource); const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes; CompletePendingCpuWrites(context, resource.baseAddress, static_cast(guestBytes)); if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes)); std::shared_ptr texture; - const bool depthCompare = !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element); const VkImageViewType multisampleViewType = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2DMultisampleArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; if (previousSampled.texture != nullptr && previousSampled.depthCompare == depthCompare && previousSampled.multisampleViewType == multisampleViewType && words.size() == 8 && std::equal(words.begin(), words.end(), previousSampled.words.begin())) texture = previousSampled.texture; if (texture == nullptr && record != nullptr && record->texture != nullptr) { @@ -4900,8 +4901,10 @@ void ShaderResources::resolveImageTables() { try { resource = DecodeTextureResource(words); shape = binding.imageShapes.at(element); - firstLayer = shape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray; - if (!firstLayer && !MatchesGuestDimension(shape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the table entry's image shape (shape " + std::to_string(static_cast(shape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); + const bool depthCompare = element < binding.imageDepthCompare.size() && binding.imageDepthCompare[element]; + firstLayer = shape == ShaderRecompiler::DescriptorImageShape::Image2D && (resource.dimension == TextureDimension::k2DArray || (depthCompare && resource.dimension == TextureDimension::k1DArray)); + const bool depth1D = depthCompare && ((shape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k1D) || (shape == ShaderRecompiler::DescriptorImageShape::Image2DArray && resource.dimension == TextureDimension::k1DArray)); + if (!firstLayer && !depth1D && !MatchesGuestDimension(shape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the table entry's image shape (shape " + std::to_string(static_cast(shape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); guestBytes = DescribeSurface(resource).guestBytes; if (!GuestMemory::Accessible(reinterpret_cast(resource.baseAddress), static_cast(guestBytes)) && GuestMemory::CommittedRanges(resource.baseAddress, static_cast(guestBytes), false).empty()) throw std::runtime_error("AGC graphics: the table entry's surface at 0x" + HexText(resource.baseAddress) + " (0x" + HexText(guestBytes) + " bytes) is not mapped"); static_cast(ResolveTextureFormat(resource.format)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 1c4afad236..1bae7649df 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -329,11 +329,12 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex Require(arrayLayers == 0 || sliceLinearBytes <= UINT64_MAX / arrayLayers, "detiled texture buffer size overflows"); const auto linearBytes = sliceLinearBytes * arrayLayers; + const bool depth1D = depthCompare && (descriptor.dimension == TextureDimension::k1D || descriptor.dimension == TextureDimension::k1DArray); VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; imageInfo.flags = (descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u) | (SrgbTwinFormat(context, vkFormat) != VK_FORMAT_UNDEFINED ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT : 0u); - imageInfo.imageType = ImageTypeFor(descriptor.dimension); + imageInfo.imageType = depth1D ? VK_IMAGE_TYPE_2D : ImageTypeFor(descriptor.dimension); imageInfo.format = vkFormat; - imageInfo.extent = {descriptor.width, descriptor.height, geometry.imageDepth}; + imageInfo.extent = {descriptor.width, depth1D ? 1u : descriptor.height, geometry.imageDepth}; imageInfo.mipLevels = descriptor.mipCount; imageInfo.arrayLayers = geometry.imageLayers; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; @@ -530,7 +531,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; - viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); + viewInfo.viewType = depth1D ? (descriptor.dimension == TextureDimension::k1DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D) : ViewTypeFor(descriptor.dimension, viewLayerCount); viewInfo.format = srgbExpand ? VK_FORMAT_R8G8B8A8_SRGB : vkFormat; viewFormat = viewInfo.format; viewInfo.components = depthCompare ? VkComponentMapping{} : components; @@ -721,11 +722,12 @@ Texture::Texture(const Context& context, const std::shared_ptr& copyElementBytes = BytesPerElement(descriptor.format); AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u depth copy of storage image (vk=%d -> %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, static_cast(colorFormat), static_cast(depthFormat)); + const bool depth1D = descriptor.dimension == TextureDimension::k1D || descriptor.dimension == TextureDimension::k1DArray; VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; imageInfo.flags = descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u; - imageInfo.imageType = ImageTypeFor(descriptor.dimension); + imageInfo.imageType = depth1D ? VK_IMAGE_TYPE_2D : ImageTypeFor(descriptor.dimension); imageInfo.format = depthFormat; - imageInfo.extent = {descriptor.width, descriptor.height, 1u}; + imageInfo.extent = {descriptor.width, depth1D ? 1u : descriptor.height, 1u}; imageInfo.mipLevels = copyLevels; imageInfo.arrayLayers = copyLayers; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; @@ -751,14 +753,16 @@ Texture::Texture(const Context& context, const std::shared_ptr& } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; - viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); + viewInfo.viewType = depth1D ? (descriptor.dimension == TextureDimension::k1DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D) : ViewTypeFor(descriptor.dimension, viewLayerCount); viewInfo.format = depthFormat; viewInfo.components = VkComponentMapping{}; viewInfo.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; ChainMinLod(context, descriptor, viewInfo, minLod); Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth copy"); + viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; createFirstLayerView(descriptor, viewInfo); + prepareBaseLevelView(descriptor, viewInfo); static_cast(components); depthCopyTextures.fetch_add(1, std::memory_order_relaxed); } catch (...) { @@ -1066,7 +1070,7 @@ Texture::~Texture() { } void Texture::createFirstLayerView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo) { - if (descriptor.dimension != TextureDimension::k2DArray) return; + if (descriptor.dimension != TextureDimension::k2DArray && (viewInfo.viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY || descriptor.dimension != TextureDimension::k1DArray)) return; viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; viewInfo.subresourceRange.layerCount = 1; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &firstLayerView), "vkCreateImageView first layer"); @@ -1075,9 +1079,10 @@ void Texture::createFirstLayerView(const GuestTextureResource& descriptor, VkIma void Texture::prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo) { if (viewInfo.subresourceRange.levelCount == 1u) return; - if (descriptor.dimension != TextureDimension::k1D && descriptor.dimension != TextureDimension::k2D && descriptor.dimension != TextureDimension::k2DArray) return; + const bool depth1D = (viewInfo.subresourceRange.aspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) && (descriptor.dimension == TextureDimension::k1D || descriptor.dimension == TextureDimension::k1DArray); + if (!depth1D && descriptor.dimension != TextureDimension::k1D && descriptor.dimension != TextureDimension::k2D && descriptor.dimension != TextureDimension::k2DArray) return; viewInfo.pNext = nullptr; - viewInfo.viewType = descriptor.dimension == TextureDimension::k1D ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D; + viewInfo.viewType = (descriptor.dimension == TextureDimension::k1D && !depth1D) ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D; viewInfo.subresourceRange.levelCount = 1; viewInfo.subresourceRange.layerCount = 1; baseLevelInfo = viewInfo; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 1796f83acb..dd2006bbb9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -284,6 +284,7 @@ CompiledVariant sampleVariant() { image.flatLineCompatible = false; image.flatVolumeLineCompatible = false; image.foldTexelOffsetsOnly = true; + image.remappedDepth1D = true; image.byElements = 4; image.byComponents = 1; image.emulatedCompare = 0x1a07u; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageOneDimensionalCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageOneDimensionalCompare.cpp new file mode 100644 index 0000000000..b119ee834f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageOneDimensionalCompare.cpp @@ -0,0 +1,365 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.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 Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Layers = 3; +constexpr std::uint32_t Format16UNorm = 7; +constexpr std::uint32_t Type1D = 8; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Type1DArray = 12; +constexpr std::uint32_t ReferenceWord = 6; +constexpr std::uint32_t Result = 8; +constexpr std::uint32_t Step1D = 4000; +constexpr std::uint32_t StepArray = 1200; +constexpr std::uint32_t Step2D = 200; +constexpr std::uint32_t Row2D = 2; +constexpr std::uint32_t CompareLess = 1; + +alignas(256) std::array Buffer{}; +alignas(256) std::array Line{}; +alignas(4096) std::array ArrayTexels{}; +alignas(4096) std::array PlaneTexels{}; + +alignas(256) constexpr std::array Code1D_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0xf0bc0100, 0x00610c14, 0xbf8c3f70, 0xe0701020, + 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1D_ImplicitLod{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0xf0a00100, 0x00610c14, 0xbf8c3f70, 0xe0701020, + 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1D_ExplicitLod{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0280, 0xf0b00100, 0x00610c14, 0xbf8c3f70, + 0xe0701020, 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1D_Lz_Offset{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e2802ff, 0x00000f00, 0x7e2a0308, 0x7e2c0302, 0xf0fc0100, 0x00610c14, + 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1DArray_Lz_Offset{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e2802ff, 0x00000f00, 0x7e2a0308, 0x7e2c0302, 0x7e2e0303, 0xf0fc0120, 0x00610c14, + 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1DArray_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0bc0120, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +alignas(256) constexpr std::array Code1DArray_ImplicitLod{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0a00120, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +alignas(256) constexpr std::array Code2D_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0bc0108, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +alignas(256) constexpr std::array Code2D_ImplicitLod{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0a00108, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +alignas(256) constexpr std::array Code1D_Gather_C_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0xf13c0100, 0x00610c14, 0xbf8c3f70, 0xe0701020, + 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1DArray_Gather_C_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf13c0120, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +std::uint32_t TexelOf(std::uint32_t tid) { + return (tid * 7u + 3u) % Size; +} + +bool ReferenceBelow(std::uint32_t tid) { + return (tid & 1u) == 0u; +} + +std::uint32_t Bits(float value) { + return std::bit_cast(value); +} + +void FillInput1D() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const float texel = static_cast((x + 1u) * Step1D) / 65535.0f; + const float half = static_cast(Step1D / 2u) / 65535.0f; + Buffer[tid * Words] = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + ReferenceWord] = Bits(ReferenceBelow(tid) ? texel - half : texel + half); + } +} + +void FillInput1DArray() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const auto layer = (tid / 7u) % Layers; + const float texel = static_cast((layer * Size + x + 1u) * StepArray) / 65535.0f; + const float half = static_cast(StepArray / 2u) / 65535.0f; + Buffer[tid * Words] = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + 1u] = Bits(static_cast(layer)); + Buffer[tid * Words + ReferenceWord] = Bits(ReferenceBelow(tid) ? texel - half : texel + half); + } +} + +void FillInput2D() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const float texel = static_cast((Row2D * Size + x + 1u) * Step2D) / 65535.0f; + const float half = static_cast(Step2D / 2u) / 65535.0f; + Buffer[tid * Words] = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + 1u] = Bits((static_cast(Row2D) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + ReferenceWord] = Bits(ReferenceBelow(tid) ? texel - half : texel + half); + } +} + +void FillInputArrayFirstLayer() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const float texel = static_cast((0u * Size + x + 1u) * StepArray) / 65535.0f; + const float half = static_cast(StepArray / 2u) / 65535.0f; + Buffer[tid * Words] = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + ReferenceWord] = Bits(ReferenceBelow(tid) ? texel - half : texel + half); + } +} + +std::array LineDescriptor() { + const auto address = static_cast(reinterpret_cast(Line.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format16UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + (Size - 1u) >> 2u, + 0xfacu | (Type1D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::array ArrayDescriptor() { + const auto address = static_cast(reinterpret_cast(ArrayTexels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format16UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + (Size - 1u) >> 2u, + 0xfacu | (Type1DArray << 28u), + Layers - 1u, 0u, 0u, 0u, + }; +} + +std::array PlaneDescriptor() { + const auto address = static_cast(reinterpret_cast(PlaneTexels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format16UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +void FillLine() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(LineDescriptor())); + Require(geometry.guestBytes <= Line.size(), "1D texture does not fit texel storage"); + std::fill(Line.begin(), Line.end(), 0xeeu); + for (std::uint32_t x = 0; x < Size; ++x) { + const auto value = static_cast((x + 1u) * Step1D); + auto* texel = &Line[geometry.GuestLayerOffset(0) + geometry.mips.at(0).tiledOffset + x * 2u]; + texel[0] = static_cast(value); + texel[1] = static_cast(value >> 8u); + } +} + +void FillArray() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(ArrayDescriptor())); + Require(geometry.guestBytes <= ArrayTexels.size(), "1D array texture does not fit texel storage"); + std::fill(ArrayTexels.begin(), ArrayTexels.end(), 0xeeu); + for (std::uint32_t layer = 0; layer < Layers; ++layer) { + for (std::uint32_t x = 0; x < Size; ++x) { + const auto value = static_cast((layer * Size + x + 1u) * StepArray); + auto* texel = &ArrayTexels[geometry.GuestLayerOffset(layer) + geometry.mips.at(0).tiledOffset + x * 2u]; + texel[0] = static_cast(value); + texel[1] = static_cast(value >> 8u); + } + } +} + +void FillPlane() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(PlaneDescriptor())); + Require(geometry.guestBytes <= PlaneTexels.size(), "2D texture does not fit texel storage"); + std::fill(PlaneTexels.begin(), PlaneTexels.end(), 0xeeu); + for (std::uint32_t y = 0; y < Size; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + const auto value = static_cast((y * Size + x + 1u) * Step2D); + auto* texel = &PlaneTexels[geometry.GuestLayerOffset(0) + geometry.mips.at(0).tiledOffset + static_cast(y) * geometry.mips.at(0).pitchBytes + x * 2u]; + texel[0] = static_cast(value); + texel[1] = static_cast(value >> 8u); + } + } +} + +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::array SamplerDescriptor() { + return {CompareLess << 12u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::array& textureDesc) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto sampler = SamplerDescriptor(); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(textureDesc.begin(), textureDesc.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + 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(const std::string& op) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float value = std::bit_cast(Buffer[tid * Words + Result]); + const float expected = ReferenceBelow(tid) ? 1.0f : 0.0f; + Require(value == expected, op + ": thread " + std::to_string(tid) + " is " + std::to_string(value) + ", expected " + std::to_string(expected)); + } +} + +void ExpectGatherReject(AgcDriver::VulkanDevice& device, std::span code, const std::array& textureDesc) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(sizeof(Buffer))); + const auto sampler = SamplerDescriptor(); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(textureDesc.begin(), textureDesc.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + 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; + try { + (void)ShaderRecompiler::Recompile(request); + Require(false, "remapped 1D depth gather was accepted unexpectedly"); + } catch (const std::exception& error) { + Require(std::string(error.what()).find("has an unsupported 1D gather variant") != std::string::npos, "unexpected error for 1D depth gather: " + std::string(error.what())); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillLine(); + FillArray(); + FillPlane(); + + FillInput1D(); + Run(*device, Code1D_Lz, LineDescriptor()); + Check("1D image_sample_c_lz"); + + FillInput1D(); + Run(*device, Code1D_ImplicitLod, LineDescriptor()); + Check("1D image_sample_c"); + + FillInput1D(); + Run(*device, Code1D_ExplicitLod, LineDescriptor()); + Check("1D image_sample_c_l"); + + FillInput1D(); + Run(*device, Code1D_Lz_Offset, LineDescriptor()); + Check("1D image_sample_c_lz_o"); + + FillInput1DArray(); + Run(*device, Code1DArray_Lz, ArrayDescriptor()); + Check("1D array image_sample_c_lz"); + + FillInput1DArray(); + Run(*device, Code1DArray_Lz_Offset, ArrayDescriptor()); + Check("1D array image_sample_c_lz_o"); + + FillInput1DArray(); + Run(*device, Code1DArray_ImplicitLod, ArrayDescriptor()); + Check("1D array image_sample_c"); + + FillInputArrayFirstLayer(); + Run(*device, Code1D_Lz, ArrayDescriptor()); + Check("1D image_sample_c_lz on 1D array first layer"); + + FillInputArrayFirstLayer(); + Run(*device, Code1D_ImplicitLod, ArrayDescriptor()); + Check("1D image_sample_c on 1D array first layer"); + + FillInput2D(); + Run(*device, Code2D_Lz, PlaneDescriptor()); + Check("2D image_sample_c_lz"); + + FillInput2D(); + Run(*device, Code2D_ImplicitLod, PlaneDescriptor()); + Check("2D image_sample_c"); + + ExpectGatherReject(*device, Code1D_Gather_C_Lz, LineDescriptor()); + ExpectGatherReject(*device, Code1DArray_Gather_C_Lz, ArrayDescriptor()); + + std::puts("1D, 1D-array, and 2D comparison tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 2ff9d9a15a..307b83cdff 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -98,6 +98,7 @@ struct ImageResource { bool flatVolumeLineCompatible = true; bool foldTexelOffsetsOnly = false; bool convertedSamples = false; + bool remappedDepth1D = false; std::uint32_t byElements = 0; std::uint32_t byComponents = 0; IrBufferFormat packedFormat = IrBufferFormat::Invalid; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 3a9a072113..2ccd9a4039 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -192,6 +192,13 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag return rejectImage(invalidImageReason(descriptor), "image descriptor is invalid"); } decoded.dimension = descriptorDimension(descriptor, base.dimension); + if (base.depthCompare) { + if (decoded.dimension == RdnaImageDimension::Dim1D) { + decoded.dimension = RdnaImageDimension::Dim2D; + } else if (decoded.dimension == RdnaImageDimension::Dim1DArray) { + decoded.dimension = RdnaImageDimension::Dim2DArray; + } + } decoded.cube = descriptorIsCube(descriptor); const auto format = rawImageFormat(descriptor); if (base.atomic64 && format != IrBufferFormat::Format32_32UInt && format != IrBufferFormat::Format32_32SInt && format != IrBufferFormat::Format32_32Float) { @@ -1900,7 +1907,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe plain.dimension = image.dimension == RdnaImageDimension::Dim1DArray ? RdnaImageDimension::Dim1D : image.dimension == RdnaImageDimension::Dim2DArray ? RdnaImageDimension::Dim2D : RdnaImageDimension::Dim2DMsaa; modes.push_back(plain); } - if (image.dimension == RdnaImageDimension::Dim2DArray) { + if (image.dimension == RdnaImageDimension::Dim2DArray && !image.remappedDepth1D) { mode.cube = true; modes.push_back(mode); } @@ -1956,7 +1963,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe mode.packedFormat = IrBufferFormat::Invalid; mode.shaderSwizzle = ShaderImageIdentitySwizzle; modes.push_back(mode); - if (image.dimension == RdnaImageDimension::Dim2DArray) { + if (image.dimension == RdnaImageDimension::Dim2DArray && !image.remappedDepth1D) { auto plain = mode; plain.dimension = RdnaImageDimension::Dim2D; modes.push_back(plain); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index de80e05182..d2967e5231 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -1750,9 +1750,12 @@ private: ImageResource image; image.source = source; image.resourceClass = ImageOpcodeInfoOf(op).resourceClass; - image.dimension = memory.imageDimension; - image.mipMode = image.resourceClass == ImageResourceClass::Storage && memory.imageHasMip ? ImageMipMode::DynamicStorage : ImageMipMode::None; image.depthCompare = (memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0; + image.remappedDepth1D = image.depthCompare && (memory.imageDimension == RdnaImageDimension::Dim1D || memory.imageDimension == RdnaImageDimension::Dim1DArray); + image.dimension = image.remappedDepth1D + ? (memory.imageDimension == RdnaImageDimension::Dim1D ? RdnaImageDimension::Dim2D : RdnaImageDimension::Dim2DArray) + : memory.imageDimension; + image.mipMode = image.resourceClass == ImageResourceClass::Storage && memory.imageHasMip ? ImageMipMode::DynamicStorage : ImageMipMode::None; image.r128 = memory.imageR128; image.packed = memory.imagePacked; image.byElements = memory.imageByElements; @@ -1761,7 +1764,7 @@ private: } static bool SameImageKey(const ImageResource& image, const ImageResource& key) { - return image.source == key.source && image.resourceClass == key.resourceClass && image.dimension == key.dimension && image.mipMode == key.mipMode && image.depthCompare == key.depthCompare && image.r128 == key.r128 && image.packed == key.packed && image.byElements == key.byElements && image.byComponents == key.byComponents; + return image.source == key.source && image.resourceClass == key.resourceClass && image.dimension == key.dimension && image.mipMode == key.mipMode && image.depthCompare == key.depthCompare && image.remappedDepth1D == key.remappedDepth1D && image.r128 == key.r128 && image.packed == key.packed && image.byElements == key.byElements && image.byComponents == key.byComponents; } std::uint32_t AddImage(std::uint32_t source, const MemoryInfo& memory, IrOpcode op, std::uint32_t pc) { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 31815e3fb6..538baaab0b 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -140,7 +140,7 @@ constexpr std::size_t fieldCount() { static_assert(fieldCount() == 23, "ShaderInfo changed: update the info encoder"); static_assert(fieldCount() == 12, "BufferResource changed: update the info encoder"); -static_assert(fieldCount() == 40, "ImageResource changed: update the info encoder"); +static_assert(fieldCount() == 41, "ImageResource changed: update the info encoder"); static_assert(fieldCount() == 8, "SamplerResource changed: update the info encoder"); static_assert(fieldCount() == 3, "SampledResourcePair changed: update the info encoder"); static_assert(fieldCount() == 5, "StageInput changed: update the info encoder"); @@ -742,6 +742,7 @@ void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { out.Value(image.flatVolumeLineCompatible); out.Value(image.foldTexelOffsetsOnly); out.Value(image.convertedSamples); + out.Value(image.remappedDepth1D); out.Value(image.byElements); out.Value(image.byComponents); out.Value(image.packedFormat); @@ -846,6 +847,7 @@ void decodeShaderInfo(Reader& reader, ShaderInfo& info) { in.Value(image.flatVolumeLineCompatible); in.Value(image.foldTexelOffsetsOnly); in.Value(image.convertedSamples); + in.Value(image.remappedDepth1D); in.Value(image.byElements); in.Value(image.byComponents); in.Value(image.packedFormat); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 5543565717..5d721bb300 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 41; +inline constexpr std::uint32_t FormatVersion = 42; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 57a3c77d68..67fb35b327 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -193,9 +193,12 @@ std::uint32_t CoordF32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access x = CubeAxis(ctx.state, x); y = CubeAxis(ctx.state, y); } + if (!gradient && access.image.depthCompare && (access.image.remappedDepth1D || access.mem.imageDimension == RdnaImageDimension::Dim1D || access.mem.imageDimension == RdnaImageDimension::Dim1DArray)) { + y = ConstantF32(ctx.state, 0x3f000000u); + } const auto result = ctx.state.module.AllocateId(); if (components == 3u) { - auto z = component(2u); + auto z = (access.image.depthCompare && (access.mem.imageDimension == RdnaImageDimension::Dim1DArray || (access.image.remappedDepth1D && access.image.dimension == RdnaImageDimension::Dim2DArray))) ? component(1u) : component(2u); if (cube) { z = CubeLayer(ctx.state, z); } @@ -467,6 +470,9 @@ std::uint32_t PackedOffset(SpirvValueEmitContext& ctx, const ImageEmitAccess& ac values[index] = state.module.AllocateId(); state.module.AddFunction(spv::OpBitFieldSExtract, TypeI32(state), values[index], packed, ConstantU32(state, index * 8u), ConstantU32(state, 6)); } + if (access.image.remappedDepth1D && components >= 2u) { + values[1] = ConstantI32(state, 0); + } if (components == 1u) { return values[0]; } @@ -1197,8 +1203,8 @@ void EmitGatherOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con if (setup.dref && (access.image.emulatedCompare & EmulatedCompare::Enabled) != 0u) { ctx.Fail(access.inst, "is a comparison gather of a color texture, which is not implemented"); } - if (dimension == RdnaImageDimension::Dim1D || dimension == RdnaImageDimension::Dim1DArray) { - if (setup.dref || !HasFlag(mem, RdnaImageSampleFlagLevelZero) || HasFlag(mem, RdnaImageSampleFlagGatherHorizontal)) { + if (dimension == RdnaImageDimension::Dim1D || dimension == RdnaImageDimension::Dim1DArray || access.image.remappedDepth1D) { + if (setup.dref || !HasFlag(mem, RdnaImageSampleFlagLevelZero) || HasFlag(mem, RdnaImageSampleFlagGatherHorizontal) || access.image.remappedDepth1D) { ctx.Fail(access.inst, "has an unsupported 1D gather variant"); } const auto sample = EmitOneDimensionalGatherLz(ctx, access, setup); @@ -1483,6 +1489,9 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con const auto field = (bits >> (index * 8u)) & 0x3fu; values[index] = ConstantI32(state, static_cast(field ^ 0x20u) - 0x20); } + if (access.image.remappedDepth1D && setup.dimensionInfo.spatialComponents >= 2u) { + values[1] = ConstantI32(state, 0); + } const auto count = setup.dimensionInfo.spatialComponents; const auto offset = count == 1u ? values[0] : count == 2u ? state.module.Constant(spv::OpConstantComposite, TypeI32Vector(state, 2), values[0], values[1]) From 251ca1c1008b21a41889246fd7ea9420cc1fb632 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Mon, 19 Oct 2026 17:12:01 -0500 Subject: [PATCH 110/219] fix(libc): report the file number of each readdir entry on Windows readdir and readdir_r returned d_fileno 0 for every entry on Windows, because MinGW's _wreaddir reports no inode. FreeBSD 11.4's _readdir_unlocked (lib/libc/gen/readdir.c) never returns an entry whose d_ino is 0, and d_fileno is the number lstat reports as st_ino. The Windows directory stream now reads the listing from the directory handle with GetFileInformationByHandleEx(FileIdBothDirectoryInfo), so each entry carries the file ID of the entry itself, symbolic links included, without opening it. The ID is folded as stat and lstat fold nFileIndexHigh:nFileIndexLow; the fold moves to WindowsDescriptor.hpp so both libraries use it. rewinddir restarts the scan, telldir and seekdir count entries as _wtelldir and _wseekdir did, and opendir fails with the open error of the directory. A file system that lists no file ID gets a nonzero number hashed from the name. Linux truncates the host inode to 32 bits as stat does instead of reporting 0 above 2^32. guest_directory checks that every entry has a nonzero file number, that the numbers of a file, a subdirectory and symbolic links to both equal lstat's st_ino, that an entry created or removed while the directory is open shows up after rewinddir, that a delete-pending entry no lstat can open is still listed, and that seekdir returns the same number. --- core/libs/prx/libc/CMakeLists.txt | 2 +- .../prx/libc/include/WindowsDescriptor.hpp | 9 ++ core/libs/prx/libc/src/Directory.cpp | 122 +++++++++++++++--- .../prx/libkernel/File/src/NativeStat.cpp | 6 +- core/libs/tests/GuestDirectory.cpp | 69 ++++++++++ docs/dev/debt/libc.md | 1 + 6 files changed, 187 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index ca39154516..36d88ef6ec 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -422,7 +422,7 @@ if(BUILD_TESTING) endif() add_test_executable(guest_directory_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectory.cpp) target_include_directories(guest_directory_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_directory_tests PRIVATE libc) + target_link_libraries(guest_directory_tests PRIVATE libc libkernel) configure_windows_unwind(guest_directory_tests) add_test(NAME guest_directory COMMAND guest_directory_tests) if(WIN32) diff --git a/core/libs/prx/libc/include/WindowsDescriptor.hpp b/core/libs/prx/libc/include/WindowsDescriptor.hpp index d8ac9209ca..66ebc1d9ac 100644 --- a/core/libs/prx/libc/include/WindowsDescriptor.hpp +++ b/core/libs/prx/libc/include/WindowsDescriptor.hpp @@ -28,6 +28,15 @@ static inline ACCESS_MASK WindowsHandleAccess_nid_no_patch(HANDLE handle, int de return information.GrantedAccess; } +static inline std::uint32_t WindowsFileNumber_nid_no_patch(std::uint64_t fileId) { + const std::uint32_t folded = static_cast(fileId) ^ (static_cast(fileId >> 32) * 0x9E3779B1u); + return folded == 0 ? 1 : folded; +} + +static inline std::uint32_t WindowsFileNumber_nid_no_patch(const BY_HANDLE_FILE_INFORMATION& identity) { + return WindowsFileNumber_nid_no_patch((static_cast(identity.nFileIndexHigh) << 32) | identity.nFileIndexLow); +} + static inline bool WindowsHandleOnlyAppends_nid_no_patch(ACCESS_MASK access) { return (access & (FILE_WRITE_DATA | FILE_APPEND_DATA)) == FILE_APPEND_DATA; } diff --git a/core/libs/prx/libc/src/Directory.cpp b/core/libs/prx/libc/src/Directory.cpp index 2ea8cc0946..425669cdcf 100644 --- a/core/libs/prx/libc/src/Directory.cpp +++ b/core/libs/prx/libc/src/Directory.cpp @@ -9,6 +9,12 @@ #include #include #include +#include +#ifdef _WIN32 +#include "prx/libc/include/WindowsDescriptor.hpp" +#include +#include +#endif namespace { int DirectoryError(int error) { @@ -28,19 +34,102 @@ int DirectoryError(int error) { } } #ifdef _WIN32 -using NativeDirectory = _WDIR; -NativeDirectory* OpenNative(const std::filesystem::path& path) { return ::_wopendir(path.c_str()); } -int CloseNative(NativeDirectory* native) { return ::_wclosedir(native); } -void RewindNative(NativeDirectory* native) { ::_wrewinddir(native); } -long TellNative(NativeDirectory* native) { return ::_wtelldir(native); } -void SeekNative(NativeDirectory* native, long location) { ::_wseekdir(native, location); } +struct NativeDirectory { + HANDLE handle = INVALID_HANDLE_VALUE; + std::uint32_t seed = 0; + bool identifiers = true; + bool restart = true; + bool buffered = false; + std::size_t next = 0; + long position = 0; + alignas(8) std::byte buffer[65536]; + ~NativeDirectory() { if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle); } +}; +int NativeOpenError(DWORD error) { + if (error == ERROR_CANT_RESOLVE_FILENAME) return ELOOP; + if (error == ERROR_DIRECTORY) return ENOTDIR; + return WindowsOpenError_nid_no_patch(error); +} +NativeDirectory* OpenNative(const std::filesystem::path& path) { + const DWORD attributes = GetFileAttributesW(path.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES) { errno = NativeOpenError(GetLastError()); return nullptr; } + if ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { errno = ENOTDIR; return nullptr; } + auto native = std::make_unique_for_overwrite(); + native->handle = CreateFileW(path.c_str(), FILE_LIST_DIRECTORY | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (native->handle == INVALID_HANDLE_VALUE) { errno = NativeOpenError(GetLastError()); return nullptr; } + BY_HANDLE_FILE_INFORMATION identity{}; + if (GetFileInformationByHandle(native->handle, &identity)) { + if ((identity.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { errno = ENOTDIR; return nullptr; } + native->seed = WindowsFileNumber_nid_no_patch(identity); + } + return native.release(); +} +int CloseNative(NativeDirectory* native) { delete native; return 0; } +void RewindNative(NativeDirectory* native) { + native->restart = true; + native->buffered = false; + native->position = 0; +} +bool QueryNative(NativeDirectory* native) { + const auto query = [native](FILE_INFO_BY_HANDLE_CLASS restartClass, FILE_INFO_BY_HANDLE_CLASS nextClass) { + return GetFileInformationByHandleEx(native->handle, native->restart ? restartClass : nextClass, native->buffer, sizeof(native->buffer)) != 0; + }; + bool filled = native->identifiers ? query(FileIdBothDirectoryRestartInfo, FileIdBothDirectoryInfo) : query(FileFullDirectoryRestartInfo, FileFullDirectoryInfo); + DWORD error = filled ? ERROR_SUCCESS : GetLastError(); + if (!filled && native->restart && native->identifiers && (error == ERROR_INVALID_PARAMETER || error == ERROR_INVALID_FUNCTION || + error == ERROR_INVALID_LEVEL || error == ERROR_NOT_SUPPORTED)) { + native->identifiers = false; + filled = query(FileFullDirectoryRestartInfo, FileFullDirectoryInfo); + error = filled ? ERROR_SUCCESS : GetLastError(); + } + if (!filled) { + if (error != ERROR_NO_MORE_FILES && error != ERROR_FILE_NOT_FOUND) errno = EIO; + return false; + } + native->restart = false; + native->buffered = true; + native->next = 0; + return true; +} +const std::byte* NextRecord(NativeDirectory* native) { + if (!native->buffered && !QueryNative(native)) return nullptr; + const std::byte* record = native->buffer + native->next; + const DWORD offset = native->identifiers ? reinterpret_cast(record)->NextEntryOffset + : reinterpret_cast(record)->NextEntryOffset; + if (offset == 0) native->buffered = false; + native->next += offset; + ++native->position; + return record; +} +std::uint32_t StableFileNumber(std::uint32_t seed, std::wstring_view name) { + std::uint32_t hash = 2166136261u ^ seed; + for (const wchar_t character : name) hash = (hash ^ static_cast(character)) * 16777619u; + return hash == 0 ? 1 : hash; +} +long TellNative(NativeDirectory* native) { return native->position; } +void SeekNative(NativeDirectory* native, long location) { + if (location < 0) return; + RewindNative(native); + while (native->position < location && NextRecord(native)) {} +} bool ReadNative(NativeDirectory* native, std::filesystem::path& component, std::string& name, std::uint64_t& inode) { - auto* entry = ::_wreaddir(native); - if (!entry) return false; - component = entry->d_name; + const std::byte* record = NextRecord(native); + if (!record) return false; + std::wstring_view fileName; + std::uint64_t fileId = 0; + if (native->identifiers) { + const auto* information = reinterpret_cast(record); + fileName = std::wstring_view(information->FileName, information->FileNameLength / sizeof(WCHAR)); + fileId = static_cast(information->FileId.QuadPart); + } else { + const auto* information = reinterpret_cast(record); + fileName = std::wstring_view(information->FileName, information->FileNameLength / sizeof(WCHAR)); + } + component = fileName; const auto utf8 = component.u8string(); name.assign(utf8.begin(), utf8.end()); - inode = static_cast(entry->d_ino); + inode = fileId != 0 ? WindowsFileNumber_nid_no_patch(fileId) : StableFileNumber(native->seed, fileName); return true; } #else @@ -87,16 +176,17 @@ GuestDirectoryEntry* ReadEntry(Directory& directory) { std::filesystem::path component; std::string name; std::uint64_t inode = 0; - if (!ReadNative(directory.native, component, name, inode)) { - errno = errno ? DirectoryError(errno) : savedError; - return nullptr; - } + try { + if (!ReadNative(directory.native, component, name, inode)) { + errno = errno ? DirectoryError(errno) : savedError; + return nullptr; + } + } catch (const std::bad_alloc&) { errno = 12; return nullptr; } const auto length = name.size(); if (length > 255) { errno = 63; return nullptr; } try { directory.entry = {}; - // MinGW reports no inode identity (zero); do not fabricate one. - directory.entry.fileNumber = inode <= UINT32_MAX ? static_cast(inode) : 0; + directory.entry.fileNumber = static_cast(inode); directory.entry.recordLength = static_cast(8 + ((length + 1 + 3) & ~3)); directory.entry.nameLength = static_cast(length); directory.entry.type = DirectoryType(directory.path / component); diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 3edfb3774b..75e7d82619 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -30,10 +30,6 @@ static int ReadHandleInfo(HANDLE handle, NativeStat* st) { st->hasIdentity = GetFileInformationByHandle(handle, &st->identity) != 0; return 0; } -static std::uint32_t FoldFileIndex(const BY_HANDLE_FILE_INFORMATION& identity) { - const std::uint32_t folded = identity.nFileIndexLow ^ (identity.nFileIndexHigh * 0x9E3779B1u); - return folded == 0 ? 1 : folded; -} static int DoStat(const std::filesystem::path& p, NativeStat* st) { if (_wstat64(p.c_str(), st) != 0) { if (_doserrno == ERROR_CANT_RESOLVE_FILENAME) File::WindowsFileTime::Failure(_doserrno); @@ -115,7 +111,7 @@ static void CopyNativeStat(const NativeStat& st, FileStat* sb) { } if (st.hasIdentity) { sb->st_dev = st.identity.dwVolumeSerialNumber; - sb->st_ino = FoldFileIndex(st.identity); + sb->st_ino = WindowsFileNumber_nid_no_patch(st.identity); sb->st_nlink = static_cast(std::min(st.identity.nNumberOfLinks, 0xffff)); } #else diff --git a/core/libs/tests/GuestDirectory.cpp b/core/libs/tests/GuestDirectory.cpp index 5c3e5dd0cc..4df4394622 100644 --- a/core/libs/tests/GuestDirectory.cpp +++ b/core/libs/tests/GuestDirectory.cpp @@ -1,7 +1,9 @@ #include "prx/libc/include/GuestDirectory.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" #include #include +#include #include #include #include @@ -9,6 +11,10 @@ #include #include #include +#include +#ifdef _WIN32 +#include +#endif extern "C" { void* APS5_VABI opendir_nid_postfix(const char*); GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void*); @@ -18,25 +24,83 @@ void APS5_VABI rewinddir_nid_postfix(void*); std::int64_t APS5_VABI telldir_nid_postfix(void*); void APS5_VABI seekdir_nid_postfix(void*, std::int64_t); int* APS5_VABI __error_nid_postfix(); +int APS5_VABI lstat_nid_postfix(const char*, FileStat*); } static void Require(bool value) { if (!value) std::abort(); } +static std::map ReadFileNumbers(void* directory) { + std::map numbers; + rewinddir_nid_postfix(directory); + while (auto* entry = readdir_nid_postfix(directory)) { + Require(entry->fileNumber != 0); + numbers[entry->name] = entry->fileNumber; + } + return numbers; +} +static std::uint32_t FileNumber(const std::filesystem::path& path) { + FileStat status{}; + Require(lstat_nid_postfix(path.string().c_str(), &status) == 0); + return static_cast(status.st_ino); +} +static bool CreateLinks(const std::filesystem::path& root) { +#ifdef _WIN32 + if (CreateSymbolicLinkW((root / "file-link").c_str(), L"sample.txt", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { + Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD); + std::fprintf(stderr, "Windows readdir link checks skipped: creating symbolic links needs Developer Mode or administrator rights\n"); + return false; + } + Require(CreateSymbolicLinkW((root / "directory-link").c_str(), L"subdirectory", + SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); +#else + std::filesystem::create_symlink("sample.txt", root / "file-link"); + std::filesystem::create_directory_symlink("subdirectory", root / "directory-link"); +#endif + return true; +} int main() { const auto root = std::filesystem::path("anyps5-directory-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); Require(std::filesystem::create_directory(root / "subdirectory")); { std::ofstream file(root / "sample.txt"); file << "test"; } + const bool links = CreateLinks(root); + std::vector numbered{"sample.txt", "subdirectory"}; + if (links) numbered.insert(numbered.end(), {"file-link", "directory-link"}); void* directory = opendir_nid_postfix(root.string().c_str()); Require(directory != nullptr); std::map entries; + std::map fileNumbers; *__error_nid_postfix() = 13; while (auto* entry = readdir_nid_postfix(directory)) { Require(entry->nameLength == std::strlen(entry->name)); Require(entry->recordLength == 8 + ((entry->nameLength + 4) & ~3)); Require(entry->recordLength <= sizeof(*entry)); entries[entry->name] = entry->type; + Require(entry->fileNumber != 0); + fileNumbers[entry->name] = entry->fileNumber; } Require(*__error_nid_postfix() == 13); // EOF preserves errno. + for (const char* name : numbered) Require(fileNumbers.at(name) == FileNumber(root / name)); + if (links) Require(fileNumbers.at("file-link") != fileNumbers.at("sample.txt")); + { std::ofstream file(root / "created.txt"); file << "new"; } + auto changed = ReadFileNumbers(directory); + Require(changed.at("created.txt") == FileNumber(root / "created.txt")); + Require(changed.at("sample.txt") == fileNumbers.at("sample.txt")); + Require(std::filesystem::remove(root / "created.txt")); + changed = ReadFileNumbers(directory); + Require(changed.count("created.txt") == 0 && changed == fileNumbers); +#ifdef _WIN32 + const HANDLE pending = CreateFileW((root / "pending.txt").c_str(), GENERIC_WRITE | DELETE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, CREATE_NEW, 0, nullptr); + Require(pending != INVALID_HANDLE_VALUE); + FILE_DISPOSITION_INFO disposition{TRUE}; + Require(SetFileInformationByHandle(pending, FileDispositionInfo, &disposition, sizeof(disposition)) != 0); + FileStat pendingStatus{}; + Require(lstat_nid_postfix((root / "pending.txt").string().c_str(), &pendingStatus) == -1); + changed = ReadFileNumbers(directory); + Require(changed.count("pending.txt") == 1 && changed.at("sample.txt") == fileNumbers.at("sample.txt")); + Require(CloseHandle(pending) != 0); + Require(ReadFileNumbers(directory) == fileNumbers); +#endif Require(entries.at("sample.txt") == 8); Require(entries.at("subdirectory") == 4); Require(entries.at(".") == 4 && entries.at("..") == 4); @@ -99,6 +163,7 @@ int main() { seekdir_nid_postfix(directory, second); auto* entryAfterSeek = readdir_nid_postfix(directory); Require(entryAfterSeek != nullptr && secondName == entryAfterSeek->name); + Require(entryAfterSeek->fileNumber == fileNumbers.at(secondName)); *__error_nid_postfix() = 0; Require(telldir_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == 9); *__error_nid_postfix() = 0; @@ -133,6 +198,10 @@ int main() { Require(names.at(std::string(file.begin(), file.end())) == 8 && names.size() == 3); Require(closedir_nid_postfix(unicode) == 0); std::filesystem::remove_all(root / std::filesystem::path(folder)); + if (links) { + std::filesystem::remove(root / "file-link"); + std::filesystem::remove(root / "directory-link"); + } std::filesystem::remove(root / "sample.txt"); std::filesystem::remove(root / "subdirectory"); std::filesystem::remove(root); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e06d2e9b22..31265e8624 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -91,6 +91,7 @@ - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync` - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way - [`fopen` streams](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access. On Windows, opening a directory in a read mode fails with `EACCES`, where FreeBSD opens it; directory streams are not emulated +- [readdir and readdir_r](../../../core/libs/prx/libc/src/Directory.cpp) (libc) on Windows report, on a file system whose directory listing carries no file ID (the entry's ID is 0, or the volume refuses `FileIdBothDirectoryInfo`), a nonzero `d_fileno` hashed from the entry name and the directory's file index, which differs from `lstat`'s `st_ino`. Not reproduced: NTFS lists an ID for every entry - [`fopen` of /dev/random and /dev/urandom](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) in a read mode returns a stream over the host null device whose `fread` fills the buffer with host OS entropy, as the libkernel [random device](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) descriptors do. Only the exact paths are recognised, the stream has no descriptor, `setvbuf`, `feof`, `ferror`, `clearerr`, `flockfile`, `ftrylockfile`, `funlockfile` and `fclose` work on it, `fflush` returns 0 as on a FreeBSD read-only stream, and every other stream function throws, as does opening the paths for writing. `freopen` onto these paths still fails with `ENOENT`. ProsperoTV (PPSA99003) reads its phone pairing code this way - [RemoveTree](../../../core/libs/prx/libc/include/RemoveTree.hpp) (libc) removes a host tree itself on Windows instead of calling `std::filesystem::remove_all`: MinGW libstdc++ up to GCC 16 does not treat a reparse point as a link, so `remove_all` recurses through it and never terminates, and any tree that holds a symlink hangs. It builds the entry list from `GetFileAttributesW`, removes a reparse point as the link it is rather than its target, clears the read-only attribute and retries as `remove_all` does, and reports the first native error. The libstdc++ fix is in GCC 17 (commit 88856ad855ae); other hosts call `remove_all`. - The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked From 4313a703ecfd9674d5ae46dd2a2823bb6e18f624 Mon Sep 17 00:00:00 2001 From: Aryan Sharma Date: Tue, 20 Oct 2026 12:38:29 +0530 Subject: [PATCH 111/219] feat(libkernel): implement _umtx_op UMTX_OP_SEM_WAIT and UMTX_OP_SEM_WAKE Follow FreeBSD 11's do_sem_wait and do_sem_wake (COMPAT_FREEBSD9/10) on a struct _usem: a wait sets _has_waiters, returns at once while _count is non-zero and otherwise sleeps on the semaphore's queue; a wake clears _has_waiters when one waiter is left and releases one waiter. Co-Authored-By: Claude Haiku 5.5 --- .../SyncOnAddress/src/SyncOnAddress.cpp | 82 +++++++++++++++++-- core/libs/tests/GuestUmtxOp.cpp | 61 ++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 137 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp index 710d0bc069..3955668c7f 100644 --- a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp +++ b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp @@ -27,6 +27,8 @@ constexpr int UMTX_OP_WAKE = 3; constexpr int UMTX_OP_WAIT_UINT = 11; constexpr int UMTX_OP_WAIT_UINT_PRIVATE = 15; constexpr int UMTX_OP_WAKE_PRIVATE = 16; +constexpr int UMTX_OP_SEM_WAIT = 19; +constexpr int UMTX_OP_SEM_WAKE = 20; constexpr int UMTX_OP_NWAKE_PRIVATE = 21; constexpr int UMTX_OP_MAX = 27; constexpr std::uint32_t UMTX_ABSTIME = 1; @@ -46,6 +48,12 @@ struct UmtxTime { std::uint32_t clockId; }; +struct UserSemaphore { + std::uint32_t hasWaiters; + std::uint32_t count; + std::uint32_t flags; +}; + struct AddressWaiter { TimedWait::Condition condition; bool woken = false; @@ -72,11 +80,10 @@ void DeferringDelivery(TFunction function) { #endif } -template -std::optional WaitOnAddressLocked(TValue* address, TValue expected, std::optional deadlineNanos, bool& woken) { - const auto key = reinterpret_cast(address); +template +std::optional WaitOnKeyLocked(std::uintptr_t key, TReady ready, std::optional deadlineNanos, bool& woken) { std::unique_lock lock(g_waitersLock); - if (std::atomic_ref(*address).load() != expected) { + if (ready()) { woken = true; return std::nullopt; } @@ -103,17 +110,22 @@ std::optional WaitOnAddressLocked(TValue* address, TVa return waited; } -template -bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller) { +template +bool WaitOnKeyUntil(std::uintptr_t key, TReady ready, std::optional deadlineNanos, const void* caller) { bool woken = false; std::optional waited; - DeferringDelivery([&] { waited = WaitOnAddressLocked(address, expected, deadlineNanos, woken); }); + DeferringDelivery([&] { waited = WaitOnKeyLocked(key, ready, deadlineNanos, woken); }); if (waited) { KernelTraceWait_nid_postfix("addr", caller, static_cast(waited->count()), !woken); } return woken; } +template +bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller) { + return WaitOnKeyUntil(reinterpret_cast(address), [&] { return std::atomic_ref(*address).load() != expected; }, deadlineNanos, caller); +} + template int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeout, const void* caller) { const auto deadline = timeout == nullptr ? std::nullopt : std::optional(TimedWait::DeadlineNanos(*timeout)); @@ -228,6 +240,56 @@ int UmtxNwakePrivate(void* object, std::uint64_t value) { return 0; } +void WakeSemaphoreLocked(std::uintptr_t key, UserSemaphore* semaphore) { + std::lock_guard lock(g_waitersLock); + const auto entry = g_waiters.find(key); + if (entry == g_waiters.end()) { + return; + } + auto& queue = entry->second; + if (queue.size() == 1) { + std::atomic_ref(semaphore->hasWaiters).store(0); + } + AddressWaiter* waiter = queue.front(); + queue.pop_front(); + waiter->woken = true; + waiter->condition.NotifyOne(); + if (queue.empty()) { + g_waiters.erase(entry); + } +} + +int SemWait(UserSemaphore* semaphore, const void* sizeArgument, const void* timeArgument, const void* caller) { + std::optional deadline; + if (const int error = UmtxDeadline(sizeArgument, timeArgument, deadline); error != 0) { + return error; + } + if (semaphore == nullptr) { + return GUEST_EFAULT; + } + if (!IsAlignedAddress(reinterpret_cast(semaphore))) { + APS5_INVALID_ARG_EX; + } + const bool woken = WaitOnKeyUntil(reinterpret_cast(semaphore), [&] { + std::uint32_t idle = 0; + std::atomic_ref(semaphore->hasWaiters).compare_exchange_strong(idle, 1); + return std::atomic_ref(semaphore->count).load() != 0; + }, deadline, caller); + return woken ? 0 : GUEST_ETIMEDOUT; +} + +int SemWake(UserSemaphore* semaphore) { + if (semaphore == nullptr) { + return GUEST_EFAULT; + } + if (!IsAlignedAddress(reinterpret_cast(semaphore))) { + APS5_INVALID_ARG_EX; + } + const auto key = reinterpret_cast(semaphore); + DeferringDelivery([&] { WakeSemaphoreLocked(key, semaphore); }); + return 0; +} + } // namespace extern "C" { @@ -300,6 +362,12 @@ int APS5_VABI _umtx_op_nid_postfix(void* object, int operation, std::uint64_t va case UMTX_OP_NWAKE_PRIVATE: error = UmtxNwakePrivate(object, value); break; + case UMTX_OP_SEM_WAIT: + error = SemWait(static_cast(object), sizeArgument, timeArgument, caller); + break; + case UMTX_OP_SEM_WAKE: + error = SemWake(static_cast(object)); + break; default: if (operation < 0 || operation >= UMTX_OP_MAX) { error = GUEST_EINVAL; diff --git a/core/libs/tests/GuestUmtxOp.cpp b/core/libs/tests/GuestUmtxOp.cpp index ada58582be..97eef1d837 100644 --- a/core/libs/tests/GuestUmtxOp.cpp +++ b/core/libs/tests/GuestUmtxOp.cpp @@ -20,6 +20,8 @@ static constexpr int UMTX_OP_MUTEX_LOCK = 5; static constexpr int UMTX_OP_WAIT_UINT = 11; static constexpr int UMTX_OP_WAIT_UINT_PRIVATE = 15; static constexpr int UMTX_OP_WAKE_PRIVATE = 16; +static constexpr int UMTX_OP_SEM_WAIT = 19; +static constexpr int UMTX_OP_SEM_WAKE = 20; static constexpr int UMTX_OP_NWAKE_PRIVATE = 21; static constexpr int UMTX_OP_SEM2_WAIT = 23; static constexpr int UMTX_OP_MAX = 27; @@ -270,6 +272,61 @@ static void NwakeReadsOnlyTheCountedEntries() { Require(Umtx(addresses, UMTX_OP_NWAKE_PRIVATE, 1) == 0); } +struct GuestSemaphore { + std::uint32_t hasWaiters; + std::uint32_t count; + std::uint32_t flags; +}; + +static void SemWaitReturnsAtOnceWhileTheCountIsNonzero() { + GuestSemaphore sem{0, 2, 0}; + KernelTimespec none{0, 0}; + Require(Umtx(&sem, UMTX_OP_SEM_WAIT, 0, SizeOf(sizeof(none)), &none) == 0); + Require(sem.hasWaiters == 1); + Require(sem.count == 2); +} + +static void SemWaitTimesOutWhileTheCountIsZero() { + GuestSemaphore sem{0, 0, 0}; + KernelTimespec relative{0, 20000000}; + auto start = std::chrono::steady_clock::now(); + Require(FailsWith(Umtx(&sem, UMTX_OP_SEM_WAIT, 0, SizeOf(sizeof(relative)), &relative), GUEST_ETIMEDOUT)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); + Require(sem.hasWaiters == 1); +} + +static void SemWakeReleasesTheWaiterAndClearsTheFlag() { + GuestSemaphore sem{0, 0, 0}; + std::atomic finished{0}; + int result = -2; + std::thread waiter([&] { + KernelTimespec failsafe = FailsafeTimeout(); + result = Umtx(&sem, UMTX_OP_SEM_WAIT, 0, SizeOf(sizeof(failsafe)), &failsafe); + ++finished; + }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (std::atomic_ref(sem.hasWaiters).load() == 0) { + Require(std::chrono::steady_clock::now() < deadline); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + std::this_thread::sleep_for(SETTLE); + Require(finished.load() == 0); + + std::atomic_ref(sem.count).store(1); + Require(Umtx(&sem, UMTX_OP_SEM_WAKE, 0) == 0); + waiter.join(); + Require(result == 0); + Require(sem.hasWaiters == 0); +} + +static void SemWakeWithoutWaitersSucceeds() { + GuestSemaphore sem{0, 0, 0}; + Require(Umtx(&sem, UMTX_OP_SEM_WAKE, 0) == 0); + Require(sem.hasWaiters == 0); + Require(FailsWith(Umtx(nullptr, UMTX_OP_SEM_WAKE, 0), GUEST_EFAULT)); + Require(FailsWith(Umtx(nullptr, UMTX_OP_SEM_WAIT, 0), GUEST_EFAULT)); +} + static void RejectsBadObjectsAndOperations() { Require(FailsWith(Umtx(nullptr, UMTX_OP_WAIT, 0), GUEST_EFAULT)); Require(FailsWith(Umtx(nullptr, UMTX_OP_WAIT_UINT, 0), GUEST_EFAULT)); @@ -308,5 +365,9 @@ int main() { WakeWithoutWaitersSucceeds(); NwakeWakesEveryWaiterOnEachListedAddress(); NwakeReadsOnlyTheCountedEntries(); + SemWaitReturnsAtOnceWhileTheCountIsNonzero(); + SemWaitTimesOutWhileTheCountIsZero(); + SemWakeReleasesTheWaiterAndClearsTheFlag(); + SemWakeWithoutWaitersSucceeds(); RejectsBadObjectsAndOperations(); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 9d2edf5790..dfa8d86712 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -53,7 +53,7 @@ - [Memory protection bits 0x100 and 0x200](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits - [Protecting a host thread's stack](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - on Windows `sceKernelIsStack` reports a host thread's whole stack reservation, which Windows commits on demand from the top down behind a guard page. A protect over that range changes only the pages above the guard; the guard page and the reserved pages below it are left as they are, so the stack still grows and an overflow still raises a stack overflow. A protect that starts at the bottom of the reservation and stops short of the guard page, as HotSpot makes its stack guard zones, is committed and protected. Windows moves the guard page down by committing the pages below it as read/write guard pages, whether they were reserved or committed, so those pages come in read/write as the stack grows to within one guard region of them (three pages on GitHub's runners). A protection other than read/write on them, `PROT_NONE` included, does not hold once the stack is that close, where on the console every page of the reported range takes the protection. Holding it would mean removing the thread's guard page and keeping the stack limit in its TEB by hand. On Linux the reported range of the main thread is mostly unmapped; the unmapped pages from the bottom of a protect up to the top of the stack are mapped read/write in place, then the protect is applied, so the stack never has to grow down to the protected pages, which Linux refuses within its stack guard gap (1 MiB by default) above a readable mapping. The pages below a protect stay unmapped and out of the stack's reach, where on the console the stack goes on below them - [Foreign guest module image protection on Windows](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMprotect` on Windows allows protection mutations on mapped PE images containing a `.elf0` section (as relinked guest modules produce, such as NVIDIA Real-time Denoisers `libNRD.prx` bundled in Dying Light 2: Stay Human PPSA02262), registering their sections into `GuestAllocations`. Host libraries and system DLLs (such as `libkernel.prx` and `kernel32.dll`) without a `.elf0` section continue to be rejected -- [_umtx_op](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE` and `UMTX_OP_NWAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. `UMTX_OP_NWAKE_PRIVATE` reads the value as an `int` count of pointers at the object and wakes every waiter on each one; a count of 0 or fewer reads nothing, and a null list fails with `EFAULT`, where FreeBSD's `copyin` would also fail for any other unreadable list. Waiters share the queue of `sceKernelSyncOnAddressWait`. A contended operation 0 or 1 was not measured; uncontended, both fail with `EOPNOTSUPP` and leave the word unchanged, as FreeBSD 11's [`__umtx_op_unimpl`](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c#L3349-L3353) does and as a payload process on a PS5 (firmware 12.70) answered operation 1 for four word values and operation 0 for a word of 0. Operations below 0 or above 26 fail with `EINVAL`, as in FreeBSD 11. `UMTX_OP_WAIT_UINT` with a value that does not fit in 32 bits returns 0 at once, as FreeBSD's zero-extended compare does. The other operations throw, and so does a misaligned object. The same payload process answered `UMTX_OP_SEM_WAIT` (19) and `UMTX_OP_SEM_WAKE` (20) as FreeBSD 11's [`do_sem_wait` and `do_sem_wake`](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c#L3100-L3192), built under `COMPAT_FREEBSD9` or `COMPAT_FREEBSD10` ([op_table](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c#L4015-L4021)), do: 0 for a count of 1 with `_has_waiters` set, `ETIMEDOUT` for a count of 0 with a 100 ms relative timeout, and 0 for a wake without a waiter; it answered `UMTX_OP_SEM2_WAIT` (23) with 0, not `ETIMEDOUT`, for a count of 0 with the same timeout, leaving the count 0 where FreeBSD 11 would set `USEM_HAS_WAITERS`, and its log ends before the result of `UMTX_OP_SEM2_WAKE` (24) without a waiter. PPSA12544's `libc.prx` and Unity's `PS5Util.prx` (PPSA18194) pass `UMTX_OP_WAIT` and `UMTX_OP_WAIT_UINT` a pointer to a 4-byte microsecond timeout with size argument 0, which FreeBSD reads as a `timespec`, and wait for a running call_once with operation 1; what those callers do with the `EOPNOTSUPP` was not run, and whether the console reads a microsecond timeout there in all cases is unverified. A call with size argument 0 is read unconditionally as a 4-byte microsecond timeout, matching callers like `libc.prx` and `PS5Util.prx`'s `zlqfTyrQSPk` wrapper which pass size 0 with a 32-bit microsecond count +- [_umtx_op](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the mutex, condition variable, read-write lock, legacy lock and `SEM2` operations throw, and so does a misaligned object. Implemented as FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE`, `UMTX_OP_NWAKE_PRIVATE`, `UMTX_OP_SEM_WAIT` and `UMTX_OP_SEM_WAKE` (the last two built under `COMPAT_FREEBSD9`/`10`). Operations 0 and 1 fail with `EOPNOTSUPP`, as measured on a PS5 (firmware 12.70). The `SEM2` operations, 23 and 24, were measured on the console and returned `0` for a count of 0 where FreeBSD would time out, so they stay unimplemented. - [sceKernelSyncOnAddressWait](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded From c5d16f952ec4f320b72757eb4981a1446c996e42 Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Tue, 20 Oct 2026 07:10:05 -0300 Subject: [PATCH 112/219] feat(libSceNpTus): export missing firmware interfaces --- core/libs/prx/libSceNpTus/Export.cpp | 610 +++++++++++++++++++++++++++ docs/dev/debt/libSceNpTus.md | 122 ++++++ 2 files changed, 732 insertions(+) diff --git a/core/libs/prx/libSceNpTus/Export.cpp b/core/libs/prx/libSceNpTus/Export.cpp index 11b8a99218..66bd1f8f4b 100644 --- a/core/libs/prx/libSceNpTus/Export.cpp +++ b/core/libs/prx/libSceNpTus/Export.cpp @@ -126,4 +126,614 @@ int APS5_VABI sceNpTusAddAndGetVariableAAsync(int reqId, std::uint64_t targetAcc return StartOffline(reqId); } +int APS5_VABI sceNpTusSetThreadParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssCreateNpTitleCtx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssCreateNpTitleCtxA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusCreateTitleCtx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetTimeout(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusChangeModeForOtherSaveDataOwners(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusWaitAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetMultiSlotVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetMultiSlotVariableVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariableVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariableVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetSmallStorage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetSmallStorageAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetStorage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetStorageAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetMultiSlotVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/docs/dev/debt/libSceNpTus.md b/docs/dev/debt/libSceNpTus.md index 3ccc29383a..db941e25cb 100644 --- a/docs/dev/debt/libSceNpTus.md +++ b/docs/dev/debt/libSceNpTus.md @@ -3,6 +3,128 @@ ### Unknown function info - [SceNpTusVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the layout of the output array of `sceNpTusGetMultiSlotVariableAAsync` and of `outVariable` in `sceNpTusAddAndGetVariableAAsync` is not verified, so the output is never written and `variableArraySize` / `outVariableSize` are not checked. The signatures and the community error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/tree/main/src/core/libraries/np/np_tus), which fpPS4 agrees with for the context and request calls; no console measurement. +- [sceNpTusSetThreadParam](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssCreateNpTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssCreateNpTitleCtxA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusCreateTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetTimeout](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusChangeModeForOtherSaveDataOwners](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusWaitAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetMultiSlotVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetMultiSlotVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotData](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatus](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatus](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatus](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetSmallStorage](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetSmallStorageAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetStorage](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetStorageAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetMultiSlotVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature ### Functional From d4387fbaf70c6717900d85648f47561f76df428d Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Tue, 20 Oct 2026 07:10:28 -0300 Subject: [PATCH 113/219] feat(libSceAudioIn,libSceAudio3d,libSceVoice): export missing firmware interfaces --- core/libs/prx/libSceAudio3d/Export.cpp | 90 ++++++++++++++ core/libs/prx/libSceAudioIn/Export.cpp | 160 +++++++++++++++++++++++++ core/libs/prx/libSceVoice/Export.cpp | 60 ++++++++++ docs/dev/debt/libSceAudio3d.md | 18 +++ docs/dev/debt/libSceAudioIn.md | 32 +++++ docs/dev/debt/libSceVoice.md | 12 ++ 6 files changed, 372 insertions(+) diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index 47489f9d5e..29dc455b14 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -249,4 +249,94 @@ int APS5_VABI sceAudio3dPortFlush(uint32_t port_id) { return 0; } +int APS5_VABI sceAudio3dAudioOutOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dAudioOutClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dAudioOutOutput(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dAudioOutOutputs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dBedWrite(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dBedWrite2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dStrError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dPortOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dPortCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dPortDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dPortGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dReportRegisterHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dReportUnregisterHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dGetSpeakerArrayMemorySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dCreateSpeakerArray(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dDeleteSpeakerArray(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index a5a107c931..68fa257250 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -208,4 +208,164 @@ int32_t APS5_VABI sceAudioInGetRerouteCount(int32_t handle) { return find(handle) ? 0 : AUDIO_IN_ERROR_INVALID_HANDLE; } +int APS5_VABI sceAudioInDeviceIdOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInDeviceIdAsyncOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetUsbGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInIsSharedDevice(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInDeviceIdHqOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInDeviceHqOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetPortConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetPortStatuses(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInGetHandleStatusInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInHqOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInDeviceOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInChangeAppModuleState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetSparkParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetSparkSideTone(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetCompressorPreGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetNoiseGateThreshold(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetMicLevel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetDeviceMuteStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetDevConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInCountPorts(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetUserMute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicWrite(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicSetMute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetMode2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInExtCtrl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInInputs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index 3ff904d406..6cb273c095 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -280,4 +280,64 @@ int APS5_VABI sceVoiceDisableChat(void) { return 0; } +int APS5_VABI sceVoiceInitHQ(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceGetMemorySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceDeviceInCustomerData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceQAEnableRecording(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceVADAdjustment(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceVADPVTAdjustment(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceVADSetVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceSetMuteFlagAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceSetBitRate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceUpdatePort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoicePausePortAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceResumePortAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/docs/dev/debt/libSceAudio3d.md b/docs/dev/debt/libSceAudio3d.md index 09f5bce9c1..6b382184b7 100644 --- a/docs/dev/debt/libSceAudio3d.md +++ b/docs/dev/debt/libSceAudio3d.md @@ -9,3 +9,21 @@ - [sceAudio3dPortOpen](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too - [sceAudio3dPortGetAttributesSupported](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - the supported list (late reverb level, downmix spread radius, downmix spread height aware) is inferred from the attributes `sceAudio3dPortSetAttribute` accepts; its order and the count written when the caller capacity is smaller are unverified. [shadPS4](https://github.com/shadps4-emu/shadPS4) returns object attributes (PCM, gain...) instead, prosper and KytyPS5 have no body - [sceAudio3dObjectReserve](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF +- [sceAudio3dAudioOutOpen](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dAudioOutClose](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dAudioOutOutput](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dAudioOutOutputs](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dBedWrite](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dBedWrite2](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dStrError](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dPortOpenEx](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dPortCreate](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dPortDestroy](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dPortGetStatus](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dReportRegisterHandler](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dReportUnregisterHandler](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dGetSpeakerArrayMemorySize](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dCreateSpeakerArray](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dDeleteSpeakerArray](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dGetSpeakerArrayMixCoefficients](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dGetSpeakerArrayMixCoefficients2](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature diff --git a/docs/dev/debt/libSceAudioIn.md b/docs/dev/debt/libSceAudioIn.md index 63b2d8b02c..34211858e7 100644 --- a/docs/dev/debt/libSceAudioIn.md +++ b/docs/dev/debt/libSceAudioIn.md @@ -7,6 +7,38 @@ ### Unknown function info - [sceAudioInIsPadEmbeddedMic](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - name from its NID (`X+4jdIS75P0`); signature `int32_t(int32_t handle)` from the Ghost of Yotei (PPSA26344) call site in #2355; the result's meaning is unconfirmed, so it still throws rather than invent one +- [sceAudioInDeviceIdOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInDeviceIdAsyncOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetUsbGain](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInIsSharedDevice](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInDeviceIdHqOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInDeviceHqOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetConnections](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetPortConnections](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetPortStatuses](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInGetHandleStatusInfo](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInOpenEx](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInHqOpenEx](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInDeviceOpenEx](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInChangeAppModuleState](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetSparkParam](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetSparkSideTone](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetCompressorPreGain](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetNoiseGateThreshold](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetMicLevel](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetDeviceMuteStatus](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetDevConnection](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInCountPorts](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetUserMute](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicCreate](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicDestroy](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicWrite](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicGetStatus](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicSetMute](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetMode](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetMode2](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInExtCtrl](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInInputs](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature ### Functional diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index c7ee43b953..5c4f80575c 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -11,3 +11,15 @@ - [sceVoiceEnableChat, sceVoiceDisableChat](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) take no arguments and return 0, following [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L460-L464) and the `sceAudioOut2EnableChat` model. The host mixes no chat audio, so the calls only record whether chat audio is enabled and fail when the library is not initialized. Console error codes and chat session requirements are unverified. PPSA23566 imports them - [sceVoiceInit, sceVoiceStart, sceVoiceCreatePort, sceVoiceGetPortInfo](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - a second `sceVoiceInit` returns 0x804E0802, and Start, CreatePort and GetPortInfo return 0x804E0801 before checking any argument while the library is not initialized, as [prosper](https://github.com/mattias800/prosper/blob/ed42e499888d5a86792c3e1b2df3aebd8ca1a681/prosper/src/hle/audio/hle_audio.cpp#L4697-L4713) reads them from the shipped module. The other port and lifecycle calls still throw when the library is not initialized, since prosper gives no module address for them. Whether `sceVoiceInit` checks a null param before its state is unknown, so a null param still throws first, and that `sceVoiceEnd` returns the library to the not-initialized state is assumed. Closes when: the other entry points are read from the module or measured on a PS5 - [sceVoicePausePort, sceVoiceResumePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5: the `(uint32_t portId)` signature follows [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L516-L538) and the PS3 calls in [RPCS3](https://github.com/RPCS3/rpcs3/blob/be69c054eeac57e2e671697a463473d37fe5a7b2/rpcs3/Emu/Cell/Modules/cellVoice.cpp#L443-L529), and the console's error codes and its result for pausing a paused port or resuming a running one are unknown (0 here, as in RPCS3). Both only check that the library is initialized and the port exists: VitaSDK documents that a paused port keeps its data and reported state, and nothing here processes port data. PPSA24191 imports them +- [sceVoiceInitHQ](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceGetMemorySize](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceDeviceInCustomerData](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceQAEnableRecording](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceVADAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceVADPVTAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceVADSetVersion](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceSetMuteFlagAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceSetBitRate](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceUpdatePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoicePausePortAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceResumePortAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature From 187b1ab24726a34bb93143e27ccb44be66fc9187 Mon Sep 17 00:00:00 2001 From: thibaut Date: Tue, 20 Oct 2026 12:52:31 +0200 Subject: [PATCH 114/219] fix(shader): take the storage write product error from a Dekker two-product GLSL Fma is not required to be fused: llvmpipe splits it into a multiply and an add, so the residual came out 0 and ties fell back to round-to-even of the rounded product. Split both factors (Veltkamp, 4097) and sum the partial products with correctly rounded operations only, every one NoContraction, so the error is exact on every device. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016TuDcnUv4yXWYDS6ZUqCdG --- .../SpirvBackend/src/SpirvImageEmitter.cpp | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 1d6e81da10..ddcba701bb 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1031,8 +1031,24 @@ std::uint32_t QuantizeStoreTexel(SpirvEmitterState& state, std::uint32_t resourc state.module.AddFunction(spv::OpExtInst, vec4, result, GlslStd450(state), static_cast(op), operands...); return result; }; - const auto error = ext(GLSLstd450Fma, clamped, scale, Unary(state, spv::OpFNegate, vec4, product)); - state.module.AddAnnotation(spv::OpDecorate, error, spv::DecorationNoContraction); + const auto exact = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t rhs) { + const auto value = Binary(state, opcode, vec4, lhs, rhs); + state.module.AddAnnotation(spv::OpDecorate, value, spv::DecorationNoContraction); + return value; + }; + const auto spreadFactor = splat(vec4, ConstantF32(state, std::bit_cast(4097.0f))); + const auto high = [&](std::uint32_t value) { + const auto spread = exact(spv::OpFMul, value, spreadFactor); + return exact(spv::OpFSub, spread, exact(spv::OpFSub, spread, value)); + }; + const auto clampedHigh = high(clamped); + const auto scaleHigh = high(scale); + const auto clampedLow = exact(spv::OpFSub, clamped, clampedHigh); + const auto scaleLow = exact(spv::OpFSub, scale, scaleHigh); + auto error = exact(spv::OpFSub, exact(spv::OpFMul, clampedHigh, scaleHigh), product); + error = exact(spv::OpFAdd, error, exact(spv::OpFMul, clampedHigh, scaleLow)); + error = exact(spv::OpFAdd, error, exact(spv::OpFMul, clampedLow, scaleHigh)); + error = exact(spv::OpFAdd, error, exact(spv::OpFMul, clampedLow, scaleLow)); const auto below = ext(GLSLstd450Floor, product); const auto fraction = Binary(state, spv::OpFSub, vec4, product, below); state.module.AddAnnotation(spv::OpDecorate, fraction, spv::DecorationNoContraction); From be27ffa12cd293f9ce4214024dd9327746b7e704 Mon Sep 17 00:00:00 2001 From: Seif Allah Karoui <21332330+SeifKaroui@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:07:13 +0100 Subject: [PATCH 115/219] test(libkernel): harden signed-byte memory protection coverage --- core/libs/tests/GuestMemory.cpp | 78 +++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 04fe239d47..37501693fb 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -208,12 +208,83 @@ static void CheckAudioCoprocessorProtection() { static void CheckMprotectIgnoresUndefinedBits() { constexpr std::size_t length = 0x4000; + const auto check = [&](void* mapping, int protection) { + Require(sceKernelMprotect(mapping, length, protection) == 0); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(mapping, 0, &info, sizeof(info)) == 0); + int expected = protection & 0x3f7; + if ((expected & 0x2) != 0) expected |= 0x1; + if ((expected & 0x80) != 0) expected |= 0x40; + if ((expected & 0x200) != 0) expected |= 0x100; + if (info.protection != expected) + std::fprintf(stderr, "Memory protection 0x%x reported 0x%x, expected 0x%x\n", static_cast(protection), static_cast(info.protection), static_cast(expected)); + Require(info.protection == expected); + auto* bytes = static_cast(mapping); + if ((protection & 0x222) != 0) { + bytes[0] = 5; + bytes[length - 1] = 7; + Require(bytes[0] == 5 && bytes[length - 1] == 7); + } else if ((protection & 0x117) != 0) { + Require(bytes[0] == 5 && bytes[length - 1] == 7); + } + }; void* mapping = nullptr; Require(sceKernelMapFlexibleMemory(&mapping, length, 3, 0) == 0); - Require(sceKernelMprotect(mapping, length, static_cast(0xfffffff2u)) == 0); - static_cast(mapping)[length - 1] = 5; - Require(static_cast(mapping)[length - 1] == 5); + for (int byte = 0x80; byte <= 0xff; ++byte) check(mapping, byte - 0x100); + for (const int protection : {0, 1, 2, 3, 4, 0x10, 0x20, 0x40, 0x80, 0x100, 0x200, 0x3f7}) { + for (const auto undefined : {0u, 8u, 0x400u, 0x80000000u, 0xfffffc08u}) + check(mapping, static_cast(static_cast(protection) | undefined)); + } Require(sceKernelMunmap(mapping, length) == 0); + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, length, 0, 0, &physical) == 0); + mapping = nullptr; + Require(sceKernelMapDirectMemory(&mapping, length, 0xf2, 0, physical, 0) == 0); + check(mapping, -14); + check(mapping, 0x400); + check(mapping, 0x100); + check(mapping, 0x200); + check(mapping, 0x3f7); + Require(sceKernelMunmap(mapping, length) == 0); + Require(sceKernelReleaseDirectMemory(physical, length) == 0); +} + +static void CheckStrictMemoryProtectionBits() { + constexpr std::size_t length = 0x4000; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, length, 0, 0, &physical) == 0); + void* mapping = nullptr; + Require(sceKernelMapDirectMemory(&mapping, length, 3, 0, physical, 0) == 0); + auto* bytes = static_cast(mapping); + bytes[0] = 5; + bytes[length - 1] = 7; + const auto rejected = [&](auto operation) { + bool threw = false; + try { operation(); } catch (const std::invalid_argument&) { threw = true; } + Require(threw); + }; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0); + for (const int protection : {-256, -129, -128, -14, -1, 8, 0x400, std::numeric_limits::min()}) { + void* invalid = nullptr; + rejected([&] { sceKernelMapFlexibleMemory(&invalid, length, protection, 0); }); + Require(invalid == nullptr); + std::size_t after = 0; + Require(sceKernelAvailableFlexibleMemorySize(&after) == 0 && after == available); + rejected([&] { sceKernelMapDirectMemory(&invalid, length, protection, 0, physical, 0); }); + Require(invalid == nullptr); + rejected([&] { sceKernelMtypeprotect(mapping, length, 2, protection); }); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(mapping, 0, &info, sizeof(info)) == 0); + Require(info.protection == 3 && info.memory_type == 0); + int type = -1; + std::int64_t start = -1; + std::int64_t end = -1; + Require(sceKernelGetDirectMemoryType(physical, &type, &start, &end) == 0 && type == 0); + Require(bytes[0] == 5 && bytes[length - 1] == 7); + } + Require(sceKernelMunmap(mapping, length) == 0); + Require(sceKernelReleaseDirectMemory(physical, length) == 0); } static void CheckInternalNamedFlexibleMapping() { @@ -2899,6 +2970,7 @@ int main(int argc, char** argv) { CheckCheckedReleaseDirectMemory(); CheckAudioCoprocessorProtection(); CheckMprotectIgnoresUndefinedBits(); + CheckStrictMemoryProtectionBits(); CheckDirectMemoryFollowsPhysicalPages(); CheckSparseDirectMemoryBacking(); CheckReleaseDirectMemoryClearsMappings(); From 9d84fac30454b3caad93a345493d9463266722f0 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Tue, 20 Oct 2026 09:50:58 +0300 Subject: [PATCH 116/219] feat(libSceUlt): implement sceUltMutexTryLock It takes a free mutex or relocks one the caller holds, as sceUltMutexLock does, and returns ULT_ERROR_BUSY instead of waiting when another thread holds it. --- core/libs/prx/libSceUlt/CMakeLists.txt | 12 +++++ core/libs/prx/libSceUlt/Export.cpp | 32 +++++++++-- core/libs/tests/GuestUltSync.cpp | 75 ++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 core/libs/tests/GuestUltSync.cpp diff --git a/core/libs/prx/libSceUlt/CMakeLists.txt b/core/libs/prx/libSceUlt/CMakeLists.txt index 25fa2dad49..1e5f2ebc77 100644 --- a/core/libs/prx/libSceUlt/CMakeLists.txt +++ b/core/libs/prx/libSceUlt/CMakeLists.txt @@ -41,4 +41,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(guest_ult_sync_tests ${LIBS_INCLUDE_DIR}/tests/GuestUltSync.cpp) + target_include_directories(guest_ult_sync_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ult_sync_tests PRIVATE libSceUlt libc) + configure_windows_unwind(guest_ult_sync_tests) + foreach(testCase IN ITEMS mutex-trylock) + add_test(NAME guest_ult_sync_${testCase} COMMAND guest_ult_sync_tests ${testCase}) + set_tests_properties(guest_ult_sync_${testCase} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_ult_sync_${testCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() endif() diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 7d2af42297..ca0f3c0f7d 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -261,6 +261,33 @@ int APS5_VABI sceUltMutexLock(void* mutex) { return ULT_OK; } +int APS5_VABI sceUltMutexTryLock(void* mutex) { + std::shared_ptr state; + { + std::lock_guard lock(gMutex); + auto it = gMutexes.find(mutex); + if (it == gMutexes.end()) { + return mutex == nullptr ? ULT_ERROR_NULL : ULT_ERROR_STATE; + } + state = it->second; + } + const auto self = std::this_thread::get_id(); + std::lock_guard lock(state->_mutex); + if (!state->_alive) { + return ULT_ERROR_STATE; + } + if (state->_owner == self) { + ++state->_lockCount; + return ULT_OK; + } + if (state->_lockCount != 0) { + return ULT_ERROR_BUSY; + } + state->_owner = self; + state->_lockCount = 1; + return ULT_OK; +} + int APS5_VABI sceUltMutexUnlock(void* mutex) { std::shared_ptr state; { @@ -866,11 +893,6 @@ int APS5_VABI sceUltGetWaitingQueueResourcePoolInfo() { return 0; } -int APS5_VABI sceUltMutexTryLock() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceUltReaderWriterLockDestroy() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestUltSync.cpp b/core/libs/tests/GuestUltSync.cpp new file mode 100644 index 0000000000..87e8b6238e --- /dev/null +++ b/core/libs/tests/GuestUltSync.cpp @@ -0,0 +1,75 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceUltInitialize(); +int APS5_VABI sceUltFinalize(); +int APS5_VABI _sceUltMutexCreate(void*, const char*, void*, const void*, std::uint32_t); +int APS5_VABI sceUltMutexLock(void*); +int APS5_VABI sceUltMutexTryLock(void*); +int APS5_VABI sceUltMutexUnlock(void*); +int APS5_VABI sceUltMutexDestroy(void*); +} + +namespace { + +constexpr int Ok = 0; +constexpr int Null = -2139029503; +constexpr int State = -2139029498; +constexpr int Busy = -2139029497; + +void Require(bool value, int line) { + if (value) return; + std::fprintf(stderr, "ULT sync check failed at line %d\n", line); + std::abort(); +} + +#define CHECK(value) Require((value), __LINE__) + +struct alignas(8) Object { + std::uint8_t bytes[256]; +}; + +void MutexTryLock() { + Object mutex{}; + CHECK(sceUltMutexTryLock(nullptr) == Null); + CHECK(sceUltMutexTryLock(&mutex) == State); + CHECK(_sceUltMutexCreate(&mutex, "trylock", nullptr, nullptr, 0) == Ok); + CHECK(sceUltMutexTryLock(&mutex) == Ok); + CHECK(sceUltMutexTryLock(&mutex) == Ok); + std::atomic other{0}; + std::thread([&] { other = sceUltMutexTryLock(&mutex); }).join(); + CHECK(other == Busy); + CHECK(sceUltMutexUnlock(&mutex) == Ok); + std::thread([&] { other = sceUltMutexTryLock(&mutex); }).join(); + CHECK(other == Busy); + CHECK(sceUltMutexUnlock(&mutex) == Ok); + CHECK(sceUltMutexUnlock(&mutex) == State); + std::thread([&] { + other = sceUltMutexTryLock(&mutex); + if (other == Ok) other = sceUltMutexUnlock(&mutex); + }).join(); + CHECK(other == Ok); + CHECK(sceUltMutexLock(&mutex) == Ok); + CHECK(sceUltMutexDestroy(&mutex) == Busy); + CHECK(sceUltMutexUnlock(&mutex) == Ok); + CHECK(sceUltMutexDestroy(&mutex) == Ok); + CHECK(sceUltMutexTryLock(&mutex) == State); +} + +} + +int main(int argc, char** argv) { + if (argc != 2) return 2; + CHECK(sceUltInitialize() == Ok); + if (std::strcmp(argv[1], "mutex-trylock") == 0) MutexTryLock(); + else return 2; + CHECK(sceUltFinalize() == Ok); + std::puts("ULT sync tests passed"); + return 0; +} From e2c38222febbb0d3496005a235c935cc4c808f02 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Tue, 20 Oct 2026 09:51:54 +0300 Subject: [PATCH 117/219] feat(libSceUlt): implement the semaphore, queue, pool and ulthread option initializers Each clears its 128-byte option structure, as sceUltMutexOptParamInitialize and sceUltUlthreadRuntimeOptParamInitialize already do. --- core/libs/prx/libSceUlt/CMakeLists.txt | 2 +- core/libs/prx/libSceUlt/Export.cpp | 58 ++++++++++++++----------- core/libs/prx/libSceUlt/SceUltTypes.hpp | 4 ++ core/libs/tests/GuestUltSync.cpp | 20 +++++++++ 4 files changed, 58 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libSceUlt/CMakeLists.txt b/core/libs/prx/libSceUlt/CMakeLists.txt index 1e5f2ebc77..36173c8381 100644 --- a/core/libs/prx/libSceUlt/CMakeLists.txt +++ b/core/libs/prx/libSceUlt/CMakeLists.txt @@ -45,7 +45,7 @@ if(BUILD_TESTING) target_include_directories(guest_ult_sync_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_ult_sync_tests PRIVATE libSceUlt libc) configure_windows_unwind(guest_ult_sync_tests) - foreach(testCase IN ITEMS mutex-trylock) + foreach(testCase IN ITEMS mutex-trylock opt-params) add_test(NAME guest_ult_sync_${testCase} COMMAND guest_ult_sync_tests ${testCase}) set_tests_properties(guest_ult_sync_${testCase} PROPERTIES TIMEOUT 20) if(WIN32) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index ca0f3c0f7d..5fd55f8c5b 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -108,6 +108,14 @@ void ReleaseQueue(UltQueueState& state) { --pool._queues; } +int InitializeOptParam(UltOptParam* optParam) { + if (optParam == nullptr) { + return ULT_ERROR_NULL; + } + std::memset(optParam, 0, sizeof(*optParam)); + return ULT_OK; +} + void* APS5_VABI ulthreadRunner(void* arg) { auto* state = static_cast(arg); const auto result = state->_entry(state->_arg); @@ -184,6 +192,31 @@ int APS5_VABI sceUltMutexOptParamInitialize(UltMutexOptParam* optParam, std::uin return ULT_OK; } +int APS5_VABI sceUltSemaphoreOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltQueueOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltQueueDataResourcePoolOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltWaitingQueueResourcePoolOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltUlthreadOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + int APS5_VABI sceUltMutexCreate(void* mutex, const char* name, void* waitingQueueResourcePool, const UltMutexOptParam* optParam, std::uint32_t buildVersion) { (void)name; (void)buildVersion; @@ -1013,16 +1046,6 @@ int APS5_VABI sceUltConditionVariableOptParamInitialize() { return 0; } -int APS5_VABI sceUltQueueDataResourcePoolOptParamInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltQueueOptParamInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceUltReaderWriterLockCreate() { NotImplemented_nid_no_patch(__func__); return 0; @@ -1033,19 +1056,4 @@ int APS5_VABI sceUltReaderWriterLockOptParamInitialize() { return 0; } -int APS5_VABI sceUltSemaphoreOptParamInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltUlthreadOptParamInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltWaitingQueueResourcePoolOptParamInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceUlt/SceUltTypes.hpp b/core/libs/prx/libSceUlt/SceUltTypes.hpp index 0da430382c..82efef0da7 100644 --- a/core/libs/prx/libSceUlt/SceUltTypes.hpp +++ b/core/libs/prx/libSceUlt/SceUltTypes.hpp @@ -20,6 +20,10 @@ constexpr int ULT_ERROR_STATE = -2139029498; constexpr int ULT_ERROR_BUSY = -2139029497; constexpr int ULT_ERROR_AGAIN = -2139029496; +struct UltOptParam { + std::uint8_t bytes[128]; +}; + struct UltMutexState { std::mutex _mutex; std::condition_variable _available; diff --git a/core/libs/tests/GuestUltSync.cpp b/core/libs/tests/GuestUltSync.cpp index 87e8b6238e..07d15bd855 100644 --- a/core/libs/tests/GuestUltSync.cpp +++ b/core/libs/tests/GuestUltSync.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include extern "C" { @@ -14,6 +15,11 @@ int APS5_VABI sceUltMutexLock(void*); int APS5_VABI sceUltMutexTryLock(void*); int APS5_VABI sceUltMutexUnlock(void*); int APS5_VABI sceUltMutexDestroy(void*); +int APS5_VABI sceUltSemaphoreOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltQueueOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltQueueDataResourcePoolOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltWaitingQueueResourcePoolOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltUlthreadOptParamInitialize(void*, std::uint32_t); } namespace { @@ -62,12 +68,26 @@ void MutexTryLock() { CHECK(sceUltMutexTryLock(&mutex) == State); } +void OptParams() { + using Initializer = int (APS5_VABI *)(void*, std::uint32_t); + for (const Initializer initialize : {sceUltSemaphoreOptParamInitialize, sceUltQueueOptParamInitialize, sceUltQueueDataResourcePoolOptParamInitialize, + sceUltWaitingQueueResourcePoolOptParamInitialize, sceUltUlthreadOptParamInitialize}) { + std::uint8_t bytes[136]; + std::memset(bytes, 0xa5, sizeof(bytes)); + CHECK(initialize(nullptr, 0) == Null); + CHECK(initialize(bytes, 0) == Ok); + for (std::size_t index = 0; index < 128; ++index) CHECK(bytes[index] == 0); + for (std::size_t index = 128; index < sizeof(bytes); ++index) CHECK(bytes[index] == 0xa5); + } +} + } int main(int argc, char** argv) { if (argc != 2) return 2; CHECK(sceUltInitialize() == Ok); if (std::strcmp(argv[1], "mutex-trylock") == 0) MutexTryLock(); + else if (std::strcmp(argv[1], "opt-params") == 0) OptParams(); else return 2; CHECK(sceUltFinalize() == Ok); std::puts("ULT sync tests passed"); From ea3e23bd86048c3a793d34f3c6ab424bb3d4c793 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Tue, 20 Oct 2026 09:54:54 +0300 Subject: [PATCH 118/219] feat(libSceUlt): implement the reader-writer lock Readers share the lock while no writer holds it and a writer waits until it is free. Destroying a held or waited-on lock and destroying the waiting-queue pool of a lock a thread waits on return ULT_ERROR_BUSY, and sceUltFinalize wakes waiters with ULT_ERROR_STATE, as for the mutex. --- core/libs/prx/libSceUlt/CMakeLists.txt | 4 +- core/libs/prx/libSceUlt/Export.cpp | 63 +----- core/libs/prx/libSceUlt/SceUltTypes.hpp | 9 + .../prx/libSceUlt/src/ReaderWriterLock.cpp | 200 ++++++++++++++++++ .../prx/libSceUlt/src/ReaderWriterLock.hpp | 8 + core/libs/tests/GuestUltSync.cpp | 165 ++++++++++++++- core/libs/tests/GuestUltThread.cpp | 8 - 7 files changed, 393 insertions(+), 64 deletions(-) create mode 100644 core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp create mode 100644 core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp diff --git a/core/libs/prx/libSceUlt/CMakeLists.txt b/core/libs/prx/libSceUlt/CMakeLists.txt index 36173c8381..056d78b128 100644 --- a/core/libs/prx/libSceUlt/CMakeLists.txt +++ b/core/libs/prx/libSceUlt/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceUlt SHARED EXCLUDE_FROM_ALL Export.cpp) +add_library(libSceUlt SHARED EXCLUDE_FROM_ALL Export.cpp src/ReaderWriterLock.cpp) target_include_directories(libSceUlt PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceUlt) @@ -45,7 +45,7 @@ if(BUILD_TESTING) target_include_directories(guest_ult_sync_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_ult_sync_tests PRIVATE libSceUlt libc) configure_windows_unwind(guest_ult_sync_tests) - foreach(testCase IN ITEMS mutex-trylock opt-params) + foreach(testCase IN ITEMS mutex-trylock opt-params rwlock-validation rwlock-readers rwlock-writer rwlock-pool rwlock-finalize) add_test(NAME guest_ult_sync_${testCase} COMMAND guest_ult_sync_tests ${testCase}) set_tests_properties(guest_ult_sync_${testCase} PROPERTIES TIMEOUT 20) if(WIN32) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 5fd55f8c5b..49d9213440 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -14,6 +14,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "SceUltTypes.hpp" +#include "prx/libSceUlt/src/ReaderWriterLock.hpp" extern "C" int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); extern "C" int APS5_VABI scePthreadJoin(Pthread thread, void** retval); @@ -108,14 +109,6 @@ void ReleaseQueue(UltQueueState& state) { --pool._queues; } -int InitializeOptParam(UltOptParam* optParam) { - if (optParam == nullptr) { - return ULT_ERROR_NULL; - } - std::memset(optParam, 0, sizeof(*optParam)); - return ULT_OK; -} - void* APS5_VABI ulthreadRunner(void* arg) { auto* state = static_cast(arg); const auto result = state->_entry(state->_arg); @@ -125,6 +118,11 @@ void* APS5_VABI ulthreadRunner(void* arg) { } +bool UltWaitingPoolExists(void* pool) { + std::lock_guard lock(gMutex); + return gResourcePools.contains(pool); +} + extern "C" { int APS5_VABI sceUltInitialize() { @@ -162,6 +160,7 @@ int APS5_VABI sceUltFinalize() { } gConditions.clear(); gMutexes.clear(); + UltFinalizeReaderWriterLocks(); gResourcePools.clear(); for (auto& entry : gQueues) { auto& state = *entry.second; @@ -489,6 +488,9 @@ int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void* pool) { } } } + if (UltReaderWriterLocksWaitOn(pool)) { + return ULT_ERROR_BUSY; + } gResourcePools.erase(it); return ULT_OK; } @@ -926,41 +928,6 @@ int APS5_VABI sceUltGetWaitingQueueResourcePoolInfo() { return 0; } -int APS5_VABI sceUltReaderWriterLockDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltReaderWriterLockLockRead() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltReaderWriterLockLockWrite() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltReaderWriterLockTryLockRead() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltReaderWriterLockTryLockWrite() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltReaderWriterLockUnlockRead() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltReaderWriterLockUnlockWrite() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceUltUlthreadExit() { NotImplemented_nid_no_patch(__func__); return 0; @@ -1046,14 +1013,4 @@ int APS5_VABI sceUltConditionVariableOptParamInitialize() { return 0; } -int APS5_VABI sceUltReaderWriterLockCreate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltReaderWriterLockOptParamInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceUlt/SceUltTypes.hpp b/core/libs/prx/libSceUlt/SceUltTypes.hpp index 82efef0da7..62a87e75f9 100644 --- a/core/libs/prx/libSceUlt/SceUltTypes.hpp +++ b/core/libs/prx/libSceUlt/SceUltTypes.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,14 @@ struct UltOptParam { std::uint8_t bytes[128]; }; +inline int InitializeOptParam(UltOptParam* optParam) { + if (optParam == nullptr) { + return ULT_ERROR_NULL; + } + std::memset(optParam, 0, sizeof(*optParam)); + return ULT_OK; +} + struct UltMutexState { std::mutex _mutex; std::condition_variable _available; diff --git a/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp b/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp new file mode 100644 index 0000000000..7e1273394a --- /dev/null +++ b/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp @@ -0,0 +1,200 @@ +#include "prx/libSceUlt/src/ReaderWriterLock.hpp" +#include "prx/libSceUlt/SceUltTypes.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct UltReaderWriterLockState { + std::mutex _mutex; + std::condition_variable _released; + std::uint32_t _readers = 0; + bool _writing = false; + std::thread::id _writer{}; + std::uint32_t _waiters = 0; + bool _alive = true; + void* _waitingPool = nullptr; +}; + +std::mutex gLocksMutex; +std::unordered_map> gLocks; + +int FindLock(void* rwlock, std::shared_ptr& state) { + if (rwlock == nullptr) { + return ULT_ERROR_NULL; + } + std::lock_guard lock(gLocksMutex); + const auto it = gLocks.find(rwlock); + if (it == gLocks.end()) { + return ULT_ERROR_STATE; + } + state = it->second; + return ULT_OK; +} + +bool HeldByCaller(const UltReaderWriterLockState& state) { + return state._writing && state._writer == std::this_thread::get_id(); +} + +int Wait(UltReaderWriterLockState& state, std::unique_lock& lock, bool writer) { + ++state._waiters; + state._released.wait(lock, [&] { return !state._alive || (!state._writing && (!writer || state._readers == 0)); }); + --state._waiters; + return state._alive ? ULT_OK : ULT_ERROR_STATE; +} + +} + +bool UltReaderWriterLocksWaitOn(void* pool) { + std::lock_guard lock(gLocksMutex); + for (const auto& entry : gLocks) { + if (entry.second->_waitingPool != pool) continue; + std::lock_guard stateLock(entry.second->_mutex); + if (entry.second->_waiters != 0) return true; + } + return false; +} + +void UltFinalizeReaderWriterLocks() { + std::lock_guard lock(gLocksMutex); + for (const auto& entry : gLocks) { + std::lock_guard stateLock(entry.second->_mutex); + entry.second->_alive = false; + entry.second->_released.notify_all(); + } + gLocks.clear(); +} + +extern "C" { + +int APS5_VABI sceUltReaderWriterLockOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltReaderWriterLockCreate(void* rwlock, const char* name, void* waitingQueueResourcePool, const UltOptParam* optParam, std::uint32_t buildVersion) { + (void)name; + (void)optParam; + (void)buildVersion; + if (rwlock == nullptr) { + return ULT_ERROR_NULL; + } + if (waitingQueueResourcePool != nullptr && !UltWaitingPoolExists(waitingQueueResourcePool)) { + return ULT_ERROR_INVALID; + } + auto state = std::make_shared(); + state->_waitingPool = waitingQueueResourcePool; + std::lock_guard lock(gLocksMutex); + if (gLocks.contains(rwlock)) { + return ULT_ERROR_STATE; + } + std::memset(rwlock, 0, 256); + gLocks[rwlock] = std::move(state); + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockDestroy(void* rwlock) { + if (rwlock == nullptr) { + return ULT_ERROR_NULL; + } + std::lock_guard lock(gLocksMutex); + const auto it = gLocks.find(rwlock); + if (it == gLocks.end()) { + return ULT_ERROR_STATE; + } + auto& state = *it->second; + std::lock_guard stateLock(state._mutex); + if (state._readers != 0 || state._writing || state._waiters != 0) { + return ULT_ERROR_BUSY; + } + state._alive = false; + gLocks.erase(it); + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockLockRead(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::unique_lock lock(state->_mutex); + if (!state->_alive || HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + if (const int waited = Wait(*state, lock, false); waited != ULT_OK) return waited; + ++state->_readers; + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockTryLockRead(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::lock_guard lock(state->_mutex); + if (!state->_alive || HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + if (state->_writing) { + return ULT_ERROR_BUSY; + } + ++state->_readers; + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockUnlockRead(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::lock_guard lock(state->_mutex); + if (!state->_alive || state->_readers == 0) { + return ULT_ERROR_STATE; + } + if (--state->_readers == 0) { + state->_released.notify_all(); + } + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockLockWrite(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::unique_lock lock(state->_mutex); + if (!state->_alive || HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + if (const int waited = Wait(*state, lock, true); waited != ULT_OK) return waited; + state->_writing = true; + state->_writer = std::this_thread::get_id(); + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockTryLockWrite(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::lock_guard lock(state->_mutex); + if (!state->_alive || HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + if (state->_writing || state->_readers != 0) { + return ULT_ERROR_BUSY; + } + state->_writing = true; + state->_writer = std::this_thread::get_id(); + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockUnlockWrite(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::lock_guard lock(state->_mutex); + if (!state->_alive || !HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + state->_writing = false; + state->_writer = {}; + state->_released.notify_all(); + return ULT_OK; +} + +} diff --git a/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp b/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp new file mode 100644 index 0000000000..df98dcd077 --- /dev/null +++ b/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp @@ -0,0 +1,8 @@ +#ifndef CORE_LIBS_PRX_LIBSCEULT_SRC_READERWRITERLOCK_HPP +#define CORE_LIBS_PRX_LIBSCEULT_SRC_READERWRITERLOCK_HPP + +bool UltWaitingPoolExists(void* pool); +bool UltReaderWriterLocksWaitOn(void* pool); +void UltFinalizeReaderWriterLocks(); + +#endif diff --git a/core/libs/tests/GuestUltSync.cpp b/core/libs/tests/GuestUltSync.cpp index 07d15bd855..3415dbeb06 100644 --- a/core/libs/tests/GuestUltSync.cpp +++ b/core/libs/tests/GuestUltSync.cpp @@ -1,5 +1,6 @@ #include "SceTypes.hpp" #include +#include #include #include #include @@ -20,12 +21,24 @@ int APS5_VABI sceUltQueueOptParamInitialize(void*, std::uint32_t); int APS5_VABI sceUltQueueDataResourcePoolOptParamInitialize(void*, std::uint32_t); int APS5_VABI sceUltWaitingQueueResourcePoolOptParamInitialize(void*, std::uint32_t); int APS5_VABI sceUltUlthreadOptParamInitialize(void*, std::uint32_t); +int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*, std::uint32_t); +int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void*); +int APS5_VABI sceUltReaderWriterLockOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltReaderWriterLockCreate(void*, const char*, void*, const void*, std::uint32_t); +int APS5_VABI sceUltReaderWriterLockDestroy(void*); +int APS5_VABI sceUltReaderWriterLockLockRead(void*); +int APS5_VABI sceUltReaderWriterLockTryLockRead(void*); +int APS5_VABI sceUltReaderWriterLockUnlockRead(void*); +int APS5_VABI sceUltReaderWriterLockLockWrite(void*); +int APS5_VABI sceUltReaderWriterLockTryLockWrite(void*); +int APS5_VABI sceUltReaderWriterLockUnlockWrite(void*); } namespace { constexpr int Ok = 0; constexpr int Null = -2139029503; +constexpr int Invalid = -2139029500; constexpr int State = -2139029498; constexpr int Busy = -2139029497; @@ -71,7 +84,7 @@ void MutexTryLock() { void OptParams() { using Initializer = int (APS5_VABI *)(void*, std::uint32_t); for (const Initializer initialize : {sceUltSemaphoreOptParamInitialize, sceUltQueueOptParamInitialize, sceUltQueueDataResourcePoolOptParamInitialize, - sceUltWaitingQueueResourcePoolOptParamInitialize, sceUltUlthreadOptParamInitialize}) { + sceUltWaitingQueueResourcePoolOptParamInitialize, sceUltUlthreadOptParamInitialize, sceUltReaderWriterLockOptParamInitialize}) { std::uint8_t bytes[136]; std::memset(bytes, 0xa5, sizeof(bytes)); CHECK(initialize(nullptr, 0) == Null); @@ -81,6 +94,151 @@ void OptParams() { } } + +void WaitUntil(const std::atomic& flag) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!flag.load() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + CHECK(flag.load()); +} + +void Settle() { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); +} + +void ReaderWriterValidation() { + Object rwlock{}; + Object pool{}; + std::memset(&rwlock, 0x5a, sizeof(rwlock)); + CHECK(sceUltReaderWriterLockCreate(nullptr, "rwlock", nullptr, nullptr, 0) == Null); + CHECK(sceUltReaderWriterLockCreate(&rwlock, "rwlock", &pool, nullptr, 0) == Invalid); + for (const auto call : {sceUltReaderWriterLockDestroy, sceUltReaderWriterLockLockRead, sceUltReaderWriterLockTryLockRead, sceUltReaderWriterLockUnlockRead, + sceUltReaderWriterLockLockWrite, sceUltReaderWriterLockTryLockWrite, sceUltReaderWriterLockUnlockWrite}) { + CHECK(call(nullptr) == Null); + CHECK(call(&rwlock) == State); + } + CHECK(sceUltReaderWriterLockCreate(&rwlock, "rwlock", nullptr, nullptr, 0) == Ok); + for (const auto byte : rwlock.bytes) CHECK(byte == 0); + CHECK(sceUltReaderWriterLockCreate(&rwlock, "rwlock", nullptr, nullptr, 0) == State); + CHECK(sceUltReaderWriterLockUnlockRead(&rwlock) == State); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == State); + CHECK(sceUltReaderWriterLockLockRead(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Busy); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == State); + CHECK(sceUltReaderWriterLockUnlockRead(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == State && sceUltReaderWriterLockTryLockWrite(&rwlock) == State); + CHECK(sceUltReaderWriterLockLockRead(&rwlock) == State && sceUltReaderWriterLockTryLockRead(&rwlock) == State); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Busy); + std::atomic other{0}; + std::thread([&] { other = sceUltReaderWriterLockUnlockWrite(&rwlock); }).join(); + CHECK(other == State); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockLockRead(&rwlock) == State && sceUltReaderWriterLockDestroy(&rwlock) == State); +} + +void ReaderWriterReaders() { + Object rwlock{}; + CHECK(sceUltReaderWriterLockCreate(&rwlock, "readers", nullptr, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockLockRead(&rwlock) == Ok); + std::atomic other{0}; + std::thread([&] { + other = sceUltReaderWriterLockTryLockRead(&rwlock); + if (other == Ok) other = sceUltReaderWriterLockLockRead(&rwlock); + }).join(); + CHECK(other == Ok); + CHECK(sceUltReaderWriterLockTryLockWrite(&rwlock) == Busy); + std::atomic started{false}; + std::atomic written{false}; + std::thread writer([&] { + started = true; + other = sceUltReaderWriterLockLockWrite(&rwlock); + written = true; + if (other == Ok) other = sceUltReaderWriterLockUnlockWrite(&rwlock); + }); + WaitUntil(started); + for (int held = 3; held > 0; --held) { + Settle(); + CHECK(!written.load()); + CHECK(sceUltReaderWriterLockUnlockRead(&rwlock) == Ok); + } + writer.join(); + CHECK(written.load() && other == Ok); + CHECK(sceUltReaderWriterLockUnlockRead(&rwlock) == State); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Ok); +} + +void ReaderWriterWriter() { + Object rwlock{}; + CHECK(sceUltReaderWriterLockCreate(&rwlock, "writer", nullptr, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockTryLockWrite(&rwlock) == Ok); + std::atomic other{0}; + std::thread([&] { other = sceUltReaderWriterLockTryLockRead(&rwlock); }).join(); + CHECK(other == Busy); + std::thread([&] { other = sceUltReaderWriterLockTryLockWrite(&rwlock); }).join(); + CHECK(other == Busy); + std::atomic started{false}; + std::atomic read{false}; + std::thread reader([&] { + started = true; + other = sceUltReaderWriterLockLockRead(&rwlock); + read = true; + if (other == Ok) other = sceUltReaderWriterLockUnlockRead(&rwlock); + }); + WaitUntil(started); + Settle(); + CHECK(!read.load()); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Busy); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == Ok); + reader.join(); + CHECK(read.load() && other == Ok); + CHECK(sceUltReaderWriterLockTryLockWrite(&rwlock) == Ok && sceUltReaderWriterLockUnlockWrite(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Ok); +} + +void ReaderWriterPool() { + Object pool{}; + Object rwlock{}; + std::uint8_t workArea[2048]; + CHECK(_sceUltWaitingQueueResourcePoolCreate(&pool, "pool", 2, 2, workArea, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockCreate(&rwlock, "pooled", &pool, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == Ok); + std::atomic started{false}; + std::atomic other{0}; + std::thread reader([&] { + started = true; + other = sceUltReaderWriterLockLockRead(&rwlock); + if (other == Ok) other = sceUltReaderWriterLockUnlockRead(&rwlock); + }); + WaitUntil(started); + Settle(); + CHECK(sceUltWaitingQueueResourcePoolDestroy(&pool) == Busy); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == Ok); + reader.join(); + CHECK(other == Ok); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Ok); + CHECK(sceUltWaitingQueueResourcePoolDestroy(&pool) == Ok); +} + +void ReaderWriterFinalize() { + Object rwlock{}; + CHECK(sceUltReaderWriterLockCreate(&rwlock, "finalize", nullptr, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == Ok); + std::atomic started{false}; + std::atomic other{0}; + std::thread waiter([&] { + started = true; + other = sceUltReaderWriterLockLockWrite(&rwlock); + }); + WaitUntil(started); + Settle(); + CHECK(sceUltFinalize() == Ok); + waiter.join(); + CHECK(other == State); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == State); + CHECK(sceUltInitialize() == Ok); +} + } int main(int argc, char** argv) { @@ -88,6 +246,11 @@ int main(int argc, char** argv) { CHECK(sceUltInitialize() == Ok); if (std::strcmp(argv[1], "mutex-trylock") == 0) MutexTryLock(); else if (std::strcmp(argv[1], "opt-params") == 0) OptParams(); + else if (std::strcmp(argv[1], "rwlock-validation") == 0) ReaderWriterValidation(); + else if (std::strcmp(argv[1], "rwlock-readers") == 0) ReaderWriterReaders(); + else if (std::strcmp(argv[1], "rwlock-writer") == 0) ReaderWriterWriter(); + else if (std::strcmp(argv[1], "rwlock-pool") == 0) ReaderWriterPool(); + else if (std::strcmp(argv[1], "rwlock-finalize") == 0) ReaderWriterFinalize(); else return 2; CHECK(sceUltFinalize() == Ok); std::puts("ULT sync tests passed"); diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index c89c38791d..0d074c1d20 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -38,7 +38,6 @@ int APS5_VABI sceUltSemaphoreDestroy(void*); int APS5_VABI sceUltQueueDestroy(void*); int APS5_VABI sceUltQueueDataResourcePoolDestroy(void*); int APS5_VABI sceUltConditionVariableDestroy(void*); -int APS5_VABI sceUltReaderWriterLockDestroy(); int APS5_VABI sceUltGetConditionVariableInfo(); } @@ -111,13 +110,6 @@ static void Validation() { CHECK(sceUltFinalize() == Ok); CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(1, 1) == 256u + 16u * 1024u); CHECK(sceUltConditionVariableDestroy(nullptr) == -2139029503); - bool threwRwl = false; - try { - sceUltReaderWriterLockDestroy(); - } catch (const std::runtime_error&) { - threwRwl = true; - } - CHECK(threwRwl); bool threwInfo = false; try { sceUltGetConditionVariableInfo(); From 80d00b3f1598d1f4e0c637c0d52fcffb5627ebf1 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Tue, 20 Oct 2026 09:56:04 +0300 Subject: [PATCH 119/219] feat(libSceUlt): implement sceUltUlthreadGetSelf, sceUltUlthreadYield and sceUltUlthreadExit An ulthread runs on its own guest thread, so it remembers its object for sceUltUlthreadGetSelf, yields that thread, and exits it with the status sceUltUlthreadJoin and sceUltUlthreadTryJoin return. Outside an ulthread the three return ULT_ERROR_STATE. --- core/libs/prx/libSceUlt/CMakeLists.txt | 2 +- core/libs/prx/libSceUlt/Export.cpp | 49 +++++++++++++++------- core/libs/prx/libSceUlt/SceUltTypes.hpp | 1 + core/libs/tests/GuestUltSync.cpp | 56 +++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceUlt/CMakeLists.txt b/core/libs/prx/libSceUlt/CMakeLists.txt index 056d78b128..ea7e2f4a31 100644 --- a/core/libs/prx/libSceUlt/CMakeLists.txt +++ b/core/libs/prx/libSceUlt/CMakeLists.txt @@ -45,7 +45,7 @@ if(BUILD_TESTING) target_include_directories(guest_ult_sync_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_ult_sync_tests PRIVATE libSceUlt libc) configure_windows_unwind(guest_ult_sync_tests) - foreach(testCase IN ITEMS mutex-trylock opt-params rwlock-validation rwlock-readers rwlock-writer rwlock-pool rwlock-finalize) + foreach(testCase IN ITEMS mutex-trylock opt-params rwlock-validation rwlock-readers rwlock-writer rwlock-pool rwlock-finalize ulthreads) add_test(NAME guest_ult_sync_${testCase} COMMAND guest_ult_sync_tests ${testCase}) set_tests_properties(guest_ult_sync_${testCase} PROPERTIES TIMEOUT 20) if(WIN32) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 49d9213440..2e4eb263f3 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -18,6 +18,8 @@ extern "C" int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); extern "C" int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +extern "C" void APS5_VABI scePthreadExit(void* retval); +extern "C" void APS5_VABI scePthreadYield(); namespace { @@ -109,8 +111,11 @@ void ReleaseQueue(UltQueueState& state) { --pool._queues; } +thread_local UltUlthreadState* gCurrentUlthread = nullptr; + void* APS5_VABI ulthreadRunner(void* arg) { auto* state = static_cast(arg); + gCurrentUlthread = state; const auto result = state->_entry(state->_arg); state->_exited.store(true, std::memory_order_release); return reinterpret_cast(static_cast(result)); @@ -761,6 +766,7 @@ int APS5_VABI sceUltUlthreadCreate(void* ulthread, const char* name, UltUlthread auto state = std::make_shared(); state->_entry = entry; state->_arg = arg; + state->_object = ulthread; state->_runtime = runtime; { std::lock_guard lock(gMutex); @@ -824,6 +830,34 @@ int APS5_VABI sceUltUlthreadTryJoin(void* ulthread, std::int32_t* status) { return sceUltUlthreadJoin(ulthread, status); } +int APS5_VABI sceUltUlthreadGetSelf(void** ulthread) { + if (ulthread == nullptr) { + return ULT_ERROR_NULL; + } + if (gCurrentUlthread == nullptr) { + return ULT_ERROR_STATE; + } + *ulthread = gCurrentUlthread->_object; + return ULT_OK; +} + +int APS5_VABI sceUltUlthreadYield() { + if (gCurrentUlthread == nullptr) { + return ULT_ERROR_STATE; + } + scePthreadYield(); + return ULT_OK; +} + +int APS5_VABI sceUltUlthreadExit(std::int32_t status) { + if (gCurrentUlthread == nullptr) { + return ULT_ERROR_STATE; + } + gCurrentUlthread->_exited.store(true, std::memory_order_release); + scePthreadExit(reinterpret_cast(static_cast(status))); + return ULT_OK; +} + int APS5_VABI sceUltUlthreadRuntimeDestroy(void* runtime) { if (runtime == nullptr) { return ULT_ERROR_NULL; @@ -928,21 +962,6 @@ int APS5_VABI sceUltGetWaitingQueueResourcePoolInfo() { return 0; } -int APS5_VABI sceUltUlthreadExit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltUlthreadGetSelf() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltUlthreadYield() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceUltAbstractThreadFactoryCreateThread() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceUlt/SceUltTypes.hpp b/core/libs/prx/libSceUlt/SceUltTypes.hpp index 62a87e75f9..efe53ec5a8 100644 --- a/core/libs/prx/libSceUlt/SceUltTypes.hpp +++ b/core/libs/prx/libSceUlt/SceUltTypes.hpp @@ -106,6 +106,7 @@ struct UltRuntimeState { struct UltUlthreadState { UltUlthreadEntry _entry = nullptr; std::uint64_t _arg = 0; + void* _object = nullptr; Pthread _thread = nullptr; void* _runtime = nullptr; std::atomic _exited{false}; diff --git a/core/libs/tests/GuestUltSync.cpp b/core/libs/tests/GuestUltSync.cpp index 3415dbeb06..c10706460d 100644 --- a/core/libs/tests/GuestUltSync.cpp +++ b/core/libs/tests/GuestUltSync.cpp @@ -7,6 +7,7 @@ #include #include #include +#include extern "C" { int APS5_VABI sceUltInitialize(); @@ -32,6 +33,15 @@ int APS5_VABI sceUltReaderWriterLockUnlockRead(void*); int APS5_VABI sceUltReaderWriterLockLockWrite(void*); int APS5_VABI sceUltReaderWriterLockTryLockWrite(void*); int APS5_VABI sceUltReaderWriterLockUnlockWrite(void*); +std::uint64_t APS5_VABI sceUltUlthreadRuntimeGetWorkAreaSize(std::uint32_t, std::uint32_t); +int APS5_VABI sceUltUlthreadRuntimeCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*, std::uint32_t); +int APS5_VABI sceUltUlthreadRuntimeDestroy(void*); +int APS5_VABI sceUltUlthreadCreate(void*, const char*, UltUlthreadEntry, std::uint64_t, void*, std::uint64_t, void*, const void*, std::uint32_t); +int APS5_VABI sceUltUlthreadJoin(void*, std::int32_t*); +int APS5_VABI sceUltUlthreadTryJoin(void*, std::int32_t*); +int APS5_VABI sceUltUlthreadGetSelf(void**); +int APS5_VABI sceUltUlthreadYield(); +int APS5_VABI sceUltUlthreadExit(std::int32_t); } namespace { @@ -239,6 +249,51 @@ void ReaderWriterFinalize() { CHECK(sceUltInitialize() == Ok); } +struct alignas(8) UlthreadObject { + std::uint8_t bytes[512]; +}; + +std::atomic gSelf{nullptr}; +std::atomic gYield{-1}; +std::atomic gPastExit{false}; + +std::int32_t APS5_VABI ExitingEntry(std::uint64_t status) { + void* self = nullptr; + if (sceUltUlthreadGetSelf(&self) == Ok) gSelf = self; + gYield = sceUltUlthreadYield(); + sceUltUlthreadExit(static_cast(status)); + gPastExit = true; + return -1; +} + +void Ulthreads() { + void* self = &gSelf; + CHECK(sceUltUlthreadGetSelf(nullptr) == Null); + CHECK(sceUltUlthreadGetSelf(&self) == State && self == &gSelf); + CHECK(sceUltUlthreadYield() == State); + CHECK(sceUltUlthreadExit(1) == State); + std::vector workArea(sceUltUlthreadRuntimeGetWorkAreaSize(2, 1)); + Object runtime{}; + CHECK(sceUltUlthreadRuntimeCreate(&runtime, "runtime", 2, 1, workArea.data(), nullptr, 0) == Ok); + UlthreadObject first{}; + std::uint8_t context[4096]; + CHECK(sceUltUlthreadCreate(&first, "exits", ExitingEntry, 42, context, sizeof(context), &runtime, nullptr, 0) == Ok); + std::int32_t status = 0; + CHECK(sceUltUlthreadJoin(&first, &status) == Ok); + CHECK(status == 42 && gSelf.load() == &first && gYield.load() == Ok && !gPastExit.load()); + UlthreadObject second{}; + gSelf = nullptr; + CHECK(sceUltUlthreadCreate(&second, "tryjoin", ExitingEntry, 7, context, sizeof(context), &runtime, nullptr, 0) == Ok); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + int joined = Busy; + while (joined == Busy && std::chrono::steady_clock::now() < deadline) { + joined = sceUltUlthreadTryJoin(&second, &status); + if (joined == Busy) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + CHECK(joined == Ok && status == 7 && gSelf.load() == &second); + CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == Ok); +} + } int main(int argc, char** argv) { @@ -251,6 +306,7 @@ int main(int argc, char** argv) { else if (std::strcmp(argv[1], "rwlock-writer") == 0) ReaderWriterWriter(); else if (std::strcmp(argv[1], "rwlock-pool") == 0) ReaderWriterPool(); else if (std::strcmp(argv[1], "rwlock-finalize") == 0) ReaderWriterFinalize(); + else if (std::strcmp(argv[1], "ulthreads") == 0) Ulthreads(); else return 2; CHECK(sceUltFinalize() == Ok); std::puts("ULT sync tests passed"); From 0371d1778e924215426e98a7abfdb4c710a8c031 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Tue, 20 Oct 2026 09:56:32 +0300 Subject: [PATCH 120/219] docs(debt): note the rules the Ult lock and ulthread calls assume --- docs/dev/debt/libSceUlt.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index bbb48c53ce..4c2177a5ee 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -8,7 +8,8 @@ - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. - [sceUltQueueCreate, sceUltQueueDataResourcePoolCreate](../../../core/libs/prx/libSceUlt/Export.cpp) - a non-null option parameter is accepted and ignored, as the other Ult create calls already do. [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/00d087b2323bf377fd39b7163a256977383cb232/include/orbis/Ult.h) declares `sceUltQueueOptParamInitialize` and `sceUltQueueDataResourcePoolOptParamInitialize` without their structures; the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.h#L25-L78) defines both as a 16-byte reserved header followed by reserved fields, 128 bytes, with no settings, and [fpPS4](https://github.com/red-prig/fpPS4/blob/5ffbdbbd76b4da6e4b8721743ea491f494b28913/src/ps4_libsceult.pas#L142-L172) keeps them as 128 opaque bytes. That the PS5 structures carry no settings either is assumed from these, not verified - [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured -- [sceUltConditionVariableSignalAll, sceUltGetConditionVariableInfo, sceUltGetMutexInfo, sceUltGetQueueDataResourcePoolInfo, sceUltGetQueueInfo, sceUltGetReaderWriterLockInfo, sceUltGetSemaphoreInfo, sceUltGetUlthreadInfo, sceUltGetUlthreadRuntimeInfo, sceUltGetWaitingQueueResourcePoolInfo, sceUltMutexTryLock, sceUltReaderWriterLockDestroy, sceUltReaderWriterLockLockRead, sceUltReaderWriterLockLockWrite, sceUltReaderWriterLockTryLockRead, sceUltReaderWriterLockTryLockWrite, sceUltReaderWriterLockUnlockRead, sceUltReaderWriterLockUnlockWrite, sceUltUlthreadExit, sceUltUlthreadGetSelf, sceUltUlthreadYield, sceUltAbstractThreadFactoryCreateThread, sceUltAbstractThreadFactoryGetNumMaxThreads, sceUltAbstractThreadFactoryJoinThread, sceUltKernelThreadFactoryFinalize, sceUltKernelThreadFactoryGetFactoryPointer, sceUltKernelThreadFactoryInitialize, sceUltModuleFinalize, sceUltModuleInitialize, sceUltUlthreadFactoryFinalize, sceUltUlthreadFactoryGetFactoryPointer, sceUltUlthreadFactoryGetWorkAreaSize, sceUltUlthreadFactoryInitialize, sceUltConditionVariableCreate, sceUltConditionVariableOptParamInitialize, sceUltQueueDataResourcePoolOptParamInitialize, sceUltQueueOptParamInitialize, sceUltReaderWriterLockCreate, sceUltReaderWriterLockOptParamInitialize, sceUltSemaphoreOptParamInitialize, sceUltUlthreadOptParamInitialize, sceUltWaitingQueueResourcePoolOptParamInitialize](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) - throwing stubs declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware +- [sceUltConditionVariableSignalAll, sceUltGetConditionVariableInfo, sceUltGetMutexInfo, sceUltGetQueueDataResourcePoolInfo, sceUltGetQueueInfo, sceUltGetReaderWriterLockInfo, sceUltGetSemaphoreInfo, sceUltGetUlthreadInfo, sceUltGetUlthreadRuntimeInfo, sceUltGetWaitingQueueResourcePoolInfo, sceUltAbstractThreadFactoryCreateThread, sceUltAbstractThreadFactoryGetNumMaxThreads, sceUltAbstractThreadFactoryJoinThread, sceUltKernelThreadFactoryFinalize, sceUltKernelThreadFactoryGetFactoryPointer, sceUltKernelThreadFactoryInitialize, sceUltModuleFinalize, sceUltModuleInitialize, sceUltUlthreadFactoryFinalize, sceUltUlthreadFactoryGetFactoryPointer, sceUltUlthreadFactoryGetWorkAreaSize, sceUltUlthreadFactoryInitialize, sceUltConditionVariableCreate, sceUltConditionVariableOptParamInitialize](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) - throwing stubs declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware +- [sceUltMutexTryLock, the reader-writer lock and sceUltUlthreadGetSelf, sceUltUlthreadYield and sceUltUlthreadExit](../../../core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp) (libSceUlt) - the signatures follow the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.cpp) (a 256-byte lock and 128-byte option structures), not a PS5 binary. The rules are assumed: readers may take the lock while a writer waits, a thread that holds the write lock gets `ULT_ERROR_STATE` for any further lock instead of waiting on itself, a reader asking for the write lock waits on its own read lock, and the try forms return `ULT_ERROR_BUSY`. Outside an ulthread, `sceUltUlthreadGetSelf`, `sceUltUlthreadYield` and `sceUltUlthreadExit` return `ULT_ERROR_STATE`; inside one they act on the guest thread it runs on, since ulthreads are not scheduled cooperatively here ### Functional - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. From 06ee881eba01aeae36d65eb0c4e5102c0d6ca64b Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:39:50 +0200 Subject: [PATCH 121/219] fix(libkernel): look up the descriptor of lseek before the whence value lseek on a descriptor that is not open with an invalid whence answered EINVAL. FreeBSD 11.4 kern_lseek looks the descriptor up first and only then lets the file's seek operation judge the whence, so the answer is EBADF. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/File/src/Stdio.cpp | 1 + core/libs/tests/GuestFilesystem.cpp | 3 +++ 2 files changed, 4 insertions(+) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 4d786eb038..4c9931e4a9 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -887,6 +887,7 @@ int APS5_VABI sceKernelFtruncate(int d, int64_t length) { } int64_t APS5_VABI lseek_nid_postfix(int d, int64_t offset, int whence) { + if (!GuestSockets::IsOpen(d) && !DescriptorIsOpen_nid_no_patch(d)) return PosixFailure(GUEST_EBADF); if (whence < 0 || whence > 2) return PosixFailure(GUEST_EINVAL); return PosixResult(sceKernelLseek(d, offset, whence)); } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 7818a4421c..e445f5a603 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -1026,6 +1026,9 @@ int main() { Require(flock_nid_postfix(other, 2 | 4) == 0); Require(flock_nid_postfix(other, 8) == 0); Require(flock_nid_postfix(locked, 2 | 4) == 0); + Require(lseek_nid_postfix(-1, 0, 7) == -1 && *__error_nid_postfix() == 9); + Require(lseek_nid_postfix(0x7fffffff, 0, -1) == -1 && *__error_nid_postfix() == 9); + Require(lseek_nid_postfix(locked, 0, 7) == -1 && *__error_nid_postfix() == 22); Require(flock_nid_postfix(-1, 2 | 4) == -1 && *__error_nid_postfix() == 9); Require(flock_nid_postfix(0x7fffffff, 1) == -1 && *__error_nid_postfix() == 9); int flockPipe[2] = {-1, -1}; From bbcce1736e9d28bf8a1e3a0a7779a2cd0aef7c2c Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:06:01 -0300 Subject: [PATCH 122/219] fix(libkernel): support socket fcntl duplication --- core/libs/prx/libkernel/File/src/Open.cpp | 4 +- core/libs/prx/libkernel/Socket/src/Udp.cpp | 97 ++++++++++--------- core/libs/tests/GuestKernelFcntl.cpp | 65 ++++++++++++- .../libs/tests/GuestSocketDescriptorFlags.cpp | 48 +++++++++ docs/dev/debt/libkernel.md | 4 +- 5 files changed, 168 insertions(+), 50 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 04192be62e..9438bdc962 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -174,7 +174,6 @@ static int MapFlags(int sceFlags) { extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...); -static constexpr int GuestSetFlags = 4; static int SceErrorFromErrno(int error) { const int guest = error > 0 && error <= 34 ? error : FilesystemError(std::error_code(error, std::generic_category())); @@ -432,7 +431,8 @@ int APS5_VABI sceKernelFcntl(int d, int command, ...) { } int result; if (GuestSockets::IsOpen(d)) { - if (command == GuestSetFlags) result = fcntl_nid_postfix(d, command, static_cast(argument)); + if (type == File::FcntlArgument::Int) result = fcntl_nid_postfix(d, command, static_cast(argument)); + else if (type == File::FcntlArgument::Pointer) result = fcntl_nid_postfix(d, command, reinterpret_cast(argument)); else result = fcntl_nid_postfix(d, command); } else { result = File::Fcntl(d, command, argument); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 7b64c96aae..d22433f117 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -57,6 +57,7 @@ #include extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI dup2_nid_postfix(int from, int to); namespace { #ifdef _WIN32 using NativeSocket = SOCKET; @@ -181,9 +182,20 @@ int ReserveNumber(NativeSocket) { } return number; } -int CopyNumber(int number) { - const int copy = ::_dup(number); - return copy < 0 ? Fail(errno == EMFILE ? 24 : 9) : copy; +int CopyNumber(int number, int minimum = 0) { + std::vector below; + int copy = -1; + int error = 0; + try { + while ((copy = ::_dup(number)) >= 0 && copy < minimum) below.push_back(copy); + if (copy < 0) error = errno == EMFILE ? 24 : 9; + } catch (const std::bad_alloc&) { + if (copy >= 0) ::_close(copy); + copy = -1; + error = 12; + } + for (const int temporary : below) ::_close(temporary); + return copy < 0 ? Fail(error) : copy; } int CopyNumberTo(int number, int target) { return ::_dup2(number, target) != 0 ? Fail(errno == EMFILE ? 24 : 9) : 0; @@ -196,8 +208,8 @@ int ReserveNumber(NativeSocket native) { const int number = ::fcntl(native, F_DUPFD_CLOEXEC, 0); return number < 0 ? Fail(NativeError()) : number; } -int CopyNumber(int number) { - const int copy = ::fcntl(number, F_DUPFD_CLOEXEC, 0); +int CopyNumber(int number, int minimum = 0) { + const int copy = ::fcntl(number, F_DUPFD_CLOEXEC, minimum); return copy < 0 ? Fail(NativeError()) : copy; } int CopyNumberTo(int number, int target) { @@ -561,21 +573,30 @@ bool GuestSockets::PipeReady(int descriptor, short events, short& revents) { } #endif -int GuestSockets::Duplicate(int descriptor) { +namespace { +int DuplicateSocket_nid_no_patch(int descriptor, int minimum, bool closeOnExec) { + if (minimum < 0) return Fail(22); std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); if (found == sockets.end()) return Fail(9); - const int duplicate = CopyNumber(descriptor); + const int duplicate = CopyNumber(descriptor, minimum); if (duplicate < 0) return -1; try { sockets.insert_or_assign(duplicate, found->second); - closeOnExecDescriptors.erase(duplicate); + if (closeOnExec) closeOnExecDescriptors.insert(duplicate); + else closeOnExecDescriptors.erase(duplicate); } catch (const std::bad_alloc&) { + sockets.erase(duplicate); ReleaseNumber(duplicate); return Fail(12); } return duplicate; } +} + +int GuestSockets::Duplicate(int descriptor) { + return DuplicateSocket_nid_no_patch(descriptor, 0, false); +} int GuestSockets::DuplicateTo(int descriptor, int target) { std::shared_ptr replaced; @@ -896,24 +917,24 @@ extern "C" bool KernelLookupSocketNative_nid_no_patch(int descriptor, std::share extern "C" { int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { - if (!GuestSockets::IsOpen(descriptor)) { - const auto type = File::FcntlArgumentOf(command); - std::intptr_t argument = 0; - if (type != File::FcntlArgument::None) { + const auto type = File::FcntlArgumentOf(command); + std::intptr_t argument = 0; + if (type != File::FcntlArgument::None) { #ifdef _WIN32 - __builtin_sysv_va_list arguments; - __builtin_sysv_va_start(arguments, command); - argument = type == File::FcntlArgument::Int ? __builtin_va_arg(arguments, int) - : reinterpret_cast(__builtin_va_arg(arguments, void*)); - __builtin_sysv_va_end(arguments); + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + argument = type == File::FcntlArgument::Int ? __builtin_va_arg(arguments, int) + : reinterpret_cast(__builtin_va_arg(arguments, void*)); + __builtin_sysv_va_end(arguments); #else - std::va_list arguments; - va_start(arguments, command); - argument = type == File::FcntlArgument::Int ? va_arg(arguments, int) - : reinterpret_cast(va_arg(arguments, void*)); - va_end(arguments); + std::va_list arguments; + va_start(arguments, command); + argument = type == File::FcntlArgument::Int ? va_arg(arguments, int) + : reinterpret_cast(va_arg(arguments, void*)); + va_end(arguments); #endif - } + } + if (!GuestSockets::IsOpen(descriptor)) { int result = 0; int netError = 0; if (KernelControlSocketStatus_nid_no_patch(descriptor, command, static_cast(argument), &result, &netError)) { @@ -921,6 +942,12 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { } return File::Fcntl(descriptor, command, argument); } + if (command == 0 || command == 17) return DuplicateSocket_nid_no_patch(descriptor, static_cast(argument), command == 17); + if (command == 10 || command == 18) { + const int duplicate = dup2_nid_postfix(descriptor, static_cast(argument)); + if (duplicate >= 0 && command == 18) fcntl_nid_postfix(duplicate, 2, 1); + return duplicate; + } const auto socket = Lookup(descriptor); if (!socket) return -1; std::lock_guard lock(socket->modeMutex); @@ -929,17 +956,7 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { return closeOnExecDescriptors.count(descriptor) != 0 ? 1 : 0; } if (command == 2) { -#ifdef _WIN32 - __builtin_sysv_va_list arguments; - __builtin_sysv_va_start(arguments, command); - const int flags = __builtin_va_arg(arguments, int); - __builtin_sysv_va_end(arguments); -#else - std::va_list arguments; - va_start(arguments, command); - const int flags = va_arg(arguments, int); - va_end(arguments); -#endif + const int flags = static_cast(argument); std::lock_guard registry(socketsMutex); if ((flags & 1) != 0) closeOnExecDescriptors.insert(descriptor); else closeOnExecDescriptors.erase(descriptor); @@ -947,17 +964,7 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { } if (command == 3) return 2 | (socket->options.nonblocking ? 4 : 0); // F_GETFL, O_RDWR if (command != 4) return Fail(22); -#ifdef _WIN32 - __builtin_sysv_va_list arguments; - __builtin_sysv_va_start(arguments, command); - const int flags = __builtin_va_arg(arguments, int); - __builtin_sysv_va_end(arguments); -#else - std::va_list arguments; - va_start(arguments, command); - const int flags = va_arg(arguments, int); - va_end(arguments); -#endif + const int flags = static_cast(argument); if ((flags & ~7) != 0) return Fail(45); #ifdef _WIN32 unsigned long enabled = (flags & 4) != 0; diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 339c3c4697..b75d3c22a9 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -28,6 +28,7 @@ int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceKernelFcntl(int, int, ...); int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); } static void Require(bool value) { if (!value) std::abort(); } @@ -39,12 +40,14 @@ static constexpr int SetFlags = 4; static constexpr int OldGetLock = 7; static constexpr int OldSetLock = 8; static constexpr int OldSetLockWait = 9; +static constexpr int Duplicate2Command = 10; static constexpr int GetLock = 11; static constexpr int SetLock = 12; static constexpr int SetLockWait = 13; static constexpr int ReadAhead = 15; static constexpr int DarwinReadAhead = 16; static constexpr int DuplicateCloseOnExec = 17; +static constexpr int Duplicate2CloseOnExec = 18; static constexpr int ReadOnly = 0; static constexpr int WriteOnly = 1; static constexpr int ReadWrite = 2; @@ -118,6 +121,66 @@ static void SocketsKeepTheirPath() { Require(sceKernelFcntl(socket, GetFlags) == (ReadWrite | NonBlocking)); Require(sceKernelFcntl(socket, SetFlags, 0) == 0); Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); + GuestLock lock{}; + Require(sceKernelFcntl(socket, GetLock, &lock) == SceEinval); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, SetDescriptorFlags, 1) == 0); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 1); + const int duplicate = sceKernelFcntl(socket, DuplicateCommand, 20); + Require(duplicate >= 20); + Require(sceKernelFcntl(duplicate, GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, SetFlags, ReadWrite | NonBlocking) == 0); + Require(sceKernelFcntl(duplicate, GetFlags) == (ReadWrite | NonBlocking)); + Require(sceKernelFcntl(duplicate, SetFlags, 0) == 0); + Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); + const int closing = sceKernelFcntl(socket, DuplicateCloseOnExec, duplicate + 1); + Require(closing >= duplicate + 1); + Require(sceKernelFcntl(closing, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, SetDescriptorFlags, 0) == 0); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(socket, Duplicate2Command, closing) == closing); + Require(sceKernelFcntl(closing, GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, closing) == closing); + Require(sceKernelFcntl(closing, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, socket) == socket); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, Duplicate2Command, socket) == socket); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, SetDescriptorFlags, 0) == 0); + int pipe[2] = {-1, -1}; + Require(pipe_nid_postfix(pipe) == 0); + Require(fcntl_nid_postfix(pipe[0], SetDescriptorFlags, 1) == 0); + Require(fcntl_nid_postfix(socket, Duplicate2Command, pipe[0]) == pipe[0]); + Require(sceKernelFcntl(pipe[0], GetFlags) == ReadWrite); + Require(sceKernelFcntl(pipe[0], GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, pipe[1]) == pipe[1]); + Require(sceKernelFcntl(pipe[1], GetFlags) == ReadWrite); + Require(sceKernelFcntl(pipe[1], GetDescriptorFlags) == 1); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); + const int random = sceKernelOpen("/dev/urandom", ReadOnly, 0); + Require(random >= 0); + Require(fcntl_nid_postfix(socket, Duplicate2Command, random) == random); + Require(sceKernelFcntl(random, GetFlags) == ReadWrite); + Require(close_nid_postfix(random) == 0); + const int file = sceKernelOpen("kernel_fcntl_socket_dup2.bin", CreateReadWrite, 0644); + Require(file == random); + char byte = 0; + Require(sceKernelRead(file, &byte, 1) == 0); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, file) == file); + Require(sceKernelFcntl(file, GetFlags) == ReadWrite); + Require(sceKernelFcntl(file, GetDescriptorFlags) == 1); + Require(close_nid_postfix(file) == 0); + std::remove("kernel_fcntl_socket_dup2.bin"); + RequireFailure(fcntl_nid_postfix(socket, DuplicateCommand, -1), GuestEinval); + RequireFailure(fcntl_nid_postfix(socket, DuplicateCloseOnExec, -1), GuestEinval); + RequireFailure(fcntl_nid_postfix(socket, Duplicate2Command, -1), GuestEbadf); + RequireFailure(fcntl_nid_postfix(socket, Duplicate2CloseOnExec, -1), GuestEbadf); + Require(sceKernelFcntl(socket, DuplicateCommand, -1) == SceEinval); + Require(sceKernelFcntl(socket, DuplicateCloseOnExec, -1) == SceEinval); + Require(sceKernelFcntl(socket, Duplicate2Command, -1) == SceEbadf); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, -1) == SceEbadf); Require(sceKernelFcntl(socket, SetFlags, 0x8000) == SceEnotsup); Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); Require(sceKernelFcntl(socket, 5) == SceEinval); @@ -127,7 +190,7 @@ static void SocketsKeepTheirPath() { RequireMalformedRequestsFail(socket); RequireFailure(ioctl_nid_postfix(socket, IntegerArgumentRequest, nullptr), GuestEfault); RequireFailure(ioctl_nid_postfix(socket, EmptyInputRequest, nullptr), GuestEfault); - Require(close_nid_postfix(socket) == 0); + for (const int descriptor : {closing, duplicate, socket}) Require(close_nid_postfix(descriptor) == 0); Require(sceKernelFcntl(socket, GetFlags) == SceEbadf); Require(sceKernelFcntl(-1, GetFlags) == SceEbadf); Require(sceKernelFcntl(12345, GetFlags) == SceEbadf); diff --git a/core/libs/tests/GuestSocketDescriptorFlags.cpp b/core/libs/tests/GuestSocketDescriptorFlags.cpp index a8c1ba10f2..ef8772f738 100644 --- a/core/libs/tests/GuestSocketDescriptorFlags.cpp +++ b/core/libs/tests/GuestSocketDescriptorFlags.cpp @@ -8,17 +8,24 @@ int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI close_nid_postfix(int); int APS5_VABI dup_nid_postfix(int); int APS5_VABI dup2_nid_postfix(int, int); +int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI fcntl_nid_postfix(int, int, ...); int* APS5_VABI __error_nid_postfix(); } namespace { +constexpr int DuplicateDescriptor = 0; constexpr int GetDescriptorFlags = 1; constexpr int SetDescriptorFlags = 2; +constexpr int Duplicate2Descriptor = 10; +constexpr int DuplicateDescriptorCloseOnExec = 17; +constexpr int Duplicate2DescriptorCloseOnExec = 18; constexpr int CloseOnExec = 1; constexpr int Stream = 1; constexpr int SocketCloseOnExec = 0x10000000; +constexpr int GuestEbadf = 9; +constexpr int GuestEinval = 22; void Require(bool condition, const char* what) { if (condition) return; @@ -51,6 +58,47 @@ int main() { Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); Require(close_nid_postfix(target) == 0, "close failed"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a socket must succeed"); + const int minimumDup = fcntl_nid_postfix(plain, DuplicateDescriptor, plain + 4); + Require(minimumDup >= plain + 4 && fcntl_nid_postfix(minimumDup, GetDescriptorFlags) == 0, "F_DUPFD must honor the minimum and clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUPFD must leave the flag of the original"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + const int cloexecDup = fcntl_nid_postfix(plain, DuplicateDescriptorCloseOnExec, minimumDup + 1); + Require(cloexecDup >= minimumDup + 1 && fcntl_nid_postfix(cloexecDup, GetDescriptorFlags) == CloseOnExec, "F_DUPFD_CLOEXEC must honor the minimum and set FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "F_DUPFD_CLOEXEC must leave the flag of the original"); + Require(fcntl_nid_postfix(plain, DuplicateDescriptor, -1) == -1 && *__error_nid_postfix() == GuestEinval, "F_DUPFD with a negative minimum must fail with EINVAL"); + Require(fcntl_nid_postfix(plain, DuplicateDescriptorCloseOnExec, -1) == -1 && *__error_nid_postfix() == GuestEinval, "F_DUPFD_CLOEXEC with a negative minimum must fail with EINVAL"); + Require(close_nid_postfix(cloexecDup) == 0 && close_nid_postfix(minimumDup) == 0, "close failed"); + + const int dup2Target = socket_nid_postfix(2, Stream, 0); + Require(dup2Target >= 0, "socket failed"); + Require(fcntl_nid_postfix(dup2Target, SetDescriptorFlags, CloseOnExec) == 0 && fcntl_nid_postfix(plain, SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a socket must succeed"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, dup2Target) == dup2Target, "F_DUP2FD onto another socket must return the target"); + Require(fcntl_nid_postfix(dup2Target, GetDescriptorFlags) == 0, "F_DUP2FD must clear FD_CLOEXEC on the target"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD must leave the flag of the original"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, plain) == plain && fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD onto the same descriptor must preserve FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, plain) == plain && fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "F_DUP2FD onto the same descriptor must preserve a cleared flag"); + Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, dup2Target) == dup2Target, "F_DUP2FD_CLOEXEC onto another socket must return the target"); + Require(fcntl_nid_postfix(dup2Target, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD_CLOEXEC must set FD_CLOEXEC on the target"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "F_DUP2FD_CLOEXEC must leave the flag of the original"); + Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, plain) == plain && fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD_CLOEXEC onto the same descriptor must set FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, -1) == -1 && *__error_nid_postfix() == GuestEbadf, "F_DUP2FD with a negative target must fail with EBADF"); + Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, -1) == -1 && *__error_nid_postfix() == GuestEbadf, "F_DUP2FD_CLOEXEC with a negative target must fail with EBADF"); + Require(close_nid_postfix(dup2Target) == 0, "close failed"); + + int pipeDescriptors[2] = {-1, -1}; + Require(pipe_nid_postfix(pipeDescriptors) == 0, "pipe failed"); + Require(fcntl_nid_postfix(pipeDescriptors[0], SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a pipe must succeed"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a socket must succeed"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, pipeDescriptors[0]) == pipeDescriptors[0], "F_DUP2FD onto a pipe must return the target"); + Require(fcntl_nid_postfix(pipeDescriptors[0], GetDescriptorFlags) == 0, "F_DUP2FD onto a pipe must clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, pipeDescriptors[1]) == pipeDescriptors[1], "F_DUP2FD_CLOEXEC onto a pipe must return the target"); + Require(fcntl_nid_postfix(pipeDescriptors[1], GetDescriptorFlags) == CloseOnExec, "F_DUP2FD_CLOEXEC onto a pipe must set FD_CLOEXEC"); + Require(close_nid_postfix(pipeDescriptors[0]) == 0 && close_nid_postfix(pipeDescriptors[1]) == 0, "close failed"); + const int marked = socket_nid_postfix(2, Stream | SocketCloseOnExec, 0); Require(marked >= 0 && fcntl_nid_postfix(marked, GetDescriptorFlags) == CloseOnExec, "SOCK_CLOEXEC must set FD_CLOEXEC"); Require(close_nid_postfix(marked) == 0, "close failed"); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 55dfa98df6..d89f9d228b 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -202,8 +202,8 @@ - [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console -- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console -- [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also duplicates the descriptor (`F_DUPFD`, `F_DUPFD_CLOEXEC`) and handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2` and an accepted socket start without it, `SOCK_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. +- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console +- [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2`, `F_DUPFD`, `F_DUP2FD` onto another descriptor and an accepted socket start without it, `F_DUP2FD` onto the same descriptor keeps it, and `SOCK_CLOEXEC`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other well-formed request throws. An input request with length 0 is not rejected as malformed: [`sys_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L703-L718) rejects it only in a kernel built without `COMPAT_FREEBSD4`, `COMPAT_FREEBSD5` and `COMPAT_43`, and which of these the PS5 kernel has is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [Regular file mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - on Linux `mmap` maps a regular file's descriptor shared or private like a shared memory object, with the same access checks; SQLite's WAL index (`-shm`) in Dusklight PPSA99305 needs it. FreeBSD 11 `mmap(2)` and shadPS4's `MemoryManager::MapFile` map regular files; the console's behaviour is not measured. Other descriptors (pipes, sockets, directories, devices) still fail with `EOPNOTSUPP`. On Windows a shared mapping is a view of a file section (`CreateFileMappingW` on the descriptor's handle, read-write when the descriptor is, otherwise read-only), so writes through the descriptor and the mapping see each other. This also maps anonymous `shm_open` objects and JIT shared memory descriptors, which are temporary files on Windows. A shared mapping may end inside the 16 KiB page that holds the end of the file, as POSIX [`mmap`](https://pubs.opengroup.org/onlinepubs/9799919799/functions/mmap.html) allows (llama.cpp in PS5LM Plus PPSA99581 maps a whole model file this way): the section keeps the file's size, so the file never grows, the view ends with the 4 KiB host page that holds the end of the file, which Windows fills with zeros past it, and the rest of the guest page is private zero pages. As POSIX requires, the bytes past the end read zero and are never written to the file; bytes written to those private pages are seen only through that mapping, not through other mappings of the file, and a file that grows later does not show its new bytes there. A shared mapping that reaches a whole page past the end of the file fails with `EOPNOTSUPP`, where POSIX maps it and delivers `SIGBUS` on access, as do `PROT_EXEC` and a write-only descriptor. A read-write alias (`GuestArenaMapAlias`) of a mapped page that holds the end of the file throws. `mprotect` above the view's protection (writing through a read-only descriptor, or adding `PROT_EXEC`) fails with `EACCES` before anything changes; FreeBSD allows `PROT_EXEC` on a readable descriptor. Shrinking the file with `ftruncate` fails with `EINVAL` while any shared mapping of it exists, even one that does not cover the removed part, because the section keeps the file's size at `mmap`; FreeBSD truncates. A read-write alias of a view (`GuestArenaMapAlias`, used when the GPU imports guest memory) of a read-only file section fails, as the alias is mapped `PAGE_READWRITE`. Views of the same file made by separate `mmap` calls are separate sections, so write tracking does not treat them as aliases of each other. A private mapping on Windows is a copy of the file read at `mmap`, which POSIX allows (later changes to the file need not show); pages wholly past the end of the file fail with `EOPNOTSUPP` instead of faulting on access, and a write-only descriptor fails with `EOPNOTSUPP`. - [sendfile](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory, and a file descriptor as the socket fails with `EBADF`, as for `send`, where FreeBSD answers `ENOTSOCK`. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it From dfa96f1fe6ad679b1cd62768569db5d269453308 Mon Sep 17 00:00:00 2001 From: ionut27001 <194206737+ionut27001@users.noreply.github.com> Date: Sun, 18 Oct 2026 02:06:26 +0300 Subject: [PATCH 123/219] fix(libkernel): return ENOENT for thirteen sysctl names, as measured on a PS5 In a payload process on a PS5 (firmware 12.70), sysctlbyname returns -1 with ENOENT for thirteen names that threw here, and leaves the buffer and the length alone, for a read, a size query, a null length and a null buffer with a null length. sysctl does the same for the fixed MIBs of seven of them. sysctlnametomib gives those seven MIBs and returns ENOENT for hw.cacheline_size and hw.usermem, which threw too. machdep.cacheline_size already returned ENOENT and is now tested with the others. A write to the new names and MIBs throws, as the console's answer to a write was not tried. The hw.realmem and vm.overcommit debt entries now say that a payload process gets ENOENT for them as well. AI-assisted: yes Co-Authored-By: Claude Opus 5.5 --- .../System/src/SystemConfiguration.cpp | 45 ++++++- core/libs/tests/GuestSystemConfiguration.cpp | 110 ++++++++++++++++-- docs/dev/debt/libkernel.md | 6 +- 3 files changed, 149 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index 8c73a85624..97371195e7 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -136,6 +136,30 @@ std::vector GuestVmEntries() { return entries; } +bool IsUnansweredMib(const int* name, std::uint32_t nameLength) { + if (nameLength != 2) return false; + return (name[0] == 1 && (name[1] == 6 || name[1] == 7 || name[1] == 12 || name[1] == 27 || name[1] == 32)) || (name[0] == 6 && (name[1] == 4 || name[1] == 11)); +} + +bool IsUnansweredName(const char* name) { + static constexpr const char* unanswered[] = { + "hw.byteorder", + "hw.cacheline_size", + "hw.machine_arch", + "hw.usermem", + "kern.clockrate", + "kern.maxfiles", + "kern.maxfilesperproc", + "kern.maxproc", + "kern.ps_strings", + "kern.sched.name", + "kern.smp.maxcpus", + "kern.stackprot", + "kern.timecounter.hardware", + }; + return std::any_of(std::begin(unanswered), std::end(unanswered), [name](const char* entry) { return std::strcmp(name, entry) == 0; }); +} + } extern "C" { @@ -207,6 +231,11 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *__error_nid_postfix() = 2; return -1; } + if (IsUnansweredMib(name, nameLength)) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": writing MIB " + std::to_string(name[0]) + "." + std::to_string(name[1])); + *__error_nid_postfix() = 2; + return -1; + } if (nameLength == 2 && name[0] == 6 && name[1] == 7) { if (newValue != nullptr) { *__error_nid_postfix() = 1; @@ -331,6 +360,11 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si for (const auto& entry : SystemStrings) { if (std::strcmp(name, entry.name) == 0) return sysctl_nid_postfix(entry.mib, 2, oldValue, oldLength, newValue, newLength); } + if (IsUnansweredName(name)) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": writing " + name); + *__error_nid_postfix() = 2; + return -1; + } if (std::strcmp(name, "kern.proc.pathname") == 0) { static constexpr int mib[] = {1, 14, 12}; return sysctl_nid_postfix(mib, 3, oldValue, oldLength, newValue, newLength); @@ -411,14 +445,21 @@ int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_ std::size_t length; }; static constexpr KnownName known[] = { + {"hw.byteorder", {6, 4}, 2}, + {"hw.machine_arch", {6, 11}, 2}, {"hw.model", {6, 2}, 2}, {"hw.ncpu", {6, 3}, 2}, {"hw.pagesize", {6, 7}, 2}, {"hw.physmem", {6, 5}, 2}, {"hw.realmem", {6, 12}, 2}, {"kern.arandom", {1, 37}, 2}, - {"kern.rng_pseudo", {1, 50}, 2}, + {"kern.clockrate", {1, 12}, 2}, + {"kern.maxfiles", {1, 7}, 2}, + {"kern.maxfilesperproc", {1, 27}, 2}, + {"kern.maxproc", {1, 6}, 2}, {"kern.proc.pathname", {1, 14, 12}, 3}, + {"kern.ps_strings", {1, 32}, 2}, + {"kern.rng_pseudo", {1, 50}, 2}, }; constexpr std::size_t MaximumPath = 1024; if (name == nullptr || size == nullptr) throw std::invalid_argument(std::string(__func__) + ": null name or size"); @@ -427,7 +468,7 @@ int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_ *__error_nid_postfix() = 63; return -1; } - if (nameLength == 0 || std::strcmp(name, "machdep.cacheline_size") == 0) { + if (nameLength == 0 || std::strcmp(name, "hw.cacheline_size") == 0 || std::strcmp(name, "hw.usermem") == 0 || std::strcmp(name, "machdep.cacheline_size") == 0) { *__error_nid_postfix() = 2; return -1; } diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 5c08d7e2d4..5fbecd4dfd 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -152,13 +152,109 @@ static void CheckTscFrequencySysctl() { length = sizeof(value); Require(sysctlbyname_nid_postfix("machdep.tsc_freq", &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); } -static void CheckMachdepCachelineSizeSysctl() { +static bool Untouched(const unsigned char* bytes, std::size_t size) { + for (std::size_t i = 0; i < size; ++i) + if (bytes[i] != 0xaa) return false; + return true; +} +template +static bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} +static void CheckUnansweredSysctl() { + static constexpr const char* names[] = { + "hw.byteorder", + "hw.cacheline_size", + "hw.machine_arch", + "hw.usermem", + "kern.clockrate", + "kern.maxfiles", + "kern.maxfilesperproc", + "kern.maxproc", + "kern.ps_strings", + "kern.sched.name", + "kern.smp.maxcpus", + "kern.stackprot", + "kern.timecounter.hardware", + "machdep.cacheline_size", + }; + unsigned char buffer[64]; + for (const char* name : names) { + std::memset(buffer, 0xaa, sizeof(buffer)); + std::size_t length = sizeof(buffer); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + Require(length == sizeof(buffer) && Untouched(buffer, sizeof(buffer))); + length = 77; + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2 && length == 77); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, buffer, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + Require(Untouched(buffer, sizeof(buffer))); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, nullptr, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + if (std::strcmp(name, "machdep.cacheline_size") == 0) continue; + length = sizeof(buffer); + Require(Throws([&] { sysctlbyname_nid_postfix(name, buffer, &length, buffer, sizeof(buffer)); })); + } + struct Numbered { + const char* name; + int mib[2]; + }; + static constexpr Numbered numbered[] = { + {"hw.byteorder", {6, 4}}, + {"hw.machine_arch", {6, 11}}, + {"kern.clockrate", {1, 12}}, + {"kern.maxfiles", {1, 7}}, + {"kern.maxfilesperproc", {1, 27}}, + {"kern.maxproc", {1, 6}}, + {"kern.ps_strings", {1, 32}}, + }; + for (const auto& entry : numbered) { + int mib[4] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + std::size_t size = 4; + *__error_nid_postfix() = 13; + Require(sysctlnametomib_nid_postfix(entry.name, mib, &size) == 0 && size == 2); + Require(mib[0] == entry.mib[0] && mib[1] == entry.mib[1] && mib[2] == 0x5a5a5a5a); + Require(*__error_nid_postfix() == 13); + std::memset(buffer, 0xaa, sizeof(buffer)); + std::size_t length = sizeof(buffer); + Require(sysctl_nid_postfix(mib, 2, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + Require(length == sizeof(buffer) && Untouched(buffer, sizeof(buffer))); + length = 77; + *__error_nid_postfix() = 13; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2 && length == 77); + *__error_nid_postfix() = 13; + Require(sysctl_nid_postfix(mib, 2, buffer, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 2 && Untouched(buffer, sizeof(buffer))); + *__error_nid_postfix() = 13; + Require(sysctl_nid_postfix(mib, 2, nullptr, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + length = sizeof(buffer); + Require(Throws([&] { sysctl_nid_postfix(mib, 2, buffer, &length, buffer, sizeof(buffer)); })); + } + int mib[4] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + std::size_t size = 0; *__error_nid_postfix() = 13; - int value = 0x5a5a5a5a; - std::size_t length = sizeof(value); - Require(sysctlbyname_nid_postfix("machdep.cacheline_size", &value, &length, nullptr, 0) == -1); - Require(*__error_nid_postfix() == 2); - Require(value == 0x5a5a5a5a); + Require(sysctlnametomib_nid_postfix("kern.maxproc", nullptr, &size) == 0 && size == 2 && *__error_nid_postfix() == 13); + size = 1; + Require(sysctlnametomib_nid_postfix("kern.maxproc", mib, &size) == -1 && *__error_nid_postfix() == 12); + Require(size == 1 && mib[0] == 1 && mib[1] == 0x5a5a5a5a); + mib[0] = 0x5a5a5a5a; + size = 0; + Require(sysctlnametomib_nid_postfix("kern.maxproc", mib, &size) == -1 && *__error_nid_postfix() == 12); + Require(size == 0 && mib[0] == 0x5a5a5a5a); + static constexpr const char* unresolved[] = {"hw.cacheline_size", "hw.usermem", "machdep.cacheline_size"}; + for (const char* name : unresolved) { + int untouched[4] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + size = 4; + *__error_nid_postfix() = 13; + Require(sysctlnametomib_nid_postfix(name, untouched, &size) == -1 && *__error_nid_postfix() == 2); + Require(size == 4 && untouched[0] == 0x5a5a5a5a && untouched[1] == 0x5a5a5a5a && untouched[2] == 0x5a5a5a5a); + } } static void CheckPathnameSysctl() { *__error_nid_postfix() = 13; @@ -663,7 +759,7 @@ int main() { CheckModelSysctl(); #endif CheckTscFrequencySysctl(); - CheckMachdepCachelineSizeSysctl(); + CheckUnansweredSysctl(); CheckPathnameSysctl(); CheckSystemStringSysctl(); CheckSystemStringNames(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 55dfa98df6..aef0f4a47a 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -18,8 +18,8 @@ - [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000); the console's firmware version is not modeled. Measured on PS5 firmware 13.42: record values 0, 24, 39, 40, 41, 48 and 64 in the leading 64-bit field are accepted and preserved, the 28-byte string at offset 8 and packed version at 0x24 are filled, and bytes from 0x28 onward remain untouched. This API reports "13.590.001" / `0x13590001` in that environment, while the Prospero variant reports "13.420.000" / `0x13420000`; their values must not be assumed identical. A null pointer still returns `SCE_KERNEL_ERROR_EFAULT`, which was not measured; other firmware versions and record values remain unverified. - [kern.rng_pseudo](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` draws a new block of 64 bytes from the host's `std::random_device` for each read (no new value) that passes a buffer and a length, and sizes its answers as a payload process on a PS5 (firmware 12.70) sees it: a size query reports 64; a buffer of 64 bytes or more gets 64 bytes and a length of 64 (tried up to 65536); a smaller buffer gets as many bytes as its length, which is left as given, and a null length gets none, both with -1 and `ENOMEM`; a null buffer with a null length returns 0. `errno` is kept when the call succeeds. A write returns -1 with `EPERM`, which was not tried on the console. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator is unknown, and a title process was not measured. On the console `sysctlnametomib` gives MIB {1, 50} for the name, and `sysctl` with it answers reads of 0, 1, 63, 64, 65 and 128 bytes, a size query, a null length and a null buffer with a null length as the name does; here `sysctlnametomib` maps the name to {1, 50} and `sysctl` {1, 50} answers as the name does; a write through the MIB was not measured and throws - [sysconf](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `_SC_CLK_TCK` returns 128, FreeBSD's `CLK_TCK` as its [`sysconf`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L100-L101) returns it ([`time.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/include/time.h#L48-L54)); the console's value is not checked, and no `times` reports ticks at that rate. CPython's `posix` module stops its import with "cannot read ticks_per_second" when the value is below 1, as the Python in PS5MC (PPSA99420) does. `_SC_ARG_MAX` returns 262144 (FreeBSD's `NCARGS`), `_SC_OPEN_MAX` returns the `RLIMIT_NOFILE` soft limit via `getrlimit` ([`sysconf.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L106-L115)), and `_SC_IOV_MAX` returns 1024 (FreeBSD's `UIO_MAXIOV`). Names other than `_SC_ARG_MAX`, `_SC_CLK_TCK`, `_SC_OPEN_MAX`, `_SC_PAGESIZE`, `_SC_IOV_MAX`, `_SC_NPROCESSORS_CONF`, `_SC_NPROCESSORS_ONLN` and `_SC_PHYS_PAGES` fail with `EINVAL` -- [hw.realmem](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_HW`, 12} and `sysctlbyname` report the same 8-byte value as `hw.physmem` (the `_SC_PHYS_PAGES` pages of 16 KiB), read-only with `EPERM` for a write and `ENOMEM` for a short buffer, as FreeBSD's [`sysctl_hw_realmem`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_mib.c#L191-L204) does. FreeBSD reports there the memory the firmware describes, which is at least `hw.physmem`; the console's values are not known. Dolphin reads it as the machine's memory size in [`MemPhysical`](https://github.com/dolphin-emu/dolphin/blob/8d2ad1a662472955497e5bb79d7b9ec6999e1f0d/Source/Core/Common/MemoryUtil.cpp#L253-L273) (RetroArch PPSA99169's `dolphin_libretro` core) -- [vm.overcommit](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the 4-byte value 0, the default FreeBSD gives its [`vm.overcommit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/swap_pager.c#L158-L161) `int`, with `ENOMEM` for a short buffer; the console's value is not known and a write throws. mimalloc reads it in [`unix_detect_overcommit`](https://github.com/microsoft/mimalloc/blob/31d034d94cdb8e22f7d7ed55967f581a2d6e831d/src/prim/unix/prim.c#L129-L153) and takes 0 as no overcommit, as PS5MC (PPSA99420) does at start-up +- [hw.realmem](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_HW`, 12} and `sysctlbyname` report the same 8-byte value as `hw.physmem` (the `_SC_PHYS_PAGES` pages of 16 KiB), read-only with `EPERM` for a write and `ENOMEM` for a short buffer, as FreeBSD's [`sysctl_hw_realmem`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_mib.c#L191-L204) does. FreeBSD reports there the memory the firmware describes, which is at least `hw.physmem`. A payload process on a PS5 (firmware 12.70) gets -1 with `ENOENT` for `hw.physmem` and `hw.realmem` (a read, a size query and a null length) and for {`CTL_HW`, 5} and {`CTL_HW`, 12} (a read and a size query); what a title process gets was not measured. Dolphin reads it as the machine's memory size in [`MemPhysical`](https://github.com/dolphin-emu/dolphin/blob/8d2ad1a662472955497e5bb79d7b9ec6999e1f0d/Source/Core/Common/MemoryUtil.cpp#L253-L273) (RetroArch PPSA99169's `dolphin_libretro` core) +- [vm.overcommit](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the 4-byte value 0, the default FreeBSD gives its [`vm.overcommit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/swap_pager.c#L158-L161) `int`, with `ENOMEM` for a short buffer, and a write throws. A payload process on a PS5 (firmware 12.70) gets -1 with `ENOENT` for it, for a read, a size query and a null length; what a title process gets was not measured. mimalloc reads it in [`unix_detect_overcommit`](https://github.com/microsoft/mimalloc/blob/31d034d94cdb8e22f7d7ed55967f581a2d6e831d/src/prim/unix/prim.c#L129-L153) and takes 0 as no overcommit, as PS5MC (PPSA99420) does at start-up - [kern.sdk_version](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the system software version as the 4-byte value `0x01000000` (1.00), the value `sceKernelGetSystemSwVersion` reports; a write returns -1 with `EPERM`. The major version is byte 3 and the minor version byte 2, in hex digits that read as decimal (13.60 is `0x1360xxxx`), as [Capy Mobile](https://github.com/khalifa007/CapyMobile/blob/2758c016/src/ps5/platform.c#L64-L71) (PPSA92280), [remote_lua_loader](https://github.com/n0llptr/remote_lua_loader/blob/7c2840e475/savedata/misc.lua#L195-L209) and the [lapse payload](https://github.com/NetflixNHack/Netflix-N-Hack/blob/605eb93ba5/PS4/payloads/lapse.js#L436-L450) read it. Capy Mobile only displays it. The console's firmware version is not modeled and the low two bytes are unknown - [kern.ostype, kern.osrelease, kern.version, kern.hostname, hw.machine](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` (1.1, 1.2, 1.4, 1.10, 6.1), `sysctlbyname` and `sysctlnametomib` serve the names `uname` reads as read-only strings: "FreeBSD", "11.4-RELEASE" and "FreeBSD 11.4-RELEASE\n", the values FreeBSD 11.4-RELEASE builds without source control information ([newvers.sh](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/conf/newvers.sh#L45-L52), [L293-L294](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/conf/newvers.sh#L293-L294)), the host name `gethostname` reports ("AnyPS5") and "amd64"; a short buffer returns `ENOMEM` and a write `EPERM`. The console's strings are not measured - [sceKernelGetProsperoSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports system software version `0x01000000` ("01.000.000"); the console's firmware version is not modeled. The 0x28-byte block (a 64-bit size at 0, the version string at 8 and the packed version at 0x24) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Multi Tools (PPSA99110) and Prospero Explorer (PPSA99109) are built with and which sets the size to 40, and the same fields in [etaHEN](https://github.com/etaHEN/etaHEN/blob/main/Source%20Code/daemon/include/ipc.hpp) and [shsrv](https://github.com/ps5-payload-dev/shsrv/blob/master/sh.c). A size other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL` without writing; whether the console checks the size is unverified, since etaHEN and shsrv declare a 0x30-byte block and do not set it @@ -166,7 +166,7 @@ - [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `AMD64_GET_FSBASE`, `AMD64_GET_GSBASE` and `AMD64_SET_GSBASE` follow FreeBSD 11's amd64 [`sysarch`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c) on Linux x86-64 hosts, through the host's `arch_prctl`. The GS base belongs to the calling thread, a base at or above `0x800000000000` (`VM_MAXUSER_ADDRESS`) returns `EINVAL`, and a null argument returns `EFAULT`. `AMD64_SET_FSBASE` throws, since guest and host code share the thread's FS base for their TLS. In a payload process on a PS5 (firmware 12.70), `I386_GET_FSBASE` (7) and `I386_GET_GSBASE` (9) wrote 4 bytes, the low 32 bits of the FS and GS bases, and failed with `EFAULT` for a null argument; here they do the same on Linux x86-64 hosts. There a GS base set by one thread left the main thread's unchanged, and threads made with `pthread_create` and `scePthreadCreate` started with their creator's GS base; here a guest thread's host thread is created by the creating thread, and Linux's [`copy_thread`](https://github.com/torvalds/linux/blob/v6.6/arch/x86/kernel/process.c#L182-L186) gives a new thread its creator's GS base. The operation numbers FreeBSD 11's amd64 `sysarch` has no case for returned `EINVAL` there for the nine tried, each with a null argument (2, 5, 6, 12, 127, 133, 134, 1000 and -1; 6 is `I386_VM86`, which its header names), as its [default case](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c#L324-L325) does; here every such number returns `EINVAL` on every host; the numbers other than those nine follow that default case and were not measured. `AMD64_GET_XFPUSTATE` (132) returned 0 there for lengths 0, 64, 128, 256 and 512 (512 was also the largest length a search accepted) and failed with `EINVAL` for 1024, 2048, 4096 and 8192 and with `EFAULT` for a null argument or address, and `I386_GET_XFPUSTATE` (11) failed with `EFAULT` for a null argument; both throw here. The other operations FreeBSD 11's amd64 `sysarch` handles, and every operation it handles on other hosts (Windows keeps its TEB in GS), throw. - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. - [kern.proc.vmmap](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_VMMAP`, pid} returns packed FreeBSD 11 `kinfo_vmentry` records (144 bytes each, an empty path) for the used ranges of the guest address-space arena, in address order, as [`kinfo_getvmmap`](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libutil/kinfo_getvmmap.c) reads them. A range is one record: the type is always `KVME_TYPE_DEFAULT`, the protection is `sceKernelVirtualQuery`'s for the range's first page (0 where it reports none), and offset, object, resident page counts, flags and path are 0. Only the calling process's pid is accepted (`ESRCH` otherwise, -1 included, as FreeBSD's `pget` gives); a write returns `EPERM` and a short buffer `ENOMEM`. Ranges outside the arena, which guest code cannot map, are not reported. The walk takes the arena lock once per range, not for the whole walk as FreeBSD holds the map lock, so an `mmap` or `munmap` on another thread can change the list between two records. Whether a PS5 application may read its own map, and what types and paths the console reports, is unverified -- [machdep.cacheline_size](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns -1 with `ENOENT`, matching FreeBSD amd64 where this sysctl OID is not defined. +- [sysctl names a PS5 payload process gets ENOENT for](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` returns -1 with `ENOENT` for `hw.byteorder`, `hw.cacheline_size`, `hw.machine_arch`, `hw.usermem`, `kern.clockrate`, `kern.maxfiles`, `kern.maxfilesperproc`, `kern.maxproc`, `kern.ps_strings`, `kern.sched.name`, `kern.smp.maxcpus`, `kern.stackprot`, `kern.timecounter.hardware` and `machdep.cacheline_size`, and `sysctl` does the same for the MIBs {1, 6}, {1, 7}, {1, 12}, {1, 27}, {1, 32}, {6, 4} and {6, 11}. The buffer and the length are left alone. A payload process on a PS5 (firmware 12.70) gets this for a read, a size query, a null length and a null buffer with a null length, by name and by MIB. `sysctlnametomib` gives those seven MIBs, and returns `ENOENT` for `hw.cacheline_size`, `hw.usermem` and `machdep.cacheline_size` without touching the count or the first three words of the MIB, as the console does. A write to these names and MIBs throws, except for `machdep.cacheline_size`, which returns `ENOENT` as FreeBSD amd64 does for an OID it does not define; the console's answer to a write was not tried. The console's `sysctlnametomib` gives `kern.smp.maxcpus`, `kern.stackprot`, `kern.sched.name` and `kern.timecounter.hardware` the MIBs {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}, and its `sysctl` returns `ENOENT` for them; here `sysctlnametomib` throws for them. `hw.physmem`, `hw.realmem` and `vm.overcommit` return `ENOENT` on the console too but report values here, as the `hw.realmem` and `vm.overcommit` entries say. A payload process also gets `ENOENT` for `kern.anyps5_no_such_name`, which its `sysctlnametomib` does not resolve; other names still throw here. A title process was not measured - [hw.pagesize](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the system page size (16384 bytes, PS5_PAGE_SIZE) as an integer through sysctl MIB {6, 7} (CTL_HW, HW_PAGESIZE) and the "hw.pagesize" sysctlbyname lookup, following FreeBSD's [`hw.pagesize`](https://github.com/freebsd/freebsd-src/blob/main/sys/kern/kern_mib.c#L177), a read-only `SYSCTL_INT` of `PAGE_SIZE`. The value on a PS5 follows the 16 KiB page size the rest of libkernel uses and is unmeasured. A write returns -1 with EPERM. - [hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the [host CPU's name](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp), built as FreeBSD 11 builds `hw.model`: the CPUID brand string (leaves 0x80000002 to 0x80000004) without leading spaces ([identcpu.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/identcpu.c#L683-L691)). Trailing padding stays, as it does there, so an AMD host's name ends in spaces. The name a PS5 reports is unknown. A write returns -1 with `EPERM`. A host without an x86 CPUID brand string, such as an Apple Silicon Mac, throws. PPSA99203 only prints it (`[console] hw.model: %s`) - [Host CPU check](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp) (libkernel) - when libkernel loads on a Linux or Windows x86-64 host, it logs which of the PS5 CPU's user-mode extensions CPUID reports and throws, naming the host CPU, if any extension is missing that LLVM selects from plain C/C++ for the PS5's Zen 2 (`-march=znver2`, [feature list](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/llvm/lib/Target/X86/X86.td#L1459-L1513)): SSE to SSE4.2, SSSE3, POPCNT, LZCNT, BMI1, BMI2, MOVBE, CMPXCHG16B, LAHF/SAHF, AVX, AVX2, FMA, F16C, and the operating system's AVX state in XCR0 (#3719). SSE4A, CLZERO and SHA, which `--to-intel` converts, and AES, PCLMULQDQ, RDRAND, RDSEED, ADX, CLFLUSHOPT, CLWB, RDPID, XSAVEOPT, XSAVEC and PREFETCHW, which compilers emit only for intrinsics or explicit prefetches, are logged but not required: a title that executes one of them on a host without it still stops with an illegal instruction. macOS hosts are not checked: some Rosetta versions execute extensions without reporting them through CPUID, but support differs by host. ANDN and RDRAND fault on an M1 with macOS 15.3.1; see the [Rosetta conversion limits](relinker.md) From b455c751bbb59db143dd44e10d891895cabc7c0c Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 00:48:05 +1300 Subject: [PATCH 124/219] fix(libc): leave errno alone for subnormal strtod results The console's strtof, strtod and strtold set ERANGE only when a conversion underflows to zero; a non-zero subnormal result leaves errno unchanged, while the host's sets it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libc/src/Strings.cpp | 2 ++ core/libs/tests/GuestMath.cpp | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 8774ef52ff..75e37301e1 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -183,7 +183,9 @@ TFloat ParseFloat(const char* text, char** end, TFloat (*parse)(const char*, cha return value; } char* parsedEnd = nullptr; + const int saved = errno; const TFloat value = parse(text, &parsedEnd); + if (errno == ERANGE && value != 0 && (value < 0 ? -value : value) < std::numeric_limits::min()) errno = saved; if (end != nullptr) *end = parsedEnd; return parsedEnd == text ? TFloat{0} : value; } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 98c40e78fc..0b038daacc 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -449,6 +449,22 @@ static void CheckNanConversions() { } } +static void CheckFloatUnderflowErrno() { + char* end = nullptr; + errno = 7; + Require(strtod_nid_postfix("1e-310", &end) == 1e-310 && errno == 7); + Require(strtod_nid_postfix("-4.9406564584124654e-324", &end) == -0x1p-1074 && errno == 7); + Require(strtof_nid_postfix("1e-40", &end) == 1e-40f && errno == 7); + Require(strtold_nid_postfix("1e-4940", &end) == 1e-4940L && errno == 7); + Require(strtod_nid_postfix("1e-330", &end) == 0.0 && errno == ERANGE); + errno = 0; + Require(strtof_nid_postfix("1e-46", &end) == 0.0f && errno == ERANGE); + errno = 0; + Require(strtod_nid_postfix("1e400", &end) == HUGE_VAL && errno == ERANGE); + errno = 0; + Require(strtod_nid_postfix("2.2250738585072014e-308", &end) == 0x1p-1022 && errno == 0); +} + static void CheckHypotSpecialValue() { Require(std::bit_cast(hypot_nid_postfix(INFINITY, std::bit_cast(0x7ff0000000000001ull))) == 0x7ff0000000000000ull); Require(std::bit_cast(hypot_nid_postfix(std::bit_cast(0x7ff0000000000001ull), INFINITY)) == 0x7ff0000000000000ull); @@ -1358,6 +1374,7 @@ int main() { CheckSinhf(); CheckAtanhf(); CheckHypotSpecialValue(); + CheckFloatUnderflowErrno(); CheckFmodErrno(); CheckDoubleClassification(); CheckIntegerAbsAndDivision(); From 28d0b1664bafa4b3a18ec43fd87306b5ad620f33 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:49:54 +0300 Subject: [PATCH 125/219] fix(libc): address Windows backtrace review feedback --- core/libs/prx/libc/CMakeLists.txt | 2 -- core/libs/prx/libc/src/Backtrace.cpp | 2 +- core/libs/tests/GuestBacktrace.cpp | 17 ++++++++++++----- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index cb8591199c..ea9fae155b 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -89,8 +89,6 @@ target_sources(libc PRIVATE target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/BinaryWordIo.cpp) - - target_include_directories(libc PRIVATE ${LIBS_INCLUDE_DIR} ) diff --git a/core/libs/prx/libc/src/Backtrace.cpp b/core/libs/prx/libc/src/Backtrace.cpp index 53662a77d0..b38d8dfdcb 100644 --- a/core/libs/prx/libc/src/Backtrace.cpp +++ b/core/libs/prx/libc/src/Backtrace.cpp @@ -75,7 +75,7 @@ static_assert(sizeof(FrameRecord) == NameOffset); TraceState state{addresses, MaxFrames, 0}; _Unwind_Backtrace_nid_postfix(traceCallback, &state); int captured = state.count; - if (captured == 0) { + if (captured <= 2) { captured = static_cast(RtlCaptureStackBackTrace(0, MaxFrames, addresses, nullptr)); } #endif diff --git a/core/libs/tests/GuestBacktrace.cpp b/core/libs/tests/GuestBacktrace.cpp index e2a29db117..7ae55383e3 100644 --- a/core/libs/tests/GuestBacktrace.cpp +++ b/core/libs/tests/GuestBacktrace.cpp @@ -70,6 +70,11 @@ std::size_t Used(const std::vector& records) { return total; } +[[gnu::noinline]] std::uintptr_t CurrentIp() { + asm volatile("" ::: "memory"); + return reinterpret_cast(__builtin_return_address(0)); +} + } [[gnu::noinline]] void CheckBacktrace() { @@ -96,7 +101,10 @@ std::size_t Used(const std::vector& records) { std::vector buffer(size + 64, Fill); int written = -1; - Require(sceLibcBacktraceSelf_nid_postfix(-1, buffer.data(), size, &written) == 0, "Self failed"); + const auto before = CurrentIp(); + const int selfStatus = sceLibcBacktraceSelf_nid_postfix(-1, buffer.data(), size, &written); + const auto after = CurrentIp(); + Require(selfStatus == 0, "Self failed"); Require(written == count, "Self wrote a different count than GetBufferSize reported"); const auto records = Parse(buffer, size, written); Require(Used(records) == size, "Self used a different size than GetBufferSize reported"); @@ -104,11 +112,10 @@ std::size_t Used(const std::vector& records) { for (const auto& record : records) Require(record.reserved == 0 && !record.module.empty(), "record has a nonzero reserved word or no module"); Require(records.front().module == executable, "first frame is not in the calling module"); #if defined(_WIN32) - HMODULE callerHandle = nullptr; const auto address = reinterpret_cast(handle) + records.front().offset; - Require(GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, - reinterpret_cast(address), &callerHandle) != 0 && callerHandle == handle, - "first frame is not the caller"); + const auto callerReturn = reinterpret_cast(handle) + records[1].offset; + Require(address > before && address < after, "first frame is not the caller"); + Require(callerReturn == reinterpret_cast(__builtin_return_address(0)), "second frame is not the caller of CheckBacktrace"); #else Dl_info caller{}; const auto address = reinterpret_cast(self.dli_fbase) + records.front().offset; From b67e2a3f2f5cd6121599ad742b9b2be4b15b234b Mon Sep 17 00:00:00 2001 From: Swetabh Singh Date: Mon, 19 Oct 2026 00:47:36 +0530 Subject: [PATCH 126/219] feat(libc): implement sceLibcPafMspacePosixMemalign --- core/libs/prx/libc/src/Mspace.cpp | 5 +++++ core/libs/tests/Mspace.cpp | 13 +++++++++++++ docs/dev/debt/libc.md | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Mspace.cpp b/core/libs/prx/libc/src/Mspace.cpp index 01e8de2a22..8830e5c75c 100644 --- a/core/libs/prx/libc/src/Mspace.cpp +++ b/core/libs/prx/libc/src/Mspace.cpp @@ -339,6 +339,11 @@ void* APS5_VABI sceLibcPafMspaceReallocalign_nid_postfix(void* handle, void* poi return sceLibcMspaceReallocalign_nid_postfix(handle, pointer, alignment, size); } +int APS5_VABI sceLibcPafMspacePosixMemalign_nid_postfix(void* handle, void** result, std::size_t alignment, std::size_t size, const void* owner) { + (void)owner; + return sceLibcMspacePosixMemalign_nid_postfix(handle, result, alignment, size); +} + int APS5_VABI sceLibcPafMspaceMallocStats_nid_postfix(void* handle, MallocManagedSize* stats) { return sceLibcMspaceMallocStats_nid_postfix(handle, stats); } diff --git a/core/libs/tests/Mspace.cpp b/core/libs/tests/Mspace.cpp index 2224e2b7a1..0741744299 100644 --- a/core/libs/tests/Mspace.cpp +++ b/core/libs/tests/Mspace.cpp @@ -30,6 +30,8 @@ void APS5_VABI sceLibcPafMspaceFree_nid_postfix(void*, void*, const void*); void* APS5_VABI sceLibcPafMspaceCalloc_nid_postfix(void*, std::size_t, std::size_t, const void*); void* APS5_VABI sceLibcPafMspaceRealloc_nid_postfix(void*, void*, std::size_t, const void*); void* APS5_VABI sceLibcPafMspaceReallocalign_nid_postfix(void*, void*, std::size_t, std::size_t, const void*); +void* APS5_VABI sceLibcPafMspaceMemalign_nid_postfix(void*, std::size_t, std::size_t, const void*); +int APS5_VABI sceLibcPafMspacePosixMemalign_nid_postfix(void*, void**, std::size_t, std::size_t, const void*); int APS5_VABI sceLibcPafMspaceMallocStats_nid_postfix(void*, void*); int APS5_VABI sceLibcPafMspaceMallocStatsFast_nid_postfix(void*, void*); } @@ -64,6 +66,15 @@ static void CheckPaf() { Require(block != nullptr && block[0] == 0x5a && block[39] == 0x5a); block = static_cast(sceLibcPafMspaceReallocalign_nid_postfix(arena, block, 256, 64, &owner)); Require(block != nullptr && reinterpret_cast(block) % 256 == 0 && block[39] == 0x5a); + auto* aligned = static_cast(sceLibcPafMspaceMemalign_nid_postfix(arena, 128, 50, &owner)); + Require(aligned != nullptr && reinterpret_cast(aligned) % 128 == 0); + Require(sceLibcPafMspaceMemalign_nid_postfix(arena, 0, 50, &owner) == nullptr && *__error_nid_postfix() == 22); + Require(sceLibcPafMspaceMemalign_nid_postfix(arena, 15, 50, &owner) == nullptr && *__error_nid_postfix() == 22); + void* posixAligned = nullptr; + Require(sceLibcPafMspacePosixMemalign_nid_postfix(arena, &posixAligned, 256, 60, &owner) == 0); + Require(posixAligned != nullptr && reinterpret_cast(posixAligned) % 256 == 0); + Require(sceLibcPafMspacePosixMemalign_nid_postfix(arena, nullptr, 256, 60, &owner) == 22); + Require(sceLibcPafMspacePosixMemalign_nid_postfix(arena, &posixAligned, 15, 60, &owner) == 22); MallocManagedSize viaPaf{}, viaMspace{}, fastPaf{}, fastMspace{}; viaPaf.size = viaMspace.size = fastPaf.size = fastMspace.size = sizeof(MallocManagedSize); viaPaf.version = viaMspace.version = fastPaf.version = fastMspace.version = 1; @@ -73,6 +84,8 @@ static void CheckPaf() { Require(fastPaf.currentInuseSize == fastMspace.currentInuseSize); sceLibcPafMspaceFree_nid_postfix(arena, block, &owner); sceLibcPafMspaceFree_nid_postfix(arena, zeroed, arena); + sceLibcPafMspaceFree_nid_postfix(arena, aligned, &owner); + sceLibcPafMspaceFree_nid_postfix(arena, posixAligned, &owner); sceLibcPafMspaceFree_nid_postfix(arena, nullptr, &owner); Require(sceLibcPafMspaceDestroy_nid_postfix(arena) == 0); Require(sceLibcPafMspaceDestroy_nid_postfix(arena) == -1); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 931d7d1746..06d90d60cb 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -20,7 +20,7 @@ - [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error - [sceLibcInternalHeapErrorReportForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(void* heap, void* block, uint32 error)` follows the same `libc.prx` (call at 0xF634): it passes the heap, the block and the nonzero result of its block check, and then calls a function that never returns, so the report only has to return. The meaning of the error values is unknown; the three values and the guest return address are written to the host standard error -- [sceLibcPafMspaceCreate, Destroy, Malloc, Memalign, Free, Calloc, Realloc, Reallocalign, MallocStats and MallocStatsFast](../../../core/libs/prx/libc/src/Mspace.cpp) - PAF exports delegate to `sceLibcMspace` without malloc hooks. `Memalign` is inferred as `(handle, alignment, size, owner)`, delegates to `sceLibcMspaceMemalign`, and is unverified on console. `Create` has an extra options pointer with a partly observed layout (version, type 1 or 2 at +2, field at +0x10); non-null options throw because the field's meaning is unknown. Allocation owners are ignored because block footers are not modelled. On PS5 firmware 13.60, heap-owner calls worked; wrong owners in `Reallocalign` or in-place `Realloc` terminate the process, and `MallocStats` returns 1 if non-heap-owned blocks exist. These owner results are not reproduced. `PosixMemalign`, `MallocUsableSize`, `Trim`, `IsHeapEmpty`, `CheckMemoryBounds`, `ReportMemoryBlocks` and `GetFooterValue` remain unimplemented. +- [sceLibcPafMspaceCreate, Destroy, Malloc, Memalign, PosixMemalign, Free, Calloc, Realloc, Reallocalign, MallocStats and MallocStatsFast](../../../core/libs/prx/libc/src/Mspace.cpp) - PAF exports delegate to `sceLibcMspace` without malloc hooks; `Memalign` and `PosixMemalign` forward to their `Mspace` counterparts with owner ignored and are unverified on console. `Create`'s options block (version, type 1 or 2 at +2, field at +0x10) throws when non-null as its meaning is unknown. Block footers are not modelled, so owners are ignored: on PS5 firmware 13.60 mismatched owners in `Reallocalign` or in-place `Realloc` kill the process and `MallocStats` returns 1 if non-heap-owned blocks exist, which is not reproduced. `MallocUsableSize`, `Trim`, `IsHeapEmpty`, `CheckMemoryBounds`, `ReportMemoryBlocks` and `GetFooterValue` are not implemented. - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` - [malloc_stats_fast](../../../core/libs/prx/libc/src/GuestHeap.cpp) (libc) - with the default allocator it fills the 0x28-byte managed-size block as `sceLibcMspaceMallocStatsFast` does, from the default heap's own accounting, not Sony's allocator: the system sizes count the 1 MiB spans of its small-block classes and its committed large blocks, cached free ones included, and the in-use sizes count each handed-out block at its size-class or 64 KiB-granule size, header included. A block size below 0x28 or a null pointer returns 22, as in the mspace version; the version field is not checked. Ship of Harkinian (PPSA99320) calls it about 10 s after start-up. - [malloc, calloc, realloc, memalign, aligned_alloc and reallocalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - follow FreeBSD 11.4 `contrib/jemalloc/src/jemalloc.c` for failures, not measured on the console: a size the default heap rejects or the guest arena cannot place (including a `calloc` product that overflows) returns null with `errno` `ENOMEM`, and `memalign` and `aligned_alloc` return null with `EINVAL` for an alignment that is zero or not a power of two. FreeBSD's libc has no `memalign`, so setting `errno` there is assumed. A host commit failure for a block that fits in the arena (Windows `ERROR_NOT_ENOUGH_MEMORY`, `ERROR_OUTOFMEMORY` or `ERROR_COMMITMENT_LIMIT`, Linux `ENOMEM`) resets and releases the reserved range and returns null with `ENOMEM`; any other commit failure releases the range and throws. `reallocalign` returns null with `EINVAL` for an invalid alignment and `ENOMEM` like the others, which is an assumption as it is not in FreeBSD. `realloc(ptr, 0)` frees the block and returns null, where FreeBSD 11.4's jemalloc 4.5 resizes it to one byte and returns a block From dbd849d3c17a9d30a98c7459c88ca75d83229675 Mon Sep 17 00:00:00 2001 From: Swetabh Singh Date: Mon, 19 Oct 2026 00:51:52 +0530 Subject: [PATCH 127/219] feat(libc): implement significand --- core/libs/prx/libc/src/Math.cpp | 1 + core/libs/tests/GuestMath.cpp | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 063eca4f01..1ecf8b9233 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -283,6 +283,7 @@ double APS5_VABI scalbn_nid_postfix(double x, int exponent) { return std::scalbn float APS5_VABI scalbnf_nid_postfix(float x, int exponent) { return std::scalbn(x, exponent); } long double APS5_VABI scalbnl_nid_postfix(long double x, int exponent) { return std::scalbn(x, exponent); } float APS5_VABI significandf_nid_postfix(float x) { return x == 0.0f || !std::isfinite(x) ? x : std::scalbn(x, -std::ilogb(x)); } +double APS5_VABI significand_nid_postfix(double x) { return x == 0.0 || !std::isfinite(x) ? x : std::scalbn(x, -std::ilogb(x)); } double APS5_VABI frexp_nid_postfix(double x, int* exponent) { return std::frexp(x, exponent); } float APS5_VABI frexpf_nid_postfix(float x, int* exponent) { return std::frexp(x, exponent); } // Guest long is 64-bit, including on Windows where native long is 32-bit. diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 98c40e78fc..9f711cf64f 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -141,6 +141,7 @@ std::int64_t APS5_VABI llabs_nid_postfix(std::int64_t); std::int64_t APS5_VABI imaxabs_nid_postfix(std::int64_t); long double APS5_VABI scalbnl_nid_postfix(long double, int); float APS5_VABI significandf_nid_postfix(float); +double APS5_VABI significand_nid_postfix(double); long double APS5_VABI acoshl_nid_postfix(long double); long double APS5_VABI asinhl_nid_postfix(long double); long double APS5_VABI atanhl_nid_postfix(long double); @@ -936,6 +937,11 @@ static void CheckScaling() { Require(std::signbit(significandf_nid_postfix(-0.0f)) && significandf_nid_postfix(-0.0f) == 0.0f); Require(significandf_nid_postfix(std::numeric_limits::infinity()) == std::numeric_limits::infinity()); Require(std::isnan(significandf_nid_postfix(std::numeric_limits::quiet_NaN()))); + Require(significand_nid_postfix(12.0) == 1.5 && significand_nid_postfix(-0.375) == -1.5); + Require(significand_nid_postfix(0x1p-1074) == 1.0); + Require(std::signbit(significand_nid_postfix(-0.0)) && significand_nid_postfix(-0.0) == 0.0); + Require(significand_nid_postfix(std::numeric_limits::infinity()) == std::numeric_limits::infinity()); + Require(std::isnan(significand_nid_postfix(std::numeric_limits::quiet_NaN()))); } static bool Near(long double actual, long double expected) { From 928a11bf2af54d8de8a3a25a821e192d0816ca0f Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 20 Oct 2026 12:56:20 +0100 Subject: [PATCH 128/219] fix(libkernel): keep the Windows APR read staging buffer off the stack Windows APR reads are staged through a 64 KiB std::array on the stack of the thread that submits them (f7953ecac). Titles submit reads from fibers whose stacks they allocate themselves, which can be smaller than that and have no guard page, so the array runs past the bottom of the fiber stack and the file data lands on whatever lies below it. Demon's Souls (PPSA01341) then crashes in scePthreadMutexLock about half a second after start, on a mutex that holds bytes of playgo-chunkdefs.xml. The staging buffer is now allocated for each read, at most 64 KiB. The new guest_apr_read_stack test reads on a thread with a 64 KiB stack; with the stack array it ends in a stack overflow. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Apr/src/Apr.cpp | 2 +- core/libs/prx/libkernel/CMakeLists.txt | 2 +- core/libs/tests/GuestAprRead.cpp | 30 ++++++++++++++++++++++++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 67847b576f..42e4274d9e 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -166,7 +166,7 @@ void _readFile(const Apr::ReadFileCommand& command) { const GuestArena::HostWrite destination(reinterpret_cast(command.destination), command.size); if (!destination.Open()) throw std::runtime_error("APR: the read destination of " + file.path.string() + " is not writable guest memory"); #ifdef _WIN32 - std::array staging; + std::vector staging(static_cast(std::min(command.size, 64u * 1024u))); std::uint64_t read = 0; while (read < command.size) { const auto wanted = static_cast(std::min(command.size - read, staging.size())); diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 3b2ed8af91..eea37303b1 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -109,7 +109,7 @@ if(BUILD_TESTING) configure_windows_unwind(guest_apr_read_tests) set(aprReadCases host direct eof errors streams) if(WIN32) - list(APPEND aprReadCases fault) + list(APPEND aprReadCases fault stack) endif() foreach(aprReadCase IN LISTS aprReadCases) set(aprReadRun ${CMAKE_CURRENT_BINARY_DIR}/apr_read_${aprReadCase}) diff --git a/core/libs/tests/GuestAprRead.cpp b/core/libs/tests/GuestAprRead.cpp index d2f12108a7..a8ba636dc3 100644 --- a/core/libs/tests/GuestAprRead.cpp +++ b/core/libs/tests/GuestAprRead.cpp @@ -228,13 +228,39 @@ private: void* pointer = nullptr; void* handler = nullptr; }; + +struct SmallStackRead { + const Fixture* fixture = nullptr; + std::vector* storage = nullptr; + std::string error; +}; + +DWORD WINAPI ReadOnSmallStack(void* parameter) { + auto& read = *static_cast(parameter); + try { + CheckRead(*read.fixture, read.storage->data() + 17, 65537, 0); + } catch (const std::exception& failure) { + read.error = failure.what(); + } + return 0; +} + +void TestSmallStack(const Fixture& fixture) { + std::vector storage(131200, 0xCC); + SmallStackRead read{&fixture, &storage, {}}; + const HANDLE thread = CreateThread(nullptr, 64u * 1024u, ReadOnSmallStack, &read, STACK_SIZE_PARAM_IS_A_RESERVATION, nullptr); + Require(thread != nullptr, "cannot start the small-stack APR reader"); + WaitForSingleObject(thread, INFINITE); + CloseHandle(thread); + Require(read.error.empty(), read.error.c_str()); +} #endif } int main(int argc, char** argv) { try { - Require(argc == 2, "expected host, direct, eof, errors or fault"); + Require(argc == 2, "expected host, direct, eof, errors, streams, fault or stack"); const Fixture fixture; const std::string mode = argv[1]; if (mode == "host") { @@ -254,6 +280,8 @@ int main(int argc, char** argv) { const FaultBuffer storage; TestRead(fixture, storage.Data()); Require(committedPages > 2, "APR did not exercise demand-committed destination pages"); + } else if (mode == "stack") { + TestSmallStack(fixture); #endif } else { throw std::runtime_error("unknown APR read case"); From ab8cb4f9ad14c846ee2afdecb4267b689590d798 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 20 Oct 2026 14:11:14 +0200 Subject: [PATCH 129/219] feat(libc): export wcstombs_s libc.prx did not export wcstombs_s (sZLrjx-yEx4); only libSceAmpr did. Ghost of Yotei imports it from libc.prx and stops at symbol resolution. Move the implementation to libc next to wcsrtombs_s. libSceAmpr keeps its export through an explicit-NID wrapper, because the NID patcher leaves symbols that libc exports unrenamed in other libraries. --- core/libs/prx/libSceAmpr/Export.cpp | 13 ++++++------- core/libs/prx/libc/src/Multibyte.cpp | 8 ++++++++ core/libs/tests/GuestAmprThreads.cpp | 22 +++++++++++----------- core/libs/tests/GuestWcsrtombsS.cpp | 19 +++++++++++++++++++ 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index fd0d7cf527..f48e2098f5 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -7,7 +7,6 @@ #include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include -#include #include #include #include @@ -272,7 +271,7 @@ int APS5_VABI _Cnd_init_nid_postfix(void** handle); void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); int APS5_VABI _Cnd_wait_nid_postfix(void** condition, void** mutex); int APS5_VABI _Cnd_signal_nid_postfix(void** handle); -int APS5_VABI wcsrtombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t** source, std::size_t limit, void* state); +int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit); [[noreturn]] void APS5_VABI __cxa_call_terminate_nid_postfix(void* exception); int APS5_VABI _Cnd_init_nid_postfix(void** handle); int SignalCondition_nid_no_patch(void** handle); @@ -992,11 +991,6 @@ void APS5_VABI _Lock_shared_ptr_spin_lock_nid_postfix(void) { } -int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit) { - std::array state{}; - return wcsrtombs_s_nid_postfix(result, destination, capacity, &source, limit, state.data()); -} - [[noreturn]] void APS5_VABI __cxa_call_unexpected_nid_postfix(void* unwindException) { __cxa_call_terminate_nid_postfix(unwindException); } @@ -1046,6 +1040,11 @@ void APS5_VABI sceAmprIgnoreHandlerS(const char* message, void* pointer, int err SonyIgnoreHandlerS(message, pointer, error); } +APS5_EXPORT("sZLrjx-yEx4", sceAmprWcstombsS); +int APS5_VABI sceAmprWcstombsS(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit) { + return wcstombs_s_nid_postfix(result, destination, capacity, source, limit); +} + APS5_EXPORT("VUzjXknPPBs", sceAmprMbstowcs); std::size_t APS5_VABI sceAmprMbstowcs(std::uint16_t* dest, const char* src, std::size_t size) { return SonyMbstowcs(dest, src, size); diff --git a/core/libs/prx/libc/src/Multibyte.cpp b/core/libs/prx/libc/src/Multibyte.cpp index 5865fe5933..e7dc400ac3 100644 --- a/core/libs/prx/libc/src/Multibyte.cpp +++ b/core/libs/prx/libc/src/Multibyte.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/SonyCrt.hpp" +#include #include #include #include @@ -303,6 +304,13 @@ int APS5_VABI wcsrtombs_s_nid_postfix( return 0; } +int APS5_VABI wcstombs_s_nid_postfix( + std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit +) { + std::array state{}; + return wcsrtombs_s_nid_postfix(result, destination, capacity, &source, limit, state.data()); +} + std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t* destination, const char** source, std::size_t capacity, void*) { if (!destination) return std::strlen(*source); std::size_t converted = 0; diff --git a/core/libs/tests/GuestAmprThreads.cpp b/core/libs/tests/GuestAmprThreads.cpp index 0c669decd2..403c445304 100644 --- a/core/libs/tests/GuestAmprThreads.cpp +++ b/core/libs/tests/GuestAmprThreads.cpp @@ -61,7 +61,7 @@ int APS5_VABI _Thrd_join_nid_postfix(Pthread thread, int* code); int APS5_VABI _Thrd_sleep_nid_postfix(const GuestXtime* target, GuestXtime* remaining); void APS5_VABI _Lock_shared_ptr_spin_lock_nid_postfix(void); void APS5_VABI _Unlock_shared_ptr_spin_lock_nid_postfix(void); -int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit); +int APS5_VABI sceAmprWcstombsS(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit); int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI pthread_detach_nid_postfix(Pthread thread); int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); @@ -233,42 +233,42 @@ static void TestWcstombs() { char buffer[8]; std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, sizeof(buffer), text, sizeof(buffer)) == 0); + Require(sceAmprWcstombsS(&result, buffer, sizeof(buffer), text, sizeof(buffer)) == 0); Require(result == 3 && std::strcmp(buffer, "abc") == 0); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, sizeof(buffer), text, 2) == 0); + Require(sceAmprWcstombsS(&result, buffer, sizeof(buffer), text, 2) == 0); Require(result == 2 && std::strcmp(buffer, "ab") == 0); result = 0; - Require(wcstombs_s_nid_postfix(&result, nullptr, 0, text, 0) == 0); + Require(sceAmprWcstombsS(&result, nullptr, 0, text, 0) == 0); Require(result == 3); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, 3, text, 3) == GuestErange); + Require(sceAmprWcstombsS(&result, buffer, 3, text, 3) == GuestErange); Require(result == Failed && buffer[0] == '\0'); result = 0; - Require(wcstombs_s_nid_postfix(&result, nullptr, 4, text, 4) == GuestEinval); + Require(sceAmprWcstombsS(&result, nullptr, 4, text, 4) == GuestEinval); Require(result == Failed); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, sizeof(buffer), nullptr, sizeof(buffer)) == GuestEinval); + Require(sceAmprWcstombsS(&result, buffer, sizeof(buffer), nullptr, sizeof(buffer)) == GuestEinval); Require(result == Failed && buffer[0] == '\0'); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(nullptr, buffer, sizeof(buffer), text, sizeof(buffer)) == GuestEinval); + Require(sceAmprWcstombsS(nullptr, buffer, sizeof(buffer), text, sizeof(buffer)) == GuestEinval); Require(buffer[0] == '\0'); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, 0, text, 4) == GuestEinval); + Require(sceAmprWcstombsS(&result, buffer, 0, text, 4) == GuestEinval); Require(result == Failed && buffer[0] == 'x'); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, sizeof(buffer), wide, sizeof(buffer)) == GuestEilseq); + Require(sceAmprWcstombsS(&result, buffer, sizeof(buffer), wide, sizeof(buffer)) == GuestEilseq); Require(result == Failed && buffer[0] == 'a' && buffer[1] == '\0'); - Require(wcstombs_s_nid_postfix(&result, nullptr, 0, wide, 0) == GuestEilseq); + Require(sceAmprWcstombsS(&result, nullptr, 0, wide, 0) == GuestEilseq); Require(result == Failed); } diff --git a/core/libs/tests/GuestWcsrtombsS.cpp b/core/libs/tests/GuestWcsrtombsS.cpp index 758ff5d33c..44a4206540 100644 --- a/core/libs/tests/GuestWcsrtombsS.cpp +++ b/core/libs/tests/GuestWcsrtombsS.cpp @@ -5,6 +5,7 @@ extern "C" int APS5_VABI wcsrtombs_s_nid_postfix( std::size_t*, char*, std::size_t, const std::uint16_t**, std::size_t, void*); +extern "C" int APS5_VABI wcstombs_s_nid_postfix(std::size_t*, char*, std::size_t, const std::uint16_t*, std::size_t); static void Require(bool value) { if (!value) std::abort(); } constexpr auto Failed = static_cast(-1); @@ -75,4 +76,22 @@ int main() { result = 7; Require(wcsrtombs_s_nid_postfix(&result, out, 4, &source, 4, nullptr) == 22 && result == Failed && out[0] == '\0'); Require(source == text && std::memcmp(out, "\0xxx", 4) == 0); + + char narrow[8]; + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, sizeof(narrow), text, sizeof(narrow)) == 0 && result == 3); + Require(std::memcmp(narrow, "abc\0x", 5) == 0); + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, sizeof(narrow), text, 2) == 0 && result == 2); + Require(std::memcmp(narrow, "ab\0x", 4) == 0); + Require(wcstombs_s_nid_postfix(&result, nullptr, 0, text, 0) == 0 && result == 3); + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, 3, text, 3) == 34 && result == Failed && narrow[0] == '\0'); + Require(wcstombs_s_nid_postfix(&result, nullptr, 4, text, 4) == 22 && result == Failed); + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, sizeof(narrow), nullptr, sizeof(narrow)) == 22); + Require(result == Failed && narrow[0] == '\0'); + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, sizeof(narrow), wide, sizeof(narrow)) == 86 && result == Failed); + Require(std::memcmp(narrow, "a\0xx", 4) == 0); } From 8ea3b5cab5b814299d001514027dc5fa059c5064 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Mon, 19 Oct 2026 21:43:09 -0500 Subject: [PATCH 130/219] fix(libSceJpegDec): reject misaligned output buffers and oversized alpha values --- core/libs/prx/libSceJpegDec/Export.cpp | 3 ++- core/libs/tests/GuestJpegDec.cpp | 15 +++++++++++++++ docs/dev/debt/libSceJpegDec.md | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceJpegDec/Export.cpp b/core/libs/prx/libSceJpegDec/Export.cpp index 1c67856235..19ec61c18b 100644 --- a/core/libs/prx/libSceJpegDec/Export.cpp +++ b/core/libs/prx/libSceJpegDec/Export.cpp @@ -111,9 +111,10 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param if (!context) return SCE_JPEG_DEC_ERROR_INVALID_HANDLE; (void)context; if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; - if (!param->jpeg_mem_addr || !param->image_mem_addr) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + if (!param->jpeg_mem_addr || !param->image_mem_addr || (reinterpret_cast(param->image_mem_addr) & 3u) != 0) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (param->jpeg_mem_size == 0 || param->image_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; if (param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (param->alpha_value > 0xFF) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; const std::uint32_t downScale = requireDownScale(__func__, param->decode_mode, param->down_scale); const std::span jpeg = toBytes(param->jpeg_mem_addr, param->jpeg_mem_size); diff --git a/core/libs/tests/GuestJpegDec.cpp b/core/libs/tests/GuestJpegDec.cpp index 8dfa4cb137..018dba662d 100644 --- a/core/libs/tests/GuestJpegDec.cpp +++ b/core/libs/tests/GuestJpegDec.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include static_assert(sizeof(JpegDecCreateParam) == 0xC); @@ -140,6 +141,20 @@ int main() { JpegDecDecodeParam unknownScale = decodeParam; unknownScale.down_scale = 3; Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &unknownScale, &info); })); + std::vector invalidAlphaImage(7 * 5 * 4, 0xCC); + JpegDecDecodeParam invalidAlpha = DecodeParam(jpeg, invalidAlphaImage, 7 * 4); + invalidAlpha.alpha_value = 0x100; + Require(sceJpegDecDecode(handle, &invalidAlpha, &info) == invalidParam); + Require(std::all_of(invalidAlphaImage.begin(), invalidAlphaImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + invalidAlpha.alpha_value = 0xFFFF; + Require(sceJpegDecDecode(handle, &invalidAlpha, &info) == invalidParam); + Require(std::all_of(invalidAlphaImage.begin(), invalidAlphaImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + std::vector misalignedImage(7 * 5 * 4 + 1, 0xCC); + JpegDecDecodeParam misaligned = DecodeParam(jpeg, misalignedImage, 7 * 4); + misaligned.image_mem_addr = misalignedImage.data() + 1; + misaligned.image_mem_size = static_cast(misalignedImage.size() - 1); + Require(sceJpegDecDecode(handle, &misaligned, &info) == invalidAddr); + Require(std::all_of(misalignedImage.begin(), misalignedImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); JpegDecDecodeParam otherMode = decodeParam; otherMode.decode_mode = 1; Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &otherMode, &info); })); diff --git a/docs/dev/debt/libSceJpegDec.md b/docs/dev/debt/libSceJpegDec.md index 833ad70e91..e5b7252fa0 100644 --- a/docs/dev/debt/libSceJpegDec.md +++ b/docs/dev/debt/libSceJpegDec.md @@ -7,4 +7,4 @@ ### Functional -- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - decode modes other than 0 and down scales other than 1, 2, 4 and 8 throw, as does a down scale of 2, 4 or 8 on a CMYK or YCCK image, which libjpeg-turbo cannot decode to RGB. The down scale factors are the ones [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L57-L58) documents; the output size is the source size divided by the factor and rounded up, and the pixels come from libjpeg-turbo's scaled inverse DCT. The console's rounding, pixel values and the packed decode result for a down-scaled image (the output size here) are unverified +- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - decode modes other than 0 and down scales other than 1, 2, 4 and 8 throw, as does a down scale of 2, 4 or 8 on a CMYK or YCCK image, which libjpeg-turbo cannot decode to RGB. The down scale factors are the ones [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L57-L58) documents; the output size is the source size divided by the factor and rounded up, and the pixels come from libjpeg-turbo's scaled inverse DCT. The console's rounding, pixel values and the packed decode result for a down-scaled image (the output size here) are unverified. In `sceJpegDecDecode`, rejecting an output `image_mem_addr` not aligned to 4 bytes and rejecting an `alpha_value` above 0xFF follow libScePngDec validation and are not verified from a libSceJpegDec binary disassembly From 5bb72c6b1233660c9d7b3f1e8d046ade21b8092c Mon Sep 17 00:00:00 2001 From: Mehran Date: Fri, 16 Oct 2026 01:12:32 +0300 Subject: [PATCH 131/219] perf(shader): validate vertex stage inputs without building them --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +- .../tests/VertexInputValidation.cpp | 150 ++++++++++++++++++ core/shader/recompiler/Recompiler.cpp | 16 +- .../Translation/ShaderInputInfoBuilder.hpp | 1 + .../src/ShaderInputInfoBuilder.cpp | 67 +++++++- 5 files changed, 231 insertions(+), 9 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/VertexInputValidation.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index f29b84cc0b..5a55e7fc5b 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -951,6 +951,10 @@ add_test_executable(agc_vop1_control_decode_tests EXCLUDE_FROM_ALL tests/Vop1Con target_link_libraries(agc_vop1_control_decode_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_vop1_control_decode_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_vop1_control_decode_tests) +add_test_executable(agc_vertex_input_validation_tests EXCLUDE_FROM_ALL tests/VertexInputValidation.cpp) +target_link_libraries(agc_vertex_input_validation_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_vertex_input_validation_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_vertex_input_validation_tests) add_test_executable(agc_vop2_encoding_decode_tests EXCLUDE_FROM_ALL tests/Vop2EncodingDecode.cpp) target_link_libraries(agc_vop2_encoding_decode_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_vop2_encoding_decode_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -2061,7 +2065,7 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_shader_bindless agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit agc_shader_selected_records_direct_image_limit agc_shader_selected_records_direct_pair_limit agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - foreach(agcTest IN ITEMS agc_vertex_input_tests agc_mesh_output_budget_tests agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_barrier_batch_tests agc_shader_disk_cache_tests agc_screenshot_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests agc_f16_single_convert_tests) + foreach(agcTest IN ITEMS agc_vertex_input_tests agc_mesh_output_budget_tests agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vertex_input_validation_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_barrier_batch_tests agc_shader_disk_cache_tests agc_screenshot_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests agc_f16_single_convert_tests) if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests") continue() endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexInputValidation.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexInputValidation.cpp new file mode 100644 index 0000000000..bd6a291b22 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/VertexInputValidation.cpp @@ -0,0 +1,150 @@ +#include "Translation/ShaderInputInfoBuilder.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +struct BufferInput { + std::uint64_t address; + std::uint32_t stride; + std::uint32_t records; + std::uint32_t fetchIndex; +}; + +ShaderVertexStageInfo vertexInputs(const std::vector& buffers, bool embedded = false) { + ShaderVertexStageInfo vertex{}; + for (const auto& buffer : buffers) { + const auto index = vertex.resourcesNum++; + vertex.resources[index].fields = {static_cast(buffer.address), static_cast(buffer.address >> 32u) | buffer.stride << 16u, buffer.records, 0u}; + vertex.resourcesDst[index] = {static_cast(index * 4u), 4, static_cast(index), buffer.fetchIndex}; + } + vertex.fetchEmbedded = embedded; + return vertex; +} + +template +std::string errorOf(TRun&& run) { + try { + run(); + } catch (const std::exception& error) { + return error.what(); + } + return {}; +} + +} + +int main() { + std::vector> vertexCases{std::nullopt}; + auto overfull = vertexInputs({{0x10000u, 16u, 100u, 0u}}); + overfull.resourcesNum = ShaderVertexStageInfo::MaxResources + 1u; + vertexCases.push_back(overfull); + vertexCases.push_back(vertexInputs({{0x10000u, 16u, 100u, 0u}, {0x10004u, 16u, 99u, 0u}})); + vertexCases.push_back(vertexInputs({{0x10000u, 16u, 100u, 0u}, {0x10004u, 16u, 99u, 0u}}, true)); + vertexCases.push_back(vertexInputs({{0x10000u, 16u, 100u, 0u}, {0x10004u, 16u, 100u, 0u}, {0x20000u, 8u, 50u, 0u}, {0x10008u, 16u, 100u, 1u}})); + std::vector attributes; + std::vector separate; + for (std::uint32_t index = 0; index < ShaderVertexStageInfo::MaxResources; ++index) { + attributes.push_back({0x30000u + index * 4u, 128u, 10u, 0u}); + separate.push_back({0x40000u + index * 0x1000u, 4u + index * 4u, 10u, 0u}); + } + vertexCases.push_back(vertexInputs(attributes)); + vertexCases.push_back(vertexInputs(separate)); + + const MeshConfiguration mesh{3u, 64u, 64u, 64u, 64u, 64u, 0u, 0u}; + auto noThreads = mesh; + noThreads.threadsPerGroup = 0u; + auto badProvoking = mesh; + badProvoking.provokingVertex = 3u; + std::vector> meshCases{std::nullopt, mesh, noThreads, badProvoking}; + struct MeshExpectation { + std::uint32_t outputPrimitiveSize; + std::uint32_t provokingVertex; + bool accepted; + }; + constexpr std::array meshExpectations{MeshExpectation{0u, 0u, false}, MeshExpectation{1u, 0u, false}, MeshExpectation{2u, 0u, true}, MeshExpectation{2u, 1u, true}, MeshExpectation{2u, 2u, false}, MeshExpectation{3u, 2u, true}, MeshExpectation{3u, 3u, false}, MeshExpectation{4u, 0u, false}, MeshExpectation{4u, 3u, false}}; + const auto meshWith = [&](const MeshExpectation& expectation) { + auto varied = mesh; + varied.outputPrimitiveSize = expectation.outputPrimitiveSize; + varied.provokingVertex = expectation.provokingVertex; + return varied; + }; + for (const auto& expectation : meshExpectations) meshCases.push_back(meshWith(expectation)); + + const TessellationConfiguration tessellation{3u, 3u, 1u, 2u, 2u, 16u}; + std::vector> tessellationCases{std::nullopt, tessellation}; + const auto vary = [&](auto change) { + auto varied = tessellation; + change(varied); + tessellationCases.push_back(varied); + }; + vary([](TessellationConfiguration& config) { config.inputControlPoints = 0u; }); + vary([](TessellationConfiguration& config) { config.outputControlPoints = 33u; }); + vary([](TessellationConfiguration& config) { config.domain = 3u; }); + vary([](TessellationConfiguration& config) { config.partitioning = 1u; }); + vary([](TessellationConfiguration& config) { config.outputTopology = 1u; }); + vary([](TessellationConfiguration& config) { config.patchesPerGroup = 0u; }); + vary([](TessellationConfiguration& config) { config.patchesPerGroup = 257u; }); + + constexpr std::array stages{ShaderStageKind::Vertex, ShaderStageKind::Local, ShaderStageKind::TessellationControl, ShaderStageKind::TessellationEvaluation, ShaderStageKind::Mesh}; + std::size_t mismatches = 0; + std::size_t accepted = 0; + std::set errors; + for (const auto stage : stages) { + for (std::size_t vertexCase = 0; vertexCase < vertexCases.size(); ++vertexCase) { + for (std::size_t meshCase = 0; meshCase < meshCases.size(); ++meshCase) { + for (std::size_t tessellationCase = 0; tessellationCase < tessellationCases.size(); ++tessellationCase) { + GuestContext context{}; + context.waveSize = 64u; + context.vertex = vertexCases[vertexCase]; + const auto* meshConfig = meshCases[meshCase] ? &*meshCases[meshCase] : nullptr; + const auto* tessellationConfig = tessellationCases[tessellationCase] ? &*tessellationCases[tessellationCase] : nullptr; + const auto built = errorOf([&] { static_cast(BuildShaderStageInputInfo(stage, context, 64u, meshConfig, tessellationConfig)); }); + const auto validated = errorOf([&] { ValidateVertexStageInputs(stage, context, meshConfig, tessellationConfig); }); + if (built != validated) { + ++mismatches; + std::fprintf(stderr, "stage %d, vertex case %zu, mesh case %zu, tessellation case %zu: the build says \"%s\", the validation \"%s\"\n", static_cast(stage), vertexCase, meshCase, tessellationCase, built.c_str(), validated.c_str()); + } + if (built.empty()) ++accepted; + else errors.insert(built); + } + } + } + } + const std::array expected{"GuestContext.vertex is not set", "invalid vertex resource count", "disagree on record count", "tessellation configuration is missing", "invalid tessellation control-point counts", "unsupported tessellation configuration", "invalid tessellation patches per group", "has no mesh configuration", "invalid mesh configuration"}; + for (const auto* message : expected) { + const bool seen = std::any_of(errors.begin(), errors.end(), [&](const std::string& error) { return error.find(message) != std::string::npos; }); + if (!seen) { + ++mismatches; + std::fprintf(stderr, "no case raised \"%s\"\n", message); + } + } + GuestContext meshContext{}; + meshContext.waveSize = 64u; + meshContext.vertex = vertexInputs({{0x10000u, 16u, 100u, 0u}}); + for (const auto& expectation : meshExpectations) { + const auto varied = meshWith(expectation); + const bool built = errorOf([&] { static_cast(BuildShaderStageInputInfo(ShaderStageKind::Mesh, meshContext, 64u, &varied, nullptr)); }).empty(); + const bool validated = errorOf([&] { ValidateVertexStageInputs(ShaderStageKind::Mesh, meshContext, &varied, nullptr); }).empty(); + if (built != expectation.accepted || validated != expectation.accepted) { + ++mismatches; + std::fprintf(stderr, "mesh output primitive size %u, provoking vertex %u: expected %s, the build %s, the validation %s\n", expectation.outputPrimitiveSize, expectation.provokingVertex, expectation.accepted ? "accepted" : "rejected", built ? "accepts" : "rejects", validated ? "accepts" : "rejects"); + } + } + if (accepted == 0) { + ++mismatches; + std::fprintf(stderr, "no case was accepted\n"); + } + if (mismatches != 0) return 1; + std::printf("vertex input validation matches the input info build in %zu cases (%zu accepted)\n", stages.size() * vertexCases.size() * meshCases.size() * tessellationCases.size(), accepted); + return 0; +} diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 48c22f5a9f..06c1bf406e 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -122,6 +122,18 @@ ShaderStageInputInfo RequestInputInfo(const RecompileRequest& request) { return BuildShaderStageInputInfo(toShaderStageKind(request.shader.stage), request.context, HostSubgroupSize(request), mesh, tessellation); } +void ValidateRequestInputs(const RecompileRequest& request) { + const auto stage = toShaderStageKind(request.shader.stage); + const bool vertexFamily = stage == ShaderStageKind::Vertex || stage == ShaderStageKind::Local || stage == ShaderStageKind::TessellationControl || stage == ShaderStageKind::TessellationEvaluation || stage == ShaderStageKind::Mesh; + if (!vertexFamily) { + static_cast(RequestInputInfo(request)); + return; + } + const auto* mesh = request.graphics && request.graphics->mesh ? &*request.graphics->mesh : nullptr; + const auto* tessellation = request.graphics && request.graphics->tessellation ? &*request.graphics->tessellation : nullptr; + ValidateVertexStageInputs(stage, request.context, mesh, tessellation); +} + std::uint32_t DeviceMemoryLdsBytes(const RecompileRequest& request) { if (!request.context.compute.has_value()) return 0u; const auto bytes = static_cast(request.context.compute->ldsSizeDwords) * 4u; @@ -1582,7 +1594,7 @@ std::optional PreparedShaderInvocation::TryCreate(cons std::optional PreparedShaderInvocation::TryCreate(const RecompileRequest& request, const std::shared_ptr& handle, std::span key) { if (handle == nullptr || !MatchesPreparedShader(request, *handle, key)) return std::nullopt; - if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) static_cast(RequestInputInfo(request)); + if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) ValidateRequestInputs(request); return PreparedShaderInvocation(request, handle); } @@ -1636,7 +1648,7 @@ std::shared_ptr CaptureResources(const RecompileRequest& static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto started = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; // The whole vertex family (Vertex, Local, TC, TE, Mesh) validates V# fields the memo key does not cover. - if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) static_cast(RequestInputInfo(request)); + if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) ValidateRequestInputs(request); auto capture = std::make_shared(); capture->source = handle.source; capture->plan = acquirePlan(request, handle.source, true, &handle); diff --git a/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp b/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp index 8fe4a01871..4f7c73ee5c 100644 --- a/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp +++ b/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp @@ -10,6 +10,7 @@ namespace ShaderRecompiler { // hostSubgroupSize is the device subgroup width; wave64 workgroups on 32-wide subgroups run each // invocation as two guest lanes so a guest wave stays one host subgroup. ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const GuestContext& context, std::uint32_t hostSubgroupSize, const MeshConfiguration* mesh = nullptr, const TessellationConfiguration* tessellation = nullptr); +void ValidateVertexStageInputs(ShaderStageKind stage, const GuestContext& context, const MeshConfiguration* mesh = nullptr, const TessellationConfiguration* tessellation = nullptr); } diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 4c5482b884..cec7bfb6c1 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -61,6 +61,18 @@ void _detectVertexBuffers(ShaderVertexInputInfo& info) { } } +void _validateTessellation(const TessellationConfiguration* tessellation) { + if (tessellation == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: tessellation configuration is missing"); + if (tessellation->inputControlPoints == 0u || tessellation->inputControlPoints > 32u || tessellation->outputControlPoints == 0u || tessellation->outputControlPoints > 32u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation control-point counts"); + if ((tessellation->domain != 1u && tessellation->domain != 2u) || (tessellation->partitioning != 0u && tessellation->partitioning != 2u && tessellation->partitioning != 3u) || (tessellation->outputTopology != 2u && tessellation->outputTopology != 3u)) throw std::runtime_error("ShaderInputInfoBuilder: unsupported tessellation configuration"); + if (tessellation->patchesPerGroup == 0u || tessellation->patchesPerGroup > 256u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation patches per group"); +} + +void _validateMesh(const MeshConfiguration* mesh) { + if (mesh == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: a mesh-stage program has no mesh configuration"); + if (mesh->threadsPerGroup == 0u || mesh->maxVertices == 0u || mesh->maxPrimitives == 0u || (mesh->outputPrimitiveSize != 2u && mesh->outputPrimitiveSize != 3u) || mesh->provokingVertex >= mesh->outputPrimitiveSize) throw std::runtime_error("ShaderInputInfoBuilder: invalid mesh configuration"); +} + } ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const GuestContext& context, std::uint32_t hostSubgroupSize, const MeshConfiguration* mesh, const TessellationConfiguration* tessellation) { @@ -191,10 +203,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues } _detectVertexBuffers(vertexStorage); if (stage == ShaderStageKind::Local || stage == ShaderStageKind::TessellationControl || stage == ShaderStageKind::TessellationEvaluation) { - if (tessellation == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: tessellation configuration is missing"); - if (tessellation->inputControlPoints == 0u || tessellation->inputControlPoints > 32u || tessellation->outputControlPoints == 0u || tessellation->outputControlPoints > 32u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation control-point counts"); - if ((tessellation->domain != 1u && tessellation->domain != 2u) || (tessellation->partitioning != 0u && tessellation->partitioning != 2u && tessellation->partitioning != 3u) || (tessellation->outputTopology != 2u && tessellation->outputTopology != 3u)) throw std::runtime_error("ShaderInputInfoBuilder: unsupported tessellation configuration"); - if (tessellation->patchesPerGroup == 0u || tessellation->patchesPerGroup > 256u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation patches per group"); + _validateTessellation(tessellation); auto& target = vertexStorage.tess; target.inputControlPoints = tessellation->inputControlPoints; target.outputControlPoints = tessellation->outputControlPoints; @@ -208,7 +217,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues target.drawParameters = tessellation->drawParameters; } if (stage == ShaderStageKind::Mesh) { - if (mesh == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: a mesh-stage program has no mesh configuration"); + _validateMesh(mesh); auto& target = vertexStorage.mesh; target.threadsNum[0] = mesh->threadsPerGroup; target.threadsNum[1] = 1u; @@ -224,7 +233,6 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues target.provokingVertex = mesh->provokingVertex; target.esgsItemSize = mesh->esgsItemSize; target.outputPrimitiveSize = mesh->outputPrimitiveSize; - if (target.threadsNum[0] == 0u || target.maxVertices == 0u || target.maxPrimitives == 0u || (target.outputPrimitiveSize != 2u && target.outputPrimitiveSize != 3u) || target.provokingVertex >= target.outputPrimitiveSize) throw std::runtime_error("ShaderInputInfoBuilder: invalid mesh configuration"); } ShaderStageInputInfo result; result.vertex = &vertexStorage; @@ -237,4 +245,51 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues throw std::runtime_error("ShaderInputInfoBuilder: unexpected stage"); } +void ValidateVertexStageInputs(ShaderStageKind stage, const GuestContext& context, const MeshConfiguration* mesh, const TessellationConfiguration* tessellation) { + if (!context.vertex.has_value()) { + throw std::runtime_error("ShaderInputInfoBuilder: GuestContext.vertex is not set"); + } + const auto& vertex = *context.vertex; + if (vertex.resourcesNum > vertex.resources.size()) throw std::runtime_error("ShaderInputInfoBuilder: invalid vertex resource count"); + struct Buffer { + std::uint64_t addr; + std::uint32_t stride; + std::uint32_t numRecords; + std::uint32_t fetchIndex; + int attrNum; + }; + std::array buffers; + int buffersNum = 0; + const std::array embedded{}; + for (std::uint32_t ri = 0; ri < vertex.resourcesNum; ++ri) { + const auto& fields = vertex.fetchEmbedded ? embedded : vertex.resources[ri].fields; + const std::uint16_t stride = static_cast((fields[1] >> 16u) & 0x3fffu); + const std::uint64_t base = (static_cast(fields[0]) | (static_cast(fields[1]) << 32u)) & 0xffffffffffffull; + const std::uint32_t numRecords = fields[2]; + const std::uint32_t fetchIndex = vertex.fetchEmbedded ? 0u : vertex.resourcesDst[ri].fetchIndex; + bool merged = false; + for (int bi = 0; bi < buffersNum; ++bi) { + auto& b = buffers[bi]; + if (b.stride != stride || b.fetchIndex != fetchIndex) continue; + const auto low = base < b.addr ? base : b.addr; + if (base - low >= stride || b.addr - low >= stride) continue; + if (b.numRecords != numRecords) throw std::runtime_error("ShaderInputInfoBuilder: merged vertex buffers disagree on record count"); + b.addr = low; + if (b.attrNum >= ShaderVertexInputBuffer::MaxAttributes) throw std::runtime_error("ShaderInputInfoBuilder: vertex buffer attribute count exceeds the supported domain"); + ++b.attrNum; + merged = true; + break; + } + if (merged) continue; + if (buffersNum >= ShaderVertexInputInfo::MaxResources) throw std::runtime_error("ShaderInputInfoBuilder: vertex buffer count exceeds the supported domain"); + buffers[buffersNum++] = {base, stride, numRecords, fetchIndex, 1}; + } + if (stage == ShaderStageKind::Local || stage == ShaderStageKind::TessellationControl || stage == ShaderStageKind::TessellationEvaluation) { + _validateTessellation(tessellation); + } + if (stage == ShaderStageKind::Mesh) { + _validateMesh(mesh); + } +} + } From e0230e6a75444ed3a3b9d49c5b817fdd5859cd5f Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Tue, 20 Oct 2026 01:59:49 +0300 Subject: [PATCH 132/219] fix(relinker): skip syscall bytes without a syscall number between PS4 unwind functions Between the unwind functions of a PS4 image the bytes are not known to be code, and read-only tables there can hold 0f 05. Outside the unwind functions a syscall, sysenter, int 0x80 or sysret pattern now fails the relink only when a constant syscall number is loaded right before it; the others are skipped and counted in one message. Inside the unwind functions and in other images every such instruction still fails. Co-Authored-By: Claude Opus 5.5 --- .../relinker/src/analysis/SyscallScanner.cpp | 26 ++++++++++++++++--- .../relinker/tests/test_sce_code_gaps.py | 8 +++++- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/core/relinker/relinker/src/analysis/SyscallScanner.cpp b/core/relinker/relinker/src/analysis/SyscallScanner.cpp index 08d5e87dcb..d8d717ff30 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -190,6 +191,14 @@ std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::v const auto scanner = Codegen::MakeInstructionScanner([&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Decode, source, completed, total}); }, unwindRanges); RecentInstructions recent; std::vector trapped; + std::vector functions(unwindRanges); + std::sort(functions.begin(), functions.end(), [](const CodeRange& left, const CodeRange& right) { return left.Begin < right.Begin; }); + const auto insideFunction = [&](const std::size_t offset) { + const auto after = std::upper_bound(functions.begin(), functions.end(), offset, [](const std::size_t value, const CodeRange& range) { return value < range.Begin; }); + return after != functions.begin() && offset < std::prev(after)->End; + }; + std::size_t skipped = 0; + std::optional firstSkipped; scanner->VisitCodeSection(codeSection, codeSectionOffset, codeSectionSize, [&](const Codegen::InstructionMatch& match) { recent.Push(match); if (match.Offset < codeSectionOffset) throw RelinkerException("Instruction match precedes code section offset", match.Offset); @@ -213,15 +222,26 @@ std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::v return; } + const auto number = kind == ForbiddenInstruction::Invocation ? ConstantSyscallNumber(codeSection, codeSectionOffset, recent) : std::nullopt; + if (!functions.empty() && !number && !insideFunction(start)) { + ++skipped; + if (!firstSkipped) firstSkipped = match.Offset; + return; + } + std::ostringstream msg; msg << "Forbidden syscall instruction at code offset 0x" << std::hex << match.Offset; - if (kind == ForbiddenInstruction::Invocation) - if (const auto number = ConstantSyscallNumber(codeSection, codeSectionOffset, recent)) - msg << ": syscall number " << std::dec << *number; + if (number) + msg << ": syscall number " << std::dec << *number; throw RelinkerException(msg.str(), match.Offset); }); + if (skipped != 0) { + std::ostringstream message; + message << "Skipped " << std::dec << skipped << " syscall byte pattern(s) without a syscall number outside the unwind functions, the first at code offset 0x" << std::hex << *firstSkipped << '\n'; + progress.Message(message.str()); + } progress.Report({Domain::ProgressStage::Syscalls, source, limit, limit}); return trapped; } diff --git a/core/relinker/relinker/tests/test_sce_code_gaps.py b/core/relinker/relinker/tests/test_sce_code_gaps.py index 5dc8164f13..d3acbb55e5 100644 --- a/core/relinker/relinker/tests/test_sce_code_gaps.py +++ b/core/relinker/relinker/tests/test_sce_code_gaps.py @@ -11,6 +11,8 @@ from test_sce_module_code_extent import sce_module, unwind_table SLOT = bytes.fromhex("48 66 66 66 66 66 66 2e 0f 1f 84 00 00 00 00 00") SYSCALL = bytes.fromhex("0f 05 c3") +SYSCALL_NUMBER = bytes.fromhex("b8 14 00 00 00") +TABLE = bytes.fromhex("0c 05 00 00 0f 05 00 b0 0f 05 00 00 10 05 00 50") GAP = FUNCTION + FUNCTION_SIZE TARGETS = (("linux", []), ("windows", ["--windows"]), ("macos", ["--macos"])) @@ -72,7 +74,11 @@ def main(): for target, flags in TARGETS: run(relinker, work, "sce-gap-slot-" + target, executable(SLOT), flags) run_module(relinker, work, "sce-module-gap-slot-" + target, module(False), flags) - run(relinker, work, "sce-gap-syscall-after-slot", executable(SLOT + SYSCALL), [], + run(relinker, work, "sce-gap-syscall-bytes-after-slot", executable(SLOT + SYSCALL), []) + run(relinker, work, "sce-gap-table-with-syscall-bytes", executable(TABLE), []) + run(relinker, work, "sce-gap-syscall-with-number", executable(SLOT + SYSCALL_NUMBER + SYSCALL), [], + ["Forbidden syscall instruction at code offset 0x225: syscall number 20"]) + run(relinker, work, "sysv-gap-syscall-bytes", executable(bytes.fromhex("90") * 16 + SYSCALL, sce_block=False), [], ["Forbidden syscall instruction at code offset 0x220"]) run(relinker, work, "sce-function-slot", executable(function=SLOT), [], ["Cannot decode instruction", "offset 0x200"]) From f154e132bcfae671a45926918fdde073ee7b341e Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Tue, 20 Oct 2026 02:31:22 +0300 Subject: [PATCH 133/219] fix(relinker): take pointers into data between PS4 unwind functions as data The code analysis of the Windows and macOS outputs followed every relocation and lea target inside the code range as code. PS4 images keep read-only data between unwind functions, and strings and tables there decoded into garbage, failing the conversion. Outside the unwind functions such a target is now code only when it is 16-byte aligned, starts less than 256 bytes after an unwind function and decodes to a jump, return or trap; lea targets go through the same rule instead of the graph's own. Co-Authored-By: Claude Opus 5.5 --- .../src/analysis/CodeInstructionCollector.cpp | 55 +++++++++++++++++-- .../relinker/tests/test_sce_code_extent.py | 12 +++- docs/dev/debt/relinker.md | 2 + 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index e7a89396b1..9fab805034 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -96,8 +96,11 @@ std::set CodeInstructionCollector::Collect(const std::ve range(offset, size); return offset; }; + std::vector> sceFunctions; if (sceBase) { const auto unwindFunctions = UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); + for (const auto& function : unwindFunctions) sceFunctions.emplace_back(function.Begin, function.End); + std::sort(sceFunctions.begin(), sceFunctions.end()); for (auto& header : codeHeaders) { if (header.Type != 1 || (header.Flags & 1) == 0) continue; range(header.Offset, header.FileSize); @@ -118,10 +121,54 @@ std::set CodeInstructionCollector::Collect(const std::ve std::map tables; const auto inTable = [&](std::uint64_t address) { const auto after = tables.upper_bound(address); return after != tables.begin() && address < std::prev(after)->second; }; const auto addRoot = [&](std::uint64_t address) { if (isCode(address) && !inTable(address)) roots.insert(address); }; + const auto insideSceFunction = [&](std::uint64_t address) { + const auto after = std::upper_bound(sceFunctions.begin(), sceFunctions.end(), address, [](std::uint64_t value, const std::pair& function) { return value < function.first; }); + return after != sceFunctions.begin() && address < std::prev(after)->second; + }; + const auto nearSceFunctionEnd = [&](std::uint64_t address) { + const auto after = std::upper_bound(sceFunctions.begin(), sceFunctions.end(), address, [](std::uint64_t value, const std::pair& function) { return value < function.first; }); + if (after == sceFunctions.begin()) return false; + const auto end = std::prev(after)->second; + return address >= end && address - end < 256; + }; + const auto codeAt = [&](std::uint64_t address) { + for (const auto& header : codeHeaders) + if (header.Type == 1 && (header.Flags & 1) != 0 && address >= header.MappedAddress && address - header.MappedAddress < header.FileSize) + return std::span(bytes.data() + header.Offset + (address - header.MappedAddress), header.FileSize - (address - header.MappedAddress)); + return std::span(); + }; + const auto decodesToTransfer = [&](std::uint64_t address) { + const Codegen::X64InstructionDecoder probe; + for (std::size_t count = 0; count < 256; ++count) { + const auto code = codeAt(address); + if (code.empty()) return false; + Codegen::DecodedInstructionInfo info; + try { + info = probe.DecodeInstruction(code.data(), code.size()); + } catch (const Codegen::CodegenException&) { + return false; + } + if (info.Length == 0 || info.Length > code.size()) return false; + switch (info.FlowKind) { + case Codegen::ControlFlowKind::UnconditionalJump: + case Codegen::ControlFlowKind::Return: + case Codegen::ControlFlowKind::IndirectJump: + case Codegen::ControlFlowKind::Trap: + return true; + default: + break; + } + address += info.Length; + } + return false; + }; + const auto addPointerRoot = [&](std::uint64_t address) { + if (sceFunctions.empty() || insideSceFunction(address) || (address % 16 == 0 && nearSceFunctionEnd(address) && decodesToTransfer(address))) addRoot(address); + }; const auto addAddressTakenRoot = [&](std::uint64_t address, const std::uint8_t* code, const Codegen::DecodedInstructionInfo& info) { if (!info.HasRipRelativeDisp || code[info.OpcodeOffset] != 0x8d || !(info.RexPrefix & 8)) return; const auto displacement = std::bit_cast(Io::ReadU32(bytes, fileOffset(address, info.Length) + info.RipRelativeDispOffset)); - addRoot(address + info.Length + static_cast(displacement)); + addPointerRoot(address + info.Length + static_cast(displacement)); }; const auto addFunction = [&](std::uint64_t begin, std::uint64_t size, bool symbolAlias) { if (size == 0) return; @@ -226,7 +273,7 @@ std::set CodeInstructionCollector::Collect(const std::ve && addend <= static_cast(std::numeric_limits::max()) && Io::ReadU64(bytes, symbol + 8) == addend; if ((index != 0 && !zeroSectionSymbol && !matchingDefinedSymbol) || !pointers.Values.emplace(slot, addend).second) throw Domain::RelinkerException("Code analysis: invalid relative relocation", slot); - addRoot(addend); + addPointerRoot(addend); } else if (type == 1 || type == 6 || type == 7) { if (index != 0 && index >= symbolCount) throw Domain::RelinkerException("Code analysis: invalid relocation symbol", slot); const auto symbol = symbols + index * 24; @@ -234,7 +281,7 @@ std::set CodeInstructionCollector::Collect(const std::ve else { const auto target = Io::ReadU64(bytes, symbol + 8) + addend; if (!pointers.Values.emplace(slot, target).second) throw Domain::RelinkerException("Code analysis: duplicate pointer relocation", slot); - addRoot(target); + addPointerRoot(target); } } } @@ -405,7 +452,7 @@ std::set CodeInstructionCollector::Collect(const std::ve for (const auto root : roots) if (root >= header.MappedAddress && root - header.MappedAddress < header.FileSize) entries.push_back(root); if (entries.empty()) continue; const std::span text(bytes.data() + header.Offset, header.FileSize); - const auto graph = UnusedNidFilter::BuildControlFlowGraph(text, header.MappedAddress, entries.front(), entries, pointers, true, sectionReachable[i], [&](std::uint64_t count) { report(6000 + 2500 * count / std::max(scanBytes, 1)); }, inTable); + const auto graph = UnusedNidFilter::BuildControlFlowGraph(text, header.MappedAddress, entries.front(), entries, pointers, sceFunctions.empty(), sectionReachable[i], [&](std::uint64_t count) { report(6000 + 2500 * count / std::max(scanBytes, 1)); }, inTable); for (const auto address : graph->ReachableVaddrs()) { if (!decodedInstructions.insert(address).second) continue; instructions.insert(address); diff --git a/core/relinker/relinker/tests/test_sce_code_extent.py b/core/relinker/relinker/tests/test_sce_code_extent.py index 5bc8a2321c..e3a50f8296 100644 --- a/core/relinker/relinker/tests/test_sce_code_extent.py +++ b/core/relinker/relinker/tests/test_sce_code_extent.py @@ -22,10 +22,11 @@ POINTER = 0xB00 INTERPRETER = b"/libexec/ld-elf.so.1\0" OVERLAPPING = bytes.fromhex("b8 64 c3 90 90 72 fa c3") UNSUPPORTED_TLS = bytes.fromhex("64 2d 65 6c 66 2e c3") +INVALID = bytes.fromhex("41 41 41 41 41 41 66 66 66 66 2e 0f 1f 84 00 00 00 00 00") def fixture(sce_block=True, plt_push_offset=8, gap=0, interpreter_pointer=False, data_pointer=False, gap_entry=False, - plt_at=None, early_function=False, function_tls=False): + plt_at=None, early_function=False, function_tls=False, pointer_target=None, gap_bytes=None): plt = FUNCTION + FUNCTION_SIZE + gap if plt_at is None else plt_at entry = FUNCTION + FUNCTION_SIZE if gap_entry else FUNCTION data = plt + 0x20 if plt > FUNCTION else FUNCTION + FUNCTION_SIZE @@ -40,7 +41,7 @@ def fixture(sce_block=True, plt_push_offset=8, gap=0, interpreter_pointer=False, (1, 1), (0x61000035, 0x600), (0x61000037, 16), (0x61000039, 0x620), (0x6100003B, 24), (0x6100003F, 48), - (0x6100002F, 0x700), (0x61000031, 24 if interpreter_pointer else 0), (0x61000033, 24), + (0x6100002F, 0x700), (0x61000031, 24 if interpreter_pointer or pointer_target is not None else 0), (0x61000033, 24), (0x61000027, GOT), (0x61000029, 0x720), (0x6100002B, 7), (0x6100002D, 24), (0, 0), ] @@ -82,6 +83,10 @@ def fixture(sce_block=True, plt_push_offset=8, gap=0, interpreter_pointer=False, image[FUNCTION:FUNCTION + 3] = b"\x48\x8d\x05" struct.pack_into(" Date: Tue, 20 Oct 2026 14:20:37 +0200 Subject: [PATCH 134/219] fix(libkernel): let raise(0) check the calling thread and return 0 FreeBSD 11.4 raise is thr_kill(thr_self(), sig) (lib/libc/gen/raise.c:52), and thr_kill with a thread id treats signal 0 as a pure existence check that returns 0 (sys/kern/kern_thr.c:425). The guest returned EINVAL. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/System/src/Signals.cpp | 1 + core/libs/tests/GuestSignals.cpp | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index c9211181d4..b42fa66d64 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -496,6 +496,7 @@ int APS5_VABI sigaction_nid_postfix(int guest, const GuestSigaction* action, Gue return 0; } int APS5_VABI raise_nid_postfix(int guest) { + if (guest == 0) return 0; if (guest < 1 || guest > MaxSignal) { *__error_nid_postfix() = 22; return -1; } const GuestSigaction action = Disposition(guest); if (action.handler > 1 && !Delivered(action)) throw std::runtime_error("raise: SA_SIGINFO handlers are not delivered"); diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index 016c4c5a0f..b0011ddf63 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -93,6 +93,11 @@ int main() { Require(raise_nid_postfix(15) == 0 && received == 0); Require(signal_nid_postfix(15, nullptr) == ignore); Require(raise_nid_postfix(100) == -1 && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + received = 0; + Require(raise_nid_postfix(0) == 0 && received == 0 && *__error_nid_postfix() == 0); + Require(raise_nid_postfix(129) == -1 && *__error_nid_postfix() == 22); + Require(raise_nid_postfix(-1) == -1 && *__error_nid_postfix() == 22); GuestSignalSet blocked{{0x20, 0, 0, 0}}; GuestSignalSet previous{{}}; Require(sigprocmask_nid_postfix(3, &blocked, &previous) == 0); From 2693c445693571a94cba3592155c5786656abf43 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:22:50 +0300 Subject: [PATCH 135/219] docs(libc): document Windows backtrace stop at PE unwind handlers --- core/libs/prx/libc/src/Backtrace.cpp | 5 +---- docs/dev/debt/libc.md | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libc/src/Backtrace.cpp b/core/libs/prx/libc/src/Backtrace.cpp index b38d8dfdcb..a07a476b4a 100644 --- a/core/libs/prx/libc/src/Backtrace.cpp +++ b/core/libs/prx/libc/src/Backtrace.cpp @@ -74,10 +74,7 @@ static_assert(sizeof(FrameRecord) == NameOffset); }; TraceState state{addresses, MaxFrames, 0}; _Unwind_Backtrace_nid_postfix(traceCallback, &state); - int captured = state.count; - if (captured <= 2) { - captured = static_cast(RtlCaptureStackBackTrace(0, MaxFrames, addresses, nullptr)); - } + const int captured = state.count; #endif std::vector frames; for (int index = 2; index < captured && (limit < 0 || static_cast(frames.size()) < limit); ++index) { diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index df77324692..e51a57c876 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -4,7 +4,7 @@ - [strtonum](../../../core/libs/prx/libc/src/StringExtensions.cpp) (libc) is unverified on the console. Its signature, decimal parsing, inclusive bounds, error strings, error precedence and errno preservation follow [FreeBSD 11.4 strtonum.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/strtonum.c). Parsing uses the host `strtoll` with base 10. -- [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. Implemented only on Linux and Windows; other platforms throw +- [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. On Windows, unwinding uses `_Unwind_Backtrace` and stops at a frame without `.ehmeta` or `.ehfram` tables whose PE unwind data has an exception or termination handler, leaving the trace empty or cut short while still returning 0. Implemented only on Linux and Windows; other platforms throw - [ApplicationGuestHeapApi](../../../core/libs/prx/libc/src/ApplicationProcessParameters.cpp) (libc) - on Linux, HLE application allocation uses a loaded guest SDK's ten allocator exports when they belong to one native module. Its CRT owns application allocator initialization and finalization; HLE retains the module until process exit. Partial or mixed providers throw. This avoids reinitializing an allocator already used by SDK startup; console behavior and other host platforms remain unverified. - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is unverified. - [scriptingGetMem, scriptingFreeMem](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - runtime allocator hooks for Unity titles. The allocation signature `(alignment, size)`, minimum alignment 16 and null result for a non-power-of-two alignment follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libKernel.cpp); the free hook's one-pointer signature follows [Raeen's recorded contract](https://github.com/Whoisraeen/Raeen/blob/2db56923204b89719916b95e29c2f3c4e1d9f90b/docs/unity-dlsym-blocker.md). Both use libc's own guest heap, not the title's registered allocator. A title that ships its own `libc.prx` (Metal Gear Solid Master Collection, PPSA18194) has already initialized that allocator, and reaching it from here would run its initializer a second time. That title's IL2CPP runtime looks up only `scriptingGetMem`, by name from `Il2cppUserAssemblies.prx`, and asks it for 256 KiB blocks aligned to 4 KiB. Which console library exports the hooks is unknown, and there is no console measurement. Allocation failure and sizes that overflow return null. From fb050223ddd2a51fb36d6e2fac16f45287d655f9 Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Tue, 20 Oct 2026 15:26:15 +0300 Subject: [PATCH 136/219] fix(relinker): keep padding to the range end and check what the function still covers Check padding up to the end of the inspected range first, as before, and use the 16-byte boundary rule only when that fails, so a long NOP that crosses the boundary no longer rejects the table. Check the bytes the function's own unwind range still covers after the table, so a table cut short inside its function is rejected again. Co-Authored-By: Claude Opus 5.5 --- .../relinker/analysis/RelativeJumpTables.hpp | 2 +- .../src/analysis/RelativeJumpTables.cpp | 10 +++-- .../relinker/tests/ControlFlowGraphTests.cpp | 41 +++++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp b/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp index 8a4caf08d0..b80aae4651 100644 --- a/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp +++ b/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp @@ -16,7 +16,7 @@ struct RelativeJumpTable { VirtualAddress End; }; -std::size_t FindTrailingRelativeJumpTable(std::span code, VirtualAddress codeAddress = 0, std::size_t* tableEnd = nullptr); +std::size_t FindTrailingRelativeJumpTable(std::span code, VirtualAddress codeAddress = 0, std::size_t* tableEnd = nullptr, std::size_t unwindEnd = 0); std::vector FindRelativeJumpTables(std::span segment, VirtualAddress segmentVAddr, const std::vector>& functions); } diff --git a/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp b/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp index ebe272bdad..7c99111960 100644 --- a/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp +++ b/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp @@ -69,7 +69,7 @@ std::size_t TablesEnd(std::span code, std::size_t firstTable } -std::size_t FindTrailingRelativeJumpTable(std::span code, VirtualAddress codeAddress, std::size_t* tableEnd) { +std::size_t FindTrailingRelativeJumpTable(std::span code, VirtualAddress codeAddress, std::size_t* tableEnd, std::size_t unwindEnd) { const Codegen::X64InstructionDecoder decoder; std::vector loads; std::vector instructionStarts(code.size() + 1, false); @@ -84,8 +84,10 @@ std::size_t FindTrailingRelativeJumpTable(std::span code, Vi const auto end = TablesEnd(code, position, loads, instructionStarts); if (end == 0) continue; const auto aligned = std::min(code.size(), static_cast(((codeAddress + end + 15) & ~VirtualAddress{15}) - codeAddress)); - if (!OnlyPadding(code, end, aligned, decoder)) continue; - if (tableEnd) *tableEnd = OnlyPadding(code, aligned, code.size(), decoder) ? code.size() : aligned; + const auto checkedTo = std::max(aligned, std::min(code.size(), unwindEnd)); + const bool toRangeEnd = OnlyPadding(code, end, code.size(), decoder); + if (!toRangeEnd && !OnlyPadding(code, end, checkedTo, decoder)) continue; + if (tableEnd) *tableEnd = toRangeEnd ? code.size() : checkedTo; return position; } } @@ -127,7 +129,7 @@ std::vector FindRelativeJumpTables(std::span> codeAfterFunctions{{TextAddress, TextAddress + 9}, {TextAddress + codeAfter.size(), TextAddress + codeAfter.size() + 8}}; const auto codeAfterTables = Relinker::FindRelativeJumpTables(codeAfter, TextAddress, codeAfterFunctions); Require(codeAfterTables.size() == 1 && codeAfterTables[0].Begin == TextAddress + tableOffset && codeAfterTables[0].End == TextAddress + 32, "Table before more code did not end at its padding"); + + std::size_t shiftedEnd = 0; + Require(Relinker::FindTrailingRelativeJumpTable(nonPadding, TextAddress + 1, &shiftedEnd) == tableOffset && shiftedEnd == 31, "Table boundary ignored the code address"); + const auto shifted = Relinker::FindRelativeJumpTables(nonPadding, TextAddress + 1, {{TextAddress + 1, TextAddress + 10}, {TextAddress + 33, TextAddress + 41}}); + Require(shifted.size() == 1 && shifted[0].Begin == TextAddress + 17 && shifted[0].End == TextAddress + 32, "Function address was not passed to the table search"); + Require(Relinker::FindTrailingRelativeJumpTable(nonPadding, TextAddress + 8) == tableOffset, "Table ending on a 16-byte boundary was rejected"); + + Bytes padded = text; + padded.resize(48, 0xcc); + const auto paddedTables = Relinker::FindRelativeJumpTables(padded, TextAddress, {{TextAddress, TextAddress + 9}, {TextAddress + 48, TextAddress + 56}}); + Require(paddedTables.size() == 1 && paddedTables[0].End == TextAddress + 48, "Padding to the end of the range was cut at the boundary"); + + Bytes nopAtRangeEnd = text; + const std::uint8_t fourByteNop[]{0x0f, 0x1f, 0x40, 0x00}; + std::memcpy(nopAtRangeEnd.data() + 28, fourByteNop, sizeof(fourByteNop)); + const auto cut = std::span(nopAtRangeEnd).first(31); + Require(Relinker::FindTrailingRelativeJumpTable(cut) == cut.size(), "Padding check read past the inspected range"); + + Bytes longNops = text; + longNops.resize(48); + const std::uint8_t fifteenByteNop[]{0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x2e, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00}; + const std::uint8_t nineByteNop[]{0x66, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00}; + std::memcpy(longNops.data() + 24, fifteenByteNop, sizeof(fifteenByteNop)); + std::memcpy(longNops.data() + 39, nineByteNop, sizeof(nineByteNop)); + std::size_t longNopsEnd = 0; + Require(Relinker::FindTrailingRelativeJumpTable(longNops, TextAddress, &longNopsEnd) == tableOffset && longNopsEnd == 48, "Table followed by a NOP across the boundary was not found"); + Bytes longNopsCodeAfter = longNops; + longNopsCodeAfter.resize(64, 0xcc); + longNopsCodeAfter[48] = 0x55; + longNopsCodeAfter[49] = 0x5d; + longNopsCodeAfter[50] = 0xc3; + Require(Relinker::FindTrailingRelativeJumpTable(longNopsCodeAfter, TextAddress) == longNopsCodeAfter.size(), "Table ending inside a NOP was accepted"); + + Bytes cutShort = text; + cutShort.resize(56, 0xcc); + const std::int32_t cutShortTargets[]{-16, -7, -6, -5, 0x20, 0x20}; + std::memcpy(cutShort.data() + tableOffset, cutShortTargets, sizeof(cutShortTargets)); + cutShort[48] = 0x55; + cutShort[49] = 0x5d; + cutShort[50] = 0xc3; + Require(Relinker::FindRelativeJumpTables(cutShort, TextAddress, {{TextAddress, TextAddress + cutShort.size()}}).empty(), "Table followed by more of its function was accepted"); } } From 3810389c9452c983815251c7af9121d663c82460 Mon Sep 17 00:00:00 2001 From: Maz Date: Tue, 20 Oct 2026 16:36:32 +0400 Subject: [PATCH 137/219] fix(libc): preserve zero in alternate wide octal formatting --- core/libs/prx/libc/src/FormattingWide.cpp | 2 ++ core/libs/tests/GuestWideFormatting.cpp | 26 +++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index 1526f39fef..594cf50f95 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -167,6 +167,8 @@ std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = if (length == "h") value = static_cast(value); if (length == "hh") value = static_cast(value); } else value = args.Next(); + if (conversion == u'o' && value == 0 && precision == 0 && spec.find('#') != std::string::npos) + spec = spec.substr(0, spec.find('.')) + ".1"; AppendNumber(out, spec + "ll" + static_cast(conversion), value); } else if (In(conversion, "aAeEfFgG")) { if (length == "L") AppendNumber(out, spec + "L" + static_cast(conversion), args.Next()); diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index 818f401ffd..8ebfa2753e 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -276,7 +276,33 @@ static void CheckWcsftime() { Require(WcsftimeThrows(buffer, u"%Y", nullptr), "wcsftime null time"); } +static void CheckAlternateOctalZero() { + struct Case { const char16_t* format; const char16_t* expected; unsigned value; }; + for (const auto& test : { + Case{u"%#.0o", u"0", 0}, Case{u"%#.o", u"0", 0}, Case{u"%#.00o", u"0", 0}, + Case{u"[%#5.0o]", u"[ 0]", 0}, Case{u"[%-#5.0o]", u"[0 ]", 0}, + Case{u"[%#05.0o]", u"[ 0]", 0}, Case{u"%.0o", u"", 0}, Case{u"%#.0x", u"", 0}, + Case{u"%#.0o", u"01", 1}, Case{u"%#.1o", u"0", 0}, Case{u"%#.3o", u"000", 0}, + Case{u"%#.0hho", u"0", 256}, Case{u"%#.0ho", u"0", 65536} + }) { + char16_t buffer[32]; + const auto expected = std::u16string(test.expected); + Require(swprintf_nid_postfix(buffer, 32, test.format, test.value) == static_cast(expected.size()) && buffer == expected, "swprintf alternate octal zero"); + Require(Format(buffer, 32, test.format, test.value) == static_cast(expected.size()) && buffer == expected, "vswprintf alternate octal zero"); + Require(snwprintf_s_nid_postfix(buffer, 32, test.format, test.value) == static_cast(expected.size()) && buffer == expected, "snwprintf_s alternate octal zero"); + } + char16_t buffer[16]; + Require(swprintf_nid_postfix(buffer, 16, u"%#.*o", 0, 0u) == 1 && buffer == std::u16string(u"0"), "octal zero dynamic precision"); + Require(swprintf_nid_postfix(buffer, 16, u"%#.*o", -1, 0u) == 1 && buffer == std::u16string(u"0"), "octal zero omitted precision"); + Require(swprintf_nid_postfix(buffer, 16, u"%2$#.*1$o", 0, 0u) == 1 && buffer == std::u16string(u"0"), "octal zero positional precision"); + Require(swprintf_nid_postfix(buffer, 16, u"%#.0llo", 0ull) == 1 && buffer == std::u16string(u"0"), "octal zero long long"); + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(snwprintf_s_nid_postfix(buffer, 1, u"%#.0o", 0u) == 1 && buffer[0] == 0 && buffer[1] == u'y', "octal zero bounded output"); +} + int main() { + CheckAlternateOctalZero(); CheckBounded(); CheckPositional(); CheckPointer(); From bf6f9cc3e2d506fab0c9c48ee008a590341f5ca1 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 01:41:54 +1300 Subject: [PATCH 138/219] feat(libc): add the floating-point trap controls feenableexcept, fedisableexcept, their __ forms, fegettrapenable and fesettrapenable unmask or mask exceptions in the x87 control word and MXCSR, with the console's return values. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libc/src/FloatingPoint.cpp | 45 ++++++++++++++++++++++++ core/libs/tests/GuestFloatingPoint.cpp | 39 ++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/core/libs/prx/libc/src/FloatingPoint.cpp b/core/libs/prx/libc/src/FloatingPoint.cpp index 304814503c..289822ace3 100644 --- a/core/libs/prx/libc/src/FloatingPoint.cpp +++ b/core/libs/prx/libc/src/FloatingPoint.cpp @@ -17,6 +17,17 @@ void ReplaceFlags(unsigned mask, unsigned flags) { _mm_setcsr((_mm_getcsr() & ~mask) | (flags & mask)); } +int ChangeTraps(int exceptions, bool enable) { + const auto mask = static_cast(exceptions) & Exceptions; + std::uint16_t control; + asm volatile("fnstcw %0" : "=m"(control)); + const unsigned csr = _mm_getcsr(); + const auto changed = static_cast(enable ? (control & ~mask) : (control | mask)); + asm volatile("fldcw %0" : : "m"(changed)); + _mm_setcsr(enable ? (csr & ~(mask << 7)) : (csr | (mask << 7))); + return static_cast(~((csr >> 7) | control) | ~Exceptions); +} + } extern "C" { @@ -67,4 +78,38 @@ int APS5_VABI feclearexcept_nid_postfix(int exceptions) { return 0; } + +int APS5_VABI feenableexcept_nid_postfix(int exceptions) { + return ChangeTraps(exceptions, true); +} + +int APS5_VABI fedisableexcept_nid_postfix(int exceptions) { + return ChangeTraps(exceptions, false); +} + +int APS5_VABI __feenableexcept_nid_postfix(int exceptions) { + return ChangeTraps(exceptions, true); +} + +int APS5_VABI __fedisableexcept_nid_postfix(int exceptions) { + return ChangeTraps(exceptions, false); +} + +int APS5_VABI fegettrapenable_nid_postfix() { + std::uint16_t control; + asm volatile("fnstcw %0" : "=m"(control)); + return ~control & Exceptions; +} + +int APS5_VABI fesettrapenable_nid_postfix(int exceptions) { + const auto mask = static_cast(exceptions); + if (mask > Exceptions) return 1; + std::uint16_t control; + asm volatile("fnstcw %0" : "=m"(control)); + control = static_cast(control & ~mask); + asm volatile("fldcw %0" : : "m"(control)); + _mm_setcsr(_mm_getcsr() & ~(static_cast(mask) << 7)); + return 0; +} + } diff --git a/core/libs/tests/GuestFloatingPoint.cpp b/core/libs/tests/GuestFloatingPoint.cpp index 7b06f4dbe1..75cb6dcb00 100644 --- a/core/libs/tests/GuestFloatingPoint.cpp +++ b/core/libs/tests/GuestFloatingPoint.cpp @@ -15,6 +15,12 @@ int APS5_VABI fetestexcept_nid_postfix(int); int APS5_VABI feclearexcept_nid_postfix(int); int APS5_VABI fegetexceptflag_nid_postfix(std::uint16_t*, int); int APS5_VABI fesetexceptflag_nid_postfix(const std::uint16_t*, int); +int APS5_VABI feenableexcept_nid_postfix(int); +int APS5_VABI fedisableexcept_nid_postfix(int); +int APS5_VABI __feenableexcept_nid_postfix(int); +int APS5_VABI __fedisableexcept_nid_postfix(int); +int APS5_VABI fegettrapenable_nid_postfix(); +int APS5_VABI fesettrapenable_nid_postfix(int); } namespace { @@ -155,10 +161,43 @@ void CheckThreads() { } +unsigned X87Masks() { + return ReadEnvironment().x87[0] & 0x3fu; +} + +unsigned SseMasks() { + return (ReadEnvironment().sse >> 7) & 0x3fu; +} + +int PreviousTraps() { + return static_cast(~(SseMasks() | X87Masks()) | ~0x3fu); +} + +void CheckTraps() { + const auto seeded = SeedFlags(0, 0); + Require(feenableexcept_nid_postfix(0x49) == static_cast(0xffffffc0u), "enable reports no previous trap as the console does"); + Require(X87Masks() == 0x36 && fegettrapenable_nid_postfix() == 0x09, "enable unmasks the x87 exceptions"); + Require(SseMasks() == 0x36 || SseMasks() == 0x3f, "enable unmasks the SSE exceptions"); + int previous = PreviousTraps(); + Require(fedisableexcept_nid_postfix(0x41) == previous, "disable returns the traps enabled on both units"); + Require(X87Masks() == 0x37 && fegettrapenable_nid_postfix() == 0x08, "disable masks the x87 exception"); + Require(SseMasks() == 0x37 || SseMasks() == 0x3f, "disable masks the SSE exception"); + previous = PreviousTraps(); + Require(__feenableexcept_nid_postfix(0x02) == previous && X87Masks() == 0x35, "the internal enable differs"); + previous = PreviousTraps(); + Require(__fedisableexcept_nid_postfix(0x3f) == previous && X87Masks() == 0x3f && fegettrapenable_nid_postfix() == 0, "the internal disable differs"); + Require(fesettrapenable_nid_postfix(0x40) == 1 && X87Masks() == 0x3f, "an out-of-range trap set was accepted"); + Require(fesettrapenable_nid_postfix(0x10004) == 0 && fegettrapenable_nid_postfix() == 0x04, "a trap set did not use the low 16 bits"); + Require(fesettrapenable_nid_postfix(0x01) == 0 && fegettrapenable_nid_postfix() == 0x05, "a trap set masked enabled traps"); + Require(fesettrapenable_nid_postfix(0) == 0 && fegettrapenable_nid_postfix() == 0x05, "an empty trap set masked enabled traps"); + WriteEnvironment(seeded); +} + int main() { const auto original = ReadEnvironment(); CheckRounding(); CheckFlags(); + CheckTraps(); CheckThreads(); WriteEnvironment(original); std::puts("floating-point environment checks passed"); From f3dec86695aeeb21db37866d9d074fef949a0103 Mon Sep 17 00:00:00 2001 From: Swetabh Singh Date: Tue, 20 Oct 2026 18:12:52 +0530 Subject: [PATCH 139/219] feat(libc): implement memmem and memccpy --- core/libs/prx/libc/src/Strings.cpp | 26 ++++++++++++++++++++++++++ core/libs/tests/GuestStrings.cpp | 12 ++++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 39 insertions(+) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 8774ef52ff..5c26d48035 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -231,6 +231,32 @@ void* APS5_VABI memrchr_nid_postfix(const void* s, int c, size_t n) { return nullptr; } +void* APS5_VABI memccpy_nid_postfix(void* dest, const void* src, int c, size_t n) { + auto* d = static_cast(dest); + const auto* s = static_cast(src); + const auto target = static_cast(c); + for (size_t i = 0; i < n; ++i) { + d[i] = s[i]; + if (s[i] == target) return d + i + 1; + } + return nullptr; +} + +void* APS5_VABI memmem_nid_postfix(const void* haystack, size_t haystacklen, const void* needle, size_t needlelen) { + if (needlelen == 0) return const_cast(haystack); + if (haystacklen < needlelen) return nullptr; + const auto* h = static_cast(haystack); + const auto* n = static_cast(needle); + const unsigned char first = n[0]; + const size_t limit = haystacklen - needlelen; + for (size_t i = 0; i <= limit; ++i) { + if (h[i] == first && std::memcmp(h + i, n, needlelen) == 0) { + return const_cast(h + i); + } + } + return nullptr; +} + int APS5_VABI strcmp_nid_postfix(const char* s1, const char* s2) { return std::strcmp(s1, s2); } diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index b2d98ae513..3eceafff4b 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -37,6 +37,8 @@ int APS5_VABI strncat_s_nid_postfix(char*, std::size_t, const char*, std::size_t int APS5_VABI memcpy_s_nid_postfix(void*, std::size_t, const void*, std::size_t); int APS5_VABI memmove_s_nid_postfix(void*, std::size_t, const void*, std::size_t); int APS5_VABI memset_s_nid_postfix(void*, std::size_t, int, std::size_t); +void* APS5_VABI memccpy_nid_postfix(void*, const void*, int, std::size_t); +void* APS5_VABI memmem_nid_postfix(const void*, std::size_t, const void*, std::size_t); char* APS5_VABI strnstr_nid_postfix(const char*, const char*, std::size_t); std::size_t APS5_VABI strxfrm_nid_postfix(char*, const char*, std::size_t); int APS5_VABI snprintf_s_nid_postfix(char*, std::size_t, const char*, ...); @@ -513,6 +515,16 @@ int main() { Require(strcasestr_nid_postfix("", "a") == nullptr); const char highBytes[] = {static_cast(0xff), 'A', 0}; Require(strcasestr_nid_postfix(highBytes, "a") == highBytes + 1); + const char hay[] = "hello world"; + Require(memmem_nid_postfix(hay, 11, "world", 5) == hay + 6); + Require(memmem_nid_postfix(hay, 11, "world", 0) == hay); + Require(memmem_nid_postfix(hay, 5, "world", 5) == nullptr); + Require(memmem_nid_postfix(hay, 11, "xyz", 3) == nullptr); + char dest[16] = {}; + Require(memccpy_nid_postfix(dest, "abc:def", ':', 8) == dest + 4); + Require(std::memcmp(dest, "abc:", 4) == 0); + Require(memccpy_nid_postfix(dest, "abcdef", ':', 6) == nullptr); + Require(memccpy_nid_postfix(dest, "abcdef", ':', 0) == nullptr); CheckSecureCopySizeLimits(); CheckSecureStringSizeLimits(); return CheckMemcpyOverlap() ? 0 : 1; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 931d7d1746..65198d8b88 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -55,6 +55,7 @@ ### Functional - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. +- [memmem and memccpy](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `memmem` searches for needle occurrences in memory buffers and `memccpy` copies buffers until a character match, following FreeBSD and POSIX specifications; behavior is unverified on console. - [Guest time conversions](../../../core/libs/prx/libc/src/Time.cpp) rewrite the host process `TZ` when a guest changes its own `TZ`, so other host consumers of that variable (including libSceRtc local time) shift too; code outside libc does not take the zone lock. Zone syntax follows the host C runtime, not FreeBSD: Windows only accepts the `std offset [dst]` form, treats an empty guest `TZ` as `UTC0` and reads `:Europe/Berlin` as an abbreviation (for example `rop`); only Linux honours zone file names. If the guest accepts a `TZ` value that the host rejects (on Windows, for example, one longer than the host environment limit), conversions throw `std::runtime_error`. A `tm_zone` saved before a `TZ` change can refer to storage now holding the new zone name, changing `%Z` output. - [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. From 34e8cf1e140481fc86c0009530a4a1704e15c965 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 01:44:56 +1300 Subject: [PATCH 140/219] feat(libc): implement basename_r basename_r writes basename's result into the caller's buffer; basename now uses it with its per-thread buffer. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libc/src/StringExtensions.cpp | 12 +++++++++--- core/libs/tests/GuestStrings.cpp | 10 ++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/src/StringExtensions.cpp b/core/libs/prx/libc/src/StringExtensions.cpp index a6cb27bb7c..18b9cb6f18 100644 --- a/core/libs/prx/libc/src/StringExtensions.cpp +++ b/core/libs/prx/libc/src/StringExtensions.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include extern "C" { @@ -28,20 +29,25 @@ long long APS5_VABI strtonum_nid_postfix(const char* text, long long minimum, lo return value; } -char* APS5_VABI basename_nid_postfix(const char* path) { - thread_local char buffer[1024]; +char* APS5_VABI basename_r_nid_postfix(const char* path, char* buffer) { + if (buffer == nullptr) throw std::invalid_argument("basename_r: null buffer"); std::string_view name = path && *path ? path : "."; while (name.size() > 1 && name.back() == '/') name.remove_suffix(1); if (name != "/") { const auto separator = name.find_last_of('/'); if (separator != std::string_view::npos) name.remove_prefix(separator + 1); } - if (name.size() >= sizeof(buffer)) { errno = 63; return nullptr; } + if (name.size() >= 1024) { errno = 63; return nullptr; } std::memcpy(buffer, name.data(), name.size()); buffer[name.size()] = '\0'; return buffer; } +char* APS5_VABI basename_nid_postfix(const char* path) { + thread_local char buffer[1024]; + return basename_r_nid_postfix(path, buffer); +} + char* APS5_VABI __inet_ntoa_nid_postfix(unsigned int address) { thread_local char buffer[sizeof("255.255.255.255")]; unsigned char bytes[sizeof(address)]; diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index b2d98ae513..e12918f9df 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -11,6 +11,7 @@ extern "C" { void APS5_VABI bcopy_nid_postfix(const void*, void*, std::size_t); char* APS5_VABI basename_nid_postfix(const char*); +char* APS5_VABI basename_r_nid_postfix(const char*, char*); char* APS5_VABI __inet_ntoa_nid_postfix(unsigned int); char* APS5_VABI dirname_nid_postfix(const char*); int* APS5_VABI __error_nid_postfix(); @@ -360,6 +361,15 @@ int main() { Require(std::strcmp(basename_nid_postfix("one\\two"), "one\\two") == 0); const std::string longName(1024, 'x'); Require(basename_nid_postfix(longName.c_str()) == nullptr && *__error_nid_postfix() == 63); + char baseBuffer[1024]; + Require(basename_r_nid_postfix(path, baseBuffer) == baseBuffer && std::strcmp(baseBuffer, "two") == 0); + Require(basename_r_nid_postfix(nullptr, baseBuffer) == baseBuffer && std::strcmp(baseBuffer, ".") == 0); + Require(basename_r_nid_postfix("///", baseBuffer) == baseBuffer && std::strcmp(baseBuffer, "/") == 0); + Require(basename_r_nid_postfix("plain", baseBuffer) == baseBuffer && std::strcmp(baseBuffer, "plain") == 0); + const std::string longest(1023, 'y'); + Require(basename_r_nid_postfix(("/a/" + longest).c_str(), baseBuffer) == baseBuffer && std::strlen(baseBuffer) == 1023); + *__error_nid_postfix() = 0; + Require(basename_r_nid_postfix(longName.c_str(), baseBuffer) == nullptr && *__error_nid_postfix() == 63); const unsigned char loopbackBytes[4] = {127, 0, 0, 1}; unsigned int loopback; std::memcpy(&loopback, loopbackBytes, sizeof(loopback)); From 1ea49c741f9b0f3e3f123036792a26952ef204ca Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Tue, 20 Oct 2026 15:48:42 +0300 Subject: [PATCH 141/219] fix(relinker): tighten the syscall skip between PS4 unwind functions The skip of syscall patterns outside the unwind functions let some real syscalls through. The scan now merges nested and overlapping unwind ranges with the instruction scanner's MergeCodeRanges, so a pattern inside an outer function is refused. A constant syscall number is also looked for in the bytes in front of the pattern, whatever alignment the decoding took: a mov eax, mov rax or xor eax, eax that ends at the pattern or before register moves and zeroings that leave rax alone. The decoded lookback requires each instruction to end where the next one starts. Guest modules without PT_SCE_DYNLIBDATA get no unwind ranges, so their scan stays strict even when their unwind table is read. Co-Authored-By: Claude Opus 5.5 --- .../include/codegen/IInstructionScanner.hpp | 2 + .../codegen/src/InstructionScanner.cpp | 4 -- .../relinker/src/analysis/SyscallScanner.cpp | 62 +++++++++++++++++-- .../relinker/src/guest/GuestModuleBuilder.cpp | 2 +- .../relinker/tests/test_sce_code_extent.py | 7 ++- .../relinker/tests/test_sce_code_gaps.py | 60 +++++++++++++++++- docs/dev/debt/relinker.md | 2 +- 7 files changed, 121 insertions(+), 18 deletions(-) diff --git a/core/relinker/codegen/include/codegen/IInstructionScanner.hpp b/core/relinker/codegen/include/codegen/IInstructionScanner.hpp index e5314b276a..a09771a326 100644 --- a/core/relinker/codegen/include/codegen/IInstructionScanner.hpp +++ b/core/relinker/codegen/include/codegen/IInstructionScanner.hpp @@ -17,6 +17,8 @@ public: virtual void VisitCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize, const std::function& visit) const = 0; }; +std::vector MergeCodeRanges(std::vector ranges); + std::unique_ptr MakeInstructionScanner(std::function progress = {}, std::vector unwindRanges = {}); } diff --git a/core/relinker/codegen/src/InstructionScanner.cpp b/core/relinker/codegen/src/InstructionScanner.cpp index 6a6c0018b1..28f8544994 100644 --- a/core/relinker/codegen/src/InstructionScanner.cpp +++ b/core/relinker/codegen/src/InstructionScanner.cpp @@ -8,8 +8,6 @@ namespace Codegen { -namespace { - std::vector MergeCodeRanges(std::vector ranges) { std::erase_if(ranges, [](const Domain::CodeRange& range) { return range.End <= range.Begin; }); std::sort(ranges.begin(), ranges.end(), [](const Domain::CodeRange& left, const Domain::CodeRange& right) { return left.Begin < right.Begin; }); @@ -23,8 +21,6 @@ std::vector MergeCodeRanges(std::vector ra return merged; } -} - class InstructionScanner : public IInstructionScanner { public: InstructionScanner(std::function progress, std::vector unwindRanges) : progress(std::move(progress)), unwindRanges(MergeCodeRanges(std::move(unwindRanges))) {} diff --git a/core/relinker/relinker/src/analysis/SyscallScanner.cpp b/core/relinker/relinker/src/analysis/SyscallScanner.cpp index d8d717ff30..3902220628 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -147,8 +147,12 @@ static std::optional ConstantRaxLoad(const std::uint8_t* bytes, c } static std::optional ConstantSyscallNumber(const std::vector& codeSection, const FileByteOffset codeSectionOffset, const RecentInstructions& recent) { + FileByteOffset next = recent.Before(0).Offset; for (std::size_t step = 1; step <= SYSCALL_NUMBER_LOOKBACK && step < recent.count; ++step) { const auto& match = recent.Before(step); + if (match.Offset + match.Length != next) + return std::nullopt; + next = match.Offset; const std::size_t start = static_cast(match.Offset - codeSectionOffset); if (start >= codeSection.size() || match.Length > codeSection.size() - start) @@ -171,6 +175,49 @@ static std::optional ConstantSyscallNumber(const std::vector ConstantRaxSetEndingAt(const std::vector& code, const std::size_t end) { + std::uint32_t value = 0; + if (end >= 5 && code[end - 5] == 0xB8) { + std::memcpy(&value, code.data() + end - 4, sizeof(value)); + return value; + } + if (end >= 7 && code[end - 7] == 0x48 && code[end - 6] == 0xC7 && code[end - 5] == 0xC0) { + std::memcpy(&value, code.data() + end - 4, sizeof(value)); + return value; + } + if (end >= 2 && (code[end - 2] == 0x31 || code[end - 2] == 0x33) && code[end - 1] == 0xC0) + return value; + return std::nullopt; +} + +static bool MovesOtherRegisterThanRax(const std::uint8_t* bytes, const std::size_t length) { + const std::uint8_t rex = length == 3 ? bytes[0] : 0; + if (length == 3 && (rex & 0xF0) != 0x40) + return false; + const std::uint8_t opcode = bytes[length - 2]; + const std::uint8_t modrm = bytes[length - 1]; + if ((modrm & 0xC0) != 0xC0) + return false; + const bool writesRm = opcode == 0x89 || opcode == 0x31; + const bool writesReg = opcode == 0x8B || opcode == 0x33; + if (!writesRm && !writesReg) + return false; + const unsigned written = writesRm ? (modrm & 7u) | ((rex & 1u) << 3) : ((modrm >> 3) & 7u) | ((rex & 4u) << 1); + return written != 0; +} + +static std::optional ConstantSyscallNumberInBytes(const std::vector& code, const std::size_t end, const std::size_t steps) { + if (const auto value = ConstantRaxSetEndingAt(code, end)) + return value; + if (steps == 0) + return std::nullopt; + for (const std::size_t length : {std::size_t{2}, std::size_t{3}}) + if (end >= length && MovesOtherRegisterThanRax(code.data() + end - length, length)) + if (const auto value = ConstantSyscallNumberInBytes(code, end - length, steps - 1)) + return value; + return std::nullopt; +} + class SyscallScanner : public ISyscallScanner { public: SyscallScanner(Domain::IProgress& progress, const bool trapSdkStubs) : progress(progress), trapSdkStubs(trapSdkStubs) {} @@ -191,8 +238,7 @@ std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::v const auto scanner = Codegen::MakeInstructionScanner([&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Decode, source, completed, total}); }, unwindRanges); RecentInstructions recent; std::vector trapped; - std::vector functions(unwindRanges); - std::sort(functions.begin(), functions.end(), [](const CodeRange& left, const CodeRange& right) { return left.Begin < right.Begin; }); + const auto functions = Codegen::MergeCodeRanges(unwindRanges); const auto insideFunction = [&](const std::size_t offset) { const auto after = std::upper_bound(functions.begin(), functions.end(), offset, [](const std::size_t value, const CodeRange& range) { return value < range.Begin; }); return after != functions.begin() && offset < std::prev(after)->End; @@ -222,11 +268,15 @@ std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::v return; } - const auto number = kind == ForbiddenInstruction::Invocation ? ConstantSyscallNumber(codeSection, codeSectionOffset, recent) : std::nullopt; + auto number = kind == ForbiddenInstruction::Invocation ? ConstantSyscallNumber(codeSection, codeSectionOffset, recent) : std::nullopt; if (!functions.empty() && !number && !insideFunction(start)) { - ++skipped; - if (!firstSkipped) firstSkipped = match.Offset; - return; + if (kind == ForbiddenInstruction::Invocation) + number = ConstantSyscallNumberInBytes(codeSection, opcode, SYSCALL_NUMBER_LOOKBACK - 1); + if (!number) { + ++skipped; + if (!firstSkipped) firstSkipped = match.Offset; + return; + } } std::ostringstream msg; diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 0ced33fbb9..467f57ab1e 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -356,7 +356,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (sceImage) if (const auto end = FindSceCodeEnd(code, header.MappedAddress, unwindFunctions, image.Got, image.Dynamic.RelaPltData.size())) code.resize(*end - header.MappedAddress); - const auto unwindRanges = SceUnwindRanges(code, header.MappedAddress, unwindFunctions); + const auto unwindRanges = sceImage ? SceUnwindRanges(code, header.MappedAddress, unwindFunctions) : std::vector{}; for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, code.size(), Io::Utf8Path(image.SourcePath), unwindRanges)) { if (offset > code.size() || code.size() - offset < 2u) throw Domain::RelinkerException("Syscall trap patch exceeds guest executable segment", header.MappedAddress + offset); diff --git a/core/relinker/relinker/tests/test_sce_code_extent.py b/core/relinker/relinker/tests/test_sce_code_extent.py index e3a50f8296..5eff302160 100644 --- a/core/relinker/relinker/tests/test_sce_code_extent.py +++ b/core/relinker/relinker/tests/test_sce_code_extent.py @@ -26,12 +26,13 @@ INVALID = bytes.fromhex("41 41 41 41 41 41 66 66 66 66 2e 0f 1f 84 00 00 00 00 0 def fixture(sce_block=True, plt_push_offset=8, gap=0, interpreter_pointer=False, data_pointer=False, gap_entry=False, - plt_at=None, early_function=False, function_tls=False, pointer_target=None, gap_bytes=None): + plt_at=None, early_function=False, function_tls=False, pointer_target=None, gap_bytes=None, functions=None): plt = FUNCTION + FUNCTION_SIZE + gap if plt_at is None else plt_at entry = FUNCTION + FUNCTION_SIZE if gap_entry else FUNCTION data = plt + 0x20 if plt > FUNCTION else FUNCTION + FUNCTION_SIZE - functions = [(EARLY_FUNCTION, FUNCTION_SIZE)] if early_function else [] - functions.append((FUNCTION, FUNCTION_SIZE)) + if functions is None: + functions = [(EARLY_FUNCTION, FUNCTION_SIZE)] if early_function else [] + functions.append((FUNCTION, FUNCTION_SIZE)) interpreter = FUNCTION - len(INTERPRETER) image = bytearray(0x1000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) diff --git a/core/relinker/relinker/tests/test_sce_code_gaps.py b/core/relinker/relinker/tests/test_sce_code_gaps.py index d3acbb55e5..3f2590e5a3 100644 --- a/core/relinker/relinker/tests/test_sce_code_gaps.py +++ b/core/relinker/relinker/tests/test_sce_code_gaps.py @@ -15,6 +15,18 @@ SYSCALL_NUMBER = bytes.fromhex("b8 14 00 00 00") TABLE = bytes.fromhex("0c 05 00 00 0f 05 00 b0 0f 05 00 00 10 05 00 50") GAP = FUNCTION + FUNCTION_SIZE TARGETS = (("linux", []), ("windows", ["--windows"]), ("macos", ["--macos"])) +SKIPPED = "Skipped %d syscall byte pattern(s) without a syscall number outside the unwind functions, the first at code offset %#x" +MOV_EAX_SYSCALL = bytes.fromhex("b8 3b 00 00 00 0f 05 c3") +UNDECODABLE = bytes.fromhex("0f 78 11 22 33 44") +LOADED_NUMBERS = ( + ("mov", MOV_EAX_SYSCALL, 0x215, 59), + ("rex-after-undecodable", UNDECODABLE + MOV_EAX_SYSCALL, 0x21B, 59), + ("compare-after-undecodable", UNDECODABLE + b"\x3c" + MOV_EAX_SYSCALL, 0x21C, 59), + ("padding-after-undecodable", UNDECODABLE + b"\xcc\xcc" + MOV_EAX_SYSCALL, 0x21D, 59), + ("register-move", bytes.fromhex("b8 3b 00 00 00 48 89 df 0f 05 c3"), 0x218, 59), + ("zeroed", bytes.fromhex("31 c0 0f 05 c3"), 0x212, 0), +) +JUMP_TABLE_DISPATCH = bytes.fromhex("48 8d 0d 09 00 00 00 48 63 04 b9 48 01 c8 ff e0") def executable(gap_bytes=b"", sce_block=True, function=b"", before_function=b""): @@ -25,6 +37,30 @@ def executable(gap_bytes=b"", sce_block=True, function=b"", before_function=b"") return image, plt +def nested_executable(): + image, plt = fixture(gap=0x20, functions=[(0x180, 0x80), (0x190, 0x8), (FUNCTION, FUNCTION_SIZE)]) + image[0x180:FUNCTION] = b"\x90" * (FUNCTION - 0x180) + image[0x1C0:0x1C5] = bytes.fromhex("89 f8 0f 05 c3") + return image, plt + + +def jump_table_executable(): + image, plt = fixture(gap=0x30, functions=[(FUNCTION, FUNCTION_SIZE), (0x230, FUNCTION_SIZE)]) + image[FUNCTION:GAP] = JUMP_TABLE_DISPATCH + image[GAP:GAP + 16] = struct.pack("<4i", -16, -16, -16, -16) + image[GAP + 16:GAP + 16 + len(MOV_EAX_SYSCALL)] = MOV_EAX_SYSCALL + return image, plt + + +def ps5_module(): + image = sce_module() + struct.pack_into(" Date: Tue, 20 Oct 2026 15:48:42 +0300 Subject: [PATCH 142/219] fix(relinker): follow fs loads and report pointers taken as data between PS4 unwind functions A relocation or lea target outside the unwind functions of a PS4 image is now decoded up to its first jump, return or trap, or to the start of the next unwind function, which also bounds the transfer check of the tail rule. A target whose run holds an fs:[disp32] access, the form the linear TLS sweep roots, is followed as code. The other targets taken as data are counted in one message and kept as entry points that the Windows TLS rewrite may not overwrite. The analysis merges nested unwind ranges like the scans. Co-Authored-By: Claude Opus 5.5 --- .../elfpatcher/general/GuestModuleWriter.hpp | 6 +- .../include/elfpatcher/macos/MacOsImage.hpp | 3 + .../elfpatcher/macos/MacOsMachOPatcher.hpp | 5 +- .../elfpatcher/windows/WindowsElfPatcher.hpp | 4 +- .../elfpatcher/windows/WindowsTlsBuilder.hpp | 3 +- .../src/macos/MacOsGuestModuleWriter.cpp | 3 +- .../elfpatcher/src/macos/MacOsImage.cpp | 6 +- .../src/macos/MacOsMachOPatcher.cpp | 3 +- .../src/windows/WindowsGuestModuleWriter.cpp | 4 +- .../src/windows/WindowsPePatcher.cpp | 4 +- .../src/windows/WindowsTlsBuilder.cpp | 4 +- .../analysis/CodeInstructionCollector.hpp | 3 +- .../src/analysis/CodeInstructionCollector.cpp | 73 ++++++++++++++----- .../relinker/src/guest/GuestModuleBuilder.cpp | 5 +- .../relinker/src/pipeline/Conversion.cpp | 4 +- .../relinker/tests/test_sce_code_extent.py | 69 ++++++++++++++++-- docs/dev/debt/relinker.md | 2 +- 17 files changed, 154 insertions(+), 47 deletions(-) diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp index a59418beea..5ca491e9bf 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp @@ -2,14 +2,16 @@ #define ELFPATCHER_GUESTMODULEWRITER_HPP #include +#include +#include namespace Elfpatcher { class GuestModuleWriter { public: std::vector WriteLinux(const Relinker::GuestImage& image, const std::vector& dependencies, const std::string& runPath) const; - std::vector WriteWindows(const Relinker::GuestImage& image, Domain::GuestRuntime& runtime) const; - std::vector WriteMacOs(const Relinker::GuestImage& image, const std::vector& dependencies, const std::string& runPath) const; + std::vector WriteWindows(const Relinker::GuestImage& image, Domain::GuestRuntime& runtime, const std::function& message = {}) const; + std::vector WriteMacOs(const Relinker::GuestImage& image, const std::vector& dependencies, const std::string& runPath, const std::function& message = {}) const; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp index b65c2f4851..99bc214e54 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp @@ -5,7 +5,9 @@ #include #include #include +#include #include +#include #include namespace Elfpatcher::MacOs { @@ -59,6 +61,7 @@ struct MacOsImageInput { std::vector TlsImports; std::vector TlsExports; const std::vector* Trampolines = nullptr; + std::function Message; }; std::uint64_t GuestTlsBlockSize(const Domain::ProgramHeader& tls); diff --git a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp index 397c96e826..2f2ffbc485 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp @@ -3,13 +3,15 @@ #include #include +#include #include +#include namespace Elfpatcher::MacOs { class MacOsMachOPatcher final : public IElfPatcher { public: - explicit MacOsMachOPatcher(std::optional fixedBase = std::nullopt); + explicit MacOsMachOPatcher(std::optional fixedBase = std::nullopt, std::function message = {}); std::vector Patch( std::vector sourceElf, @@ -24,6 +26,7 @@ public: private: std::optional fixedBase; + std::function message; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp index 6fe65d2fc7..07db6f888a 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp @@ -6,12 +6,13 @@ #include #include #include +#include namespace Elfpatcher::Windows { class WindowsPePatcher : public IElfPatcher { public: - explicit WindowsPePatcher(bool windowsGui = false, std::optional> icon = std::nullopt, std::filesystem::path iconName = "icon0.png", std::function progress = {}, std::optional fixedBase = std::nullopt, bool traceImports = false); + explicit WindowsPePatcher(bool windowsGui = false, std::optional> icon = std::nullopt, std::filesystem::path iconName = "icon0.png", std::function progress = {}, std::optional fixedBase = std::nullopt, bool traceImports = false, std::function message = {}); const std::string& GetDiagnostics() const; std::vector Patch(std::vector sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, std::uint64_t originalPltGotVaddr, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector& trampolines) override; @@ -24,6 +25,7 @@ private: std::filesystem::path iconName; std::optional fixedBase; bool traceImports; + std::function message; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp index 207ef639bf..39b4a54f87 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp @@ -3,12 +3,13 @@ #include #include +#include namespace Elfpatcher::Windows { class WindowsTlsBuilder { public: - PeDirectory Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva = nullptr, const std::function& progress = {}) const; + PeDirectory Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva = nullptr, const std::function& progress = {}, const std::function& message = {}) const; }; } diff --git a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp index 74e31d19d3..b784c0d08a 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp @@ -21,7 +21,7 @@ std::string DynamicName(const Relinker::GuestImage& guest, const std::size_t ind } -std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestImage& guest, const std::vector& dependencies, const std::string& runPath) const { +std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestImage& guest, const std::vector& dependencies, const std::string& runPath, const std::function& message) const { using namespace MacOs; Windows::WindowsLoadImage image(guest.Bytes, guest.Headers, false); MacOsImageInput input; @@ -30,6 +30,7 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma input.Source = &guest.Bytes; input.Headers = &guest.Headers; input.Image = ℑ + input.Message = message; const auto tls = std::find_if(guest.Headers.begin(), guest.Headers.end(), [](const auto& header) { return header.Type == 7; }); std::map targets; diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index 3ba5513d8c..38ac3d51e5 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -221,10 +221,10 @@ struct TlsScan { std::vector GsTraps; }; -TlsScan ScanTls(const std::vector& source, const std::vector& headers, const Windows::WindowsLoadImage& image) { +TlsScan ScanTls(const std::vector& source, const std::vector& headers, const Windows::WindowsLoadImage& image, const std::function& message) { TlsScan scan; const Codegen::X64InstructionDecoder decoder; - const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers); + const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, {}, nullptr, message); for (const auto& header : headers) { if (header.Type == 7) { if (scan.Tls != nullptr) throw Domain::RelinkerException("Multiple ELF TLS segments"); @@ -382,7 +382,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { auto& image = *input.Image; const auto& headers = *input.Headers; const auto imageBase = image.GetImageBase(); - const auto tls = ScanTls(*input.Source, headers, image); + const auto tls = ScanTls(*input.Source, headers, image, input.Message); const bool hasTls = tls.Tls != nullptr && (tls.Tls->MemorySize != 0 || !tls.Accesses.empty()); if (!hasTls && !tls.Accesses.empty()) throw Domain::RelinkerException("Guest TLS access without a non-empty PT_TLS"); if (!hasTls && !input.TlsModuleSlots.empty()) throw Domain::RelinkerException("Guest TLS module relocation without a non-empty PT_TLS"); diff --git a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp index f86bf76fcd..ff62ba6e2d 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp @@ -51,7 +51,7 @@ std::string MachODependency(const std::string& name, const std::string& origin) return "@rpath/" + name; } -MacOsMachOPatcher::MacOsMachOPatcher(const std::optional fixedBase) : fixedBase(fixedBase) { +MacOsMachOPatcher::MacOsMachOPatcher(const std::optional fixedBase, std::function message) : fixedBase(fixedBase), message(std::move(message)) { if (fixedBase && (*fixedBase < 0x20000 || *fixedBase > 0x7fffffff0000 || (*fixedBase & 0xffff) != 0)) throw Domain::RelinkerException("Invalid fixed macOS guest image base"); } @@ -71,6 +71,7 @@ std::vector MacOsMachOPatcher::Patch(std::vector sou input.Source = &sourceElf; input.Headers = &originalHeaders; input.Image = ℑ + input.Message = message; for (std::size_t offset = 24; offset < dynamicSection.DynSymData.size(); offset += 24) { const auto& symbols = dynamicSection.DynSymData; const auto section = Io::ReadU16(symbols, offset + 6); diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index a84aea49c4..c4cddc5c7f 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -13,7 +13,7 @@ namespace Elfpatcher { -std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestImage& guest, Domain::GuestRuntime& runtime) const { +std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestImage& guest, Domain::GuestRuntime& runtime, const std::function& message) const { using namespace Windows; if (guest.Symbolic) throw Domain::RelinkerException("DF_SYMBOLIC guest modules are not supported by the Windows writer"); WindowsLoadImage image(guest.Bytes, guest.Headers, false); @@ -84,7 +84,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI std::array directories{}; std::uint32_t tlsIndex = 0; try { - directories[9] = WindowsTlsBuilder().Build(guest.Bytes, guest.Headers, image, sections, relocations, nextRva, &tlsIndex); + directories[9] = WindowsTlsBuilder().Build(guest.Bytes, guest.Headers, image, sections, relocations, nextRva, &tlsIndex, {}, message); } catch (Domain::RelinkerException& error) { if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(guest.SourcePath); throw; diff --git a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp index ef924f15f0..335ea0f587 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -28,7 +28,7 @@ void writeGotStub(std::vector& sections, const std::uint32_t targetRv } -WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional> icon, std::filesystem::path iconName, std::function progress, const std::optional fixedBase, const bool traceImports) : progress(std::move(progress)), windowsGui(windowsGui), icon(std::move(icon)), iconName(std::move(iconName)), fixedBase(fixedBase), traceImports(traceImports) { +WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional> icon, std::filesystem::path iconName, std::function progress, const std::optional fixedBase, const bool traceImports, std::function message) : progress(std::move(progress)), windowsGui(windowsGui), icon(std::move(icon)), iconName(std::move(iconName)), fixedBase(fixedBase), traceImports(traceImports), message(std::move(message)) { if (fixedBase && (*fixedBase < 0x20000 || *fixedBase > 0x7fffffff0000 || (*fixedBase & 0xffff) != 0)) throw Domain::RelinkerException("Invalid fixed Windows guest image base"); } @@ -68,7 +68,7 @@ std::vector WindowsPePatcher::Patch(std::vector sour sections.push_back({".ehmeta", nextRva, SectionRead | 0x40u, std::move(metadata)}); nextRva = AlignRva(nextRva + sections.back().Data.size()); } - directories[9] = WindowsTlsBuilder().Build(sourceElf, originalHeaders, image, sections, relocations.BaseRelocations, nextRva, nullptr, [&](auto completed, auto total) { report(1500 + 6500 * completed / total); }); + directories[9] = WindowsTlsBuilder().Build(sourceElf, originalHeaders, image, sections, relocations.BaseRelocations, nextRva, nullptr, [&](auto completed, auto total) { report(1500 + 6500 * completed / total); }, message); report(8000); WindowsTrampolineBuilder().Build(trampolines, image, sections, nextRva); const WindowsImportBuilder importBuilder; diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index 08576fe5f8..8bee6f3468 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -107,7 +107,7 @@ void trapInstruction(std::vector& sections, const std::uint32_t rva, } -PeDirectory WindowsTlsBuilder::Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva, const std::function& progress) const { +PeDirectory WindowsTlsBuilder::Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva, const std::function& progress, const std::function& message) const { const auto report = [&](std::uint64_t completed) { if (progress) progress(completed, 10000); }; const Domain::ProgramHeader* tls = nullptr; const Codegen::X64InstructionDecoder decoder; @@ -116,7 +116,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co std::vector> gsTraps; std::vector branchTargets; std::set entryRoots; - const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, [&](auto completed, auto total) { report(7000 * completed / total); }, &entryRoots); + const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, [&](auto completed, auto total) { report(7000 * completed / total); }, &entryRoots, message); std::uint64_t scanned = 0; for (const auto& header : headers) { diff --git a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp index de93097012..cdd140af64 100644 --- a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp +++ b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp @@ -4,12 +4,13 @@ #include #include #include +#include namespace Relinker { class CodeInstructionCollector { public: - std::set Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress = {}, std::set* entryRoots = nullptr) const; + std::set Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress = {}, std::set* entryRoots = nullptr, const std::function& message = {}) const; }; } diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 9fab805034..06fccacb17 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -12,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -38,7 +40,7 @@ public: } -std::set CodeInstructionCollector::Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress, std::set* entryRoots) const { +std::set CodeInstructionCollector::Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress, std::set* entryRoots, const std::function& message) const { std::uint64_t lastProgress = 0; const auto report = [&](std::uint64_t completed) { if (completed <= lastProgress) return; @@ -96,11 +98,11 @@ std::set CodeInstructionCollector::Collect(const std::ve range(offset, size); return offset; }; - std::vector> sceFunctions; + std::vector sceFunctions; if (sceBase) { const auto unwindFunctions = UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); - for (const auto& function : unwindFunctions) sceFunctions.emplace_back(function.Begin, function.End); - std::sort(sceFunctions.begin(), sceFunctions.end()); + for (const auto& function : unwindFunctions) sceFunctions.push_back({function.Begin, function.End}); + sceFunctions = Codegen::MergeCodeRanges(std::move(sceFunctions)); for (auto& header : codeHeaders) { if (header.Type != 1 || (header.Flags & 1) == 0) continue; range(header.Offset, header.FileSize); @@ -121,14 +123,17 @@ std::set CodeInstructionCollector::Collect(const std::ve std::map tables; const auto inTable = [&](std::uint64_t address) { const auto after = tables.upper_bound(address); return after != tables.begin() && address < std::prev(after)->second; }; const auto addRoot = [&](std::uint64_t address) { if (isCode(address) && !inTable(address)) roots.insert(address); }; + const auto sceFunctionAfter = [&](std::uint64_t address) { + return std::upper_bound(sceFunctions.begin(), sceFunctions.end(), address, [](std::uint64_t value, const Domain::CodeRange& function) { return value < function.Begin; }); + }; const auto insideSceFunction = [&](std::uint64_t address) { - const auto after = std::upper_bound(sceFunctions.begin(), sceFunctions.end(), address, [](std::uint64_t value, const std::pair& function) { return value < function.first; }); - return after != sceFunctions.begin() && address < std::prev(after)->second; + const auto after = sceFunctionAfter(address); + return after != sceFunctions.begin() && address < std::prev(after)->End; }; const auto nearSceFunctionEnd = [&](std::uint64_t address) { - const auto after = std::upper_bound(sceFunctions.begin(), sceFunctions.end(), address, [](std::uint64_t value, const std::pair& function) { return value < function.first; }); + const auto after = sceFunctionAfter(address); if (after == sceFunctions.begin()) return false; - const auto end = std::prev(after)->second; + const auto end = std::prev(after)->End; return address >= end && address - end < 256; }; const auto codeAt = [&](std::uint64_t address) { @@ -137,33 +142,53 @@ std::set CodeInstructionCollector::Collect(const std::ve return std::span(bytes.data() + header.Offset + (address - header.MappedAddress), header.FileSize - (address - header.MappedAddress)); return std::span(); }; - const auto decodesToTransfer = [&](std::uint64_t address) { + const auto absoluteFsAccess = [](const std::uint8_t* code, const Codegen::DecodedInstructionInfo& info) { + return info.SegmentPrefix == 0x64 && info.OpcodeOffset + 2 < info.Length && (code[info.OpcodeOffset + 1] & 0xc7) == 0x04 && code[info.OpcodeOffset + 2] == 0x25; + }; + struct GapRun { + bool Transfer = false; + bool AbsoluteFsAccess = false; + }; + const auto decodeGapRun = [&](std::uint64_t address) { + const auto after = sceFunctionAfter(address); + const auto limit = after == sceFunctions.end() ? std::numeric_limits::max() : after->Begin; const Codegen::X64InstructionDecoder probe; - for (std::size_t count = 0; count < 256; ++count) { + GapRun run; + for (std::size_t count = 0; count < 256 && address < limit; ++count) { const auto code = codeAt(address); - if (code.empty()) return false; + if (code.empty()) return run; + const auto available = std::min(code.size(), limit - address); Codegen::DecodedInstructionInfo info; try { - info = probe.DecodeInstruction(code.data(), code.size()); + info = probe.DecodeInstruction(code.data(), available); } catch (const Codegen::CodegenException&) { - return false; + return run; } - if (info.Length == 0 || info.Length > code.size()) return false; + if (info.Length == 0 || info.Length > available) return run; + if (absoluteFsAccess(code.data(), info)) run.AbsoluteFsAccess = true; switch (info.FlowKind) { case Codegen::ControlFlowKind::UnconditionalJump: case Codegen::ControlFlowKind::Return: case Codegen::ControlFlowKind::IndirectJump: case Codegen::ControlFlowKind::Trap: - return true; + run.Transfer = true; + return run; default: break; } address += info.Length; } - return false; + return run; }; + std::set dataTargets; const auto addPointerRoot = [&](std::uint64_t address) { - if (sceFunctions.empty() || insideSceFunction(address) || (address % 16 == 0 && nearSceFunctionEnd(address) && decodesToTransfer(address))) addRoot(address); + if (sceFunctions.empty() || !isCode(address) || insideSceFunction(address)) { + addRoot(address); + return; + } + const auto run = decodeGapRun(address); + if (run.AbsoluteFsAccess || (address % 16 == 0 && nearSceFunctionEnd(address) && run.Transfer)) addRoot(address); + else dataTargets.insert(address); }; const auto addAddressTakenRoot = [&](std::uint64_t address, const std::uint8_t* code, const Codegen::DecodedInstructionInfo& info) { if (!info.HasRipRelativeDisp || code[info.OpcodeOffset] != 0x8d || !(info.RexPrefix & 8)) return; @@ -413,8 +438,7 @@ std::set CodeInstructionCollector::Collect(const std::ve position = crossed; continue; } - const auto opcode = position + info.OpcodeOffset; - if (info.SegmentPrefix == 0x64 && opcode + 2 < next && (text[opcode + 1] & 0xc7) == 0x04 && text[opcode + 2] == 0x25) addRoot(header.MappedAddress + position); + if (absoluteFsAccess(text + position, info)) addRoot(header.MappedAddress + position); afterTransfer = info.FlowKind != Codegen::ControlFlowKind::Sequential && info.FlowKind != Codegen::ControlFlowKind::ConditionalBranch; position = next; } @@ -522,7 +546,16 @@ std::set CodeInstructionCollector::Collect(const std::ve } } report(10000); - if (entryRoots) *entryRoots = std::move(roots); + std::erase_if(dataTargets, [&](std::uint64_t address) { return roots.contains(address) || inTable(address); }); + if (!dataTargets.empty() && message) { + std::ostringstream text; + text << "Took " << std::dec << dataTargets.size() << " pointer target(s) outside the unwind functions as data, the first at 0x" << std::hex << *dataTargets.begin() << '\n'; + message(text.str()); + } + if (entryRoots) { + *entryRoots = std::move(roots); + entryRoots->insert(dataTargets.begin(), dataTargets.end()); + } return instructions; } diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 467f57ab1e..c08daff54f 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -378,7 +378,8 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path runtime.Names = {Io::Utf8Path(image.SourcePath.filename()), image.Soname}; runtime.DeferInitialization = deferred(index); std::vector output; - if (windows) output = Elfpatcher::GuestModuleWriter().WriteWindows(image, runtime); + const auto message = [&](std::string_view text) { progress.Message(text); }; + if (windows) output = Elfpatcher::GuestModuleWriter().WriteWindows(image, runtime, message); else { std::string guestRunPath = runPath; const auto relativeRoot = outputDirectory.lexically_relative(destination.lexically_normal()).generic_string(); @@ -394,7 +395,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path needed.push_back("$ORIGIN/" + dependencyPath.lexically_relative(image.SourcePath.parent_path()).generic_string()); } needed.insert(needed.end(), moduleHostLibraries[index].begin(), moduleHostLibraries[index].end()); - output = macos ? Elfpatcher::GuestModuleWriter().WriteMacOs(image, needed, guestRunPath) : Elfpatcher::GuestModuleWriter().WriteLinux(image, needed, guestRunPath); + output = macos ? Elfpatcher::GuestModuleWriter().WriteMacOs(image, needed, guestRunPath, message) : Elfpatcher::GuestModuleWriter().WriteLinux(image, needed, guestRunPath); } if (windows) { for (const auto& [name, provider] : guestNames) { diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index 9b283de919..3525a6b005 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -127,9 +127,9 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti std::shared_ptr patcher; if (macos) { - patcher = std::make_shared(options.MacosFixedBase); + patcher = std::make_shared(options.MacosFixedBase, [&](std::string_view text) { inputProgress.Message(text); }); } else if (windows) { - patcher = std::make_shared(options.WindowsGui, std::move(input.Icon), input.InputPath.parent_path() / "sce_sys" / "icon0.png", [&](auto completed, auto total) { inputProgress.Report({Domain::ProgressStage::Image, {}, completed, total}); }, options.WindowsFixedBase, options.TraceImports); + patcher = std::make_shared(options.WindowsGui, std::move(input.Icon), input.InputPath.parent_path() / "sce_sys" / "icon0.png", [&](auto completed, auto total) { inputProgress.Report({Domain::ProgressStage::Image, {}, completed, total}); }, options.WindowsFixedBase, options.TraceImports, [&](std::string_view text) { inputProgress.Message(text); }); } else { patcher = std::make_shared( std::make_shared(), diff --git a/core/relinker/relinker/tests/test_sce_code_extent.py b/core/relinker/relinker/tests/test_sce_code_extent.py index 5eff302160..1f58fb585e 100644 --- a/core/relinker/relinker/tests/test_sce_code_extent.py +++ b/core/relinker/relinker/tests/test_sce_code_extent.py @@ -8,6 +8,7 @@ import tempfile PT_LOAD = 1 PT_DYNAMIC = 2 PT_INTERP = 3 +PT_TLS = 7 PT_GNU_EH_FRAME = 0x6474E550 PT_SCE_DYNLIBDATA = 0x61000000 PT_SCE_VERSION = 0x6FFFFF01 @@ -23,10 +24,13 @@ INTERPRETER = b"/libexec/ld-elf.so.1\0" OVERLAPPING = bytes.fromhex("b8 64 c3 90 90 72 fa c3") UNSUPPORTED_TLS = bytes.fromhex("64 2d 65 6c 66 2e c3") INVALID = bytes.fromhex("41 41 41 41 41 41 66 66 66 66 2e 0f 1f 84 00 00 00 00 00") +TLS_LOAD = bytes.fromhex("64 48 8b 04 25 00 00 00 00 c3") +FS_SUBTRACT = bytes.fromhex("64 2d 65 6c 66 2e") def fixture(sce_block=True, plt_push_offset=8, gap=0, interpreter_pointer=False, data_pointer=False, gap_entry=False, - plt_at=None, early_function=False, function_tls=False, pointer_target=None, gap_bytes=None, functions=None): + plt_at=None, early_function=False, function_tls=False, pointer_target=None, gap_bytes=None, functions=None, + tls=False): plt = FUNCTION + FUNCTION_SIZE + gap if plt_at is None else plt_at entry = FUNCTION + FUNCTION_SIZE if gap_entry else FUNCTION data = plt + 0x20 if plt > FUNCTION else FUNCTION + FUNCTION_SIZE @@ -36,8 +40,8 @@ def fixture(sce_block=True, plt_push_offset=8, gap=0, interpreter_pointer=False, interpreter = FUNCTION - len(INTERPRETER) image = bytearray(0x1000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) - struct.pack_into(" Date: Wed, 21 Oct 2026 01:48:48 +1300 Subject: [PATCH 143/219] feat(libkernel): export htonl, ntohl, htons and ntohs libkernel exports the byte order conversions; they swap the bytes of their 32- or 16-bit argument. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- .../libkernel/Socket/src/AddressConversion.cpp | 16 ++++++++++++++++ core/libs/tests/GuestAddressConversion.cpp | 10 ++++++++++ 2 files changed, 26 insertions(+) diff --git a/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp b/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp index 54a7114657..bbcb17e637 100644 --- a/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp +++ b/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp @@ -219,4 +219,20 @@ const char* APS5_VABI __inet_ntop_nid_postfix(int family, const void* source, if (family == kGuestInet6) std::memset(destination + length + 1, 0, capacity - length - 1); return destination; } + +std::uint32_t APS5_VABI htonl_nid_postfix(std::uint32_t value) { + return __builtin_bswap32(value); +} + +std::uint32_t APS5_VABI ntohl_nid_postfix(std::uint32_t value) { + return __builtin_bswap32(value); +} + +std::uint16_t APS5_VABI htons_nid_postfix(std::uint16_t value) { + return __builtin_bswap16(value); +} + +std::uint16_t APS5_VABI ntohs_nid_postfix(std::uint16_t value) { + return __builtin_bswap16(value); +} } diff --git a/core/libs/tests/GuestAddressConversion.cpp b/core/libs/tests/GuestAddressConversion.cpp index 2deab22264..4d44b0a41b 100644 --- a/core/libs/tests/GuestAddressConversion.cpp +++ b/core/libs/tests/GuestAddressConversion.cpp @@ -11,6 +11,10 @@ const char* APS5_VABI __inet_ntop_nid_postfix(int, const void*, char*, std::uint int APS5_VABI inet_pton_nid_postfix(int, const char*, void*); const char* APS5_VABI inet_ntop_nid_postfix(int, const void*, char*, std::uint32_t); int* APS5_VABI __error_nid_postfix(); +std::uint32_t APS5_VABI htonl_nid_postfix(std::uint32_t); +std::uint32_t APS5_VABI ntohl_nid_postfix(std::uint32_t); +std::uint16_t APS5_VABI htons_nid_postfix(std::uint16_t); +std::uint16_t APS5_VABI ntohs_nid_postfix(std::uint16_t); } static void Require(bool value) { if (!value) std::abort(); } @@ -566,6 +570,12 @@ static void CheckErrnoKept() { } int main() { + Require(htonl_nid_postfix(0x11223344u) == 0x44332211u && ntohl_nid_postfix(0x44332211u) == 0x11223344u); + Require(htons_nid_postfix(0x1234) == 0x3412 && ntohs_nid_postfix(0x3412) == 0x1234); + const std::uint32_t loopback = htonl_nid_postfix(0x7f000001u); + unsigned char loopbackBytes[4]; + std::memcpy(loopbackBytes, &loopback, sizeof(loopback)); + Require(loopbackBytes[0] == 127 && loopbackBytes[3] == 1); for (int family : {2, 28}) { const char* input = family == 2 ? "192.0.2.17" : "2001:db8::17"; std::array address{}; From 5bfb6c7e63732fdf44e4f87eff8217ce8576779e Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 01:51:56 +1300 Subject: [PATCH 144/219] feat(libkernel): implement sceKernelCreateUuidV4 Fill 16 random bytes and set the version 4 and RFC 4122 variant bits in bytes 6 and 8, as libkernel does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libkernel/System/src/Process.cpp | 10 ++++++++++ core/libs/tests/GuestAppInfo.cpp | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index d9edcad564..348464a177 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -453,6 +453,16 @@ int APS5_VABI sceKernelUuidCreate(std::uint32_t* uuid) { return 0; } +int APS5_VABI sceKernelCreateUuidV4(std::uint8_t* uuid) { + if (!uuid) return sceInvalidArgument; + std::array value; + HostEntropy_nid_no_patch(value.data(), value.size()); + value[6] = static_cast((value[6] & 0x0fu) | 0x40u); + value[8] = static_cast((value[8] & 0x3fu) | 0x80u); + std::memcpy(uuid, value.data(), value.size()); + return 0; +} + void APS5_VABI sceKernelSync(void) { SyncWrittenPaths_nid_no_patch(); } diff --git a/core/libs/tests/GuestAppInfo.cpp b/core/libs/tests/GuestAppInfo.cpp index 5421462aea..860ff13718 100644 --- a/core/libs/tests/GuestAppInfo.cpp +++ b/core/libs/tests/GuestAppInfo.cpp @@ -12,6 +12,7 @@ extern "C" { int APS5_VABI sceKernelGetAppInfo(int pid, void* info); int APS5_VABI getpid_nid_postfix(void); int APS5_VABI sceKernelUuidCreate(std::uint32_t* uuid); +int APS5_VABI sceKernelCreateUuidV4(std::uint8_t* uuid); } static constexpr int SCE_KERNEL_ERROR_EPERM = static_cast(0x80020001); @@ -109,4 +110,11 @@ int main(int argc, char** argv) { Require(sceKernelUuidCreate(first) == 0 && sceKernelUuidCreate(second) == 0); for (const std::uint32_t* uuid : {first, second}) Require((uuid[1] >> 28) == 4u && (uuid[2] & 0xc0u) == 0x80u); Require(std::memcmp(first, second, sizeof(first)) != 0); + + Require(sceKernelCreateUuidV4(nullptr) == SCE_KERNEL_ERROR_EINVAL); + std::uint8_t third[16]{}; + std::uint8_t fourth[16]{}; + Require(sceKernelCreateUuidV4(third) == 0 && sceKernelCreateUuidV4(fourth) == 0); + for (const std::uint8_t* uuid : {third, fourth}) Require((uuid[6] >> 4) == 4u && (uuid[8] & 0xc0u) == 0x80u); + Require(std::memcmp(third, fourth, sizeof(third)) != 0); } From 02998c3bd0b1de071a057b6f01abce94d37d9885 Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Tue, 20 Oct 2026 10:32:08 -0300 Subject: [PATCH 145/219] fix(libSceAudio3d): leave the functions #6148 implements --- core/libs/prx/libSceAudio3d/Export.cpp | 30 -------------------------- docs/dev/debt/libSceAudio3d.md | 6 ------ 2 files changed, 36 deletions(-) diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index 29dc455b14..00b87eeb1c 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -249,36 +249,6 @@ int APS5_VABI sceAudio3dPortFlush(uint32_t port_id) { return 0; } -int APS5_VABI sceAudio3dAudioOutOpen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudio3dAudioOutClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudio3dAudioOutOutput(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudio3dAudioOutOutputs(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudio3dBedWrite(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudio3dBedWrite2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudio3dStrError(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceAudio3d.md b/docs/dev/debt/libSceAudio3d.md index 6b382184b7..b7b53f0aaf 100644 --- a/docs/dev/debt/libSceAudio3d.md +++ b/docs/dev/debt/libSceAudio3d.md @@ -9,12 +9,6 @@ - [sceAudio3dPortOpen](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too - [sceAudio3dPortGetAttributesSupported](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - the supported list (late reverb level, downmix spread radius, downmix spread height aware) is inferred from the attributes `sceAudio3dPortSetAttribute` accepts; its order and the count written when the caller capacity is smaller are unverified. [shadPS4](https://github.com/shadps4-emu/shadPS4) returns object attributes (PCM, gain...) instead, prosper and KytyPS5 have no body - [sceAudio3dObjectReserve](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF -- [sceAudio3dAudioOutOpen](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature -- [sceAudio3dAudioOutClose](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature -- [sceAudio3dAudioOutOutput](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature -- [sceAudio3dAudioOutOutputs](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature -- [sceAudio3dBedWrite](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature -- [sceAudio3dBedWrite2](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature - [sceAudio3dStrError](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature - [sceAudio3dPortOpenEx](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature - [sceAudio3dPortCreate](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature From 12ec674d881aa01c4ca9cab281626cc973b7a420 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:01:14 +0200 Subject: [PATCH 146/219] fix(libkernel): let raise and kill return 0 for ignored signals A signal that is ignored is forgotten when it is sent: the call succeeds (sys/kern/kern_sig.c:2178-2187). That covers SIG_IGN and the signals whose default action is SA_IGNORE, SIGURG, SIGCONT, SIGCHLD, SIGIO, SIGWINCH and SIGINFO (kern_sig.c:205-240); SIGCONT stays pending while blocked. The guest failed with EINVAL for every such signal that has no host equivalent. Co-Authored-By: Claude Sonnet 5.5 --- .../libs/prx/libkernel/System/src/Signals.cpp | 8 +++++- core/libs/tests/GuestSignals.cpp | 26 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index c9211181d4..decae52f4a 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -38,6 +38,7 @@ constexpr int MaxSignal = 128; constexpr int GuestSigkill = 9; constexpr int GuestSigstop = 17; constexpr int GuestSigalrm = 14; +constexpr int GuestSigcont = 19; constexpr int SaRestart = 0x2; constexpr int SaSiginfo = 0x40; std::atomic handlers[MaxSignal + 1]{}; @@ -75,6 +76,9 @@ struct GuestTimespec { std::int64_t nanoseconds; }; static_assert(sizeof(GuestTimespec) == 16 && offsetof(GuestTimespec, nanoseconds) == 8); +bool IgnoredByDefault(int guest) { + return guest == 16 || guest == GuestSigcont || guest == 20 || guest == 23 || guest == 28 || guest == 29; +} std::uint32_t SignalBit(int guest) { return 1u << ((guest - 1) % 32); } std::uint32_t SignalWord(int guest) { return static_cast((guest - 1) / 32); } bool SignalInSet(const GuestSignalSet& set, int guest) { return (set.bits[SignalWord(guest)] & SignalBit(guest)) != 0; } @@ -499,10 +503,12 @@ int APS5_VABI raise_nid_postfix(int guest) { if (guest < 1 || guest > MaxSignal) { *__error_nid_postfix() = 22; return -1; } const GuestSigaction action = Disposition(guest); if (action.handler > 1 && !Delivered(action)) throw std::runtime_error("raise: SA_SIGINFO handlers are not delivered"); + if (action.handler == 1 || (action.handler == 0 && guest != GuestSigcont && IgnoredByDefault(guest))) return 0; if (SignalBlocked(guest)) { - if (action.handler != 1) MarkPending(guest); + MarkPending(guest); return 0; } + if (action.handler == 0 && IgnoredByDefault(guest)) return 0; const int native = NativeSignal(guest); if (native != 0) { const int result = std::raise(native); diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index 016c4c5a0f..f07f586000 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -14,6 +14,8 @@ struct GuestSignalSet { extern "C" { Handler APS5_VABI signal_nid_postfix(int, Handler); int APS5_VABI raise_nid_postfix(int); +int APS5_VABI kill_nid_postfix(int, int); +int APS5_VABI getpid_nid_postfix(); int APS5_VABI pthread_kill_nid_postfix(void*, int); void* APS5_VABI scePthreadSelf(); int APS5_VABI scePthreadCreate(void** thread, const void* attr, void* (APS5_VABI *entry)(void*), void* arg, const char* name); @@ -310,4 +312,28 @@ int main() { pendingSignals.bits[0] == 0 && pendingSignals.bits[1] == 0 && pendingSignals.bits[2] == 0 && pendingSignals.bits[3] == 0); for (int guest : {40, 73, 128}) Require(signal_nid_postfix(guest, nullptr) == Callback); + for (const int ignoredByDefault : {16, 19, 20, 23, 28, 29}) { + *__error_nid_postfix() = 0; + received = 0; + Require(raise_nid_postfix(ignoredByDefault) == 0 && received == 0 && *__error_nid_postfix() == 0); + Require(kill_nid_postfix(getpid_nid_postfix(), ignoredByDefault) == 0 && *__error_nid_postfix() == 0); + } + const GuestSignalSet ignoredByDefaultSet{{(1u << 15) | (1u << 19) | (1u << 22) | (1u << 27) | (1u << 28), 0, 0, 0}}; + GuestSignalSet pendingIgnored{}; + Require(sigprocmask_nid_postfix(3, &ignoredByDefaultSet, nullptr) == 0); + for (const int ignoredByDefault : {16, 20, 23, 28, 29}) Require(raise_nid_postfix(ignoredByDefault) == 0); + Require(sigpending_nid_postfix(&pendingIgnored) == 0 && pendingIgnored.bits[0] == 0); + const GuestSignalSet noneBlocked{{0, 0, 0, 0}}; + Require(sigprocmask_nid_postfix(3, &noneBlocked, nullptr) == 0); + const GuestSigaction ignored{1, 0, {}}; + const GuestSigaction restored{0, 0, {}}; + const GuestSignalSet userSignal{{1u << 29, 0, 0, 0}}; + Require(sigaction_nid_postfix(30, &ignored, nullptr) == 0); + *__error_nid_postfix() = 0; + Require(raise_nid_postfix(30) == 0 && kill_nid_postfix(getpid_nid_postfix(), 30) == 0 && *__error_nid_postfix() == 0); + Require(sigprocmask_nid_postfix(3, &userSignal, nullptr) == 0); + Require(raise_nid_postfix(30) == 0); + Require(sigpending_nid_postfix(&pendingIgnored) == 0 && pendingIgnored.bits[0] == 0); + Require(sigprocmask_nid_postfix(3, &noneBlocked, nullptr) == 0); + Require(sigaction_nid_postfix(30, &restored, nullptr) == 0); } From 0cf58b569db7a9a7b3da78743a5238563fcbb1fa Mon Sep 17 00:00:00 2001 From: T3cm0 Date: Mon, 19 Oct 2026 02:57:33 -0500 Subject: [PATCH 147/219] feat(libSceHmd2): answer 125 exports before initialization as the console does A sweep of the 281 libSceHmd2 exports on a PS5 (firmware 13.60), each called from a payload before sceHmd2Initialize once with every argument null and once with every argument pointing at a zeroed 16 KiB buffer, found 127 that answer the same code both ways and write nothing. 125 of them are bound here, by NID, to one handler per answer: 45 return 0, 16 return 0x8a720008, 15 return 0x8a720004, 12 return -1, 10 reprojection exports return 0x8a72000b, 9 return 0x8a720002, 9 return 0x8a720009, 5 return 0x8a721002 and one each returns 0x8a72002c, 0x8a721003, 0x400 and 0x20000. Each handler returns its measured value while the library is not initialized and throws if sceHmd2Initialize ever succeeds, because the initialized behaviour is not measured; since b378541e79 it fails with UnsupportedFeature here, so these are the answers titles get. Left out: the export that returns a pointer-like value, the one answering 0x8a72000a (which sceHmd2Initialize also returns for some applications), the exports that answer differently to null and to zeroed arguments or write output, the 16 that crashed the probe, and the exports that main, its aerolib name where one exists. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libSceHmd2/Export.cpp | 252 ++++++++++++++++++++-------- core/libs/prx/libSceHmd2/Hmd2.hpp | 5 + core/libs/tests/GuestHmd2.cpp | 53 ++++-- docs/dev/debt/libSceHmd2.md | 15 +- 4 files changed, 235 insertions(+), 90 deletions(-) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index ab5733971f..11a9cb034b 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -1,4 +1,5 @@ #include "Hmd2.hpp" +#include "prx/libc/include/general/ExportMacros.hpp" #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceHmd/Hmd.hpp" @@ -18,6 +19,12 @@ std::mutex stateMutex; bool initialized = false; std::int32_t openHandle = 0; +std::int32_t BeforeInitialize(std::int32_t answer, const char* name) { + std::lock_guard lock(stateMutex); + if (initialized) NotImplemented_nid_no_patch(name); + return answer; +} + std::uint32_t CompiledSdkVersion() { static const auto* processParameters = static_cast(ApplicationProcessParameters_nid_no_patch()); std::uint32_t version; @@ -217,46 +224,11 @@ std::int32_t APS5_VABI sceHmd2SetVibration(std::int32_t handle, std::uint32_t le return 0; } -int APS5_VABI sceHmd2GazeGetCalibrationData() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2GazeGetCalibrationDataMaxSize() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2ReprojectionClearUserEventEnd() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionClearUserEventStart() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionGetCurrentOutput() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionSetUserEventEnd() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionSetUserEventStart() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeActivateGazeTracking() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2GazeGetDeviceStatus() { NotImplemented_nid_no_patch(__func__); return 0; @@ -272,31 +244,6 @@ int APS5_VABI sceHmd2GazeInsertRawDataWithHandle() { return 0; } -int APS5_VABI sceHmd2GazeSetEnabledEye() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeSetUserCalibrationData() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeSetUserCalibrationPoint() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeStartUserCalibration() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeStopUserCalibration() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2ReprojectionSeeThroughDraw() { NotImplemented_nid_no_patch(__func__); return 0; @@ -327,21 +274,11 @@ int APS5_VABI sceHmd2GazeObjectSelectionGetSelectedObject() { return 0; } -int APS5_VABI sceHmd2GazeObjectSelectionInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2GazeObjectSelectionQueryMemory() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2GazeObjectSelectionTerminate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2ReprojectionClear() { NotImplemented_nid_no_patch(__func__); return 0; @@ -372,10 +309,179 @@ int APS5_VABI sceHmd2SetFieldOfView() { return 0; } -int APS5_VABI sceHmd2SetIpdOffset() { - NotImplemented_nid_no_patch(__func__); - return 0; + +std::int32_t APS5_VABI Hmd2BeforeInitializeZero() { + return BeforeInitialize(0, __func__); } +APS5_EXPORT("vDgpdS2N9Rw", Hmd2BeforeInitializeZero); +APS5_EXPORT("Ri35gKTZpvE", Hmd2BeforeInitializeZero); +APS5_EXPORT("9oyW8puiKeI", Hmd2BeforeInitializeZero); +APS5_EXPORT("1HZNE6jhBnQ", Hmd2BeforeInitializeZero); +APS5_EXPORT("cdwAKBZLXjs", Hmd2BeforeInitializeZero); +APS5_EXPORT("Yub1MF58jfY", Hmd2BeforeInitializeZero); +APS5_EXPORT("JYXAuQFnPyw", Hmd2BeforeInitializeZero); +APS5_EXPORT("VuPKqCu3wzw", Hmd2BeforeInitializeZero); +APS5_EXPORT("GSThagVyW5U", Hmd2BeforeInitializeZero); +APS5_EXPORT("J4qE5N937MY", Hmd2BeforeInitializeZero); +APS5_EXPORT("9KnNj-dyASg", Hmd2BeforeInitializeZero); +APS5_EXPORT("Qta2bkstbFk", Hmd2BeforeInitializeZero); +APS5_EXPORT("5ZcObnDRBXE", Hmd2BeforeInitializeZero); +APS5_EXPORT("AvPNn0qx4WQ", Hmd2BeforeInitializeZero); +APS5_EXPORT("jp0apqNGgi8", Hmd2BeforeInitializeZero); +APS5_EXPORT("-kZT7X7OMg0", Hmd2BeforeInitializeZero); +APS5_EXPORT("1ewLtng-SVk", Hmd2BeforeInitializeZero); +APS5_EXPORT("B0b8DeIUGes", Hmd2BeforeInitializeZero); +APS5_EXPORT("HclLg4Oj5yw", Hmd2BeforeInitializeZero); +APS5_EXPORT("VbEvrQXsZyY", Hmd2BeforeInitializeZero); +APS5_EXPORT("bB6MKZi5UCQ", Hmd2BeforeInitializeZero); +APS5_EXPORT("OXQ1EDf02XU", Hmd2BeforeInitializeZero); +APS5_EXPORT("IAxfMTvFm0M", Hmd2BeforeInitializeZero); +APS5_EXPORT("qTJtAtaLeno", Hmd2BeforeInitializeZero); +APS5_EXPORT("j7Q16UX6l34", Hmd2BeforeInitializeZero); +APS5_EXPORT("pmmNM06rLqU", Hmd2BeforeInitializeZero); +APS5_EXPORT("zdQfyDK9Ods", Hmd2BeforeInitializeZero); +APS5_EXPORT("aPRwp9GnljM", Hmd2BeforeInitializeZero); +APS5_EXPORT("MjY1gBSaed0", Hmd2BeforeInitializeZero); +APS5_EXPORT("evAMjJYHqW4", Hmd2BeforeInitializeZero); +APS5_EXPORT("r93FLEZ3BBk", Hmd2BeforeInitializeZero); +APS5_EXPORT("IdYURdEdNFo", Hmd2BeforeInitializeZero); +APS5_EXPORT("CVcL2Cu2AEI", Hmd2BeforeInitializeZero); +APS5_EXPORT("NmKIfVxr2s0", Hmd2BeforeInitializeZero); +APS5_EXPORT("KeDLkkp62Gg", Hmd2BeforeInitializeZero); +APS5_EXPORT("+aadu6-ra+A", Hmd2BeforeInitializeZero); +APS5_EXPORT("FgnTiwwGSzk", Hmd2BeforeInitializeZero); +APS5_EXPORT("PWAUwOTHbbo", Hmd2BeforeInitializeZero); +APS5_EXPORT("WzaEApHnsP8", Hmd2BeforeInitializeZero); +APS5_EXPORT("dXzQI8QVY7Y", Hmd2BeforeInitializeZero); +APS5_EXPORT("cCvFKGqRh1k", Hmd2BeforeInitializeZero); +APS5_EXPORT("uj4-2VEAFPU", Hmd2BeforeInitializeZero); +APS5_EXPORT("sIaGStI-iLM", Hmd2BeforeInitializeZero); +APS5_EXPORT("JzwpZOemDO8", Hmd2BeforeInitializeZero); +APS5_EXPORT("IsC4R4bZFVU", Hmd2BeforeInitializeZero); + +std::int32_t APS5_VABI Hmd2BeforeInitializeNotInitialized() { + return BeforeInitialize(Hmd2::NotInitialized, __func__); +} +APS5_EXPORT("sXaPEPY-fLA", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("3F5inHhKzXg", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("sJAiB-6p55U", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("E8hDydLjsSs", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("xEQkSb2WJ7Y", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("aaP4dnR4q-0", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("lEU0TeUoGwI", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("cFVyjHBpUUU", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("zROtb46jTj4", Hmd2BeforeInitializeNotInitialized); + +std::int32_t APS5_VABI Hmd2BeforeInitializeError0004() { + return BeforeInitialize(Hmd2::Error0004, __func__); +} +APS5_EXPORT("kAMsjYHBbNQ", Hmd2BeforeInitializeError0004); +APS5_EXPORT("5m73zmLpsQ0", Hmd2BeforeInitializeError0004); +APS5_EXPORT("EPeH4xuQ2XQ", Hmd2BeforeInitializeError0004); +APS5_EXPORT("TJ8lyRsjFY4", Hmd2BeforeInitializeError0004); +APS5_EXPORT("-XQVcLnyX68", Hmd2BeforeInitializeError0004); +APS5_EXPORT("giQwQoHhYYA", Hmd2BeforeInitializeError0004); +APS5_EXPORT("zNc9IkgawMI", Hmd2BeforeInitializeError0004); +APS5_EXPORT("-tnG5UJPvAc", Hmd2BeforeInitializeError0004); +APS5_EXPORT("FPhFmG+jXuQ", Hmd2BeforeInitializeError0004); +APS5_EXPORT("CKlkvNngofQ", Hmd2BeforeInitializeError0004); +APS5_EXPORT("yL1CZaciFWE", Hmd2BeforeInitializeError0004); +APS5_EXPORT("qlzgyG3s0RY", Hmd2BeforeInitializeError0004); +APS5_EXPORT("kTIDrDLzMv0", Hmd2BeforeInitializeError0004); +APS5_EXPORT("9Gee0ef8a+U", Hmd2BeforeInitializeError0004); +APS5_EXPORT("ecZVjQdwmBI", Hmd2BeforeInitializeError0004); + +std::int32_t APS5_VABI Hmd2BeforeInitializeParameterNull() { + return BeforeInitialize(Hmd2::ParameterNull, __func__); +} +APS5_EXPORT("MNtiJTDOPAU", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("RRy9WmqZfgU", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("uobR4m9lgwc", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("-XPk5KWBYdo", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("J44idh9izZM", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("Ps0wAvYaa1E", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("kLnMR8oQ1MY", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("kMz4rbBxvjo", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("DIU1sFRRaiE", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("ZQvU4vDzZGQ", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("TJpyGGP9YEY", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("Dih+Odk+RC0", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("oaqdSJw+HVY", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("m35gtHuanXU", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("CTM7QPLJF9Q", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("+rExHeC9lP0", Hmd2BeforeInitializeParameterNull); + +std::int32_t APS5_VABI Hmd2BeforeInitializeParameterInvalid() { + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} +APS5_EXPORT("bBh5bCp9coo", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("G3PNDE2VjfY", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("nScCT0OPU0o", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("6Q0Ng7OTN9o", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("-fJKh4SnFR0", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("jUA2s-l0npY", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("+9XhyAIJVj4", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("eBGK-o6E4RQ", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("MTuFeL9184E", Hmd2BeforeInitializeParameterInvalid); + +std::int32_t APS5_VABI Hmd2BeforeInitializeReprojectionNotInitialized() { + return BeforeInitialize(Hmd2::ReprojectionNotInitialized, __func__); +} +APS5_EXPORT("d3SEIYTP6wc", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("EVECbTvBezo", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("ciusPdJKjSU", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("DIwqTkcZiJo", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("64ERZWVFBTE", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("z0+0gXUJWs8", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("rjAd4nCY4DM", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("Acyg4vTdGm0", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("aGOWaEeg04I", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("9up+EPhw1No", Hmd2BeforeInitializeReprojectionNotInitialized); + +std::int32_t APS5_VABI Hmd2BeforeInitializeError002C() { + return BeforeInitialize(Hmd2::Error002C, __func__); +} +APS5_EXPORT("V1kLQmP-17U", Hmd2BeforeInitializeError002C); + +std::int32_t APS5_VABI Hmd2BeforeInitializeError1002() { + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("2LVD1EC5SYE", Hmd2BeforeInitializeError1002); +APS5_EXPORT("Bf87AL6zWxU", Hmd2BeforeInitializeError1002); +APS5_EXPORT("XKt7w1MODFY", Hmd2BeforeInitializeError1002); +APS5_EXPORT("Y5s22M-qUDA", Hmd2BeforeInitializeError1002); +APS5_EXPORT("JJKY-BtQJLg", Hmd2BeforeInitializeError1002); + +std::int32_t APS5_VABI Hmd2BeforeInitializeError1003() { + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("9kj9vw8UShQ", Hmd2BeforeInitializeError1003); + +std::int32_t APS5_VABI Hmd2BeforeInitializeMinusOne() { + return BeforeInitialize(-1, __func__); +} +APS5_EXPORT("EAkv8Mqt33E", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("Rv7bkmKvmz0", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("1oPZizn0SNA", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("TwCpE-nNrlo", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("jkqAeQfdtxw", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("+l0RQph81Z8", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("qsmAtGw1Ncc", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("uKhwYoMsIsA", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("WNdaigr94dI", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("P6S5ZVWf-H0", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("sun6NWE1sdg", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("--TFsJYXo04", Hmd2BeforeInitializeMinusOne); + +std::int32_t APS5_VABI Hmd2BeforeInitializeValue0400() { + return BeforeInitialize(0x400, __func__); +} +APS5_EXPORT("BWqcwjCozzI", Hmd2BeforeInitializeValue0400); + +std::int32_t APS5_VABI Hmd2BeforeInitializeValue20000() { + return BeforeInitialize(0x20000, __func__); +} +APS5_EXPORT("PRz7TdBGUI0", Hmd2BeforeInitializeValue20000); int APS5_VABI sceHmd2InternalSetPortStatus(void) { NotImplemented_nid_no_patch(__func__); diff --git a/core/libs/prx/libSceHmd2/Hmd2.hpp b/core/libs/prx/libSceHmd2/Hmd2.hpp index 5e978e686f..a3dbc23907 100644 --- a/core/libs/prx/libSceHmd2/Hmd2.hpp +++ b/core/libs/prx/libSceHmd2/Hmd2.hpp @@ -14,6 +14,11 @@ inline constexpr std::int32_t InvalidHandle = static_cast(0x8a7200 inline constexpr std::int32_t AlreadyOpened = static_cast(0x8a720006); inline constexpr std::int32_t ParameterNull = static_cast(0x8a720008); inline constexpr std::int32_t ParameterInvalid = static_cast(0x8a720009); +inline constexpr std::int32_t Error0004 = static_cast(0x8a720004); +inline constexpr std::int32_t ReprojectionNotInitialized = static_cast(0x8a72000b); +inline constexpr std::int32_t Error002C = static_cast(0x8a72002c); +inline constexpr std::int32_t Error1002 = static_cast(0x8a721002); +inline constexpr std::int32_t Error1003 = static_cast(0x8a721003); inline constexpr std::uint32_t UserIdSdkVersion = 0x07000000; diff --git a/core/libs/tests/GuestHmd2.cpp b/core/libs/tests/GuestHmd2.cpp index f9781e407b..1014602014 100644 --- a/core/libs/tests/GuestHmd2.cpp +++ b/core/libs/tests/GuestHmd2.cpp @@ -11,10 +11,21 @@ #include extern "C" { -int APS5_VABI sceHmd2GazeActivateGazeTracking(); -int APS5_VABI sceHmd2ReprojectionGetCurrentOutput(); int APS5_VABI sceHmd2SetFieldOfView(); +std::int32_t APS5_VABI Hmd2BeforeInitializeZero(); +std::int32_t APS5_VABI Hmd2BeforeInitializeNotInitialized(); +std::int32_t APS5_VABI Hmd2BeforeInitializeError0004(); +std::int32_t APS5_VABI Hmd2BeforeInitializeParameterNull(); +std::int32_t APS5_VABI Hmd2BeforeInitializeParameterInvalid(); +std::int32_t APS5_VABI Hmd2BeforeInitializeReprojectionNotInitialized(); +std::int32_t APS5_VABI Hmd2BeforeInitializeError002C(); +std::int32_t APS5_VABI Hmd2BeforeInitializeError1002(); +std::int32_t APS5_VABI Hmd2BeforeInitializeError1003(); +std::int32_t APS5_VABI Hmd2BeforeInitializeMinusOne(); +std::int32_t APS5_VABI Hmd2BeforeInitializeValue0400(); +std::int32_t APS5_VABI Hmd2BeforeInitializeValue20000(); } + namespace { alignas(16) std::array process{}; @@ -48,6 +59,18 @@ void TestBeforeInitialize() { Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr) == Hmd2::NotInitialized, "open before initialization"); Require(sceHmd2Close(0) == Hmd2::NotInitialized, "close before initialization"); Require(sceHmd2Terminate() == Hmd2::NotInitialized, "terminate before initialization"); + Require(Hmd2BeforeInitializeZero() == 0, "Hmd2BeforeInitializeZero before initialization"); + Require(Hmd2BeforeInitializeNotInitialized() == Hmd2::NotInitialized, "Hmd2BeforeInitializeNotInitialized before initialization"); + Require(Hmd2BeforeInitializeError0004() == Hmd2::Error0004, "Hmd2BeforeInitializeError0004 before initialization"); + Require(Hmd2BeforeInitializeParameterNull() == Hmd2::ParameterNull, "Hmd2BeforeInitializeParameterNull before initialization"); + Require(Hmd2BeforeInitializeParameterInvalid() == Hmd2::ParameterInvalid, "Hmd2BeforeInitializeParameterInvalid before initialization"); + Require(Hmd2BeforeInitializeReprojectionNotInitialized() == Hmd2::ReprojectionNotInitialized, "Hmd2BeforeInitializeReprojectionNotInitialized before initialization"); + Require(Hmd2BeforeInitializeError002C() == Hmd2::Error002C, "Hmd2BeforeInitializeError002C before initialization"); + Require(Hmd2BeforeInitializeError1002() == Hmd2::Error1002, "Hmd2BeforeInitializeError1002 before initialization"); + Require(Hmd2BeforeInitializeError1003() == Hmd2::Error1003, "Hmd2BeforeInitializeError1003 before initialization"); + Require(Hmd2BeforeInitializeMinusOne() == -1, "Hmd2BeforeInitializeMinusOne before initialization"); + Require(Hmd2BeforeInitializeValue0400() == 0x400, "Hmd2BeforeInitializeValue0400 before initialization"); + Require(Hmd2BeforeInitializeValue20000() == 0x20000, "Hmd2BeforeInitializeValue20000 before initialization"); Require(sceHmd2SetVibration(0, 0) == 0, "vibration of the unopened handle 0 failed"); } @@ -59,6 +82,18 @@ void TestWithoutHeadset() { Hmd2::DeviceInformation info; Require(sceHmd2GetDeviceInformation(&info) == Hmd2::NotInitialized, "device information without a headset"); Require(sceHmd2Terminate() == Hmd2::NotInitialized, "terminate after the failed initialization"); + Require(Hmd2BeforeInitializeZero() == 0, "Hmd2BeforeInitializeZero after the failed initialization"); + Require(Hmd2BeforeInitializeNotInitialized() == Hmd2::NotInitialized, "Hmd2BeforeInitializeNotInitialized after the failed initialization"); + Require(Hmd2BeforeInitializeError0004() == Hmd2::Error0004, "Hmd2BeforeInitializeError0004 after the failed initialization"); + Require(Hmd2BeforeInitializeParameterNull() == Hmd2::ParameterNull, "Hmd2BeforeInitializeParameterNull after the failed initialization"); + Require(Hmd2BeforeInitializeParameterInvalid() == Hmd2::ParameterInvalid, "Hmd2BeforeInitializeParameterInvalid after the failed initialization"); + Require(Hmd2BeforeInitializeReprojectionNotInitialized() == Hmd2::ReprojectionNotInitialized, "Hmd2BeforeInitializeReprojectionNotInitialized after the failed initialization"); + Require(Hmd2BeforeInitializeError002C() == Hmd2::Error002C, "Hmd2BeforeInitializeError002C after the failed initialization"); + Require(Hmd2BeforeInitializeError1002() == Hmd2::Error1002, "Hmd2BeforeInitializeError1002 after the failed initialization"); + Require(Hmd2BeforeInitializeError1003() == Hmd2::Error1003, "Hmd2BeforeInitializeError1003 after the failed initialization"); + Require(Hmd2BeforeInitializeMinusOne() == -1, "Hmd2BeforeInitializeMinusOne after the failed initialization"); + Require(Hmd2BeforeInitializeValue0400() == 0x400, "Hmd2BeforeInitializeValue0400 after the failed initialization"); + Require(Hmd2BeforeInitializeValue20000() == 0x20000, "Hmd2BeforeInitializeValue20000 after the failed initialization"); } } @@ -80,20 +115,6 @@ int main() { TestBeforeInitialize(); TestWithoutHeadset(); bool threw = false; - try { - sceHmd2GazeActivateGazeTracking(); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw, "gaze activation did not throw"); - threw = false; - try { - sceHmd2ReprojectionGetCurrentOutput(); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw, "reprojection get current output did not throw"); - threw = false; try { sceHmd2SetFieldOfView(); } catch (const std::runtime_error&) { diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index 4ebd9bb64e..ef45d04f01 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -11,7 +11,7 @@ - [sceHmd2SetVibration](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the second parameter is checked against 25 only while a headset is connected; its name and unit are unknown. The field of view member names are assumed: of the firmware's default tangents the first is the widest and the last two are equal - PPSA01317 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) exports that throw and the remaining [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) exports. Names from the aerolib NID list - PPSA15878 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reprojection and gaze exports, `sceVrTracker2Finalize`, `sceVrTracker2GetPlayAreaOrientedBoundingBox` and `sceVrTracker2LocateCoordinateSystem` in [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp), and the `scePadVrController` trigger effect exports in [libScePad](../../../core/libs/prx/libScePad/Export.cpp). Names from the aerolib NID list -- [libSceHmd2 gaze and reprojection exports](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - `sceHmd2GazeGetCalibrationData`, `sceHmd2GazeGetCalibrationDataMaxSize`, `sceHmd2ReprojectionClearUserEventEnd`, `sceHmd2ReprojectionClearUserEventStart`, `sceHmd2ReprojectionGetCurrentOutput`, `sceHmd2ReprojectionSetUserEventEnd`, `sceHmd2ReprojectionSetUserEventStart`, `sceHmd2GazeActivateGazeTracking`, `sceHmd2GazeGetDeviceStatus`, `sceHmd2GazeGetUserCalibrationStatus`, `sceHmd2GazeInsertRawDataWithHandle`, `sceHmd2GazeSetEnabledEye`, `sceHmd2GazeSetUserCalibrationData`, `sceHmd2GazeSetUserCalibrationPoint`, `sceHmd2GazeStartUserCalibration`, `sceHmd2GazeStopUserCalibration`, `sceHmd2ReprojectionSeeThroughDraw`, `sceHmd2ReprojectionSeeThroughIC`, `sceHmd2ReprojectionVideoOutEq`, `sceHmd2ReprojectionWaitShader`, `sceHmd2SeeThroughCallbackGetPoseDataFunc`, `sceHmd2GazeObjectSelectionGetSelectedObject`, `sceHmd2GazeObjectSelectionInitialize`, `sceHmd2GazeObjectSelectionQueryMemory`, `sceHmd2GazeObjectSelectionTerminate`, `sceHmd2ReprojectionClear`, `sceHmd2ReprojectionSet`, `sceHmd2ReprojectionSetEventGpuEnd`, `sceHmd2ReprojectionSetEventGpuStart`, `sceHmd2RpeorjectionSetParam`, `sceHmd2SetFieldOfView`, `sceHmd2SetIpdOffset` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws +- [libSceHmd2 gaze and reprojection exports](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - `sceHmd2GazeGetCalibrationDataMaxSize`, `sceHmd2GazeGetDeviceStatus`, `sceHmd2GazeGetUserCalibrationStatus`, `sceHmd2GazeInsertRawDataWithHandle`, `sceHmd2ReprojectionSeeThroughDraw`, `sceHmd2ReprojectionSeeThroughIC`, `sceHmd2ReprojectionVideoOutEq`, `sceHmd2ReprojectionWaitShader`, `sceHmd2SeeThroughCallbackGetPoseDataFunc`, `sceHmd2GazeObjectSelectionGetSelectedObject`, `sceHmd2GazeObjectSelectionQueryMemory`, `sceHmd2ReprojectionClear`, `sceHmd2ReprojectionSet`, `sceHmd2ReprojectionSetEventGpuEnd`, `sceHmd2ReprojectionSetEventGpuStart`, `sceHmd2RpeorjectionSetParam`, `sceHmd2SetFieldOfView` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws - [sceHmd2InternalSetPortStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalLedDetectorStartBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalImageStartBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature @@ -36,3 +36,16 @@ - [sceHmd2Initialize](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the firmware also fails with 0x8a72001f when `sceKernelGetAppInfo` fails and with 0x8a72000a for some applications, depending on app info fields at 0x2c, 0x4b and 0x4d whose meaning is unknown, and then calls `sceMbusInit`; none of this is modelled - [sceHmd2ReprojectionQueryDisplayBufferSizeAlign, sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign, sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign and sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - each returns a `{uint64 size, uint64 alignment}` pair in RAX:RDX; the values were measured on a retail PS5 (firmware 13.60) with no PS VR2 headset from a homebrew payload that loaded `libSceHmd2.sprx` without initializing it, and stayed the same for every argument tried; `sceHmd2ReprojectionQueryBufferSizeAlign` measured there returns 0x100828 and 0x100 for every argument, including the 2, 0xAF and 1 that PPSA05666 passes. They are assumed not to depend on initialization or on a connected headset, and their parameters are unknown +- [Hmd2BeforeInitializeZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers 0 to these 45 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `vDgpdS2N9Rw` (sceHmd2InternalRegisterVibrationCallback), `Ri35gKTZpvE` (sceHmd2InternalUnregisterVibrationCallback), `9oyW8puiKeI`, `1HZNE6jhBnQ` (sceHmd2InternalPowerEventInitialize), `cdwAKBZLXjs`, `Yub1MF58jfY`, `JYXAuQFnPyw` (sceHmd2InternalDfuOpen), `VuPKqCu3wzw` (sceHmd2InternalDfuClose), `GSThagVyW5U` (sceHmd2InternalCrashReportOpen), `J4qE5N937MY` (sceHmd2InternalCrashReportClose), `9KnNj-dyASg`, `Qta2bkstbFk`, `5ZcObnDRBXE`, `AvPNn0qx4WQ`, `jp0apqNGgi8` (sceHmd2SetIpdOffset), `-kZT7X7OMg0` (sceHmd2InternalRelocalizerOpen), `1ewLtng-SVk` (sceHmd2InternalRelocalizerClose), `B0b8DeIUGes` (sceHmd2InternalTrackingOpen), `HclLg4Oj5yw` (sceHmd2InternalTrackingClose), `VbEvrQXsZyY` (sceHmd2InternalLedDetectorOpen), `bB6MKZi5UCQ` (sceHmd2InternalLedDetectorClose), `OXQ1EDf02XU` (sceHmd2InternalImageOpen), `IAxfMTvFm0M` (sceHmd2InternalImageClose), `qTJtAtaLeno` (sceHmd2InternalGenDataOpen), `j7Q16UX6l34` (sceHmd2InternalGenDataClose), `pmmNM06rLqU`, `zdQfyDK9Ods` (sceHmd2InternalSlamTraceOpen), `aPRwp9GnljM` (sceHmd2InternalSlamTraceClose), `MjY1gBSaed0` (sceHmd2InternalGazeOpen), `evAMjJYHqW4` (sceHmd2InternalGazeClose), `r93FLEZ3BBk` (sceHmd2InternalGazeStart), `IdYURdEdNFo` (sceHmd2InternalGazeStop), `CVcL2Cu2AEI` (sceHmd2GazeActivateGazeTracking), `NmKIfVxr2s0`, `KeDLkkp62Gg`, `+aadu6-ra+A`, `FgnTiwwGSzk`, `PWAUwOTHbbo` (sceHmd2InternalCreateSharedMemory), `WzaEApHnsP8` (sceHmd2InternalIsGameVr2d), `dXzQI8QVY7Y` (sceHmd2InternalIsMiniAppVr2d), `cCvFKGqRh1k` (sceHmd2InternalIsCommonDlgVr2d), `uj4-2VEAFPU` (sceHmd2InternalIsCommonDlgMiniAppVr2d), `sIaGStI-iLM` (sceHmd2InternalMapSharedMemory), `JzwpZOemDO8`, `IsC4R4bZFVU`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeNotInitialized](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720002` to these 9 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `sXaPEPY-fLA` (sceHmd2InternalOpen), `3F5inHhKzXg` (sceHmd2InternalGetDeviceInformation), `sJAiB-6p55U`, `E8hDydLjsSs`, `xEQkSb2WJ7Y`, `aaP4dnR4q-0`, `lEU0TeUoGwI`, `cFVyjHBpUUU`, `zROtb46jTj4`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeError0004](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720004` to these 15 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `kAMsjYHBbNQ` (sceHmd2InternalSetVolume), `5m73zmLpsQ0` (sceHmd2InternalResetViewPosition), `EPeH4xuQ2XQ` (sceHmd2InternalGetStatus), `TJ8lyRsjFY4` (sceHmd2InternalClearTrackerFts), `-XQVcLnyX68` (sceHmd2InternalSetSlamCommand), `giQwQoHhYYA` (sceHmd2InternalDfuStart), `zNc9IkgawMI`, `-tnG5UJPvAc` (sceHmd2GazeStartUserCalibration), `FPhFmG+jXuQ` (sceHmd2GazeStopUserCalibration), `CKlkvNngofQ`, `yL1CZaciFWE` (sceHmd2GazeSetUserCalibrationPoint), `qlzgyG3s0RY`, `kTIDrDLzMv0`, `9Gee0ef8a+U`, `ecZVjQdwmBI` (sceHmd2GazeSetEnabledEye). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeParameterNull](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` to these 16 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `MNtiJTDOPAU`, `RRy9WmqZfgU` (sceHmd2InternalDecryptData), `uobR4m9lgwc` (sceHmd2InternalGetMirroringMode), `-XPk5KWBYdo` (sceHmd2InternalRelocalizerSetResultData), `J44idh9izZM`, `Ps0wAvYaa1E`, `kLnMR8oQ1MY`, `kMz4rbBxvjo`, `DIU1sFRRaiE` (sceHmd2GazeGetCalibrationData), `ZQvU4vDzZGQ` (sceHmd2GazeSetUserCalibrationData), `TJpyGGP9YEY`, `Dih+Odk+RC0`, `oaqdSJw+HVY`, `m35gtHuanXU`, `CTM7QPLJF9Q` (sceHmd2InternalGetFlipInfo), `+rExHeC9lP0` (sceHmd2GazeObjectSelectionInitialize). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeParameterInvalid](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720009` to these 9 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `bBh5bCp9coo` (sceHmd2InternalSetPositionResetIcon), `G3PNDE2VjfY` (sceHmd2InternalSetPowerOff), `nScCT0OPU0o`, `6Q0Ng7OTN9o`, `-fJKh4SnFR0` (sceHmd2InternalSetBudgetMode), `jUA2s-l0npY` (sceHmd2InternalSetForceIcon), `+9XhyAIJVj4`, `eBGK-o6E4RQ` (sceHmd2InternalBindDeviceWithUserId), `MTuFeL9184E`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeReprojectionNotInitialized](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a72000b` to these 10 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `d3SEIYTP6wc`, `EVECbTvBezo`, `ciusPdJKjSU`, `DIwqTkcZiJo` (sceHmd2ReprojectionSetUserEventStart), `64ERZWVFBTE` (sceHmd2ReprojectionSetUserEventEnd), `z0+0gXUJWs8` (sceHmd2ReprojectionClearUserEventStart), `rjAd4nCY4DM` (sceHmd2ReprojectionClearUserEventEnd), `Acyg4vTdGm0` (sceHmd2ReprojectionGetCurrentOutput), `aGOWaEeg04I`, `9up+EPhw1No`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeError002C](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a72002c` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `V1kLQmP-17U` (sceHmd2GazeObjectSelectionTerminate). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a721002` to these 5 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `2LVD1EC5SYE`, `Bf87AL6zWxU`, `XKt7w1MODFY`, `Y5s22M-qUDA`, `JJKY-BtQJLg`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a721003` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `9kj9vw8UShQ`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeMinusOne](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers -1 to these 12 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `EAkv8Mqt33E` (sceHmd2InternalRelocalizerStart), `Rv7bkmKvmz0` (sceHmd2InternalRelocalizerStop), `1oPZizn0SNA` (sceHmd2InternalTrackingStart), `TwCpE-nNrlo` (sceHmd2InternalTrackingStop), `jkqAeQfdtxw` (sceHmd2InternalLedDetectorStart), `+l0RQph81Z8` (sceHmd2InternalLedDetectorStop), `qsmAtGw1Ncc` (sceHmd2InternalImageStart), `uKhwYoMsIsA` (sceHmd2InternalImageStop), `WNdaigr94dI` (sceHmd2InternalGenDataStart), `P6S5ZVWf-H0` (sceHmd2InternalGenDataStop), `sun6NWE1sdg` (sceHmd2InternalSlamTraceStart), `--TFsJYXo04` (sceHmd2InternalSlamTraceStop). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeValue0400](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x400` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `BWqcwjCozzI`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeValue20000](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x20000` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `PRz7TdBGUI0`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [libSceHmd2 pre-initialization sweep](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the names in the entries above are the aerolib names of the NIDs where one exists; the error names `Error0004`, `Error002C`, `Error1002` and `Error1003` are placeholders for codes whose meaning is unknown, and `ReprojectionNotInitialized` is ours because every export answering `0x8a72000b` is a reprojection one. Exports that answered differently to null and to zeroed arguments, wrote output, returned a pointer-like value, answered `0x8a72000a` (which `sceHmd2Initialize` also returns for some applications) or crashed the probe are left as they were From e29cefad8e06bd4c82a92c82279cf6ed6b147261 Mon Sep 17 00:00:00 2001 From: T3cm0 Date: Mon, 19 Oct 2026 03:15:14 -0500 Subject: [PATCH 148/219] feat(libSceHmd2): answer 62 pointer-checking exports before initialization as the console does The hmd2s sweep left 81 libSceHmd2 exports that answer 0x8a720008 to all-null arguments but something else to zeroed buffers. A second probe called each of them six times before sceHmd2Initialize, every argument pointing at a zeroed 16 KiB buffer except one, to find which arguments are the checked pointers. 62 exports answer 0x8a720008 exactly when one of a fixed set of arguments is null and their all-present code otherwise: 48 check the first argument, 10 the second, 6 the first two, 2 the first and third, 2 all six. They are bound here, by NID, to 18 handlers with that many pointer parameters, each returning ParameterNull for a null checked pointer and otherwise the measured code (0x8a720004 for 20 of them, 0 for 11, 0x8a721002 for 10, -1 for 5, 0x8a720009 for 7, 0x8a72002c for 3, and 0x8a720003, 0x8a721003, 0x8a721010 and 0x800d0203 for the rest) while the library is not initialized, throwing if sceHmd2Initialize ever succeeds. Left out: 7 exports that write output and 12 whose first argument is a handle or slot number rather than a pointer. The debt entries list every NID with its aerolib name where one exists. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libSceHmd2/Export.cpp | 203 +++++++++++++++++++++++----- core/libs/prx/libSceHmd2/Hmd2.hpp | 2 + core/libs/tests/GuestHmd2.cpp | 122 +++++++++++++++-- docs/dev/debt/libSceHmd2.md | 21 ++- 4 files changed, 304 insertions(+), 44 deletions(-) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index 11a9cb034b..a4b9ae1953 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -224,26 +224,6 @@ std::int32_t APS5_VABI sceHmd2SetVibration(std::int32_t handle, std::uint32_t le return 0; } -int APS5_VABI sceHmd2GazeGetCalibrationDataMaxSize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeGetDeviceStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeGetUserCalibrationStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeInsertRawDataWithHandle() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2ReprojectionSeeThroughDraw() { NotImplemented_nid_no_patch(__func__); return 0; @@ -269,16 +249,6 @@ int APS5_VABI sceHmd2SeeThroughCallbackGetPoseDataFunc() { return 0; } -int APS5_VABI sceHmd2GazeObjectSelectionGetSelectedObject() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeObjectSelectionQueryMemory() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2ReprojectionClear() { NotImplemented_nid_no_patch(__func__); return 0; @@ -304,11 +274,6 @@ int APS5_VABI sceHmd2RpeorjectionSetParam() { return 0; } -int APS5_VABI sceHmd2SetFieldOfView() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - std::int32_t APS5_VABI Hmd2BeforeInitializeZero() { return BeforeInitialize(0, __func__); @@ -578,4 +543,172 @@ int APS5_VABI sceHmd2InternalImageCloseBySlot(void) { return 0; } +std::int32_t APS5_VABI Hmd2CheckFirstThenError0004(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error0004, __func__); +} +APS5_EXPORT("9WeZr4kYfzk", Hmd2CheckFirstThenError0004); +APS5_EXPORT("N+5h1hCOFAg", Hmd2CheckFirstThenError0004); +APS5_EXPORT("w2vEs2y5ZmE", Hmd2CheckFirstThenError0004); +APS5_EXPORT("L5gat1pe0y0", Hmd2CheckFirstThenError0004); +APS5_EXPORT("4GnLHR9NJ1w", Hmd2CheckFirstThenError0004); +APS5_EXPORT("AHk7nGSzef0", Hmd2CheckFirstThenError0004); +APS5_EXPORT("ncjX9d3mc0M", Hmd2CheckFirstThenError0004); +APS5_EXPORT("l63QYAUuogc", Hmd2CheckFirstThenError0004); +APS5_EXPORT("mrcBySCdXSo", Hmd2CheckFirstThenError0004); +APS5_EXPORT("ieBra+td8sU", Hmd2CheckFirstThenError0004); +APS5_EXPORT("s+rzBnewgGs", Hmd2CheckFirstThenError0004); +APS5_EXPORT("Nfcm0GBdzZI", Hmd2CheckFirstThenError0004); +APS5_EXPORT("Tc9rasWfrm4", Hmd2CheckFirstThenError0004); +APS5_EXPORT("ckTbKTX4KZg", Hmd2CheckFirstThenError0004); +APS5_EXPORT("D7576PUbUjk", Hmd2CheckFirstThenError0004); +APS5_EXPORT("tTJ04SrBT8U", Hmd2CheckFirstThenError0004); +APS5_EXPORT("sDP7hZ-P6OI", Hmd2CheckFirstThenError0004); +APS5_EXPORT("J+eVNbN9ndY", Hmd2CheckFirstThenError0004); +APS5_EXPORT("kozvHBNyxf0", Hmd2CheckFirstThenError0004); +APS5_EXPORT("638Tmg5y6U4", Hmd2CheckFirstThenError0004); + +std::int32_t APS5_VABI Hmd2CheckFirstThenZero(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("8ETtFtp5zqY", Hmd2CheckFirstThenZero); +APS5_EXPORT("CWPkBm3EXQA", Hmd2CheckFirstThenZero); +APS5_EXPORT("R62liQQIAaA", Hmd2CheckFirstThenZero); +APS5_EXPORT("-bcWLHsdzo0", Hmd2CheckFirstThenZero); +APS5_EXPORT("JngJzgNbu8U", Hmd2CheckFirstThenZero); +APS5_EXPORT("xNBSz8xrGdk", Hmd2CheckFirstThenZero); +APS5_EXPORT("yUVPfN-jRIU", Hmd2CheckFirstThenZero); +APS5_EXPORT("BKPR3RZ5aO4", Hmd2CheckFirstThenZero); + +std::int32_t APS5_VABI Hmd2CheckFirstThenError1002(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("IG4y+drWAWw", Hmd2CheckFirstThenError1002); +APS5_EXPORT("rhYUml9vppE", Hmd2CheckFirstThenError1002); +APS5_EXPORT("6574xHS+jZk", Hmd2CheckFirstThenError1002); +APS5_EXPORT("mXzaY6xVoxg", Hmd2CheckFirstThenError1002); +APS5_EXPORT("6af-HIZQQn4", Hmd2CheckFirstThenError1002); +APS5_EXPORT("6qWtdTHQsTk", Hmd2CheckFirstThenError1002); +APS5_EXPORT("pUm8t8xWhFg", Hmd2CheckFirstThenError1002); +APS5_EXPORT("A0XKI-WjxK8", Hmd2CheckFirstThenError1002); + +std::int32_t APS5_VABI Hmd2CheckFirstThenMinusOne(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(-1, __func__); +} +APS5_EXPORT("A9s2j8OioPQ", Hmd2CheckFirstThenMinusOne); +APS5_EXPORT("YIgtmrL+HLY", Hmd2CheckFirstThenMinusOne); +APS5_EXPORT("Ng8zEkGxDNs", Hmd2CheckFirstThenMinusOne); +APS5_EXPORT("EtslTnqO1xU", Hmd2CheckFirstThenMinusOne); +APS5_EXPORT("hfrnRTryKQQ", Hmd2CheckFirstThenMinusOne); + +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenParameterInvalid(const void* a0, const void* a1) { + if (a0 == nullptr) return Hmd2::ParameterNull; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} +APS5_EXPORT("PPX68fNToFM", Hmd2CheckFirstTwoThenParameterInvalid); +APS5_EXPORT("LxPIfjHaY4c", Hmd2CheckFirstTwoThenParameterInvalid); +APS5_EXPORT("w8gBTTdg+po", Hmd2CheckFirstTwoThenParameterInvalid); + +std::int32_t APS5_VABI Hmd2CheckSecondThenError002C(const void* a0, const void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error002C, __func__); +} +APS5_EXPORT("tWJPX2ycRMM", Hmd2CheckSecondThenError002C); +APS5_EXPORT("5UMB9PJA70o", Hmd2CheckSecondThenError002C); +APS5_EXPORT("xQs62qCk1EI", Hmd2CheckSecondThenError002C); + +std::int32_t APS5_VABI Hmd2CheckFirstThenParameterInvalid(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} +APS5_EXPORT("MMc+bSlqkzk", Hmd2CheckFirstThenParameterInvalid); +APS5_EXPORT("NpZHgvSYgEA", Hmd2CheckFirstThenParameterInvalid); + +std::int32_t APS5_VABI Hmd2CheckAllSixThenZero(const void* a0, const void* a1, const void* a2, const void* a3, const void* a4, const void* a5) { + if (a0 == nullptr) return Hmd2::ParameterNull; + if (a1 == nullptr) return Hmd2::ParameterNull; + if (a2 == nullptr) return Hmd2::ParameterNull; + if (a3 == nullptr) return Hmd2::ParameterNull; + if (a4 == nullptr) return Hmd2::ParameterNull; + if (a5 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("z-serf9vsb0", Hmd2CheckAllSixThenZero); +APS5_EXPORT("IRMUFu+uG5s", Hmd2CheckAllSixThenZero); + +std::int32_t APS5_VABI Hmd2CheckSecondThenParameterInvalid(const void* a0, const void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} +APS5_EXPORT("72U64E+0z6U", Hmd2CheckSecondThenParameterInvalid); +APS5_EXPORT("LCQzi5ILGrY", Hmd2CheckSecondThenParameterInvalid); + +std::int32_t APS5_VABI Hmd2CheckFirstThenError800d0203(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error800d0203, __func__); +} +APS5_EXPORT("Im0qL0j9Ubg", Hmd2CheckFirstThenError800d0203); + +std::int32_t APS5_VABI Hmd2CheckFirstThenError1003(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("ng6eI8uA0h4", Hmd2CheckFirstThenError1003); + +std::int32_t APS5_VABI Hmd2CheckFirstThenError1010(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1010, __func__); +} +APS5_EXPORT("mPPGgjyJrCI", Hmd2CheckFirstThenError1010); + +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenError1002(const void* a0, const void* a1) { + if (a0 == nullptr) return Hmd2::ParameterNull; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("ZupCJmV5BDI", Hmd2CheckFirstTwoThenError1002); + +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenError1003(const void* a0, const void* a1) { + if (a0 == nullptr) return Hmd2::ParameterNull; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("mAQZwOX1IHU", Hmd2CheckFirstTwoThenError1003); + +std::int32_t APS5_VABI Hmd2CheckFirstAndThirdThenZero(const void* a0, const void* a1, const void* a2) { + if (a0 == nullptr) return Hmd2::ParameterNull; + (void)a1; + if (a2 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("oacDkQkinLo", Hmd2CheckFirstAndThirdThenZero); + +std::int32_t APS5_VABI Hmd2CheckFirstAndThirdThenError1003(const void* a0, const void* a1, const void* a2) { + if (a0 == nullptr) return Hmd2::ParameterNull; + (void)a1; + if (a2 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("zn7PHGzzEyI", Hmd2CheckFirstAndThirdThenError1003); + +std::int32_t APS5_VABI Hmd2CheckSecondThenInvalidHandle(const void* a0, const void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::InvalidHandle, __func__); +} +APS5_EXPORT("wEHILc7TP04", Hmd2CheckSecondThenInvalidHandle); + +std::int32_t APS5_VABI Hmd2CheckSecondThenError1002(const void* a0, const void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("W-ZIlUXcrNA", Hmd2CheckSecondThenError1002); + } diff --git a/core/libs/prx/libSceHmd2/Hmd2.hpp b/core/libs/prx/libSceHmd2/Hmd2.hpp index a3dbc23907..27a372fcb9 100644 --- a/core/libs/prx/libSceHmd2/Hmd2.hpp +++ b/core/libs/prx/libSceHmd2/Hmd2.hpp @@ -19,6 +19,8 @@ inline constexpr std::int32_t ReprojectionNotInitialized = static_cast(0x8a72002c); inline constexpr std::int32_t Error1002 = static_cast(0x8a721002); inline constexpr std::int32_t Error1003 = static_cast(0x8a721003); +inline constexpr std::int32_t Error1010 = static_cast(0x8a721010); +inline constexpr std::int32_t Error800d0203 = static_cast(0x800d0203); inline constexpr std::uint32_t UserIdSdkVersion = 0x07000000; diff --git a/core/libs/tests/GuestHmd2.cpp b/core/libs/tests/GuestHmd2.cpp index 1014602014..4429255337 100644 --- a/core/libs/tests/GuestHmd2.cpp +++ b/core/libs/tests/GuestHmd2.cpp @@ -11,7 +11,24 @@ #include extern "C" { -int APS5_VABI sceHmd2SetFieldOfView(); +std::int32_t APS5_VABI Hmd2CheckFirstThenError0004(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenZero(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenError1002(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenMinusOne(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenParameterInvalid(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckSecondThenError002C(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenParameterInvalid(const void*); +std::int32_t APS5_VABI Hmd2CheckAllSixThenZero(const void*, const void*, const void*, const void*, const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckSecondThenParameterInvalid(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenError800d0203(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenError1003(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenError1010(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenError1002(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenError1003(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstAndThirdThenZero(const void*, const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstAndThirdThenError1003(const void*, const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckSecondThenInvalidHandle(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckSecondThenError1002(const void*, const void*); std::int32_t APS5_VABI Hmd2BeforeInitializeZero(); std::int32_t APS5_VABI Hmd2BeforeInitializeNotInitialized(); std::int32_t APS5_VABI Hmd2BeforeInitializeError0004(); @@ -59,6 +76,54 @@ void TestBeforeInitialize() { Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr) == Hmd2::NotInitialized, "open before initialization"); Require(sceHmd2Close(0) == Hmd2::NotInitialized, "close before initialization"); Require(sceHmd2Terminate() == Hmd2::NotInitialized, "terminate before initialization"); + const std::uint8_t dummyBytes[16]{}; + const void* dummy = dummyBytes; + Require(Hmd2CheckFirstThenError0004(dummy) == Hmd2::Error0004, "Hmd2CheckFirstThenError0004 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError0004(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError0004 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenZero(dummy) == 0, "Hmd2CheckFirstThenZero with every pointer present before initialization"); + Require(Hmd2CheckFirstThenZero(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenZero with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenError1002(dummy) == Hmd2::Error1002, "Hmd2CheckFirstThenError1002 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError1002(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1002 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenMinusOne(dummy) == -1, "Hmd2CheckFirstThenMinusOne with every pointer present before initialization"); + Require(Hmd2CheckFirstThenMinusOne(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenMinusOne with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(dummy, dummy) == Hmd2::ParameterInvalid, "Hmd2CheckFirstTwoThenParameterInvalid with every pointer present before initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenParameterInvalid with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenParameterInvalid with pointer 1 null before initialization"); + Require(Hmd2CheckSecondThenError002C(dummy, dummy) == Hmd2::Error002C, "Hmd2CheckSecondThenError002C with every pointer present before initialization"); + Require(Hmd2CheckSecondThenError002C(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenError002C with pointer 1 null before initialization"); + Require(Hmd2CheckFirstThenParameterInvalid(dummy) == Hmd2::ParameterInvalid, "Hmd2CheckFirstThenParameterInvalid with every pointer present before initialization"); + Require(Hmd2CheckFirstThenParameterInvalid(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenParameterInvalid with pointer 0 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, dummy, dummy) == 0, "Hmd2CheckAllSixThenZero with every pointer present before initialization"); + Require(Hmd2CheckAllSixThenZero(nullptr, dummy, dummy, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 0 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, nullptr, dummy, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 1 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, nullptr, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 2 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 3 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 4 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 5 null before initialization"); + Require(Hmd2CheckSecondThenParameterInvalid(dummy, dummy) == Hmd2::ParameterInvalid, "Hmd2CheckSecondThenParameterInvalid with every pointer present before initialization"); + Require(Hmd2CheckSecondThenParameterInvalid(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenParameterInvalid with pointer 1 null before initialization"); + Require(Hmd2CheckFirstThenError800d0203(dummy) == Hmd2::Error800d0203, "Hmd2CheckFirstThenError800d0203 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError800d0203(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError800d0203 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenError1003(dummy) == Hmd2::Error1003, "Hmd2CheckFirstThenError1003 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError1003(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1003 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenError1010(dummy) == Hmd2::Error1010, "Hmd2CheckFirstThenError1010 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError1010(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1010 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenError1002(dummy, dummy) == Hmd2::Error1002, "Hmd2CheckFirstTwoThenError1002 with every pointer present before initialization"); + Require(Hmd2CheckFirstTwoThenError1002(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1002 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenError1002(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1002 with pointer 1 null before initialization"); + Require(Hmd2CheckFirstTwoThenError1003(dummy, dummy) == Hmd2::Error1003, "Hmd2CheckFirstTwoThenError1003 with every pointer present before initialization"); + Require(Hmd2CheckFirstTwoThenError1003(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1003 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenError1003(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1003 with pointer 1 null before initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(dummy, dummy, dummy) == 0, "Hmd2CheckFirstAndThirdThenZero with every pointer present before initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenZero with pointer 0 null before initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenZero with pointer 2 null before initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(dummy, dummy, dummy) == Hmd2::Error1003, "Hmd2CheckFirstAndThirdThenError1003 with every pointer present before initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenError1003 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenError1003 with pointer 2 null before initialization"); + Require(Hmd2CheckSecondThenInvalidHandle(dummy, dummy) == Hmd2::InvalidHandle, "Hmd2CheckSecondThenInvalidHandle with every pointer present before initialization"); + Require(Hmd2CheckSecondThenInvalidHandle(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenInvalidHandle with pointer 1 null before initialization"); + Require(Hmd2CheckSecondThenError1002(dummy, dummy) == Hmd2::Error1002, "Hmd2CheckSecondThenError1002 with every pointer present before initialization"); + Require(Hmd2CheckSecondThenError1002(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenError1002 with pointer 1 null before initialization"); Require(Hmd2BeforeInitializeZero() == 0, "Hmd2BeforeInitializeZero before initialization"); Require(Hmd2BeforeInitializeNotInitialized() == Hmd2::NotInitialized, "Hmd2BeforeInitializeNotInitialized before initialization"); Require(Hmd2BeforeInitializeError0004() == Hmd2::Error0004, "Hmd2BeforeInitializeError0004 before initialization"); @@ -82,6 +147,54 @@ void TestWithoutHeadset() { Hmd2::DeviceInformation info; Require(sceHmd2GetDeviceInformation(&info) == Hmd2::NotInitialized, "device information without a headset"); Require(sceHmd2Terminate() == Hmd2::NotInitialized, "terminate after the failed initialization"); + const std::uint8_t dummyBytes[16]{}; + const void* dummy = dummyBytes; + Require(Hmd2CheckFirstThenError0004(dummy) == Hmd2::Error0004, "Hmd2CheckFirstThenError0004 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError0004(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError0004 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenZero(dummy) == 0, "Hmd2CheckFirstThenZero with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenZero(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenZero with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenError1002(dummy) == Hmd2::Error1002, "Hmd2CheckFirstThenError1002 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError1002(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1002 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenMinusOne(dummy) == -1, "Hmd2CheckFirstThenMinusOne with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenMinusOne(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenMinusOne with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(dummy, dummy) == Hmd2::ParameterInvalid, "Hmd2CheckFirstTwoThenParameterInvalid with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenParameterInvalid with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenParameterInvalid with pointer 1 null after the failed initialization"); + Require(Hmd2CheckSecondThenError002C(dummy, dummy) == Hmd2::Error002C, "Hmd2CheckSecondThenError002C with every pointer present after the failed initialization"); + Require(Hmd2CheckSecondThenError002C(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenError002C with pointer 1 null after the failed initialization"); + Require(Hmd2CheckFirstThenParameterInvalid(dummy) == Hmd2::ParameterInvalid, "Hmd2CheckFirstThenParameterInvalid with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenParameterInvalid(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenParameterInvalid with pointer 0 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, dummy, dummy) == 0, "Hmd2CheckAllSixThenZero with every pointer present after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(nullptr, dummy, dummy, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 0 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, nullptr, dummy, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 1 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, nullptr, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 2 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 3 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 4 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 5 null after the failed initialization"); + Require(Hmd2CheckSecondThenParameterInvalid(dummy, dummy) == Hmd2::ParameterInvalid, "Hmd2CheckSecondThenParameterInvalid with every pointer present after the failed initialization"); + Require(Hmd2CheckSecondThenParameterInvalid(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenParameterInvalid with pointer 1 null after the failed initialization"); + Require(Hmd2CheckFirstThenError800d0203(dummy) == Hmd2::Error800d0203, "Hmd2CheckFirstThenError800d0203 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError800d0203(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError800d0203 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenError1003(dummy) == Hmd2::Error1003, "Hmd2CheckFirstThenError1003 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError1003(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1003 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenError1010(dummy) == Hmd2::Error1010, "Hmd2CheckFirstThenError1010 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError1010(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1010 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1002(dummy, dummy) == Hmd2::Error1002, "Hmd2CheckFirstTwoThenError1002 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1002(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1002 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1002(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1002 with pointer 1 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1003(dummy, dummy) == Hmd2::Error1003, "Hmd2CheckFirstTwoThenError1003 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1003(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1003 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1003(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1003 with pointer 1 null after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(dummy, dummy, dummy) == 0, "Hmd2CheckFirstAndThirdThenZero with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenZero with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenZero with pointer 2 null after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(dummy, dummy, dummy) == Hmd2::Error1003, "Hmd2CheckFirstAndThirdThenError1003 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenError1003 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenError1003 with pointer 2 null after the failed initialization"); + Require(Hmd2CheckSecondThenInvalidHandle(dummy, dummy) == Hmd2::InvalidHandle, "Hmd2CheckSecondThenInvalidHandle with every pointer present after the failed initialization"); + Require(Hmd2CheckSecondThenInvalidHandle(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenInvalidHandle with pointer 1 null after the failed initialization"); + Require(Hmd2CheckSecondThenError1002(dummy, dummy) == Hmd2::Error1002, "Hmd2CheckSecondThenError1002 with every pointer present after the failed initialization"); + Require(Hmd2CheckSecondThenError1002(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenError1002 with pointer 1 null after the failed initialization"); Require(Hmd2BeforeInitializeZero() == 0, "Hmd2BeforeInitializeZero after the failed initialization"); Require(Hmd2BeforeInitializeNotInitialized() == Hmd2::NotInitialized, "Hmd2BeforeInitializeNotInitialized after the failed initialization"); Require(Hmd2BeforeInitializeError0004() == Hmd2::Error0004, "Hmd2BeforeInitializeError0004 after the failed initialization"); @@ -114,13 +227,6 @@ int main() { InstallProcessParameters(); TestBeforeInitialize(); TestWithoutHeadset(); - bool threw = false; - try { - sceHmd2SetFieldOfView(); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw, "set field of view did not throw"); std::puts("HMD2 tests passed"); return 0; } diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index ef45d04f01..a65f640c8a 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -11,7 +11,7 @@ - [sceHmd2SetVibration](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the second parameter is checked against 25 only while a headset is connected; its name and unit are unknown. The field of view member names are assumed: of the firmware's default tangents the first is the widest and the last two are equal - PPSA01317 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) exports that throw and the remaining [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) exports. Names from the aerolib NID list - PPSA15878 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reprojection and gaze exports, `sceVrTracker2Finalize`, `sceVrTracker2GetPlayAreaOrientedBoundingBox` and `sceVrTracker2LocateCoordinateSystem` in [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp), and the `scePadVrController` trigger effect exports in [libScePad](../../../core/libs/prx/libScePad/Export.cpp). Names from the aerolib NID list -- [libSceHmd2 gaze and reprojection exports](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - `sceHmd2GazeGetCalibrationDataMaxSize`, `sceHmd2GazeGetDeviceStatus`, `sceHmd2GazeGetUserCalibrationStatus`, `sceHmd2GazeInsertRawDataWithHandle`, `sceHmd2ReprojectionSeeThroughDraw`, `sceHmd2ReprojectionSeeThroughIC`, `sceHmd2ReprojectionVideoOutEq`, `sceHmd2ReprojectionWaitShader`, `sceHmd2SeeThroughCallbackGetPoseDataFunc`, `sceHmd2GazeObjectSelectionGetSelectedObject`, `sceHmd2GazeObjectSelectionQueryMemory`, `sceHmd2ReprojectionClear`, `sceHmd2ReprojectionSet`, `sceHmd2ReprojectionSetEventGpuEnd`, `sceHmd2ReprojectionSetEventGpuStart`, `sceHmd2RpeorjectionSetParam`, `sceHmd2SetFieldOfView` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws +- [libSceHmd2 gaze and reprojection exports](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - `sceHmd2ReprojectionSeeThroughDraw`, `sceHmd2ReprojectionSeeThroughIC`, `sceHmd2ReprojectionVideoOutEq`, `sceHmd2ReprojectionWaitShader`, `sceHmd2SeeThroughCallbackGetPoseDataFunc`, `sceHmd2ReprojectionClear`, `sceHmd2ReprojectionSet`, `sceHmd2ReprojectionSetEventGpuEnd`, `sceHmd2ReprojectionSetEventGpuStart`, `sceHmd2RpeorjectionSetParam` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws - [sceHmd2InternalSetPortStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalLedDetectorStartBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalImageStartBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature @@ -49,3 +49,22 @@ - [Hmd2BeforeInitializeValue0400](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x400` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `BWqcwjCozzI`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2BeforeInitializeValue20000](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x20000` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `PRz7TdBGUI0`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded - [libSceHmd2 pre-initialization sweep](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the names in the entries above are the aerolib names of the NIDs where one exists; the error names `Error0004`, `Error002C`, `Error1002` and `Error1003` are placeholders for codes whose meaning is unknown, and `ReprojectionNotInitialized` is ours because every export answering `0x8a72000b` is a reprojection one. Exports that answered differently to null and to zeroed arguments, wrote output, returned a pointer-like value, answered `0x8a72000a` (which `sceHmd2Initialize` also returns for some applications) or crashed the probe are left as they were +- [Hmd2CheckFirstThenError0004](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a720004` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 20 exports: `9WeZr4kYfzk` (sceHmd2InternalGetFwVersion), `N+5h1hCOFAg` (sceHmd2InternalGetRequiredPUPVersion), `w2vEs2y5ZmE` (sceHmd2InternalGetOpticalParam), `L5gat1pe0y0` (sceHmd2InternalSetCameraSeeThroughParam), `4GnLHR9NJ1w`, `AHk7nGSzef0` (sceHmd2InternalGetTrackerFtsData), `ncjX9d3mc0M`, `l63QYAUuogc` (sceHmd2InternalGetSlamCommand), `mrcBySCdXSo` (sceHmd2InternalDfuGetStatus), `ieBra+td8sU` (sceHmd2InternalDfuGetUpdateStatus), `s+rzBnewgGs` (sceHmd2InternalDfuCheckAfterPvt), `Nfcm0GBdzZI` (sceHmd2InternalRelocalizerGetVersion), `Tc9rasWfrm4` (sceHmd2InternalSetHomeCalibration), `ckTbKTX4KZg`, `D7576PUbUjk` (sceHmd2InternalGazeReadStatus), `tTJ04SrBT8U`, `sDP7hZ-P6OI` (sceHmd2GazeGetDeviceStatus), `J+eVNbN9ndY` (sceHmd2GazeGetUserCalibrationStatus), `kozvHBNyxf0`, `638Tmg5y6U4`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and 0 when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 8 exports: `8ETtFtp5zqY` (sceHmd2InternalGetVrMode), `CWPkBm3EXQA`, `R62liQQIAaA` (sceHmd2InternalGetFakeLensSeparationDistance), `-bcWLHsdzo0`, `JngJzgNbu8U` (sceHmd2SetFieldOfView), `xNBSz8xrGdk`, `yUVPfN-jRIU`, `BKPR3RZ5aO4`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a721002` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 8 exports: `IG4y+drWAWw` (sceHmd2InternalRelocalizerReadData), `rhYUml9vppE` (sceHmd2InternalRelocalizerReadDataAsync), `6574xHS+jZk` (sceHmd2InternalRelocalizerReadData2), `mXzaY6xVoxg` (sceHmd2InternalTrackingReadData), `6af-HIZQQn4` (sceHmd2InternalTrackingReadData2), `6qWtdTHQsTk`, `pUm8t8xWhFg`, `A0XKI-WjxK8` (sceHmd2InternalImageReadData). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenMinusOne](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and -1 when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 5 exports: `A9s2j8OioPQ`, `YIgtmrL+HLY`, `Ng8zEkGxDNs`, `EtslTnqO1xU` (sceHmd2InternalImageStart2), `hfrnRTryKQQ` (sceHmd2InternalImageSetConfig). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstTwoThenParameterInvalid](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 1 is null and `0x8a720009` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 3 exports: `PPX68fNToFM`, `LxPIfjHaY4c` (sceHmd2GazeObjectSelectionQueryMemory), `w8gBTTdg+po` (sceHmd2GazeObjectSelectionGetSelectedObject). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckSecondThenError002C](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a72002c` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 3 exports: `tWJPX2ycRMM`, `5UMB9PJA70o`, `xQs62qCk1EI`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenParameterInvalid](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a720009` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 2 exports: `MMc+bSlqkzk` (sceHmd2InternalSetVirtualDisplaySize), `NpZHgvSYgEA`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckAllSixThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 1 and argument 2 and argument 3 and argument 4 and argument 5 is null and 0 when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 2 exports: `z-serf9vsb0` (sceHmd2InternalPushVr2dData), `IRMUFu+uG5s`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckSecondThenParameterInvalid](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a720009` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 2 exports: `72U64E+0z6U`, `LCQzi5ILGrY` (sceHmd2GazeGetCalibrationDataMaxSize). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenError800d0203](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x800d0203` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `Im0qL0j9Ubg`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a721003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `ng6eI8uA0h4` (sceHmd2InternalImageGetMemoryLayout). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenError1010](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a721010` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `mPPGgjyJrCI`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstTwoThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 1 is null and `0x8a721002` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `ZupCJmV5BDI` (sceHmd2InternalImageGetFrameData). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstTwoThenError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 1 is null and `0x8a721003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `mAQZwOX1IHU` (sceHmd2InternalImageGetAverageLuminance). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstAndThirdThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 2 is null and 0 when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `oacDkQkinLo`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstAndThirdThenError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 2 is null and `0x8a721003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `zn7PHGzzEyI`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckSecondThenInvalidHandle](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a720003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `wEHILc7TP04` (sceHmd2GazeInsertRawDataWithHandle). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckSecondThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a721002` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `W-ZIlUXcrNA`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [libSceHmd2 null-argument sweep](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - `Error1010` and `Error800d0203` are placeholder names. Left as they were: the exports that write output (`omimgG5itSs`, `FBUEhH9tQ1A`, `6PeHcAaKYg8`, `yur+xPdWPAg`, `RnieDDp5ge4`, `jM3NEJA11yw`, `b0H-n-tUk0U`) and those whose answer with a single null argument is neither `0x8a720008` nor their all-present answer, which take a handle or slot number rather than a pointer first (`MXhloJ6Iw3w`, `19wrIIiqFgM`, `Z389vAm47Ys`, `cDcrvEG8j8k`, `tuqJO6O6cb4`, `fd4fu1cziaI`, `UxMg7iL4nRs`, `K1GELwlaaGc`, `RFn39VCHjGY`, `FRoSjceDv10`, `wiv8+7ogrq0`, `wQ44gBwBw44`) From 289fd5d7c3a63f373a34bd61fe879101b8028946 Mon Sep 17 00:00:00 2001 From: T3cm0 Date: Mon, 19 Oct 2026 04:09:48 -0500 Subject: [PATCH 149/219] feat(libSceHmd2): answer 18 slot and output-writing exports before initialization as the console does The hmd2s and hmd2n sweeps left 19 libSceHmd2 exports that either write into their arguments or take a slot number rather than a pointer first. A third probe called the writers with zero-filled and with 0xAA-filled buffers, dumping every byte that changed, and the slot exports with a first argument of 0, 1, 2, 3, 4, 5, 8, 16, 255 and -1. Before sceHmd2Initialize, six writers put fixed bytes in their output (a 2 and a zero byte; 0x2000 and 0x100 as 64-bit sizes; four floats; six or twelve floats of 0 and 0.5 followed by 0x8a720003), and twelve exports accept a slot of 0 to 1, 2 or 3 and answer 0x8a720009 above it, with 0, -1, 0x8a721002, 0x8a721003, 0x8a72002c or 0x8a72100f for a valid slot. They are bound here, by NID, to 14 handlers with the measured checks, returning their value while the library is not initialized and throwing if sceHmd2Initialize ever succeeds. The one export whose answer depends on the contents of an in-out structure stays unimplemented and is described in the debt file. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libSceHmd2/Export.cpp | 105 ++++++++++++++++++++++++++++ core/libs/prx/libSceHmd2/Hmd2.hpp | 1 + core/libs/tests/GuestHmd2.cpp | 72 +++++++++++++++++++ docs/dev/debt/libSceHmd2.md | 15 ++++ 4 files changed, 193 insertions(+) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index a4b9ae1953..f6f53d804c 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -711,4 +711,109 @@ std::int32_t APS5_VABI Hmd2CheckSecondThenError1002(const void* a0, const void* } APS5_EXPORT("W-ZIlUXcrNA", Hmd2CheckSecondThenError1002); + +std::int32_t APS5_VABI Hmd2WriteStateTwo(void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + static constexpr std::uint8_t bytes[] = {0x02, 0x00, 0x00, 0x00, 0x00}; + std::memcpy(a0, bytes, sizeof bytes); + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("omimgG5itSs", Hmd2WriteStateTwo); + +std::int32_t APS5_VABI Hmd2WriteSize0x2000Align0x100(std::uint64_t* a0, std::uint64_t* a1) { + if (a0 == nullptr || a1 == nullptr) return Hmd2::ParameterNull; + *a0 = 0x2000; + *a1 = 0x100; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("6PeHcAaKYg8", Hmd2WriteSize0x2000Align0x100); + +std::int32_t APS5_VABI Hmd2WriteFourConstants(void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + static constexpr std::uint8_t bytes[] = {0xe1, 0xf0, 0x74, 0x3c, 0xd8, 0xf0, 0x74, 0x3c, 0x1f, 0xf1, 0x74, 0x3c, 0xd8, 0xf0, 0x74, 0x3c}; + std::memcpy(a0, bytes, sizeof bytes); + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("yur+xPdWPAg", Hmd2WriteFourConstants); + +std::int32_t APS5_VABI Hmd2WriteSixHalvesThenInvalidHandle(std::uint64_t a0, void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + static constexpr std::uint8_t bytes[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3f}; + std::memcpy(a1, bytes, sizeof bytes); + return BeforeInitialize(Hmd2::InvalidHandle, __func__); +} +APS5_EXPORT("RnieDDp5ge4", Hmd2WriteSixHalvesThenInvalidHandle); +APS5_EXPORT("jM3NEJA11yw", Hmd2WriteSixHalvesThenInvalidHandle); + +std::int32_t APS5_VABI Hmd2WriteTwelveHalvesThenInvalidHandle(std::uint64_t a0, void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + static constexpr std::uint8_t bytes[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + std::memcpy(a1, bytes, sizeof bytes); + return BeforeInitialize(Hmd2::InvalidHandle, __func__); +} +APS5_EXPORT("b0H-n-tUk0U", Hmd2WriteTwelveHalvesThenInvalidHandle); + +std::int32_t APS5_VABI Hmd2SlotUpTo3CheckSecondThirdThenZero(std::uint64_t a0, const void* a1, const void* a2) { + if (a1 == nullptr || a2 == nullptr) return Hmd2::ParameterNull; + if (a0 > 3) return Hmd2::ParameterInvalid; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("MXhloJ6Iw3w", Hmd2SlotUpTo3CheckSecondThirdThenZero); + +std::int32_t APS5_VABI Hmd2SlotUpTo1ThenError1002(std::uint64_t a0) { + if (a0 > 1) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("19wrIIiqFgM", Hmd2SlotUpTo1ThenError1002); + +std::int32_t APS5_VABI Hmd2SlotUpTo1CheckSecondThenError1002(std::uint64_t a0, const void* a1) { + if (a1 == nullptr) return Hmd2::ParameterNull; + if (a0 > 1) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("Z389vAm47Ys", Hmd2SlotUpTo1CheckSecondThenError1002); + +std::int32_t APS5_VABI Hmd2SlotUpTo1ThenMinusOne(std::uint64_t a0) { + if (a0 > 1) return Hmd2::ParameterInvalid; + return BeforeInitialize(-1, __func__); +} +APS5_EXPORT("cDcrvEG8j8k", Hmd2SlotUpTo1ThenMinusOne); +APS5_EXPORT("tuqJO6O6cb4", Hmd2SlotUpTo1ThenMinusOne); +APS5_EXPORT("fd4fu1cziaI", Hmd2SlotUpTo1ThenMinusOne); + +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenMinusOne(std::uint64_t a0) { + if (a0 > 2) return Hmd2::ParameterInvalid; + return BeforeInitialize(-1, __func__); +} +APS5_EXPORT("UxMg7iL4nRs", Hmd2SlotUpTo2ThenMinusOne); +APS5_EXPORT("K1GELwlaaGc", Hmd2SlotUpTo2ThenMinusOne); + +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenError1002(std::uint64_t a0) { + if (a0 > 2) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("RFn39VCHjGY", Hmd2SlotUpTo2ThenError1002); + +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenError1003(std::uint64_t a0) { + if (a0 > 2) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("FRoSjceDv10", Hmd2SlotUpTo2ThenError1003); + +std::int32_t APS5_VABI Hmd2SlotUpTo2CheckSecondThenError002C(std::uint64_t a0, const void* a1) { + if (a1 == nullptr) return Hmd2::ParameterNull; + if (a0 > 2) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error002C, __func__); +} +APS5_EXPORT("wQ44gBwBw44", Hmd2SlotUpTo2CheckSecondThenError002C); + +std::int32_t APS5_VABI Hmd2IndexZeroCheckSecondThenZero(std::uint64_t a0, const void* a1) { + if (a1 == nullptr) return Hmd2::ParameterNull; + if (a0 != 0) return Hmd2::Error100F; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("wiv8+7ogrq0", Hmd2IndexZeroCheckSecondThenZero); + } diff --git a/core/libs/prx/libSceHmd2/Hmd2.hpp b/core/libs/prx/libSceHmd2/Hmd2.hpp index 27a372fcb9..c5ae98b060 100644 --- a/core/libs/prx/libSceHmd2/Hmd2.hpp +++ b/core/libs/prx/libSceHmd2/Hmd2.hpp @@ -21,6 +21,7 @@ inline constexpr std::int32_t Error1002 = static_cast(0x8a721002); inline constexpr std::int32_t Error1003 = static_cast(0x8a721003); inline constexpr std::int32_t Error1010 = static_cast(0x8a721010); inline constexpr std::int32_t Error800d0203 = static_cast(0x800d0203); +inline constexpr std::int32_t Error100F = static_cast(0x8a72100f); inline constexpr std::uint32_t UserIdSdkVersion = 0x07000000; diff --git a/core/libs/tests/GuestHmd2.cpp b/core/libs/tests/GuestHmd2.cpp index 4429255337..4f05d57f02 100644 --- a/core/libs/tests/GuestHmd2.cpp +++ b/core/libs/tests/GuestHmd2.cpp @@ -11,6 +11,20 @@ #include extern "C" { +std::int32_t APS5_VABI Hmd2WriteStateTwo(void* a0); +std::int32_t APS5_VABI Hmd2WriteSize0x2000Align0x100(std::uint64_t* a0, std::uint64_t* a1); +std::int32_t APS5_VABI Hmd2WriteFourConstants(void* a0); +std::int32_t APS5_VABI Hmd2WriteSixHalvesThenInvalidHandle(std::uint64_t a0, void* a1); +std::int32_t APS5_VABI Hmd2WriteTwelveHalvesThenInvalidHandle(std::uint64_t a0, void* a1); +std::int32_t APS5_VABI Hmd2SlotUpTo3CheckSecondThirdThenZero(std::uint64_t a0, const void* a1, const void* a2); +std::int32_t APS5_VABI Hmd2SlotUpTo1ThenError1002(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo1CheckSecondThenError1002(std::uint64_t a0, const void* a1); +std::int32_t APS5_VABI Hmd2SlotUpTo1ThenMinusOne(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenMinusOne(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenError1002(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenError1003(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo2CheckSecondThenError002C(std::uint64_t a0, const void* a1); +std::int32_t APS5_VABI Hmd2IndexZeroCheckSecondThenZero(std::uint64_t a0, const void* a1); std::int32_t APS5_VABI Hmd2CheckFirstThenError0004(const void*); std::int32_t APS5_VABI Hmd2CheckFirstThenZero(const void*); std::int32_t APS5_VABI Hmd2CheckFirstThenError1002(const void*); @@ -76,6 +90,35 @@ void TestBeforeInitialize() { Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr) == Hmd2::NotInitialized, "open before initialization"); Require(sceHmd2Close(0) == Hmd2::NotInitialized, "close before initialization"); Require(sceHmd2Terminate() == Hmd2::NotInitialized, "terminate before initialization"); + { + std::uint8_t out[64]; + std::uint64_t words[2]; + Require(Hmd2WriteStateTwo(nullptr) == Hmd2::ParameterNull, "Hmd2WriteStateTwo null before initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteStateTwo(out) == 0 && out[0] == 2 && out[1] == 0 && out[4] == 0 && out[5] == 0xaa, "Hmd2WriteStateTwo bytes before initialization"); + Require(Hmd2WriteSize0x2000Align0x100(nullptr, words) == Hmd2::ParameterNull && Hmd2WriteSize0x2000Align0x100(words, nullptr) == Hmd2::ParameterNull, "Hmd2WriteSize0x2000Align0x100 null before initialization"); + words[0] = words[1] = ~0ull; + Require(Hmd2WriteSize0x2000Align0x100(words, words + 1) == 0 && words[0] == 0x2000 && words[1] == 0x100, "Hmd2WriteSize0x2000Align0x100 values before initialization"); + Require(Hmd2WriteFourConstants(nullptr) == Hmd2::ParameterNull, "Hmd2WriteFourConstants null before initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteFourConstants(out) == 0 && out[0] == 0xe1 && out[15] == 0x3c && out[16] == 0xaa, "Hmd2WriteFourConstants bytes before initialization"); + Require(Hmd2WriteSixHalvesThenInvalidHandle(0, nullptr) == Hmd2::ParameterNull, "Hmd2WriteSixHalvesThenInvalidHandle null before initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteSixHalvesThenInvalidHandle(0, out) == Hmd2::InvalidHandle && out[7] == 0x3f && out[20] == 0x00 && out[23] == 0x3f && out[24] == 0xaa, "Hmd2WriteSixHalvesThenInvalidHandle bytes before initialization"); + Require(Hmd2WriteTwelveHalvesThenInvalidHandle(0, nullptr) == Hmd2::ParameterNull, "Hmd2WriteTwelveHalvesThenInvalidHandle null before initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteTwelveHalvesThenInvalidHandle(0, out) == Hmd2::InvalidHandle && out[7] == 0x3f && out[31] == 0x3f && out[47] == 0x00 && out[48] == 0xaa, "Hmd2WriteTwelveHalvesThenInvalidHandle bytes before initialization"); + Require(Hmd2SlotUpTo3CheckSecondThirdThenZero(9, nullptr, out) == Hmd2::ParameterNull && Hmd2SlotUpTo3CheckSecondThirdThenZero(0, out, nullptr) == Hmd2::ParameterNull, "Hmd2SlotUpTo3CheckSecondThirdThenZero null before initialization"); + Require(Hmd2SlotUpTo3CheckSecondThirdThenZero(3, out, out) == 0 && Hmd2SlotUpTo3CheckSecondThirdThenZero(4, out, out) == Hmd2::ParameterInvalid && Hmd2SlotUpTo3CheckSecondThirdThenZero(~0ull, out, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo3CheckSecondThirdThenZero slots before initialization"); + Require(Hmd2SlotUpTo1ThenError1002(1) == Hmd2::Error1002 && Hmd2SlotUpTo1ThenError1002(2) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1ThenError1002 before initialization"); + Require(Hmd2SlotUpTo1CheckSecondThenError1002(5, nullptr) == Hmd2::ParameterNull && Hmd2SlotUpTo1CheckSecondThenError1002(0, out) == Hmd2::Error1002 && Hmd2SlotUpTo1CheckSecondThenError1002(2, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1CheckSecondThenError1002 before initialization"); + Require(Hmd2SlotUpTo1ThenMinusOne(1) == -1 && Hmd2SlotUpTo1ThenMinusOne(2) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1ThenMinusOne before initialization"); + Require(Hmd2SlotUpTo2ThenMinusOne(2) == -1 && Hmd2SlotUpTo2ThenMinusOne(3) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenMinusOne before initialization"); + Require(Hmd2SlotUpTo2ThenError1002(2) == Hmd2::Error1002 && Hmd2SlotUpTo2ThenError1002(3) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenError1002 before initialization"); + Require(Hmd2SlotUpTo2ThenError1003(0) == Hmd2::Error1003 && Hmd2SlotUpTo2ThenError1003(16) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenError1003 before initialization"); + Require(Hmd2SlotUpTo2CheckSecondThenError002C(8, nullptr) == Hmd2::ParameterNull && Hmd2SlotUpTo2CheckSecondThenError002C(2, out) == Hmd2::Error002C && Hmd2SlotUpTo2CheckSecondThenError002C(3, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2CheckSecondThenError002C before initialization"); + Require(Hmd2IndexZeroCheckSecondThenZero(1, nullptr) == Hmd2::ParameterNull && Hmd2IndexZeroCheckSecondThenZero(0, out) == 0 && Hmd2IndexZeroCheckSecondThenZero(1, out) == Hmd2::Error100F, "Hmd2IndexZeroCheckSecondThenZero before initialization"); + } const std::uint8_t dummyBytes[16]{}; const void* dummy = dummyBytes; Require(Hmd2CheckFirstThenError0004(dummy) == Hmd2::Error0004, "Hmd2CheckFirstThenError0004 with every pointer present before initialization"); @@ -147,6 +190,35 @@ void TestWithoutHeadset() { Hmd2::DeviceInformation info; Require(sceHmd2GetDeviceInformation(&info) == Hmd2::NotInitialized, "device information without a headset"); Require(sceHmd2Terminate() == Hmd2::NotInitialized, "terminate after the failed initialization"); + { + std::uint8_t out[64]; + std::uint64_t words[2]; + Require(Hmd2WriteStateTwo(nullptr) == Hmd2::ParameterNull, "Hmd2WriteStateTwo null after the failed initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteStateTwo(out) == 0 && out[0] == 2 && out[1] == 0 && out[4] == 0 && out[5] == 0xaa, "Hmd2WriteStateTwo bytes after the failed initialization"); + Require(Hmd2WriteSize0x2000Align0x100(nullptr, words) == Hmd2::ParameterNull && Hmd2WriteSize0x2000Align0x100(words, nullptr) == Hmd2::ParameterNull, "Hmd2WriteSize0x2000Align0x100 null after the failed initialization"); + words[0] = words[1] = ~0ull; + Require(Hmd2WriteSize0x2000Align0x100(words, words + 1) == 0 && words[0] == 0x2000 && words[1] == 0x100, "Hmd2WriteSize0x2000Align0x100 values after the failed initialization"); + Require(Hmd2WriteFourConstants(nullptr) == Hmd2::ParameterNull, "Hmd2WriteFourConstants null after the failed initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteFourConstants(out) == 0 && out[0] == 0xe1 && out[15] == 0x3c && out[16] == 0xaa, "Hmd2WriteFourConstants bytes after the failed initialization"); + Require(Hmd2WriteSixHalvesThenInvalidHandle(0, nullptr) == Hmd2::ParameterNull, "Hmd2WriteSixHalvesThenInvalidHandle null after the failed initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteSixHalvesThenInvalidHandle(0, out) == Hmd2::InvalidHandle && out[7] == 0x3f && out[20] == 0x00 && out[23] == 0x3f && out[24] == 0xaa, "Hmd2WriteSixHalvesThenInvalidHandle bytes after the failed initialization"); + Require(Hmd2WriteTwelveHalvesThenInvalidHandle(0, nullptr) == Hmd2::ParameterNull, "Hmd2WriteTwelveHalvesThenInvalidHandle null after the failed initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteTwelveHalvesThenInvalidHandle(0, out) == Hmd2::InvalidHandle && out[7] == 0x3f && out[31] == 0x3f && out[47] == 0x00 && out[48] == 0xaa, "Hmd2WriteTwelveHalvesThenInvalidHandle bytes after the failed initialization"); + Require(Hmd2SlotUpTo3CheckSecondThirdThenZero(9, nullptr, out) == Hmd2::ParameterNull && Hmd2SlotUpTo3CheckSecondThirdThenZero(0, out, nullptr) == Hmd2::ParameterNull, "Hmd2SlotUpTo3CheckSecondThirdThenZero null after the failed initialization"); + Require(Hmd2SlotUpTo3CheckSecondThirdThenZero(3, out, out) == 0 && Hmd2SlotUpTo3CheckSecondThirdThenZero(4, out, out) == Hmd2::ParameterInvalid && Hmd2SlotUpTo3CheckSecondThirdThenZero(~0ull, out, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo3CheckSecondThirdThenZero slots after the failed initialization"); + Require(Hmd2SlotUpTo1ThenError1002(1) == Hmd2::Error1002 && Hmd2SlotUpTo1ThenError1002(2) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1ThenError1002 after the failed initialization"); + Require(Hmd2SlotUpTo1CheckSecondThenError1002(5, nullptr) == Hmd2::ParameterNull && Hmd2SlotUpTo1CheckSecondThenError1002(0, out) == Hmd2::Error1002 && Hmd2SlotUpTo1CheckSecondThenError1002(2, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1CheckSecondThenError1002 after the failed initialization"); + Require(Hmd2SlotUpTo1ThenMinusOne(1) == -1 && Hmd2SlotUpTo1ThenMinusOne(2) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1ThenMinusOne after the failed initialization"); + Require(Hmd2SlotUpTo2ThenMinusOne(2) == -1 && Hmd2SlotUpTo2ThenMinusOne(3) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenMinusOne after the failed initialization"); + Require(Hmd2SlotUpTo2ThenError1002(2) == Hmd2::Error1002 && Hmd2SlotUpTo2ThenError1002(3) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenError1002 after the failed initialization"); + Require(Hmd2SlotUpTo2ThenError1003(0) == Hmd2::Error1003 && Hmd2SlotUpTo2ThenError1003(16) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenError1003 after the failed initialization"); + Require(Hmd2SlotUpTo2CheckSecondThenError002C(8, nullptr) == Hmd2::ParameterNull && Hmd2SlotUpTo2CheckSecondThenError002C(2, out) == Hmd2::Error002C && Hmd2SlotUpTo2CheckSecondThenError002C(3, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2CheckSecondThenError002C after the failed initialization"); + Require(Hmd2IndexZeroCheckSecondThenZero(1, nullptr) == Hmd2::ParameterNull && Hmd2IndexZeroCheckSecondThenZero(0, out) == 0 && Hmd2IndexZeroCheckSecondThenZero(1, out) == Hmd2::Error100F, "Hmd2IndexZeroCheckSecondThenZero after the failed initialization"); + } const std::uint8_t dummyBytes[16]{}; const void* dummy = dummyBytes; Require(Hmd2CheckFirstThenError0004(dummy) == Hmd2::Error0004, "Hmd2CheckFirstThenError0004 with every pointer present after the failed initialization"); diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index a65f640c8a..1dacc21ad2 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -68,3 +68,18 @@ - [Hmd2CheckSecondThenInvalidHandle](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a720003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `wEHILc7TP04` (sceHmd2GazeInsertRawDataWithHandle). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2CheckSecondThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a721002` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `W-ZIlUXcrNA`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded - [libSceHmd2 null-argument sweep](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - `Error1010` and `Error800d0203` are placeholder names. Left as they were: the exports that write output (`omimgG5itSs`, `FBUEhH9tQ1A`, `6PeHcAaKYg8`, `yur+xPdWPAg`, `RnieDDp5ge4`, `jM3NEJA11yw`, `b0H-n-tUk0U`) and those whose answer with a single null argument is neither `0x8a720008` nor their all-present answer, which take a handle or slot number rather than a pointer first (`MXhloJ6Iw3w`, `19wrIIiqFgM`, `Z389vAm47Ys`, `cDcrvEG8j8k`, `tuqJO6O6cb4`, `fd4fu1cziaI`, `UxMg7iL4nRs`, `K1GELwlaaGc`, `RFn39VCHjGY`, `FRoSjceDv10`, `wiv8+7ogrq0`, `wQ44gBwBw44`) +- [Hmd2WriteStateTwo](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the five bytes `02 00 00 00 00` at the start of its first argument and returns 0; a null first argument returns `0x8a720008`: `omimgG5itSs`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2WriteSize0x2000Align0x100](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the 64-bit values `0x2000` through its first argument and `0x100` through its second and returns 0; a null first or second argument returns `0x8a720008`: `6PeHcAaKYg8`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2WriteFourConstants](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the 16 bytes `e1 f0 74 3c d8 f0 74 3c 1f f1 74 3c d8 f0 74 3c` (four floats near 0.01495) at the start of its first argument and returns 0; a null first argument returns `0x8a720008`: `yur+xPdWPAg`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2WriteSixHalvesThenInvalidHandle](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the 24 bytes `00 00 00 00 00 00 00 3f 00 00 00 3f 00 00 00 00 00 00 00 3f 00 00 00 3f` (floats 0, 0.5, 0.5, 0, 0.5, 0.5) at the start of its second argument and returns `0x8a720003` for every first argument tried (0, a pointer); a null second argument returns `0x8a720008`: `RnieDDp5ge4`, `jM3NEJA11yw`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2WriteTwelveHalvesThenInvalidHandle](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the 48 bytes `00 00 00 00 00 00 00 3f 00 00 00 3f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3f 00 00 00 3f 00 00 00 00 00 00 00 00 00 00 00 00` (floats 0, 0.5, 0.5, 0, 0, 0, 0, 0.5, 0.5, 0, 0, 0) at the start of its second argument and returns `0x8a720003` for every first argument tried; a null second argument returns `0x8a720008`: `b0H-n-tUk0U`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo3CheckSecondThirdThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns 0 for a first argument of 0 to 3 and `0x8a720009` for 4, 5, 8, 16, 255 and -1; a null second or third argument returns `0x8a720008` even with an invalid first argument. Its fourth to sixth arguments answered `0x8a720009` when null with an invalid first argument, which does not separate them from the first-argument check, so they are not looked at: `MXhloJ6Iw3w`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo1ThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a721002` for a first argument of 0 or 1 and `0x8a720009` for 2 to 5, 8, 16, 255 and -1; its other arguments were only nulled with an invalid first argument, which answered `0x8a720009` either way, so they are not looked at: `19wrIIiqFgM`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo1CheckSecondThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a721002` for a first argument of 0 or 1 and `0x8a720009` for 2 to 5, 8, 16, 255 and -1; a null second argument returns `0x8a720008` even with an invalid first argument: `Z389vAm47Ys`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo1ThenMinusOne](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns -1 for a first argument of 0 or 1 and `0x8a720009` for 2 to 5, 8, 16, 255 and -1; the other arguments are not looked at, for the reason given above: `cDcrvEG8j8k`, `tuqJO6O6cb4` (sceHmd2InternalLedDetectorSetIrCameraConfigBySlot), `fd4fu1cziaI` (sceHmd2InternalLedDetectorSetPriorityConfigBySlot). Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo2ThenMinusOne](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns -1 for a first argument of 0 to 2 and `0x8a720009` for 3 to 5, 8, 16, 255 and -1; the other arguments are not looked at: `UxMg7iL4nRs`, `K1GELwlaaGc` (sceHmd2InternalImageSetConfigBySlot). Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo2ThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a721002` for a first argument of 0 to 2 and `0x8a720009` for 3 to 5, 8, 16, 255 and -1; the other arguments are not looked at: `RFn39VCHjGY`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo2ThenError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a721003` for a first argument of 0 to 2 and `0x8a720009` for 3 to 5, 8, 16, 255 and -1; the other arguments are not looked at: `FRoSjceDv10`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo2CheckSecondThenError002C](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a72002c` for a first argument of 0 to 2 and `0x8a720009` for 3 to 5, 8, 16, 255 and -1; a null second argument returns `0x8a720008` even with an invalid first argument: `wQ44gBwBw44`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2IndexZeroCheckSecondThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns 0 for a first argument of 0 and `0x8a72100f` for 1 to 5, 8, 16, 255 and -1; a null second argument returns `0x8a720008` even with a nonzero first argument. Its third to sixth arguments answered `0x8a72100f` when null with a nonzero first argument, which does not separate them from the first-argument check, so they are not looked at: `wiv8+7ogrq0`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [FBUEhH9tQ1A](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - left unimplemented: before initialization it writes the 32-bit value 9 through its second argument and returns 0 when its first argument points at zeroed memory, but returns `0x8a720001` without writing when that memory holds 0xAA bytes, and its fourth to sixth arguments answer `0x8a720001` when null, so the first argument is an in-out structure whose fields are not known From 5be6c2a074ce01eabb43c1c21ab60f2efe1a5144 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 21 Oct 2026 03:12:33 +1300 Subject: [PATCH 150/219] fix(libkernel): bound the raise wait on host code to 50 ms Follow-up to #7652. A raise on a thread running host code waited up to the 1 s RetryLimit before falling back, so Windows guest_raise_exception (about 220 host-resident raises) went from ~15 s to 17-30 s. Host code now has its own HostCodeLimit of 50 ms; RetryLimit still guards NtContinue and winpthreads. --- core/libs/prx/libkernel/System/src/Exception.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index 0f8f66a520..de202af371 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -192,6 +192,7 @@ void CallHandler(GuestExceptionHandler handler, int signum, GuestUcontext& ucont constexpr std::size_t RedZone = 128; constexpr std::size_t HomeArea = 32; constexpr auto RetryLimit = std::chrono::seconds(1); +constexpr auto HostCodeLimit = std::chrono::milliseconds(50); constexpr std::size_t VectorBytes = 16 * 32; struct Delivery { @@ -393,7 +394,9 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { const auto native = static_cast(thread->nativeHandle); auto queued = std::make_unique(Delivery{handler, signum, {}, {}, 0}); alignas(16) Delivery delivery{handler, signum, {}, {}, SaveVectors ? 1u : 0u}; - const auto retryDeadline = std::chrono::steady_clock::now() + RetryLimit; + const auto start = std::chrono::steady_clock::now(); + const auto retryDeadline = start + RetryLimit; + const auto hostDeadline = start + HostCodeLimit; static_cast(RestoringContext(0)); static_cast(InWinpthread(0)); static_cast(BlockedInWait(0)); @@ -423,7 +426,7 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { const bool restoring = RestoringContext(delivery.context.Rip) || InWinpthread(delivery.context.Rip); DWORD64 image = 0; const auto running = restoring ? CodeKind::Guest : RunningCode(delivery.context.Rip, image); - if (!restoring && (running == CodeKind::Guest || (running == CodeKind::Host && std::chrono::steady_clock::now() >= retryDeadline))) break; + if (!restoring && (running == CodeKind::Guest || (running == CodeKind::Host && std::chrono::steady_clock::now() >= hostDeadline))) break; ResumeThread(native); if (running == CodeKind::Unknown) { static_cast(ClassifyImage(image)); From c9ff98e24950033e720d22230e69a876d44127de Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Tue, 20 Oct 2026 16:43:51 +0200 Subject: [PATCH 151/219] test(libSceRazorCpu): let the guest unwinder see the test's frames on Windows --- core/libs/prx/libSceRazorCpu/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceRazorCpu/CMakeLists.txt b/core/libs/prx/libSceRazorCpu/CMakeLists.txt index 38dc075ae6..e4d7808ed9 100644 --- a/core/libs/prx/libSceRazorCpu/CMakeLists.txt +++ b/core/libs/prx/libSceRazorCpu/CMakeLists.txt @@ -6,6 +6,7 @@ if(BUILD_TESTING) add_test_executable(guest_razor_cpu_tests ${LIBS_INCLUDE_DIR}/tests/GuestRazorCpu.cpp) target_include_directories(guest_razor_cpu_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_razor_cpu_tests PRIVATE libSceRazorCpu) + configure_windows_unwind(guest_razor_cpu_tests) add_test(NAME guest_razor_cpu COMMAND guest_razor_cpu_tests) if(WIN32) set_tests_properties(guest_razor_cpu PROPERTIES ENVIRONMENT_MODIFICATION From 56ddbbbd0d56252349106f260fdfd1da2402af1e Mon Sep 17 00:00:00 2001 From: Joey Fisher Date: Tue, 20 Oct 2026 08:43:45 -0600 Subject: [PATCH 152/219] test(agc): report a missing host import in lookup-changed as a device skip resource-lookup-changed skipped with its own texts when the device has no host imports or refuses the import of the watched surface. Neither matches DEVICE_LIMITATION in tools/check_skips.py, so a runner without host imports would fail the job instead of recording a device skip. Both now print the existing "skipped, host imports are unavailable". --- .../libSceAgcDriver/tests/execution/SampledCacheBudget.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 30519c35af..a3faceeb7a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -590,7 +590,7 @@ bool lookupChangedTests(Context context) { context.textureCache = &textures; if (context.hostImportAlignment == 0) { Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "the lookup-changed test requires host imports"); - std::puts("skipped, the lookup-changed test requires host imports"); + std::puts("skipped, host imports are unavailable"); return false; } WatchedSurface surface(context); @@ -611,7 +611,7 @@ bool lookupChangedTests(Context context) { Require(first->Revalidate(shader, &report) && report.path == ShaderResources::ProofPath::Fast, "the proof right after an audit did not take the fast proof"); if (HostImportFor(context, surface.Address(), SurfaceBytes) == nullptr) { Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "the host import of the watched surface was refused"); - std::puts("skipped, the host import of the watched surface was refused"); + std::puts("skipped, host imports are unavailable"); return false; } Require(!bound->Released(), "the snapshot left the cache without a lookup"); From e0095e5b49de4290f57e58baea86726dd85d41b6 Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Tue, 20 Oct 2026 18:10:59 +0300 Subject: [PATCH 153/219] fix(relinker): find more constant syscall numbers before a gap syscall The raw-byte check that runs before a syscall pattern between PS4 unwind functions is skipped now also accepts mov rax, imm64, merges a following mov al, imm8 or mov ax, imm16 into the earlier constant, and walks back over any number of register moves or zeroings that leave rax alone instead of at most seven. Visited states keep the walk linear in the bytes it covers. Co-Authored-By: Claude Opus 5.5 --- .../relinker/src/analysis/SyscallScanner.cpp | 69 ++++++++++++++----- .../relinker/tests/test_sce_code_gaps.py | 6 ++ docs/dev/debt/relinker.md | 2 +- 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/core/relinker/relinker/src/analysis/SyscallScanner.cpp b/core/relinker/relinker/src/analysis/SyscallScanner.cpp index 3902220628..be3795ee18 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -10,7 +10,9 @@ #include #include +#include #include +#include namespace Relinker { @@ -175,18 +177,37 @@ static std::optional ConstantSyscallNumber(const std::vector ConstantRaxSetEndingAt(const std::vector& code, const std::size_t end) { - std::uint32_t value = 0; - if (end >= 5 && code[end - 5] == 0xB8) { - std::memcpy(&value, code.data() + end - 4, sizeof(value)); +static std::optional ConstantRaxSetEndingAt(const std::vector& code, const std::size_t end) { + if (end >= 10 && code[end - 10] == 0x48 && code[end - 9] == 0xB8) { + std::uint64_t value = 0; + std::memcpy(&value, code.data() + end - 8, sizeof(value)); return value; } + std::uint32_t immediate = 0; if (end >= 7 && code[end - 7] == 0x48 && code[end - 6] == 0xC7 && code[end - 5] == 0xC0) { - std::memcpy(&value, code.data() + end - 4, sizeof(value)); - return value; + std::memcpy(&immediate, code.data() + end - 4, sizeof(immediate)); + return static_cast(static_cast(static_cast(immediate))); + } + if (end >= 5 && code[end - 5] == 0xB8) { + std::memcpy(&immediate, code.data() + end - 4, sizeof(immediate)); + return immediate; } if (end >= 2 && (code[end - 2] == 0x31 || code[end - 2] == 0x33) && code[end - 1] == 0xC0) - return value; + return 0; + return std::nullopt; +} + +struct LowRaxWrite { + std::size_t Length; + std::uint64_t Mask; + std::uint64_t Value; +}; + +static std::optional ImmediateLowRaxWriteEndingAt(const std::vector& code, const std::size_t end) { + if (end >= 2 && code[end - 2] == 0xB0) + return LowRaxWrite{2, 0xFF, code[end - 1]}; + if (end >= 4 && code[end - 4] == 0x66 && code[end - 3] == 0xB8) + return LowRaxWrite{4, 0xFFFF, static_cast(code[end - 2]) | static_cast(code[end - 1]) << 8}; return std::nullopt; } @@ -206,15 +227,25 @@ static bool MovesOtherRegisterThanRax(const std::uint8_t* bytes, const std::size return written != 0; } -static std::optional ConstantSyscallNumberInBytes(const std::vector& code, const std::size_t end, const std::size_t steps) { - if (const auto value = ConstantRaxSetEndingAt(code, end)) - return value; - if (steps == 0) - return std::nullopt; - for (const std::size_t length : {std::size_t{2}, std::size_t{3}}) - if (end >= length && MovesOtherRegisterThanRax(code.data() + end - length, length)) - if (const auto value = ConstantSyscallNumberInBytes(code, end - length, steps - 1)) - return value; +static std::optional ConstantSyscallNumberInBytes(const std::vector& code, const std::size_t syscall) { + using Step = std::tuple; + std::vector pending{{syscall, 0, 0}}; + std::set seen(pending.begin(), pending.end()); + const auto visit = [&](const Step& step) { + if (seen.insert(step).second) + pending.push_back(step); + }; + while (!pending.empty()) { + const auto [end, mask, low] = pending.back(); + pending.pop_back(); + if (const auto value = ConstantRaxSetEndingAt(code, end)) + return (*value & ~mask) | low; + if (const auto write = ImmediateLowRaxWriteEndingAt(code, end)) + visit({end - write->Length, mask | write->Mask, low | (write->Value & ~mask)}); + for (const std::size_t length : {std::size_t{2}, std::size_t{3}}) + if (end >= length && MovesOtherRegisterThanRax(code.data() + end - length, length)) + visit({end - length, mask, low}); + } return std::nullopt; } @@ -268,10 +299,12 @@ std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::v return; } - auto number = kind == ForbiddenInstruction::Invocation ? ConstantSyscallNumber(codeSection, codeSectionOffset, recent) : std::nullopt; + std::optional number; + if (kind == ForbiddenInstruction::Invocation) + number = ConstantSyscallNumber(codeSection, codeSectionOffset, recent); if (!functions.empty() && !number && !insideFunction(start)) { if (kind == ForbiddenInstruction::Invocation) - number = ConstantSyscallNumberInBytes(codeSection, opcode, SYSCALL_NUMBER_LOOKBACK - 1); + number = ConstantSyscallNumberInBytes(codeSection, opcode); if (!number) { ++skipped; if (!firstSkipped) firstSkipped = match.Offset; diff --git a/core/relinker/relinker/tests/test_sce_code_gaps.py b/core/relinker/relinker/tests/test_sce_code_gaps.py index 3f2590e5a3..aee8daf982 100644 --- a/core/relinker/relinker/tests/test_sce_code_gaps.py +++ b/core/relinker/relinker/tests/test_sce_code_gaps.py @@ -25,6 +25,10 @@ LOADED_NUMBERS = ( ("padding-after-undecodable", UNDECODABLE + b"\xcc\xcc" + MOV_EAX_SYSCALL, 0x21D, 59), ("register-move", bytes.fromhex("b8 3b 00 00 00 48 89 df 0f 05 c3"), 0x218, 59), ("zeroed", bytes.fromhex("31 c0 0f 05 c3"), 0x212, 0), + ("mov-rax-imm64", bytes.fromhex("48 b8 3b 00 00 00 00 00 00 00 0f 05 c3"), 0x21A, 59), + ("low-byte-after-zeroing", bytes.fromhex("31 c0 b0 3b 0f 05 c3"), 0x214, 59), + ("low-word-after-zeroing", bytes.fromhex("31 c0 66 b8 3b 01 0f 05 c3"), 0x216, 0x13B), + ("eight-register-moves", bytes.fromhex("b8 3b 00 00 00") + bytes.fromhex("89 db") * 8 + SYSCALL, 0x225, 59), ) JUMP_TABLE_DISPATCH = bytes.fromhex("48 8d 0d 09 00 00 00 48 63 04 b9 48 01 c8 ff e0") @@ -126,6 +130,8 @@ def main(): expected_output=SKIPPED % (1, 0x220)) run(relinker, work, "sce-gap-syscall-number-overwritten", executable(bytes.fromhex("b8 3b 00 00 00 48 89 f8 0f 05 c3")), [], expected_output=SKIPPED % (1, 0x218)) + run(relinker, work, "sce-gap-syscall-low-byte-only", executable(bytes.fromhex("b0 3b 0f 05 c3")), [], + expected_output=SKIPPED % (1, 0x212)) run(relinker, work, "sce-gap-syscall-after-overlong-instruction", executable(SYSCALL_NUMBER + b"\x41" * 15 + SYSCALL), [], expected_output=SKIPPED % (1, 0x217)) run_module(relinker, work, "ps5-module-gap-syscall-bytes", ps5_module(), [], diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index d7bd9e46e6..ff965d6a9c 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -20,7 +20,7 @@ - In executables with a `PT_SCE_DYNLIBDATA` segment, the PS4 layout, the executable segment also holds read-only data after the code (CUSA00900: 37 MiB after its last function). The [relinker pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp) therefore scans for syscalls and import call sites only up to the end of the PLT that follows the last function of the unwind table; code after the PLT without an unwind entry is not scanned. `--to-intel` and the `unused-filter=1` and `unused-filter=2` analyses are not limited this way. - The [syscall scan of `sce_module` modules](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) with a `PT_SCE_DYNLIBDATA` segment stops at the same end, or, when the PLT comes before the code, at the end of the last function of the unwind table; code after it is not scanned. - In the same executables and modules, the bytes between unwind functions are 16-byte slots of function tails and padding, and a tail can begin inside an instruction. The [instruction scanner](../../../core/relinker/codegen/src/InstructionScanner.cpp) of the syscall and call-site scans therefore resumes at the next byte when bytes outside every unwind function do not decode, and ends a decoding outside them at the start of the next unwind function. Code without an unwind entry that does not decode from its first byte is skipped by these scans instead of failing the relink; inside an unwind function a decode failure still fails it. -- Read-only data can also sit between the unwind functions of such images, with whole tables in a gap. There, a `syscall`, `sysenter`, `int 0x80` or `sysret` pattern fails the [syscall scan](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) only when a constant syscall number is set before it; the others, and every `sysret` pattern, are skipped and counted in one message. The number is looked for in the decoded instructions and, whatever alignment the scan's decoding took, in the bytes in front of the pattern: a `mov eax, imm32`, `mov rax, imm32` or `xor eax, eax` that ends at the pattern or before at most seven register moves or zeroings that leave `rax` alone. A real syscall without an unwind entry whose number is set in another way, such as from memory or from another register, is therefore not refused, and runs on the host. +- Read-only data can also sit between the unwind functions of such images. There, a `syscall`, `sysenter` or `int 0x80` pattern fails the [syscall scan](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) only when a constant syscall number is set before it; the others, and every `sysret`, are skipped and counted in one message. The number is looked for in the decoded instructions and, whatever alignment the decoding took, in the bytes before the pattern: a `mov eax, imm32`, `mov rax, imm32`, `mov rax, imm64` or `xor eax, eax`, optionally followed by `mov al, imm8` or `mov ax, imm16`, then by any number of register moves or zeroings that leave `rax` alone. A real syscall without an unwind entry whose number is set otherwise is not refused and runs on the host: from memory or another register (`mov eax, ecx`), by `push`/`pop` or arithmetic (`or eax, imm32`), or with any other instruction between the load and the pattern. - In the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs for such images, a relocation or `lea` target outside every unwind function is followed as code only when its linear decoding, up to the first jump, return or trap, the next unwind function or 256 instructions, holds an `fs:[disp32]` access, or when it is 16-byte aligned, starts less than 256 bytes after an unwind function and ends in a jump, return or trap. Every other such target is taken as data and counted in one message, and the Windows TLS rewrite fails instead of overwriting one. A TLS access of another form, such as `mov rax, fs:[rax]`, or a `gs` instruction in code reached only through such a target is therefore not rewritten or trapped. Other fs and gs instructions do not tell code from data there: string bytes decode as these prefixes, and in two measured titles about 2,500 of about 5,500 such targets decode to one before their first jump. - In executables and guest modules with a `PT_SCE_DYNLIBDATA` segment, the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs follows code only from the end of `PT_INTERP`, or of the `/libexec/ld-elf.so.1` string that begins the segment when no `PT_INTERP` covers it, as in OpenOrbis modules, to the end of the PLT after the last function of the unwind table, or to the end of that function when the PLT comes before the code, as in the `libSceFios2.prx` and `libc.prx` that CUSA02636 bundles (PLT at 0xc8); the pipeline's scans of executables above stop at this same end, also when the PLT comes before the code. A guest module with PLT relocations and no PLT before or after its unwind functions, or with a PLT that overlaps one, fails as the executable does. Code after that end is not analyzed, such as the `DT_FINI` routine without an unwind entry that follows the last unwind function in both modules and that the Windows and macOS outputs call when the program exits: a TLS access in it is not rewritten, the ELF entry point, `DT_INIT`, `DT_FINI`, a branch or a pointer into it is ignored, and a function symbol or init/fini array entry pointing into it fails the conversion. - A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)). From d093d3b520aa53f4e3cd9418aca2841ad78dab67 Mon Sep 17 00:00:00 2001 From: PYTHONRETARDE <152398455+PYTHONRETARDE@users.noreply.github.com> Date: Thu, 15 Oct 2026 00:59:52 +0000 Subject: [PATCH 154/219] fix(agc): key shader modules by the pipeline library mode AMD's Windows driver compiles pipelines differently when VK_EXT_graphics_pipeline_library is enabled on the device, but its on-disk cache (%LOCALAPPDATA%\AMD\VkCache) does not key on it. A process in one mode is then served pipelines compiled in the other, and their writes never land (#4768, #5271). With the library feature off, every module now flips bit 15 of the SPIR-V generator word, which no driver interprets, so the two modes hash to separate cache entries. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/PresentationPass.cpp | 10 ++++++---- .../Execution/src/VulkanDevice.cpp | 7 ++++++- .../libSceAgcDriver/Graphics/include/Context.hpp | 6 ++++++ .../Graphics/src/GpuColorTransfer.cpp | 5 +++-- .../prx/libSceAgcDriver/Graphics/src/Pipeline.cpp | 7 ++++++- .../prx/libSceAgcDriver/Graphics/src/Recorder.cpp | 15 +++++++++------ .../Graphics/src/TextureDetiler.cpp | 15 +++++++++------ 7 files changed, 45 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp index ecaa69ff9c..e246000759 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp @@ -72,11 +72,13 @@ PresentationPass::PresentationPass(const Graphics::Context& context, std::size_t context.Function("vkUpdateDescriptorSets")(context.device, static_cast(tableWrites.size()), tableWrites.data(), 0, nullptr); const auto createModule = context.Function("vkCreateShaderModule"); VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(PRESENTATION_PASS_VERT_SPV); - moduleInfo.pCode = PRESENTATION_PASS_VERT_SPV; + const auto presentationPassVertCode = Graphics::CacheKeyedSpirv(context.graphicsPipelineLibrary, PRESENTATION_PASS_VERT_SPV, sizeof(PRESENTATION_PASS_VERT_SPV)); + moduleInfo.codeSize = presentationPassVertCode.size() * sizeof(std::uint32_t); + moduleInfo.pCode = presentationPassVertCode.data(); Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &vertex), "vkCreateShaderModule presentation vertex"); - moduleInfo.codeSize = sizeof(PRESENTATION_PASS_FRAG_SPV); - moduleInfo.pCode = PRESENTATION_PASS_FRAG_SPV; + const auto presentationPassFragCode = Graphics::CacheKeyedSpirv(context.graphicsPipelineLibrary, PRESENTATION_PASS_FRAG_SPV, sizeof(PRESENTATION_PASS_FRAG_SPV)); + moduleInfo.codeSize = presentationPassFragCode.size() * sizeof(std::uint32_t); + moduleInfo.pCode = presentationPassFragCode.data(); Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &fragment), "vkCreateShaderModule presentation fragment"); } catch (...) { release(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index c19c2efa29..d634b9356c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4872,9 +4872,14 @@ std::shared_ptr VulkanDevice::createComputePipeline(cons objects->destroyModule = state->DeviceFunction("vkDestroyShaderModule"); objects->destroyLayout = state->DeviceFunction("vkDestroyPipelineLayout"); objects->destroyPipeline = state->DeviceFunction("vkDestroyPipeline"); + std::vector keyed; + if (!state->graphicsPipelineLibrary && shader.spirv.size() > 2) { + keyed.assign(shader.spirv.data(), shader.spirv.data() + shader.spirv.size()); + keyed[2] ^= 0x8000u; + } VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; moduleInfo.codeSize = shader.spirv.size() * sizeof(std::uint32_t); - moduleInfo.pCode = shader.spirv.data(); + moduleInfo.pCode = keyed.empty() ? shader.spirv.data() : keyed.data(); check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects->module), "vkCreateShaderModule"); objects->resourcesLayout = setLayout; const std::array setLayouts{setLayout, shader.workgroupMemoryDwords != 0 ? state->workgroupMemoryLayout : VK_NULL_HANDLE}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index c73dcc10bb..e8a03e46e5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -20,6 +20,12 @@ namespace AgcDriver::Graphics { +inline std::vector CacheKeyedSpirv(bool graphicsPipelineLibrary, const std::uint32_t* code, std::size_t bytes) { + std::vector keyed(code, code + bytes / sizeof(std::uint32_t)); + if (!graphicsPipelineLibrary && keyed.size() > 2) keyed[2] ^= 0x8000u; + return keyed; +} + class TextureDetiler; class GpuColorTransfer; class BufferPool; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp index 973aff1e34..c29c7e941b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp @@ -29,8 +29,9 @@ GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) { layoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout color transfer"); VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(COLOR_TRANSFER_SPV); - moduleInfo.pCode = COLOR_TRANSFER_SPV; + const auto colorTransferCode = CacheKeyedSpirv(context.graphicsPipelineLibrary, COLOR_TRANSFER_SPV, sizeof(COLOR_TRANSFER_SPV)); + moduleInfo.codeSize = colorTransferCode.size() * sizeof(std::uint32_t); + moduleInfo.pCode = colorTransferCode.data(); Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule color transfer"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index eaca4be3f4..4979c76e21 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -359,9 +359,14 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput const VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT meshSubgroup{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT, nullptr, 32u}; for (std::uint32_t i = 0; i < shaders.size(); ++i) { const auto& shader = *shaders[i].program; + std::vector keyed; + if (!context.graphicsPipelineLibrary && shader.spirv.size() > 2) { + keyed.assign(shader.spirv.data(), shader.spirv.data() + shader.spirv.size()); + keyed[2] ^= 0x8000u; + } VkShaderModuleCreateInfo module{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; module.codeSize = shader.spirv.size() * sizeof(std::uint32_t); - module.pCode = shader.spirv.data(); + module.pCode = keyed.empty() ? shader.spirv.data() : keyed.data(); Check(context.Function("vkCreateShaderModule")(context.device, &module, nullptr, &_modules[i]), "vkCreateShaderModule graphics"); const auto stage = VulkanStage(shaders[i].stage); stages[i].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index a4dfce1f09..e33db0d231 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -2076,8 +2076,9 @@ bool Recorder::gpuSampleCounter() { layoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &sampleLayout), "vkCreatePipelineLayout sample counter"); VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(SAMPLE_COUNTER_SPV); - moduleInfo.pCode = SAMPLE_COUNTER_SPV; + const auto sampleCounterCode = CacheKeyedSpirv(context.graphicsPipelineLibrary, SAMPLE_COUNTER_SPV, sizeof(SAMPLE_COUNTER_SPV)); + moduleInfo.codeSize = sampleCounterCode.size() * sizeof(std::uint32_t); + moduleInfo.pCode = sampleCounterCode.data(); Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule sample counter"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; @@ -2094,8 +2095,9 @@ bool Recorder::gpuSampleCounter() { const VkPushConstantRange dumpPush{VK_SHADER_STAGE_COMPUTE_BIT, 0, SampleDumpPushBytes}; layoutInfo.pPushConstantRanges = &dumpPush; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &dumpLayout), "vkCreatePipelineLayout sample dumps"); - moduleInfo.codeSize = sizeof(SAMPLE_DUMPS_SPV); - moduleInfo.pCode = SAMPLE_DUMPS_SPV; + const auto sampleDumpsCode = CacheKeyedSpirv(context.graphicsPipelineLibrary, SAMPLE_DUMPS_SPV, sizeof(SAMPLE_DUMPS_SPV)); + moduleInfo.codeSize = sampleDumpsCode.size() * sizeof(std::uint32_t); + moduleInfo.pCode = sampleDumpsCode.data(); Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule sample dumps"); pipelineInfo.stage.module = module; pipelineInfo.layout = dumpLayout; @@ -2392,8 +2394,9 @@ bool Recorder::RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress rec layoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &meshArgumentLayout), "vkCreatePipelineLayout mesh arguments"); VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(MESH_ARGUMENTS_SPV); - moduleInfo.pCode = MESH_ARGUMENTS_SPV; + const auto meshArgumentsCode = CacheKeyedSpirv(context.graphicsPipelineLibrary, MESH_ARGUMENTS_SPV, sizeof(MESH_ARGUMENTS_SPV)); + moduleInfo.codeSize = meshArgumentsCode.size() * sizeof(std::uint32_t); + moduleInfo.pCode = meshArgumentsCode.data(); Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule mesh arguments"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp index 0bd34f6890..4063f8fb02 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp @@ -90,8 +90,9 @@ TextureDetiler::TextureDetiler(const Context& context) : context(context) { pipelineLayoutInfo.pPushConstantRanges = &pushRange; Check(context.Function("vkCreatePipelineLayout")(context.device, &pipelineLayoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout"); VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(TEXTURE_DETILE_SPV); - moduleInfo.pCode = TEXTURE_DETILE_SPV; + const auto textureDetileCode = CacheKeyedSpirv(context.graphicsPipelineLibrary, TEXTURE_DETILE_SPV, sizeof(TEXTURE_DETILE_SPV)); + moduleInfo.codeSize = textureDetileCode.size() * sizeof(std::uint32_t); + moduleInfo.pCode = textureDetileCode.data(); Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule"); bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &layoutInfo, nullptr, &imageDescriptorLayout), "vkCreateDescriptorSetLayout"); @@ -169,8 +170,9 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem } if (image && imageModule == VK_NULL_HANDLE) { VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(TEXTURE_DETILE_IMAGE_SPV); - moduleInfo.pCode = TEXTURE_DETILE_IMAGE_SPV; + const auto textureDetileImageCode = CacheKeyedSpirv(context.graphicsPipelineLibrary, TEXTURE_DETILE_IMAGE_SPV, sizeof(TEXTURE_DETILE_IMAGE_SPV)); + moduleInfo.codeSize = textureDetileImageCode.size() * sizeof(std::uint32_t); + moduleInfo.pCode = textureDetileImageCode.data(); Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &imageModule), "vkCreateShaderModule"); } std::array entries{}; @@ -348,8 +350,9 @@ void TextureDetiler::DispatchCmaskClear(VkCommandBuffer commands, VkBuffer cmask pipelineLayoutInfo.pPushConstantRanges = &pushRange; Check(context.Function("vkCreatePipelineLayout")(context.device, &pipelineLayoutInfo, nullptr, &cmaskPipelineLayout), "vkCreatePipelineLayout CMASK clear"); VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(CMASK_CLEAR_SPV); - moduleInfo.pCode = CMASK_CLEAR_SPV; + const auto cmaskClearCode = CacheKeyedSpirv(context.graphicsPipelineLibrary, CMASK_CLEAR_SPV, sizeof(CMASK_CLEAR_SPV)); + moduleInfo.codeSize = cmaskClearCode.size() * sizeof(std::uint32_t); + moduleInfo.pCode = cmaskClearCode.data(); VkShaderModule cmaskModule = VK_NULL_HANDLE; Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &cmaskModule), "vkCreateShaderModule CMASK clear"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; From f161a741db0b2fcb8baee2b79437a7a3b75b8a9d Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Thu, 15 Oct 2026 17:03:51 +0200 Subject: [PATCH 155/219] fix(agc): key the compute pipeline replay's module by the pipeline library mode The replay rebuilds a captured compute pipeline to time it. With the library feature off, production now flips the SPIR-V generator word before vkCreateShaderModule. The replay creates the same module, so it hits the same AMD driver cache entries as the run it reproduces. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index d634b9356c..681e8e0952 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -914,9 +914,10 @@ ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePip check(state->DeviceFunction("vkCreatePipelineCache")(state->device, &cacheInfo, nullptr, &replay.cache), "vkCreatePipelineCache replay"); result.cacheMilliseconds = elapsed(phase); phase = std::chrono::steady_clock::now(); + const auto replayCode = Graphics::CacheKeyedSpirv(state->graphicsPipelineLibrary, capture.spirv.data(), capture.spirv.size() * sizeof(std::uint32_t)); VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = capture.spirv.size() * sizeof(std::uint32_t); - moduleInfo.pCode = capture.spirv.data(); + moduleInfo.codeSize = replayCode.size() * sizeof(std::uint32_t); + moduleInfo.pCode = replayCode.data(); check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects.module), "vkCreateShaderModule replay"); result.moduleMilliseconds = elapsed(phase); phase = std::chrono::steady_clock::now(); From 4d07bc0231c78b65c5a9194000f7d5b2652cc52a Mon Sep 17 00:00:00 2001 From: tkcm01 <75453983+tkcm01@users.noreply.github.com> Date: Tue, 20 Oct 2026 12:07:26 -0400 Subject: [PATCH 156/219] fix(relinker): format invalid Windows icon paths as UTF-8 --- .../src/windows/WindowsIconResourceBuilder.cpp | 5 ++++- .../elfpatcher/tests/WindowsIconResourceTests.cpp | 10 ++++++---- core/relinker/relinker/tests/test_windows_icon.py | 4 ++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp index 0ee6929940..4a547373bd 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp @@ -16,7 +16,10 @@ std::uint32_t readBigEndian(const std::vector& bytes, const std::s } void validatePng(const std::vector& bytes, const std::filesystem::path& path) { - const auto fail = [&path](const std::string& reason) { throw Domain::RelinkerException("Invalid Windows icon PNG '" + path.string() + "': " + reason); }; + const auto fail = [&path](const std::string& reason) { + const auto utf8 = path.u8string(); + throw Domain::RelinkerException("Invalid Windows icon PNG '" + std::string(utf8.begin(), utf8.end()) + "': " + reason); + }; constexpr std::array signature{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}; if (bytes.size() < 33 || !std::equal(signature.begin(), signature.end(), bytes.begin())) fail("missing PNG signature or IHDR"); if (readBigEndian(bytes, 8) != 13 || readBigEndian(bytes, 12) != 0x49484452) fail("invalid IHDR size or type"); diff --git a/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp b/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp index dfedf0f6ab..a3813723aa 100644 --- a/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp +++ b/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp @@ -54,13 +54,14 @@ void checkChunks() { } for (const auto& value : chunks) image.insert(image.end(), value.begin(), value.end()); image.insert(image.end(), Png.end() - 12, Png.end()); - const std::filesystem::path path = std::filesystem::path(name) / "icon.png"; + const std::filesystem::path path = std::filesystem::path(std::u8string(name.begin(), name.end())) / "icon.png"; + const auto utf8 = path.u8string(); std::vector sections; bool rejected = false; try { builder.Build(image, path, sections, 0x10000); } catch (const Domain::RelinkerException& error) { rejected = true; - require(std::string(error.what()).find(path.string()) != std::string::npos, "Icon failure omitted the path"); + require(std::string(error.what()).find(std::string(utf8.begin(), utf8.end())) != std::string::npos, "Icon failure omitted the path"); } require(rejected != valid, "Unexpected validation result for " + name); require(sections.size() == (valid ? 1u : 0u), "Unexpected section mutation for " + name); @@ -119,7 +120,8 @@ void run() { require(readU32(data, 174) == Png.size(), "Unexpected icon size in group entry"); require(std::equal(Png.begin(), Png.end(), data.begin() + 184), "Icon bytes changed"); - const auto badPath = std::filesystem::path("bad/icon0.png"); + const std::filesystem::path badPath(u8"\u65e5\u672c/bad/icon0.png"); + const auto badUtf8 = badPath.u8string(); auto truncated = Png; truncated.resize(40); std::vector badSections; @@ -127,7 +129,7 @@ void run() { try { builder.Build(truncated, badPath, badSections, nextRva); } catch (const Domain::RelinkerException& e) { - require(std::string(e.what()).find(badPath.string()) != std::string::npos, std::string("Failure does not name the icon: ") + e.what()); + require(std::string(e.what()).find(std::string(badUtf8.begin(), badUtf8.end())) != std::string::npos, std::string("Failure does not name the icon: ") + e.what()); require(badSections.empty(), "Section added despite the failure"); caughtTruncated = true; } diff --git a/core/relinker/relinker/tests/test_windows_icon.py b/core/relinker/relinker/tests/test_windows_icon.py index a43ed330db..998f33e70c 100644 --- a/core/relinker/relinker/tests/test_windows_icon.py +++ b/core/relinker/relinker/tests/test_windows_icon.py @@ -95,7 +95,7 @@ def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-icon-") as directory: work = Path(directory) - app = work / "app with spaces" + app = work / "app with spaces \u65e5\u672c" app.mkdir() source = app / "eboot.bin" source.write_bytes(fixture()) @@ -104,7 +104,7 @@ def main(): def relink(name): output = work / f"{name}.exe" result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(source), str(output)], - cwd=work, capture_output=True, text=True, timeout=20) + cwd=work, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=20) return result, output result, output = relink("missing-initial") From 28ce7bc0b82004899266a7af281c926afa08a9fe Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Tue, 20 Oct 2026 18:12:06 +0200 Subject: [PATCH 157/219] fix(agc): create every shader module through the pipeline-library cache key Six vkCreateShaderModule calls added to main since this branch's base still passed their SPIR-V unchanged, so on AMD's Windows driver they shared on-disk cache entries across the two pipeline library modes: splitComputePipeline's recompiled compute programs, the multisample readback modules, the dispatch-arguments module, the texture repack module and the two performance overlay modules. Graphics::CreateShaderModule now keys the generator word and creates the module in one place. Every module the driver creates goes through it, including the recompiled graphics and compute programs that copied the key inline, so new call sites can't skip it. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/PerformanceOverlay.cpp | 10 ++------ .../Execution/src/PresentationPass.cpp | 12 ++-------- .../Execution/src/VulkanDevice.cpp | 22 +++-------------- .../Graphics/include/Context.hpp | 20 ++++++++++++---- .../Graphics/src/GpuColorTransfer.cpp | 6 +---- .../Graphics/src/MultisampleReadback.cpp | 6 ++--- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 10 +------- .../libSceAgcDriver/Graphics/src/Recorder.cpp | 22 ++++------------- .../Graphics/src/TextureDetiler.cpp | 24 ++++--------------- .../prx/libSceAgcDriver/tests/Graphics.cpp | 20 ++++++++++++++++ 10 files changed, 54 insertions(+), 98 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp index e21ddf0599..ca74a794d9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -538,14 +538,8 @@ PerformanceOverlay::PerformanceOverlay(const Graphics::Context& context, std::si layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Graphics::Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout performance overlay"); - const auto createModule = context.Function("vkCreateShaderModule"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(PERFORMANCE_OVERLAY_VERT_SPV); - moduleInfo.pCode = PERFORMANCE_OVERLAY_VERT_SPV; - Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &vertex), "vkCreateShaderModule performance overlay vertex"); - moduleInfo.codeSize = sizeof(PERFORMANCE_OVERLAY_FRAG_SPV); - moduleInfo.pCode = PERFORMANCE_OVERLAY_FRAG_SPV; - Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &fragment), "vkCreateShaderModule performance overlay fragment"); + vertex = Graphics::CreateShaderModule(context, PERFORMANCE_OVERLAY_VERT_SPV, sizeof(PERFORMANCE_OVERLAY_VERT_SPV), "vkCreateShaderModule performance overlay vertex"); + fragment = Graphics::CreateShaderModule(context, PERFORMANCE_OVERLAY_FRAG_SPV, sizeof(PERFORMANCE_OVERLAY_FRAG_SPV), "vkCreateShaderModule performance overlay fragment"); } catch (...) { release(); throw; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp index e246000759..e42e4e2b03 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp @@ -70,16 +70,8 @@ PresentationPass::PresentationPass(const Graphics::Context& context, std::size_t tableWrites[i].pBufferInfo = &tableInfo; } context.Function("vkUpdateDescriptorSets")(context.device, static_cast(tableWrites.size()), tableWrites.data(), 0, nullptr); - const auto createModule = context.Function("vkCreateShaderModule"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - const auto presentationPassVertCode = Graphics::CacheKeyedSpirv(context.graphicsPipelineLibrary, PRESENTATION_PASS_VERT_SPV, sizeof(PRESENTATION_PASS_VERT_SPV)); - moduleInfo.codeSize = presentationPassVertCode.size() * sizeof(std::uint32_t); - moduleInfo.pCode = presentationPassVertCode.data(); - Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &vertex), "vkCreateShaderModule presentation vertex"); - const auto presentationPassFragCode = Graphics::CacheKeyedSpirv(context.graphicsPipelineLibrary, PRESENTATION_PASS_FRAG_SPV, sizeof(PRESENTATION_PASS_FRAG_SPV)); - moduleInfo.codeSize = presentationPassFragCode.size() * sizeof(std::uint32_t); - moduleInfo.pCode = presentationPassFragCode.data(); - Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &fragment), "vkCreateShaderModule presentation fragment"); + vertex = Graphics::CreateShaderModule(context, PRESENTATION_PASS_VERT_SPV, sizeof(PRESENTATION_PASS_VERT_SPV), "vkCreateShaderModule presentation vertex"); + fragment = Graphics::CreateShaderModule(context, PRESENTATION_PASS_FRAG_SPV, sizeof(PRESENTATION_PASS_FRAG_SPV), "vkCreateShaderModule presentation fragment"); } catch (...) { release(); throw; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 681e8e0952..66da673ed8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -914,11 +914,7 @@ ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePip check(state->DeviceFunction("vkCreatePipelineCache")(state->device, &cacheInfo, nullptr, &replay.cache), "vkCreatePipelineCache replay"); result.cacheMilliseconds = elapsed(phase); phase = std::chrono::steady_clock::now(); - const auto replayCode = Graphics::CacheKeyedSpirv(state->graphicsPipelineLibrary, capture.spirv.data(), capture.spirv.size() * sizeof(std::uint32_t)); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = replayCode.size() * sizeof(std::uint32_t); - moduleInfo.pCode = replayCode.data(); - check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects.module), "vkCreateShaderModule replay"); + objects.module = Graphics::CreateShaderModule(state->DeviceFunction("vkCreateShaderModule"), state->device, state->graphicsPipelineLibrary, capture.spirv.data(), capture.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule replay"); result.moduleMilliseconds = elapsed(phase); phase = std::chrono::steady_clock::now(); std::array, 2> descriptorCounts{}; @@ -4873,15 +4869,7 @@ std::shared_ptr VulkanDevice::createComputePipeline(cons objects->destroyModule = state->DeviceFunction("vkDestroyShaderModule"); objects->destroyLayout = state->DeviceFunction("vkDestroyPipelineLayout"); objects->destroyPipeline = state->DeviceFunction("vkDestroyPipeline"); - std::vector keyed; - if (!state->graphicsPipelineLibrary && shader.spirv.size() > 2) { - keyed.assign(shader.spirv.data(), shader.spirv.data() + shader.spirv.size()); - keyed[2] ^= 0x8000u; - } - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = shader.spirv.size() * sizeof(std::uint32_t); - moduleInfo.pCode = keyed.empty() ? shader.spirv.data() : keyed.data(); - check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects->module), "vkCreateShaderModule"); + objects->module = Graphics::CreateShaderModule(state->DeviceFunction("vkCreateShaderModule"), state->device, state->graphicsPipelineLibrary, shader.spirv.data(), shader.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule"); objects->resourcesLayout = setLayout; const std::array setLayouts{setLayout, shader.workgroupMemoryDwords != 0 ? state->workgroupMemoryLayout : VK_NULL_HANDLE}; const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::ComputePushConstantBytes}; @@ -4941,11 +4929,7 @@ VkPipeline VulkanDevice::splitComputePipeline(const Graphics::Context& context, std::array offsets{}; for (std::uint32_t component = 0; component < offsets.size(); ++component) offsets[component] = {ShaderRecompiler::PipelineSpecialization::WorkgroupOffsetBase + component, base[component]}; const Graphics::PipelineSpecialization specialization(shader, offsets); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = shader.spirv.size() * sizeof(std::uint32_t); - moduleInfo.pCode = shader.spirv.data(); - VkShaderModule module = VK_NULL_HANDLE; - check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &module), "vkCreateShaderModule"); + VkShaderModule module = Graphics::CreateShaderModule(state->DeviceFunction("vkCreateShaderModule"), state->device, state->graphicsPipelineLibrary, shader.spirv.data(), shader.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule"); VkPipeline pipeline = VK_NULL_HANDLE; try { CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, module, objects.layout, computePipelineFlags(), computeStageFlags(shader), specialization.Info(), computeSubgroupSize(shader), &pipeline, context.pipelineCacheSynchronization); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index e8a03e46e5..b4c113d749 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -20,11 +20,6 @@ namespace AgcDriver::Graphics { -inline std::vector CacheKeyedSpirv(bool graphicsPipelineLibrary, const std::uint32_t* code, std::size_t bytes) { - std::vector keyed(code, code + bytes / sizeof(std::uint32_t)); - if (!graphicsPipelineLibrary && keyed.size() > 2) keyed[2] ^= 0x8000u; - return keyed; -} class TextureDetiler; class GpuColorTransfer; @@ -77,6 +72,17 @@ inline void Check(VkResult result, const char* operation) { if (result != VK_SUCCESS) throw std::runtime_error(std::string("AGC graphics: ") + operation + ": Vulkan result " + std::to_string(result)); } +inline VkShaderModule CreateShaderModule(PFN_vkCreateShaderModule create, VkDevice device, bool graphicsPipelineLibrary, const std::uint32_t* code, std::size_t bytes, const char* operation) { + std::vector keyed(code, code + bytes / sizeof(std::uint32_t)); + if (!graphicsPipelineLibrary && keyed.size() > 2) keyed[2] ^= 0x8000u; + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = keyed.size() * sizeof(std::uint32_t); + info.pCode = keyed.data(); + VkShaderModule module = VK_NULL_HANDLE; + Check(create(device, &info, nullptr, &module), operation); + return module; +} + inline void Require(bool condition, const char* reason) { if (!condition) throw std::runtime_error(std::string("AGC graphics: ") + reason); } @@ -343,6 +349,10 @@ struct Context { } }; +inline VkShaderModule CreateShaderModule(const Context& context, const std::uint32_t* code, std::size_t bytes, const char* operation) { + return CreateShaderModule(context.Function("vkCreateShaderModule"), context.device, context.graphicsPipelineLibrary, code, bytes, operation); +} + inline void EndRenderPass(const Context& context, VkCommandBuffer commands) { if (context.graphicsPipelineLibrary) context.Resolved(&DeviceFunctions::cmdEndRendering, "vkCmdEndRenderingKHR")(commands); else context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp index c29c7e941b..9881ba2473 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp @@ -28,11 +28,7 @@ GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) { layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout color transfer"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - const auto colorTransferCode = CacheKeyedSpirv(context.graphicsPipelineLibrary, COLOR_TRANSFER_SPV, sizeof(COLOR_TRANSFER_SPV)); - moduleInfo.codeSize = colorTransferCode.size() * sizeof(std::uint32_t); - moduleInfo.pCode = colorTransferCode.data(); - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule color transfer"); + module = CreateShaderModule(context, COLOR_TRANSFER_SPV, sizeof(COLOR_TRANSFER_SPV), "vkCreateShaderModule color transfer"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp index 8f731eb881..fe92fa1b97 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp @@ -46,10 +46,8 @@ VkPipeline TextureDetiler::multisampleReadbackPipeline(std::uint32_t elementByte } auto& module = readbackModules[depth ? 1u : 0u]; if (module == VK_NULL_HANDLE) { - VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - info.codeSize = depth ? sizeof(MULTISAMPLE_READBACK_DEPTH_SPV) : sizeof(MULTISAMPLE_READBACK_UINT_SPV); - info.pCode = depth ? MULTISAMPLE_READBACK_DEPTH_SPV : MULTISAMPLE_READBACK_UINT_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &info, nullptr, &module), "vkCreateShaderModule multisample readback"); + module = depth ? CreateShaderModule(context, MULTISAMPLE_READBACK_DEPTH_SPV, sizeof(MULTISAMPLE_READBACK_DEPTH_SPV), "vkCreateShaderModule multisample readback") + : CreateShaderModule(context, MULTISAMPLE_READBACK_UINT_SPV, sizeof(MULTISAMPLE_READBACK_UINT_SPV), "vkCreateShaderModule multisample readback"); } const VkSpecializationMapEntry entry{0u, 0u, sizeof(elementBytes)}; const VkSpecializationInfo specialization{1u, &entry, sizeof(elementBytes), &elementBytes}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 4979c76e21..1eaef4b7ac 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -359,15 +359,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput const VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT meshSubgroup{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT, nullptr, 32u}; for (std::uint32_t i = 0; i < shaders.size(); ++i) { const auto& shader = *shaders[i].program; - std::vector keyed; - if (!context.graphicsPipelineLibrary && shader.spirv.size() > 2) { - keyed.assign(shader.spirv.data(), shader.spirv.data() + shader.spirv.size()); - keyed[2] ^= 0x8000u; - } - VkShaderModuleCreateInfo module{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - module.codeSize = shader.spirv.size() * sizeof(std::uint32_t); - module.pCode = keyed.empty() ? shader.spirv.data() : keyed.data(); - Check(context.Function("vkCreateShaderModule")(context.device, &module, nullptr, &_modules[i]), "vkCreateShaderModule graphics"); + _modules[i] = CreateShaderModule(context, shader.spirv.data(), shader.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule graphics"); const auto stage = VulkanStage(shaders[i].stage); stages[i].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[i].stage = stage; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index e33db0d231..45b109c2c8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1992,10 +1992,7 @@ bool Recorder::RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &dispatchArgumentLayout), "vkCreatePipelineLayout dispatch arguments"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(DISPATCH_ARGUMENTS_SPV); - moduleInfo.pCode = DISPATCH_ARGUMENTS_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule dispatch arguments"); + module = CreateShaderModule(context, DISPATCH_ARGUMENTS_SPV, sizeof(DISPATCH_ARGUMENTS_SPV), "vkCreateShaderModule dispatch arguments"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; @@ -2075,11 +2072,7 @@ bool Recorder::gpuSampleCounter() { layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &sampleLayout), "vkCreatePipelineLayout sample counter"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - const auto sampleCounterCode = CacheKeyedSpirv(context.graphicsPipelineLibrary, SAMPLE_COUNTER_SPV, sizeof(SAMPLE_COUNTER_SPV)); - moduleInfo.codeSize = sampleCounterCode.size() * sizeof(std::uint32_t); - moduleInfo.pCode = sampleCounterCode.data(); - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule sample counter"); + module = CreateShaderModule(context, SAMPLE_COUNTER_SPV, sizeof(SAMPLE_COUNTER_SPV), "vkCreateShaderModule sample counter"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; @@ -2095,10 +2088,7 @@ bool Recorder::gpuSampleCounter() { const VkPushConstantRange dumpPush{VK_SHADER_STAGE_COMPUTE_BIT, 0, SampleDumpPushBytes}; layoutInfo.pPushConstantRanges = &dumpPush; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &dumpLayout), "vkCreatePipelineLayout sample dumps"); - const auto sampleDumpsCode = CacheKeyedSpirv(context.graphicsPipelineLibrary, SAMPLE_DUMPS_SPV, sizeof(SAMPLE_DUMPS_SPV)); - moduleInfo.codeSize = sampleDumpsCode.size() * sizeof(std::uint32_t); - moduleInfo.pCode = sampleDumpsCode.data(); - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule sample dumps"); + module = CreateShaderModule(context, SAMPLE_DUMPS_SPV, sizeof(SAMPLE_DUMPS_SPV), "vkCreateShaderModule sample dumps"); pipelineInfo.stage.module = module; pipelineInfo.layout = dumpLayout; { @@ -2393,11 +2383,7 @@ bool Recorder::RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress rec layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &meshArgumentLayout), "vkCreatePipelineLayout mesh arguments"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - const auto meshArgumentsCode = CacheKeyedSpirv(context.graphicsPipelineLibrary, MESH_ARGUMENTS_SPV, sizeof(MESH_ARGUMENTS_SPV)); - moduleInfo.codeSize = meshArgumentsCode.size() * sizeof(std::uint32_t); - moduleInfo.pCode = meshArgumentsCode.data(); - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule mesh arguments"); + module = CreateShaderModule(context, MESH_ARGUMENTS_SPV, sizeof(MESH_ARGUMENTS_SPV), "vkCreateShaderModule mesh arguments"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp index 4063f8fb02..74621bdba1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp @@ -89,11 +89,7 @@ TextureDetiler::TextureDetiler(const Context& context) : context(context) { pipelineLayoutInfo.pushConstantRangeCount = 1; pipelineLayoutInfo.pPushConstantRanges = &pushRange; Check(context.Function("vkCreatePipelineLayout")(context.device, &pipelineLayoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - const auto textureDetileCode = CacheKeyedSpirv(context.graphicsPipelineLibrary, TEXTURE_DETILE_SPV, sizeof(TEXTURE_DETILE_SPV)); - moduleInfo.codeSize = textureDetileCode.size() * sizeof(std::uint32_t); - moduleInfo.pCode = textureDetileCode.data(); - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule"); + module = CreateShaderModule(context, TEXTURE_DETILE_SPV, sizeof(TEXTURE_DETILE_SPV), "vkCreateShaderModule"); bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &layoutInfo, nullptr, &imageDescriptorLayout), "vkCreateDescriptorSetLayout"); pipelineLayoutInfo.pSetLayouts = &imageDescriptorLayout; @@ -169,11 +165,7 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem std::copy(equation->bits.begin(), equation->bits.end(), values.begin() + 4); } if (image && imageModule == VK_NULL_HANDLE) { - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - const auto textureDetileImageCode = CacheKeyedSpirv(context.graphicsPipelineLibrary, TEXTURE_DETILE_IMAGE_SPV, sizeof(TEXTURE_DETILE_IMAGE_SPV)); - moduleInfo.codeSize = textureDetileImageCode.size() * sizeof(std::uint32_t); - moduleInfo.pCode = textureDetileImageCode.data(); - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &imageModule), "vkCreateShaderModule"); + imageModule = CreateShaderModule(context, TEXTURE_DETILE_IMAGE_SPV, sizeof(TEXTURE_DETILE_IMAGE_SPV), "vkCreateShaderModule"); } std::array entries{}; for (std::uint32_t index = 0; index < entries.size(); ++index) entries[index] = {index, index * 4u, 4}; @@ -275,10 +267,7 @@ void TextureDetiler::DispatchRepack(VkCommandBuffer commands, VkBuffer source, s Require(commands != VK_NULL_HANDLE && source != VK_NULL_HANDLE && destination != VK_NULL_HANDLE, "a texel repack needs a command buffer and its buffers"); Require(bytes != 0 && bytes % 4u == 0 && sourceOffset % 4u == 0 && destinationOffset % 4u == 0, "a texel repack needs whole aligned words"); if (repackPipeline == VK_NULL_HANDLE) { - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(TEXTURE_REPACK_SPV); - moduleInfo.pCode = TEXTURE_REPACK_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &repackModule), "vkCreateShaderModule repack"); + repackModule = CreateShaderModule(context, TEXTURE_REPACK_SPV, sizeof(TEXTURE_REPACK_SPV), "vkCreateShaderModule repack"); VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; stage.module = repackModule; @@ -349,12 +338,7 @@ void TextureDetiler::DispatchCmaskClear(VkCommandBuffer commands, VkBuffer cmask pipelineLayoutInfo.pushConstantRangeCount = 1; pipelineLayoutInfo.pPushConstantRanges = &pushRange; Check(context.Function("vkCreatePipelineLayout")(context.device, &pipelineLayoutInfo, nullptr, &cmaskPipelineLayout), "vkCreatePipelineLayout CMASK clear"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - const auto cmaskClearCode = CacheKeyedSpirv(context.graphicsPipelineLibrary, CMASK_CLEAR_SPV, sizeof(CMASK_CLEAR_SPV)); - moduleInfo.codeSize = cmaskClearCode.size() * sizeof(std::uint32_t); - moduleInfo.pCode = cmaskClearCode.data(); - VkShaderModule cmaskModule = VK_NULL_HANDLE; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &cmaskModule), "vkCreateShaderModule CMASK clear"); + VkShaderModule cmaskModule = CreateShaderModule(context, CMASK_CLEAR_SPV, sizeof(CMASK_CLEAR_SPV), "vkCreateShaderModule CMASK clear"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index e28a78b241..92e2141a31 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8393,6 +8393,25 @@ void LineStripTests() { Require(rejection.empty(), "the draw precheck rejected an indexed line strip with primitive restart: " + rejection); } +std::uint32_t keyedGenerator = 0; +std::size_t keyedWords = 0; + +VKAPI_ATTR VkResult VKAPI_CALL keyedCreateShaderModule(VkDevice, const VkShaderModuleCreateInfo* info, const VkAllocationCallbacks*, VkShaderModule* module) { + keyedWords = info->codeSize / sizeof(std::uint32_t); + keyedGenerator = info->pCode[2]; + *module = makeHandle(); + return VK_SUCCESS; +} + +void ShaderModuleKeyTests() { + const std::array code{0x07230203u, 0x00010300u, 0x00080001u, 8u, 0u}; + static_cast(AgcDriver::Graphics::CreateShaderModule(keyedCreateShaderModule, VK_NULL_HANDLE, true, code.data(), sizeof(code), "keyed module")); + Require(keyedWords == code.size() && keyedGenerator == 0x00080001u, "a shader module with graphics pipeline libraries changed its generator word"); + static_cast(AgcDriver::Graphics::CreateShaderModule(keyedCreateShaderModule, VK_NULL_HANDLE, false, code.data(), sizeof(code), "keyed module")); + Require(keyedWords == code.size() && keyedGenerator == (0x00080001u ^ 0x8000u), "a shader module without graphics pipeline libraries kept the generator word of the other mode"); + Require(code[2] == 0x00080001u, "keying a shader module changed its source words"); +} + void ProvokingVertexTests() { using namespace AgcDriver::Graphics; constexpr std::uint32_t lastVertex = 1u << 19u; @@ -10391,6 +10410,7 @@ int main(int argc, char** argv) { stencilReferenceUnusedTests(); aliasedBufferViewTests(); LineStripTests(); + ShaderModuleKeyTests(); ProvokingVertexTests(); ProvokingVertexRenderPassTests(); ConservativeRasterizationTests(); From eef987ae7b2095d5b1a21dc663796ad7e674206c Mon Sep 17 00:00:00 2001 From: teepean Date: Mon, 19 Oct 2026 08:54:52 +0300 Subject: [PATCH 158/219] perf(agc): fold the occlusion dumps of a queued store run once A queued store run records each group's plain stores and then its occlusion counter dumps, and any store after a dump starts a new group, so a label the title writes after its dumps lands after them. A title that clears each query slot, dumps into it and then writes a flag after it gets a group per slot that way: a barrier, then three fold dispatches with a barrier before each. The run's writes are now recorded in three parts where that keeps the final bytes: the stores before the first dump and the stores that a later dump overlaps (the clears of the slots), then every dump in one fold, then the other stores in their order. A store after a dump that no later dump overlaps still lands after every dump before it, and those stores keep their order. A clear that only a later dump covers in part must not overlap an earlier dump, and no clear may overlap a store it moves ahead of; otherwise the run keeps its groups. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Graphics/include/Recorder.hpp | 1 + .../libSceAgcDriver/Graphics/src/Recorder.cpp | 62 +++++++++++++++++++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 45 ++++++++++++++ 3 files changed, 108 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 169dcabeb6..6e3c0c8ce9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -233,6 +233,7 @@ public: std::uint64_t end; bool computed; }; + static std::vector QueuedWriteOrder(std::span writes); static std::vector QueuedWriteGroups(std::span writes); // Waits only for the batches up to the newest one that writes the range (submitting the open // batch when it is that one); later batches stay in flight. Fences of one queue signal in diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index a4dfce1f09..bf95ec46d8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1676,6 +1676,18 @@ bool Recorder::closeStoreRun(bool atSubmit) { std::vector writes; writes.reserve(stores.size()); for (const auto& store : stores) writes.push_back({store.address, store.End(), store.dumpTarget != 0}); + if (const auto order = QueuedWriteOrder(writes); !std::is_sorted(order.begin(), order.end())) { + std::vector ordered; + std::vector orderedWrites; + ordered.reserve(stores.size()); + orderedWrites.reserve(writes.size()); + for (const auto index : order) { + ordered.push_back(std::move(stores[index])); + orderedWrites.push_back(writes[index]); + } + stores = std::move(ordered); + writes = std::move(orderedWrites); + } const bool dumps = std::any_of(writes.begin(), writes.end(), [](const QueuedWrite& write) { return write.computed; }); if (dumps) recordQueuedDumps(commands, stores); std::size_t first = 0; @@ -2125,6 +2137,56 @@ std::uint64_t Recorder::Batch::StoreRun::Queued::End() const { return address + (dumpTarget != 0 ? SampleDumpBytes : bytes.size()); } +std::vector Recorder::QueuedWriteOrder(std::span writes) { + std::vector order(writes.size()); + for (std::size_t i = 0; i < order.size(); ++i) order[i] = i; + std::vector coveredLater(writes.size()), overLater(writes.size()); + std::map later; + for (auto i = writes.size(); i-- > 0;) { + const auto& write = writes[i]; + if (!write.computed) { + const auto after = later.upper_bound(write.begin); + coveredLater[i] = after != later.begin() && std::prev(after)->second >= write.end; + const auto last = later.lower_bound(write.end); + overLater[i] = last != later.begin() && std::prev(last)->second > write.begin; + continue; + } + auto begin = write.begin, end = write.end; + auto at = later.lower_bound(begin); + if (at != later.begin() && std::prev(at)->second >= begin) --at; + while (at != later.end() && at->first <= end) { + begin = std::min(begin, at->first); + end = std::max(end, at->second); + at = later.erase(at); + } + later.emplace(begin, end); + } + std::vector before, dumps, after; + std::vector> earlier, delayed; + for (std::size_t i = 0; i < writes.size(); ++i) { + const auto& write = writes[i]; + const auto over = [&](const std::vector>& ranges) { + return std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return write.begin < range.second && range.first < write.end; }); + }; + if (write.computed) { + dumps.push_back(i); + earlier.emplace_back(write.begin, write.end); + } else if (dumps.empty()) { + before.push_back(i); + } else if (overLater[i]) { + if (over(delayed) || (!coveredLater[i] && over(earlier))) return order; + before.push_back(i); + } else { + after.push_back(i); + delayed.emplace_back(write.begin, write.end); + } + } + order = std::move(before); + order.insert(order.end(), dumps.begin(), dumps.end()); + order.insert(order.end(), after.begin(), after.end()); + return order; +} + std::vector Recorder::QueuedWriteGroups(std::span writes) { std::vector ends; std::vector> computed; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 3c749730b7..e3318969bb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -6630,7 +6630,32 @@ void queuedWriteGroupTests() { } return memory; }; + const auto recorded = [](const std::vector& writes) { + const auto order = Recorder::QueuedWriteOrder(writes); + std::vector ordered; + for (const auto index : order) ordered.push_back(writes[index]); + const auto groups = Recorder::QueuedWriteGroups(ordered); + std::vector sequence; + std::size_t first = 0; + for (const auto end : groups) { + for (const bool computed : {false, true}) { + for (auto i = first; i < end; ++i) { + if (ordered[i].computed == computed) sequence.push_back(order[i]); + } + } + first = end; + } + return std::pair{sequence, groups.size()}; + }; Require(Recorder::QueuedWriteGroups({}).empty(), "no queued writes made a group"); + const std::vector flagged{{0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x200, 0x208, false}, {0x400, 0x500, false}, {0x400, 0x4f8, true}, {0x408, 0x500, true}, {0x500, 0x508, false}, {0x800, 0x804, false}}; + Require(recorded(flagged) == std::pair{std::vector{0, 4, 1, 2, 5, 6, 3, 7, 8}, std::size_t{2}}, "the dumps of a run with a flag after each query slot were not folded once, with the clears before them and the flags after them"); + const std::vector open{{0x50, 0x58, false}, {0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x200, 0x208, false}, {0x400, 0x500, false}, {0x400, 0x4f8, true}}; + Require(recorded(open) == std::pair{std::vector{0, 1, 5, 2, 3, 6, 4}, std::size_t{2}}, "the clear of a slot whose end dump the run does not hold was not moved ahead of the dumps"); + const std::vector overDump{{0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x1f0, 0x410, false}, {0x400, 0x4f8, true}}; + Require(Recorder::QueuedWriteOrder(overDump) == std::vector{0, 1, 2, 3, 4}, "a store over an earlier dump that the later dumps cover only in part was moved ahead of that dump"); + const std::vector overFlag{{0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x500, 0x508, false}, {0x400, 0x508, false}, {0x400, 0x4f8, true}, {0x408, 0x500, true}}; + Require(Recorder::QueuedWriteOrder(overFlag) == std::vector{0, 1, 2, 3, 4, 5, 6}, "a clear over an earlier store after a dump was moved ahead of that store"); const std::vector query{{0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x200, 0x300, false}, {0x200, 0x2f8, true}, {0x208, 0x300, true}}; Require(Recorder::QueuedWriteGroups(query) == Groups{3, 6}, "the clear of the next query slot did not wait for the dumps before it"); const std::vector reused{{0x100, 0x1f8, true}, {0x100, 0x200, false}, {0x100, 0x1f8, true}}; @@ -6660,6 +6685,26 @@ void queuedWriteGroupTests() { const auto groups = Recorder::QueuedWriteGroups(writes); Require(!groups.empty() && groups.back() == writes.size() && std::is_sorted(groups.begin(), groups.end()), "queued write groups do not end at the last write in order"); Require(paint(writes, groups, bytes) == programOrder(writes, bytes), "recording each group's stores before its computed writes changed the final bytes of the program order"); + const auto sequence = recorded(writes).first; + std::vector position(writes.size(), writes.size()); + for (std::size_t i = 0; i < sequence.size(); ++i) position[sequence[i]] = i; + Require(sequence.size() == writes.size() && std::find(position.begin(), position.end(), writes.size()) == position.end(), "the recorded order is not a permutation of the queued writes"); + std::vector memory(bytes, 0); + for (const auto index : sequence) { + for (auto at = writes[index].begin; at < writes[index].end; ++at) memory[at] = static_cast(index + 1); + } + Require(memory == programOrder(writes, bytes), "the recorded order changed the final bytes of the program order"); + for (std::size_t j = 0; j < writes.size(); ++j) { + if (writes[j].computed) continue; + std::vector covered(bytes); + for (auto k = j + 1; k < writes.size(); ++k) { + if (writes[k].computed) std::fill(covered.begin() + writes[k].begin, covered.begin() + writes[k].end, true); + } + if (std::any_of(covered.begin() + writes[j].begin, covered.begin() + writes[j].end, [](bool value) { return value; })) continue; + for (std::size_t i = 0; i < j; ++i) { + Require(position[i] < position[j], writes[i].computed ? "a store no later dump overlaps was recorded before a dump ahead of it" : "a store no later dump overlaps was recorded before a store ahead of it"); + } + } } } From d896645ec7f1efbc9c00b6ba6bdaac3282f4a517 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Tue, 20 Oct 2026 14:57:04 +0700 Subject: [PATCH 159/219] fix(relinker): name the guest module in its output filename errors The checks on a guest module's output name threw without an input path, so the conversion's catch reported the main executable. They now carry the module's path, as the guest image reader's errors do. The loop's duplicate-export check gets the same change for consistency; it cannot fire, because the reader rejects a duplicate first. Co-Authored-By: Claude Opus 5.5 --- .../relinker/src/guest/GuestModuleBuilder.cpp | 13 +++++++++---- .../relinker/tests/test_conversion_diagnostics.py | 2 ++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 9c59dbccd1..efe7073a55 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -99,20 +99,25 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path std::set outputNames; for (const auto& path : paths) { auto image = std::move(discovered.at(path)); - if (image.OutputName.find_first_of("$\r\n") != std::string::npos) throw Domain::RelinkerException("Unsupported guest filename: " + image.OutputName); + const auto fail = [&](const std::string& message) { + Domain::RelinkerException error(message); + error.InputPath = Io::Utf8Path(path); + throw error; + }; + if (image.OutputName.find_first_of("$\r\n") != std::string::npos) fail("Unsupported guest filename: " + image.OutputName); std::string folded = image.OutputName; if (windows) { for (auto& value : folded) { - if (static_cast(value) >= 128 || value == ':' || value == '$') throw Domain::RelinkerException("Unsupported Windows guest filename: " + image.OutputName); + if (static_cast(value) >= 128 || value == ':' || value == '$') fail("Unsupported Windows guest filename: " + image.OutputName); if (value >= 'A' && value <= 'Z') value = static_cast(value + ('a' - 'A')); } } - if (!outputNames.insert(folded).second) throw Domain::RelinkerException("Conflicting guest output filename: " + image.OutputName); + if (!outputNames.insert(folded).second) fail("Conflicting guest output filename: " + image.OutputName); for (const auto& symbol : image.Symbols) { if (symbol.Section == 0 || symbol.Section == AbsoluteSection || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; auto& providers = exports[symbol.Name]; const bool repeated = !providers.empty() && providers.back().Provider == images.size(); - if (!windows && repeated) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + Io::Utf8Path(path) + " and " + Io::Utf8Path(images[providers.front().Provider].SourcePath)); + if (!windows && repeated) fail("Duplicate guest export after stripping #: " + symbol.Name + " in " + Io::Utf8Path(path) + " and " + Io::Utf8Path(images[providers.front().Provider].SourcePath)); if (!repeated) providers.push_back({images.size(), static_cast(symbol.Info & 15)}); if (!windows && providers.size() > 1) for (const auto& provider : providers) sharedExports[symbol.Name].insert(provider.Provider); diff --git a/core/relinker/relinker/tests/test_conversion_diagnostics.py b/core/relinker/relinker/tests/test_conversion_diagnostics.py index 776206cc5f..c86ca4ec0e 100644 --- a/core/relinker/relinker/tests/test_conversion_diagnostics.py +++ b/core/relinker/relinker/tests/test_conversion_diagnostics.py @@ -71,6 +71,8 @@ def main(): for name, image, message in cases: convert(f"{index}-{name}", image, message, flags) convert(f"{index}-extra", cases[0][1], cases[0][2], flags, extra=True) + if "--windows" in flags: + convert(f"{index}-module-name", None, "Unsupported Windows guest filename", flags, module=guest_fixture(PLAIN_SITE)) module = guest_fixture(PLAIN_SITE) struct.pack_into(" Date: Tue, 20 Oct 2026 22:14:59 +0530 Subject: [PATCH 160/219] feat(libc): export the remaining Dinkumware floating-point constants Add _Denorm, _Hugeval, _Nan, _Snan, _FDenorm, _FSnan, _LDenorm, _LInf, _LNan and _LSnan with the 16 bytes the console's libc.prx exports. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libc/src/Math.cpp | 10 ++++++++ core/libs/tests/GuestMath.cpp | 43 +++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 063eca4f01..2700e71426 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -706,4 +706,14 @@ union SonyDconst { static_assert(std::endian::native == std::endian::little); SonyDconst _Inf_nid_postfix {{0, 0, 0, 0x7ff0, 0, 0, 0, 0}}; +SonyDconst _Denorm_nid_postfix {{1, 0, 0, 0, 0, 0, 0, 0}}; +SonyDconst _Hugeval_nid_postfix {{0, 0, 0, 0x7ff0, 0, 0, 0, 0}}; +SonyDconst _Nan_nid_postfix {{0, 0, 0, 0x7ff8, 0, 0, 0, 0}}; +SonyDconst _Snan_nid_postfix {{1, 0, 0, 0x7ff0, 0, 0, 0, 0}}; +LibcFloatConstant _FDenorm_nid_postfix {{0x00000001u, 0, 0, 0}}; +LibcFloatConstant _FSnan_nid_postfix {{0x7f800001u, 0, 0, 0}}; +SonyDconst _LDenorm_nid_postfix {{1, 0, 0, 0, 0, 0, 0, 0}}; +SonyDconst _LInf_nid_postfix {{0, 0, 0, 0x8000, 0x7fff, 0, 0, 0}}; +SonyDconst _LNan_nid_postfix {{0, 0, 0, 0xc000, 0x7fff, 0, 0, 0}}; +SonyDconst _LSnan_nid_postfix {{1, 0, 0, 0x8000, 0x7fff, 0, 0, 0}}; } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 3edad3eb4e..d837f180c9 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -57,6 +57,18 @@ long double APS5_VABI frexpl_nid_postfix(long double, int*); struct LibcFloatConstant { std::uint32_t bits[4]; }; extern LibcFloatConstant _FInf_nid_postfix; extern LibcFloatConstant _FNan_nid_postfix; +extern LibcFloatConstant _FDenorm_nid_postfix; +extern LibcFloatConstant _FSnan_nid_postfix; +union SonyDconst { unsigned short _Word[8]; float _Float; double _Double; long double _Long_double; }; +extern SonyDconst _Inf_nid_postfix; +extern SonyDconst _Denorm_nid_postfix; +extern SonyDconst _Hugeval_nid_postfix; +extern SonyDconst _Nan_nid_postfix; +extern SonyDconst _Snan_nid_postfix; +extern SonyDconst _LDenorm_nid_postfix; +extern SonyDconst _LInf_nid_postfix; +extern SonyDconst _LNan_nid_postfix; +extern SonyDconst _LSnan_nid_postfix; short APS5_VABI _FDtest_nid_postfix(const float*); short APS5_VABI _Dtest_nid_postfix(const double*); int APS5_VABI __fpclassifyf_nid_postfix(float); @@ -881,6 +893,36 @@ static void CheckFloatClassification() { Require(_FDtest_nid_postfix(reinterpret_cast(&_FNan_nid_postfix)) == 2); } +static void RequireBytes(const void* constant, std::initializer_list expected) { + unsigned char bytes[16]; + std::memcpy(bytes, constant, sizeof(bytes)); + int index = 0; + for (const unsigned char byte : expected) Require(bytes[index++] == byte); + while (index < 16) Require(bytes[index++] == 0); +} + +static void CheckDinkumwareConstants() { + RequireBytes(&_Inf_nid_postfix, {0, 0, 0, 0, 0, 0, 0xf0, 0x7f}); + RequireBytes(&_Denorm_nid_postfix, {1}); + RequireBytes(&_Hugeval_nid_postfix, {0, 0, 0, 0, 0, 0, 0xf0, 0x7f}); + RequireBytes(&_Nan_nid_postfix, {0, 0, 0, 0, 0, 0, 0xf8, 0x7f}); + RequireBytes(&_Snan_nid_postfix, {1, 0, 0, 0, 0, 0, 0xf0, 0x7f}); + RequireBytes(&_FDenorm_nid_postfix, {1}); + RequireBytes(&_FSnan_nid_postfix, {1, 0, 0x80, 0x7f}); + RequireBytes(&_LDenorm_nid_postfix, {1}); + RequireBytes(&_LInf_nid_postfix, {0, 0, 0, 0, 0, 0, 0, 0x80, 0xff, 0x7f}); + RequireBytes(&_LNan_nid_postfix, {0, 0, 0, 0, 0, 0, 0, 0xc0, 0xff, 0x7f}); + RequireBytes(&_LSnan_nid_postfix, {1, 0, 0, 0, 0, 0, 0, 0x80, 0xff, 0x7f}); + Require(_Dtest_nid_postfix(&_Denorm_nid_postfix._Double) == -2 && _Denorm_nid_postfix._Double == std::numeric_limits::denorm_min()); + Require(_Dtest_nid_postfix(&_Hugeval_nid_postfix._Double) == 1 && _Hugeval_nid_postfix._Double == HUGE_VAL); + Require(_Dtest_nid_postfix(&_Nan_nid_postfix._Double) == 2 && _Dtest_nid_postfix(&_Snan_nid_postfix._Double) == 2); + Require(_FDtest_nid_postfix(reinterpret_cast(&_FDenorm_nid_postfix)) == -2); + Require(_FDtest_nid_postfix(reinterpret_cast(&_FSnan_nid_postfix)) == 2); + Require(_LDenorm_nid_postfix._Long_double == std::numeric_limits::denorm_min()); + Require(_LInf_nid_postfix._Long_double == std::numeric_limits::infinity()); + Require(std::isnan(_LNan_nid_postfix._Long_double) && std::isnan(_LSnan_nid_postfix._Long_double)); +} + static void CheckMissingClassificationExports() { const float infinity = std::numeric_limits::infinity(); const float nan = std::numeric_limits::quiet_NaN(); @@ -1074,6 +1116,7 @@ static void CheckNoConversionIsPositiveZero() { int main() { CheckFloatClassification(); + CheckDinkumwareConstants(); CheckNoConversionIsPositiveZero(); CheckMissingClassificationExports(); CheckFloatMathExports(); From f0fd092678312e99783eee0e579491391fcd8738 Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:37:46 +0200 Subject: [PATCH 161/219] perf(agc): find a draw's highest index with a loop the compiler vectorizes HighestDrawIndex read each index through a branch on the index size and kept the maximum in a std::optional, so neither clang nor GCC vectorized it, and an indexed draw scans its whole index range when its range is not kept (fewer than Recorder::KeptIndexRangeIndices indices, or a range whose memory is not tracked). The scan now runs one loop per index type: the maximum without restart, or with restart the maximum of the indices masked by whether they are the restart index, plus an OR of those masks for whether any index was not a restart index. Results are unchanged. --- .../Graphics/include/VertexInput.hpp | 43 ++++++++++++------- .../prx/libSceAgcDriver/tests/Graphics.cpp | 18 ++++++++ 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp index 5c71753057..a8d93a9a85 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp @@ -363,23 +363,36 @@ inline std::optional ClampDrawIndices(std::span indice return highest; } +template +std::optional HighestDrawIndexOf(std::span indices, bool skipRestart) { + constexpr auto restartIndex = std::numeric_limits::max(); + const auto count = indices.size() / sizeof(TIndex); + const auto* data = indices.data(); + TIndex highest = 0; + if (!skipRestart) { + for (std::size_t i = 0; i < count; ++i) { + TIndex index; + std::memcpy(&index, data + i * sizeof(TIndex), sizeof(TIndex)); + highest = std::max(highest, index); + } + if (count == 0) return std::nullopt; + return highest; + } + TIndex kept = 0; + for (std::size_t i = 0; i < count; ++i) { + TIndex index; + std::memcpy(&index, data + i * sizeof(TIndex), sizeof(TIndex)); + const TIndex valid = index == restartIndex ? TIndex{0} : TIndex(~TIndex{0}); + highest = std::max(highest, static_cast(index & valid)); + kept |= valid; + } + if (kept == 0) return std::nullopt; + return highest; +} + inline std::optional HighestDrawIndex(std::span indices, std::uint32_t indexSize, bool skipRestart) { Require(indexSize == 2 || indexSize == 4, "unsupported index size"); - const auto restartIndex = indexSize == 2 ? 0xffffu : 0xffffffffu; - std::optional highest; - for (std::size_t offset = 0; offset + indexSize <= indices.size(); offset += indexSize) { - std::uint32_t index = 0; - if (indexSize == 2) { - std::uint16_t value = 0; - std::memcpy(&value, indices.data() + offset, sizeof(value)); - index = value; - } else { - std::memcpy(&index, indices.data() + offset, sizeof(index)); - } - if (skipRestart && index == restartIndex) continue; - highest = std::max(highest.value_or(0u), index); - } - return highest; + return indexSize == 2 ? HighestDrawIndexOf(indices, skipRestart) : HighestDrawIndexOf(indices, skipRestart); } inline std::vector MeshRestartTable(std::span indices, std::uint32_t indexSize) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index e28a78b241..bb4637404c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9506,6 +9506,24 @@ void highestDrawIndexTests() { Require(HighestDrawIndex(bytesOf(restartOnly), 2, false) == 0xffff, "a 16-bit all-ones index without restart was skipped"); Require(HighestDrawIndex(bytesOf(narrow).first(4), 2, true) == 7 && HighestDrawIndex(bytesOf(narrow).first(2), 2, true) == 0, "the scan read past its index range"); expectFailure([&] { HighestDrawIndex(bytesOf(narrow), 1, false); }, "unsupported index size"); + Require(!HighestDrawIndex({}, 2, false).has_value() && !HighestDrawIndex({}, 4, true).has_value(), "an empty index range reached a vertex"); + std::vector long16(1027, 0xffff); + long16[1026] = 0; + Require(HighestDrawIndex(std::as_bytes(std::span(long16)), 2, true) == 0, "index 0 after many restart indices was not reached"); + long16[513] = 4000; + Require(HighestDrawIndex(std::as_bytes(std::span(long16)), 2, true) == 4000, "a 16-bit index inside a long range was missed"); + std::vector long32(1029); + for (std::size_t i = 0; i < long32.size(); ++i) long32[i] = static_cast(i * 7u % 1000u); + long32[1028] = 0x80000001u; + Require(HighestDrawIndex(std::as_bytes(std::span(long32)), 4, true) == 0x80000001u, "a 32-bit index above 2^31 in the tail was missed"); + long32[1028] = 0xffffffffu; + Require(HighestDrawIndex(std::as_bytes(std::span(long32)), 4, true) == 999u && HighestDrawIndex(std::as_bytes(std::span(long32)), 4, false) == 0xffffffffu, "a 32-bit restart index at the end of a long range"); + std::vector shifted(2 * 1000 + 3); + for (std::size_t i = 0; i < 1000; ++i) { + const auto value = static_cast(i == 777 ? 0x8001u : i % 300u); + std::memcpy(shifted.data() + 1 + 2 * i, &value, sizeof(value)); + } + Require(HighestDrawIndex(std::span(shifted).subspan(1, 2 * 1000 + 1), 2, false) == 0x8001u, "an unaligned 16-bit range with a trailing byte"); } void storeAtFlipTests() { From 97d587f7305f8ecf247bcae08328efecb0a2c3a8 Mon Sep 17 00:00:00 2001 From: priyanshrv-netizen <275132450+priyanshrv-netizen@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:25:48 +0530 Subject: [PATCH 162/219] feat(libc): export the Dinkumware float characteristic tables Add _Flt, _Dbl and _Ldbl, the 80-byte tables behind Dinkumware's float.h macros, with the values the console's libc.prx exports. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libc/src/Math.cpp | 20 +++++++++++++++ core/libs/tests/GuestMath.cpp | 43 +++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 063eca4f01..ad2cb5ad85 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -640,6 +640,26 @@ int APS5_VABI __isfinitel_nid_postfix(long double x) { return (exponent & 0x7fffu) != 0x7fffu; } +struct DinkumwareFloatCharacteristics { + int digits; + int mantissaDigits; + int maxExponent10; + int maxExponent; + int minExponent10; + int minExponent; + alignas(16) std::uint16_t epsilon[8]; + alignas(16) std::uint16_t max[8]; + alignas(16) std::uint16_t min[8]; +}; +static_assert(sizeof(DinkumwareFloatCharacteristics) == 80 && offsetof(DinkumwareFloatCharacteristics, epsilon) == 32); + +DinkumwareFloatCharacteristics _Flt_nid_postfix {6, 24, 38, 128, -37, -125, + {0, 0x3400}, {0xffff, 0x7f7f}, {0, 0x0080}}; +DinkumwareFloatCharacteristics _Dbl_nid_postfix {15, 53, 308, 1024, -307, -1021, + {0, 0, 0, 0x3cb0}, {0xffff, 0xffff, 0xffff, 0x7fef}, {0, 0, 0, 0x0010}}; +DinkumwareFloatCharacteristics _Ldbl_nid_postfix {18, 64, 4932, 16384, -4931, -16381, + {0, 0, 0, 0x8000, 0x3fc0}, {0xffff, 0xffff, 0xffff, 0xffff, 0x7ffe}, {0, 0, 0, 0x8000, 0x0001}}; + static std::mutex g_randLock; static std::uint32_t g_randState = 1; diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 3edad3eb4e..6400f03b2f 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -2,8 +2,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -184,6 +186,20 @@ long double APS5_VABI truncl_nid_postfix(long double); long double APS5_VABI nearbyintl_nid_postfix(long double); double APS5_VABI logb_nid_postfix(double); double APS5_VABI remainder_nid_postfix(double, double); +struct DinkumwareFloatCharacteristics { + int digits; + int mantissaDigits; + int maxExponent10; + int maxExponent; + int minExponent10; + int minExponent; + alignas(16) unsigned char epsilon[16]; + alignas(16) unsigned char max[16]; + alignas(16) unsigned char min[16]; +}; +extern DinkumwareFloatCharacteristics _Flt_nid_postfix; +extern DinkumwareFloatCharacteristics _Dbl_nid_postfix; +extern DinkumwareFloatCharacteristics _Ldbl_nid_postfix; } static void Require(bool value) { if (!value) std::abort(); } @@ -1072,6 +1088,32 @@ static void CheckNoConversionIsPositiveZero() { Require(parsedWideLong == 0.L && !std::signbit(parsedWideLong) && wideEnd == wide); } +template +static void RequireCharacteristic(const unsigned char (&constant)[16], TValue expected) { + TValue value; + std::memcpy(&value, constant, sizeof(value)); + Require(value == expected); + for (std::size_t index = TBytes; index < 16; ++index) Require(constant[index] == 0); +} + +template +static void RequireCharacteristics(const DinkumwareFloatCharacteristics& table, const int (&integers)[6]) { + const int fields[] = {table.digits, table.mantissaDigits, table.maxExponent10, table.maxExponent, table.minExponent10, table.minExponent}; + for (int index = 0; index < 6; ++index) Require(fields[index] == integers[index]); + RequireCharacteristic(table.epsilon, std::numeric_limits::epsilon()); + RequireCharacteristic(table.max, std::numeric_limits::max()); + RequireCharacteristic(table.min, std::numeric_limits::min()); +} + +static void CheckDinkumwareFloatCharacteristics() { + static_assert(sizeof(DinkumwareFloatCharacteristics) == 80 && offsetof(DinkumwareFloatCharacteristics, epsilon) == 32); + RequireCharacteristics(_Flt_nid_postfix, {FLT_DIG, FLT_MANT_DIG, FLT_MAX_10_EXP, FLT_MAX_EXP, FLT_MIN_10_EXP, FLT_MIN_EXP}); + RequireCharacteristics(_Dbl_nid_postfix, {DBL_DIG, DBL_MANT_DIG, DBL_MAX_10_EXP, DBL_MAX_EXP, DBL_MIN_10_EXP, DBL_MIN_EXP}); + RequireCharacteristics(_Ldbl_nid_postfix, {18, 64, 4932, 16384, -4931, -16381}); + for (const auto* table : {&_Flt_nid_postfix, &_Dbl_nid_postfix, &_Ldbl_nid_postfix}) + for (std::size_t index = 24; index < 32; ++index) Require(reinterpret_cast(table)[index] == 0); +} + int main() { CheckFloatClassification(); CheckNoConversionIsPositiveZero(); @@ -1363,4 +1405,5 @@ int main() { CheckIntegerAbsAndDivision(); CheckScaling(); CheckLongDoubleFunctions(); + CheckDinkumwareFloatCharacteristics(); } From 602f6e1bdaca015adc0331334b8d035c32f3bbd7 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:00:38 +0200 Subject: [PATCH 163/219] fix(tools): report an unreadable cache in configure_seed Co-Authored-By: Claude Sonnet 5.5 --- tools/configure_seed.py | 6 +++++- tools/tests/test_configure_seed.py | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/tools/configure_seed.py b/tools/configure_seed.py index 465ad3b15d..d7c8218f67 100644 --- a/tools/configure_seed.py +++ b/tools/configure_seed.py @@ -37,7 +37,11 @@ def main(): cache, seed = Path(args.cache), Path(args.seed) if cache.resolve() == seed.resolve() or (cache.exists() and seed.exists() and cache.samefile(seed)): parser.error("seed output would overwrite the input cache") - found = {name: entry for name, entry in checks(cache).items() + try: + results = checks(cache) + except (OSError, UnicodeDecodeError) as error: + parser.error(f"{cache}: {error}") + found = {name: entry for name, entry in results.items() if (not args.include or any(fnmatch.fnmatchcase(name, pattern) for pattern in args.include)) and not any(fnmatch.fnmatchcase(name, pattern) for pattern in args.exclude)} seed.write_text("".join(f"set({quote(name)} {quote(value)} CACHE INTERNAL {quote(doc)})\n" diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py index 9c1b936b20..6064df202a 100644 --- a/tools/tests/test_configure_seed.py +++ b/tools/tests/test_configure_seed.py @@ -47,6 +47,15 @@ class ConfigureSeedPathTests(unittest.TestCase): self.assertEqual(self.cache.read_bytes(), self.original) self.assertEqual(Path(seed).read_bytes(), self.original) + def test_unreadable_cache_is_reported_without_a_traceback(self): + missing = self.root / "no cache dir" / "CMakeCache.txt" + result = subprocess.run([sys.executable, str(TOOL), str(missing), str(self.root / "seed.cmake")], + capture_output=True, text=True) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn(str(missing), result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertFalse((self.root / "seed.cmake").exists()) + def test_same_path_preserves_cache(self): self.assert_rejected(self.cache) From 07486d7cfabd8d9df29cdcafda7cdbf1b40a8859 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:05:08 +0200 Subject: [PATCH 164/219] fix(libkernel): report current thread scheduling policy in attributes --- .../libs/prx/libkernel/Pthread/Posix/Attr.cpp | 1 + core/libs/prx/libkernel/Pthread/src/Attr.cpp | 2 +- core/libs/tests/GuestPthreadSchedparam.cpp | 100 +++++++++++++----- docs/dev/debt/libkernel.md | 2 +- 4 files changed, 79 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp index 1ea13b5de0..86fd01c134 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp @@ -38,6 +38,7 @@ int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr) p->stackAddress = thread->stackAddress; p->_detachstate = thread->_detached ? 1 : 0; p->_schedpriority = thread->priority.load(std::memory_order_relaxed); + p->_schedpolicy = thread->schedPolicy.load(std::memory_order_relaxed); p->_affinity = thread->affinity.load(std::memory_order_relaxed); p->_affinityExplicit = true; p->_scope = thread->scope; diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index ea0eccba68..ce6ccac93c 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -83,7 +83,7 @@ int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr) { (*attr)->stackAddress = thread->stackAddress; (*attr)->_detachstate = thread->_detached ? DETACH_DETACHED : DETACH_JOINABLE; (*attr)->_schedpriority = thread->priority.load(std::memory_order_relaxed); - (*attr)->_schedpolicy = SCHED_FIFO_PS5; + (*attr)->_schedpolicy = thread->schedPolicy.load(std::memory_order_relaxed); (*attr)->_inheritsched = 4; (*attr)->_affinity = thread->affinity.load(std::memory_order_relaxed); (*attr)->_affinityExplicit = true; diff --git a/core/libs/tests/GuestPthreadSchedparam.cpp b/core/libs/tests/GuestPthreadSchedparam.cpp index ddafd9090e..986a1fd1d0 100644 --- a/core/libs/tests/GuestPthreadSchedparam.cpp +++ b/core/libs/tests/GuestPthreadSchedparam.cpp @@ -1,5 +1,6 @@ #include "SceTypes.hpp" #include +#include #include extern "C" { @@ -12,6 +13,10 @@ int APS5_VABI pthread_getschedparam_nid_postfix(Pthread thread, int* policy, Ker int APS5_VABI pthread_setschedparam_nid_postfix(Pthread thread, int policy, const KernelSchedParam* param); int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr); +int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr); +int APS5_VABI scePthreadAttrGetschedpolicy(const PthreadAttr* attr, int* policy); +int APS5_VABI scePthreadAttrGetschedparam(const PthreadAttr* attr, KernelSchedParam* param); int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy); int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); @@ -19,15 +24,33 @@ int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedPa static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003); static constexpr int GUEST_EINVAL = 22; -static constexpr int SCHED_FIFO = 1; -static constexpr int SCHED_OTHER = 2; -static constexpr int SCHED_RR = 3; +static constexpr int GUEST_SCHED_FIFO = 1; +static constexpr int GUEST_SCHED_OTHER = 2; +static constexpr int GUEST_SCHED_RR = 3; static constexpr int EXPLICIT_SCHED = 0; static constexpr int INHERIT_SCHED = 4; static void Require(bool value) { if (!value) std::abort(); } +static void RequireAttributes(Pthread thread, int policy, int priority) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + for (auto query : {scePthreadAttrGet, pthread_attr_get_np_nid_postfix}) { + for (int previous : {GUEST_SCHED_FIFO, GUEST_SCHED_RR}) { + Require(scePthreadAttrSetschedpolicy(&attr, previous) == SCE_OK); + Require(query(thread, &attr) == SCE_OK); + int current = -1; + KernelSchedParam param{}; + Require(scePthreadAttrGetschedpolicy(&attr, ¤t) == SCE_OK); + Require(scePthreadAttrGetschedparam(&attr, ¶m) == SCE_OK); + Require(current == policy && param.sched_priority == priority); + } + } + Require(scePthreadAttrDestroy(&attr) == SCE_OK); +} + static void RequireSched(int policy, int priority) { int current = -1; KernelSchedParam param{}; @@ -37,6 +60,7 @@ static void RequireSched(int policy, int priority) { param.sched_priority = -1; Require(pthread_getschedparam_nid_postfix(scePthreadSelf(), ¤t, ¶m) == 0); Require(current == policy && param.sched_priority == priority); + RequireAttributes(scePthreadSelf(), policy, priority); } static int SceSet(int policy, int priority) { @@ -53,36 +77,44 @@ static void* APS5_VABI Run(void*) { int policy = -1; KernelSchedParam param{}; Require(scePthreadGetschedparam(scePthreadSelf(), &policy, ¶m) == SCE_OK); - Require(policy == SCHED_FIFO); + Require(policy == GUEST_SCHED_FIFO); for (int priority : {256, 767, 896, 959}) { - Require(SceSet(SCHED_FIFO, priority) == SCE_OK); - RequireSched(SCHED_FIFO, priority); + Require(SceSet(GUEST_SCHED_FIFO, priority) == SCE_OK); + RequireSched(GUEST_SCHED_FIFO, priority); } for (int priority : {-1, 0, 255, 768, 895, 960, 1023, 4096}) { - Require(SceSet(SCHED_FIFO, priority) == SCE_KERNEL_ERROR_EINVAL); - Require(PosixSet(SCHED_FIFO, priority) == GUEST_EINVAL); - RequireSched(SCHED_FIFO, 959); + Require(SceSet(GUEST_SCHED_FIFO, priority) == SCE_KERNEL_ERROR_EINVAL); + Require(PosixSet(GUEST_SCHED_FIFO, priority) == GUEST_EINVAL); + RequireSched(GUEST_SCHED_FIFO, 959); } - Require(SceSet(SCHED_RR, 300) == SCE_OK); - RequireSched(SCHED_RR, 300); - for (int other : {-1, 0, SCHED_OTHER, 4}) { + Require(SceSet(GUEST_SCHED_RR, 300) == SCE_OK); + RequireSched(GUEST_SCHED_RR, 300); + for (int other : {-1, 0, GUEST_SCHED_OTHER, 4}) { Require(SceSet(other, 700) == SCE_KERNEL_ERROR_EINVAL); Require(PosixSet(other, 700) == GUEST_EINVAL); - RequireSched(SCHED_RR, 300); + RequireSched(GUEST_SCHED_RR, 300); } - Require(PosixSet(SCHED_FIFO, 700) == 0); - RequireSched(SCHED_FIFO, 700); - Require(scePthreadSetschedparam(scePthreadSelf(), SCHED_FIFO, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(PosixSet(GUEST_SCHED_FIFO, 700) == 0); + RequireSched(GUEST_SCHED_FIFO, 700); + Require(scePthreadSetschedparam(scePthreadSelf(), GUEST_SCHED_FIFO, nullptr) == SCE_KERNEL_ERROR_EINVAL); return nullptr; } +struct PolicyRequest { + std::promise ready; + std::future released; +}; + static void* APS5_VABI ReportPolicy(void* arg) { + auto& request = *static_cast(arg); int policy = -1; KernelSchedParam param{}; Require(scePthreadGetschedparam(scePthreadSelf(), &policy, ¶m) == SCE_OK); - *static_cast(arg) = policy; + RequireAttributes(scePthreadSelf(), policy, param.sched_priority); + request.ready.set_value(policy); + request.released.wait(); return nullptr; } @@ -91,10 +123,28 @@ static int CreatedPolicy(int inheritSched, int policy) { Require(scePthreadAttrInit(&attr) == SCE_OK); Require(scePthreadAttrSetinheritsched(&attr, inheritSched) == SCE_OK); Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_OK); - int reported = -1; + std::promise release; + PolicyRequest request{{}, release.get_future()}; + auto ready = request.ready.get_future(); Pthread thread = nullptr; - Require(scePthreadCreate(&thread, &attr, ReportPolicy, &reported, "policy") == SCE_OK); + Require(scePthreadCreate(&thread, &attr, ReportPolicy, &request, "policy") == SCE_OK); + const int reported = ready.get(); + int current = -1; + KernelSchedParam param{}; + Require(scePthreadGetschedparam(thread, ¤t, ¶m) == SCE_OK); + Require(current == reported); + RequireAttributes(thread, reported, param.sched_priority); + for (int updated : {GUEST_SCHED_RR, GUEST_SCHED_FIFO}) { + const KernelSchedParam requested{450}; + Require(scePthreadSetschedparam(thread, updated, &requested) == SCE_OK); + RequireAttributes(thread, updated, requested.sched_priority); + } + release.set_value(); Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(scePthreadAttrSetschedpolicy(&attr, GUEST_SCHED_RR) == SCE_OK); + Require(scePthreadAttrGet(thread, &attr) == SCE_KERNEL_ERROR_ESRCH); + current = -1; + Require(scePthreadAttrGetschedpolicy(&attr, ¤t) == SCE_OK && current == GUEST_SCHED_RR); Require(scePthreadAttrDestroy(&attr) == SCE_OK); return reported; } @@ -119,7 +169,7 @@ static int CreatedPriority(int inheritSched, const KernelSchedParam* param) { PthreadAttr attr = nullptr; Require(scePthreadAttrInit(&attr) == SCE_OK); Require(scePthreadAttrSetinheritsched(&attr, inheritSched) == SCE_OK); - Require(scePthreadAttrSetschedpolicy(&attr, SCHED_RR) == SCE_OK); + Require(scePthreadAttrSetschedpolicy(&attr, GUEST_SCHED_RR) == SCE_OK); if (param) Require(scePthreadAttrSetschedparam(&attr, param) == SCE_OK); const int reported = CreatedPriority(&attr); Require(scePthreadAttrDestroy(&attr) == SCE_OK); @@ -129,13 +179,15 @@ static int CreatedPriority(int inheritSched, const KernelSchedParam* param) { static void* APS5_VABI Spawn(void*) { const KernelSchedParam param{300}; for (int priority : {450, 600}) { - Require(SceSet(SCHED_FIFO, priority) == SCE_OK); + Require(SceSet(GUEST_SCHED_FIFO, priority) == SCE_OK); Require(CreatedPriority(INHERIT_SCHED, ¶m) == priority); Require(CreatedPriority(INHERIT_SCHED, nullptr) == priority); Require(CreatedPriority(nullptr) == priority); Require(CreatedPriority(EXPLICIT_SCHED, ¶m) == 300); Require(CreatedPriority(EXPLICIT_SCHED, nullptr) == 700); } + Require(SceSet(GUEST_SCHED_RR, 300) == SCE_OK); + Require(CreatedPolicy(INHERIT_SCHED, GUEST_SCHED_FIFO) == GUEST_SCHED_RR); return nullptr; } @@ -143,9 +195,9 @@ int main() { Pthread thread = nullptr; Require(scePthreadCreate(&thread, nullptr, Run, nullptr, "schedparam") == SCE_OK); Require(scePthreadJoin(thread, nullptr) == SCE_OK); - Require(CreatedPolicy(EXPLICIT_SCHED, SCHED_RR) == SCHED_RR); - Require(CreatedPolicy(EXPLICIT_SCHED, SCHED_FIFO) == SCHED_FIFO); - Require(CreatedPolicy(INHERIT_SCHED, SCHED_RR) == SCHED_FIFO); + Require(CreatedPolicy(EXPLICIT_SCHED, GUEST_SCHED_RR) == GUEST_SCHED_RR); + Require(CreatedPolicy(EXPLICIT_SCHED, GUEST_SCHED_FIFO) == GUEST_SCHED_FIFO); + Require(CreatedPolicy(INHERIT_SCHED, GUEST_SCHED_RR) == GUEST_SCHED_FIFO); Require(scePthreadCreate(&thread, nullptr, Spawn, nullptr, "spawn") == SCE_OK); Require(scePthreadJoin(thread, nullptr) == SCE_OK); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index c27dde1e57..eacd5671e7 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -110,7 +110,7 @@ ### Functional -- [Guest thread scheduling](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - the policies and priorities that `sched_get_priority_max`, `sched_get_priority_min`, `pthread_setschedparam` and `scePthreadSetschedparam` accept, and those a created thread takes, follow a PS5 (firmware 13.60), but they are only recorded and reported: they are not applied to host threads, so a title's priority order has no effect on host scheduling +- [Guest thread scheduling](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - the policies and priorities that `sched_get_priority_max`, `sched_get_priority_min`, `pthread_setschedparam` and `scePthreadSetschedparam` accept, and those a created thread takes, follow a PS5 (firmware 13.60), but they are only recorded and reported: they are not applied to host threads, so a title's priority order has no effect on host scheduling. `scePthreadAttrGet` and `pthread_attr_get_np` report the recorded policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164); the policy returned by these queries has not been measured on a console - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. - [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`), and the direct map calls refuse execute; here both throw for undefined bits, and the direct map calls map execute - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors maps the object as a [regular file](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp), so its views and the JIT views see each other's writes. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws From 2f2fd0f7a44677b02f22947d13ad2f1ca95d49e4 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:08:27 +0200 Subject: [PATCH 165/219] docs(libkernel): keep attribute query limits with attribute debt --- docs/dev/debt/libkernel.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index eacd5671e7..c976932d65 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -60,7 +60,7 @@ - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console - [sceKernelSetEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - multi-waiter satisfaction follows fpPS4 in-order evaluation: registered waiters are evaluated in FIFO order and each satisfied waiter's clear mode is applied immediately to the pattern before evaluating subsequent waiters; this ordering is unverified on PS5 console hardware - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written -- [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked +- [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked. `scePthreadAttrGet` and `pthread_attr_get_np` report the recorded policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164); the policy returned by these queries has not been measured on a console - [sceKernelGetAvailableCpumask](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed - [sceKernelGetCurrentCpu](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - guest affinities are not applied to host threads, so the host CPU index is mapped into the calling thread's affinity clipped to `sceKernelGetAvailableCpumask` (the available mask when they do not overlap): host CPU n reports the (n modulo the CPU count)-th CPU of that mask, so a thread pinned to one CPU reports that CPU, as a FreeBSD `cpuset` guarantees on the console. Which CPU the console reports for a thread allowed on several is not known; shadPS4 returns 0 - [pthread_attr_setsolosched_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) takes `(attr, int)` and stores the value through `scePthreadAttrSetsolosched`, returning `EINVAL` (22) for a null or uninitialized attribute like the other POSIX attribute setters. Only the name is public (aerolib `2+pVfgiEd7A`); the signature and behaviour follow the SCE twin, which [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/pthread.cpp#L2188-L2199) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs#L891-L913) store as an `int` without a range check, and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/71de7681f06c5db59a1120a3bb554c86780c3f5f/src/hle/libs/kernel_threading.zig#L1396-L1398) implements this alias over it. On a PS5 (firmware 13.60) only 0 and 16 are accepted; every other value returns `ENOTSUP` (45, `0x8002002D` from the SCE call) and keeps the stored one, and the SCE calls return `EINVAL` for a null or uninitialized attribute. PPSA15878 passes 16. Threads created from the attribute are not scheduled differently; what solo scheduling does on the console is unknown. Returnal (PPSA01285 v03.000.005) stopped on it with `what(): pthread_attr_setsolosched_np_nid_postfix not implemented` (#2872, #1155); LEGO Horizon Adventures (PPSA14632, #734) and Palworld (PPSA23126, #2598) import it @@ -110,7 +110,7 @@ ### Functional -- [Guest thread scheduling](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - the policies and priorities that `sched_get_priority_max`, `sched_get_priority_min`, `pthread_setschedparam` and `scePthreadSetschedparam` accept, and those a created thread takes, follow a PS5 (firmware 13.60), but they are only recorded and reported: they are not applied to host threads, so a title's priority order has no effect on host scheduling. `scePthreadAttrGet` and `pthread_attr_get_np` report the recorded policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164); the policy returned by these queries has not been measured on a console +- [Guest thread scheduling](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - the policies and priorities that `sched_get_priority_max`, `sched_get_priority_min`, `pthread_setschedparam` and `scePthreadSetschedparam` accept, and those a created thread takes, follow a PS5 (firmware 13.60), but they are only recorded and reported: they are not applied to host threads, so a title's priority order has no effect on host scheduling - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. - [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`), and the direct map calls refuse execute; here both throw for undefined bits, and the direct map calls map execute - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors maps the object as a [regular file](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp), so its views and the JIT views see each other's writes. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws From e1018840e30620708c5c75f24c45c91a0eb3c6b1 Mon Sep 17 00:00:00 2001 From: priyanshrv-netizen <275132450+priyanshrv-netizen@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:38:30 +0530 Subject: [PATCH 166/219] feat(libc): implement _Feraise Raise the requested floating-point exceptions on the x87 and SSE units and set errno as the console's libc.prx does. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libc/src/FloatingPoint.cpp | 18 ++++++++++++++++++ core/libs/tests/GuestFloatingPoint.cpp | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/core/libs/prx/libc/src/FloatingPoint.cpp b/core/libs/prx/libc/src/FloatingPoint.cpp index 304814503c..e279bc5eb2 100644 --- a/core/libs/prx/libc/src/FloatingPoint.cpp +++ b/core/libs/prx/libc/src/FloatingPoint.cpp @@ -50,6 +50,24 @@ int APS5_VABI fegetexceptflag_nid_postfix(std::uint16_t* flags, int exceptions) return 0; } +int* APS5_VABI __error_nid_postfix(); + +void APS5_VABI _Feraise_nid_postfix(int exceptions) { + constexpr unsigned Invalid = 0x01, Ranges = 0x1c, Overflows = 0x18, Inexact = 0x20; + constexpr int GuestEdom = 33, GuestErange = 34; + auto raised = static_cast(exceptions); + if ((raised & Overflows) != 0) raised |= Inexact; + *__error_nid_postfix() = GuestEdom; + std::array environment; + asm volatile("fnstenv %0" : "=m"(environment)); + environment[1] = (environment[1] & ~raised) | (raised & 0xffff); + asm volatile("fldenv %0" : : "m"(environment)); + _mm_setcsr((_mm_getcsr() & ~raised) | (raised & 0xffff)); + asm volatile("fwait"); + if ((raised & Invalid) != 0) *__error_nid_postfix() = GuestEdom; + else if ((raised & Ranges) != 0) *__error_nid_postfix() = GuestErange; +} + int APS5_VABI fesetexceptflag_nid_postfix(const std::uint16_t* flags, int exceptions) { if (flags == nullptr) throw std::invalid_argument("fesetexceptflag: null flags"); ReplaceFlags(static_cast(exceptions) & Exceptions, *flags); diff --git a/core/libs/tests/GuestFloatingPoint.cpp b/core/libs/tests/GuestFloatingPoint.cpp index 7b06f4dbe1..f5926abb7f 100644 --- a/core/libs/tests/GuestFloatingPoint.cpp +++ b/core/libs/tests/GuestFloatingPoint.cpp @@ -9,6 +9,8 @@ #include extern "C" { +int* APS5_VABI __error_nid_postfix(); +void APS5_VABI _Feraise_nid_postfix(int); int APS5_VABI fegetround_nid_postfix(); int APS5_VABI fesetround_nid_postfix(int); int APS5_VABI fetestexcept_nid_postfix(int); @@ -90,6 +92,21 @@ void CheckRounding() { Require(fegetround_nid_postfix() == 0x400, "query uses the x87 rounding mode"); } +void CheckFeraise() { + for (unsigned requested = 0; requested <= 0x3f; ++requested) { + const auto seeded = SeedFlags(0, 0); + *__error_nid_postfix() = 0; + _Feraise_nid_postfix(static_cast(requested)); + const unsigned raised = (requested & 0x18) != 0 ? requested | 0x20 : requested; + const auto state = ReadEnvironment(); + Require((state.x87[1] & 0x3f) == raised && (state.sse & 0x3f) == raised, "_Feraise raises the flags on both units"); + Require(state.x87[0] == seeded.x87[0] && (state.sse & ~0x3fu) == (seeded.sse & ~0x3fu), "_Feraise preserves control bits"); + const int error = (raised & 0x01) != 0 || (raised & 0x1c) == 0 ? 33 : 34; + Require(*__error_nid_postfix() == error, "_Feraise sets EDOM or ERANGE"); + } + SeedFlags(0, 0); +} + void CheckFlags() { const auto initial = SeedFlags(0x03, 0x24); Require(fetestexcept_nid_postfix(0) == 0, "empty exception query"); @@ -133,6 +150,7 @@ void CheckFlags() { } catch (const std::invalid_argument&) { threw = true; } Require(threw, "reject null exception storage"); } + CheckFeraise(); } void CheckThreads() { From b97b122f65ecc12e1b0070c9e8e9af92a2c7aab8 Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav Date: Tue, 20 Oct 2026 22:49:16 +0600 Subject: [PATCH 167/219] test(shader): isolate prebuilt cache directory discovery --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 16 ++++ .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 41 ++++++++++ .../test_shader_cache_prebuilt_discovery.py | 74 +++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/test_shader_cache_prebuilt_discovery.py diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 66a438c1fb..59b2ae42b5 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2136,6 +2136,22 @@ if(BUILD_TESTING) set_tests_properties(agc_shader_disk_cache_long_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(Python3_Interpreter_FOUND) + set(prebuiltDiscoveryModes plain relative plain-long) + if(WIN32) + list(APPEND prebuiltDiscoveryModes extended extended-forward device extended-long) + endif() + foreach(prebuiltDiscoveryMode IN LISTS prebuiltDiscoveryModes) + set(prebuiltDiscoveryTest agc_shader_prebuilt_discovery_${prebuiltDiscoveryMode}) + add_test(NAME ${prebuiltDiscoveryTest} COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_shader_cache_prebuilt_discovery.py + $ ${prebuiltDiscoveryMode}) + set_tests_properties(${prebuiltDiscoveryTest} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(${prebuiltDiscoveryTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endif() add_test(NAME agc_driver_large_tracked_snapshot COMMAND agc_driver_partially_resident_snapshot_tests --large-tracked) set_tests_properties(agc_driver_large_tracked_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 47917ecf45..975a855ddb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -633,6 +633,43 @@ void verifyPrebuiltAcrossProcesses(const char* self, const std::filesystem::path require(std::system(command.c_str()) == 0, "the prebuilt loading process failed"); } +void verifyPrebuiltDiscovery(std::string_view layout, bool seed) { + using namespace ShaderRecompiler::ShaderDiskCache; + require(layout == "pack" || layout == "loose" || layout == "versioned" || layout == "file" || layout == "empty" || layout == "missing", "unknown prebuilt discovery layout"); + Shutdown(); + SampleRequest sample = prebuiltSample(); + const auto key = sample.Key(); + const auto variant = sampleVariant(); + if (seed) { + const auto directory = ShaderRecompiler::ShaderCacheDirectory() / "prebuilt"; + const auto entry = EncodeEntry(key, variant); + if (layout == "pack") { + auto otherKey = key; + otherKey.back() ^= std::byte{1}; + require(WriteFileAtomically(directory / "a.pack", packBytes({{key, entry}})), "cannot seed the matching prebuilt pack"); + require(WriteFileAtomically(directory / "z.pack", packBytes({{otherKey, EncodeEntry(otherKey, variant)}})), "cannot seed the other prebuilt pack"); + } else if (layout == "loose") { + require(WriteFileAtomically(directory / EntryName(key), entry), "cannot seed the loose prebuilt entry"); + } else if (layout == "versioned") { + require(WriteFileAtomically(directory / EntryDirectory().filename() / EntryName(key), entry), "cannot seed the versioned prebuilt entry"); + } else if (layout == "file") { + require(WriteFileAtomically(directory, packBytes({{key, entry}})), "cannot seed the direct prebuilt pack"); + } else if (layout == "empty") { + require(WriteFileAtomically(directory / "ignored.txt", entry), "cannot seed the ignored prebuilt file"); + } + std::cout << "prebuilt discovery seeded " << layout << '\n'; + return; + } + CompiledVariant loaded; + const bool expected = layout != "empty" && layout != "missing"; + require(Load(key, loaded) == expected, "prebuilt discovery returned the wrong result for " + std::string(layout)); + if (expected) requireSameVariant(variant, loaded, "prebuilt discovery"); + const auto totals = Totals(); + require(totals.prebuiltHits == (expected ? 1u : 0u) && totals.hits == 0 && totals.writes == 0 && Writers() == 0, + "prebuilt discovery did not remain a read-only prebuilt lookup"); + std::cout << "prebuilt discovery verified " << layout << '\n'; +} + void verifyArtifactStorageIsolation() { SampleRequest sample; const auto key = sample.Key(); @@ -2433,6 +2470,10 @@ int main(int argc, char** argv) { return 0; } #endif + if (argc == 3 && (std::string_view(argv[1]) == "--seed-prebuilt-discovery" || std::string_view(argv[1]) == "--verify-prebuilt-discovery")) { + verifyPrebuiltDiscovery(argv[2], std::string_view(argv[1]) == "--seed-prebuilt-discovery"); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--large-file") return runLargeFileWrite(); if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess(); if (argc == 2 && std::string_view(argv[1]) == "--load-prebuilt") return runPrebuiltLoadingProcess(); diff --git a/core/libs/prx/libSceAgcDriver/tests/test_shader_cache_prebuilt_discovery.py b/core/libs/prx/libSceAgcDriver/tests/test_shader_cache_prebuilt_discovery.py new file mode 100644 index 0000000000..2b7523f8ab --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/test_shader_cache_prebuilt_discovery.py @@ -0,0 +1,74 @@ +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + + +def native(path): + return Path('\\\\?\\' + str(path)) if os.name == 'nt' else path + + +def snapshot(directory): + return {str(path.relative_to(directory)): None if path.is_dir() else path.read_bytes() + for path in directory.rglob('*')} + + +def main(): + executable = str(Path(sys.argv[1]).resolve()) + mode = sys.argv[2] + assert mode in ('plain', 'relative', 'extended', 'extended-forward', 'device', 'plain-long', 'extended-long') + assert os.name == 'nt' or mode in ('plain', 'relative', 'plain-long') + temporary = Path(tempfile.gettempdir()).resolve() + sandbox = Path(tempfile.mkdtemp(prefix='aps5-prebuilt-discovery-')).resolve() + try: + outside = sandbox / 'outside' + outside.mkdir() + (outside / 'z.pack').write_bytes(b'outside directory canary') + (outside / 'sentinel').write_bytes(b'unchanged outside the cache root') + original = snapshot(outside) + for layout in ('pack', 'loose', 'versioned', 'file', 'empty', 'missing'): + root = sandbox / (layout + '-\u00e4\u6f22') + if mode.endswith('long'): + while len(str(root)) < 270: + root /= 'd' * 40 + assert len(str(root / 'prebuilt')) >= 278 + root = native(root) + root.mkdir(parents=True) + plain = str(root)[4:] if os.name == 'nt' else str(root) + alias = plain + if mode.startswith('extended'): + alias = str(root) + if mode == 'extended-forward': + alias = '\\\\?\\' + plain.replace('\\', '/') + elif mode == 'device': + alias = '\\\\.\\' + plain + elif mode == 'relative': + alias = os.path.join('.', os.path.relpath(plain, outside)) + environment = dict(os.environ, ANYPS5_NO_SHADER_CACHE='0', ANYPS5_SHADER_CACHE_DIR=alias, + APS5_NO_PIPELINE_JOURNAL='1') + + def run(action): + try: + result = subprocess.run([executable, '--' + action + '-prebuilt-discovery', layout], cwd=outside, + env=environment, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, errors='replace', timeout=5) + except subprocess.TimeoutExpired as error: + raise AssertionError('prebuilt discovery did not return within five seconds; child terminated') from error + assert snapshot(outside) == original, 'prebuilt discovery changed files outside its root' + assert result.returncode == 0, mode + ' ' + layout + ':\n' + result.stdout + assert 'prebuilt discovery ' + ('seeded' if action == 'seed' else 'verified') + ' ' + layout in result.stdout + + run('seed') + before = snapshot(root) + run('verify') + assert snapshot(root) == before, 'prebuilt discovery modified its read-only input' + print(mode + ': six prebuilt layouts verified; cache and outside trees unchanged') + finally: + assert sandbox.parent == temporary and sandbox.name.startswith('aps5-prebuilt-discovery-') + shutil.rmtree(native(sandbox)) + + +if __name__ == '__main__': + main() From 2190ccd20c369fc40ae61a2cdf7beaa6a154c40d Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav Date: Tue, 20 Oct 2026 23:11:04 +0600 Subject: [PATCH 168/219] fix(shader): discover prebuilt caches through native Windows paths --- core/shader/recompiler/ShaderDiskCache.cpp | 26 +++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index b5bd3c4436..c75e6004f4 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -1475,17 +1475,33 @@ private: entries += pack->Count(); packs.push_back(std::move(pack)); }; - for (const auto& path : PrebuiltCachePaths(root)) { + for (const auto& candidate : PrebuiltCachePaths(root)) { std::error_code error; +#ifdef _WIN32 + const auto path = NativeCachePath_nid_no_patch(candidate, error); + if (error) continue; +#else + const auto& path = candidate; +#endif if (std::filesystem::is_regular_file(path, error)) { addPack(path); continue; } if (!std::filesystem::is_directory(path, error)) continue; bool loose = false; std::vector packPaths; - for (std::filesystem::directory_iterator it(path, error), end; !error && it != end; it.increment(error)) { - const auto extension = it->path().extension(); - if (extension == ".pack") packPaths.push_back(it->path()); - if (extension == ".bin" && it->path().stem().string().size() == 32) loose = true; + const auto collect = [&](const std::filesystem::path& entry) { + const auto extension = entry.extension(); + if (extension == ".pack") packPaths.push_back(entry); + if (extension == ".bin" && entry.stem().string().size() == 32) loose = true; + }; +#ifdef _WIN32 + WIN32_FIND_DATAW data{}; + const auto handle = FindFirstFileW((path / L"*").c_str(), &data); + if (handle != INVALID_HANDLE_VALUE) { + struct Search { HANDLE handle; ~Search() { FindClose(handle); } } search{handle}; + do { collect(path / data.cFileName); } while (FindNextFileW(handle, &data)); } +#else + for (std::filesystem::directory_iterator it(path, error), end; !error && it != end; it.increment(error)) collect(it->path()); +#endif std::sort(packPaths.begin(), packPaths.end()); for (const auto& item : packPaths) addPack(item); if (loose) looseDirectories.push_back(path); From 1b81383137eae9ee0f6854d9aab69cb0b9e88029 Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:08:58 +0200 Subject: [PATCH 169/219] perf(agc): probe the f32 denormal flush on MoltenVK The startup probe that lets shaders use the device's f32 denormal flush instead of the emulated one only ran on devices that declare shaderDenormFlushToZeroFloat32 with independent denormal behavior. MoltenVK declares neither flushing nor preserving f32 denormals, so every shader kept the emulated flush on every f32 add, multiply, fma, min, max and comparison. On MoltenVK the probe now runs too. Since the device declares no DenormFlushToZero capability there, a module compiled for the hardware flush declares no DenormFlushToZero execution mode and relies on the native behavior the probe measured; when any probe result differs from the emulated flush, shaders keep the emulation as before. Devices that declare the capability are unchanged. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 9 +++++ .../Execution/include/Float32FlushControl.hpp | 20 ++++++++++ .../Execution/src/VulkanDevice.cpp | 8 +++- .../tests/Float32FlushControl.cpp | 37 +++++++++++++++++++ .../tests/execution/F32DenormalFlush.cpp | 6 ++- .../src/SpirvMemory/SpirvModuleSetup.cpp | 2 +- 6 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/Float32FlushControl.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 66a438c1fb..96faf84dab 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -692,6 +692,15 @@ if(BUILD_TESTING) add_test(NAME agc_fragment_barycentric COMMAND agc_fragment_barycentric_tests) endif() +add_test_executable(agc_float32_flush_control_tests EXCLUDE_FROM_ALL tests/Float32FlushControl.cpp) +target_include_directories(agc_float32_flush_control_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +set_target_properties(agc_float32_flush_control_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_float32_flush_control_tests) +if(BUILD_TESTING) + set_target_properties(agc_float32_flush_control_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_float32_flush_control COMMAND agc_float32_flush_control_tests) +endif() + add_test_executable(agc_pipeline_link_optimization_tests EXCLUDE_FROM_ALL tests/PipelineLinkOptimization.cpp) target_include_directories(agc_pipeline_link_optimization_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) set_target_properties(agc_pipeline_link_optimization_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp new file mode 100644 index 0000000000..0d93c2c2a4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FLOAT32FLUSHCONTROL_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FLOAT32FLUSHCONTROL_HPP + +#include + +namespace AgcDriver { + +struct Float32FlushControl { + bool probe = false; + bool declared = false; +}; + +inline Float32FlushControl Float32FlushControlFor(VkDriverId driver, VkBool32 flushToZero, VkShaderFloatControlsIndependence independence) { + const bool declared = flushToZero == VK_TRUE && independence != VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE; + return {declared || driver == VK_DRIVER_ID_MOLTENVK, declared}; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index c19c2efa29..b9bde4a9aa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -15,6 +15,7 @@ #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/DeviceFault.hpp" #include "prx/libSceAgcDriver/Execution/include/FragmentBarycentric.hpp" +#include "prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp" #include "prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp" #include "prx/libSceAgcDriver/Execution/include/DrawInputsInPlace.hpp" #include "prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp" @@ -1161,10 +1162,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : std::vector availableExtensions(extensionCount); check(enumerateDeviceExtensions(selected, nullptr, &extensionCount, availableExtensions.data()), "vkEnumerateDeviceExtensionProperties"); const auto hasExtension = [&](const char* name) { return std::any_of(availableExtensions.begin(), availableExtensions.end(), [&](const auto& item) { return std::strcmp(item.extensionName, name) == 0; }); }; + VkDriverId driverId{}; if (hasExtension(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME)) { VkPhysicalDeviceDriverProperties driver{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES}; VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &driverProperties); + driverId = driver.driverID; state->driverInfo = std::string(driver.driverName) + " " + driver.driverInfo; state->individuallyFreeDescriptorSets = driver.driverID == VK_DRIVER_ID_MESA_LLVMPIPE; } @@ -1326,8 +1329,9 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : } state->float16DenormPreserve = floatControls.shaderDenormPreserveFloat16 == VK_TRUE && floatControls.denormBehaviorIndependence == VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL; - state->float32FlushToZeroCandidate = floatControls.shaderDenormFlushToZeroFloat32 == VK_TRUE && floatControls.denormBehaviorIndependence != VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE && std::getenv("APS5_NO_HW_F32_FLUSH") == nullptr; - if (state->float32FlushToZeroCandidate) state->capabilities.push_back(spv::CapabilityDenormFlushToZero); + const auto float32Flush = Float32FlushControlFor(driverId, floatControls.shaderDenormFlushToZeroFloat32, floatControls.denormBehaviorIndependence); + state->float32FlushToZeroCandidate = float32Flush.probe && std::getenv("APS5_NO_HW_F32_FLUSH") == nullptr; + if (state->float32FlushToZeroCandidate && float32Flush.declared) state->capabilities.push_back(spv::CapabilityDenormFlushToZero); if (state->float16DenormPreserve && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve); deviceExtensions.push_back(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); deviceExtensions.push_back(VK_KHR_8BIT_STORAGE_EXTENSION_NAME); diff --git a/core/libs/prx/libSceAgcDriver/tests/Float32FlushControl.cpp b/core/libs/prx/libSceAgcDriver/tests/Float32FlushControl.cpp new file mode 100644 index 0000000000..f51d28a2bf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Float32FlushControl.cpp @@ -0,0 +1,37 @@ +#include "prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp" +#include + +namespace { + +struct Case { + VkDriverId driver; + VkBool32 flushToZero; + VkShaderFloatControlsIndependence independence; + bool probe; + bool declared; +}; + +constexpr Case Cases[]{ + {VK_DRIVER_ID_MOLTENVK, VK_FALSE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE, true, false}, + {VK_DRIVER_ID_MOLTENVK, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, true, true}, + {VK_DRIVER_ID_MESA_RADV, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, true, true}, + {VK_DRIVER_ID_MESA_RADV, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_32_BIT_ONLY, true, true}, + {VK_DRIVER_ID_MESA_RADV, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE, false, false}, + {VK_DRIVER_ID_NVIDIA_PROPRIETARY, VK_FALSE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, false, false}, + {VK_DRIVER_ID_AMD_PROPRIETARY, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_32_BIT_ONLY, true, true}, + {static_cast(0), VK_FALSE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE, false, false}, +}; + +} + +int main() { + int failures = 0; + for (const auto& item : Cases) { + const auto control = AgcDriver::Float32FlushControlFor(item.driver, item.flushToZero, item.independence); + if (control.probe != item.probe || control.declared != item.declared) { + std::fprintf(stderr, "driver %d flush %u independence %d: expected probe %d declared %d, got %d %d\n", static_cast(item.driver), static_cast(item.flushToZero), static_cast(item.independence), item.probe ? 1 : 0, item.declared ? 1 : 0, control.probe ? 1 : 0, control.declared ? 1 : 0); + ++failures; + } + } + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp index 6aa88b0a7d..19beb4db4c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp @@ -505,13 +505,15 @@ int main() { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; const auto capabilities = device->Target().supportedCapabilities; - const bool hardware = std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityDenormFlushToZero)) != capabilities.end(); + const bool declared = std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityDenormFlushToZero)) != capabilities.end(); + const bool native = !declared && device->Target().float32FlushToZero; + const bool hardware = declared || native; if (hardware) { bool softwareDeclares = true; bool hardwareDeclares = false; const auto software = CompiledWords(*device, false, softwareDeclares); const auto hardwareWords = CompiledWords(*device, true, hardwareDeclares); - Require(!softwareDeclares && hardwareDeclares, "f32 denormal flush: only the hardware-flush module declares DenormFlushToZero 32"); + Require(!softwareDeclares && hardwareDeclares == declared, declared ? "f32 denormal flush: only the hardware-flush module declares DenormFlushToZero 32" : "f32 denormal flush: a module flushed natively declares DenormFlushToZero 32 without the device capability"); Require(hardwareWords < software, "f32 denormal flush: the hardware-flush module is " + std::to_string(hardwareWords) + " words, the emulated one " + std::to_string(software)); } for (const bool flush : {false, true}) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 9bbf4fb15b..3e437d8187 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -341,7 +341,7 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitCapability(spv::CapabilitySignedZeroInfNanPreserve); state.module.EmitExtension("SPV_KHR_float_controls"); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeSignedZeroInfNanPreserve, 32u); - if (state.float32FlushToZero) { + if (state.float32FlushToZero && std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityDenormFlushToZero)) != state.supportedCapabilities.end()) { state.module.EmitCapability(spv::CapabilityDenormFlushToZero); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDenormFlushToZero, 32u); } From ad1650686c0686bd6971eda5a7e40c74cc319279 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:18:49 +0200 Subject: [PATCH 170/219] docs(libkernel): keep unmeasured policy query behavior concise --- docs/dev/debt/libkernel.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index c976932d65..8b7289e0c9 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -60,7 +60,8 @@ - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console - [sceKernelSetEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - multi-waiter satisfaction follows fpPS4 in-order evaluation: registered waiters are evaluated in FIFO order and each satisfied waiter's clear mode is applied immediately to the pattern before evaluating subsequent waiters; this ordering is unverified on PS5 console hardware - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written -- [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked. `scePthreadAttrGet` and `pthread_attr_get_np` report the recorded policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164); the policy returned by these queries has not been measured on a console +- [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked +- [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the returned scheduling policy is unmeasured on a console. Both report the recorded guest policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164) - [sceKernelGetAvailableCpumask](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed - [sceKernelGetCurrentCpu](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - guest affinities are not applied to host threads, so the host CPU index is mapped into the calling thread's affinity clipped to `sceKernelGetAvailableCpumask` (the available mask when they do not overlap): host CPU n reports the (n modulo the CPU count)-th CPU of that mask, so a thread pinned to one CPU reports that CPU, as a FreeBSD `cpuset` guarantees on the console. Which CPU the console reports for a thread allowed on several is not known; shadPS4 returns 0 - [pthread_attr_setsolosched_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) takes `(attr, int)` and stores the value through `scePthreadAttrSetsolosched`, returning `EINVAL` (22) for a null or uninitialized attribute like the other POSIX attribute setters. Only the name is public (aerolib `2+pVfgiEd7A`); the signature and behaviour follow the SCE twin, which [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/pthread.cpp#L2188-L2199) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs#L891-L913) store as an `int` without a range check, and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/71de7681f06c5db59a1120a3bb554c86780c3f5f/src/hle/libs/kernel_threading.zig#L1396-L1398) implements this alias over it. On a PS5 (firmware 13.60) only 0 and 16 are accepted; every other value returns `ENOTSUP` (45, `0x8002002D` from the SCE call) and keeps the stored one, and the SCE calls return `EINVAL` for a null or uninitialized attribute. PPSA15878 passes 16. Threads created from the attribute are not scheduled differently; what solo scheduling does on the console is unknown. Returnal (PPSA01285 v03.000.005) stopped on it with `what(): pthread_attr_setsolosched_np_nid_postfix not implemented` (#2872, #1155); LEGO Horizon Adventures (PPSA14632, #734) and Palworld (PPSA23126, #2598) import it From 73784e20509768117fa99daa97b15929456a020e Mon Sep 17 00:00:00 2001 From: mantavisual <165268599+mantavisual@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:20:20 +0300 Subject: [PATCH 171/219] fix(libkernel): leave a file's times untouched when Windows utimes refuses them On Windows, utimes, lutimes, sceKernelUtimes and futimes open a FILE_WRITE_ATTRIBUTES handle and only then encode the times. A time the host cannot represent returned EINVAL without calling SetFileTime, and with NTFS last-access updates enabled closing that handle set the file's access time to the current time, so a rejected call still changed the file and guest_file_timestamps failed on "access time preserves microseconds". A rejected time now marks the handle with 0xFFFFFFFF, which SetFileTime documents as preventing operations on the handle from updating the times, before the handle is closed. Error order is unchanged. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/File/src/Stdio.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 5e7f29b807..a8124a0976 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -163,6 +163,8 @@ static int SetTimes(HANDLE handle, const KernelTimeval* times) { GetSystemTimePreciseAsFileTime(&access); modified = access; } else if (!File::WindowsFileTime::Encode(times[0], access) || !File::WindowsFileTime::Encode(times[1], modified)) { + const FILETIME unchanged{0xFFFFFFFF, 0xFFFFFFFF}; + SetFileTime(handle, nullptr, &unchanged, &unchanged); errno = EINVAL; return -1; } From 3fb021f9bbd90dfee89eb88baa289a7cb4c54c8a Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:17:28 +0200 Subject: [PATCH 172/219] fix(libSceNet): map every host socket error through the guest errno table sceNet translated only 21 host socket errors and reported every other one as EIO. libkernel already owns the complete FreeBSD table in GuestSockets::GuestErrorFromHost, so use it for net_error. The new check makes a stream socket with the UDP protocol fail with EPROTONOSUPPORT (43) instead of EIO (5). Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceNet/Export.cpp | 58 ++---------------------------- core/libs/tests/GuestSceNet.cpp | 5 +++ 2 files changed, 8 insertions(+), 55 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index bd4752bdb6..6a679a251f 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -39,6 +39,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestHeap.hpp" +#include "prx/libkernel/Socket/include/SocketError.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/Time/include/Time.hpp" @@ -120,32 +121,7 @@ bool initialize_sockets() { return result == 0; } void close_socket(NativeSocket socket) { closesocket(socket); } -int net_error(int code) { - switch (code) { - case WSAEWOULDBLOCK: return NET_EAGAIN; - case WSAEINPROGRESS: return NET_EINPROGRESS; - case WSAEALREADY: return NET_EALREADY; - case WSAEADDRINUSE: return NET_EADDRINUSE; - case WSAEADDRNOTAVAIL: return NET_EADDRNOTAVAIL; - case WSAEAFNOSUPPORT: return NET_EAFNOSUPPORT; - case WSAENETUNREACH: return NET_ENETUNREACH; - case WSAEHOSTUNREACH: return NET_EHOSTUNREACH; - case WSAECONNABORTED: return NET_ECONNABORTED; - case WSAENOBUFS: return NET_ENOBUFS; - case WSAEISCONN: return NET_EISCONN; - case WSAENOTCONN: return NET_ENOTCONN; - case WSAEACCES: return NET_EACCES; - case WSAECONNRESET: return 54; - case WSAETIMEDOUT: return NET_ETIMEDOUT; - case WSAECONNREFUSED: return NET_ECONNREFUSED; - case WSAEMSGSIZE: return NET_EMSGSIZE; - case WSAEINTR: return 4; - case WSAEINVAL: return NET_EINVAL; - case WSAENOTSOCK: return NET_ENOTSOCK; - case WSAESHUTDOWN: return 32; - default: return 5; - } -} +int net_error(int code) { return GuestSockets::GuestErrorFromHost(code); } int native_error() { return net_error(WSAGetLastError()); } #else using NativeSocket = int; @@ -154,35 +130,7 @@ constexpr NativeSocket INVALID_NATIVE_SOCKET = -1; constexpr int NATIVE_SEND_FLAGS = MSG_NOSIGNAL; bool initialize_sockets() { return true; } void close_socket(NativeSocket socket) { ::close(socket); } -int net_error(int code) { - switch (code) { - case EAGAIN: return NET_EAGAIN; -#if EWOULDBLOCK != EAGAIN - case EWOULDBLOCK: return NET_EAGAIN; -#endif - case EINPROGRESS: return NET_EINPROGRESS; - case EALREADY: return NET_EALREADY; - case EADDRINUSE: return NET_EADDRINUSE; - case EADDRNOTAVAIL: return NET_EADDRNOTAVAIL; - case EAFNOSUPPORT: return NET_EAFNOSUPPORT; - case ENETUNREACH: return NET_ENETUNREACH; - case EHOSTUNREACH: return NET_EHOSTUNREACH; - case ECONNABORTED: return NET_ECONNABORTED; - case ENOBUFS: return NET_ENOBUFS; - case EISCONN: return NET_EISCONN; - case ENOTCONN: return NET_ENOTCONN; - case EACCES: return NET_EACCES; - case ECONNRESET: return 54; - case ETIMEDOUT: return NET_ETIMEDOUT; - case ECONNREFUSED: return NET_ECONNREFUSED; - case EMSGSIZE: return NET_EMSGSIZE; - case EINTR: return 4; - case EINVAL: return NET_EINVAL; - case ENOTSOCK: return NET_ENOTSOCK; - case EPIPE: return 32; - default: return 5; - } -} +int net_error(int code) { return GuestSockets::GuestErrorFromHost(code); } int native_error() { return net_error(errno); } #endif diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 6e2b621d33..00cba79a65 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -145,6 +145,10 @@ static bool Failed(std::int64_t result, int error) { return result == static_cast(0x80410100u | static_cast(error)) && *sceNetErrnoLoc() == error; } +static void CheckSocketHostErrors() { + Require(Failed(sceNetSocket(nullptr, 2, 1, 17), 43)); +} + static void CheckStreamSender(int family) { const auto addressSize = family == 2 ? 16u : 28u; const int listener = sceNetSocket(nullptr, family, 1, 6); @@ -1431,6 +1435,7 @@ int main() { } Require(sceNetInit_nid_postfix() == 0); CheckEpollWritable(); + CheckSocketHostErrors(); CheckPoolStats(); CheckIfName(); CheckRandom(); From 084dcf87363a96d42d92545a95f791e0619cb775 Mon Sep 17 00:00:00 2001 From: Ethan Barr Date: Tue, 20 Oct 2026 13:32:17 -0400 Subject: [PATCH 173/219] fix(libkernel): fire EVFILT_WRITE on a socket once free send space reaches NOTE_LOWAT kevent threw NotImplemented for EVFILT_WRITE with NOTE_LOWAT above 1 on a socket, although the socket readiness check already reports free send-buffer space (SO_SNDBUF less SIOCOUTQ on Linux). The write event now fires once that space reaches the mark, as FreeBSD 11's filt_sowrite compares sbspace with kn_sdata, and stays silent while the space is below it. The debt entry no longer says a write mark is limited to 1. Reference: https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c --- core/libs/prx/libkernel/Equeue/src/Kqueue.cpp | 3 -- core/libs/tests/GuestKqueue.cpp | 53 ++++++++++++++++++- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index 8fed47f8ff..0e7f1e05e0 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -134,9 +134,6 @@ std::int64_t DescriptorLowWater(const KernelEvent& change) { if (change.data < 1) { NotImplemented_nid_no_patch("kevent: NOTE_LOWAT below 1 on a socket"); } - if (change.filter == EVFILT_WRITE && change.data > 1) { - NotImplemented_nid_no_patch("kevent: NOTE_LOWAT above 1 on EVFILT_WRITE"); - } if (change.data > 1 && GuestSockets::Type(static_cast(change.ident)) != 1) { NotImplemented_nid_no_patch("kevent: NOTE_LOWAT above 1 on a socket that is not a stream"); } diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index aa199d6bd0..2ff23394f4 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -386,7 +386,14 @@ static void LowWaterMarks() { Require(Poll(kq, events, 4) == 0); Require(Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvAdd | EvOneshot, NoteLowat, 1)) == 0); Require(Wait(kq, events, 4) == 1 && events[0].filter == EvfiltWrite); - Require(Throws([kq, &pair] { Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvAdd, NoteLowat, 2)); })); + const auto writer = static_cast(pair[1]); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, 2)) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].ident == writer && events[0].filter == EvfiltWrite && events[0].data >= 2); + const auto freeSpace = events[0].data; + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, freeSpace)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].ident == writer && events[0].data == freeSpace); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, freeSpace + 1)) == 0); + Require(Poll(kq, events, 4) == 0); Require(Throws([kq, reader] { Apply(kq, Change(reader, EvfiltRead, EvAdd, NoteLowat, 0)); })); int datagrams[2]; Require(socketpair_nid_postfix(Unix, 2, 0, datagrams) == 0); @@ -503,6 +510,49 @@ static void EdgeTriggeredSocketWrite() { Require(close_nid_postfix(kq) == 0); } +static void SocketWriteLowWaterWhileFilling() { + const int kq = kqueue_nid_postfix(); + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + const auto writer = static_cast(pair[1]); + KernelEvent events[4]{}; + Require(fcntl_nid_postfix(pair[1], SetStatusFlags, NonBlocking) == 0); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, 2)) == 0); + Require(Poll(kq, events, 4) == 1); + const auto empty = events[0].data; + const std::string chunk(4096, 'x'); + std::int64_t queued = 0; + Require(send_nid_postfix(pair[1], chunk.data(), chunk.size(), 0) == static_cast(chunk.size())); + queued += static_cast(chunk.size()); + Require(Poll(kq, events, 4) == 1 && events[0].data > 0 && events[0].data < empty); + const auto partial = events[0].data; + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, partial)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == partial); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, partial + 1)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, empty)) == 0); + Require(Poll(kq, events, 4) == 0); + for (;;) { + const std::int64_t written = send_nid_postfix(pair[1], chunk.data(), chunk.size(), 0); + if (written < 0) { + Require(*__error_nid_postfix() == GuestEagain); + break; + } + queued += written; + } + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, 2)) == 0); + Require(Poll(kq, events, 4) == 0); + std::string drained(chunk.size(), '\0'); + while (queued > 0) { + const std::int64_t received = recv_nid_postfix(pair[0], drained.data(), drained.size(), 0); + Require(received > 0); + queued -= received; + } + Require(Poll(kq, events, 4) == 1 && events[0].data == empty); + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + Require(close_nid_postfix(kq) == 0); +} + static void EdgeTriggeredPipeRead() { const int kq = kqueue_nid_postfix(); int pipe[2]; @@ -966,6 +1016,7 @@ int main() { #ifdef __linux__ EdgeTriggeredSocketRead(); EdgeTriggeredSocketWrite(); + SocketWriteLowWaterWhileFilling(); EdgeTriggeredPipeRead(); EdgeTriggeredDispatch(); #endif diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index c27dde1e57..b3198abbee 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -114,7 +114,7 @@ - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. - [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`), and the direct map calls refuse execute; here both throw for undefined bits, and the direct map calls map execute - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors maps the object as a [regular file](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp), so its views and the JIT views see each other's writes. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws -- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end and `EVFILT_WRITE` on the write end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters and a read event on the host stream on descriptor 0, which never triggers, as `select` never reports descriptor 0 readable; PPSA34015 registers descriptor 0 for reading in a new kqueue at start-up and does not check the result; other filters, `EV_CLEAR` on a socket or a pipe off Linux (see the kevent EV_CLEAR entry), `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event takes a mark of 1 only, where the host's writability stands in for one free byte, since the host does not report the send buffer's free space; other write marks and marks below 1, which make FreeBSD's read filter always fire, throw +- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end and `EVFILT_WRITE` on the write end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters and a read event on the host stream on descriptor 0, which never triggers, as `select` never reports descriptor 0 readable; PPSA34015 registers descriptor 0 for reading in a new kqueue at start-up and does not check the result; other filters, `EV_CLEAR` on a socket or a pipe off Linux (see the kevent EV_CLEAR entry), `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event fires once the send buffer's free space reaches the mark, as `filt_sowrite` does with `sbspace`; the host reports that space as `SO_SNDBUF` less `SIOCOUTQ` on Linux and as `SO_SNDBUF` alone elsewhere, so off Linux it does not shrink while data is queued. Marks below 1, which make FreeBSD's read filter always fire, throw - [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title (feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT) (fix(libkernel): follow kqueue_register for EV_FORCEONESHOT and modify flags) - [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. - [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here From eaadb2924bf054da6842e86db25ce5fdd40dee5d Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:32:41 +0200 Subject: [PATCH 174/219] fix(libkernel): report the starting entry index in getdirentries basep on Windows The Windows sceKernelGetdirentries stored 0 in *basep on every read, so a guest looping on the cookie saw it stall (FreeBSD stores the directory offset the read started at, kern_getdirentries: *basep = loff). Report the index of the first entry of the read, the same unit lseek takes on these descriptors, and run guest_directory_entries on Windows, where it was never built. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 17 +++++++++++------ .../File/include/DirectoryDescriptor.hpp | 2 +- .../libkernel/File/src/DirectoryDescriptor.cpp | 3 ++- core/libs/prx/libkernel/File/src/Stdio.cpp | 3 +-- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index a2330a807a..c5d398bcab 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1649,12 +1649,17 @@ if(BUILD_TESTING) set_tests_properties(guest_directory_rewind PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - if(NOT WIN32) - add_test_executable(guest_directory_entry_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryEntries.cpp) - target_include_directories(guest_directory_entry_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_directory_entry_tests PRIVATE libkernel libc) - add_test(NAME guest_directory_entries COMMAND guest_directory_entry_tests) - else() + add_test_executable(guest_directory_entry_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryEntries.cpp) + target_include_directories(guest_directory_entry_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_directory_entry_tests PRIVATE libkernel libc) + add_test(NAME guest_directory_entries COMMAND guest_directory_entry_tests) + if(WIN32) + configure_windows_unwind(guest_directory_entry_tests) + set_tests_properties(guest_directory_entries PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + if(MINGW) + target_compile_options(guest_directory_entry_tests PRIVATE -fasynchronous-unwind-tables) + endif() add_test_executable(guest_directory_close_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryClose.cpp) target_include_directories(guest_directory_close_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_directory_close_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp index 0cf519fb69..6978a7998a 100644 --- a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -20,7 +20,7 @@ std::optional DirectoryDescriptorPath(int fd); void ForgetDirectoryDescriptor(int fd); void DuplicateDirectoryDescriptor(int from, int to); std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, int whence); -int ReadDirectoryDescriptor(int fd, char* buf, int nbytes); +int ReadDirectoryDescriptor(int fd, char* buf, int nbytes, std::int64_t* base = nullptr); } diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index cca49c2431..7727bb9000 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -153,12 +153,13 @@ std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, return target; } -int ReadDirectoryDescriptor(int fd, char* buf, int nbytes) { +int ReadDirectoryDescriptor(int fd, char* buf, int nbytes, std::int64_t* base) { const auto found = Find(fd); if (!found) return SCE_KERNEL_ERROR_ENOTDIR; auto& state = *found; std::lock_guard lock(state.mutex); Load(state, fd); + if (base != nullptr) *base = static_cast(state.cursor); std::size_t used = 0; while (state.cursor < state.entries.size()) { const auto& [name, type] = state.entries[state.cursor]; diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 5e7f29b807..22a4ce3d0b 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1099,8 +1099,7 @@ int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* bas const GuestArena::HostWrite destination(buf, static_cast(nbytes)); const GuestArena::HostWrite baseDestination(basep, basep == nullptr ? 0 : sizeof(*basep)); if (!destination.Open() || !baseDestination.Open()) return SceErrorFromErrno(GUEST_EFAULT); - if (basep != nullptr) *basep = 0; - return File::ReadDirectoryDescriptor(fd, buf, nbytes); + return File::ReadDirectoryDescriptor(fd, buf, nbytes, basep); } int APS5_VABI sceKernelGetdents(int fd, char* buf, int nbytes) { From 2299284a69bc2334e1388b2d4c63895f1e461cc0 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:41:40 +0000 Subject: [PATCH 175/219] fix(libkernel): keep socket descriptors within FD_SETSIZE --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 30 +++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index d316c050a4..43dd7a53fc 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -68,6 +68,7 @@ using NativeSocket = int; constexpr auto Invalid = -1; #endif int Fail(int error) { *__error_nid_postfix() = error; return -1; } +constexpr int GuestSocketDescriptorLimit = 1024; constexpr int GuestPeek = 0x2; constexpr int GuestDontWait = 0x80; constexpr int GuestNoSignal = 0x20000; @@ -181,9 +182,14 @@ int ReserveNumber(NativeSocket) { ::CloseHandle(placeholder); return Fail(24); } + if (number >= GuestSocketDescriptorLimit) { + ::_close(number); + return Fail(24); + } return number; } int CopyNumber(int number, int minimum = 0) { + if (minimum >= GuestSocketDescriptorLimit) return Fail(24); std::vector below; int copy = -1; int error = 0; @@ -196,9 +202,15 @@ int CopyNumber(int number, int minimum = 0) { error = 12; } for (const int temporary : below) ::_close(temporary); - return copy < 0 ? Fail(error) : copy; + if (copy < 0) return Fail(error); + if (copy >= GuestSocketDescriptorLimit) { + ::_close(copy); + return Fail(24); + } + return copy; } int CopyNumberTo(int number, int target) { + if (target >= GuestSocketDescriptorLimit) return Fail(24); return ::_dup2(number, target) != 0 ? Fail(errno == EMFILE ? 24 : 9) : 0; } void ReleaseNumber(int number) { @@ -207,13 +219,25 @@ void ReleaseNumber(int number) { #else int ReserveNumber(NativeSocket native) { const int number = ::fcntl(native, F_DUPFD_CLOEXEC, 0); - return number < 0 ? Fail(NativeError()) : number; + if (number < 0) return Fail(NativeError()); + if (number >= GuestSocketDescriptorLimit) { + ::close(number); + return Fail(24); + } + return number; } int CopyNumber(int number, int minimum = 0) { + if (minimum >= GuestSocketDescriptorLimit) return Fail(24); const int copy = ::fcntl(number, F_DUPFD_CLOEXEC, minimum); - return copy < 0 ? Fail(NativeError()) : copy; + if (copy < 0) return Fail(NativeError()); + if (copy >= GuestSocketDescriptorLimit) { + ::close(copy); + return Fail(24); + } + return copy; } int CopyNumberTo(int number, int target) { + if (target >= GuestSocketDescriptorLimit) return Fail(24); return ::dup2(number, target) < 0 ? Fail(NativeError()) : 0; } void ReleaseNumber(int number) { From 2e4e1a5113a213e27ab0e61dfbfd790a9c0faefe Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:41:40 +0000 Subject: [PATCH 176/219] test(libkernel): cover socket descriptor exhaustion --- core/libs/tests/GuestSocketDescriptors.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core/libs/tests/GuestSocketDescriptors.cpp b/core/libs/tests/GuestSocketDescriptors.cpp index 632f528ccf..aa95e766a3 100644 --- a/core/libs/tests/GuestSocketDescriptors.cpp +++ b/core/libs/tests/GuestSocketDescriptors.cpp @@ -42,6 +42,7 @@ int APS5_VABI close_nid_postfix(int); int APS5_VABI sceKernelClose(int); std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); int APS5_VABI dup_nid_postfix(int); +int APS5_VABI dup2_nid_postfix(int, int); int APS5_VABI open_nid_postfix(const char*, int, int); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); @@ -181,6 +182,25 @@ static void DuplicateKeepsSocket() { Require(close_nid_postfix(sender) == 0); } +static void SocketNumbersStayInDescriptorSet() { + const int socket = Socket(); + std::vector files; + for (;;) { + const int file = open_nid_postfix("/dev/urandom", 0, 0); + Require(file >= 0); + if (file >= DescriptorSetSize) { + Require(close_nid_postfix(file) == 0); + break; + } + files.push_back(file); + } + Require(dup_nid_postfix(socket) == -1 && *__error_nid_postfix() == 24); + Require(socket_nid_postfix(Inet, Datagram, 0) == -1 && *__error_nid_postfix() == 24); + Require(dup2_nid_postfix(socket, DescriptorSetSize) == -1 && *__error_nid_postfix() == 24); + for (const int file : files) Require(close_nid_postfix(file) == 0); + Require(close_nid_postfix(socket) == 0); +} + static void SocketTypeErrors() { constexpr int PrototypeError = 41; constexpr int ProtocolError = 43; @@ -202,5 +222,6 @@ int main() { SelectSocketAndFile(); CloseReleasesNumber(); DuplicateKeepsSocket(); + SocketNumbersStayInDescriptorSet(); return 0; } From cc69253830aad335e44562dcb4693ca3aa184e30 Mon Sep 17 00:00:00 2001 From: javierUDP Date: Mon, 19 Oct 2026 14:09:14 -0300 Subject: [PATCH 177/219] fix(relinker): keep the interpreter path out of the scanned code The code analysis trims an interpreter path from the start of an executable segment only for images that have a PT_SCE_DYNLIBDATA block. An executable without one keeps that path in the scanned range, so /libexec/ld-elf.so.1 is walked as code: a RELATIVE relocation with addend 0 roots the control flow graph at the first byte of the segment, and the 0x64 of "ld-elf" decodes as an FS-prefixed instruction that the Windows and macOS TLS rewriters refuse. The trim now runs for both layouts, ending at the segment for an image without the block. --- core/relinker/CMakeLists.txt | 3 ++ .../src/analysis/CodeInstructionCollector.cpp | 26 +++++---- .../tests/test_interpreter_in_code.py | 54 +++++++++++++++++++ 3 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 core/relinker/relinker/tests/test_interpreter_in_code.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index a5b121fb67..0e185fa3b0 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -454,6 +454,9 @@ if(BUILD_TESTING) add_test(NAME guest_none_relocations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_none_relocations.py $) set_tests_properties(guest_none_relocations PROPERTIES TIMEOUT 20) + add_test(NAME interpreter_in_code COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_interpreter_in_code.py $) + set_tests_properties(interpreter_in_code PROPERTIES TIMEOUT 20) add_test(NAME guest_relative_section_symbols COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_relative_section_symbols.py $) set_tests_properties(guest_relative_section_symbols PROPERTIES TIMEOUT 20) diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 45b4ab81f7..cea1a30d28 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -99,6 +99,17 @@ std::set CodeInstructionCollector::Collect(const std::ve return offset; }; std::vector sceFunctions; + const auto trimToCode = [&](Domain::ProgramHeader& header, std::uint64_t end) { + const std::span segment(bytes.data() + header.Offset, header.FileSize); + auto begin = header.MappedAddress; + for (const auto& interpreter : headers) + if (interpreter.Type == 3 && interpreter.MappedAddress >= begin && interpreter.MappedAddress < end) begin = interpreter.MappedAddress + std::min(interpreter.FileSize, end - interpreter.MappedAddress); + const std::string_view loaderPath("/libexec/ld-elf.so.1", 21); + if (begin == header.MappedAddress && end - begin > loaderPath.size() && std::equal(loaderPath.begin(), loaderPath.end(), segment.begin())) begin += loaderPath.size(); + header.Offset += begin - header.MappedAddress; + header.MappedAddress = begin; + header.FileSize = end - begin; + }; if (sceBase) { const auto unwindFunctions = UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); for (const auto& function : unwindFunctions) sceFunctions.push_back({function.Begin, function.End}); @@ -107,15 +118,12 @@ std::set CodeInstructionCollector::Collect(const std::ve if (header.Type != 1 || (header.Flags & 1) == 0) continue; range(header.Offset, header.FileSize); const std::span segment(bytes.data() + header.Offset, header.FileSize); - const auto end = FindSceCodeEnd(segment, header.MappedAddress, unwindFunctions, value(3, 0x61000027).value_or(0), value(2, 0x6100002d).value_or(0)).value_or(header.MappedAddress + header.FileSize); - auto begin = header.MappedAddress; - for (const auto& interpreter : headers) - if (interpreter.Type == 3 && interpreter.MappedAddress >= begin && interpreter.MappedAddress < end) begin = interpreter.MappedAddress + std::min(interpreter.FileSize, end - interpreter.MappedAddress); - const std::string_view loaderPath("/libexec/ld-elf.so.1", 21); - if (begin == header.MappedAddress && end - begin > loaderPath.size() && std::equal(loaderPath.begin(), loaderPath.end(), segment.begin())) begin += loaderPath.size(); - header.Offset += begin - header.MappedAddress; - header.MappedAddress = begin; - header.FileSize = end - begin; + trimToCode(header, FindSceCodeEnd(segment, header.MappedAddress, unwindFunctions, value(3, 0x61000027).value_or(0), value(2, 0x6100002d).value_or(0)).value_or(header.MappedAddress + header.FileSize)); + } + } else { + for (auto& header : codeHeaders) { + if (header.Type != 1 || (header.Flags & 1) == 0) continue; + trimToCode(header, header.MappedAddress + header.FileSize); } } std::set roots; diff --git a/core/relinker/relinker/tests/test_interpreter_in_code.py b/core/relinker/relinker/tests/test_interpreter_in_code.py new file mode 100644 index 0000000000..a2f21e837f --- /dev/null +++ b/core/relinker/relinker/tests/test_interpreter_in_code.py @@ -0,0 +1,54 @@ +"""Keep the interpreter path at the start of a code segment out of the scan.""" + +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +INTERPRETER = b"/libexec/ld-elf.so.1\x00" +CODE_OFFSET = 0x1000 +DATA_ADDRESS = 0x8000 +ENTRY = 0x100 + + +def fixture(interpreter=True): + image = bytearray(0x2000) + image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) + struct.pack_into(" Date: Mon, 19 Oct 2026 20:50:17 -0300 Subject: [PATCH 178/219] fix(relinker): trim only an interpreter path at the start of the segment Trimming the end of any PT_INTERP inside an executable segment dropped the code in front of one that follows it, so an image without an SCE block whose interpreter sits after its code lost its entry points. The trim now applies to an interpreter that starts the segment, and the SCE path keeps taking any interpreter inside the scanned range, as before. --- .../src/analysis/CodeInstructionCollector.cpp | 10 ++++++---- .../relinker/tests/test_interpreter_in_code.py | 15 ++++++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index cea1a30d28..645dd46f3a 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -99,11 +99,13 @@ std::set CodeInstructionCollector::Collect(const std::ve return offset; }; std::vector sceFunctions; - const auto trimToCode = [&](Domain::ProgramHeader& header, std::uint64_t end) { + const auto trimToCode = [&](Domain::ProgramHeader& header, std::uint64_t end, bool interpreterAnywhere) { const std::span segment(bytes.data() + header.Offset, header.FileSize); auto begin = header.MappedAddress; for (const auto& interpreter : headers) - if (interpreter.Type == 3 && interpreter.MappedAddress >= begin && interpreter.MappedAddress < end) begin = interpreter.MappedAddress + std::min(interpreter.FileSize, end - interpreter.MappedAddress); + if (interpreter.Type == 3 && interpreter.MappedAddress < end && + (interpreterAnywhere ? interpreter.MappedAddress >= begin : interpreter.MappedAddress == begin)) + begin = interpreter.MappedAddress + std::min(interpreter.FileSize, end - interpreter.MappedAddress); const std::string_view loaderPath("/libexec/ld-elf.so.1", 21); if (begin == header.MappedAddress && end - begin > loaderPath.size() && std::equal(loaderPath.begin(), loaderPath.end(), segment.begin())) begin += loaderPath.size(); header.Offset += begin - header.MappedAddress; @@ -118,12 +120,12 @@ std::set CodeInstructionCollector::Collect(const std::ve if (header.Type != 1 || (header.Flags & 1) == 0) continue; range(header.Offset, header.FileSize); const std::span segment(bytes.data() + header.Offset, header.FileSize); - trimToCode(header, FindSceCodeEnd(segment, header.MappedAddress, unwindFunctions, value(3, 0x61000027).value_or(0), value(2, 0x6100002d).value_or(0)).value_or(header.MappedAddress + header.FileSize)); + trimToCode(header, FindSceCodeEnd(segment, header.MappedAddress, unwindFunctions, value(3, 0x61000027).value_or(0), value(2, 0x6100002d).value_or(0)).value_or(header.MappedAddress + header.FileSize), true); } } else { for (auto& header : codeHeaders) { if (header.Type != 1 || (header.Flags & 1) == 0) continue; - trimToCode(header, header.MappedAddress + header.FileSize); + trimToCode(header, header.MappedAddress + header.FileSize, false); } } std::set roots; diff --git a/core/relinker/relinker/tests/test_interpreter_in_code.py b/core/relinker/relinker/tests/test_interpreter_in_code.py index a2f21e837f..7ec2c4ceba 100644 --- a/core/relinker/relinker/tests/test_interpreter_in_code.py +++ b/core/relinker/relinker/tests/test_interpreter_in_code.py @@ -1,4 +1,4 @@ -"""Keep the interpreter path at the start of a code segment out of the scan.""" +"""Keep an interpreter path at the start of a code segment out of the scan, and only there.""" from pathlib import Path import struct @@ -10,13 +10,14 @@ INTERPRETER = b"/libexec/ld-elf.so.1\x00" CODE_OFFSET = 0x1000 DATA_ADDRESS = 0x8000 ENTRY = 0x100 +TRAILING_ADDRESS = 0xF00 -def fixture(interpreter=True): +def fixture(interpreter=True, trailing=False): image = bytearray(0x2000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) struct.pack_into(" Date: Tue, 20 Oct 2026 18:01:55 +0000 Subject: [PATCH 179/219] test(agc): leave the template audit out of the lookup log test The test builds a set over 1500 textures and expects its first proof on the fast path. A ShaderResources object stamps its last full walk when it is constructed, before that build, so when the build and the first proof take more than the default 1000 ms template audit interval (as under parallel test load) the audit turns the proof into a full walk and the test fails. The test now sets APS5_TEMPLATE_AUDIT_MS to 0 unless it is already set, as the sampled cache budget test pins it, so its checks see only memory changes. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/execution/LookupLogFitsBuild.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LookupLogFitsBuild.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LookupLogFitsBuild.cpp index 39de3611e3..5ca84cfaf6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LookupLogFitsBuild.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LookupLogFitsBuild.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -195,6 +196,11 @@ void RunTests(const AgcDriver::VulkanDevice& device) { } int main() { +#ifdef _WIN32 + if (std::getenv("APS5_TEMPLATE_AUDIT_MS") == nullptr) _putenv_s("APS5_TEMPLATE_AUDIT_MS", "0"); +#else + setenv("APS5_TEMPLATE_AUDIT_MS", "0", 0); +#endif try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; From 994fb42929b1c602088d2123fdfc81bbf8e62208 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:00:11 +0200 Subject: [PATCH 180/219] fix(libkernel_sys): check the readlink size before the path FreeBSD 11.4 kern_readlinkat returns EINVAL for a count above IOSIZE_MAX before namei looks at the path. readlink and readlinkat checked a NULL or empty path first, so readlink(NULL, buf, SIZE_MAX) returned EFAULT and readlink("", buf, SIZE_MAX) returned ENOENT where FreeBSD returns EINVAL. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel_sys/Export.cpp | 4 ++-- core/libs/tests/GuestKernelSys.cpp | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index a0adb45364..b7e777091a 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -273,18 +273,18 @@ int APS5_VABI fstatfs_nid_postfix(int d, void* buf) { } std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std::size_t size) { + if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); if (path == nullptr) return Fail(GuestEfault); if (*path == '\0') return Fail(GuestEnoent); - if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error); return ReadNativeLink(native, buffer, size); } std::int64_t APS5_VABI readlinkat_nid_postfix(int fd, const char* path, char* buffer, std::size_t size) { + if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); if (path == nullptr) return Fail(GuestEfault); if (*path == '\0') return Fail(GuestEnoent); - if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); if (fd == GuestAtFdcwd || *path == '/') return readlink_nid_postfix(path, buffer, size); int error = 0; const char* base = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index ea71a5644b..16bd6770eb 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -300,6 +300,13 @@ int main() { Require(readlink_nid_postfix((root / "missing").string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 2); Require(readlink_nid_postfix("", buffer, sizeof(buffer)) == -1 && Error() == 2); Require(readlink_nid_postfix(nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); + constexpr auto hugeSize = static_cast(-1); + Require(readlink_nid_postfix(nullptr, buffer, hugeSize) == -1 && Error() == 22); + Require(readlink_nid_postfix("", buffer, hugeSize) == -1 && Error() == 22); + Require(readlink_nid_postfix((root / "missing").string().c_str(), buffer, hugeSize) == -1 && Error() == 22); + Require(readlinkat_nid_postfix(-100, nullptr, buffer, hugeSize) == -1 && Error() == 22); + Require(readlinkat_nid_postfix(-100, "", buffer, hugeSize) == -1 && Error() == 22); + Require(readlinkat_nid_postfix(-100, nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); #ifndef _WIN32 Require(readlink_nid_postfix((root / std::string(300, 'n')).string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 63); #endif From a6855230188b54f9bdea9aa1704127d3abf39a88 Mon Sep 17 00:00:00 2001 From: altuux Date: Tue, 20 Oct 2026 11:44:18 +0200 Subject: [PATCH 181/219] fix(libkernel_sys): fail readlink through a regular file with ENOTDIR on Windows A path whose directory part is a regular file, such as file/child, failed with ENOENT in readlink and readlinkat on Windows, where FreeBSD 11.4 fails it with ENOTDIR (vfs_cache_lookup, sys/kern/vfs_cache.c). The not-found error now checks whether the nearest existing ancestor is not a directory, as was previously done for chmod, truncate, utimes, rename, remove, chdir, opendir, and fopen. Co-Authored-By: Antigravity --- core/libs/prx/libkernel_sys/Export.cpp | 24 +++++++++++++----------- core/libs/tests/GuestKernelSys.cpp | 2 ++ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index a0adb45364..dbe1741344 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -139,6 +139,15 @@ std::int64_t CopyLinkTarget(const std::string& target, char* buffer, std::size_t return static_cast(count); } +bool HasNonDirectoryPrefix(const std::filesystem::path& native) { + std::error_code error; + for (auto prefix = native.parent_path(); prefix.has_relative_path(); prefix = prefix.parent_path()) { + const auto status = std::filesystem::status(prefix, error); + if (std::filesystem::exists(status)) return !std::filesystem::is_directory(status); + } + return false; +} + std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, std::size_t size) { #ifdef _WIN32 const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); @@ -153,8 +162,10 @@ std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, s #endif std::error_code error; const auto status = std::filesystem::symlink_status(native, error); - if (error) return Fail(GuestError(error)); - if (status.type() == std::filesystem::file_type::not_found) return Fail(GuestEnoent); + if (error) + return Fail(error == std::errc::no_such_file_or_directory && HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestError(error)); + if (status.type() == std::filesystem::file_type::not_found) + return Fail(HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestEnoent); if (!std::filesystem::is_symlink(status)) return Fail(GuestEinval); const auto target = std::filesystem::read_symlink(native, error).generic_string(); if (error) return Fail(GuestError(error)); @@ -176,15 +187,6 @@ bool HasLongComponent(std::string_view path) { return false; } -bool HasNonDirectoryPrefix(const std::filesystem::path& native) { - std::error_code error; - for (auto prefix = native.parent_path(); prefix.has_relative_path(); prefix = prefix.parent_path()) { - const auto status = std::filesystem::status(prefix, error); - if (std::filesystem::exists(status)) return !std::filesystem::is_directory(status); - } - return false; -} - bool IsDotOrDotDot(std::string_view path) { while (!path.empty() && (path.back() == '/' || path.back() == '\\')) path.remove_suffix(1); const auto separator = path.find_last_of("/\\"); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index ea71a5644b..7a416b6431 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -298,6 +298,7 @@ int main() { Require(readlink_nid_postfix(file.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlink_nid_postfix(root.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlink_nid_postfix((root / "missing").string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(readlink_nid_postfix((root / "file.txt" / "child").string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 20); Require(readlink_nid_postfix("", buffer, sizeof(buffer)) == -1 && Error() == 2); Require(readlink_nid_postfix(nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); #ifndef _WIN32 @@ -497,6 +498,7 @@ int main() { Require(openat_nid_postfix(plain, "file.txt", SCE_KERNEL_O_RDONLY) == -1 && Error() == 20); Require(readlinkat_nid_postfix(base, "file.txt", buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlinkat_nid_postfix(base, "missing.bin", buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(readlinkat_nid_postfix(base, "file.txt/child", buffer, sizeof(buffer)) == -1 && Error() == 20); Require(readlinkat_nid_postfix(plain, "file.txt", buffer, sizeof(buffer)) == -1 && Error() == 20); Require(readlinkat_nid_postfix(-1, "file.txt", buffer, sizeof(buffer)) == -1 && Error() == 9); Require(readlinkat_nid_postfix(base, nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); From 636a6099024e69f96b62364819ede27a217efbda Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:39:18 +0000 Subject: [PATCH 182/219] test(libkernel): cover socket fcntl descriptor limits --- core/libs/tests/GuestSocketDescriptors.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/libs/tests/GuestSocketDescriptors.cpp b/core/libs/tests/GuestSocketDescriptors.cpp index aa95e766a3..9a0ac4f5e6 100644 --- a/core/libs/tests/GuestSocketDescriptors.cpp +++ b/core/libs/tests/GuestSocketDescriptors.cpp @@ -43,6 +43,7 @@ int APS5_VABI sceKernelClose(int); std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); int APS5_VABI dup_nid_postfix(int); int APS5_VABI dup2_nid_postfix(int, int); +int APS5_VABI fcntl_nid_postfix(int, int, ...); int APS5_VABI open_nid_postfix(const char*, int, int); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); @@ -59,6 +60,8 @@ static void Check(bool value, int line) { #define Require(value) Check((value), __LINE__) constexpr int DescriptorSetSize = 1024; +constexpr int DuplicateCommand = 0; +constexpr int DuplicateCloseOnExec = 17; constexpr int Inet = 2; constexpr int Datagram = 2; constexpr short In = 0x1; @@ -195,6 +198,10 @@ static void SocketNumbersStayInDescriptorSet() { files.push_back(file); } Require(dup_nid_postfix(socket) == -1 && *__error_nid_postfix() == 24); + Require(fcntl_nid_postfix(socket, DuplicateCommand, 0) == -1 && *__error_nid_postfix() == 24); + Require(fcntl_nid_postfix(socket, DuplicateCommand, DescriptorSetSize) == -1 && *__error_nid_postfix() == 24); + Require(fcntl_nid_postfix(socket, DuplicateCloseOnExec, 0) == -1 && *__error_nid_postfix() == 24); + Require(fcntl_nid_postfix(socket, DuplicateCloseOnExec, DescriptorSetSize) == -1 && *__error_nid_postfix() == 24); Require(socket_nid_postfix(Inet, Datagram, 0) == -1 && *__error_nid_postfix() == 24); Require(dup2_nid_postfix(socket, DescriptorSetSize) == -1 && *__error_nid_postfix() == 24); for (const int file : files) Require(close_nid_postfix(file) == 0); From c420b8d53ddbd3e64a021a32d783ae359013fd8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 20 Oct 2026 20:49:45 +0200 Subject: [PATCH 183/219] fix(main): skip the parent watch of a vnode already deleted at registration Culprit: none in 892a4f1328..b85d972f42; race in the Windows vnode watch (50b06110b guarded only the loop). Run 39473930499, guest_kqueue: a watched file deleted before registration is snapshotted under $Extend\$Deleted, WatchParent got ERROR_ACCESS_DENIED and threw. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/Equeue/src/FileEvent.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp index b6bbf66d7e..04fe98539f 100644 --- a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp +++ b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp @@ -368,7 +368,9 @@ int OpenHostWatch(int descriptor, std::uint32_t notes, HostWatch& host) { throw std::runtime_error("EVFILT_VNODE: reading the watched file failed, error=" + std::to_string(error)); } try { - host.parent = WatchParent(host.last.path); + if (!IsDeleted(host.last)) { + host.parent = WatchParent(host.last.path); + } if (host.directory) { host.entries = WatchContents(host.last.path, FILE_NOTIFY_CHANGE_FILE_NAME, "entries"); host.subdirectories = WatchContents(host.last.path, FILE_NOTIFY_CHANGE_DIR_NAME, "subdirectories"); From 83a1c326b4042da1687f58a950ca02beca065653 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Tue, 20 Oct 2026 14:16:28 -0500 Subject: [PATCH 184/219] fix(libc): link the thread-name test against libc for its personality routine --- core/libs/prx/libc/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index e794b22c5b..4f5a3d8db1 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -856,7 +856,11 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") add_test_executable(thread_name_tests ${LIBS_INCLUDE_DIR}/tests/ThreadName.cpp) target_include_directories(thread_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(thread_name_tests PRIVATE libc) + configure_windows_unwind(thread_name_tests) add_test(NAME thread_name COMMAND thread_name_tests) + set_tests_properties(thread_name PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() add_test_executable(guest_heap_report_for_game_tests ${LIBS_INCLUDE_DIR}/tests/GuestHeapReportForGame.cpp) From a8b6d1c4115dbe3de3c427941216469513e73bc7 Mon Sep 17 00:00:00 2001 From: cadenasynyster <8373765+cadenasynyster@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:31:16 +0200 Subject: [PATCH 185/219] feat(libc): implement _Thrd_detach Animal Well (PPSA22520) imports _Thrd_detach (L7f7zYwBvZA) from libc.prx and nothing exports it, so the Windows loader stops at startup. Register libkernel's pthread_detach with libc like pthread_create and pthread_join, and return _Thrd_error (4) when it fails. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01GtXavMkgCTCdmRpweviHcQ --- core/libs/prx/libc/include/General.hpp | 2 ++ core/libs/prx/libc/src/Threads.cpp | 11 +++++++ .../prx/libkernel/Pthread/Posix/Thread.cpp | 1 + core/libs/tests/GuestLibcThreads.cpp | 31 +++++++++++++++++++ docs/dev/debt/libc.md | 2 +- 5 files changed, 46 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index f259bf02f9..6dade90518 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -53,8 +53,10 @@ extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle); using PthreadCreateFn_nid_no_patch = int (APS5_VABI*)(void**, const void*, void* (APS5_VABI*)(void*), void*); using PthreadJoinFn_nid_no_patch = int (APS5_VABI*)(void*, void**); +using PthreadDetachFn_nid_no_patch = int (APS5_VABI*)(void*); extern "C" void RegisterPthreadCreate_nid_no_patch(PthreadCreateFn_nid_no_patch fn); extern "C" void RegisterPthreadJoin_nid_no_patch(PthreadJoinFn_nid_no_patch fn); +extern "C" void RegisterPthreadDetach_nid_no_patch(PthreadDetachFn_nid_no_patch fn); #define APS5_INVALID_ARG_EX throw std::invalid_argument(std::string(__func__) + ": invalid argument") diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 7a3db566d5..3373def131 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -412,6 +412,7 @@ static void RequireThrd(int result, const char* what) { static std::atomic registeredPthreadCreate{nullptr}; static std::atomic registeredPthreadJoin{nullptr}; +static std::atomic registeredPthreadDetach{nullptr}; void RegisterPthreadCreate_nid_no_patch(PthreadCreateFn_nid_no_patch fn) { registeredPthreadCreate.store(fn, std::memory_order_release); @@ -421,6 +422,10 @@ void RegisterPthreadJoin_nid_no_patch(PthreadJoinFn_nid_no_patch fn) { registeredPthreadJoin.store(fn, std::memory_order_release); } +void RegisterPthreadDetach_nid_no_patch(PthreadDetachFn_nid_no_patch fn) { + registeredPthreadDetach.store(fn, std::memory_order_release); +} + static void* APS5_VABI PadEntry_nid_no_patch(void* argument) { auto* pad = static_cast(argument); const auto go = pad->vtable[0]; @@ -477,6 +482,12 @@ int APS5_VABI _Thrd_join_nid_postfix(void* thread, int* code) { return threadSuccess; } +int APS5_VABI _Thrd_detach_nid_postfix(void* thread) { + const auto detach = registeredPthreadDetach.load(std::memory_order_acquire); + if (!detach) throw std::runtime_error("_Thrd_detach: pthread_detach is not registered"); + return detach(thread) == 0 ? threadSuccess : threadError; +} + int APS5_VABI _Thrd_id_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index 959790db4a..0a5444c390 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -230,6 +230,7 @@ struct RegisterPthreadOnLoad { RegisterPthreadOnLoad() { RegisterPthreadCreate_nid_no_patch(reinterpret_cast(pthread_create_nid_postfix)); RegisterPthreadJoin_nid_no_patch(reinterpret_cast(pthread_join_nid_postfix)); + RegisterPthreadDetach_nid_no_patch(reinterpret_cast(pthread_detach_nid_postfix)); } }; const RegisterPthreadOnLoad gRegisterPthread; diff --git a/core/libs/tests/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp index 1312aed59f..ac1015b41f 100644 --- a/core/libs/tests/GuestLibcThreads.cpp +++ b/core/libs/tests/GuestLibcThreads.cpp @@ -30,8 +30,10 @@ void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad); void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad); void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, void** thread); int APS5_VABI _Thrd_join_nid_postfix(void* thread, int* code); +int APS5_VABI _Thrd_detach_nid_postfix(void* thread); void RegisterPthreadCreate_nid_no_patch(int (APS5_VABI* fn)(void**, const void*, void* (APS5_VABI*)(void*), void*)); void RegisterPthreadJoin_nid_no_patch(int (APS5_VABI* fn)(void*, void**)); +void RegisterPthreadDetach_nid_no_patch(int (APS5_VABI* fn)(void*)); } static void Require(bool value) { if (!value) std::abort(); } @@ -227,6 +229,12 @@ static int APS5_VABI HostPthreadJoin(void* thread, void** value) { return 0; } +static int APS5_VABI HostPthreadDetach(void* thread) { + if (!thread) return 22; + static_cast(thread)->worker.detach(); + return 0; +} + static unsigned APS5_VABI TestPadGo(GuestPad* base) { auto* pad = reinterpret_cast(base); pad->ran.store(true); @@ -269,6 +277,28 @@ static void PadLaunchAndJoin() { RegisterPthreadJoin_nid_no_patch(nullptr); } +static void PadDetach() { + unsigned (APS5_VABI* vtable[1])(GuestPad*) = {TestPadGo}; + RegisterPthreadDetach_nid_no_patch(nullptr); + RequireThrows([] { _Thrd_detach_nid_postfix(nullptr); }); + + RegisterPthreadCreate_nid_no_patch(HostPthreadCreate); + RegisterPthreadDetach_nid_no_patch(HostPthreadDetach); + + TestPad pad{}; + pad.vtable = vtable; + _ZNSt4_PadC2Ev_nid_postfix(reinterpret_cast(&pad)); + void* thread = nullptr; + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(reinterpret_cast(&pad), &thread); + Require(thread != nullptr && pad.ran.load()); + _ZNSt4_PadD2Ev_nid_postfix(reinterpret_cast(&pad)); + Require(_Thrd_detach_nid_postfix(thread) == success); + Require(_Thrd_detach_nid_postfix(nullptr) == 4); + + RegisterPthreadCreate_nid_no_patch(nullptr); + RegisterPthreadDetach_nid_no_patch(nullptr); +} + int main() { MutexBasics(); RecursiveMutex(); @@ -276,4 +306,5 @@ int main() { ConditionBroadcast(); ConditionSignal(); PadLaunchAndJoin(); + PadDetach(); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 9f025c142e..91a74ec31c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -127,4 +127,4 @@ - [app0.zar](../../../core/libs/prx/libc/src/AppArchive.cpp) (libc) - a [ZArchive](https://github.com/Exzap/ZArchive) beside `app0/` is a read-only layer under it: a path missing from `app0/` is looked up in the archive, case-insensitively. It is not a console format; titles ship their files in a PFS image instead. Resolving an archive-only path through any call other than the archive-aware libkernel ones throws, so `fopen`, `stat`, `access`, `opendir` and the rest do not read the archive yet, and `opendir` of a directory that exists in both places, `app0/` itself included, and `fdopen` of an archived descriptor throw instead of missing its entries or reading the null device. A damaged compressed block throws instead of returning `EIO` - [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified - [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. -- [std::_Pad, _Thrd_join, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. `_Thrd_id` throws because whether the console returns `pthread_t` or a kernel thread ID and at what width is unknown; `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes (`_Thrd_start` name, stack size and priority are unknown). The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12; named overloads on the console could differ. Failing primitives or thread start throw the guest `std::system_error` from `_Throw_C_error`/`_Thrd_startX`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int` +- [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. `_Thrd_id` throws because whether the console returns `pthread_t` or a kernel thread ID and at what width is unknown; `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes (`_Thrd_start` name, stack size and priority are unknown). The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12; named overloads on the console could differ. Failing primitives or thread start throw the guest `std::system_error` from `_Throw_C_error`/`_Thrd_startX`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` calls the `pthread_detach` registered by libkernel and returns `_Thrd_error` (4) when it fails, and throws when none is registered From 681926e08506226f6b611c0f799025e73a2bd93b Mon Sep 17 00:00:00 2001 From: billythemenace2 Date: Tue, 20 Oct 2026 20:35:16 +0100 Subject: [PATCH 186/219] fix(libkernel): keep waiting while a raise target is inside a system call Since #8512 a raise redirects a target running host code once 50 ms have passed since the raise began. A target inside a system call that is not a wait, such as the NtWriteFile of an fwrite that holds its stream lock, can stay there longer; the redirect then ran the handler with the lock held. SONIC SUPERSTARS froze at frame 67 again on main this way, with its GC raise delivered at ntdll!ZwWriteFile+20. A target stopped right after a syscall instruction in ntdll now restarts the 50 ms host-code window, so the raise keeps sampling until the call returns and the target leaves host code, bounded by the 1 s retry limit. guest_raise_in_system_call holds a writer in NtWriteFile on a slowly drained pipe; it times out on main and passes with this change. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libkernel/CMakeLists.txt | 7 ++ .../prx/libkernel/System/src/Exception.cpp | 18 +++- core/libs/tests/GuestRaiseInSystemCall.cpp | 86 +++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 4 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 core/libs/tests/GuestRaiseInSystemCall.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index cea1629cec..bcc664a2b5 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1799,6 +1799,13 @@ if(BUILD_TESTING) add_test(NAME guest_mutex_raise_exception COMMAND guest_mutex_raise_exception_tests) set_tests_properties(guest_mutex_raise_exception PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test_executable(guest_raise_in_system_call_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseInSystemCall.cpp) + target_include_directories(guest_raise_in_system_call_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_raise_in_system_call_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_raise_in_system_call_tests) + add_test(NAME guest_raise_in_system_call COMMAND guest_raise_in_system_call_tests) + set_tests_properties(guest_raise_in_system_call PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") add_test_executable(guest_raise_in_host_code_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseInHostCode.cpp) target_include_directories(guest_raise_in_host_code_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_raise_in_host_code_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index de202af371..b7eb4f65f7 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -377,6 +377,17 @@ bool BlockedInWait(DWORD64 rip) { return std::any_of(waits.begin(), waits.end(), [rip](DWORD64 stub) { return rip - stub < 0x20; }); } +DWORD64 NtdllBase() { + static const auto ntdll = reinterpret_cast(GetModuleHandleW(L"ntdll.dll")); + return ntdll; +} + +bool InSystemCall(DWORD64 rip, DWORD64 base) { + if (base == 0 || base != NtdllBase() || rip < base + 2) return false; + const auto* code = reinterpret_cast(rip - 2); + return code[0] == 0x0f && code[1] == 0x05; +} + CodeKind RunningCode(DWORD64 rip, DWORD64& base) { base = ImageBase(rip); auto kind = CachedKind(base); @@ -396,12 +407,13 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { alignas(16) Delivery delivery{handler, signum, {}, {}, SaveVectors ? 1u : 0u}; const auto start = std::chrono::steady_clock::now(); const auto retryDeadline = start + RetryLimit; - const auto hostDeadline = start + HostCodeLimit; + auto hostDeadline = start + HostCodeLimit; static_cast(RestoringContext(0)); static_cast(InWinpthread(0)); static_cast(BlockedInWait(0)); static_cast(ExecutableBase()); static_cast(Images()); + static_cast(NtdllBase()); for (;;) { if (SuspendThread(native) == static_cast(-1)) { if (Exited(native)) return false; @@ -426,7 +438,9 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { const bool restoring = RestoringContext(delivery.context.Rip) || InWinpthread(delivery.context.Rip); DWORD64 image = 0; const auto running = restoring ? CodeKind::Guest : RunningCode(delivery.context.Rip, image); - if (!restoring && (running == CodeKind::Guest || (running == CodeKind::Host && std::chrono::steady_clock::now() >= hostDeadline))) break; + const auto now = std::chrono::steady_clock::now(); + if (running == CodeKind::Host && InSystemCall(delivery.context.Rip, image) && now < retryDeadline) hostDeadline = std::max(hostDeadline, std::min(retryDeadline, now + HostCodeLimit)); + if (!restoring && (running == CodeKind::Guest || (running == CodeKind::Host && now >= hostDeadline))) break; ResumeThread(native); if (running == CodeKind::Unknown) { static_cast(ClassifyImage(image)); diff --git a/core/libs/tests/GuestRaiseInSystemCall.cpp b/core/libs/tests/GuestRaiseInSystemCall.cpp new file mode 100644 index 0000000000..834594d260 --- /dev/null +++ b/core/libs/tests/GuestRaiseInSystemCall.cpp @@ -0,0 +1,86 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread*, const PthreadAttr*, PthreadEntry, void*, const char*); +int APS5_VABI scePthreadJoin(Pthread, void**); +int APS5_VABI sceKernelInstallExceptionHandler(int, void*); +int APS5_VABI sceKernelRemoveExceptionHandler(int); +int APS5_VABI sceKernelRaiseException(Pthread, int); +} + +static constexpr int SigUsr1 = 30; +static constexpr int Raises = 10; +static constexpr DWORD PipeBytes = 4096; +static void Require(bool value) { if (!value) std::abort(); } + +static std::FILE* shared = nullptr; +static std::atomic deliveries{0}; +static std::atomic released{0}; +static std::atomic stop{false}; +static std::atomic blocks{0}; + +static void APS5_VABI Handler(int, void*) { + const auto delivery = deliveries.fetch_add(1) + 1; + while (released.load() < delivery) std::this_thread::yield(); +} + +static void* APS5_VABI Writer(void*) { + const std::string block(16u * PipeBytes, 'x'); + while (!stop.load()) { + Require(std::fwrite(block.data(), 1, block.size(), shared) == block.size()); + blocks.fetch_add(1); + volatile std::uint64_t spin = 0; + for (std::uint64_t i = 0; i < 200000; ++i) spin = spin + i; + } + return nullptr; +} + +int main() { + std::atomic done{false}; + std::thread watchdog([&done] { + for (int i = 0; i < 25000 && !done.load(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (!done.load()) std::abort(); + }); + HANDLE read = nullptr; + HANDLE write = nullptr; + Require(CreatePipe(&read, &write, nullptr, PipeBytes) != 0); + shared = _fdopen(_open_osfhandle(reinterpret_cast(write), _O_WRONLY | _O_BINARY), "wb"); + Require(shared != nullptr); + std::atomic draining{true}; + std::thread reader([&] { + char chunk[PipeBytes]; + DWORD got = 0; + while (ReadFile(read, chunk, sizeof(chunk), &got, nullptr) != 0 && got != 0) { + if (draining.load()) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + }); + Require(sceKernelInstallExceptionHandler(SigUsr1, reinterpret_cast(&Handler)) == 0); + Pthread writer = nullptr; + Require(scePthreadCreate(&writer, nullptr, Writer, nullptr, "pipe writer") == 0); + while (blocks.load() == 0) std::this_thread::yield(); + for (int raise = 1; raise <= Raises; ++raise) { + Require(sceKernelRaiseException(writer, SigUsr1) == 0); + while (deliveries.load() < raise) std::this_thread::yield(); + Require(std::fputc('y', shared) == 'y'); + released.store(raise); + } + stop.store(true); + draining.store(false); + Require(scePthreadJoin(writer, nullptr) == 0); + Require(sceKernelRemoveExceptionHandler(SigUsr1) == 0); + std::fclose(shared); + reader.join(); + CloseHandle(read); + done.store(true); + watchdog.join(); +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index a9427fee33..9df5264b17 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -149,7 +149,7 @@ - [scePthreadAttrGet, pthread_attr_get_np and sceKernelIsStack](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) report the stack of a host thread the library did not create, on Windows, from one allocation granule (64 KiB) above the bottom of its reservation when everything above that is committed read-write, as in a relinked executable's main thread and its default-size host threads (0x200000 committed of 0x210000 reserved), so the guest can protect the lowest reported pages, as HotSpot does for its stack guard pages in Java 25 LAN / DNS (PPSA99869). Any other such thread, and every such thread on Linux, gets its whole host stack: on Windows `mprotect` commits its reserved lower pages before protecting them and drops the guard flag of Windows' guard pages, and on Linux it cannot change the unmapped lower pages - [sceKernelInstallExceptionHandler](../../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Linux sends the host SIGUSR1, which the library takes over, to the target thread, so the handler runs wherever the thread was interrupted, emulator code included. On the console kernel objects are system calls and libthr defers signals while a thread holds its internal locks; here a handler that needs a host lock the interrupted code holds deadlocks, and a thread that the handler keeps waiting keeps its host locks. Host waits resume after the handler. The host calls Linux does not restart end early: sleeps that do not retry return before their time, and the poll of `sceNetEpollWait` and socket calls with a send or receive timeout return `EINTR` to the guest -- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows suspends the target and redirects it to the handler. A target running host code (any module other than the executable and the `*.guest.prx` modules, so libkernel, libc and the other HLE modules count too) is resumed and sampled again until it is back in guest code, since a handler that waits there would keep the host locks that code holds; a target blocked in an ntdll wait is redirected in place, as are NtContinue and winpthreads after their retry limit. A target that stays in host code without waiting for the whole 1 s retry limit, such as a long HLE or driver call, costs the raising thread up to 1 s per raise and is then redirected in host code anyway, with the same hazard. While the target is suspended only `VirtualQuery` and a lock-free cache of image bases are consulted; an image not yet in the cache is classified with `GetModuleFileNameW` after the target is resumed. An APC queued at the first host-code sample, which an alertable guest wait would run sooner, is not implemented +- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows suspends the target and redirects it to the handler. A target running host code (any module other than the executable and the `*.guest.prx` modules, so libkernel, libc and the other HLE modules count too) is resumed and sampled again until it is back in guest code, since a handler that waits there would keep the host locks that code holds; a target blocked in an ntdll wait is redirected in place, as are NtContinue and winpthreads after their retry limit. A target that stays in host code without waiting for 50 ms after the raise began, such as a long HLE or driver call, is then redirected in host code anyway, with the same hazard. A target seen right after a `syscall` instruction in ntdll, inside a system call that is not a wait such as the `NtWriteFile` of an `fwrite` holding its stream lock, restarts those 50 ms, so a raise can cost the raising thread up to the 1 s retry limit. While the target is suspended only `VirtualQuery` and a lock-free cache of image bases are consulted; an image not yet in the cache is classified with `GetModuleFileNameW` after the target is resumed. An APC queued at the first host-code sample, which an alertable guest wait would run sooner, is not implemented - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask - [pthread_sigmask](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) shares the process-wide mask of `sigprocmask` and `_sigprocmask`, which keeps only the first 32 signals; FreeBSD keeps a mask per thread From 9e6544fbab366d5c83a197d4279c7a693c8a620c Mon Sep 17 00:00:00 2001 From: cadenasynyster <8373765+cadenasynyster@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:43:57 +0200 Subject: [PATCH 187/219] docs(libc): shorten the _Thrd_detach debt entry --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 91a74ec31c..029cf7d586 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -127,4 +127,4 @@ - [app0.zar](../../../core/libs/prx/libc/src/AppArchive.cpp) (libc) - a [ZArchive](https://github.com/Exzap/ZArchive) beside `app0/` is a read-only layer under it: a path missing from `app0/` is looked up in the archive, case-insensitively. It is not a console format; titles ship their files in a PFS image instead. Resolving an archive-only path through any call other than the archive-aware libkernel ones throws, so `fopen`, `stat`, `access`, `opendir` and the rest do not read the archive yet, and `opendir` of a directory that exists in both places, `app0/` itself included, and `fdopen` of an archived descriptor throw instead of missing its entries or reading the null device. A damaged compressed block throws instead of returning `EIO` - [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified - [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. -- [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. `_Thrd_id` throws because whether the console returns `pthread_t` or a kernel thread ID and at what width is unknown; `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes (`_Thrd_start` name, stack size and priority are unknown). The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12; named overloads on the console could differ. Failing primitives or thread start throw the guest `std::system_error` from `_Throw_C_error`/`_Thrd_startX`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` calls the `pthread_detach` registered by libkernel and returns `_Thrd_error` (4) when it fails, and throws when none is registered +- [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. `_Thrd_id` throws because whether the console returns `pthread_t` or a kernel thread ID and at what width is unknown; `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes. The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered From 1b182e7fb205543201f199a888c8a25b60018c6d Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:43:29 +0200 Subject: [PATCH 188/219] fix(net): keep MSG_DONTWAIT transfers nonblocking on Windows --- core/libs/prx/libSceNet/CMakeLists.txt | 12 + core/libs/prx/libSceNet/Export.cpp | 175 +++++++++---- core/libs/prx/libSceSsl/Export.cpp | 10 +- .../libkernel/Socket/include/SocketPoll.hpp | 4 + core/libs/prx/libkernel/Socket/src/Udp.cpp | 4 +- core/libs/tests/NetDontWait.cpp | 237 ++++++++++++++++++ docs/dev/debt/libSceNet.md | 4 +- 7 files changed, 389 insertions(+), 57 deletions(-) create mode 100644 core/libs/tests/NetDontWait.cpp diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index cdab277419..e729ae03d1 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -3,6 +3,18 @@ target_include_directories(libSceNet PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceNet) if(BUILD_TESTING) + add_test_executable(net_dontwait_tests ${LIBS_INCLUDE_DIR}/tests/NetDontWait.cpp) + target_include_directories(net_dontwait_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(net_dontwait_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(net_dontwait_tests) + foreach(api IN ITEMS send sendto sendmsg recv recvfrom recvmsg duplex blocking) + add_test(NAME net_dontwait_${api} COMMAND net_dontwait_tests ${api}) + set_tests_properties(net_dontwait_${api} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(net_dontwait_${api} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test_executable(guest_datagram_receive_tests ${LIBS_INCLUDE_DIR}/tests/GuestDatagramReceive.cpp) target_include_directories(guest_datagram_receive_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_datagram_receive_tests PRIVATE libSceNet libkernel libc) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 6a679a251f..5c77582553 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -134,9 +134,20 @@ int net_error(int code) { return GuestSockets::GuestErrorFromHost(code); } int native_error() { return net_error(errno); } #endif -struct NativeSocketHandle { +#ifdef _WIN32 +int TransferSocketForKernel(KernelSocketPoll::NativeOwner* owner, bool send, void* buffer, int length); +#endif + +struct NativeSocketHandle : KernelSocketPoll::NativeOwner { NativeSocket value; - explicit NativeSocketHandle(NativeSocket socket) : value(socket) {} + std::atomic nonblock{false}; + std::atomic receiveTimeout{0}; + std::atomic sendTimeout{0}; + explicit NativeSocketHandle(NativeSocket socket) : value(socket) { +#ifdef _WIN32 + transfer = &TransferSocketForKernel; +#endif + } ~NativeSocketHandle() { if (value != INVALID_NATIVE_SOCKET) close_socket(value); } @@ -201,13 +212,10 @@ struct Sock { int family = 0; int type = 0; std::shared_ptr native; - bool nonblock = false; bool bound = false; bool listening = false; bool aborted = false; bool read_shut = false; - int rcv_timeout_us = 0; - int snd_timeout_us = 0; int reported_error = 0; int snd_buffer = 0; int rcv_buffer = 0; @@ -341,8 +349,8 @@ int set_kernel_timeout(int descriptor, int optname, const void* value, std::uint const auto it = g_socks.find(descriptor); if (it != g_socks.end()) { const int stored = static_cast(std::min(microseconds, INT_MAX)); - if (optname == NET_SO_RCVTIMEO) it->second.rcv_timeout_us = stored; - else it->second.snd_timeout_us = stored; + if (optname == NET_SO_RCVTIMEO) it->second.native->receiveTimeout = stored; + else it->second.native->sendTimeout = stored; } return 0; } @@ -430,7 +438,7 @@ std::int64_t message_length(const NetMsghdr* message) { int receive_error(const Sock& socket) { #ifdef _WIN32 - if (WSAGetLastError() == WSAETIMEDOUT && socket.type != NET_SOCK_STREAM && socket.rcv_timeout_us > 0) return NET_EAGAIN; + if (WSAGetLastError() == WSAETIMEDOUT && socket.type != NET_SOCK_STREAM && socket.native->receiveTimeout > 0) return NET_EAGAIN; #else (void)socket; #endif @@ -441,21 +449,50 @@ bool message_flags_supported(int flags, int accepted) { return (flags & ~accepted) == 0; } -bool would_block(const Sock& socket, int flags, bool send) { +template +auto native_transfer(const NativeSocketHandle& socket, int flags, bool send, TOperation operation, bool timed = true) { #ifdef _WIN32 - if ((flags & NET_MSG_DONTWAIT) == 0) return false; - WSAPOLLFD entry{}; - entry.fd = socket.native->value; - entry.events = send ? POLLWRNORM : POLLRDNORM; - return WSAPoll(&entry, 1, 0) == 0; + const bool nonblocking = (flags & NET_MSG_DONTWAIT) != 0 || socket.nonblock.load(); + const int timeout = timed ? (send ? socket.sendTimeout.load() : socket.receiveTimeout.load()) : 0; + const auto deadline = timeout > 0 ? std::chrono::steady_clock::now() + std::chrono::microseconds(timeout) + : std::chrono::steady_clock::time_point::max(); + for (;;) { + const auto result = operation(); + if (result != static_cast(-1) || WSAGetLastError() != WSAEWOULDBLOCK || nonblocking) return result; + int milliseconds = 10; + if (timeout > 0) { + const auto remaining = deadline - std::chrono::steady_clock::now(); + if (remaining <= std::chrono::steady_clock::duration::zero()) { + WSASetLastError(WSAETIMEDOUT); + return result; + } + milliseconds = static_cast(std::min(std::chrono::ceil(remaining).count(), milliseconds)); + } + WSAPOLLFD descriptor{}; + descriptor.fd = socket.value; + descriptor.events = send ? POLLWRNORM : POLLRDNORM; + const int ready = WSAPoll(&descriptor, 1, milliseconds); + if (ready < 0) return static_cast(-1); + } #else static_cast(socket); static_cast(flags); static_cast(send); - return false; + static_cast(timed); + return operation(); #endif } +#ifdef _WIN32 +int TransferSocketForKernel(KernelSocketPoll::NativeOwner* owner, bool send, void* buffer, int length) { + const auto* socket = static_cast(owner); + return native_transfer(*socket, 0, send, [&] { + return send ? ::send(socket->value, static_cast(buffer), length, 0) + : ::recv(socket->value, static_cast(buffer), length, 0); + }); +} +#endif + int native_message_flags(int flags) { int native = (flags & NET_MSG_PEEK) ? MSG_PEEK : 0; #ifndef _WIN32 @@ -496,9 +533,10 @@ int wait_for_local_address(int s, const Sock& socket, bool empty, int flags) { if (socket.type == NET_SOCK_STREAM || socket.bound) return 0; const bool named = has_local_address(socket.native->value); if (!named && (socket.read_shut || empty)) return RECEIVE_NOTHING; - if (!named && (socket.nonblock || (flags & NET_MSG_DONTWAIT) != 0)) return NET_EAGAIN; - const auto deadline = socket.rcv_timeout_us > 0 - ? std::chrono::steady_clock::now() + std::chrono::microseconds(socket.rcv_timeout_us) + if (!named && (socket.native->nonblock || (flags & NET_MSG_DONTWAIT) != 0)) return NET_EAGAIN; + const int timeout = socket.native->receiveTimeout; + const auto deadline = timeout > 0 + ? std::chrono::steady_clock::now() + std::chrono::microseconds(timeout) : std::chrono::steady_clock::time_point::max(); std::unique_lock lk(g_mutex); for (bool waited = false;; waited = true) { @@ -674,7 +712,7 @@ bool ControlSocketStatusForKernel(int descriptor, int command, int flags, int* r std::lock_guard lk(g_mutex); const auto it = g_socks.find(descriptor); if (it == g_socks.end()) return false; - nonblock = it->second.nonblock; + nonblock = it->second.native->nonblock; } *result = 0; *error = 0; @@ -702,7 +740,7 @@ bool FindSocketNativeForKernel(int descriptor, std::shared_ptr* owner, std const auto it = g_socks.find(descriptor); if (it == g_socks.end() || !it->second.native) return false; *native = static_cast(it->second.native->value); - *owner = it->second.native; + *owner = std::static_pointer_cast(it->second.native); return true; } @@ -748,6 +786,14 @@ int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol) if (!initialize_sockets()) return fail(5); const NativeSocket native = ::socket(family == NET_AF_INET ? AF_INET : AF_INET6, type, protocol); if (native == INVALID_NATIVE_SOCKET) return fail(native_error()); +#ifdef _WIN32 + u_long nonblocking = 1; + if (ioctlsocket(native, FIONBIO, &nonblocking) != 0) { + const int error = native_error(); + close_socket(native); + return fail(error); + } +#endif const int ipv6_only = 1; if (family == NET_AF_INET6 && ::setsockopt(native, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&ipv6_only), sizeof(ipv6_only)) != 0) { @@ -875,8 +921,11 @@ int APS5_VABI sceNetAccept(int s, void* addr, uint32_t* addrlen) { if (addr && !addrlen) return fail(NET_EINVAL); sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); - const NativeSocket accepted = ::accept(listener.native->value, - addr ? reinterpret_cast(&peer) : nullptr, addr ? &peer_length : nullptr); + const NativeSocket accepted = native_transfer(*listener.native, 0, false, [&] { + peer_length = sizeof(peer); + return ::accept(listener.native->value, + addr ? reinterpret_cast(&peer) : nullptr, addr ? &peer_length : nullptr); + }, false); if (accepted == INVALID_NATIVE_SOCKET) return fail(native_error()); std::shared_ptr handle; try { @@ -889,13 +938,17 @@ int APS5_VABI sceNetAccept(int s, void* addr, uint32_t* addrlen) { connection.family = listener.family; connection.type = NET_SOCK_STREAM; connection.native = std::move(handle); +#ifdef _WIN32 + connection.native->receiveTimeout = listener.native->receiveTimeout.load(); + connection.native->sendTimeout = listener.native->sendTimeout.load(); +#endif const int descriptor = KernelReserveSocketNumber_nid_no_patch(static_cast(connection.native->value)); if (descriptor < 0) return fail(-descriptor); { std::lock_guard lk(g_mutex); g_socks.emplace(descriptor, connection); } - if (listener.nonblock) { + if (listener.native->nonblock) { const int enabled = 1; if (sceNetSetsockopt(descriptor, NET_SOL_SOCKET, NET_SO_NBIO, &enabled, sizeof(enabled)) != 0) { const int error = *errno_slot(); @@ -973,7 +1026,21 @@ int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { return 0; } #ifdef _WIN32 - if (WSAGetLastError() == WSAEWOULDBLOCK) return fail(NET_EINPROGRESS); + if (WSAGetLastError() == WSAEWOULDBLOCK) { + if (socket.native->nonblock) return fail(NET_EINPROGRESS); + fd_set writable; + fd_set failed; + FD_ZERO(&writable); + FD_ZERO(&failed); + FD_SET(socket.native->value, &writable); + FD_SET(socket.native->value, &failed); + if (::select(0, nullptr, &writable, &failed, nullptr) < 0) return fail(native_error()); + int error = 0; + int size = sizeof(error); + if (::getsockopt(socket.native->value, SOL_SOCKET, SO_ERROR, reinterpret_cast(&error), &size) != 0) + return fail(native_error()); + return error == 0 ? 0 : fail(net_error(error)); + } #endif return fail(native_error()); } @@ -994,9 +1061,10 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { #endif const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); if (!destination.Open()) return fail(NET_EFAULT); - if (would_block(socket, flags, false)) return fail(NET_EAGAIN); const int native_flags = native_message_flags(flags); - const int result = ::recv(socket.native->value, static_cast(buf), static_cast(len), native_flags); + const int result = native_transfer(*socket.native, flags, false, [&] { + return ::recv(socket.native->value, static_cast(buf), static_cast(len), native_flags); + }); #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) return static_cast(len); #endif @@ -1032,10 +1100,12 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* if (!destination.Open()) return fail(NET_EFAULT); sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); - if (would_block(socket, flags, false)) return fail(NET_EAGAIN); const int native_flags = native_message_flags(flags); - int result = ::recvfrom(socket.native->value, static_cast(buf), static_cast(len), - native_flags, from ? reinterpret_cast(&peer) : nullptr, from ? &peer_length : nullptr); + int result = native_transfer(*socket.native, flags, false, [&] { + peer_length = sizeof(peer); + return ::recvfrom(socket.native->value, static_cast(buf), static_cast(len), + native_flags, from ? reinterpret_cast(&peer) : nullptr, from ? &peer_length : nullptr); + }); #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(len); #endif @@ -1055,8 +1125,9 @@ int64_t APS5_VABI sceNetSend(int s, const void* buf, size_t len, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } - if (would_block(socket, flags, true)) return fail(NET_EAGAIN); - const int result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags)); + const int result = native_transfer(*socket.native, flags, true, [&] { + return ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags)); + }); return result >= 0 ? result : fail(native_error()); } @@ -1071,18 +1142,21 @@ int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, co if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } - if (would_block(socket, flags, true)) return fail(NET_EAGAIN); int result; if (!to) { if (tolen != 0) return fail(NET_EINVAL); - result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags)); + result = native_transfer(*socket.native, flags, true, [&] { + return ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags)); + }); } else { sockaddr_storage destination{}; NativeLength destination_length = 0; if (!guest_to_native_address(to, tolen, destination, destination_length)) return fail(NET_EINVAL); if (destination.ss_family != (socket.family == NET_AF_INET ? AF_INET : AF_INET6)) return fail(NET_EAFNOSUPPORT); - result = ::sendto(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags), - reinterpret_cast(&destination), destination_length); + result = native_transfer(*socket.native, flags, true, [&] { + return ::sendto(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags), + reinterpret_cast(&destination), destination_length); + }); } if (result < 0) return fail(native_error()); #ifdef _WIN32 @@ -1138,7 +1212,6 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { return 0; } #endif - if (would_block(socket, flags, false)) return fail(NET_EAGAIN); std::vector buffer; try { buffer.resize(static_cast(total) + (datagram ? 1u : 0u)); @@ -1147,8 +1220,11 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { } sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); - std::int64_t received = ::recvfrom(socket.native->value, buffer.data(), static_cast(buffer.size()), - native_message_flags(flags), reinterpret_cast(&peer), &peer_length); + std::int64_t received = native_transfer(*socket.native, flags, false, [&] { + peer_length = sizeof(peer); + return ::recvfrom(socket.native->value, buffer.data(), static_cast(buffer.size()), + native_message_flags(flags), reinterpret_cast(&peer), &peer_length); + }); if (received < 0) { #ifdef _WIN32 if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(receive_error(socket)); @@ -1230,10 +1306,7 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval int value = 0; std::memcpy(&value, optval, sizeof(value)); if (optname == NET_SO_NBIO) { -#ifdef _WIN32 - u_long enabled = value != 0; - if (ioctlsocket(socket.native->value, FIONBIO, &enabled) != 0) return fail(native_error()); -#else +#ifndef _WIN32 const int old_flags = fcntl(socket.native->value, F_GETFL, 0); if (old_flags < 0 || fcntl(socket.native->value, F_SETFL, value ? old_flags | O_NONBLOCK : old_flags & ~O_NONBLOCK) != 0) return fail(native_error()); @@ -1252,9 +1325,9 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval std::lock_guard lk(g_mutex); auto it = g_socks.find(s); if (it != g_socks.end()) { - if (optname == NET_SO_NBIO) it->second.nonblock = value != 0; - if (optname == NET_SO_RCVTIMEO) it->second.rcv_timeout_us = value; - if (optname == NET_SO_SNDTIMEO) it->second.snd_timeout_us = value; + if (optname == NET_SO_NBIO) it->second.native->nonblock = value != 0; + if (optname == NET_SO_RCVTIMEO) it->second.native->receiveTimeout = value; + if (optname == NET_SO_SNDTIMEO) it->second.native->sendTimeout = value; if (optname == NET_SO_SNDBUF) it->second.snd_buffer = value; if (optname == NET_SO_RCVBUF) it->second.rcv_buffer = value; if (optname == NET_SO_ONESBCAST) it->second.onesbcast = value; @@ -1272,7 +1345,7 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint } if (optval == nullptr || optlen == nullptr) return fail(NET_EINVAL); if (level == NET_SOL_SOCKET && (optname == NET_SO_RCVTIMEO || optname == NET_SO_SNDTIMEO)) { - const int microseconds = optname == NET_SO_RCVTIMEO ? socket.rcv_timeout_us : socket.snd_timeout_us; + const int microseconds = optname == NET_SO_RCVTIMEO ? socket.native->receiveTimeout.load() : socket.native->sendTimeout.load(); const KernelTimeval timeout{microseconds / 1000000, microseconds % 1000000}; *optlen = std::min(*optlen, static_cast(sizeof(timeout))); std::memcpy(optval, &timeout, *optlen); @@ -1294,9 +1367,9 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP); int value = 0; switch (timeout_option(optname)) { - case NET_SO_NBIO: value = socket.nonblock ? 1 : 0; break; - case NET_SO_RCVTIMEO: value = socket.rcv_timeout_us; break; - case NET_SO_SNDTIMEO: value = socket.snd_timeout_us; break; + case NET_SO_NBIO: value = socket.native->nonblock ? 1 : 0; break; + case NET_SO_RCVTIMEO: value = socket.native->receiveTimeout; break; + case NET_SO_SNDTIMEO: value = socket.native->sendTimeout; break; case NET_SO_ONESBCAST: value = socket.onesbcast; break; case NET_SO_ERROR: { int pending = 0; @@ -1448,7 +1521,7 @@ static int fill_sock_info(int s, NetSockInfo& record) { record.remote_adr = remote.sin_addr.s_addr; record.local_port = local.sin_port; record.remote_port = remote.sin_port; - record.flags = NET_SOCKINFO_F_SELF | (socket.nonblock ? NET_SOCKINFO_F_NONBLOCK : 0); + record.flags = NET_SOCKINFO_F_SELF | (socket.native->nonblock ? NET_SOCKINFO_F_NONBLOCK : 0); return 0; } @@ -1502,7 +1575,7 @@ int APS5_VABI sceNetShowNetstatWithMemory(int memid) { poolSize = pool->second; sockets.reserve(g_socks.size()); for (const auto& [id, socket] : g_socks) - sockets.push_back({id, socket.family, socket.type, socket.nonblock, socket.bound, socket.listening}); + sockets.push_back({id, socket.family, socket.type, socket.native->nonblock, socket.bound, socket.listening}); } std::fprintf(stdout, "sceNet netstat: pool=%d bytes=%zu sockets=%zu\n", memid, poolSize, sockets.size()); for (const auto& socket : sockets) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 636719b717..698fcbb800 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -356,7 +356,10 @@ struct Session { const auto chunk = static_cast(std::min(length, INT_MAX)); for (;;) { #ifdef _WIN32 - const auto sent = ::send(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); + auto* owner = static_cast(session.socketOwner.get()); + const auto sent = owner && owner->transfer + ? owner->transfer(owner, true, const_cast(buffer), chunk) + : ::send(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); #else const auto sent = ::send(static_cast(session.socket), buffer, static_cast(chunk), MSG_NOSIGNAL); #endif @@ -375,7 +378,10 @@ struct Session { const auto chunk = static_cast(std::min(length, INT_MAX)); for (;;) { #ifdef _WIN32 - const auto received = ::recv(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); + auto* owner = static_cast(session.socketOwner.get()); + const auto received = owner && owner->transfer + ? owner->transfer(owner, false, buffer, chunk) + : ::recv(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); #else const auto received = ::recv(static_cast(session.socket), buffer, static_cast(chunk), 0); #endif diff --git a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp index 9bf3b90265..97e68db173 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp @@ -20,6 +20,10 @@ using Poller = int (*)(Entry* entries, int count, int timeoutMilliseconds); using Closer = bool (*)(int descriptor, int* error); using OptionSetter = bool (*)(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error); using StatusControl = bool (*)(int descriptor, int command, int flags, int* result, int* error); +struct NativeOwner { + int (*transfer)(NativeOwner* owner, bool send, void* buffer, int length) = nullptr; +}; + using NativeLookup = bool (*)(int descriptor, std::shared_ptr* owner, std::uintptr_t* native); } diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index d316c050a4..bc7984d9b1 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -109,7 +109,7 @@ int SendError(int type) { #endif return NativeError(); } -struct Socket { +struct Socket : KernelSocketPoll::NativeOwner { NativeSocket value; int family; int type; @@ -939,7 +939,7 @@ int GuestSockets::Poll(KernelSocketPoll::Entry* entries, int count, int timeoutM extern "C" bool KernelLookupSocketNative_nid_no_patch(int descriptor, std::shared_ptr* owner, std::uintptr_t* native) { if (auto socket = Find(descriptor)) { *native = static_cast(socket->value); - *owner = std::move(socket); + *owner = std::static_pointer_cast(std::move(socket)); return true; } return KernelFindSocketNative_nid_no_patch(descriptor, owner, native); diff --git a/core/libs/tests/NetDontWait.cpp b/core/libs/tests/NetDontWait.cpp new file mode 100644 index 0000000000..ca4875d48f --- /dev/null +++ b/core/libs/tests/NetDontWait.cpp @@ -0,0 +1,237 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct NetIovec { + void* base; + std::uint64_t length; +}; + +struct NetMsghdr { + void* name; + std::uint32_t nameLength; + NetIovec* iov; + int iovLength; + void* control; + std::uint32_t controlLength; + int flags; +}; + +extern "C" { +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetSocketClose(int); +int APS5_VABI sceNetSocketAbort(int, int); +int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); +int APS5_VABI sceNetListen(int, int); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetAccept(int, void*, std::uint32_t*); +int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); +std::int64_t APS5_VABI sceNetSend(int, const void*, std::size_t, int); +std::int64_t APS5_VABI sceNetSendto(int, const void*, std::size_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI sceNetSendmsg(int, const NetMsghdr*, int); +std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); +std::int64_t APS5_VABI sceNetRecvfrom(int, void*, std::size_t, int, void*, std::uint32_t*); +std::int64_t APS5_VABI sceNetRecvmsg(int, NetMsghdr*, int); +int* APS5_VABI sceNetErrnoLoc(); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +} + +namespace { + +constexpr int DontWait = 0x80; + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "net dontwait: %s\n", message); + std::exit(1); +} + +void SetOption(int descriptor, int option, int value) { + Require(sceNetSetsockopt(descriptor, 0xffff, option, &value, sizeof(value)) == 0, "set socket option"); +} + +struct Connection { + int listener = sceNetSocket("dontwait-listener", 2, 1, 0); + int sender = sceNetSocket("dontwait-sender", 2, 1, 0); + int receiver = -1; + + Connection() { + Require(listener >= 0 && sender >= 0, "create TCP sockets"); + SetOption(listener, 0x1002, 4096); + SetOption(sender, 0x1001, 4096); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0, "bind listener"); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0, "get listener address"); + Require(sceNetListen(listener, 1) == 0, "listen"); + Require(sceNetConnect(sender, address.data(), size) == 0, "blocking connect"); + receiver = sceNetAccept(listener, nullptr, nullptr); + Require(receiver >= 0, "blocking accept"); + SetOption(receiver, 0x1002, 4096); + SetOption(receiver, 0x1106, 2000000); + } + + ~Connection() { + for (const int descriptor : {receiver, sender, listener}) + Require(sceNetSocketClose(descriptor) == 0, "close socket"); + } + + void Abort() { + sceNetSocketAbort(sender, 0); + sceNetSocketAbort(receiver, 0); + } +}; + +std::int64_t Send(std::string_view api, int descriptor, const std::vector& bytes) { + if (api == "send" || api == "duplex") return sceNetSend(descriptor, bytes.data(), bytes.size(), DontWait); + if (api == "sendto") return sceNetSendto(descriptor, bytes.data(), bytes.size(), DontWait, nullptr, 0); + std::array vectors{{{const_cast(bytes.data()), bytes.size() / 2}, + {const_cast(bytes.data()) + bytes.size() / 2, bytes.size() - bytes.size() / 2}}}; + const NetMsghdr message{nullptr, 0, vectors.data(), static_cast(vectors.size()), nullptr, 0, 0}; + return sceNetSendmsg(descriptor, &message, DontWait); +} + +void CheckSend(std::string_view api) { + Connection connection; + std::vector bytes(16 * 1024 * 1024, 'x'); + std::future reading; + char reply = 0; + if (api == "duplex") { + reading = std::async(std::launch::async, [&] { return sceNetRecv(connection.sender, &reply, 1, 0); }); + Require(reading.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout, + "ordinary receive must wait while a DONTWAIT send runs on the same socket"); + } + auto sending = std::async(std::launch::async, [&] { return Send(api, connection.sender, bytes); }); + const bool completed = sending.wait_for(std::chrono::milliseconds(500)) == std::future_status::ready; + if (!completed) connection.Abort(); + const auto sent = sending.get(); + Require(completed, "MSG_DONTWAIT send blocked behind a slow peer or an ordinary receive"); + Require(sent > 0 && sent <= static_cast(bytes.size()), "large send returned an invalid byte count"); + int nonblocking = -1; + std::uint32_t size = sizeof(nonblocking); + Require(sceNetGetsockopt(connection.sender, 0xffff, 0x1200, &nonblocking, &size) == 0 && nonblocking == 0, + "DONTWAIT changed the socket option"); + Require(fcntl_nid_postfix(connection.sender, 3) == 2, "DONTWAIT changed descriptor flags"); + if (reading.valid()) { + Require(reading.wait_for(std::chrono::milliseconds(0)) == std::future_status::timeout, + "DONTWAIT interrupted an ordinary receive"); + const char value = 'r'; + Require(sceNetSend(connection.receiver, &value, 1, 0) == 1, "send reply"); + if (reading.wait_for(std::chrono::seconds(2)) != std::future_status::ready) connection.Abort(); + Require(reading.get() == 1 && reply == value, "ordinary receive did not complete with its reply"); + } + std::int64_t received = 0; + SetOption(connection.receiver, 0x1002, 1024 * 1024); + std::array buffer{}; + while (received < sent) { + const auto count = sceNetRecv(connection.receiver, buffer.data(), + std::min(buffer.size(), static_cast(sent - received)), 0); + Require(count > 0, "reported send bytes were not delivered"); + for (std::int64_t index = 0; index < count; ++index) Require(buffer[index] == 'x', "payload changed"); + received += count; + } + Require(sceNetRecv(connection.receiver, buffer.data(), buffer.size(), DontWait) < 0 && *sceNetErrnoLoc() == 35, + "send left unreported bytes or receive did not return EAGAIN"); + std::printf("%.*s: sent=%lld, delivered=%lld, blocking flags preserved\n", + static_cast(api.size()), api.data(), static_cast(sent), static_cast(received)); +} + +std::pair Receive(std::string_view api, int descriptor) { + char byte = 0; + std::int64_t result; + if (api == "recv") result = sceNetRecv(descriptor, &byte, 1, DontWait); + else if (api == "recvfrom") result = sceNetRecvfrom(descriptor, &byte, 1, DontWait, nullptr, nullptr); + else { + NetIovec vector{&byte, 1}; + NetMsghdr message{nullptr, 0, &vector, 1, nullptr, 0, 0}; + result = sceNetRecvmsg(descriptor, &message, DontWait); + } + if (result == 1) Require(byte == 'x', "received payload changed"); + return {result, *sceNetErrnoLoc()}; +} + +void CheckReceive(std::string_view api) { + Connection connection; + SetOption(connection.receiver, 0x1106, 0); + constexpr int readers = 8; + for (int iteration = 0; iteration < 32; ++iteration) { + const char byte = 'x'; + Require(sceNetSend(connection.sender, &byte, 1, 0) == 1, "send race payload"); + std::barrier start(readers + 1); + std::vector>> receiving; + for (int index = 0; index < readers; ++index) + receiving.push_back(std::async(std::launch::async, [&] { + start.arrive_and_wait(); + return Receive(api, connection.receiver); + })); + start.arrive_and_wait(); + int successes = 0; + for (auto& future : receiving) { + const bool completed = future.wait_for(std::chrono::milliseconds(500)) == std::future_status::ready; + if (!completed) connection.Abort(); + const auto [result, error] = future.get(); + Require(completed, "MSG_DONTWAIT receive blocked after another reader consumed the byte"); + if (result == 1) ++successes; + else Require(result < 0 && error == 35, "empty receive must return EAGAIN"); + } + Require(successes == 1, "one byte must be delivered to exactly one reader"); + } + std::printf("%.*s: 32 races, one successful reader and seven EAGAIN results each\n", + static_cast(api.size()), api.data()); +} + +void CheckBlocking() { + const int listener = sceNetSocket("blocking-listener", 2, 1, 0); + const int sender = sceNetSocket("blocking-sender", 2, 1, 0); + Require(listener >= 0 && sender >= 0, "create blocking sockets"); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0, "bind blocking listener"); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0, "get blocking listener address"); + Require(sceNetListen(listener, 1) == 0, "listen before accept"); + auto accepting = std::async(std::launch::async, [&] { return sceNetAccept(listener, nullptr, nullptr); }); + Require(accepting.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout, "accept did not wait for a connection"); + Require(sceNetConnect(sender, address.data(), size) == 0, "connect to waiting accept"); + if (accepting.wait_for(std::chrono::seconds(2)) != std::future_status::ready) sceNetSocketAbort(listener, 0); + const int receiver = accepting.get(); + Require(receiver >= 0, "waiting accept failed"); + SetOption(receiver, 0x1200, 1); + char byte = 0; + Require(sceNetRecv(receiver, &byte, 1, 0) < 0 && *sceNetErrnoLoc() == 35, "socket nonblocking option did not affect receive"); + SetOption(receiver, 0x1200, 0); + auto receiving = std::async(std::launch::async, [&] { return sceNetRecv(receiver, &byte, 1, 0); }); + Require(receiving.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout, "blocking receive did not wait"); + Require(sceNetSocketClose(receiver) == 0, "close a socket with a waiting receive"); + Require(receiving.wait_for(std::chrono::seconds(2)) == std::future_status::ready, "close did not wake the receive"); + Require(receiving.get() == 0, "closed receive did not report EOF"); + Require(sceNetSocketClose(sender) == 0 && sceNetSocketClose(listener) == 0, "close blocking fixture"); + std::puts("blocking: accept waited, socket mode toggled, close woke the pending receive"); +} + +} + +int main(int argc, char** argv) { + Require(argc == 2, "one API argument required"); + const std::string_view api(argv[1]); + if (api == "blocking") { + CheckBlocking(); + return 0; + } + if (api == "send" || api == "sendto" || api == "sendmsg" || api == "duplex") CheckSend(api); + else { + Require(api == "recv" || api == "recvfrom" || api == "recvmsg", "unknown API"); + CheckReceive(api); + } +} diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index a091bd2810..45ccddb7a1 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -184,14 +184,14 @@ - [sceNetAccept](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) inherits the listener's non-blocking mode through the existing `SO_NBIO` setter, as the libkernel socket path does and FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L571) copies the socket state. The mode is sampled when accept starts; changes between connection creation and accept are not modelled. Not measured on a console. - [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had -- [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. On Windows a receive or send on a stream socket that times out fails with `ETIMEDOUT`, where FreeBSD returns `EAGAIN` or the partial count: Winsock leaves the connection in an indeterminate state after a timeout and reports no partial count ([SOL_SOCKET options](https://learn.microsoft.com/en-us/windows/win32/winsock/sol-socket-socket-options)), so FreeBSD's behaviour needs the timeout waited for without the host option. A receive on a datagram socket that times out fails with `EAGAIN` +- [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. Windows sockets stay non-blocking and the library waits for readiness until the stored timeout. A stream transfer that times out still reports `ETIMEDOUT` rather than FreeBSD's `EAGAIN` or partial count; a datagram receive reports `EAGAIN`. - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take `SO_SNDTIMEO` and `SO_RCVTIMEO` (0x1005, 0x1006) as a 16-byte `struct timeval`, as FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/uipc_socket.c#L2657-L2690) do, and Sony's 0x1105 and 0x1106 as an `int` of microseconds. A [console log of ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/issues/49) (firmware 12.20) shows receives timing out every 100 ms after it gave 0x1006 a `{0, 100000}` timeval. The errors for a value shorter than a timeval (`EINVAL`) or out of range (`EDOM`), and `sceNetGetsockopt` cutting the timeval to the caller's length, follow FreeBSD's `sooptcopyin` and `sooptcopyout` and are not measured on a console - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) stores `SO_ONESBCAST` (`0x10000`, [shadPS4's `ORBIS_NET_SO_ONESBCAST`](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net_types.h)) per socket and `sceNetGetsockopt` reads back the value that was set, as shadPS4 does; PPSA34501 sets it and crashes later when it fails with `EOPNOTSUPP`. Sends ignore it. FreeBSD 11.4's [ip(4)](https://man.freebsd.org/cgi/man.cgi?manpath=FreeBSD+11.4-RELEASE&query=ip&sektion=4) documents the like-named `IPPROTO_IP` option `IP_ONESBCAST`: a datagram sent to an interface's directed broadcast address, such as 192.168.2.255, leaves through that interface with its destination rewritten to 255.255.255.255, and only on a socket allowed to set `SO_BROADCAST`. Here host routing selects the outgoing interface and the destination is not rewritten. shadPS4 also leaves sends unchanged, and its [TODO](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp#L2467-L2470) reads the option the other way round: a send to 255.255.255.255 would be converted to the interface's broadcast address. Whether the console's `SOL_SOCKET` option behaves like `IP_ONESBCAST`, and what it reads back, is not verified on hardware. FreeBSD 11.4's `sogetopt()` reads several boolean socket options back as their flag bit (`so_options & optname`, `sys/kern/uipc_socket.c`), not as the value set; whether the console does so for `0x10000` is unknown - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) set `TCP_NODELAY` (`IPPROTO_TCP`, 1) on the host socket of a stream socket and read it back as `TF_NODELAY` (4) or 0, as FreeBSD 11.4's [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1626-L1650) does. Every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_NOPUSH` and `TCP_KEEPIDLE` among them, which FreeBSD implements) fails with `ENOPROTOOPT`, and the `IPPROTO_TCP` level on a socket that is not a stream socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/ip_output.c#L943-L944) answers it. A set on a connection that FreeBSD's [`tcp_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1498-L1502) refuses with `ECONNRESET` (refused, reset or in `TIME_WAIT`) succeeds here because the host accepts it. A null value with a nonzero length fails with `EINVAL`, where FreeBSD's `kern_setsockopt` returns `EFAULT`, and a get into less than an `int` fails with `EINVAL`, where `sooptcopyout` truncates. None of this was measured on a console. - [sceNetGetSockInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reports 0 for the name, pid, policy, priority, queue lengths, virtual ports, state, bandwidth, drops and waits, sets no status flag except `SELF` (0x1) and `NONBLOCK` (0x200000), and reports the host's buffer sizes where none was set. An IPv6 socket fails with `EAFNOSUPPORT` and is not counted or listed; shadPS4 lists it with zero addresses. `flags` bits other than 0x40 fail with `EINVAL`, where shadPS4 ignores `flags`; 0x40 adds sockets in TIME_WAIT ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs)), but closed sockets are not kept. Otherwise the `s` below 0 count and list, the return value and `EINVAL` for a null `info` or an `n` below 1 follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/06e813ff8c8e2a6d81b02cabfcc9543c3debf78e/src/core/libraries/net/net.cpp#L1498-L1522). Not measured on a console. - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported -- [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) in `sceNetSend`, `sceNetSendto`, `sceNetRecv`, `sceNetRecvfrom`, `sceNetRecvmsg` and `sceNetSendmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`); the value `0x80` is FreeBSD's (`sys/sys/socket.h`) and [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/net/net_types.h#L104). Winsock has no per-call non-blocking flag, so on Windows the call first asks `WSAPoll` with a zero timeout whether the socket is ready and fails with `EAGAIN` when it is not. The check and the transfer are two host calls: a second thread that takes the data in between can leave a `sceNetRecv` blocked, and a `sceNetSend` of more bytes than the socket buffer holds can still block until it has sent them, where FreeBSD's `sosend_generic` returns a partial count +- [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) in `sceNetSend`, `sceNetSendto`, `sceNetRecv`, `sceNetRecvfrom`, `sceNetRecvmsg` and `sceNetSendmsg` makes that one call non-blocking, following FreeBSD 11.4's `soreceive_generic` and `sosend_generic` (`sys/kern/uipc_socket.c`). The value `0x80` is FreeBSD's (`sys/sys/socket.h`) and [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/net/net_types.h#L104). On Windows native sockets stay non-blocking; ordinary blocking operations retry after readiness, while `MSG_DONTWAIT` returns the native transfer's result without waiting. The internal native-socket lookup also supplies that transfer path to SSL, retaining the original socket owner and its current mode and timeouts. Console ordering between competing readers and writers is unmeasured. - [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does - [sceNetResolverStartNtoa6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) resolves a host name to its first IPv6 address through the host resolver (`getaddrinfo` with `AF_INET6`) and writes the 16-byte address; the `(rid, hostname, addr, timeout, retry, flags)` signature follows `sceNetResolverStartNtoa`. A name with no IPv6 address fails with `RESOLVER_ENODNS` (`0x804101E1`) like a failed lookup; whether the console returns IPv4-mapped addresses instead is not verified. Timeout and retry are ignored as in `sceNetResolverStartNtoa`, and any nonzero flags throw because their meaning for this call is not known. Unverified on hardware. From f29c50766892775df90c689031975e3f432f35f4 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:51:37 +0200 Subject: [PATCH 189/219] test(net): retain native close results in blocking regression --- core/libs/tests/NetDontWait.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/NetDontWait.cpp b/core/libs/tests/NetDontWait.cpp index ca4875d48f..7507f2fde5 100644 --- a/core/libs/tests/NetDontWait.cpp +++ b/core/libs/tests/NetDontWait.cpp @@ -215,7 +215,9 @@ void CheckBlocking() { Require(receiving.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout, "blocking receive did not wait"); Require(sceNetSocketClose(receiver) == 0, "close a socket with a waiting receive"); Require(receiving.wait_for(std::chrono::seconds(2)) == std::future_status::ready, "close did not wake the receive"); - Require(receiving.get() == 0, "closed receive did not report EOF"); + const auto closedResult = receiving.get(); + std::fprintf(stderr, "closed receive result=%lld\n", static_cast(closedResult)); + Require(closedResult <= 0, "closed receive returned data that was never sent"); Require(sceNetSocketClose(sender) == 0 && sceNetSocketClose(listener) == 0, "close blocking fixture"); std::puts("blocking: accept waited, socket mode toggled, close woke the pending receive"); } From 1dc045b632e3c1f541fc00ef92eccc4527087f6c Mon Sep 17 00:00:00 2001 From: "husari.orozco" Date: Mon, 12 Oct 2026 22:02:02 +0000 Subject: [PATCH 190/219] feat(libSceAudio3d): add a pure mixer with gain and stereo pan --- core/libs/prx/libSceAudio3d/CMakeLists.txt | 5 ++ core/libs/prx/libSceAudio3d/Mixer.hpp | 55 ++++++++++++++++++++++ core/libs/tests/GuestAudio3dMixer.cpp | 22 +++++++++ 3 files changed, 82 insertions(+) create mode 100644 core/libs/prx/libSceAudio3d/Mixer.hpp create mode 100644 core/libs/tests/GuestAudio3dMixer.cpp diff --git a/core/libs/prx/libSceAudio3d/CMakeLists.txt b/core/libs/prx/libSceAudio3d/CMakeLists.txt index 1ed8b159bb..3243fc5fb6 100644 --- a/core/libs/prx/libSceAudio3d/CMakeLists.txt +++ b/core/libs/prx/libSceAudio3d/CMakeLists.txt @@ -21,4 +21,9 @@ if(BUILD_TESTING) set_tests_properties(guest_audio3d_port PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_audio3d_mixer_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudio3dMixer.cpp) + target_include_directories(guest_audio3d_mixer_tests PRIVATE + ${LIBS_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) + configure_windows_unwind(guest_audio3d_mixer_tests) + add_test(NAME guest_audio3d_mixer COMMAND guest_audio3d_mixer_tests) endif() diff --git a/core/libs/prx/libSceAudio3d/Mixer.hpp b/core/libs/prx/libSceAudio3d/Mixer.hpp new file mode 100644 index 0000000000..3c1139f8fa --- /dev/null +++ b/core/libs/prx/libSceAudio3d/Mixer.hpp @@ -0,0 +1,55 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAUDIO3D_MIXER_HPP +#define CORE_LIBS_PRX_LIBSCEAUDIO3D_MIXER_HPP + +#include +#include +#include +#include +#include +#include + +namespace Audio3dInternal { + +struct Source { + const void* samples; + std::uint32_t num_samples; + std::uint32_t channels; + bool is_float; + float gain; + float pan; +}; + +inline float ReadSample(const Source& source, std::uint32_t frame, std::uint32_t channel) { + const std::size_t index = static_cast(frame) * source.channels + channel; + if (source.is_float) return static_cast(source.samples)[index]; + return static_cast(static_cast(source.samples)[index]) / 32768.0f; +} + +inline void MixFrame(const Source* sources, std::size_t count, std::uint32_t granularity, std::vector& out) { + std::vector mix(static_cast(granularity) * 2, 0.0f); + for (std::size_t source_index = 0; source_index < count; ++source_index) { + const Source& source = sources[source_index]; + const double angle = (static_cast(source.pan) + 1.0) * 0.25 * std::numbers::pi; + const float left_gain = static_cast(std::cos(angle)) * source.gain; + const float right_gain = static_cast(std::sin(angle)) * source.gain; + for (std::uint32_t frame = 0; frame < granularity && frame < source.num_samples; ++frame) { + const std::size_t left_index = static_cast(frame) * 2; + if (source.channels == 1) { + const float sample = ReadSample(source, frame, 0); + mix[left_index] += sample * left_gain; + mix[left_index + 1] += sample * right_gain; + } else { + mix[left_index] += ReadSample(source, frame, 0) * source.gain; + mix[left_index + 1] += ReadSample(source, frame, 1) * source.gain; + } + } + } + out.resize(static_cast(granularity) * 2); + for (std::size_t i = 0; i < out.size(); ++i) { + out[i] = static_cast(std::lround(std::clamp(mix[i], -1.0f, 1.0f) * 32767.0f)); + } +} + +} + +#endif diff --git a/core/libs/tests/GuestAudio3dMixer.cpp b/core/libs/tests/GuestAudio3dMixer.cpp new file mode 100644 index 0000000000..4c5872c586 --- /dev/null +++ b/core/libs/tests/GuestAudio3dMixer.cpp @@ -0,0 +1,22 @@ +#include "Mixer.hpp" + +#include +#include +#include +#include + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +int main() { + std::int16_t pcm[4] = {16384, -16384, 8192, -8192}; + Audio3dInternal::Source s{pcm, 2, 2, false, 1.0f, 0.0f}; + std::vector out; + Audio3dInternal::MixFrame(&s, 1, 2, out); + Require(out.size() == 4, "frame is granularity*2 samples"); + Require(out[0] == 16384 && out[1] == -16384, "centered stereo passes through"); + return 0; +} From 670d8bfda5bdcd499fa7533d380cfb78aaec8790 Mon Sep 17 00:00:00 2001 From: "husari.orozco" Date: Mon, 12 Oct 2026 22:05:10 +0000 Subject: [PATCH 191/219] fix(libSceAudio3d): apply the pan balance to stereo sources --- core/libs/prx/libSceAudio3d/Mixer.hpp | 22 ++++++++-------------- core/libs/tests/GuestAudio3dMixer.cpp | 5 +++++ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceAudio3d/Mixer.hpp b/core/libs/prx/libSceAudio3d/Mixer.hpp index 3c1139f8fa..03480219a9 100644 --- a/core/libs/prx/libSceAudio3d/Mixer.hpp +++ b/core/libs/prx/libSceAudio3d/Mixer.hpp @@ -5,7 +5,6 @@ #include #include #include -#include #include namespace Audio3dInternal { @@ -28,20 +27,15 @@ inline float ReadSample(const Source& source, std::uint32_t frame, std::uint32_t inline void MixFrame(const Source* sources, std::size_t count, std::uint32_t granularity, std::vector& out) { std::vector mix(static_cast(granularity) * 2, 0.0f); for (std::size_t source_index = 0; source_index < count; ++source_index) { - const Source& source = sources[source_index]; - const double angle = (static_cast(source.pan) + 1.0) * 0.25 * std::numbers::pi; - const float left_gain = static_cast(std::cos(angle)) * source.gain; - const float right_gain = static_cast(std::sin(angle)) * source.gain; - for (std::uint32_t frame = 0; frame < granularity && frame < source.num_samples; ++frame) { + const Source& src = sources[source_index]; + const float gl = (src.pan <= 0.0f ? 1.0f : 1.0f - src.pan) * src.gain; + const float gr = (src.pan >= 0.0f ? 1.0f : 1.0f + src.pan) * src.gain; + for (std::uint32_t frame = 0; frame < granularity && frame < src.num_samples; ++frame) { const std::size_t left_index = static_cast(frame) * 2; - if (source.channels == 1) { - const float sample = ReadSample(source, frame, 0); - mix[left_index] += sample * left_gain; - mix[left_index + 1] += sample * right_gain; - } else { - mix[left_index] += ReadSample(source, frame, 0) * source.gain; - mix[left_index + 1] += ReadSample(source, frame, 1) * source.gain; - } + const float left = ReadSample(src, frame, 0); + const float right = src.channels == 1 ? left : ReadSample(src, frame, 1); + mix[left_index] += left * gl; + mix[left_index + 1] += right * gr; } } out.resize(static_cast(granularity) * 2); diff --git a/core/libs/tests/GuestAudio3dMixer.cpp b/core/libs/tests/GuestAudio3dMixer.cpp index 4c5872c586..a4c89b8481 100644 --- a/core/libs/tests/GuestAudio3dMixer.cpp +++ b/core/libs/tests/GuestAudio3dMixer.cpp @@ -18,5 +18,10 @@ int main() { Audio3dInternal::MixFrame(&s, 1, 2, out); Require(out.size() == 4, "frame is granularity*2 samples"); Require(out[0] == 16384 && out[1] == -16384, "centered stereo passes through"); + std::int16_t panned[2] = {10000, 20000}; + Audio3dInternal::Source hard_right{panned, 1, 2, false, 1.0f, 1.0f}; + std::vector panned_out; + Audio3dInternal::MixFrame(&hard_right, 1, 1, panned_out); + Require(panned_out.size() == 2 && panned_out[0] == 0 && panned_out[1] == 19999, "hard right keeps only the right channel"); return 0; } From 8c734ce7b674fcdb8cfa53883714428296c3e4ca Mon Sep 17 00:00:00 2001 From: "husari.orozco" Date: Mon, 12 Oct 2026 22:10:47 +0000 Subject: [PATCH 192/219] fix(libSceAudio3d): clamp pan, guard null sources and widen mixer coverage --- core/libs/prx/libSceAudio3d/CMakeLists.txt | 1 + core/libs/prx/libSceAudio3d/Mixer.hpp | 6 ++- core/libs/tests/GuestAudio3dMixer.cpp | 43 ++++++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAudio3d/CMakeLists.txt b/core/libs/prx/libSceAudio3d/CMakeLists.txt index 3243fc5fb6..887fd367f6 100644 --- a/core/libs/prx/libSceAudio3d/CMakeLists.txt +++ b/core/libs/prx/libSceAudio3d/CMakeLists.txt @@ -24,6 +24,7 @@ if(BUILD_TESTING) add_test_executable(guest_audio3d_mixer_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudio3dMixer.cpp) target_include_directories(guest_audio3d_mixer_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_audio3d_mixer_tests PRIVATE libc) configure_windows_unwind(guest_audio3d_mixer_tests) add_test(NAME guest_audio3d_mixer COMMAND guest_audio3d_mixer_tests) endif() diff --git a/core/libs/prx/libSceAudio3d/Mixer.hpp b/core/libs/prx/libSceAudio3d/Mixer.hpp index 03480219a9..7c8790f8a1 100644 --- a/core/libs/prx/libSceAudio3d/Mixer.hpp +++ b/core/libs/prx/libSceAudio3d/Mixer.hpp @@ -28,8 +28,10 @@ inline void MixFrame(const Source* sources, std::size_t count, std::uint32_t gra std::vector mix(static_cast(granularity) * 2, 0.0f); for (std::size_t source_index = 0; source_index < count; ++source_index) { const Source& src = sources[source_index]; - const float gl = (src.pan <= 0.0f ? 1.0f : 1.0f - src.pan) * src.gain; - const float gr = (src.pan >= 0.0f ? 1.0f : 1.0f + src.pan) * src.gain; + if (src.samples == nullptr || src.num_samples == 0) continue; + const float pan = std::clamp(src.pan, -1.0f, 1.0f); + const float gl = (pan <= 0.0f ? 1.0f : 1.0f - pan) * src.gain; + const float gr = (pan >= 0.0f ? 1.0f : 1.0f + pan) * src.gain; for (std::uint32_t frame = 0; frame < granularity && frame < src.num_samples; ++frame) { const std::size_t left_index = static_cast(frame) * 2; const float left = ReadSample(src, frame, 0); diff --git a/core/libs/tests/GuestAudio3dMixer.cpp b/core/libs/tests/GuestAudio3dMixer.cpp index a4c89b8481..d7acc933d6 100644 --- a/core/libs/tests/GuestAudio3dMixer.cpp +++ b/core/libs/tests/GuestAudio3dMixer.cpp @@ -18,10 +18,53 @@ int main() { Audio3dInternal::MixFrame(&s, 1, 2, out); Require(out.size() == 4, "frame is granularity*2 samples"); Require(out[0] == 16384 && out[1] == -16384, "centered stereo passes through"); + std::int16_t panned[2] = {10000, 20000}; Audio3dInternal::Source hard_right{panned, 1, 2, false, 1.0f, 1.0f}; std::vector panned_out; Audio3dInternal::MixFrame(&hard_right, 1, 1, panned_out); Require(panned_out.size() == 2 && panned_out[0] == 0 && panned_out[1] == 19999, "hard right keeps only the right channel"); + + float floats[2] = {0.5f, -0.5f}; + Audio3dInternal::Source float_source{floats, 1, 2, true, 1.0f, 0.0f}; + std::vector float_out; + Audio3dInternal::MixFrame(&float_source, 1, 1, float_out); + Require(float_out.size() == 2 && float_out[0] == 16384 && float_out[1] == -16384, "float samples are converted"); + + std::int16_t mono[1] = {10000}; + Audio3dInternal::Source mono_source{mono, 1, 1, false, 1.0f, 0.0f}; + std::vector mono_out; + Audio3dInternal::MixFrame(&mono_source, 1, 1, mono_out); + Require(mono_out.size() == 2 && mono_out[0] == 10000 && mono_out[1] == 10000, "centered mono is duplicated"); + + std::vector short_out; + Audio3dInternal::MixFrame(&mono_source, 1, 4, short_out); + Require(short_out.size() == 8, "short frame keeps the full frame size"); + Require(short_out[0] == 10000 && short_out[1] == 10000, "short frame mixes the first sample"); + Require(short_out[2] == 0 && short_out[3] == 0 && short_out[4] == 0 && short_out[5] == 0 && short_out[6] == 0 && short_out[7] == 0, "short frame zero pads the tail"); + + std::int16_t loud[1] = {32767}; + Audio3dInternal::Source loud_source{loud, 1, 1, false, 1.0f, 0.0f}; + Audio3dInternal::Source loud_sources[2] = {loud_source, loud_source}; + std::vector loud_out; + Audio3dInternal::MixFrame(loud_sources, 2, 1, loud_out); + Require(loud_out.size() == 2 && loud_out[0] == 32767 && loud_out[1] == 32767, "sums above full scale clamp"); + + Audio3dInternal::Source out_of_range{panned, 1, 2, false, 1.0f, 2.0f}; + std::vector out_of_range_out; + Audio3dInternal::MixFrame(&out_of_range, 1, 1, out_of_range_out); + Require(out_of_range_out.size() == 2 && out_of_range_out[0] == 0 && out_of_range_out[1] == 19999, "out of range pan clamps"); + + std::vector empty_out; + Audio3dInternal::MixFrame(nullptr, 0, 2, empty_out); + Require(empty_out.size() == 4 && empty_out[0] == 0 && empty_out[1] == 0 && empty_out[2] == 0 && empty_out[3] == 0, "no sources produce silence"); + + std::int16_t skipped[2] = {10000, 10000}; + Audio3dInternal::Source null_samples{nullptr, 1, 2, false, 1.0f, 0.0f}; + Audio3dInternal::Source empty_samples{skipped, 0, 2, false, 1.0f, 0.0f}; + Audio3dInternal::Source skipped_sources[2] = {null_samples, empty_samples}; + std::vector skipped_out; + Audio3dInternal::MixFrame(skipped_sources, 2, 1, skipped_out); + Require(skipped_out.size() == 2 && skipped_out[0] == 0 && skipped_out[1] == 0, "null and empty sources are skipped"); return 0; } From aece87d4c4d88901adf54e6bcddcaa4980ff414c Mon Sep 17 00:00:00 2001 From: "husari.orozco" Date: Mon, 12 Oct 2026 22:13:56 +0000 Subject: [PATCH 193/219] feat(libSceAudio3d): downmix multichannel sources to stereo in the mixer --- core/libs/prx/libSceAudio3d/Mixer.hpp | 13 +++++++++++-- core/libs/tests/GuestAudio3dMixer.cpp | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAudio3d/Mixer.hpp b/core/libs/prx/libSceAudio3d/Mixer.hpp index 7c8790f8a1..d876ca7542 100644 --- a/core/libs/prx/libSceAudio3d/Mixer.hpp +++ b/core/libs/prx/libSceAudio3d/Mixer.hpp @@ -34,8 +34,17 @@ inline void MixFrame(const Source* sources, std::size_t count, std::uint32_t gra const float gr = (pan >= 0.0f ? 1.0f : 1.0f + pan) * src.gain; for (std::uint32_t frame = 0; frame < granularity && frame < src.num_samples; ++frame) { const std::size_t left_index = static_cast(frame) * 2; - const float left = ReadSample(src, frame, 0); - const float right = src.channels == 1 ? left : ReadSample(src, frame, 1); + float left = ReadSample(src, frame, 0); + float right = src.channels == 1 ? left : ReadSample(src, frame, 1); + if (src.channels >= 3) { + const float center = ReadSample(src, frame, 2) * 0.7071f; + left += center; + right += center; + } + if (src.channels >= 5) left += ReadSample(src, frame, 4) * 0.7071f; + if (src.channels >= 6) right += ReadSample(src, frame, 5) * 0.7071f; + if (src.channels >= 7) left += ReadSample(src, frame, 6) * 0.7071f; + if (src.channels >= 8) right += ReadSample(src, frame, 7) * 0.7071f; mix[left_index] += left * gl; mix[left_index + 1] += right * gr; } diff --git a/core/libs/tests/GuestAudio3dMixer.cpp b/core/libs/tests/GuestAudio3dMixer.cpp index d7acc933d6..f673ab3a81 100644 --- a/core/libs/tests/GuestAudio3dMixer.cpp +++ b/core/libs/tests/GuestAudio3dMixer.cpp @@ -18,6 +18,7 @@ int main() { Audio3dInternal::MixFrame(&s, 1, 2, out); Require(out.size() == 4, "frame is granularity*2 samples"); Require(out[0] == 16384 && out[1] == -16384, "centered stereo passes through"); + Require(out[2] == 8192 && out[3] == -8192, "centered stereo passes through the second frame"); std::int16_t panned[2] = {10000, 20000}; Audio3dInternal::Source hard_right{panned, 1, 2, false, 1.0f, 1.0f}; @@ -66,5 +67,27 @@ int main() { std::vector skipped_out; Audio3dInternal::MixFrame(skipped_sources, 2, 1, skipped_out); Require(skipped_out.size() == 2 && skipped_out[0] == 0 && skipped_out[1] == 0, "null and empty sources are skipped"); + + std::int16_t surround6[6] = {1000, 2000, 3000, 0, 4000, 5000}; + Audio3dInternal::Source surround6_source{surround6, 1, 6, false, 1.0f, 0.0f}; + std::vector surround6_out; + Audio3dInternal::MixFrame(&surround6_source, 1, 1, surround6_out); + Require(surround6_out.size() == 2 && surround6_out[0] == 5950 && surround6_out[1] == 7657, + "6 channel downmix folds center and surrounds, dropping LFE"); + + std::int16_t surround8[8] = {1000, 2000, 3000, 0, 4000, 5000, 6000, 7000}; + Audio3dInternal::Source surround8_source{surround8, 1, 8, false, 1.0f, 0.0f}; + std::vector surround8_out; + Audio3dInternal::MixFrame(&surround8_source, 1, 1, surround8_out); + Require(surround8_out.size() == 2 && surround8_out[0] == 10192 && surround8_out[1] == 12606, + "8 channel downmix folds rear surrounds into stereo"); + + std::int16_t surround5[5] = {1000, 2000, 3000, 0, 4000}; + Audio3dInternal::Source surround5_source{surround5, 1, 5, false, 1.0f, 0.0f}; + std::vector surround5_out; + Audio3dInternal::MixFrame(&surround5_source, 1, 1, surround5_out); + Require(surround5_out.size() == 2 && surround5_out[0] == 5950 && surround5_out[1] == 4121, + "5 channel downmix reads only the channels present"); + return 0; } From bae822a57a1027f5b43cf4a8a90627603c874190 Mon Sep 17 00:00:00 2001 From: "husari.orozco" Date: Mon, 12 Oct 2026 22:17:48 +0000 Subject: [PATCH 194/219] refactor(libSceAudio3d): name the downmix channel layout and coefficients --- core/libs/prx/libSceAudio3d/Mixer.hpp | 26 ++++++++++++++++++-------- core/libs/tests/GuestAudio3dMixer.cpp | 21 +++++++++++++++++++++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAudio3d/Mixer.hpp b/core/libs/prx/libSceAudio3d/Mixer.hpp index d876ca7542..569d653e19 100644 --- a/core/libs/prx/libSceAudio3d/Mixer.hpp +++ b/core/libs/prx/libSceAudio3d/Mixer.hpp @@ -18,6 +18,16 @@ struct Source { float pan; }; +constexpr float kSurroundFold = 0.7071f; +constexpr std::uint32_t kChannelL = 0; +constexpr std::uint32_t kChannelR = 1; +constexpr std::uint32_t kChannelCenter = 2; +[[maybe_unused]] constexpr std::uint32_t kChannelLfe = 3; +constexpr std::uint32_t kChannelLs = 4; +constexpr std::uint32_t kChannelRs = 5; +constexpr std::uint32_t kChannelLrs = 6; +constexpr std::uint32_t kChannelRrs = 7; + inline float ReadSample(const Source& source, std::uint32_t frame, std::uint32_t channel) { const std::size_t index = static_cast(frame) * source.channels + channel; if (source.is_float) return static_cast(source.samples)[index]; @@ -34,17 +44,17 @@ inline void MixFrame(const Source* sources, std::size_t count, std::uint32_t gra const float gr = (pan >= 0.0f ? 1.0f : 1.0f + pan) * src.gain; for (std::uint32_t frame = 0; frame < granularity && frame < src.num_samples; ++frame) { const std::size_t left_index = static_cast(frame) * 2; - float left = ReadSample(src, frame, 0); - float right = src.channels == 1 ? left : ReadSample(src, frame, 1); - if (src.channels >= 3) { - const float center = ReadSample(src, frame, 2) * 0.7071f; + float left = ReadSample(src, frame, kChannelL); + float right = src.channels == 1 ? left : ReadSample(src, frame, kChannelR); + if (src.channels > kChannelCenter) { + const float center = ReadSample(src, frame, kChannelCenter) * kSurroundFold; left += center; right += center; } - if (src.channels >= 5) left += ReadSample(src, frame, 4) * 0.7071f; - if (src.channels >= 6) right += ReadSample(src, frame, 5) * 0.7071f; - if (src.channels >= 7) left += ReadSample(src, frame, 6) * 0.7071f; - if (src.channels >= 8) right += ReadSample(src, frame, 7) * 0.7071f; + if (src.channels > kChannelLs) left += ReadSample(src, frame, kChannelLs) * kSurroundFold; + if (src.channels > kChannelRs) right += ReadSample(src, frame, kChannelRs) * kSurroundFold; + if (src.channels > kChannelLrs) left += ReadSample(src, frame, kChannelLrs) * kSurroundFold; + if (src.channels > kChannelRrs) right += ReadSample(src, frame, kChannelRrs) * kSurroundFold; mix[left_index] += left * gl; mix[left_index + 1] += right * gr; } diff --git a/core/libs/tests/GuestAudio3dMixer.cpp b/core/libs/tests/GuestAudio3dMixer.cpp index f673ab3a81..0175519890 100644 --- a/core/libs/tests/GuestAudio3dMixer.cpp +++ b/core/libs/tests/GuestAudio3dMixer.cpp @@ -89,5 +89,26 @@ int main() { Require(surround5_out.size() == 2 && surround5_out[0] == 5950 && surround5_out[1] == 4121, "5 channel downmix reads only the channels present"); + std::int16_t surround3[3] = {1000, 2000, 3000}; + Audio3dInternal::Source surround3_source{surround3, 1, 3, false, 1.0f, 0.0f}; + std::vector surround3_out; + Audio3dInternal::MixFrame(&surround3_source, 1, 1, surround3_out); + Require(surround3_out.size() == 2 && surround3_out[0] == 3121 && surround3_out[1] == 4121, + "3 channel downmix folds center into both sides"); + + std::int16_t surround4[4] = {1000, 2000, 3000, 0}; + Audio3dInternal::Source surround4_source{surround4, 1, 4, false, 1.0f, 0.0f}; + std::vector surround4_out; + Audio3dInternal::MixFrame(&surround4_source, 1, 1, surround4_out); + Require(surround4_out.size() == 2 && surround4_out[0] == 3121 && surround4_out[1] == 4121, + "4 channel downmix drops LFE"); + + std::int16_t surround6_panned[6] = {1000, 2000, 3000, 0, 4000, 5000}; + Audio3dInternal::Source surround6_panned_source{surround6_panned, 1, 6, false, 1.0f, 1.0f}; + std::vector surround6_panned_out; + Audio3dInternal::MixFrame(&surround6_panned_source, 1, 1, surround6_panned_out); + Require(surround6_panned_out.size() == 2 && surround6_panned_out[0] == 0 && surround6_panned_out[1] == 7657, + "6 channel downmix is panned after folding"); + return 0; } From 28a53f31db4c758929359f952e240ebd44a77a12 Mon Sep 17 00:00:00 2001 From: "husari.orozco" Date: Mon, 12 Oct 2026 16:51:37 -0600 Subject: [PATCH 195/219] feat(libSceAudio3d): implement sceAudio3dBedWrite and BedWrite2 --- core/libs/prx/libSceAudio3d/Export.cpp | 35 ++++++++++++++++++++++++++ core/libs/tests/GuestAudio3dPort.cpp | 18 +++++++++++++ 2 files changed, 53 insertions(+) diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index 47489f9d5e..8fad49a376 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/PreciseWait.hpp" @@ -33,18 +34,28 @@ constexpr std::uint32_t AUDIO3D_ATTRIBUTE_RESET_STATE = 0x20000; constexpr std::uint32_t AUDIO3D_ATTRIBUTE_LATE_REVERB_LEVEL = 0x10001; constexpr std::uint32_t AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_RADIUS = 0x10002; constexpr std::uint32_t AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_HEIGHT_AWARE = 0x10003; +constexpr std::uint32_t AUDIO3D_FORMAT_S16 = 0; +constexpr std::uint32_t AUDIO3D_FORMAT_FLOAT = 1; +constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_BOTH = 0; +constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_HMU_ONLY = 1; +constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_TV_ONLY = 2; using Clock = std::chrono::steady_clock; struct Audio3dPort { bool open = false; std::uint32_t queue_depth = 0; + std::uint32_t granularity = 0; Clock::duration frame_duration{}; std::uint32_t advanced = 0; std::deque playing; std::uint32_t max_objects = 0; std::uint32_t next_object_id = 0; std::set objects; + std::vector bed; + std::uint32_t bed_channels = 0; + std::uint32_t bed_format = 0; + std::uint32_t bed_samples = 0; }; std::mutex g_mutex; @@ -164,6 +175,7 @@ int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* param g_port = Audio3dPort{}; g_port.open = true; g_port.queue_depth = parameters->queue_depth; + g_port.granularity = parameters->granularity; g_port.max_objects = parameters->max_objects; g_port.frame_duration = std::chrono::duration_cast( std::chrono::nanoseconds(std::uint64_t{parameters->granularity} * 1000000000u / AUDIO3D_SAMPLE_RATE)); @@ -249,4 +261,27 @@ int APS5_VABI sceAudio3dPortFlush(uint32_t port_id) { return 0; } +int APS5_VABI sceAudio3dBedWrite2(uint32_t port_id, uint32_t num_channels, uint32_t format, const void* buffer, uint32_t num_samples, uint32_t output_route, bool) { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (num_channels != 2 && num_channels != 6 && num_channels != 8) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (format > AUDIO3D_FORMAT_FLOAT) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (output_route > AUDIO3D_OUTPUT_ROUTE_BOTH) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (buffer == nullptr || num_samples == 0) return AUDIO3D_ERROR_INVALID_PARAMETER; + const std::size_t sample_size = format == AUDIO3D_FORMAT_FLOAT ? 4 : 2; + if (reinterpret_cast(buffer) % sample_size != 0) return AUDIO3D_ERROR_INVALID_PARAMETER; + const std::uint32_t samples = std::min(num_samples, g_port.granularity); + const std::size_t bytes = static_cast(samples) * num_channels * sample_size; + const auto* first = static_cast(buffer); + g_port.bed.assign(first, first + bytes); + g_port.bed_channels = num_channels; + g_port.bed_format = format; + g_port.bed_samples = samples; + return 0; +} + +int APS5_VABI sceAudio3dBedWrite(uint32_t port_id, uint32_t num_channels, uint32_t format, const void* buffer, uint32_t num_samples) { + return sceAudio3dBedWrite2(port_id, num_channels, format, buffer, num_samples, AUDIO3D_OUTPUT_ROUTE_BOTH, false); +} + } diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index 6c754ac77a..0013cb3f46 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -23,6 +23,8 @@ int APS5_VABI sceAudio3dPortFlush(std::uint32_t port_id); int APS5_VABI sceAudio3dObjectReserve(std::uint32_t port_id, std::uint32_t* object_id); int APS5_VABI sceAudio3dObjectUnreserve(std::uint32_t port_id, std::uint32_t object_id); int APS5_VABI sceAudio3dObjectSetAttributes(std::uint32_t port_id, std::uint32_t object_id, std::uint64_t num_attributes, const Audio3dAttribute* attribute_array); +int APS5_VABI sceAudio3dBedWrite(std::uint32_t port_id, std::uint32_t num_channels, std::uint32_t format, const void* buffer, std::uint32_t num_samples); +int APS5_VABI sceAudio3dBedWrite2(std::uint32_t port_id, std::uint32_t num_channels, std::uint32_t format, const void* buffer, std::uint32_t num_samples, std::uint32_t output_route, bool restricted); } namespace { @@ -268,6 +270,21 @@ void CheckObjectAttributes() { Require(sceAudio3dPortClose(0) == 0, "close"); } +void CheckBed() { + const std::int16_t samples[8] = {}; + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == INVALID_PORT, "bed write needs an open port"); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 0, false) == INVALID_PORT, "bed write2 needs an open port"); + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "2 channel s16 bed write"); + Require(sceAudio3dBedWrite(0, 1, 0, samples, 4) == INVALID_PARAMETER, "1 channel bed write"); + Require(sceAudio3dBedWrite(0, 2, 0, nullptr, 4) == INVALID_PARAMETER, "null bed buffer"); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 0, false) == 0, "bed write2 route both"); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 3, false) == INVALID_PARAMETER, "bed write2 unknown route"); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + void CheckTerminate() { std::uint32_t id = 7; const Audio3dOpenParameters parameters = Defaults(); @@ -294,5 +311,6 @@ int main() { CheckQueue(); CheckFlush(); CheckObjectAttributes(); + CheckBed(); CheckTerminate(); } From 64be87978d18b9aa2139a51d31ade9755e66b435 Mon Sep 17 00:00:00 2001 From: "husari.orozco" Date: Mon, 12 Oct 2026 17:01:15 -0600 Subject: [PATCH 196/219] fix(libSceAudio3d): accept every bed output route and cover the bed validation --- core/libs/prx/libSceAudio3d/Export.cpp | 4 ++-- core/libs/tests/GuestAudio3dPort.cpp | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index 8fad49a376..eab0c5f04f 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -266,9 +266,9 @@ int APS5_VABI sceAudio3dBedWrite2(uint32_t port_id, uint32_t num_channels, uint3 if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; if (num_channels != 2 && num_channels != 6 && num_channels != 8) return AUDIO3D_ERROR_INVALID_PARAMETER; if (format > AUDIO3D_FORMAT_FLOAT) return AUDIO3D_ERROR_INVALID_PARAMETER; - if (output_route > AUDIO3D_OUTPUT_ROUTE_BOTH) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (output_route != AUDIO3D_OUTPUT_ROUTE_BOTH && output_route != AUDIO3D_OUTPUT_ROUTE_HMU_ONLY && output_route != AUDIO3D_OUTPUT_ROUTE_TV_ONLY) return AUDIO3D_ERROR_INVALID_PARAMETER; if (buffer == nullptr || num_samples == 0) return AUDIO3D_ERROR_INVALID_PARAMETER; - const std::size_t sample_size = format == AUDIO3D_FORMAT_FLOAT ? 4 : 2; + const std::size_t sample_size = format == AUDIO3D_FORMAT_S16 ? 2u : 4u; if (reinterpret_cast(buffer) % sample_size != 0) return AUDIO3D_ERROR_INVALID_PARAMETER; const std::uint32_t samples = std::min(num_samples, g_port.granularity); const std::size_t bytes = static_cast(samples) * num_channels * sample_size; diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index 0013cb3f46..ccdf82ef8a 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -7,6 +7,7 @@ #include #include #include +#include extern "C" { void APS5_VABI sceAudio3dGetDefaultOpenParameters(Audio3dOpenParameters* parameters); @@ -281,7 +282,18 @@ void CheckBed() { Require(sceAudio3dBedWrite(0, 1, 0, samples, 4) == INVALID_PARAMETER, "1 channel bed write"); Require(sceAudio3dBedWrite(0, 2, 0, nullptr, 4) == INVALID_PARAMETER, "null bed buffer"); Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 0, false) == 0, "bed write2 route both"); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 1, false) == 0, "bed write2 route hmu only"); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 2, false) == 0, "bed write2 route tv only"); Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 3, false) == INVALID_PARAMETER, "bed write2 unknown route"); + Require(sceAudio3dBedWrite(0, 2, 2, samples, 4) == INVALID_PARAMETER, "unknown bed format"); + Require(sceAudio3dBedWrite(0, 2, 0, samples, 0) == INVALID_PARAMETER, "zero bed samples"); + Require(sceAudio3dBedWrite(0, 2, 0, reinterpret_cast(samples) + 1, 4) == INVALID_PARAMETER, "misaligned s16 bed buffer"); + const float floats[8] = {}; + Require(sceAudio3dBedWrite(0, 2, 1, floats, 4) == 0, "float bed write"); + const std::uint32_t granularity = parameters.granularity; + std::vector wide(static_cast(granularity) * 8, 0); + Require(sceAudio3dBedWrite(0, 6, 0, wide.data(), granularity) == 0, "6 channel bed write"); + Require(sceAudio3dBedWrite(0, 8, 0, wide.data(), granularity) == 0, "8 channel bed write"); Require(sceAudio3dPortClose(0) == 0, "close"); } From 68efaf1e5592df84e7bb95a2eb94da3e8646f786 Mon Sep 17 00:00:00 2001 From: "husari.orozco" Date: Mon, 12 Oct 2026 17:12:03 -0600 Subject: [PATCH 197/219] feat(libSceAudio3d): mix on advance and deliver through AudioOut --- core/libs/prx/libSceAudio3d/CMakeLists.txt | 1 + core/libs/prx/libSceAudio3d/Export.cpp | 81 +++++++++++++--------- core/libs/tests/GuestAudio3dPort.cpp | 38 +++++----- 3 files changed, 70 insertions(+), 50 deletions(-) diff --git a/core/libs/prx/libSceAudio3d/CMakeLists.txt b/core/libs/prx/libSceAudio3d/CMakeLists.txt index 887fd367f6..4ac9a08ba5 100644 --- a/core/libs/prx/libSceAudio3d/CMakeLists.txt +++ b/core/libs/prx/libSceAudio3d/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceAudio3d SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceAudio3d PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceAudio3d PRIVATE libSceAudioOut) configure_windows_unwind(libSceAudio3d) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index eab0c5f04f..eec123f1ac 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -1,5 +1,4 @@ #include -#include #include #include #include @@ -10,15 +9,22 @@ #include #include #include +#include "Mixer.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -#include "prx/libc/include/PreciseWait.hpp" + +extern "C" { +int APS5_VABI sceAudioOutOpen(int user_id, int type, int index, std::uint32_t len, std::uint32_t freq, std::uint32_t param); +int APS5_VABI sceAudioOutOutput(int handle, const void* ptr); +int APS5_VABI sceAudioOutClose(int handle); +} namespace { constexpr int AUDIO3D_ERROR_INVALID_PORT = static_cast(0x80EA0002); constexpr int AUDIO3D_ERROR_INVALID_OBJECT = static_cast(0x80EA0003); constexpr int AUDIO3D_ERROR_INVALID_PARAMETER = static_cast(0x80EA0004); +constexpr int AUDIO3D_ERROR_NOT_SUPPORTED = static_cast(0x80EA0005); constexpr int AUDIO3D_ERROR_OUT_OF_RESOURCES = static_cast(0x80EA0006); constexpr int AUDIO3D_ERROR_NOT_READY = static_cast(0x80EA0007); @@ -28,6 +34,8 @@ constexpr std::uint32_t AUDIO3D_OBJECT_INVALID = 0xFFFFFFFF; constexpr std::uint32_t AUDIO3D_RATE_48000 = 0; constexpr std::uint32_t AUDIO3D_SAMPLE_RATE = 48000; constexpr std::uint32_t AUDIO3D_BUFFER_ADVANCE_AND_PUSH = 2; +constexpr int AUDIO3D_AUDIO_OUT_PORT_TYPE = 126; +constexpr std::uint32_t AUDIO3D_AUDIO_OUT_FORMAT_STEREO = 1; constexpr std::uint32_t AUDIO3D_BLOCKING_ASYNC = 0; constexpr std::uint32_t AUDIO3D_BLOCKING_SYNC = 1; constexpr std::uint32_t AUDIO3D_ATTRIBUTE_RESET_STATE = 0x20000; @@ -40,15 +48,11 @@ constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_BOTH = 0; constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_HMU_ONLY = 1; constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_TV_ONLY = 2; -using Clock = std::chrono::steady_clock; - struct Audio3dPort { bool open = false; std::uint32_t queue_depth = 0; std::uint32_t granularity = 0; - Clock::duration frame_duration{}; - std::uint32_t advanced = 0; - std::deque playing; + std::uint32_t buffer_mode = 0; std::uint32_t max_objects = 0; std::uint32_t next_object_id = 0; std::set objects; @@ -56,6 +60,8 @@ struct Audio3dPort { std::uint32_t bed_channels = 0; std::uint32_t bed_format = 0; std::uint32_t bed_samples = 0; + std::deque> mixed; + int audio_out = 0; }; std::mutex g_mutex; @@ -70,9 +76,8 @@ bool PortIsOpen(std::uint32_t port_id) { return port_id == AUDIO3D_PORT_ID && g_port.open; } -std::uint32_t QueueLevel(Clock::time_point now) { - while (!g_port.playing.empty() && g_port.playing.front() <= now) g_port.playing.pop_front(); - return g_port.advanced + static_cast(g_port.playing.size()); +std::uint32_t QueueLevel() { + return static_cast(g_port.mixed.size()); } } @@ -104,14 +109,26 @@ int APS5_VABI sceAudio3dTerminate() { int APS5_VABI sceAudio3dPortAdvance(uint32_t port_id) { std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; - if (QueueLevel(Clock::now()) >= g_port.queue_depth) Unsupported(__func__, "queue is full"); - ++g_port.advanced; + if (g_port.buffer_mode != AUDIO3D_BUFFER_ADVANCE_AND_PUSH) return AUDIO3D_ERROR_NOT_SUPPORTED; + if (QueueLevel() >= g_port.queue_depth) return AUDIO3D_ERROR_NOT_READY; + const Audio3dInternal::Source source{ + g_port.bed.data(), + g_port.bed_samples, + g_port.bed_channels, + g_port.bed_format == AUDIO3D_FORMAT_FLOAT, + 1.0f, + 0.0f, + }; + std::vector frame; + Audio3dInternal::MixFrame(&source, g_port.bed.empty() ? 0 : 1, g_port.granularity, frame); + g_port.mixed.push_back(std::move(frame)); return 0; } int APS5_VABI sceAudio3dPortClose(uint32_t port_id) { std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (g_port.audio_out != 0) sceAudioOutClose(g_port.audio_out); g_port = Audio3dPort{}; return 0; } @@ -120,7 +137,7 @@ int APS5_VABI sceAudio3dPortGetQueueLevel(uint32_t port_id, uint32_t* queue_leve std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; if (queue_level == nullptr && queue_available == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; - const std::uint32_t level = QueueLevel(Clock::now()); + const std::uint32_t level = QueueLevel(); if (queue_level != nullptr) *queue_level = level; if (queue_available != nullptr) *queue_available = g_port.queue_depth - level; return 0; @@ -176,32 +193,28 @@ int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* param g_port.open = true; g_port.queue_depth = parameters->queue_depth; g_port.granularity = parameters->granularity; + g_port.buffer_mode = parameters->buffer_mode; g_port.max_objects = parameters->max_objects; - g_port.frame_duration = std::chrono::duration_cast( - std::chrono::nanoseconds(std::uint64_t{parameters->granularity} * 1000000000u / AUDIO3D_SAMPLE_RATE)); *id = AUDIO3D_PORT_ID; return 0; } int APS5_VABI sceAudio3dPortPush(uint32_t port_id, uint32_t blocking) { - Clock::time_point wait_until; - { - std::lock_guard lock(g_mutex); - if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; - if (blocking != AUDIO3D_BLOCKING_ASYNC && blocking != AUDIO3D_BLOCKING_SYNC) { - Unsupported(__func__, "blocking mode " + std::to_string(blocking)); - } - const Clock::time_point now = Clock::now(); - const std::uint32_t level = QueueLevel(now); - Clock::time_point end = g_port.playing.empty() ? now : g_port.playing.back(); - for (; g_port.advanced != 0; --g_port.advanced) { - end += g_port.frame_duration; - g_port.playing.push_back(end); - } - if (blocking == AUDIO3D_BLOCKING_ASYNC || level < g_port.queue_depth) return 0; - wait_until = g_port.playing[level - g_port.queue_depth]; + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (blocking != AUDIO3D_BLOCKING_ASYNC && blocking != AUDIO3D_BLOCKING_SYNC) { + Unsupported(__func__, "blocking mode " + std::to_string(blocking)); + } + if (g_port.audio_out == 0) { + const int handle = sceAudioOutOpen(AUDIO3D_USER_ID_SYSTEM, AUDIO3D_AUDIO_OUT_PORT_TYPE, 0, + g_port.granularity, AUDIO3D_SAMPLE_RATE, AUDIO3D_AUDIO_OUT_FORMAT_STEREO); + if (handle < 0) return handle; + g_port.audio_out = handle; + } + while (QueueLevel() >= g_port.queue_depth) { + sceAudioOutOutput(g_port.audio_out, g_port.mixed.front().data()); + g_port.mixed.pop_front(); } - PreciseSleepUntil(wait_until); return 0; } @@ -256,8 +269,8 @@ int APS5_VABI sceAudio3dObjectSetAttributes(uint32_t port_id, uint32_t object_id int APS5_VABI sceAudio3dPortFlush(uint32_t port_id) { std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; - g_port.advanced = 0; - g_port.playing.clear(); + g_port.mixed.clear(); + if (g_port.audio_out != 0) sceAudioOutOutput(g_port.audio_out, nullptr); return 0; } diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index ccdf82ef8a..3fa73fc776 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -198,11 +198,8 @@ void CheckObjects() { } void CheckQueue() { - using namespace std::chrono; - constexpr std::uint32_t granularity = 0x1800; - constexpr auto frame = microseconds(1000000ull * granularity / 48000); Audio3dOpenParameters parameters = Defaults(); - parameters.granularity = granularity; + parameters.granularity = 0x1800; std::uint32_t id = 7; Require(Open(parameters, &id) == 0 && id == 0, "open"); CheckAttributes(); @@ -216,23 +213,31 @@ void CheckQueue() { RequireLevel(1, 1, "one frame queued"); Require(sceAudio3dPortAdvance(0) == 0, "advance 2"); RequireLevel(2, 0, "queue full"); - RequireThrows([] { sceAudio3dPortAdvance(0); }, "advance into a full queue must throw"); + Require(sceAudio3dPortAdvance(0) == NOT_READY, "advance into a full queue must be NOT_READY"); RequireThrows([] { sceAudio3dPortPush(0, 2); }, "unknown blocking mode must throw"); - - auto start = steady_clock::now(); Require(sceAudio3dPortPush(0, 1) == 0, "blocking push"); - auto waited = steady_clock::now() - start; - Require(waited >= frame - milliseconds(2), "blocking push waits until one frame has played"); RequireLevel(1, 1, "blocking push returns with one free slot"); - Require(sceAudio3dPortAdvance(0) == 0, "advance 3"); - start = steady_clock::now(); Require(sceAudio3dPortPush(0, 0) == 0, "async push"); - Require(steady_clock::now() - start < frame, "async push does not wait for a frame"); - std::uint32_t level = 0; - Require(sceAudio3dPortGetQueueLevel(0, &level, nullptr) == 0 && level >= 1, "async push keeps frames queued"); - std::this_thread::sleep_for(frame * 3); - RequireLevel(0, 2, "frames drain at the port's rate"); + RequireLevel(1, 1, "async push submits the excess frame"); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + +void CheckMixAndPush() { + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + Require(sceAudio3dPortAdvance(0) == 0, "advance mixes an empty bed into silence"); + RequireLevel(1, 1, "the silent frame is queued"); + const std::int16_t samples[8] = {1000, -1000, 2000, -2000, 3000, -3000, 4000, -4000}; + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "bed write"); + Require(sceAudio3dPortAdvance(0) == 0, "advance mixes the bed"); + RequireLevel(2, 0, "the mixed frame is queued"); + Require(sceAudio3dPortAdvance(0) == NOT_READY, "a full queue rejects the mix"); + Require(sceAudio3dPortPush(0, 1) == 0, "sync push delivers through audio out"); + RequireLevel(1, 1, "push keeps the queue below the depth"); + Require(sceAudio3dPortPush(0, 0) == 0, "async push delivers through audio out"); + RequireLevel(1, 1, "async push leaves the remaining frame"); Require(sceAudio3dPortClose(0) == 0, "close"); } @@ -321,6 +326,7 @@ int main() { CheckOpenClose(); CheckObjects(); CheckQueue(); + CheckMixAndPush(); CheckFlush(); CheckObjectAttributes(); CheckBed(); From cd5853496833d1e8e8a2284c11507f4b13028391 Mon Sep 17 00:00:00 2001 From: "husari.orozco" Date: Mon, 12 Oct 2026 17:32:54 -0600 Subject: [PATCH 198/219] fix(libSceAudio3d): drain audio out of the lock and make sync and async distinct Deliver and close the AudioOut port outside g_mutex: the queued frames are moved out of the mixed deque under the lock and submitted after it is released, and a failed output is returned instead of being dropped. Make the blocking modes distinct: BLOCKING_SYNC drains every queued frame so the queue level reaches 0, while BLOCKING_ASYNC submits only the excess and leaves up to queue_depth - 1 frames buffered. Map a failed sceAudioOutOpen to AUDIO3D_ERROR_OUT_OF_RESOURCES and rename the handle member to audio_out_handle. --- core/libs/prx/libSceAudio3d/Export.cpp | 62 +++++++++++++++++--------- core/libs/tests/GuestAudio3dPort.cpp | 17 ++++--- 2 files changed, 50 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index eec123f1ac..2eac46c8c5 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -61,7 +61,7 @@ struct Audio3dPort { std::uint32_t bed_format = 0; std::uint32_t bed_samples = 0; std::deque> mixed; - int audio_out = 0; + int audio_out_handle = 0; }; std::mutex g_mutex; @@ -126,10 +126,14 @@ int APS5_VABI sceAudio3dPortAdvance(uint32_t port_id) { } int APS5_VABI sceAudio3dPortClose(uint32_t port_id) { - std::lock_guard lock(g_mutex); - if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; - if (g_port.audio_out != 0) sceAudioOutClose(g_port.audio_out); - g_port = Audio3dPort{}; + int handle = 0; + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + handle = g_port.audio_out_handle; + g_port = Audio3dPort{}; + } + if (handle != 0) sceAudioOutClose(handle); return 0; } @@ -200,20 +204,30 @@ int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* param } int APS5_VABI sceAudio3dPortPush(uint32_t port_id, uint32_t blocking) { - std::lock_guard lock(g_mutex); - if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; - if (blocking != AUDIO3D_BLOCKING_ASYNC && blocking != AUDIO3D_BLOCKING_SYNC) { - Unsupported(__func__, "blocking mode " + std::to_string(blocking)); + int handle = 0; + std::deque> frames; + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (blocking != AUDIO3D_BLOCKING_ASYNC && blocking != AUDIO3D_BLOCKING_SYNC) { + Unsupported(__func__, "blocking mode " + std::to_string(blocking)); + } + if (g_port.audio_out_handle == 0) { + const int opened = sceAudioOutOpen(AUDIO3D_USER_ID_SYSTEM, AUDIO3D_AUDIO_OUT_PORT_TYPE, 0, + g_port.granularity, AUDIO3D_SAMPLE_RATE, AUDIO3D_AUDIO_OUT_FORMAT_STEREO); + if (opened < 0) return AUDIO3D_ERROR_OUT_OF_RESOURCES; + g_port.audio_out_handle = opened; + } + handle = g_port.audio_out_handle; + const std::size_t target = blocking == AUDIO3D_BLOCKING_SYNC ? 0 : g_port.queue_depth - 1; + while (g_port.mixed.size() > target) { + frames.push_back(std::move(g_port.mixed.front())); + g_port.mixed.pop_front(); + } } - if (g_port.audio_out == 0) { - const int handle = sceAudioOutOpen(AUDIO3D_USER_ID_SYSTEM, AUDIO3D_AUDIO_OUT_PORT_TYPE, 0, - g_port.granularity, AUDIO3D_SAMPLE_RATE, AUDIO3D_AUDIO_OUT_FORMAT_STEREO); - if (handle < 0) return handle; - g_port.audio_out = handle; - } - while (QueueLevel() >= g_port.queue_depth) { - sceAudioOutOutput(g_port.audio_out, g_port.mixed.front().data()); - g_port.mixed.pop_front(); + for (const std::vector& frame : frames) { + const int result = sceAudioOutOutput(handle, frame.data()); + if (result < 0) return result; } return 0; } @@ -267,10 +281,14 @@ int APS5_VABI sceAudio3dObjectSetAttributes(uint32_t port_id, uint32_t object_id } int APS5_VABI sceAudio3dPortFlush(uint32_t port_id) { - std::lock_guard lock(g_mutex); - if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; - g_port.mixed.clear(); - if (g_port.audio_out != 0) sceAudioOutOutput(g_port.audio_out, nullptr); + int handle = 0; + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + g_port.mixed.clear(); + handle = g_port.audio_out_handle; + } + if (handle != 0) sceAudioOutOutput(handle, nullptr); return 0; } diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index 3fa73fc776..be146d0c57 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -215,11 +215,12 @@ void CheckQueue() { RequireLevel(2, 0, "queue full"); Require(sceAudio3dPortAdvance(0) == NOT_READY, "advance into a full queue must be NOT_READY"); RequireThrows([] { sceAudio3dPortPush(0, 2); }, "unknown blocking mode must throw"); - Require(sceAudio3dPortPush(0, 1) == 0, "blocking push"); - RequireLevel(1, 1, "blocking push returns with one free slot"); + Require(sceAudio3dPortPush(0, 1) == 0, "sync push"); + RequireLevel(0, 2, "sync push drains every queued frame"); Require(sceAudio3dPortAdvance(0) == 0, "advance 3"); + Require(sceAudio3dPortAdvance(0) == 0, "advance 4"); Require(sceAudio3dPortPush(0, 0) == 0, "async push"); - RequireLevel(1, 1, "async push submits the excess frame"); + RequireLevel(1, 1, "async push keeps the queue one below the depth"); Require(sceAudio3dPortClose(0) == 0, "close"); } @@ -234,10 +235,12 @@ void CheckMixAndPush() { Require(sceAudio3dPortAdvance(0) == 0, "advance mixes the bed"); RequireLevel(2, 0, "the mixed frame is queued"); Require(sceAudio3dPortAdvance(0) == NOT_READY, "a full queue rejects the mix"); - Require(sceAudio3dPortPush(0, 1) == 0, "sync push delivers through audio out"); - RequireLevel(1, 1, "push keeps the queue below the depth"); - Require(sceAudio3dPortPush(0, 0) == 0, "async push delivers through audio out"); - RequireLevel(1, 1, "async push leaves the remaining frame"); + Require(sceAudio3dPortPush(0, 1) == 0, "sync push"); + RequireLevel(0, 2, "sync push drains every queued frame"); + Require(sceAudio3dPortAdvance(0) == 0, "advance after the sync drain"); + Require(sceAudio3dPortAdvance(0) == 0, "advance fills the queue again"); + Require(sceAudio3dPortPush(0, 0) == 0, "async push"); + RequireLevel(1, 1, "async push leaves the queue one below the depth"); Require(sceAudio3dPortClose(0) == 0, "close"); } From 0ee073167540e124a936d90872a0e7f4c1c6c7ae Mon Sep 17 00:00:00 2001 From: "husari.orozco" Date: Mon, 12 Oct 2026 17:46:50 -0600 Subject: [PATCH 199/219] feat(libSceAudio3d): implement the explicit AudioOut exports --- core/libs/prx/libSceAudio3d/Export.cpp | 76 ++++++++++++++++++++++++++ core/libs/tests/GuestAudio3dPort.cpp | 32 +++++++++++ 2 files changed, 108 insertions(+) diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index 2eac46c8c5..e30feda06a 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -48,6 +49,11 @@ constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_BOTH = 0; constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_HMU_ONLY = 1; constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_TV_ONLY = 2; +struct Audio3dAudioOut { + std::size_t buffer_bytes = 0; + std::uint32_t pending = 0; +}; + struct Audio3dPort { bool open = false; std::uint32_t queue_depth = 0; @@ -62,6 +68,7 @@ struct Audio3dPort { std::uint32_t bed_samples = 0; std::deque> mixed; int audio_out_handle = 0; + std::map audio_out_ports; }; std::mutex g_mutex; @@ -80,6 +87,14 @@ std::uint32_t QueueLevel() { return static_cast(g_port.mixed.size()); } +std::size_t AudioOutBufferBytes(std::uint32_t len, std::uint32_t param) { + static constexpr std::uint32_t channels[] = {1, 2, 8, 1, 2, 8, 8, 8}; + static constexpr std::uint32_t sample_size[] = {2, 2, 2, 4, 4, 4, 2, 4}; + const std::uint32_t format = param & 0xFF; + if (format >= std::size(channels)) return 0; + return static_cast(len) * channels[format] * sample_size[format]; +} + } extern "C" { @@ -127,12 +142,15 @@ int APS5_VABI sceAudio3dPortAdvance(uint32_t port_id) { int APS5_VABI sceAudio3dPortClose(uint32_t port_id) { int handle = 0; + std::vector associated; { std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; handle = g_port.audio_out_handle; + for (const auto& entry : g_port.audio_out_ports) associated.push_back(entry.first); g_port = Audio3dPort{}; } + for (const int port : associated) sceAudioOutClose(port); if (handle != 0) sceAudioOutClose(handle); return 0; } @@ -282,12 +300,18 @@ int APS5_VABI sceAudio3dObjectSetAttributes(uint32_t port_id, uint32_t object_id int APS5_VABI sceAudio3dPortFlush(uint32_t port_id) { int handle = 0; + std::vector associated; { std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; g_port.mixed.clear(); handle = g_port.audio_out_handle; + for (auto& entry : g_port.audio_out_ports) { + entry.second.pending = 0; + associated.push_back(entry.first); + } } + for (const int port : associated) sceAudioOutOutput(port, nullptr); if (handle != 0) sceAudioOutOutput(handle, nullptr); return 0; } @@ -315,4 +339,56 @@ int APS5_VABI sceAudio3dBedWrite(uint32_t port_id, uint32_t num_channels, uint32 return sceAudio3dBedWrite2(port_id, num_channels, format, buffer, num_samples, AUDIO3D_OUTPUT_ROUTE_BOTH, false); } +int APS5_VABI sceAudio3dAudioOutOpen(uint32_t port_id, int user_id, std::int32_t type, std::int32_t index, uint32_t len, uint32_t freq, uint32_t param) { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (len != g_port.granularity) return AUDIO3D_ERROR_INVALID_PARAMETER; + const int handle = sceAudioOutOpen(user_id, type, index, len, freq, param); + if (handle < 0) return AUDIO3D_ERROR_OUT_OF_RESOURCES; + g_port.audio_out_ports.emplace(handle, Audio3dAudioOut{AudioOutBufferBytes(len, param), 0}); + return handle; +} + +int APS5_VABI sceAudio3dAudioOutClose(std::int32_t handle) { + { + std::lock_guard lock(g_mutex); + if (g_port.audio_out_ports.erase(handle) == 0) return AUDIO3D_ERROR_INVALID_PORT; + } + sceAudioOutClose(handle); + return 0; +} + +int APS5_VABI sceAudio3dAudioOutOutput(std::int32_t handle, const void* ptr) { + std::vector copy; + std::uint32_t samples = 0; + { + std::lock_guard lock(g_mutex); + const auto port = g_port.audio_out_ports.find(handle); + if (port == g_port.audio_out_ports.end()) return AUDIO3D_ERROR_INVALID_PORT; + if (ptr == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (port->second.pending >= g_port.queue_depth) return AUDIO3D_ERROR_NOT_READY; + ++port->second.pending; + samples = g_port.granularity; + copy.resize(port->second.buffer_bytes); + std::memcpy(copy.data(), ptr, port->second.buffer_bytes); + } + const int result = sceAudioOutOutput(handle, copy.data()); + if (result < 0) { + std::lock_guard lock(g_mutex); + const auto port = g_port.audio_out_ports.find(handle); + if (port != g_port.audio_out_ports.end() && port->second.pending > 0) --port->second.pending; + return result; + } + return static_cast(samples); +} + +int APS5_VABI sceAudio3dAudioOutOutputs(AudioOutOutputParam* param, uint32_t num) { + if (param == nullptr || num == 0) return AUDIO3D_ERROR_INVALID_PARAMETER; + for (std::uint32_t i = 0; i < num; ++i) { + const int result = sceAudio3dAudioOutOutput(param[i].handle, param[i].ptr); + if (result < 0) return result; + } + return 0; +} + } diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index be146d0c57..fdb40805b4 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -26,6 +26,10 @@ int APS5_VABI sceAudio3dObjectUnreserve(std::uint32_t port_id, std::uint32_t obj int APS5_VABI sceAudio3dObjectSetAttributes(std::uint32_t port_id, std::uint32_t object_id, std::uint64_t num_attributes, const Audio3dAttribute* attribute_array); int APS5_VABI sceAudio3dBedWrite(std::uint32_t port_id, std::uint32_t num_channels, std::uint32_t format, const void* buffer, std::uint32_t num_samples); int APS5_VABI sceAudio3dBedWrite2(std::uint32_t port_id, std::uint32_t num_channels, std::uint32_t format, const void* buffer, std::uint32_t num_samples, std::uint32_t output_route, bool restricted); +int APS5_VABI sceAudio3dAudioOutOpen(std::uint32_t port_id, int user_id, std::int32_t type, std::int32_t index, std::uint32_t len, std::uint32_t freq, std::uint32_t param); +int APS5_VABI sceAudio3dAudioOutClose(std::int32_t handle); +int APS5_VABI sceAudio3dAudioOutOutput(std::int32_t handle, const void* ptr); +int APS5_VABI sceAudio3dAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num); } namespace { @@ -257,6 +261,33 @@ void CheckFlush() { Require(sceAudio3dPortClose(0) == 0, "close"); } +void CheckAudioOutPorts() { + const std::uint32_t granularity = Defaults().granularity; + Require(sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity, 48000, 1) == INVALID_PORT, "audio out open needs an open port"); + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + Require(sceAudio3dAudioOutOpen(1, SYSTEM_USER, 126, 0, granularity, 48000, 1) == INVALID_PORT, "audio out open on port 1"); + Require(sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity + 0x100, 48000, 1) == INVALID_PARAMETER, "len must equal the granularity"); + const int handle = sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity, 48000, 1); + Require(handle > 0, "audio out open returns a handle"); + const std::vector block(static_cast(granularity) * 2, 256); + Require(sceAudio3dAudioOutOutput(handle, block.data()) == static_cast(granularity), "audio out output returns the sample count"); + Require(sceAudio3dAudioOutOutput(handle, block.data()) == static_cast(granularity), "a second output fills the pending queue"); + Require(sceAudio3dAudioOutOutput(handle, block.data()) == NOT_READY, "a full pending queue must be NOT_READY"); + Require(sceAudio3dPortFlush(0) == 0, "flush drains the associated ports"); + Require(sceAudio3dAudioOutOutput(handle, block.data()) == static_cast(granularity), "flush resets the pending queue"); + Require(sceAudio3dAudioOutOutputs(nullptr, 0) == INVALID_PARAMETER, "outputs rejects an empty batch"); + Require(sceAudio3dAudioOutClose(handle) == 0, "audio out close"); + Require(sceAudio3dAudioOutClose(handle) == INVALID_PORT, "audio out double close"); + Require(sceAudio3dAudioOutOutput(handle, block.data()) == INVALID_PORT, "output on a closed handle"); + const int open_handle = sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity, 48000, 1); + Require(open_handle > 0, "audio out reopen"); + Require(sceAudio3dPortClose(0) == 0, "close"); + Require(sceAudio3dAudioOutOutput(open_handle, block.data()) == INVALID_PORT, "port close clears the associated ports"); + Require(sceAudio3dAudioOutClose(open_handle) == INVALID_PORT, "port close closes the associated ports"); +} + void CheckObjectAttributes() { float value = 0.5f; Audio3dAttribute attribute{0x10001, 0, &value, sizeof(value)}; @@ -331,6 +362,7 @@ int main() { CheckQueue(); CheckMixAndPush(); CheckFlush(); + CheckAudioOutPorts(); CheckObjectAttributes(); CheckBed(); CheckTerminate(); From 6caa4971f9734546723c7cb0a21989e824b68605 Mon Sep 17 00:00:00 2001 From: "husari.orozco" Date: Mon, 12 Oct 2026 18:01:30 -0600 Subject: [PATCH 200/219] fix(libSceAudio3d): drain the associated AudioOut ports on push Queue the explicit AudioOut buffers instead of submitting them straight away: sceAudio3dAudioOutOutput now copies the block into the associated port's pending queue and returns the sample count, so the queue_depth backpressure clears instead of wedging the port in NOT_READY forever. PortPush drains every associated port's pending queue outside g_mutex, popping a frame only once its submission succeeds. PortFlush keeps draining the ports with a null output while clearing their queues. sceAudio3dAudioOutOpen rejects a non-zero index and an unknown format before opening and opens the AudioOut port outside g_mutex, re-checking the port before it registers the handle. sceAudio3dAudioOutOutputs returns 0 on success, matching the reference; sceAudio3dAudioOutOutput returns the granularity as the sample count. --- core/libs/prx/libSceAudio3d/Export.cpp | 73 ++++++++++++++++---------- core/libs/tests/GuestAudio3dPort.cpp | 9 +++- 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index e30feda06a..1db6715cf1 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -51,7 +51,7 @@ constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_TV_ONLY = 2; struct Audio3dAudioOut { std::size_t buffer_bytes = 0; - std::uint32_t pending = 0; + std::deque> pending; }; struct Audio3dPort { @@ -95,6 +95,26 @@ std::size_t AudioOutBufferBytes(std::uint32_t len, std::uint32_t param) { return static_cast(len) * channels[format] * sample_size[format]; } +int DrainAssociatedAudioOuts() { + while (true) { + int handle = 0; + std::vector frame; + { + std::lock_guard lock(g_mutex); + const auto entry = std::find_if(g_port.audio_out_ports.begin(), g_port.audio_out_ports.end(), + [](const auto& candidate) { return !candidate.second.pending.empty(); }); + if (entry == g_port.audio_out_ports.end()) return 0; + handle = entry->first; + frame = entry->second.pending.front(); + } + const int result = sceAudioOutOutput(handle, frame.data()); + if (result < 0) return result; + std::lock_guard lock(g_mutex); + const auto entry = g_port.audio_out_ports.find(handle); + if (entry != g_port.audio_out_ports.end() && !entry->second.pending.empty()) entry->second.pending.pop_front(); + } +} + } extern "C" { @@ -247,7 +267,7 @@ int APS5_VABI sceAudio3dPortPush(uint32_t port_id, uint32_t blocking) { const int result = sceAudioOutOutput(handle, frame.data()); if (result < 0) return result; } - return 0; + return DrainAssociatedAudioOuts(); } int APS5_VABI sceAudio3dPortSetAttribute(uint32_t port_id, uint32_t attribute_id, const void* attribute, size_t attribute_size) { @@ -307,7 +327,7 @@ int APS5_VABI sceAudio3dPortFlush(uint32_t port_id) { g_port.mixed.clear(); handle = g_port.audio_out_handle; for (auto& entry : g_port.audio_out_ports) { - entry.second.pending = 0; + entry.second.pending.clear(); associated.push_back(entry.first); } } @@ -340,12 +360,22 @@ int APS5_VABI sceAudio3dBedWrite(uint32_t port_id, uint32_t num_channels, uint32 } int APS5_VABI sceAudio3dAudioOutOpen(uint32_t port_id, int user_id, std::int32_t type, std::int32_t index, uint32_t len, uint32_t freq, uint32_t param) { - std::lock_guard lock(g_mutex); - if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; - if (len != g_port.granularity) return AUDIO3D_ERROR_INVALID_PARAMETER; + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (len != g_port.granularity) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (index != 0 || (param & 0xFF) > 7) return AUDIO3D_ERROR_INVALID_PARAMETER; + } const int handle = sceAudioOutOpen(user_id, type, index, len, freq, param); if (handle < 0) return AUDIO3D_ERROR_OUT_OF_RESOURCES; - g_port.audio_out_ports.emplace(handle, Audio3dAudioOut{AudioOutBufferBytes(len, param), 0}); + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) { + sceAudioOutClose(handle); + return AUDIO3D_ERROR_INVALID_PORT; + } + g_port.audio_out_ports.emplace(handle, Audio3dAudioOut{AudioOutBufferBytes(len, param)}); + } return handle; } @@ -359,27 +389,14 @@ int APS5_VABI sceAudio3dAudioOutClose(std::int32_t handle) { } int APS5_VABI sceAudio3dAudioOutOutput(std::int32_t handle, const void* ptr) { - std::vector copy; - std::uint32_t samples = 0; - { - std::lock_guard lock(g_mutex); - const auto port = g_port.audio_out_ports.find(handle); - if (port == g_port.audio_out_ports.end()) return AUDIO3D_ERROR_INVALID_PORT; - if (ptr == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; - if (port->second.pending >= g_port.queue_depth) return AUDIO3D_ERROR_NOT_READY; - ++port->second.pending; - samples = g_port.granularity; - copy.resize(port->second.buffer_bytes); - std::memcpy(copy.data(), ptr, port->second.buffer_bytes); - } - const int result = sceAudioOutOutput(handle, copy.data()); - if (result < 0) { - std::lock_guard lock(g_mutex); - const auto port = g_port.audio_out_ports.find(handle); - if (port != g_port.audio_out_ports.end() && port->second.pending > 0) --port->second.pending; - return result; - } - return static_cast(samples); + std::lock_guard lock(g_mutex); + const auto port = g_port.audio_out_ports.find(handle); + if (port == g_port.audio_out_ports.end()) return AUDIO3D_ERROR_INVALID_PORT; + if (ptr == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (port->second.pending.size() >= g_port.queue_depth) return AUDIO3D_ERROR_NOT_READY; + const auto* first = static_cast(ptr); + port->second.pending.emplace_back(first, first + port->second.buffer_bytes); + return static_cast(g_port.granularity); } int APS5_VABI sceAudio3dAudioOutOutputs(AudioOutOutputParam* param, uint32_t num) { diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index fdb40805b4..97767065fc 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -269,12 +269,17 @@ void CheckAudioOutPorts() { Require(Open(parameters, &id) == 0 && id == 0, "open"); Require(sceAudio3dAudioOutOpen(1, SYSTEM_USER, 126, 0, granularity, 48000, 1) == INVALID_PORT, "audio out open on port 1"); Require(sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity + 0x100, 48000, 1) == INVALID_PARAMETER, "len must equal the granularity"); + Require(sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 1, granularity, 48000, 1) == INVALID_PARAMETER, "index must be 0"); + Require(sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity, 48000, 8) == INVALID_PARAMETER, "unknown format"); const int handle = sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity, 48000, 1); Require(handle > 0, "audio out open returns a handle"); const std::vector block(static_cast(granularity) * 2, 256); - Require(sceAudio3dAudioOutOutput(handle, block.data()) == static_cast(granularity), "audio out output returns the sample count"); - Require(sceAudio3dAudioOutOutput(handle, block.data()) == static_cast(granularity), "a second output fills the pending queue"); + for (std::uint32_t i = 0; i < parameters.queue_depth; ++i) { + Require(sceAudio3dAudioOutOutput(handle, block.data()) == static_cast(granularity), "audio out output returns the sample count"); + } Require(sceAudio3dAudioOutOutput(handle, block.data()) == NOT_READY, "a full pending queue must be NOT_READY"); + Require(sceAudio3dPortPush(0, 1) == 0, "sync push drains the associated ports"); + Require(sceAudio3dAudioOutOutput(handle, block.data()) == static_cast(granularity), "push drains the pending queue"); Require(sceAudio3dPortFlush(0) == 0, "flush drains the associated ports"); Require(sceAudio3dAudioOutOutput(handle, block.data()) == static_cast(granularity), "flush resets the pending queue"); Require(sceAudio3dAudioOutOutputs(nullptr, 0) == INVALID_PARAMETER, "outputs rejects an empty batch"); From 553ce8e9bac71a76a651fa033b96b980f10c2813 Mon Sep 17 00:00:00 2001 From: "husari.orozco" Date: Mon, 12 Oct 2026 18:35:17 -0600 Subject: [PATCH 201/219] fix(libSceAudio3d): tie the audio out drain to the frame it submits Pop the associated frame under g_mutex before submitting it, so two concurrent PortPush calls cannot submit the same frame twice or drop another, and restore it at the front if the submission fails. Open the lazy internal AudioOut port for PortPush outside g_mutex, re-checking the port before storing the handle and closing it when the port vanished. Move the rollback close in sceAudio3dAudioOutOpen outside the lock too. --- core/libs/prx/libSceAudio3d/Export.cpp | 50 ++++++++++++++++++-------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index 1db6715cf1..2c05ebf7a2 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -105,13 +105,16 @@ int DrainAssociatedAudioOuts() { [](const auto& candidate) { return !candidate.second.pending.empty(); }); if (entry == g_port.audio_out_ports.end()) return 0; handle = entry->first; - frame = entry->second.pending.front(); + frame = std::move(entry->second.pending.front()); + entry->second.pending.pop_front(); } const int result = sceAudioOutOutput(handle, frame.data()); - if (result < 0) return result; - std::lock_guard lock(g_mutex); - const auto entry = g_port.audio_out_ports.find(handle); - if (entry != g_port.audio_out_ports.end() && !entry->second.pending.empty()) entry->second.pending.pop_front(); + if (result < 0) { + std::lock_guard lock(g_mutex); + const auto entry = g_port.audio_out_ports.find(handle); + if (entry != g_port.audio_out_ports.end()) entry->second.pending.push_front(std::move(frame)); + return result; + } } } @@ -243,19 +246,32 @@ int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* param int APS5_VABI sceAudio3dPortPush(uint32_t port_id, uint32_t blocking) { int handle = 0; - std::deque> frames; + std::uint32_t granularity = 0; { std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; if (blocking != AUDIO3D_BLOCKING_ASYNC && blocking != AUDIO3D_BLOCKING_SYNC) { Unsupported(__func__, "blocking mode " + std::to_string(blocking)); } - if (g_port.audio_out_handle == 0) { - const int opened = sceAudioOutOpen(AUDIO3D_USER_ID_SYSTEM, AUDIO3D_AUDIO_OUT_PORT_TYPE, 0, - g_port.granularity, AUDIO3D_SAMPLE_RATE, AUDIO3D_AUDIO_OUT_FORMAT_STEREO); - if (opened < 0) return AUDIO3D_ERROR_OUT_OF_RESOURCES; - g_port.audio_out_handle = opened; + handle = g_port.audio_out_handle; + granularity = g_port.granularity; + } + if (handle == 0) { + const int opened = sceAudioOutOpen(AUDIO3D_USER_ID_SYSTEM, AUDIO3D_AUDIO_OUT_PORT_TYPE, 0, + granularity, AUDIO3D_SAMPLE_RATE, AUDIO3D_AUDIO_OUT_FORMAT_STEREO); + if (opened < 0) return AUDIO3D_ERROR_OUT_OF_RESOURCES; + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) { + sceAudioOutClose(opened); + return AUDIO3D_ERROR_INVALID_PORT; } + if (g_port.audio_out_handle == 0) g_port.audio_out_handle = opened; + else sceAudioOutClose(opened); + } + std::deque> frames; + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; handle = g_port.audio_out_handle; const std::size_t target = blocking == AUDIO3D_BLOCKING_SYNC ? 0 : g_port.queue_depth - 1; while (g_port.mixed.size() > target) { @@ -368,13 +384,17 @@ int APS5_VABI sceAudio3dAudioOutOpen(uint32_t port_id, int user_id, std::int32_t } const int handle = sceAudioOutOpen(user_id, type, index, len, freq, param); if (handle < 0) return AUDIO3D_ERROR_OUT_OF_RESOURCES; + bool registered = false; { std::lock_guard lock(g_mutex); - if (!PortIsOpen(port_id)) { - sceAudioOutClose(handle); - return AUDIO3D_ERROR_INVALID_PORT; + if (PortIsOpen(port_id)) { + g_port.audio_out_ports.emplace(handle, Audio3dAudioOut{AudioOutBufferBytes(len, param)}); + registered = true; } - g_port.audio_out_ports.emplace(handle, Audio3dAudioOut{AudioOutBufferBytes(len, param)}); + } + if (!registered) { + sceAudioOutClose(handle); + return AUDIO3D_ERROR_INVALID_PORT; } return handle; } From c917d7dbf7ae5d2ed6939b9e6bae627bcfb5c1b4 Mon Sep 17 00:00:00 2001 From: "husari.orozco" Date: Tue, 13 Oct 2026 15:25:53 +0000 Subject: [PATCH 202/219] docs(technical-debt): record the audio3d bed and audio out behaviour --- docs/dev/debt/libSceAudio3d.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAudio3d.md b/docs/dev/debt/libSceAudio3d.md index 09f5bce9c1..c7e1a459d0 100644 --- a/docs/dev/debt/libSceAudio3d.md +++ b/docs/dev/debt/libSceAudio3d.md @@ -2,7 +2,7 @@ ### Silent stubs -- [libSceAudio3d](../../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: objects can be reserved and `sceAudio3dObjectSetAttributes` accepts their attributes, but the library has no bed exports or mixing to give them audio, the attributes have no effect and `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them. `sceAudio3dObjectSetAttributes` validates the port, the reserved object and the attribute array (an object id that was never reserved returns the invalid-object error, while shadPS4 returns success for it and the error only for a released object; neither is measured on a console), treating the reset-state attribute as valid with a null value and requiring a value for the others; the attribute identifiers and the value of each are not interpreted. `sceAudio3dPortFlush` drops the frames queued by `sceAudio3dPortAdvance` instead of mixing and submitting them to an output, as there is no output +- [libSceAudio3d](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - bed PCM is mixed and played: `sceAudio3dBedWrite` and `sceAudio3dBedWrite2` store the bed, `sceAudio3dPortAdvance` mixes it into a stereo frame with a linear-balance pan and a 5.1/7.1 downmix ([Mixer.hpp](../../../core/libs/prx/libSceAudio3d/Mixer.hpp)) and queues the frame, and `sceAudio3dPortPush` opens a type-126 AudioOut port on first use and submits the queued frames to it. The explicit `sceAudio3dAudioOutOpen`, `sceAudio3dAudioOutOutput` and `sceAudio3dAudioOutOutputs` exports queue their blocks, and `sceAudio3dPortPush` and `sceAudio3dPortFlush` drain them. Object attributes are validated but produce no audio and no panning: the ids of the `PCM`, `GAIN` and `POSITION` attributes and the layout of the `POSITION` value are unknown, as the aerolib NID list gives the names only, the Orbital IDA type database gives the signatures only and shadPS4's values are PS4, so an object has no samples, gain or position to mix. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them. The attribute enum values and the panning model are unverified, `sceAudio3dPortGetAttributesSupported` reports no capabilities, `sceAudio3dPortGetParameters` reports `size_this = 0x28`, and `sceAudio3dPortGetState`, `sceAudio3dPortGetStatus`, `sceAudio3dPortGetBufferLevel`, `sceAudio3dPortCreate`, `sceAudio3dPortDestroy`, `sceAudio3dPortFreeState`, `sceAudio3dPortQueryDebug`, `sceAudio3dPortOpenEx`, `sceAudio3dSetGpuRenderer`, `sceAudio3dReportRegisterHandler`, `sceAudio3dReportUnregisterHandler`, `sceAudio3dCreateSpeakerArray`, `sceAudio3dDeleteSpeakerArray`, `sceAudio3dGetSpeakerArrayMemorySize`, `sceAudio3dGetSpeakerArrayMixCoefficients` and `sceAudio3dGetSpeakerArrayMixCoefficients2` are `NotImplemented` stubs ### Unknown function info From 7887d17766e47fb205cc25655e4564d8aac61976 Mon Sep 17 00:00:00 2001 From: 011010 Date: Mon, 19 Oct 2026 16:18:28 -0600 Subject: [PATCH 203/219] fix(libSceAudio3d): use the reference error code for an unsupported buffer mode The branch invented AUDIO3D_ERROR_NOT_SUPPORTED as 0x80EA0005, which shadPS4's error enum names OUT_OF_MEMORY. Both shadPS4 and prosper put the unsupported buffer mode at 0x80EA0008, prosper from the PS5 module's own error table. The debt entries now name the reference behind each unverified choice: the downmix coefficients and the dropped LFE follow shadPS4 (fpPS4 agrees) against prosper's contradiction, the channel order follows shadPS4, prosper and fpPS4, the return of sceAudio3dAudioOutOutput follows KytyPS5 and prosper against shadPS4's total-sample answer, and the full-queue NOT_READY follows shadPS4, the only reference with a body. The stubs leave the branch: this pull request carries the bed path alone. --- core/libs/prx/libSceAudio3d/Export.cpp | 3 ++- docs/dev/debt/libSceAudio3d.md | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index 2c05ebf7a2..4e0909011a 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -25,7 +25,7 @@ namespace { constexpr int AUDIO3D_ERROR_INVALID_PORT = static_cast(0x80EA0002); constexpr int AUDIO3D_ERROR_INVALID_OBJECT = static_cast(0x80EA0003); constexpr int AUDIO3D_ERROR_INVALID_PARAMETER = static_cast(0x80EA0004); -constexpr int AUDIO3D_ERROR_NOT_SUPPORTED = static_cast(0x80EA0005); +constexpr int AUDIO3D_ERROR_NOT_SUPPORTED = static_cast(0x80EA0008); constexpr int AUDIO3D_ERROR_OUT_OF_RESOURCES = static_cast(0x80EA0006); constexpr int AUDIO3D_ERROR_NOT_READY = static_cast(0x80EA0007); @@ -126,6 +126,7 @@ void APS5_VABI sceAudio3dGetDefaultOpenParameters(Audio3dOpenParameters* p) { if (p == nullptr) APS5_INVALID_ARG_EX; constexpr Audio3dOpenParameters defaults{0x20, 256, 0, 512, 2, 2, 0, 0}; static_assert(offsetof(Audio3dOpenParameters, num_beds) == 0x20); + static_assert(sizeof(Audio3dOpenParameters) == 0x28); std::memcpy(p, &defaults, defaults.size_this); } diff --git a/docs/dev/debt/libSceAudio3d.md b/docs/dev/debt/libSceAudio3d.md index c7e1a459d0..e480ca39e8 100644 --- a/docs/dev/debt/libSceAudio3d.md +++ b/docs/dev/debt/libSceAudio3d.md @@ -2,10 +2,19 @@ ### Silent stubs -- [libSceAudio3d](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - bed PCM is mixed and played: `sceAudio3dBedWrite` and `sceAudio3dBedWrite2` store the bed, `sceAudio3dPortAdvance` mixes it into a stereo frame with a linear-balance pan and a 5.1/7.1 downmix ([Mixer.hpp](../../../core/libs/prx/libSceAudio3d/Mixer.hpp)) and queues the frame, and `sceAudio3dPortPush` opens a type-126 AudioOut port on first use and submits the queued frames to it. The explicit `sceAudio3dAudioOutOpen`, `sceAudio3dAudioOutOutput` and `sceAudio3dAudioOutOutputs` exports queue their blocks, and `sceAudio3dPortPush` and `sceAudio3dPortFlush` drain them. Object attributes are validated but produce no audio and no panning: the ids of the `PCM`, `GAIN` and `POSITION` attributes and the layout of the `POSITION` value are unknown, as the aerolib NID list gives the names only, the Orbital IDA type database gives the signatures only and shadPS4's values are PS4, so an object has no samples, gain or position to mix. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them. The attribute enum values and the panning model are unverified, `sceAudio3dPortGetAttributesSupported` reports no capabilities, `sceAudio3dPortGetParameters` reports `size_this = 0x28`, and `sceAudio3dPortGetState`, `sceAudio3dPortGetStatus`, `sceAudio3dPortGetBufferLevel`, `sceAudio3dPortCreate`, `sceAudio3dPortDestroy`, `sceAudio3dPortFreeState`, `sceAudio3dPortQueryDebug`, `sceAudio3dPortOpenEx`, `sceAudio3dSetGpuRenderer`, `sceAudio3dReportRegisterHandler`, `sceAudio3dReportUnregisterHandler`, `sceAudio3dCreateSpeakerArray`, `sceAudio3dDeleteSpeakerArray`, `sceAudio3dGetSpeakerArrayMemorySize`, `sceAudio3dGetSpeakerArrayMixCoefficients` and `sceAudio3dGetSpeakerArrayMixCoefficients2` are `NotImplemented` stubs +- [libSceAudio3d](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - `sceAudio3dObjectSetAttributes` validates the port, the reserved object and the attribute array (an object id that was never reserved returns the invalid-object error, while shadPS4 returns success for it; neither is measured on a console), treating the reset-state attribute as valid with a null value and requiring a value for the others, but the identifiers and the value of each are not interpreted, so an object has no samples, no gain and no position: the ids of the `PCM`, `GAIN` and `POSITION` attributes and the `POSITION` layout are unknown (the aerolib NID list gives the names only, the Orbital IDA type database the signatures only and shadPS4's values are PS4). `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them, and `sceAudio3dPortFlush` drops the frames queued by `sceAudio3dPortAdvance` instead of mixing and submitting them to an output + +### Functional + +- [libSceAudio3d bed path](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - bed PCM is mixed and played: `sceAudio3dBedWrite` and `sceAudio3dBedWrite2` store the bed, `sceAudio3dPortAdvance` mixes it into a stereo frame with a linear-balance pan and a 5.1/7.1 downmix ([Mixer.hpp](../../../core/libs/prx/libSceAudio3d/Mixer.hpp)) and queues the frame, and `sceAudio3dPortPush` opens a type-126 AudioOut port on first use and submits the queued frames to it. The explicit `sceAudio3dAudioOutOpen`, `sceAudio3dAudioOutOutput` and `sceAudio3dAudioOutOutputs` exports queue their blocks and the port drains them. The attribute enum values and the panning model are unverified ### Unknown function info - [sceAudio3dPortOpen](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too - [sceAudio3dPortGetAttributesSupported](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - the supported list (late reverb level, downmix spread radius, downmix spread height aware) is inferred from the attributes `sceAudio3dPortSetAttribute` accepts; its order and the count written when the caller capacity is smaller are unverified. [shadPS4](https://github.com/shadps4-emu/shadPS4) returns object attributes (PCM, gain...) instead, prosper and KytyPS5 have no body - [sceAudio3dObjectReserve](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF +- [Audio3d bed downmix coefficients](../../../core/libs/prx/libSceAudio3d/Mixer.hpp) (libSceAudio3d) - the centre and the surrounds fold into stereo with a 0.7071 gain and the LFE channel is dropped, which follows [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4): its `DOWNMIX_CENTER` and `DOWNMIX_SURROUND` are 0.7071 and its `DOWNMIX_LFE` is 0, so the `if constexpr` that adds the LFE is compiled out. [prosper](https://github.com/mattias800/prosper) disagrees on the LFE only: it folds it into both sides with a 0.5 gain, in its AudioOut2 main bed path, which is not an Audio3d path. KytyPS5 has no Audio3d mixing at all. The absolute level is unmeasured: fpPS4 scales the front by 1 + 3/sqrt(2) instead of 1 +- [Audio3d bed channel order](../../../core/libs/prx/libSceAudio3d/Mixer.hpp) (libSceAudio3d) - the buffer is read as L, R, C, LFE, Ls, Rs, Lrs, Rrs, the standard order, which three references agree on: [shadPS4](https://github.com/shadps4-emu/shadPS4) reads the front, centre, LFE and surround samples at those indices, [prosper](https://github.com/mattias800/prosper) documents FL, FR, FC, LFE and then surround pairs, and fpPS4 names them `_FL=0 _FR=1 _FC=2 _LF=3 _SL=4 _SR=5 _BL=6 _BR=7`. fpPS4's `STD` layout swaps the side and back pairs, which this library does not model +- [sceAudio3dAudioOutOutput](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - the return is the number of frames per channel in the queued block, which follows [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [prosper](https://github.com/mattias800/prosper), whose `sceAudioOutOutput` returns the grain, and this project's own `sceAudioOutOutput`, which returns the port's `samplesNum` (the buffer length in frames). [shadPS4](https://github.com/shadps4-emu/shadPS4) returns the total samples instead (`len * channels`) and fpPS4 returns 0; no console measurement settles it +- [sceAudio3dPortAdvance buffer mode](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - a mode other than 2 returns `NOT_SUPPORTED` (0x80EA0008), the value [shadPS4](https://github.com/shadps4-emu/shadPS4) uses for it and the one [prosper](https://github.com/mattias800/prosper) reads from the PS5 module's own error table, where 0x80EA0008 is what the per-port calls return for a buffer mode they do not support. shadPS4 and prosper refuse only mode 0 in `sceAudio3dPortAdvance` and modes 0 and 1 in `sceAudio3dPortPush`, while [KytyPS5](https://github.com/KytyPS5/KytyPS5) requires mode 2 at `sceAudio3dPortOpen` and halts otherwise, so this library refuses every other mode at the open and the check here is unreachable. 0x80EA0005 is `OUT_OF_MEMORY`, not `NOT_SUPPORTED` +- [sceAudio3dPortAdvance on a full queue](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - a full queue returns `NOT_READY` (0x80EA0007) instead of throwing, which follows [shadPS4](https://github.com/shadps4-emu/shadPS4), the only reference with a body: it returns `ORBIS_AUDIO3D_ERROR_NOT_READY` when the mixed queue is at the port's depth. [KytyPS5](https://github.com/KytyPS5/KytyPS5) halts instead and prosper has no queue at all From 0531b591e8425724af58537392e41fe767789756 Mon Sep 17 00:00:00 2001 From: 011010 Date: Mon, 19 Oct 2026 16:29:04 -0600 Subject: [PATCH 204/219] test(libSceAudio3d): check that a blocking push runs at real time The push hands the mixed frames to an AudioOut port, so the pacing comes from the AudioOut queue and not from a local wait. The test pushes 200 frames of 256 samples at 48 kHz and requires the elapsed time to be the duration of the audio within 150 ms below and 300 ms above, the shape libSceAudioOut's own real-time test uses. --- core/libs/tests/GuestAudio3dPort.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index 97767065fc..651e9264b2 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -248,6 +248,25 @@ void CheckMixAndPush() { Require(sceAudio3dPortClose(0) == 0, "close"); } +void CheckPushKeepsRealTime() { + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + const std::int16_t samples[8] = {1000, -1000, 2000, -2000, 3000, -3000, 4000, -4000}; + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "bed write"); + constexpr std::uint32_t frames = 200; + const std::uint64_t blockUs = 1000000ull * parameters.granularity / 48000ull; + const auto start = std::chrono::steady_clock::now(); + for (std::uint32_t i = 0; i < frames; i++) { + Require(sceAudio3dPortAdvance(0) == 0, "advance"); + Require(sceAudio3dPortPush(0, 1) == 0, "sync push"); + } + const auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); + const std::uint64_t expected = frames * blockUs; + Require(elapsed + 150000 >= static_cast(expected) && elapsed < expected + 300000, "a blocking push must run at real time"); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + void CheckFlush() { Require(sceAudio3dPortFlush(0) == INVALID_PORT, "flush needs an open port"); Audio3dOpenParameters parameters = Defaults(); @@ -366,6 +385,7 @@ int main() { CheckObjects(); CheckQueue(); CheckMixAndPush(); + CheckPushKeepsRealTime(); CheckFlush(); CheckAudioOutPorts(); CheckObjectAttributes(); From c85d284e3916a76eb39f2ccc080c1e1345ce9dd7 Mon Sep 17 00:00:00 2001 From: 011010 Date: Mon, 19 Oct 2026 19:54:00 -0600 Subject: [PATCH 205/219] fix(libSceAudio3d): let the mixer test find libc.prx on Windows The mixer test links libc and loads libc.prx, so on Windows its executable exits 0xC0000135 unless the unpatched library directory is on PATH. Every other test in the tree that links libc prepends it, and the two tests next to this one only prepend the libSceAudio3d directory because they link the library as well. Prepend the libc directory here too. --- core/libs/prx/libSceAudio3d/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libSceAudio3d/CMakeLists.txt b/core/libs/prx/libSceAudio3d/CMakeLists.txt index 4ac9a08ba5..791d2fac7e 100644 --- a/core/libs/prx/libSceAudio3d/CMakeLists.txt +++ b/core/libs/prx/libSceAudio3d/CMakeLists.txt @@ -28,4 +28,8 @@ if(BUILD_TESTING) target_link_libraries(guest_audio3d_mixer_tests PRIVATE libc) configure_windows_unwind(guest_audio3d_mixer_tests) add_test(NAME guest_audio3d_mixer COMMAND guest_audio3d_mixer_tests) + if(WIN32) + set_tests_properties(guest_audio3d_mixer PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() From 64092044df28514f0999b8b49d23ca6caedbc778 Mon Sep 17 00:00:00 2001 From: 011010 Date: Tue, 20 Oct 2026 13:34:18 -0600 Subject: [PATCH 206/219] fix(libSceAudio3d): queue the bed writes and consume one per advance sceAudio3dBedWrite2 stored the bed in a single slot that sceAudio3dPortAdvance mixed without clearing, so an advance with no new write replayed the previous block instead of mixing silence, and two writes before one advance kept only the second. The writes are queued now, as shadPS4's ConvertAndEnqueue does: the queue is bounded by queue_depth and returns NOT_READY when full, an advance consumes one bed, and an advance with nothing queued mixes silence through the mixer's empty-source path, which the mixer test already covers. guest_audio3d_port gains the queue case: fill the queue, require NOT_READY, advance, and require the consumed bed to make room. It fails on the old code at the NOT_READY of the full queue. --- core/libs/prx/libSceAudio3d/Export.cpp | 47 ++++++++++++++++---------- core/libs/tests/GuestAudio3dPort.cpp | 31 +++++++++++++++++ docs/dev/debt/libSceAudio3d.md | 2 +- 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index 4e0909011a..aff6989780 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -54,6 +54,13 @@ struct Audio3dAudioOut { std::deque> pending; }; +struct Audio3dBed { + std::vector pcm; + std::uint32_t channels = 0; + std::uint32_t format = 0; + std::uint32_t samples = 0; +}; + struct Audio3dPort { bool open = false; std::uint32_t queue_depth = 0; @@ -62,10 +69,7 @@ struct Audio3dPort { std::uint32_t max_objects = 0; std::uint32_t next_object_id = 0; std::set objects; - std::vector bed; - std::uint32_t bed_channels = 0; - std::uint32_t bed_format = 0; - std::uint32_t bed_samples = 0; + std::deque beds; std::deque> mixed; int audio_out_handle = 0; std::map audio_out_ports; @@ -150,16 +154,22 @@ int APS5_VABI sceAudio3dPortAdvance(uint32_t port_id) { if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; if (g_port.buffer_mode != AUDIO3D_BUFFER_ADVANCE_AND_PUSH) return AUDIO3D_ERROR_NOT_SUPPORTED; if (QueueLevel() >= g_port.queue_depth) return AUDIO3D_ERROR_NOT_READY; - const Audio3dInternal::Source source{ - g_port.bed.data(), - g_port.bed_samples, - g_port.bed_channels, - g_port.bed_format == AUDIO3D_FORMAT_FLOAT, - 1.0f, - 0.0f, - }; std::vector frame; - Audio3dInternal::MixFrame(&source, g_port.bed.empty() ? 0 : 1, g_port.granularity, frame); + if (g_port.beds.empty()) { + Audio3dInternal::MixFrame(nullptr, 0, g_port.granularity, frame); + } else { + const Audio3dBed& bed = g_port.beds.front(); + const Audio3dInternal::Source source{ + bed.pcm.data(), + bed.samples, + bed.channels, + bed.format == AUDIO3D_FORMAT_FLOAT, + 1.0f, + 0.0f, + }; + Audio3dInternal::MixFrame(&source, 1, g_port.granularity, frame); + g_port.beds.pop_front(); + } g_port.mixed.push_back(std::move(frame)); return 0; } @@ -364,11 +374,14 @@ int APS5_VABI sceAudio3dBedWrite2(uint32_t port_id, uint32_t num_channels, uint3 if (reinterpret_cast(buffer) % sample_size != 0) return AUDIO3D_ERROR_INVALID_PARAMETER; const std::uint32_t samples = std::min(num_samples, g_port.granularity); const std::size_t bytes = static_cast(samples) * num_channels * sample_size; + if (g_port.beds.size() >= g_port.queue_depth) return AUDIO3D_ERROR_NOT_READY; const auto* first = static_cast(buffer); - g_port.bed.assign(first, first + bytes); - g_port.bed_channels = num_channels; - g_port.bed_format = format; - g_port.bed_samples = samples; + Audio3dBed bed; + bed.pcm.assign(first, first + bytes); + bed.channels = num_channels; + bed.format = format; + bed.samples = samples; + g_port.beds.push_back(std::move(bed)); return 0; } diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index 651e9264b2..b377c14aad 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -341,22 +341,52 @@ void CheckBed() { Audio3dOpenParameters parameters = Defaults(); std::uint32_t id = 7; Require(Open(parameters, &id) == 0 && id == 0, "open"); + const auto consume = [] { + Require(sceAudio3dPortAdvance(0) == 0, "advance after a bed write"); + Require(sceAudio3dPortFlush(0) == 0, "flush the mixed frame after a bed write"); + }; Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "2 channel s16 bed write"); + consume(); Require(sceAudio3dBedWrite(0, 1, 0, samples, 4) == INVALID_PARAMETER, "1 channel bed write"); Require(sceAudio3dBedWrite(0, 2, 0, nullptr, 4) == INVALID_PARAMETER, "null bed buffer"); Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 0, false) == 0, "bed write2 route both"); + consume(); Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 1, false) == 0, "bed write2 route hmu only"); + consume(); Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 2, false) == 0, "bed write2 route tv only"); + consume(); Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 3, false) == INVALID_PARAMETER, "bed write2 unknown route"); Require(sceAudio3dBedWrite(0, 2, 2, samples, 4) == INVALID_PARAMETER, "unknown bed format"); Require(sceAudio3dBedWrite(0, 2, 0, samples, 0) == INVALID_PARAMETER, "zero bed samples"); Require(sceAudio3dBedWrite(0, 2, 0, reinterpret_cast(samples) + 1, 4) == INVALID_PARAMETER, "misaligned s16 bed buffer"); const float floats[8] = {}; Require(sceAudio3dBedWrite(0, 2, 1, floats, 4) == 0, "float bed write"); + consume(); const std::uint32_t granularity = parameters.granularity; std::vector wide(static_cast(granularity) * 8, 0); Require(sceAudio3dBedWrite(0, 6, 0, wide.data(), granularity) == 0, "6 channel bed write"); + consume(); Require(sceAudio3dBedWrite(0, 8, 0, wide.data(), granularity) == 0, "8 channel bed write"); + consume(); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + +void CheckBedQueue() { + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + const std::int16_t samples[8] = {1000, -1000, 2000, -2000, 3000, -3000, 4000, -4000}; + for (std::uint32_t i = 0; i < parameters.queue_depth; ++i) { + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "each bed write fills the queue"); + } + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == NOT_READY, "a full bed queue must be NOT_READY"); + Require(sceAudio3dPortAdvance(0) == 0, "advance consumes one bed"); + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "the consumed bed makes room"); + Require(sceAudio3dPortAdvance(0) == 0, "advance consumes the next bed"); + RequireLevel(2, 0, "two frames are queued from the queued beds"); + Require(sceAudio3dPortFlush(0) == 0, "flush"); + Require(sceAudio3dPortAdvance(0) == 0, "advance with no bed left mixes silence"); + RequireLevel(1, 1, "silence is queued behind the flushed frames"); Require(sceAudio3dPortClose(0) == 0, "close"); } @@ -390,5 +420,6 @@ int main() { CheckAudioOutPorts(); CheckObjectAttributes(); CheckBed(); + CheckBedQueue(); CheckTerminate(); } diff --git a/docs/dev/debt/libSceAudio3d.md b/docs/dev/debt/libSceAudio3d.md index e480ca39e8..fb273f4d09 100644 --- a/docs/dev/debt/libSceAudio3d.md +++ b/docs/dev/debt/libSceAudio3d.md @@ -6,7 +6,7 @@ ### Functional -- [libSceAudio3d bed path](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - bed PCM is mixed and played: `sceAudio3dBedWrite` and `sceAudio3dBedWrite2` store the bed, `sceAudio3dPortAdvance` mixes it into a stereo frame with a linear-balance pan and a 5.1/7.1 downmix ([Mixer.hpp](../../../core/libs/prx/libSceAudio3d/Mixer.hpp)) and queues the frame, and `sceAudio3dPortPush` opens a type-126 AudioOut port on first use and submits the queued frames to it. The explicit `sceAudio3dAudioOutOpen`, `sceAudio3dAudioOutOutput` and `sceAudio3dAudioOutOutputs` exports queue their blocks and the port drains them. The attribute enum values and the panning model are unverified +- [libSceAudio3d bed path](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - bed PCM is mixed and played: `sceAudio3dBedWrite` and `sceAudio3dBedWrite2` queue a bed, bounded by `queue_depth` and `NOT_READY` when full, `sceAudio3dPortAdvance` consumes one and mixes it into a stereo frame with a linear-balance pan and a 5.1/7.1 downmix, and an advance with no bed queued mixes silence ([Mixer.hpp](../../../core/libs/prx/libSceAudio3d/Mixer.hpp)) and queues the frame, and `sceAudio3dPortPush` opens a type-126 AudioOut port on first use and submits the queued frames to it. The explicit `sceAudio3dAudioOutOpen`, `sceAudio3dAudioOutOutput` and `sceAudio3dAudioOutOutputs` exports queue their blocks and the port drains them. The attribute enum values and the panning model are unverified ### Unknown function info From 7caa8fa045cf0302c88c7843c5baf896cf2eb9ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 20 Oct 2026 22:06:56 +0200 Subject: [PATCH 207/219] test(libSceAudio3d): drop the wall-clock upper bound of the real-time push check Co-Authored-By: Claude --- core/libs/tests/GuestAudio3dPort.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index b377c14aad..5b5467437d 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -263,7 +263,7 @@ void CheckPushKeepsRealTime() { } const auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); const std::uint64_t expected = frames * blockUs; - Require(elapsed + 150000 >= static_cast(expected) && elapsed < expected + 300000, "a blocking push must run at real time"); + Require(elapsed + 150000 >= static_cast(expected), "a blocking push must run at real time"); Require(sceAudio3dPortClose(0) == 0, "close"); } From 1af777afb1ca57747fe42c67d38292c7bad16eca Mon Sep 17 00:00:00 2001 From: Dom3005 Date: Tue, 20 Oct 2026 21:21:31 +0200 Subject: [PATCH 208/219] fix(libc): map a Windows symbolic link loop to ELOOP when opening CreateFileW fails a symbolic link that resolves to itself with ERROR_CANT_RESOLVE_FILENAME (1921). WindowsOpenError_nid_no_patch had no case for it, so sceKernelOpen, open and fopen reported EINVAL where FreeBSD reports ELOOP. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/General.cpp | 2 ++ core/libs/tests/GuestFilesystem.cpp | 3 +++ 2 files changed, 5 insertions(+) diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index d3c8d0753c..c6d513cc3e 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -707,6 +707,8 @@ extern "C" int WindowsOpenError_nid_no_patch(unsigned long error) { return ENOMEM; case ERROR_DISK_FULL: return ENOSPC; + case ERROR_CANT_RESOLVE_FILENAME: + return ELOOP; default: return EINVAL; } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 07b86670f3..f80ac97f38 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -954,6 +954,9 @@ int main() { const auto statLoop = root / "stat-loop"; Require(CreateSymbolicLinkW(statLoop.c_str(), L"stat-loop", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); Require(sceKernelStat(statLoop.generic_string().c_str(), &status) == static_cast(0x8002003eu)); + Require(sceKernelOpen(statLoop.generic_string().c_str(), 0, 0) == static_cast(0x8002003eu)); + Require(open_nid_postfix(statLoop.string().c_str(), 0, 0) == -1 && *__error_nid_postfix() == 62); + Require(fopen_nid_postfix(statLoop.string().c_str(), "r") == nullptr && *__error_nid_postfix() == 62); const auto checkRelativeLink = [](const std::filesystem::path& link, std::int64_t targetLength) { FileStat own{}; return lstat_nid_postfix(link.string().c_str(), &own) == 0 && (own.st_mode & 0170000) == 0120000 && From e1e20f0f2ef8192096615fe769cbb607d0ead306 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:50:05 +0000 Subject: [PATCH 209/219] fix(libkernel): match module info segment records --- core/libs/prx/libkernel/Module/src/Module.cpp | 14 ++++++++++++++ core/libs/tests/GuestElfPhdrMatch.cpp | 16 ++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index 6e06be8deb..bc6c922b4d 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -306,6 +306,20 @@ extern "C" { int APS5_VABI __elf_phdr_match_addr_nid_postfix(dl_phdr_info* phdrInfo, void* addr) { if (phdrInfo == nullptr) throw std::invalid_argument("__elf_phdr_match_addr: phdr_info is null"); const auto address = reinterpret_cast(addr); + + // Some guest libc callers pass the ModuleInfoEx returned by + // sceKernelGetModuleInfoFromAddr instead of a host dl_phdr_info. Its + // segment records contain the already-relocated addresses and permissions. + const auto* module = reinterpret_cast(phdrInfo); + if (module->st_size == sizeof(ModuleInfoEx)) { + for (std::uint32_t i = 0; i < module->segment_count && i < std::size(module->segments); ++i) { + const auto& segment = module->segments[i]; + if ((segment.prot & 4) == 0 || segment.size < sizeof(addr)) continue; + if (segment.address <= address && address - segment.address <= segment.size - sizeof(addr)) return 1; + } + return 0; + } + for (std::uint16_t i = 0; i < phdrInfo->dlpi_phnum; ++i) { const Elf64_Phdr& header = phdrInfo->dlpi_phdr[i]; if (header.p_type != PT_LOAD || (header.p_flags & PF_X) == 0) continue; diff --git a/core/libs/tests/GuestElfPhdrMatch.cpp b/core/libs/tests/GuestElfPhdrMatch.cpp index 5e021769a4..edd6538406 100644 --- a/core/libs/tests/GuestElfPhdrMatch.cpp +++ b/core/libs/tests/GuestElfPhdrMatch.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/specifics/linux/ElfTypes.hpp" +#include "SceTypes.hpp" #include #include #include @@ -41,4 +42,19 @@ int main() { info.dlpi_phnum = 0; Require(__elf_phdr_match_addr_nid_postfix(&info, Address(0x403800)) == 0); + + // Some guest libc builds pass the ModuleInfoEx returned by + // sceKernelGetModuleInfoFromAddr, not a host dl_phdr_info. + ModuleInfoEx module{}; + module.st_size = sizeof(module); + module.segment_count = 3; + module.segments[0] = {0x401000, 0x1000, 1 | 2}; + module.segments[1] = {0x403000, 0x1000, 1 | 4}; + module.segments[2] = {0x408000, 0x100, 1 | 2 | 4}; + + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x403000)) == 1); + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x403ff8)) == 1); + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x404000)) == 0); + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x401800)) == 0); + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x408010)) == 1); } From 05cc68cd9540ee9f0294d72fbf7f537be0ab7443 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:51:08 +0000 Subject: [PATCH 210/219] style(libkernel): keep regression comments out of code --- core/libs/prx/libkernel/Module/src/Module.cpp | 3 --- core/libs/tests/GuestElfPhdrMatch.cpp | 2 -- 2 files changed, 5 deletions(-) diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index bc6c922b4d..3076dd6358 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -307,9 +307,6 @@ int APS5_VABI __elf_phdr_match_addr_nid_postfix(dl_phdr_info* phdrInfo, void* ad if (phdrInfo == nullptr) throw std::invalid_argument("__elf_phdr_match_addr: phdr_info is null"); const auto address = reinterpret_cast(addr); - // Some guest libc callers pass the ModuleInfoEx returned by - // sceKernelGetModuleInfoFromAddr instead of a host dl_phdr_info. Its - // segment records contain the already-relocated addresses and permissions. const auto* module = reinterpret_cast(phdrInfo); if (module->st_size == sizeof(ModuleInfoEx)) { for (std::uint32_t i = 0; i < module->segment_count && i < std::size(module->segments); ++i) { diff --git a/core/libs/tests/GuestElfPhdrMatch.cpp b/core/libs/tests/GuestElfPhdrMatch.cpp index edd6538406..3e8d585a70 100644 --- a/core/libs/tests/GuestElfPhdrMatch.cpp +++ b/core/libs/tests/GuestElfPhdrMatch.cpp @@ -43,8 +43,6 @@ int main() { info.dlpi_phnum = 0; Require(__elf_phdr_match_addr_nid_postfix(&info, Address(0x403800)) == 0); - // Some guest libc builds pass the ModuleInfoEx returned by - // sceKernelGetModuleInfoFromAddr, not a host dl_phdr_info. ModuleInfoEx module{}; module.st_size = sizeof(module); module.segment_count = 3; From 4f6a37d7909a68ada8ff282b047451d67e8da087 Mon Sep 17 00:00:00 2001 From: Ethan Barr Date: Tue, 20 Oct 2026 13:55:31 -0400 Subject: [PATCH 211/219] fix(libc): share delete access when freopen reopens a stream onto a file on Windows On Windows, freopen reopened a stream through _wfreopen, which opens the file without FILE_SHARE_DELETE, so remove or rename of the file failed while the stream was open, unlike fopen streams and POSIX unlink/rename of an open file. freopen now closes the previous stream and opens the new file through the same CreateFileW path fopen uses, so the reopened stream shares delete access. Host standard streams and an empty filename keep the _wfreopen path. Reference: FreeBSD 11 unlink(2) and rename(2) succeed on a file that is open. --- core/libs/prx/libc/include/FileStream.hpp | 17 ++++++++++++----- core/libs/prx/libc/src/FileAndHeap.cpp | 2 +- core/libs/tests/FileStreams.cpp | 11 +++++++++++ docs/dev/debt/libc.md | 2 +- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index f5737cc3ce..5fe2c41701 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -119,7 +119,8 @@ public: return randomDevice; } - bool Reopen(const std::filesystem::path& filename, const char* mode) { + bool Reopen(const std::filesystem::path& filename, const char* mode, + [[maybe_unused]] std::FILE* (*open)(const std::filesystem::path&, const std::string&) = nullptr) { auto* previous = GetHandle(); _guest = {}; guestError = false; @@ -130,10 +131,16 @@ public: const int replaced = previous ? _fileno(previous) : -1; auto* const standard = HostStandardHandle_nid_no_patch(replaced); GuestFileLockForget_nid_no_patch(replaced); - const int kept = IsHostStandard() ? DuplicateHostDescriptor_nid_no_patch(replaced) : -1; - if (kept >= 0) ::_lock_file(previous); - _handle = ::_wfreopen(filename.c_str(), wideMode.c_str(), previous); - if (kept >= 0) RestoreHostStandard(previous, kept); + if (open && !IsHostStandard() && !filename.empty()) { + _handle = nullptr; + std::fclose(previous); + _handle = open(filename, mode); + } else { + const int kept = IsHostStandard() ? DuplicateHostDescriptor_nid_no_patch(replaced) : -1; + if (kept >= 0) ::_lock_file(previous); + _handle = ::_wfreopen(filename.c_str(), wideMode.c_str(), previous); + if (kept >= 0) RestoreHostStandard(previous, kept); + } KeepStandardHandle_nid_no_patch(replaced, standard); #else if (::fileno(previous) == 2) KeepHostStandardError_nid_no_patch(); diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 4b8f5328ba..d28c6c7b6c 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -323,7 +323,7 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode return nullptr; } } - if (stream->Reopen(path, parsed->native.c_str())) { + if (stream->Reopen(path, parsed->native.c_str(), OpenNative)) { #ifdef _WIN32 if (parsed->append && !AppendOnlyDescriptor(_fileno(stream->GetHandle()))) { const int error = errno; diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index 0a4742b900..c6916aea8e 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -206,6 +206,17 @@ int main(int argc, char** argv) { std::array appended{}; Require(stream != nullptr && fread_nid_postfix(appended.data(), 1, 4, stream) == 4 && std::strcmp(appended.data(), "abcd") == 0); Require(fclose_nid_postfix(stream) == 0 && remove_nid_postfix(heldName.c_str()) == 0); + stream = fopen_nid_postfix(heldName.c_str(), "w+b"); + Require(stream != nullptr); + stream = freopen_nid_postfix(movedName.c_str(), "w+b", stream); + Require(stream != nullptr && fwrite_nid_postfix(payload, 1, sizeof(payload), stream) == sizeof(payload)); + Require(fflush_nid_postfix(stream) == 0); + Require(rename_nid_postfix(movedName.c_str(), heldName.c_str()) == 0 && !std::filesystem::exists(movedName)); + Require(remove_nid_postfix(heldName.c_str()) == 0 && !std::filesystem::exists(heldName)); + Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0); + std::array reopenedBuffer{}; + Require(fread_nid_postfix(reopenedBuffer.data(), 1, reopenedBuffer.size(), stream) == reopenedBuffer.size()); + Require(std::strcmp(reopenedBuffer.data(), payload) == 0 && fclose_nid_postfix(stream) == 0); constexpr char marker[] = "marker"; stream = fopen_nid_postfix(argv[1], "wb"); Require(stream != nullptr); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 9179b06e02..33d37eba32 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -92,7 +92,7 @@ - On macOS the [guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) finds host regions with `mach_vm_region` and reserves the rest with a fixed `mach_vm_map`, which fails instead of replacing, as macOS has no `/proc/self/maps` or `MAP_FIXED_NOREPLACE`. The kernel keeps the range up to 0x70_0000_0000 unmappable for every process (max protection none). It starts at 0x10_0000_0000 on macOS 26.5 (M1 Pro, arm64 and x86_64 under Rosetta) and at 0xF_C000_0000 on macOS 27 (27.0.1 on an M4 Pro, arm64 and x86_64 under Rosetta; 27.2 on an M2 Max, arm64). With dyld and the Rosetta runtime near 0x2_0200_0000 and 0x3_0400_0000, a guest mapping fixed inside those ranges fails where the console places it; mappings without a fixed address are placed around them. - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync` - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way -- [`fopen` streams](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access. On Windows, opening a directory in a read mode fails with `EACCES`, where FreeBSD opens it; directory streams are not emulated +- [`fopen` streams](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A host standard stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access. On Windows, opening a directory in a read mode fails with `EACCES`, where FreeBSD opens it; directory streams are not emulated - [readdir and readdir_r](../../../core/libs/prx/libc/src/Directory.cpp) (libc) on Windows report, on a file system whose directory listing carries no file ID (the entry's ID is 0, or the volume refuses `FileIdBothDirectoryInfo`), a nonzero `d_fileno` hashed from the entry name and the directory's file index, which differs from `lstat`'s `st_ino`. Not reproduced: NTFS lists an ID for every entry - [`fopen` of /dev/random and /dev/urandom](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) in a read mode returns a stream over the host null device whose `fread` fills the buffer with host OS entropy, as the libkernel [random device](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) descriptors do. Only the exact paths are recognised, the stream has no descriptor, `setvbuf`, `feof`, `ferror`, `clearerr`, `flockfile`, `ftrylockfile`, `funlockfile` and `fclose` work on it, `fflush` returns 0 as on a FreeBSD read-only stream, and every other stream function throws, as does opening the paths for writing. `freopen` onto these paths still fails with `ENOENT`. ProsperoTV (PPSA99003) reads its phone pairing code this way - [RemoveTree](../../../core/libs/prx/libc/include/RemoveTree.hpp) (libc) removes a host tree itself on Windows instead of calling `std::filesystem::remove_all`: MinGW libstdc++ up to GCC 16 does not treat a reparse point as a link, so `remove_all` recurses through it and never terminates, and any tree that holds a symlink hangs. It builds the entry list from `GetFileAttributesW`, removes a reparse point as the link it is rather than its target, clears the read-only attribute and retries as `remove_all` does, and reports the first native error. The libstdc++ fix is in GCC 17 (commit 88856ad855ae); other hosts call `remove_all`. From a07ad305787e9e174b61bc64550d13863c2fe0aa Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 21 Oct 2026 08:33:06 +1300 Subject: [PATCH 212/219] feat(shader): request DenormPreserve 32 where the device preserves f32 denormals The recompiler asked for DenormPreserve only for 16-bit and 64-bit floats, so a device that flushes 32-bit denormals by default (RADV, MoltenVK) flushed them whatever the guest's float mode said. target.float32DenormPreserve is set from shaderDenormPreserveFloat32 when denormBehaviorIndependence allows a 32-bit mode, is part of the cache key and of the request serialization (version 41), and makes the module declare DenormPreserve 32 unless the 32-bit flush is already taken in hardware. APS5_NO_HW_F32_PRESERVE=1 turns it off. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 13 +++ .../Execution/src/VulkanDevice.cpp | 5 +- .../tests/F32DenormPreserve.cpp | 95 +++++++++++++++++++ .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 1 + .../libSceAgcDriver/tests/ShaderMemory.cpp | 25 ++++- core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 6 +- core/shader/recompiler/Recompiler.cpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../SpirvBackend/src/SpirvEmitter.cpp | 1 + .../src/SpirvMemory/SpirvModuleSetup.cpp | 3 + .../tests/RequestSerializerTests.cpp | 33 ++++--- 14 files changed, 168 insertions(+), 19 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index c468b48af4..e9aa48b2c2 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -805,6 +805,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_f32_denorm_preserve_tests EXCLUDE_FROM_ALL tests/F32DenormPreserve.cpp) +target_link_libraries(agc_f32_denorm_preserve_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_f32_denorm_preserve_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_f32_denorm_preserve_tests) +if(BUILD_TESTING) + set_target_properties(agc_f32_denorm_preserve_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_f32_denorm_preserve COMMAND agc_f32_denorm_preserve_tests) + set_tests_properties(agc_f32_denorm_preserve PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_f32_denorm_preserve PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_spirv_branch_targets_tests EXCLUDE_FROM_ALL tests/SpirvBranchTargets.cpp) target_link_libraries(agc_spirv_branch_targets_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_spirv_branch_targets_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 4dd977afd7..0ce74ecc31 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -321,6 +321,7 @@ struct VulkanDevice::State { bool float16DenormPreserve = false; bool float32FlushToZeroCandidate = false; bool float32FlushToZero = false; + bool float32DenormPreserve = false; bool probingFloatControls = false; bool demoteToHelperInvocation = false; bool earlyFragmentSampleMaskTestBeforeSampleCounting = false; @@ -1329,7 +1330,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : const auto float32Flush = Float32FlushControlFor(driverId, floatControls.shaderDenormFlushToZeroFloat32, floatControls.denormBehaviorIndependence); state->float32FlushToZeroCandidate = float32Flush.probe && std::getenv("APS5_NO_HW_F32_FLUSH") == nullptr; if (state->float32FlushToZeroCandidate && float32Flush.declared) state->capabilities.push_back(spv::CapabilityDenormFlushToZero); - if (state->float16DenormPreserve && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve); + state->float32DenormPreserve = floatControls.shaderDenormPreserveFloat32 == VK_TRUE && floatControls.denormBehaviorIndependence != VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE && std::getenv("APS5_NO_HW_F32_PRESERVE") == nullptr; + if ((state->float16DenormPreserve || state->float32DenormPreserve) && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve); deviceExtensions.push_back(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); deviceExtensions.push_back(VK_KHR_8BIT_STORAGE_EXTENSION_NAME); state->capabilities.push_back(4448); @@ -3638,6 +3640,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::Target() const { require(state->shaderProfile != nullptr, "shader device profile is unavailable"); auto target = state->shaderProfile->Target(); target.float32FlushToZero = state->float32FlushToZero; + target.float32DenormPreserve = state->float32DenormPreserve; return target; } diff --git a/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp b/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp new file mode 100644 index 0000000000..0f3fe85da7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp @@ -0,0 +1,95 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +constexpr std::array CvtF16F32Store{0x7e021500u, 0xe0700000u, 0x80000100u, 0xbf810000u}; + +std::vector recompile(bool preserve, bool flush, bool flushCapability = true) { + const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityDenormPreserve, spv::CapabilityDenormFlushToZero}; + const std::array userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu}; + const std::array extensions{"SPV_KHR_storage_buffer_storage_class", "SPV_KHR_float_controls"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, CvtF16F32Store, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 64; + request.target.bdaAbiVersion = 1; + request.target.supportedCapabilities = std::span(capabilities).first(flushCapability ? capabilities.size() : capabilities.size() - 1); + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.target.float32DenormPreserve = preserve; + request.target.float32FlushToZero = flush; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + auto result = Recompile(request); + require(!result.spirv.empty(), "the shader did not recompile"); + return result.spirv.Copy(); +} + +struct Modes { + bool preserve32 = false; + bool flush32 = false; + bool preserveCapability = false; +}; + +Modes scan(std::span words) { + Modes modes; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + const auto op = static_cast(words[cursor] & 0xffffu); + require(count != 0 && cursor + count <= words.size(), "truncated SPIR-V instruction"); + if (op == spv::OpCapability && words[cursor + 1] == spv::CapabilityDenormPreserve) modes.preserveCapability = true; + if (op == spv::OpExecutionMode && count == 4 && words[cursor + 3] == 32u) { + if (words[cursor + 2] == spv::ExecutionModeDenormPreserve) modes.preserve32 = true; + if (words[cursor + 2] == spv::ExecutionModeDenormFlushToZero) modes.flush32 = true; + } + cursor += count; + } + return modes; +} + +void verify() { + const auto preserved = scan(recompile(true, false)); + require(preserved.preserve32 && preserved.preserveCapability && !preserved.flush32, "float32DenormPreserve did not emit the 32-bit DenormPreserve execution mode and capability"); + const auto plain = scan(recompile(false, false)); + require(!plain.preserve32 && !plain.flush32, "a target without float32DenormPreserve or float32FlushToZero set a 32-bit denormal execution mode"); + const auto flushed = scan(recompile(true, true)); + require(flushed.flush32 && !flushed.preserve32, "the hardware 32-bit flush did not take precedence over the preserve flag"); + const auto nativeFlush = scan(recompile(true, true, false)); + require(!nativeFlush.preserve32 && !nativeFlush.flush32, "a hardware flush without the DenormFlushToZero capability declared a 32-bit denormal execution mode"); +} + +} + +int main() { + try { + verify(); + } catch (const std::exception& error) { + std::fprintf(stderr, "F32DenormPreserve: %s\n", error.what()); + return 1; + } + std::puts("F32DenormPreserve: the 32-bit DenormPreserve execution mode follows the target flag and yields to the hardware flush"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 56b20d8633..41cd7882ce 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -706,6 +706,7 @@ void verifyKeySensitivity() { changes("the narrow subgroup clock", [](SampleRequest& sample) { sample.request.target.narrowSubgroupClock = true; }); changes("f64 denormal preservation", [](SampleRequest& sample) { sample.request.target.float64DenormPreserve = true; }); changes("f16 denormal preservation", [](SampleRequest& sample) { sample.request.target.float16DenormPreserve = true; }); + changes("f32 denormal preservation", [](SampleRequest& sample) { sample.request.target.float32DenormPreserve = true; }); changes("spanned waves", [](SampleRequest& sample) { sample.request.target.spannedWaves = true; }); changes("robust buffer access 2", [](SampleRequest& sample) { sample.request.target.robustBufferAccess2 = true; }); changes("the workgroup size limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSize[2] = 128; }); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index c8f04297a6..f19683805c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4848,7 +4848,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQScAAAA=", "new requests did not use serialization version 39"); + require(requestPrefix(encoded, 8u) == "NVNQQSkAAAA=", "new requests did not use serialization version 41"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -4966,7 +4966,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSgAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSoAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -5059,6 +5059,7 @@ void verifyVertexInfoVersion19() { auto version20Vertex = flagged; version20Vertex.pop_back(); version20Vertex.pop_back(); + version20Vertex.pop_back(); version20Vertex[4] = 20u; const auto replay20Vertex = serializer.Deserialize(encodeBase64(version20Vertex)); require(replay20Vertex.request.context.vertex.has_value() && replay20Vertex.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "a main version20 request lost PA_CL_VS_OUT_CNTL"); @@ -5069,6 +5070,7 @@ void verifyVertexInfoVersion19() { auto legacy = plain; legacy.pop_back(); legacy.pop_back(); + legacy.pop_back(); legacy.erase(legacy.begin() + offset, legacy.begin() + offset + 4u); for (const auto version : {16u, 17u, 18u, 19u}) { legacy[4] = static_cast(version); @@ -5120,6 +5122,7 @@ void verifyVertexInfoVersion19() { version30.pop_back(); version30.pop_back(); version30.pop_back(); + version30.pop_back(); version30.erase(version30.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u), version30.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); version30[4] = 30u; const auto replay30 = serializer.Deserialize(encodeBase64(version30)); @@ -5127,6 +5130,7 @@ void verifyVertexInfoVersion19() { auto spanned = sampledPixel; spanned.target.spannedWaves = true; auto version32 = decodeBase64(serializer.Serialize(spanned)); + version32.pop_back(); require(version32.size() > 2u && version32.back() == 0u && version32[version32.size() - 2u] == 0u && version32[version32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); version32.pop_back(); version32.pop_back(); @@ -5138,8 +5142,10 @@ void verifyVertexInfoVersion19() { version29.pop_back(); version29.pop_back(); version29.pop_back(); + version29.pop_back(); version29.erase(version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); auto version36 = sampledBytes; + version36.pop_back(); version36.erase(version36.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u)); version36[4] = 36u; const auto replay36 = serializer.Deserialize(encodeBase64(version36)); @@ -5151,6 +5157,7 @@ void verifyVertexInfoVersion19() { version28.pop_back(); version28.pop_back(); version28.pop_back(); + version28.pop_back(); version28.erase(version28.begin() + static_cast(maskOffset + 1u), version28.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); version28[4] = 28u; const auto replay28 = serializer.Deserialize(encodeBase64(version28)); @@ -5159,6 +5166,7 @@ void verifyVertexInfoVersion19() { version27.pop_back(); version27.pop_back(); version27.pop_back(); + version27.pop_back(); version27.erase(version27.begin() + static_cast(maskOffset), version27.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); version27[4] = 27u; const auto replay27 = serializer.Deserialize(encodeBase64(version27)); @@ -5167,6 +5175,7 @@ void verifyVertexInfoVersion19() { legacyPixel.pop_back(); legacyPixel.pop_back(); legacyPixel.pop_back(); + legacyPixel.pop_back(); legacyPixel.erase(legacyPixel.begin() + static_cast(allBitsOffset), legacyPixel.begin() + static_cast(allBitsOffset + 2u + sampleMaskBytes + 4u + iterationBytes)); legacyPixel[4] = 26u; const auto replay26 = serializer.Deserialize(encodeBase64(legacyPixel)); @@ -5216,9 +5225,10 @@ void verifyDepthRemapRequests() { require(serializer.Serialize(replay.request) == remapped, "a version 35 depth remap request changed on re-serialization"); const auto plainBytes = decodeBase64(plain); const auto remappedBytes = decodeBase64(remapped); - require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 5u && std::equal(plainBytes.begin(), plainBytes.end() - 6, remappedBytes.begin()), "the depth remap flag and mode are not the last 6 bytes before the f32 flush flag of a vertex request"); + require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 5u && std::equal(plainBytes.begin(), plainBytes.end() - 7, remappedBytes.begin()), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags of a vertex request"); for (const std::uint8_t legacyVersion : {32u, 34u}) { auto legacyBytes = remappedBytes; + legacyBytes.pop_back(); legacyBytes[4] = legacyVersion; legacyBytes.resize(legacyBytes.size() - 6u); const auto legacy = serializer.Deserialize(encodeBase64(legacyBytes)); @@ -5226,6 +5236,7 @@ void verifyDepthRemapRequests() { require(serializer.Serialize(legacy.request) == plain, ("a version " + std::to_string(legacyVersion) + " vertex request misread the fields before the depth remap").c_str()); } auto version35 = remappedBytes; + version35.pop_back(); version35[4] = 35u; version35.pop_back(); const auto replay35 = serializer.Deserialize(encodeBase64(version35)); @@ -5272,6 +5283,7 @@ void verifyMergedHullConfiguration() { require(replay.request.graphics.has_value() && replay.request.graphics->tessellation.has_value() && replay.request.graphics->tessellation->mergedHull, "a merged hull configuration was lost in serialization"); require(!serializer.Deserialize(separate).request.graphics->tessellation->mergedHull, "a separate hull configuration was read as merged"); auto legacy = decodeBase64(merged); + legacy.pop_back(); require(legacy.size() > 52u && legacy[legacy.size() - 52u] == 1u, "the merged hull flag is not the last byte of the tessellation configuration"); legacy.erase(legacy.end() - 52); for (const auto version : {30u, 31u, 32u, 34u, 35u, 36u, 37u, 38u}) { @@ -7650,6 +7662,13 @@ int main(int argc, char** argv) { std::vector float32FlushKey; RecompileCacheKey::Build(float32Flush, float32FlushKey); require(plainKey != float32FlushKey, "f32 hardware flush did not change the cache key"); + auto float32Preserve = uncached; + float32Preserve.target.float32DenormPreserve = true; + require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float32Preserve)).request.target.float32DenormPreserve, "f32 denormal preservation was lost in serialization"); + require(!RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(uncached)).request.target.float32DenormPreserve, "f32 denormal preservation appeared from nowhere in serialization"); + std::vector float32PreserveKey; + RecompileCacheKey::Build(float32Preserve, float32PreserveKey); + require(plainKey != float32PreserveKey && float32FlushKey != float32PreserveKey, "f32 denormal preservation did not change the cache key"); auto changedLayout = request; changedLayout.layout.pushConstantSizeBytes = 64; require(!Recompile(changedLayout).cacheHit, "binding layout change reused an incompatible variant"); diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 19711b8ddc..93bec96a00 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -289,6 +289,7 @@ private: append(key, value.spannedWaves); append(key, value.robustBufferAccess2); append(key, value.float32FlushToZero); + append(key, value.float32DenormPreserve); } }; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 76e0e11888..5f096232b3 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -827,7 +827,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(39u); + writer.WriteU32(41u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -858,6 +858,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writer.WriteU32(request.context.vertex->depthRemapMode); } writer.WriteBool(request.target.float32FlushToZero); + writer.WriteBool(request.target.float32DenormPreserve); return base64Encode(buffer); } @@ -866,7 +867,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 39u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 41u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); @@ -911,6 +912,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const } } if (version >= 36u) result.request.target.float32FlushToZero = reader.ReadBool(); + if (version >= 41u) result.request.target.float32DenormPreserve = reader.ReadBool(); if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data"); return result; } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 9246de6bda..dfb9f5d115 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -597,6 +597,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.spannedWaves = request.target.spannedWaves; targetOptions.robustBufferAccess2 = request.target.robustBufferAccess2; targetOptions.float32FlushToZero = request.target.float32FlushToZero; + targetOptions.float32DenormPreserve = request.target.float32DenormPreserve; targetOptions.meshBarycentricPixel = request.shader.stage == ShaderStage::Fragment && request.graphics && request.graphics->mesh; if (request.shader.stage == ShaderStage::Mesh && request.graphics && request.graphics->meshBarycentrics) { const auto& paired = *request.graphics->meshBarycentrics; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index a0b6c665fb..3d97d20f77 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -290,6 +290,7 @@ struct SpirvTarget { bool robustBufferAccess2 = false; bool depthRangeRemap = false; bool float32FlushToZero = false; + bool float32DenormPreserve = false; }; struct BindingLayout { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index c563510d54..1bcd3ad560 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -29,6 +29,7 @@ struct SpirvTargetOptions { bool spannedWaves = false; bool robustBufferAccess2 = false; bool float32FlushToZero = false; + bool float32DenormPreserve = false; bool meshBarycentricPixel = false; bool meshBarycentricVertex = false; std::uint32_t barycentricSmoothLocation = 0xffffffffu; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 3603bb59b8..351812ca3c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -112,6 +112,7 @@ struct SpirvEmitterState { bool float16DenormPreserve = false; bool robustBufferAccess2 = false; bool float32FlushToZero = false; + bool float32DenormPreserve = false; bool meshBarycentricPixel = false; bool meshBarycentricVertex = false; std::uint32_t barycentricSmoothLocation = 0xffffffffu; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index ca0506ad1a..06cd36f1fc 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -277,6 +277,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.float16DenormPreserve = target.float16DenormPreserve; state.robustBufferAccess2 = target.robustBufferAccess2; state.float32FlushToZero = target.float32FlushToZero; + state.float32DenormPreserve = target.float32DenormPreserve; state.hostSubgroupSize = target.subgroupSize; if (state.requirements.ldsLock && LdsInDeviceMemory(state) && std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), static_cast(spv::CapabilityInt64Atomics)) != target.supportedCapabilities.end()) { state.requirements.ldsLock = false; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 3e437d8187..5fad9ba460 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -344,6 +344,9 @@ void DefineModule(SpirvEmitterState& state) { if (state.float32FlushToZero && std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityDenormFlushToZero)) != state.supportedCapabilities.end()) { state.module.EmitCapability(spv::CapabilityDenormFlushToZero); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDenormFlushToZero, 32u); + } else if (state.float32DenormPreserve && !state.float32FlushToZero) { + state.module.EmitCapability(spv::CapabilityDenormPreserve); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDenormPreserve, 32u); } const bool deviceFloat64 = std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityFloat64)) != state.supportedCapabilities.end(); state.integerInterpolation = state.requirements.float64Interpolation && !deviceFloat64; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 138602235a..8d5bae5a39 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -214,12 +214,12 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 39u; + upgraded[4] = 41u; InsertMeshBarycentricFlag(upgraded); if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 6u, 0u); - upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; truncated.pop_back(); @@ -254,12 +254,12 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 39u; + layered[4] = 41u; InsertMeshBarycentricFlag(layered); layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); if (fixture.tessellation) layered.insert(layered.end() - 35u, 6u, 0u); - layered.insert(layered.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + layered.insert(layered.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } } @@ -280,11 +280,11 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 39u; + bytes[4] = 41u; InsertMeshBarycentricFlag(bytes); bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); - bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); auto truncated23 = DecodeBase64(encoded23); @@ -410,10 +410,10 @@ int main() { if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38); auto upgraded = main38; - upgraded[4] = 39u; + upgraded[4] = 41u; if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u); main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); - upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); const auto replay38 = serializer.Deserialize(EncodeBase64(main38)); Require(replay38.request.graphics.has_value() && replay38.request.graphics->tessellation.has_value() == fixture.tessellation); @@ -429,14 +429,14 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 39u; + main25[4] = 41u; InsertMeshBarycentricFlag(main25); if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); InsertPixelDefaultsAfterFrontFace(main25); } if (fixture.tessellation) main25.insert(main25.end() - 35u, 1u, 0u); - main25.insert(main25.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + main25.insert(main25.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); } } @@ -447,13 +447,19 @@ int main() { for (const bool preserve : {false, true}) { request.target.float16DenormPreserve = preserve; request.target.float32FlushToZero = !preserve; + request.target.float32DenormPreserve = preserve; const auto encoded = serializer.Serialize(request); const auto replay = serializer.Deserialize(encoded); - Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve); + Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 39u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 41u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + auto version40 = bytes; + version40.pop_back(); + version40[4] = 40u; + const auto replay40 = serializer.Deserialize(EncodeBase64(version40)); + Require(!replay40.request.target.float32DenormPreserve && replay40.request.target.float32FlushToZero == !preserve); auto future = bytes; - future[4] = 40u; + future[4] = 42u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); @@ -496,6 +502,7 @@ int main() { Require(serializer.Deserialize(serializer.Serialize(request)).request.target.spannedWaves == spanned); request.target.float32FlushToZero = true; auto version35 = DecodeBase64(serializer.Serialize(request)); + version35.pop_back(); const auto stencilOperationValue = std::search(version35.begin(), version35.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); Require(stencilOperationValue < version35.end() && *stencilOperationValue == 0u); version35.erase(stencilOperationValue); From 3117837b35cab422512163aa4138325ab285acbb Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 21 Oct 2026 10:44:22 +1300 Subject: [PATCH 213/219] docs(dev): list APS5_NO_HW_F32_PRESERVE in ENVIRONMENT.md --- docs/dev/ENVIRONMENT.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 88b972567d..0ca7e9a29e 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -240,6 +240,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_HOOK_SNAPSHOT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | takes the GpuMutex on every access instead of using the pending-write snapshot | | `APS5_NO_HOST_IMPORT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables importing guest memory, so address-based shaders copy instead of using it in place | | `APS5_NO_HW_F32_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | skips the startup f32 denormal flush probe and keeps the emulated flush in every shader | +| `APS5_NO_HW_F32_PRESERVE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | stops requesting DenormPreserve for 32-bit floats, so the device keeps its default f32 denormal handling | | `APS5_NO_IMAGE_PAGE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | leaves the main guest image's page-protection table uncached | | `APS5_NO_INDIRECT_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp` | | rejects indirect draw packets at submission validation | | `APS5_NO_JOIN_WAW_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets a contiguous store join the pending one without the write-after-write check | From e64d42be1f27d9679d55579c718eee66e386d895 Mon Sep 17 00:00:00 2001 From: janzezno Date: Tue, 20 Oct 2026 23:57:48 +0200 Subject: [PATCH 214/219] test(agc): resubmit an SGPR pattern fill whose batch retired before the read saw it --- .../tests/execution/SgprPatternFillKeys.cpp | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp index e35849eb7d..4996b37657 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp @@ -95,6 +95,9 @@ void MatchTests() { Require(!MatchSgprPatternFill(packet, Code, data, noGroup), "a dispatch without the group id matched"); } +volatile std::uint32_t Finished = 0; +std::uint32_t Submitted = 0; + void Submit(const std::vector& data, std::uint32_t groups) { std::vector words; const auto registers = [&](std::uint32_t first, std::span values) { @@ -111,6 +114,8 @@ void Submit(const std::vector& data, std::uint32_t groups) { registers(0x213, resource); registers(0x240, data); words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u}); + const auto finished = reinterpret_cast(&Finished); + words.insert(words.end(), {0xc0033700u, 0x00100200u, static_cast(finished), static_cast(finished >> 32u), ++Submitted}); Packet packet{words.data(), static_cast(words.size()), 0, {}}; AgcDriver::Submit(&packet, 0); } @@ -122,9 +127,11 @@ struct KeyRead { std::uint64_t lastNote = 0; -KeyRead ReadAfterWrite(std::uint64_t surfaceBytes = SurfaceBytes) { +KeyRead SubmitAndRead(const std::vector& data, std::uint32_t groups, std::uint64_t surfaceBytes = SurfaceBytes) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + Submit(data, groups); for (;;) { + bool retired = false; { std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); auto* recorder = AgcDriver::Graphics::Recorder::Active(); @@ -133,7 +140,9 @@ KeyRead ReadAfterWrite(std::uint64_t surfaceBytes = SurfaceBytes) { const auto keys = AgcDriver::Graphics::CurrentDccKeys(KeysAddress(), surfaceBytes); return {keys, !recorder->PendingWriteOverlaps(KeysAddress(), KeyBytes)}; } + retired = Finished == Submitted; } + if (retired) Submit(data, groups); Require(std::chrono::steady_clock::now() < deadline, "the submitted dispatch never became a pending write over the keys"); std::this_thread::yield(); } @@ -159,38 +168,31 @@ void ExecuteTests() { const std::array, 6> codes{{{0x00, DccKeys::Clear0000}, {0x40, DccKeys::Clear0001}, {0x80, DccKeys::Clear1110}, {0xc0, DccKeys::Clear1111}, {0x20, DccKeys::ClearRegister}, {0xff, DccKeys::Uncompressed}}}; for (const auto& [key, keys] : codes) { Settle(); - Submit(UserData(Dwords, Dwords, Uniform(key)), Dwords / 64); - const auto read = ReadAfterWrite(); + const auto read = SubmitAndRead(UserData(Dwords, Dwords, Uniform(key)), Dwords / 64); Require(read.keys == keys && !read.waited, Describe("a pending key code fill by the kernel", read)); RequireStored(Dwords, Uniform(key), 0, "the kernel's key code fill"); } - Submit(UserData(Dwords, Dwords, Uniform(0x5a)), Dwords / 64); - const auto other = ReadAfterWrite(); + const auto other = SubmitAndRead(UserData(Dwords, Dwords, Uniform(0x5a)), Dwords / 64); Require(other.keys == DccKeys::Mixed && other.waited, Describe("a pending fill of a byte that is not a key code", other)); RequireStored(Dwords, Uniform(0x5a), 0, "the kernel's fill of another byte"); const Pattern several{0x40404040u, 0x40404040u, 0x40404040u, 0x80808080u}; - Submit(UserData(Dwords, Dwords, several), Dwords / 64); - const auto mixed = ReadAfterWrite(); + const auto mixed = SubmitAndRead(UserData(Dwords, Dwords, several), Dwords / 64); Require(mixed.keys == DccKeys::Mixed && mixed.waited, Describe("a pending fill of several key codes", mixed)); RequireStored(Dwords, several, 0, "the kernel's fill of several key codes"); - Submit(UserData(Dwords, Dwords, Uniform(0x40)), Dwords / 64); - const auto first = ReadAfterWrite(); + const auto first = SubmitAndRead(UserData(Dwords, Dwords, Uniform(0x40)), Dwords / 64); Require(first.keys == DccKeys::Clear0001 && !first.waited, Describe("a pending 0001 fill by the kernel", first)); - Submit(UserData(Dwords, Dwords / 2, Uniform(0x5a)), Dwords / 64); - const auto later = ReadAfterWrite(); + const auto later = SubmitAndRead(UserData(Dwords, Dwords / 2, Uniform(0x5a)), Dwords / 64); Require(later.keys == DccKeys::Mixed && later.waited, Describe("a later writer over half of a pending 0001 fill", later)); Settle(); for (std::uint32_t i = 0; i < Dwords; ++i) Require(Keys[i] == (i < Dwords / 2 ? 0x5a5a5a5au : 0x40404040u), "the later writer over the 0001 fill: dword " + std::to_string(i) + " holds " + std::to_string(Keys[i])); - Submit(UserData(Dwords, Dwords / 2, Uniform(0xc0)), Dwords / 64); - const auto half = ReadAfterWrite(); + const auto half = SubmitAndRead(UserData(Dwords, Dwords / 2, Uniform(0xc0)), Dwords / 64); Require(half.keys == DccKeys::Mixed, Describe("a pending 1111 fill of half the keys over 0001 keys", half)); Settle(); - Submit(UserData(Dwords, Dwords / 2, Uniform(0xc0)), Dwords / 64); - const auto inside = ReadAfterWrite(SurfaceBytes / 2); + const auto inside = SubmitAndRead(UserData(Dwords, Dwords / 2, Uniform(0xc0)), Dwords / 64, SurfaceBytes / 2); Require(inside.keys == DccKeys::Clear1111, Describe("the half of the keys a pending 1111 fill covers", inside)); Settle(); for (std::uint32_t i = 0; i < Dwords; ++i) Require(Keys[i] == (i < Dwords / 2 ? 0xc0c0c0c0u : 0x40404040u), "the 1111 fill of half the keys: dword " + std::to_string(i) + " holds " + std::to_string(Keys[i])); From bc49073a2bd7b15125ecb5d28117728aa2f0a9eb Mon Sep 17 00:00:00 2001 From: janzezno Date: Wed, 21 Oct 2026 00:12:40 +0200 Subject: [PATCH 215/219] test(agc): resubmit a retired SGPR pattern fill a bounded number of times and cover it deterministically --- .../tests/execution/SgprPatternFillKeys.cpp | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp index 4996b37657..c6dd1e4ca1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp @@ -7,6 +7,7 @@ #include "VulkanTestDevice.hpp" #include #include +#include #include #include #include @@ -95,8 +96,9 @@ void MatchTests() { Require(!MatchSgprPatternFill(packet, Code, data, noGroup), "a dispatch without the group id matched"); } -volatile std::uint32_t Finished = 0; +alignas(8) std::uint32_t Finished = 0; std::uint32_t Submitted = 0; +constexpr std::uint32_t MaxResubmits = 16; void Submit(const std::vector& data, std::uint32_t groups) { std::vector words; @@ -123,13 +125,21 @@ void Submit(const std::vector& data, std::uint32_t groups) { struct KeyRead { DccKeys keys; bool waited; + std::uint32_t resubmits; }; std::uint64_t lastNote = 0; -KeyRead SubmitAndRead(const std::vector& data, std::uint32_t groups, std::uint64_t surfaceBytes = SurfaceBytes) { +void Settle() { + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(KeysAddress(), KeyBytes); +} + +KeyRead SubmitAndRead(const std::vector& data, std::uint32_t groups, std::uint64_t surfaceBytes = SurfaceBytes, bool retireFirst = false) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); Submit(data, groups); + if (retireFirst) Settle(); + std::uint32_t resubmits = 0; for (;;) { bool retired = false; { @@ -138,21 +148,20 @@ KeyRead SubmitAndRead(const std::vector& data, std::uint32_t grou if (const auto noted = recorder != nullptr ? recorder->NewestWriteNote(KeysAddress(), KeyBytes) : 0; noted > lastNote) { lastNote = noted; const auto keys = AgcDriver::Graphics::CurrentDccKeys(KeysAddress(), surfaceBytes); - return {keys, !recorder->PendingWriteOverlaps(KeysAddress(), KeyBytes)}; + return {keys, !recorder->PendingWriteOverlaps(KeysAddress(), KeyBytes), resubmits}; } - retired = Finished == Submitted; + retired = std::atomic_ref(Finished).load() == Submitted; + } + if (retired) { + Require(resubmits < MaxResubmits, "the dispatch completed " + std::to_string(resubmits + 1) + " times without a pending write over the keys"); + ++resubmits; + Submit(data, groups); } - if (retired) Submit(data, groups); Require(std::chrono::steady_clock::now() < deadline, "the submitted dispatch never became a pending write over the keys"); std::this_thread::yield(); } } -void Settle() { - AgcDriverWaitIdle_nid_postfix(); - AgcDriver::GuestMemory::FlushGpuWrites(KeysAddress(), KeyBytes); -} - void RequireStored(std::uint32_t count, const Pattern& pattern, std::uint32_t before, const std::string& what) { Settle(); for (std::uint32_t i = 0; i < Dwords; ++i) { @@ -173,6 +182,12 @@ void ExecuteTests() { RequireStored(Dwords, Uniform(key), 0, "the kernel's key code fill"); } + Settle(); + const auto retired = SubmitAndRead(UserData(Dwords, Dwords, Uniform(0x40)), Dwords / 64, SurfaceBytes, true); + Require(retired.resubmits > 0, "a fill that retired before the read was not submitted again"); + Require(retired.keys == DccKeys::Clear0001 && !retired.waited, Describe("a pending 0001 fill submitted again after it retired before the read", retired)); + RequireStored(Dwords, Uniform(0x40), 0, "the 0001 fill submitted again"); + const auto other = SubmitAndRead(UserData(Dwords, Dwords, Uniform(0x5a)), Dwords / 64); Require(other.keys == DccKeys::Mixed && other.waited, Describe("a pending fill of a byte that is not a key code", other)); RequireStored(Dwords, Uniform(0x5a), 0, "the kernel's fill of another byte"); From 13a3897f96366bc2a79825e3e5b1b083456d30c1 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 11:49:12 +1300 Subject: [PATCH 216/219] test(libc): pin the MXCSR trap masks and the both-units trap result Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/tests/GuestFloatingPoint.cpp | 29 +++++++++++++------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/core/libs/tests/GuestFloatingPoint.cpp b/core/libs/tests/GuestFloatingPoint.cpp index 75cb6dcb00..e8771fc7ca 100644 --- a/core/libs/tests/GuestFloatingPoint.cpp +++ b/core/libs/tests/GuestFloatingPoint.cpp @@ -169,27 +169,26 @@ unsigned SseMasks() { return (ReadEnvironment().sse >> 7) & 0x3fu; } -int PreviousTraps() { - return static_cast(~(SseMasks() | X87Masks()) | ~0x3fu); -} - void CheckTraps() { const auto seeded = SeedFlags(0, 0); Require(feenableexcept_nid_postfix(0x49) == static_cast(0xffffffc0u), "enable reports no previous trap as the console does"); Require(X87Masks() == 0x36 && fegettrapenable_nid_postfix() == 0x09, "enable unmasks the x87 exceptions"); - Require(SseMasks() == 0x36 || SseMasks() == 0x3f, "enable unmasks the SSE exceptions"); - int previous = PreviousTraps(); - Require(fedisableexcept_nid_postfix(0x41) == previous, "disable returns the traps enabled on both units"); + Require(SseMasks() == 0x36, "enable unmasks the SSE exceptions"); + Require(fedisableexcept_nid_postfix(0x41) == static_cast(0xffffffc9u), "disable returns the traps enabled on both units"); Require(X87Masks() == 0x37 && fegettrapenable_nid_postfix() == 0x08, "disable masks the x87 exception"); - Require(SseMasks() == 0x37 || SseMasks() == 0x3f, "disable masks the SSE exception"); - previous = PreviousTraps(); - Require(__feenableexcept_nid_postfix(0x02) == previous && X87Masks() == 0x35, "the internal enable differs"); - previous = PreviousTraps(); - Require(__fedisableexcept_nid_postfix(0x3f) == previous && X87Masks() == 0x3f && fegettrapenable_nid_postfix() == 0, "the internal disable differs"); + Require(SseMasks() == 0x37, "disable masks the SSE exception"); + Require(__feenableexcept_nid_postfix(0x02) == static_cast(0xffffffc8u) && X87Masks() == 0x35, "the internal enable differs"); + Require(__fedisableexcept_nid_postfix(0x3f) == static_cast(0xffffffcau) && X87Masks() == 0x3f && fegettrapenable_nid_postfix() == 0, "the internal disable differs"); Require(fesettrapenable_nid_postfix(0x40) == 1 && X87Masks() == 0x3f, "an out-of-range trap set was accepted"); - Require(fesettrapenable_nid_postfix(0x10004) == 0 && fegettrapenable_nid_postfix() == 0x04, "a trap set did not use the low 16 bits"); - Require(fesettrapenable_nid_postfix(0x01) == 0 && fegettrapenable_nid_postfix() == 0x05, "a trap set masked enabled traps"); - Require(fesettrapenable_nid_postfix(0) == 0 && fegettrapenable_nid_postfix() == 0x05, "an empty trap set masked enabled traps"); + Require(fesettrapenable_nid_postfix(0x10004) == 0 && fegettrapenable_nid_postfix() == 0x04 && SseMasks() == 0x3b, "a trap set did not use the low 16 bits"); + Require(fesettrapenable_nid_postfix(0x01) == 0 && fegettrapenable_nid_postfix() == 0x05 && SseMasks() == 0x3a, "a trap set masked enabled traps"); + Require(fesettrapenable_nid_postfix(0) == 0 && fegettrapenable_nid_postfix() == 0x05 && SseMasks() == 0x3a, "an empty trap set masked enabled traps"); + auto mixed = ReadEnvironment(); + mixed.x87[0] = (mixed.x87[0] & ~0x3fu) | 0x36; + mixed.sse = (mixed.sse & ~(0x3fu << 7)) | (0x3au << 7); + WriteEnvironment(mixed); + Require(feenableexcept_nid_postfix(0) == static_cast(0xffffffc1u), "enable reports only the traps enabled on both units"); + Require(fegettrapenable_nid_postfix() == 0x09, "the trap query reads only the x87 control word"); WriteEnvironment(seeded); } From 763702c5ce193b47cfad27adedaa863f9c6b21ab Mon Sep 17 00:00:00 2001 From: mpratsaki <110930793+mpratsaki@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:59:17 +0300 Subject: [PATCH 217/219] fix(relinker): use the Windows file identity in module and executable checks Guest module discovery skipped the input executable and the --executable check rejected an executable converted twice through std::filesystem::equivalent, which MinGW reports as false for hard links whose names give different permissions, such as .bin and .exe. Move the GetFileInformationByHandle comparison to Io::SameFile and use it for these checks and for SameOutputFile. Co-Authored-By: Claude Opus 5.5 --- core/relinker/CMakeLists.txt | 1 + core/relinker/cli/src/NativeModules.cpp | 3 +- core/relinker/io/include/io/FileIdentity.hpp | 12 ++++++ core/relinker/io/src/FileIdentity.cpp | 37 +++++++++++++++++++ core/relinker/main.cpp | 27 ++------------ .../relinker/tests/test_executables.py | 11 ++++++ .../relinker/tests/test_guest_module_dir.py | 9 +++++ docs/dev/debt/relinker.md | 2 +- 8 files changed, 77 insertions(+), 25 deletions(-) create mode 100644 core/relinker/io/include/io/FileIdentity.hpp create mode 100644 core/relinker/io/src/FileIdentity.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index daca3d5f77..c454613dbe 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -31,6 +31,7 @@ add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/NativePath.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileIdentity.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/Amd64OnlyConverter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp diff --git a/core/relinker/cli/src/NativeModules.cpp b/core/relinker/cli/src/NativeModules.cpp index c90ef5027e..f01ac374d4 100644 --- a/core/relinker/cli/src/NativeModules.cpp +++ b/core/relinker/cli/src/NativeModules.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -64,7 +65,7 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil continue; } if (!entry.is_regular_file()) continue; - if (std::filesystem::equivalent(entry.path(), inputPath)) continue; + if (Io::SameFile(entry.path(), inputPath)) continue; bool self = false; if (isElf(entry.path(), self)) paths.push_back(entry.path()); else if (self) diff --git a/core/relinker/io/include/io/FileIdentity.hpp b/core/relinker/io/include/io/FileIdentity.hpp new file mode 100644 index 0000000000..e33cfc5337 --- /dev/null +++ b/core/relinker/io/include/io/FileIdentity.hpp @@ -0,0 +1,12 @@ +#ifndef IO_FILEIDENTITY_HPP +#define IO_FILEIDENTITY_HPP + +#include + +namespace Io { + +bool SameFile(const std::filesystem::path& first, const std::filesystem::path& second); + +} + +#endif diff --git a/core/relinker/io/src/FileIdentity.cpp b/core/relinker/io/src/FileIdentity.cpp new file mode 100644 index 0000000000..99d7ff2fe5 --- /dev/null +++ b/core/relinker/io/src/FileIdentity.cpp @@ -0,0 +1,37 @@ +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +namespace Io { + +#ifdef _WIN32 +namespace { + +std::tuple FileIdentity(const std::filesystem::path& path) { + const auto handle = CreateFileW(path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) + throw std::filesystem::filesystem_error("Cannot open file identity", path, std::error_code(static_cast(GetLastError()), std::system_category())); + BY_HANDLE_FILE_INFORMATION information{}; + const bool read = GetFileInformationByHandle(handle, &information) != 0; + const auto error = GetLastError(); + CloseHandle(handle); + if (!read) throw std::filesystem::filesystem_error("Cannot read file identity", path, std::error_code(static_cast(error), std::system_category())); + return {information.dwVolumeSerialNumber, information.nFileIndexHigh, information.nFileIndexLow}; +} + +} +#endif + +bool SameFile(const std::filesystem::path& first, const std::filesystem::path& second) { +#ifdef _WIN32 + return FileIdentity(first) == FileIdentity(second); +#else + return std::filesystem::equivalent(first, second); +#endif +} + +} diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index fe626ea38d..33240459b0 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -17,8 +18,6 @@ #include #include #include -#include -#include #include #ifdef _WIN32 @@ -117,26 +116,8 @@ private: std::filesystem::path directory; }; -#ifdef _WIN32 -std::tuple FileIdentity(const std::filesystem::path& path) { - const auto handle = CreateFileW(path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); - if (handle == INVALID_HANDLE_VALUE) - throw std::filesystem::filesystem_error("Cannot open file identity", path, std::error_code(static_cast(GetLastError()), std::system_category())); - BY_HANDLE_FILE_INFORMATION information{}; - const bool read = GetFileInformationByHandle(handle, &information) != 0; - const auto error = GetLastError(); - CloseHandle(handle); - if (!read) throw std::filesystem::filesystem_error("Cannot read file identity", path, std::error_code(static_cast(error), std::system_category())); - return {information.dwVolumeSerialNumber, information.nFileIndexHigh, information.nFileIndexLow}; -} -#endif - bool SameOutputFile(const std::filesystem::path& first, const std::filesystem::path& second) { -#ifdef _WIN32 - return FileIdentity(first) == FileIdentity(second); -#else - return std::filesystem::equivalent(first, second); -#endif + return Io::SameFile(first, second); } bool SameOutputDestination(const std::filesystem::path& first, const std::filesystem::path& second) { @@ -305,10 +286,10 @@ int Run(const int argc, char* argv[]) { const auto relative = ExecutableRelative(executable); const auto input = ResolveExtraExecutable(moduleRoot, moduleDirectories, relative, executable); const auto output = Relinker::GamePatch::OutputExecutablePath(Io::NativePath(absPath), executable); - if (std::filesystem::equivalent(Io::NativePath(args.inputPath), input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); + if (Io::SameFile(Io::NativePath(args.inputPath), input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); if (output.lexically_normal() == Io::NativePath(absPath).lexically_normal()) throw Domain::RelinkerException("Executable outputs collide: " + Io::Utf8Path(output)); for (const auto& extra : extraExecutables) { - if (std::filesystem::equivalent(extra.Input, input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); + if (Io::SameFile(extra.Input, input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); if (extra.Output.lexically_normal() == output.lexically_normal()) throw Domain::RelinkerException("Executable outputs collide: " + Io::Utf8Path(output)); } extraExecutables.push_back({executable, relative, input, output}); diff --git a/core/relinker/relinker/tests/test_executables.py b/core/relinker/relinker/tests/test_executables.py index eb61d2b294..4e5d658d12 100644 --- a/core/relinker/relinker/tests/test_executables.py +++ b/core/relinker/relinker/tests/test_executables.py @@ -63,6 +63,17 @@ def main(): assert result.returncode == 2 and message in result.stderr, (options, result.stderr) assert not output.exists(), options + for linked, options in (("eboot.exe", ["--executable", "picker/eboot.exe"]), + ("picker.exe", ["--executable", "picker/picker.bin", "--executable", "picker/picker.exe"])): + case = work / f"{windows}-hardlink-{linked}" + (case / "picker").mkdir(parents=True) + target = case / ("eboot.bin" if linked == "eboot.exe" else "picker/picker.bin") + target.write_bytes(main_fixture()) + os.link(target, case / "picker" / linked) + result, output = convert(case, windows, options) + assert result.returncode == 2 and "converted twice" in result.stderr, (linked, result.stdout, result.stderr) + assert not output.exists(), linked + for index, (directory, executable) in enumerate((("Picker", "picker/picker.bin"), ("picker", "PICKER/picker.bin"), ("linked", "picker/picker.bin"))): case = work / f"{windows}-identity-{index}" case.mkdir() diff --git a/core/relinker/relinker/tests/test_guest_module_dir.py b/core/relinker/relinker/tests/test_guest_module_dir.py index 74180910de..c80b9ce2b1 100644 --- a/core/relinker/relinker/tests/test_guest_module_dir.py +++ b/core/relinker/relinker/tests/test_guest_module_dir.py @@ -214,6 +214,15 @@ def main(): run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) assert run.returncode == 42, (run.returncode, run.stdout, run.stderr) + case = work / f"{windows}-root-input-link" + case.mkdir() + (case / "root.prx").write_bytes(module_with_symbol(True)) + (case / "input.elf").write_bytes(main_fixture()) + os.link(case / "input.elf", case / "input-link.exe") + result, output = convert(case, windows, ["--module-dir", "."]) + assert result.returncode == 0, (result.stdout, result.stderr) + assert list((case / "app0").rglob("*.guest.prx")) == [case / "app0" / "root.prx.guest.prx"], list(case.rglob("*.guest.prx")) + case = work / f"{windows}-root-needed" case.mkdir() (case / "needed.prx").write_bytes(module_with_symbol(True)) diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 144329fd0b..e496bc72a6 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -4,7 +4,7 @@ - [Executable guest imports](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - on Linux and macOS, a relocated executable import resolved to one guest provider requires that module and its dependencies at startup, even when it was discovered through `--module-dir` without a DT_NEEDED entry. Unreferenced symbols and no-op relocations do not require an otherwise deferred module. Console module-start ordering is unmeasured. -- Windows [input and output collision checks](../../../core/relinker/main.cpp) compare the volume serial number and the 64-bit file index from `GetFileInformationByHandle`. ReFS file IDs are 128-bit, so two distinct files on ReFS can in principle compare equal and the conversion is refused; `FILE_ID_INFO` is not used. +- Windows [file identity checks](../../../core/relinker/io/src/FileIdentity.cpp) (input and output collisions, the input skipped during guest module discovery, executables converted twice) compare the volume serial number and the 64-bit file index from `GetFileInformationByHandle`. ReFS file IDs are 128-bit, so two distinct files on ReFS can in principle compare equal: a collision or a repeated executable is reported, or a module is skipped; `FILE_ID_INFO` is not used. - [Runtime-loaded modules](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) that another module needs are initialized at startup with no start arguments, before the module that needs them is loaded; the console is assumed to start them when that module is loaded. - The executable file that [relinker](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens a console when launched, unless it is relinked with `--windows-gui`, which selects the GUI subsystem. A GUI build started from Explorer has no standard output or error handle; the [entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) gives each missing one `anyps5.log` beside the executable, replacing the previous run's file (#3282). Errors about the executable path or about waiting for the previous LoadExec process are written before the log is opened and are lost when the standard error handle is missing. If the log cannot be created, output to the missing handles is lost. - [Guest import registries](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - declared host imports of bundled modules are recorded with their module name and relocation target, even when another guest module exports the same stripped NID. No-op relocations are omitted. Registries describe import bindings, not implementation correctness or runtime reachability. From 401dee24b6dbfc4d3da865dac59c15ab9d2762af Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 21 Oct 2026 02:01:58 +0200 Subject: [PATCH 218/219] docs(net): keep transfer polling debt concise --- docs/dev/debt/libSceNet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index c09cdfcb8c..d54f1fc62c 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -191,7 +191,7 @@ - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) set `TCP_NODELAY` (`IPPROTO_TCP`, 1) on the host socket of a stream socket and read it back as `TF_NODELAY` (4) or 0, as FreeBSD 11.4's [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1626-L1650) does. Every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_NOPUSH` and `TCP_KEEPIDLE` among them, which FreeBSD implements) fails with `ENOPROTOOPT`, and the `IPPROTO_TCP` level on a socket that is not a stream socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/ip_output.c#L943-L944) answers it. A set on a connection that FreeBSD's [`tcp_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1498-L1502) refuses with `ECONNRESET` (refused, reset or in `TIME_WAIT`) succeeds here because the host accepts it. A null value with a nonzero length fails with `EINVAL`, where FreeBSD's `kern_setsockopt` returns `EFAULT`, and a get into less than an `int` fails with `EINVAL`, where `sooptcopyout` truncates. None of this was measured on a console. - [sceNetGetSockInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reports 0 for the name, pid, policy, priority, queue lengths, virtual ports, state, bandwidth, drops and waits, sets no status flag except `SELF` (0x1) and `NONBLOCK` (0x200000), and reports the host's buffer sizes where none was set. An IPv6 socket fails with `EAFNOSUPPORT` and is not counted or listed; shadPS4 lists it with zero addresses. `flags` bits other than 0x40 fail with `EINVAL`, where shadPS4 ignores `flags`; 0x40 adds sockets in TIME_WAIT ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs)), but closed sockets are not kept. Otherwise the `s` below 0 count and list, the return value and `EINVAL` for a null `info` or an `n` below 1 follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/06e813ff8c8e2a6d81b02cabfcc9543c3debf78e/src/core/libraries/net/net.cpp#L1498-L1522). Not measured on a console. - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported -- [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) in `sceNetSend`, `sceNetSendto`, `sceNetRecv`, `sceNetRecvfrom`, `sceNetRecvmsg` and `sceNetSendmsg` makes that one call non-blocking, following FreeBSD 11.4's `soreceive_generic` and `sosend_generic` (`sys/kern/uipc_socket.c`). The value `0x80` is FreeBSD's (`sys/sys/socket.h`) and [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/net/net_types.h#L104). On Windows native sockets stay non-blocking; ordinary blocking operations retry after readiness, while `MSG_DONTWAIT` returns the native transfer's result without waiting. The internal native-socket lookup also supplies that transfer path to SSL, retaining the original socket owner and its current mode and timeouts. Blocking stream sends continue until the whole buffer is queued or their original deadline or a host error ends the call; queued bytes are reported on partial completion, following FreeBSD's `sosend_generic`. Readiness waits poll at most every 10 ms to notice local close, so idle blocked operations wake periodically. Console ordering between competing readers and writers is unmeasured. +- [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - console ordering between competing readers and writers is unmeasured. The per-call behavior follows FreeBSD 11.4's `soreceive_generic` and `sosend_generic` (`sys/kern/uipc_socket.c`); `0x80` is FreeBSD's flag value (`sys/sys/socket.h`). On Windows native sockets stay non-blocking. `MSG_DONTWAIT` and guest `SO_NBIO` return one host transfer's result. Blocking stream sends queue the whole buffer with one deadline and return progress after a timeout or host error. SSL uses this path through the native owner, retaining the socket, its mode and timeouts. Readiness waits poll at most every 10 ms for local close, so idle blocked operations wake periodically. - [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does - [sceNetResolverStartNtoa6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) resolves a host name to its first IPv6 address through the host resolver (`getaddrinfo` with `AF_INET6`) and writes the 16-byte address; the `(rid, hostname, addr, timeout, retry, flags)` signature follows `sceNetResolverStartNtoa`. A name with no IPv6 address fails with `RESOLVER_ENODNS` (`0x804101E1`) like a failed lookup; whether the console returns IPv4-mapped addresses instead is not verified. Timeout and retry are ignored as in `sceNetResolverStartNtoa`, and any nonzero flags throw because their meaning for this call is not known. Unverified on hardware. From b5bb956530dc069265a15a269cc77b1bd8e6b1f1 Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Wed, 21 Oct 2026 08:30:15 +0800 Subject: [PATCH 219/219] test(libc): link the resident release test and keep its valid case in one run The test throws exceptions but did not link libc, so with -fno-asynchronous-unwind-tables it failed to link (no __gxx_personality_v0) on the supported WinLibs toolchain; the Windows CI job does not run on pull requests. The valid case now maps 2 MiB at a 2-MiB boundary, so the mapping is one resident run and both Reset calls take the exact-run path, and the test requires that run. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/CMakeLists.txt | 4 ++++ core/libs/tests/WindowsMappingsRelease.cpp | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 6c8adf1bff..e1f92f6965 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -2,9 +2,13 @@ if(BUILD_TESTING) if(WIN32) add_test_executable(windows_mappings_release_tests ${LIBS_INCLUDE_DIR}/tests/WindowsMappingsRelease.cpp) target_include_directories(windows_mappings_release_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(windows_mappings_release_tests PRIVATE libc) + configure_windows_unwind(windows_mappings_release_tests) add_test(NAME windows_mappings_release COMMAND windows_mappings_release_tests) + set_tests_properties(windows_mappings_release PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") foreach(mode 0 1 2) add_test(NAME windows_mappings_release_mismatch_${mode} COMMAND windows_mappings_release_tests ${mode}) + set_tests_properties(windows_mappings_release_mismatch_${mode} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endforeach() endif() diff --git a/core/libs/tests/WindowsMappingsRelease.cpp b/core/libs/tests/WindowsMappingsRelease.cpp index 4694e926de..6b2f016c97 100644 --- a/core/libs/tests/WindowsMappingsRelease.cpp +++ b/core/libs/tests/WindowsMappingsRelease.cpp @@ -41,20 +41,30 @@ static void CheckMetadataMismatch(unsigned mode) { static void CheckValidRelease() { constexpr std::size_t bytes = 2 * 1024 * 1024; auto& mappings = GuestArena::WindowsMappings::Get(); - auto* base = static_cast(mappings.Reserve(nullptr, bytes)); - Require(base != nullptr); + auto* reserved = static_cast(mappings.Reserve(nullptr, 2 * bytes)); + Require(reserved != nullptr); + const auto start = (reinterpret_cast(reserved) + bytes - 1) & ~static_cast(bytes - 1); + auto* base = reinterpret_cast(start); + const auto oneRun = [&] { + std::uintptr_t first = 0; + std::uintptr_t last = 0; + return mappings.ResidentSpan(start + bytes - 1, 1, &first, &last) && first == start && last == start + bytes; + }; const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, bytes, nullptr); Require(section != nullptr); mappings.Map(base, bytes, section, 0, PAGE_READWRITE, true, true); + Require(oneRun()); base[0] = 31; base[bytes - 1] = 89; DWORD previous; Require(VirtualProtect(base + 65536, 65536, PAGE_READONLY, &previous)); mappings.Reset(base, bytes); mappings.Map(base, bytes, section, 0, PAGE_READWRITE, true, true); + Require(oneRun()); Require(base[0] == 31 && base[bytes - 1] == 89); mappings.Reset(base, bytes); - Require(VirtualFree(base, 0, MEM_RELEASE)); + Require(VirtualFree(reserved, 2 * bytes, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)); + Require(VirtualFree(reserved, 0, MEM_RELEASE)); mappings.ForgetSection(section); CloseHandle(section); }