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/167] 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 4c2b12439f3883cff7c4dcba10f31a3b047bf311 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 15 Oct 2026 14:17:17 -0400 Subject: [PATCH 002/167] fix(audio-propagation): preserve existing destination mixes --- .../prx/libSceAudioPropagation/Export.cpp | 1 - core/libs/tests/GuestAudioPropagation.cpp | 41 +++++++++++-------- docs/dev/debt/libSceAudioPropagation.md | 2 +- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index c3cdff194d..22a39889d0 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -311,7 +311,6 @@ std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle sy require(info.output != nullptr && info.outputSize != 0, __func__); if (info.format != RenderFormat) throw std::runtime_error(std::string(__func__) + ": render format " + std::to_string(info.format) + " is unknown"); } - for (std::uint32_t index = 0; index < count; ++index) std::memset(infos[index].output, 0, infos[index].outputSize); return 0; } diff --git a/core/libs/tests/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index 8930cf679d..9584ba78e5 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -143,20 +143,16 @@ int main() { Require(sceAudioPropagationSourceCreate(system, &secondSource) == 0, "second source create failed"); float outputs[2][64]; - const auto fill = [&] { std::memset(outputs, 0xab, sizeof(outputs)); }; - const auto zeroed = [&](int index) { - for (const float sample : outputs[index]) { - if (sample != 0.0f) return false; + float expected[2][64]; + const auto fill = [&] { + for (std::size_t channel = 0; channel < 2; ++channel) { + for (std::size_t sample = 0; sample < 64; ++sample) { + outputs[channel][sample] = static_cast(sample + 1) / (channel == 0 ? 128.0f : -128.0f); + } } - return true; - }; - const auto untouched = [&] { - const auto* bytes = reinterpret_cast(outputs); - for (std::size_t index = 0; index < sizeof(outputs); ++index) { - if (bytes[index] != 0xab) return false; - } - return true; + std::memcpy(expected, outputs, sizeof(outputs)); }; + const auto untouched = [&] { return std::memcmp(outputs, expected, sizeof(outputs)) == 0; }; AudioPropagation::RenderInfo infos[2]{}; const auto reset = [&] { infos[0] = {{AudioPropagation::RenderInfoId, AudioPropagation::RenderInfoSize}, source, outputs[0], sizeof(outputs[0]), AudioPropagation::RenderFormat, 0}; @@ -165,13 +161,26 @@ int main() { }; reset(); Require(sceAudioPropagationSourceRender(system, infos, 1) == 0, "render failed"); - Require(zeroed(0), "render left the output unwritten"); - const auto* tail = reinterpret_cast(outputs[1]); - Require(tail[0] == 0xab, "render wrote past the output size"); + Require(untouched(), "empty source erased an existing mix"); reset(); infos[0].outputSize = sizeof(float) * 16; Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render of two sources failed"); - Require(outputs[0][15] == 0.0f && outputs[0][16] != 0.0f && zeroed(1), "render did not write each output for its size"); + Require(untouched(), "empty sources modified separate destination mixes or their tails"); + reset(); + infos[1].output = outputs[0]; + Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render into a shared mix failed"); + Require(untouched(), "empty sources modified a shared destination mix"); + reset(); + infos[0].outputSize = sizeof(float) * 32; + infos[1].output = outputs[0] + 16; + infos[1].outputSize = sizeof(float) * 32; + Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render into overlapping mixes failed"); + Require(untouched(), "empty sources modified overlapping destination mixes"); + reset(); + std::memset(outputs, 0, sizeof(outputs)); + std::memcpy(expected, outputs, sizeof(outputs)); + Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render into silence failed"); + Require(untouched(), "empty sources modified a silent destination mix"); reset(); RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 0); }, "render without infos"); diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 0785892d93..eee79ac531 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -2,7 +2,7 @@ ### Silent stubs -- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested and sources report no audio paths. Ray results, path calculation and audio paths are accepted only when empty and throw otherwise; `sceAudioPropagationSourceRender` writes silence (zeroes) to each source's output buffer for the given size, without a dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. +- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested and sources report no audio paths. Ray results, path calculation and audio paths are accepted only when empty and throw otherwise; `sceAudioPropagationSourceRender` preserves each destination mix because there are no path contributions, following the additive-output contract in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs); console behaviour has not been measured. It adds no dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. ### Unknown function info From ab9d21afcfbc4b0ebeba3305b06d8d763b0e6c76 Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Fri, 16 Oct 2026 08:47:17 +0530 Subject: [PATCH 003/167] 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 004/167] 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 005/167] 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 02:06:48 +0200 Subject: [PATCH 006/167] 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 ce4a1dc05461e1d0656cf873018c4a9c1bbcb189 Mon Sep 17 00:00:00 2001 From: Thomas Abdelmalak Date: Sun, 18 Oct 2026 13:23:28 +0300 Subject: [PATCH 007/167] feat(libSceFont): implement handle and glyph attributes --- .../prx/libSceFont/include/FontInternal.hpp | 2 ++ core/libs/prx/libSceFont/src/Render.cpp | 16 +++++++-- core/libs/prx/libSceFont/src/Style.cpp | 17 +++++++++ .../libs/prx/libSceFont/src/Unimplemented.cpp | 15 -------- core/libs/tests/GuestFont.cpp | 35 ++++++++++++++++--- docs/dev/debt/libSceFont.md | 6 ++-- 6 files changed, 65 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libSceFont/include/FontInternal.hpp b/core/libs/prx/libSceFont/include/FontInternal.hpp index b2a742c291..1911a35344 100644 --- a/core/libs/prx/libSceFont/include/FontInternal.hpp +++ b/core/libs/prx/libSceFont/include/FontInternal.hpp @@ -31,6 +31,7 @@ struct FontState { float scaleH = 16.0f; std::map scriptLanguages; std::map typographicFeatures; + int attribute = 0; FontState() = default; FontState(const FontState&) = delete; @@ -52,6 +53,7 @@ struct GeneratedGlyph { std::vector outlineContours; bool metricsInitialized = false; bool outlineInitialized = false; + int attribute = 0; }; struct SystemFontFile { diff --git a/core/libs/prx/libSceFont/src/Render.cpp b/core/libs/prx/libSceFont/src/Render.cpp index 55d3de834d..4b667c8710 100644 --- a/core/libs/prx/libSceFont/src/Render.cpp +++ b/core/libs/prx/libSceFont/src/Render.cpp @@ -419,10 +419,20 @@ int APS5_VABI sceFontDeleteGlyph(const FontMemory* memory, FontGlyph* pGlyph) { return SCE_FONT_OK; } -int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph glyph, std::uint32_t attribute, std::uint64_t value) { +int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph glyph, int attribute, int* oldAttribute) { + auto* generated = TryGetGeneratedGlyph(glyph); + if (!generated) return SCE_FONT_ERROR_INVALID_GLYPH; + if (oldAttribute) *oldAttribute = generated->attribute; + generated->attribute = attribute; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontGlyphGetAttribute(FontGlyph glyph, int attribute, int* nowAttribute) { (void)attribute; - (void)value; - if (!glyph || glyph->magic != GLYPH_MAGIC) return SCE_FONT_ERROR_INVALID_GLYPH; + if (!nowAttribute) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* generated = TryGetGeneratedGlyph(glyph); + if (!generated) return SCE_FONT_ERROR_INVALID_GLYPH; + *nowAttribute = generated->attribute; return SCE_FONT_OK; } diff --git a/core/libs/prx/libSceFont/src/Style.cpp b/core/libs/prx/libSceFont/src/Style.cpp index 67e7abe139..e4adb89793 100644 --- a/core/libs/prx/libSceFont/src/Style.cpp +++ b/core/libs/prx/libSceFont/src/Style.cpp @@ -101,6 +101,23 @@ float ClampWeightDelta(float scale) { extern "C" { +int APS5_VABI sceFontDefineAttribute(FontHandle fontHandle, int attribute, int* oldAttribute) { + return AccessFontSettings(fontHandle, [&](FontState& state) { + if (oldAttribute) *oldAttribute = state.attribute; + state.attribute = attribute; + return SCE_FONT_OK; + }); +} + +int APS5_VABI sceFontGetAttribute(FontHandle fontHandle, int attribute, int* nowAttribute) { + (void)attribute; + if (!nowAttribute) return SCE_FONT_ERROR_INVALID_PARAMETER; + return AccessFontSettings(fontHandle, [&](FontState& state) { + *nowAttribute = state.attribute; + return SCE_FONT_OK; + }); +} + int APS5_VABI sceFontSetScriptLanguage(FontHandle fontHandle, int fontScript, int fontLanguage) { return SetFontSetting(fontHandle, &FontState::scriptLanguages, fontScript, fontLanguage); } diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index aa341a63b5..bc79aed66e 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -32,11 +32,6 @@ int APS5_VABI sceFontCreateWords() { return 0; } -int APS5_VABI sceFontDefineAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontDestroyGraphicsDevice() { NotImplemented_nid_no_patch(__func__); return 0; @@ -53,11 +48,6 @@ int APS5_VABI sceFontDestroyWords() { } -int APS5_VABI sceFontGetAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontGetFontGlyphsOutlineProfile() { NotImplemented_nid_no_patch(__func__); return 0; @@ -78,11 +68,6 @@ int APS5_VABI sceFontGetGlyphExpandBufferState() { return 0; } -int APS5_VABI sceFontGlyphGetAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontGlyphRenderImage() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 53f9158dd4..b40f821b15 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -33,6 +33,8 @@ int APS5_VABI sceFontSetScriptLanguage(FontHandle, int, int); int APS5_VABI sceFontGetScriptLanguage(FontHandle, int, int*); int APS5_VABI sceFontSetTypographicDesign(FontHandle, int, int); int APS5_VABI sceFontGetTypographicDesign(FontHandle, int, int*); +int APS5_VABI sceFontDefineAttribute(FontHandle, int, int*); +int APS5_VABI sceFontGetAttribute(FontHandle, int, int*); int APS5_VABI sceFontSetResolutionDpi(FontHandle, std::uint32_t, std::uint32_t); int APS5_VABI sceFontGetResolutionDpi(FontHandle, std::uint32_t*, std::uint32_t*); int APS5_VABI sceFontSetScalePixel(FontHandle, float, float); @@ -46,7 +48,8 @@ int APS5_VABI sceFontGetCharGlyphCode(FontHandle, std::uint32_t, std::uint32_t*) int APS5_VABI sceFontGetFontResolution(FontHandle, std::uint32_t*, float*); int APS5_VABI sceFontGetRenderScaledKerning(FontHandle, std::uint32_t, std::uint32_t, FontKerning*); int APS5_VABI sceFontGenerateCharGlyph(FontHandle, std::uint32_t, const FontGenerateGlyphDetail*, FontGlyph*); -int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph, std::uint32_t, std::uint64_t); +int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph, int, int*); +int APS5_VABI sceFontGlyphGetAttribute(FontGlyph, int, int*); int APS5_VABI sceFontDeleteGlyph(const FontMemory*, FontGlyph*); int APS5_VABI sceFontTextSourceInit(FontTextSource*, const void*, std::uint32_t, FontTextParseFunction, void*); int APS5_VABI sceFontCreateString(const FontMemory*, FontTextSource*, const FontCreateStringDetail*, FontString*); @@ -306,6 +309,19 @@ int main() { Require(sceFontGetTypographicDesign(font, 1, &setting) == SCE_FONT_OK && setting == 2); Require(sceFontGetTypographicDesign(font, 5, &setting) == SCE_FONT_OK && setting == 0); Require(sceFontGetTypographicDesign(nullptr, 1, &setting) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + int attribute = -1; + Require(sceFontGetAttribute(font, 0x11, &attribute) == SCE_FONT_OK && attribute == 0); + Require(sceFontDefineAttribute(font, 0x11, &attribute) == SCE_FONT_OK && attribute == 0); + attribute = -1; + Require(sceFontGetAttribute(font, 0x99, &attribute) == SCE_FONT_OK && attribute == 0x11); + Require(sceFontDefineAttribute(font, 0, &attribute) == SCE_FONT_OK && attribute == 0x11); + Require(sceFontDefineAttribute(font, 5, nullptr) == SCE_FONT_OK); + attribute = -1; + Require(sceFontGetAttribute(font, 0, &attribute) == SCE_FONT_OK && attribute == 5); + Require(sceFontGetAttribute(font, 0x11, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + attribute = -1; + Require(sceFontDefineAttribute(nullptr, 0x11, &attribute) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && attribute == -1); + Require(sceFontGetAttribute(nullptr, 0x11, &attribute) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && attribute == -1); std::uint32_t hDpi = 1; std::uint32_t vDpi = 1; Require(sceFontGetResolutionDpi(font, &hDpi, &vDpi) == SCE_FONT_OK && hDpi == 72 && vDpi == 72); @@ -368,10 +384,21 @@ int main() { Require(sceFontSetScalePixel(font, 100.0f, 100.0f) == SCE_FONT_OK); FontGlyph glyph = nullptr; Require(sceFontGenerateCharGlyph(font, 'A', nullptr, &glyph) == SCE_FONT_OK && glyph != nullptr); - Require(sceFontGlyphDefineAttribute(glyph, 0x11, 0) == SCE_FONT_OK); - Require(sceFontGlyphDefineAttribute(nullptr, 0x11, 0) == SCE_FONT_ERROR_INVALID_GLYPH); + int glyphAttribute = -1; + Require(sceFontGlyphDefineAttribute(glyph, 0x11, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0); + Require(sceFontGlyphGetAttribute(glyph, 0x11, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0x11); + glyphAttribute = -1; + Require(sceFontGlyphDefineAttribute(glyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0x11); + Require(sceFontGlyphDefineAttribute(glyph, 5, nullptr) == SCE_FONT_OK); + glyphAttribute = -1; + Require(sceFontGlyphGetAttribute(glyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 5); + Require(sceFontGlyphGetAttribute(glyph, 0x11, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + glyphAttribute = -1; + Require(sceFontGlyphDefineAttribute(nullptr, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH && glyphAttribute == -1); + Require(sceFontGlyphGetAttribute(nullptr, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH && glyphAttribute == -1); FontGlyphOpaque notAGlyph{}; - Require(sceFontGlyphDefineAttribute(¬AGlyph, 0x11, 0) == SCE_FONT_ERROR_INVALID_GLYPH); + Require(sceFontGlyphDefineAttribute(¬AGlyph, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH); + Require(sceFontGlyphGetAttribute(¬AGlyph, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH); Require(sceFontDeleteGlyph(&memory, &glyph) == SCE_FONT_OK && glyph == nullptr); FontKerning kerning{1.0f, 1.0f, 1.0f, 1.0f}; Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -20.0f)); diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 15c10ccaf8..6dfb5a445f 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -1,9 +1,5 @@ # libSceFont technical debt -### Silent stubs - -- [sceFontGlyphDefineAttribute](../../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) checks the glyph and ignores the attribute and its value, whose meaning is unknown. PPSA01325 sets attribute 0x11 to 0 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused, and draws the character with `sceFontRenderCharGlyphImageHorizontal` - ### Unknown function info - [sceFontSetScriptLanguage, sceFontGetScriptLanguage, sceFontSetTypographicDesign, sceFontGetTypographicDesign](../../../core/libs/prx/libSceFont/src/Style.cpp) (libSceFont) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.cs), not confirmed for the PS5. The handle keeps the language set for each script and the feature set for each typographic design and returns them, with 0 for one never set; the console default and its valid ranges are unknown, so any value is accepted. Neither setting changes shaping or rendering yet @@ -18,6 +14,8 @@ - [sceFontCharacterGetSyllableStringState](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signature `(textCharacter, int* syllableStringState)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs). Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): the state is 0 for Latin, CJK, Hangul, Arabic, Hebrew, Devanagari and the other scripts tried, and the library sets 1, 3 or 4 (never 2 on real text) for Thai while it creates the string; a null character or a null result returns `SCE_FONT_ERROR_INVALID_PARAMETER`, and a null character writes 0 to a non-null result. This returns 0 for every character outside the Thai block (U+0E00 to U+0E7F), as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1805-L1815) does, and throws for Thai characters, whose cluster states are not modelled. SharpProspero's `SceFontSyllableString` names 0 NonDetection, 1 Starting, 2 Following, 3 Depending and 4 Connecting. PPSA06699 (Hot Shots Golf 2) calls it. - [sceFontCharactersRefersTextCodes, sceFontTextCodesStepNext and sceFontTextCodesStepBack](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signatures follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L679) IDA type database and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFont.Engine.cs#L395-L406), and the 64-byte `FontTextCodes` with the order pointer at 0 and the code at 8 follows SharpProspero's [SceFontTextCodes](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFontStructs.cs#L216-L228). The traversal follows the PS4 11.00 `libSceFont.sprx` disassembly posted in [#4050](https://github.com/boykopovar/AnyPS5/pull/4050#issuecomment-6114504202), not a PS5 measurement: the cursor keeps the current, start and term characters at +0x28, +0x30 and +0x38; `Refers` never compares the character with the term and returns null for a clustered or synthetic character; `StepNext` returns null at the term after visiting it; `StepBack` returns null when the current character is the start; both steps skip synthetic characters. Assumed and not covered by the report, which says `StepNext` returns null only at the term: `StepNext` also returns null at the end of the chain (for example with a null term), and `StepBack` at the head of the chain. A null cursor or output returns null as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1696-L1740); a null text character throws, as its console result is unknown +- [sceFontDefineAttribute, sceFontGetAttribute](../../../core/libs/prx/libSceFont/src/Style.cpp), [sceFontGlyphDefineAttribute, sceFontGlyphGetAttribute](../../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - the signatures `(handle, int attribute, int* oldAttribute)` and `(handle, int attribute, int* nowAttribute)`, and the same shape for a glyph, follow the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings, matching the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1249-L1264), not confirmed on a title. Every handle and every glyph keeps one attribute, an opaque int whose meaning and valid values are unknown: define stores the new value and returns the previous one, and get returns the stored value with its `attribute` argument unused, as in KytyPS5. An attribute never set is 0. The null checks and the invalid-object errors follow the other libSceFont functions and diverge from [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1251-L1265), which returns `OK` for a null handle or glyph after writing 0 to the old-attribute pointer: every other handle and glyph entry point in this library reports the invalid object instead, and a title that passes one is already failing. shadPS4 leaves both exports unimplemented, so there is no second implementation to compare. PPSA01325 calls the glyph define with 0x11 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused and draws with `sceFontRenderCharGlyphImageHorizontal`; the three bindings all read the third argument as an out pointer, not a value, so this call sets the glyph's attribute to 0x11. The attribute changes neither shaping nor rendering. + ### 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. From f099292f8cfde933c0261b6a128dfa19359f5efc Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 18 Oct 2026 10:31:17 +0000 Subject: [PATCH 008/167] 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 d484281b6112dfb2232e787857601a1ac9e2a257 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 18 Oct 2026 11:21:00 +0000 Subject: [PATCH 009/167] 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 8aea41b5cc7caafce45ceda508536326864cadd4 Mon Sep 17 00:00:00 2001 From: Thomas Abdelmalak Date: Sun, 18 Oct 2026 14:25:29 +0300 Subject: [PATCH 010/167] test(libSceFont): assert attributes are isolated per handle and per glyph --- core/libs/tests/GuestFont.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index b40f821b15..d8ccaf2fac 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -322,6 +322,15 @@ int main() { attribute = -1; Require(sceFontDefineAttribute(nullptr, 0x11, &attribute) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && attribute == -1); Require(sceFontGetAttribute(nullptr, 0x11, &attribute) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && attribute == -1); + FontHandle otherFont = nullptr; + Require(sceFontOpenFontMemory(library, fontData.data(), static_cast(fontData.size()), nullptr, &otherFont) == SCE_FONT_OK && otherFont != nullptr); + attribute = -1; + Require(sceFontGetAttribute(otherFont, 0, &attribute) == SCE_FONT_OK && attribute == 0); + Require(sceFontDefineAttribute(otherFont, 7, &attribute) == SCE_FONT_OK && attribute == 0); + attribute = -1; + Require(sceFontGetAttribute(otherFont, 0, &attribute) == SCE_FONT_OK && attribute == 7); + Require(sceFontGetAttribute(font, 0, &attribute) == SCE_FONT_OK && attribute == 5); + Require(sceFontCloseFont(otherFont) == SCE_FONT_OK); std::uint32_t hDpi = 1; std::uint32_t vDpi = 1; Require(sceFontGetResolutionDpi(font, &hDpi, &vDpi) == SCE_FONT_OK && hDpi == 72 && vDpi == 72); @@ -399,6 +408,15 @@ int main() { FontGlyphOpaque notAGlyph{}; Require(sceFontGlyphDefineAttribute(¬AGlyph, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH); Require(sceFontGlyphGetAttribute(¬AGlyph, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH); + FontGlyph otherGlyph = nullptr; + Require(sceFontGenerateCharGlyph(font, 'V', nullptr, &otherGlyph) == SCE_FONT_OK && otherGlyph != nullptr); + glyphAttribute = -1; + Require(sceFontGlyphGetAttribute(otherGlyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0); + Require(sceFontGlyphDefineAttribute(otherGlyph, 9, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0); + glyphAttribute = -1; + Require(sceFontGlyphGetAttribute(otherGlyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 9); + Require(sceFontGlyphGetAttribute(glyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 5); + Require(sceFontDeleteGlyph(&memory, &otherGlyph) == SCE_FONT_OK && otherGlyph == nullptr); Require(sceFontDeleteGlyph(&memory, &glyph) == SCE_FONT_OK && glyph == nullptr); FontKerning kerning{1.0f, 1.0f, 1.0f, 1.0f}; Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -20.0f)); From a605f9473223f8560d3e26fe572951973606097a Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:06:26 +0530 Subject: [PATCH 011/167] fix(savedata): return parameter error on null arguments in setParam --- core/libs/prx/libSceSaveData.native/Export.cpp | 2 +- core/libs/tests/GuestSaveDataMountStatus.cpp | 15 +++++++++++++++ docs/dev/debt/libSceSaveData.native.md | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 03a86db6dd..c10ccf31ce 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -608,7 +608,7 @@ int APS5_VABI sceSaveDataSaveIconByPath(const SaveDataMountPoint* mount_point, c static int setParam(const SaveDataMountPoint* mount_point, uint32_t param_type, const void* param_buf, size_t param_buf_size) { if (mount_point == nullptr || param_buf == nullptr) { - throw std::runtime_error("sceSaveDataSetParam: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); const int slot = find_slot_by_mount_point(mount_point->data); diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index 5e7786a5dd..7d40fb6266 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -14,6 +14,7 @@ int APS5_VABI sceSaveDataMount3(const SaveDataMount3*, SaveDataMountResult*); int APS5_VABI sceSaveDataUmount2(std::uint32_t, const SaveDataMountPoint*); int APS5_VABI sceSaveDataGetMountInfo(const SaveDataMountPoint*, SaveDataMountInfo*); int APS5_VABI sceSaveDataGetParam(const SaveDataMountPoint*, std::uint32_t, void*, std::size_t, std::size_t*); +int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint*, std::uint32_t, const void*, std::size_t); } namespace { @@ -46,6 +47,9 @@ void CheckUnmountedCalls() { Check(sceSaveDataGetParam(nullptr, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null point is PARAMETER"); Check(sceSaveDataGetParam(&point, 1, nullptr, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null buf is PARAMETER"); Check(sceSaveDataGetParam(&point, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorNotMounted, "getParam unmounted point is NOT_MOUNTED"); + Check(sceSaveDataSetParam(nullptr, 1, buf, sizeof(buf)) == SaveDataErrorParameter, "setParam null point is PARAMETER"); + Check(sceSaveDataSetParam(&point, 1, nullptr, sizeof(buf)) == SaveDataErrorParameter, "setParam null buf is PARAMETER"); + Check(sceSaveDataSetParam(&point, 1, buf, sizeof(buf)) == SaveDataErrorNotMounted, "setParam unmounted point is NOT_MOUNTED"); } void CheckMountInfoAndParam(const SaveDataMountPoint* point) { @@ -55,6 +59,8 @@ void CheckMountInfoAndParam(const SaveDataMountPoint* point) { std::size_t gotSize = 0; SaveDataMountPoint unmounted{}; std::memcpy(unmounted.data, "unmounted", sizeof("unmounted")); + const char newTitle[] = "Mounted Title"; + char readTitle[128]{}; Check(sceSaveDataGetMountInfo(nullptr, &info) == SaveDataErrorParameter, "getMountInfo null point is PARAMETER"); Check(sceSaveDataGetMountInfo(point, nullptr) == SaveDataErrorParameter, "getMountInfo null info is PARAMETER"); Check(sceSaveDataGetMountInfo(&unmounted, &info) == SaveDataErrorNotMounted, "getMountInfo unmounted point is NOT_MOUNTED"); @@ -66,6 +72,15 @@ void CheckMountInfoAndParam(const SaveDataMountPoint* point) { Check(sceSaveDataGetParam(point, 1, buf, sizeof(buf), &gotSize) == 0, "getParam on mounted save succeeds"); Check(gotSize == sizeof(buf), "getParam reports expected size"); Check(sceSaveDataGetParam(point, 1, buf, sizeof(buf), nullptr) == 0, "getParam with null got_size on mounted save succeeds"); + Check(sceSaveDataSetParam(nullptr, 1, newTitle, sizeof(newTitle)) == SaveDataErrorParameter, "setParam null point is PARAMETER"); + Check(sceSaveDataSetParam(point, 1, nullptr, sizeof(newTitle)) == SaveDataErrorParameter, "setParam null buf is PARAMETER"); + Check(sceSaveDataSetParam(point, 999, newTitle, sizeof(newTitle)) == SaveDataErrorParameter, "setParam invalid type is PARAMETER"); + Check(sceSaveDataSetParam(point, 1, newTitle, 0) == SaveDataErrorParameter, "setParam zero size is PARAMETER"); + Check(sceSaveDataSetParam(point, 4, smallBuf, 1) == SaveDataErrorParameter, "setParam short user param is PARAMETER"); + Check(sceSaveDataSetParam(&unmounted, 1, newTitle, sizeof(newTitle)) == SaveDataErrorNotMounted, "setParam unmounted point is NOT_MOUNTED"); + Check(sceSaveDataSetParam(point, 1, newTitle, sizeof(newTitle)) == 0, "setParam on mounted save succeeds"); + Check(sceSaveDataGetParam(point, 1, readTitle, sizeof(readTitle), &gotSize) == 0, "getParam reads back set title"); + Check(std::strcmp(readTitle, newTitle) == 0, "readTitle matches newTitle"); } void MountAndCheck(const char* name, std::uint32_t mode, std::uint32_t expectedStatus, const std::string& what) { diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 042e5e2109..380169570f 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -17,5 +17,5 @@ - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for `sceSaveDataPrepare`, its file and `sceSaveDataUmount2` - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated or empty directory name, `.`, `..`, or a name containing `/`, `\` or `:` returns `SAVE_DATA_ERROR_PARAMETER`, which the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-mount3.md#L196-L204) lists for an invalid parameter, and leaves the result as it was, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L652-L658) does. The [SDK](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-dir-name.md#L32-L36) lets an application use only letters, digits, `-`, `.` and `@` in a name, and KytyPS5 also accepts `_` ([`valid_path_component`](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L276-L287)); which other names the console's mount rejects is not checked on hardware -- [sceSaveDataGetMountInfo, sceSaveDataGetParam](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup +- [sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataSetParam](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup From 6f2a61ade037899700062a744ec8d2c658a26488 Mon Sep 17 00:00:00 2001 From: mugurc Date: Sat, 17 Oct 2026 18:01:02 +0300 Subject: [PATCH 012/167] fix(libkernel): recognize listening sockets on macOS hosts macOS does not support getsockopt with SO_ACCEPTCONN; it fails with ENOPROTOOPT. So on macOS the guest's getsockopt of SO_ACCEPTCONN failed, connect did not see that a stream socket was listening (an address of another family failed with EAFNOSUPPORT instead of EOPNOTSUPP), and the read event of a listening socket reported FIONREAD's 0 instead of the waiting connection and did not ignore its low watermark. On macOS the listening state is read from the SO_ACCEPTCONN flag of the socket options that proc_pidfdinfo reports, and the three places that ask go through NativeListening. Other hosts keep getsockopt. --- .../Socket/include/SocketOptions.hpp | 1 + .../libkernel/Socket/src/SocketOptions.cpp | 25 ++++++++++++++++--- core/libs/prx/libkernel/Socket/src/Udp.cpp | 9 +++---- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp index c81d93d46b..cfe594b32e 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp @@ -11,6 +11,7 @@ struct SocketOptionState { std::int64_t receiveTimeout = 0; }; +bool NativeListening(std::uintptr_t native, bool& listening); int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int option, const void* value, std::uint32_t length); int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int type, int option, void* value, std::uint32_t* length); int SetTcpLevelOption(std::uintptr_t native, bool tcp, int option, const void* value, std::uint32_t length); diff --git a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp index 184219224d..6ffe7966eb 100644 --- a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp +++ b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp @@ -7,6 +7,10 @@ #include #include #include +#ifdef __APPLE__ +#include +#include +#endif #endif #include "prx/libkernel/Socket/include/SocketError.hpp" #include "prx/libkernel/Socket/include/SocketOptions.hpp" @@ -159,6 +163,20 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int op return ErrorNoProtocolOption; } +bool NativeListening(std::uintptr_t native, bool& listening) { +#ifdef __APPLE__ + socket_fdinfo info{}; + if (proc_pidfdinfo(getpid(), static_cast(native), PROC_PIDFDSOCKETINFO, &info, sizeof(info)) != static_cast(sizeof(info))) return false; + listening = (info.psi.soi_options & SO_ACCEPTCONN) != 0; +#else + int accepting = 0; + socklen_t size = sizeof(accepting); + if (::getsockopt(static_cast(native), SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&accepting), &size)) return false; + listening = accepting != 0; +#endif + return true; +} + int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int type, int option, void* value, std::uint32_t* length) { const auto handle = static_cast(native); if (option == GuestLinger) { @@ -179,10 +197,9 @@ int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int ty if (::getsockopt(handle, SOL_SOCKET, nativeOption, reinterpret_cast(&enabled), &size)) return HostError(); number = enabled != 0 ? option : 0; } else if (option == GuestAcceptConnection) { - int listening = 0; - socklen_t size = sizeof(listening); - if (::getsockopt(handle, SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&listening), &size)) return HostError(); - number = listening != 0 ? GuestAcceptConnection : 0; + bool listening = false; + if (!NativeListening(native, listening)) return HostError(); + number = listening ? GuestAcceptConnection : 0; } else if (option == GuestType) { number = type; } else if (option == GuestNoSigpipe) { diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 799c7b8b9b..d81e62166e 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -253,9 +253,8 @@ std::uint16_t& PortOf(sockaddr_storage& address) { return address.ss_family == AF_INET ? reinterpret_cast(address).sin_port : reinterpret_cast(address).sin6_port; } bool Listening(const Socket& socket) { - int listening = 0; - socklen_t size = sizeof(listening); - return ::getsockopt(socket.value, SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&listening), &size) == 0 && listening != 0; + bool listening = false; + return GuestSockets::NativeListening(static_cast(socket.value), listening) && listening; } int Disconnect(const Socket& socket) { sockaddr_storage peer{}; @@ -419,9 +418,7 @@ bool GuestSockets::Listening(int descriptor) { if (found == sockets.end()) return false; socket = found->second; } - int accepting = 0; - socklen_t length = sizeof(accepting); - return getsockopt(socket->value, SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&accepting), &length) == 0 && accepting != 0; + return Listening(*socket); } extern "C" bool GuestSocketIsOpen_nid_no_patch(int descriptor) { From 23aa711e7fab0f9687903d9c3e32d1484d62248d Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:45:15 +0530 Subject: [PATCH 013/167] fix(pad): return invalid argument error on null buffer in scePadRead and scePadReadState scePadRead_nid_postfix and scePadReadState threw APS5_INVALID_ARG_EX (std::invalid_argument) when passed a null output buffer or a non-positive buffer count, terminating the guest process across the PRX ABI boundary. Both functions now return PAD_ERROR_INVALID_ARG (-2137915391) on invalid arguments, matching the error return contract of all sibling libScePad APIs. --- core/libs/prx/libScePad/Export.cpp | 4 ++-- core/libs/tests/GuestPad.cpp | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 3dd4c0650a..032f3708df 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -179,13 +179,13 @@ int APS5_VABI scePadOutputReport() { NotImplemented_nid_no_patch(__func__); retu int APS5_VABI scePadRead_nid_postfix(int handle, PadData* data, int num) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; - if (data == nullptr || num <= 0) APS5_INVALID_ARG_EX; + if (data == nullptr || num <= 0) return PAD_ERROR_INVALID_ARG; return Pad::Read(data, num); } int APS5_VABI scePadReadState(int handle, PadData* data) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; - if (data == nullptr) APS5_INVALID_ARG_EX; + if (data == nullptr) return PAD_ERROR_INVALID_ARG; *data = Pad::ReadState(); diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 84b0ad50ab..f4dc717ec9 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -206,6 +206,10 @@ static void CheckReadStateHandle(int handle) { Require(scePadReadState(handle + 1, &data) == PAD_ERROR_INVALID_HANDLE); Require(scePadRead_nid_postfix(0, &data, 1) == PAD_ERROR_INVALID_HANDLE); Require(scePadReadState(handle, &data) == PAD_OK); + Require(scePadRead_nid_postfix(handle, nullptr, 1) == PAD_ERROR_INVALID_ARG); + Require(scePadRead_nid_postfix(handle, &data, 0) == PAD_ERROR_INVALID_ARG); + Require(scePadRead_nid_postfix(handle, &data, -1) == PAD_ERROR_INVALID_ARG); + Require(scePadReadState(handle, nullptr) == PAD_ERROR_INVALID_ARG); } static void CheckRemoteController(int handle) { From e79cf0f754588d2b2b4ad95508c80445a1262ad1 Mon Sep 17 00:00:00 2001 From: Maz Date: Sun, 18 Oct 2026 17:19:02 +0400 Subject: [PATCH 014/167] fix(libkernel): stop O_NOFOLLOW from opening a Windows directory junction lstat reports the junction as a symbolic link, but open with O_NOFOLLOW still opened the target. --- core/libs/prx/libkernel/File/src/Open.cpp | 8 ++++++++ core/libs/tests/GuestFilesystem.cpp | 5 +++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 23fd92fc59..f67625d5ff 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -28,6 +28,7 @@ #include #include #include +#include "prx/libkernel/File/include/WindowsSymbolicLink.hpp" static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint16_t mode) { const int access = nativeFlags & (_O_RDONLY | _O_WRONLY | _O_RDWR); const DWORD desired = access == _O_RDONLY ? GENERIC_READ : access == _O_WRONLY ? GENERIC_WRITE : GENERIC_READ | GENERIC_WRITE; @@ -207,6 +208,13 @@ static int OpenNative(const std::filesystem::path& native, int flags, std::uint1 constexpr int GuestEmlink = 31; std::error_code error; if (std::filesystem::is_symlink(native, error)) return SceErrorFromErrno(GuestEmlink); +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { + std::string junctionTarget; + if (File::WindowsSymbolicLink::ReadJunction(native, junctionTarget, "open")) return SceErrorFromErrno(GuestEmlink); + } +#endif } int fd = NativeOpen(native, MapFlags(flags), mode); #ifdef _WIN32 diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 238ae84299..8903e256db 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -793,6 +793,11 @@ int main() { Require((junctionStatus.st_mode & 0170000) == 0120000 && junctionStatus.st_size > 0); FileStat junctionFollowed{}; Require(stat_nid_postfix(junction.string().c_str(), &junctionFollowed) == 0 && (junctionFollowed.st_mode & 0170000) == 0040000); + constexpr int noFollow = 0x100; + Require(sceKernelOpen(junction.string().c_str(), noFollow, 0) == static_cast(0x8002001Fu)); + Require(open_nid_postfix(junction.string().c_str(), noFollow, 0) == -1 && *__error_nid_postfix() == 31); + const int followedJunction = sceKernelOpen(junction.string().c_str(), 0, 0); + Require(followedJunction >= 0 && sceKernelClose(followedJunction) == 0); Require(std::filesystem::remove(junction) && std::filesystem::remove(junctionTarget)); #endif FileStat identity{}; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index baef0291c7..17230fcb45 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -116,7 +116,7 @@ - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond - [EVFILT_VNODE on macOS](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) registers `EVFILT_VNODE` on a duplicate of the descriptor in a host kqueue, whose notes have FreeBSD's values, and drops the `NOTE_LINK` that XNU adds to `NOTE_DELETE` when the watched file or directory is removed, since FreeBSD's `vop_remove_post` and `vop_rmdir_post` report `NOTE_DELETE` alone. As on Windows, a directory registered with `NOTE_EXTEND` is accepted but never receives it: XNU reports a move between directories as `NOTE_WRITE` alone, with `NOTE_LINK` for a subdirectory - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails -- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction is reported the same way when the name left after that prefix is a drive path (`X:\...`): the print name when that name is a drive path, otherwise the substitute name. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, and any other reparse point throw, as for `readlink` +- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction is reported the same way when the name left after that prefix is a drive path (`X:\...`): the print name when that name is a drive path, otherwise the substitute name. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, and any other reparse point throw, as for `readlink`. `open` with `O_NOFOLLOW` returns `EMLINK` for that junction and does not open the target - [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. With `SIG_IGN` an expiry is dropped. With `SIGALRM` blocked it is left on the pending set of `sigtimedwait`, as `kern_psignal` does, where `sigwait`, `sigtimedwait` and `sigwaitinfo` take it; unblocking it runs a guest handler on the unblocking thread, and with the default disposition exits the process with status 142. Unblocked, the default disposition exits the process with status 142 and a handler throws, since guest `SIGALRM` handlers are never run from the timer thread. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it - On Linux, [direct memory](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified From e9a7b8ea5498c5fc8eff47f42e33914c0ce38cc4 Mon Sep 17 00:00:00 2001 From: iAlturki <120382911+iAlturki@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:27:26 +0300 Subject: [PATCH 015/167] fix(tools): accept UTF-8 BOMs in trace digest inputs --- tools/tests/test_trace_digest.py | 43 ++++++++++++++++++++++++++++++++ tools/trace_digest.py | 8 +++--- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index 5864781302..9734a60332 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -128,6 +128,41 @@ class TraceDigestTests(unittest.TestCase): return trace_digest.digest(trace_digest.load_trace(self.trace), trace_digest.load_summary(None, self.trace), trace_digest.load_stacks(self.stacks), worst=2, top=3) + def json_output(self, trace, *arguments): + result = subprocess.run([sys.executable, trace_digest.__file__, str(trace), "--json", *arguments], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 0, result.stderr) + return json.loads(result.stdout) + + def test_utf8_bom_plain_and_compressed_traces_preserve_the_digest(self): + events = [dict(event, args={"name": "worker \u03a9"}) if event["name"] == "thread_name" and event["tid"] == 1 + else event for event in EVENTS] + text = json.dumps({"traceEvents": events}, ensure_ascii=False) + self.trace.write_bytes(gzip.compress(text.encode("utf-8"))) + expected = self.json_output(self.trace) + for trace in (self.trace, self.trace.with_suffix("")): + for encoding in ("utf-8", "utf-8-sig"): + with self.subTest(trace=trace.name, encoding=encoding): + data = text.encode(encoding) + trace.write_bytes(gzip.compress(data) if trace.suffix == ".gz" else data) + self.assertEqual(self.json_output(trace), expected) + + def test_utf8_bom_summary_preserves_the_digest(self): + expected = self.json_output(self.trace) + summary = self.trace.with_name("aps5-summary.jsonl") + summary.write_text(summary.read_text(encoding="utf-8"), encoding="utf-8-sig") + self.assertEqual(self.json_output(self.trace), expected) + + def test_utf8_bom_stacks_preserve_comments_and_frame_names(self): + for text in (STACKS, "root;module\u03a9+0x1 2\n"): + with self.subTest(comments=text.startswith("#")): + self.stacks.write_text(text, encoding="utf-8") + expected = self.json_output(self.trace, "--stacks", str(self.stacks)) + expected_stacks = trace_digest.load_stacks(self.stacks) + self.stacks.write_text(text, encoding="utf-8-sig") + self.assertEqual(self.json_output(self.trace, "--stacks", str(self.stacks)), expected) + self.assertEqual(trace_digest.load_stacks(self.stacks), expected_stacks) + def test_summary_is_the_line_of_this_trace(self): self.assertEqual(self.result()["summary"]["trace"], "aps5-trace-abc-7.json.gz") @@ -464,6 +499,14 @@ class IntervalDigestTests(unittest.TestCase): def test_window_lines_are_skipped(self): self.assertEqual(len(trace_digest.load_intervals(self.path)), 4) + def test_utf8_bom_intervals_preserve_the_command_line_digest(self): + expected = trace_digest.digest_intervals(trace_digest.load_intervals(self.path)) + self.path.write_text(self.path.read_text(encoding="utf-8"), encoding="utf-8-sig") + result = subprocess.run([sys.executable, trace_digest.__file__, str(self.path), "--json"], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(result.stdout), expected) + def test_rows(self): result = trace_digest.digest_intervals(trace_digest.load_intervals(self.path)) first, second, third, _ = result["intervals"] diff --git a/tools/trace_digest.py b/tools/trace_digest.py index 08504ca43a..96885639fc 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -72,7 +72,7 @@ class Trace: def load_trace(path): opener = gzip.open if str(path).endswith(".gz") else open - with opener(path, "rt", encoding="utf-8") as handle: + with opener(path, "rt", encoding="utf-8-sig") as handle: return Trace(json.load(handle)) @@ -80,7 +80,7 @@ def load_summary(path, trace_path): candidate = Path(path) if path else Path(trace_path).with_name("aps5-summary.jsonl") if not candidate.exists(): return None - lines = [json.loads(line) for line in candidate.read_text(encoding="utf-8").splitlines() if line.strip()] + lines = [json.loads(line) for line in candidate.read_text(encoding="utf-8-sig").splitlines() if line.strip()] name = Path(trace_path).name alternate = name[:-3] if name.endswith(".gz") else name + ".gz" for filename in (name, alternate): @@ -92,7 +92,7 @@ def load_summary(path, trace_path): def load_stacks(path): stacks = [] - for number, line in enumerate(Path(path).read_text(encoding="utf-8").splitlines(), 1): + for number, line in enumerate(Path(path).read_text(encoding="utf-8-sig").splitlines(), 1): if line.startswith("#") or not line.strip(): continue frames, _, count = line.rpartition(" ") @@ -264,7 +264,7 @@ def markdown(result): def load_intervals(path): - lines = [json.loads(line) for line in Path(path).read_text(encoding="utf-8").splitlines() if line.strip()] + lines = [json.loads(line) for line in Path(path).read_text(encoding="utf-8-sig").splitlines() if line.strip()] return [line for line in lines if "interval_start_s" in line] From 0029bddb55435741bfca8045e5e0f8ed357c44ea Mon Sep 17 00:00:00 2001 From: slopggobblercobbler81 <336610228+slopggobblercobbler81@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:15:57 +0200 Subject: [PATCH 016/167] fix(libSceAppContent): return NOT_MOUNTED for invalid download mount point Return SCE_APP_CONTENT_ERROR_NOT_MOUNTED instead of throwing APS5_INVALID_ARG_EX when a mount point other than /download0 is passed to sceAppContentDownloadDataFormat or sceAppContentDownloadDataGetAvailableSpaceKb, aligning with sceAppContentTemporaryDataFormat. Check null mount_point in sceAppContentDownloadDataGetAvailableSpaceKb with SCE_APP_CONTENT_ERROR_PARAMETER. --- core/libs/prx/libSceAppContent/Export.cpp | 6 +++--- core/libs/tests/GuestAppContent.cpp | 4 ++++ docs/dev/debt/libSceAppContent.md | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index be7cad24d9..b65bbb9a00 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -95,7 +95,7 @@ int APS5_VABI sceAppContentAppParamGetInt(uint32_t param_id, int32_t* value) { int APS5_VABI sceAppContentDownloadDataFormat(const AppContentMountPoint* mount_point) { if (!mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER; - if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; + if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED; const auto directory = ResolvePath_nid_no_patch(DOWNLOAD_MOUNT_POINT); if (std::filesystem::create_directories(directory)) RecordWrittenPath_nid_no_patch(directory); ClearDirectory(directory); @@ -103,8 +103,8 @@ int APS5_VABI sceAppContentDownloadDataFormat(const AppContentMountPoint* mount_ } int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMountPoint* mount_point, size_t* available_space_kb) { - if (!available_space_kb) return SCE_APP_CONTENT_ERROR_PARAMETER; - if (!mount_point || std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; + if (!mount_point || !available_space_kb) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED; const std::uint64_t quotaKb = GetAppDownloadDataSizeMiB_nid_postfix() * 1024u; if (quotaKb == 0) { *available_space_kb = 0; diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index fef723221e..6d3745cd70 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -51,6 +51,9 @@ int main() { Require(sceAppContentDownloadDataGetAvailableSpaceKb(&download, &availableKb) == 0); Require(availableKb == 0); Require(sceAppContentDownloadDataGetAvailableSpaceKb(&download, nullptr) == ErrorParameter); + Require(sceAppContentDownloadDataGetAvailableSpaceKb(nullptr, &availableKb) == ErrorParameter); + AppContentMountPoint invalidMount{}; + Require(sceAppContentDownloadDataGetAvailableSpaceKb(&invalidMount, &availableKb) == ErrorNotMounted); const auto downloadData = ResolvePath_nid_no_patch("/download0"); Require(sceAppContentDownloadDataFormat(&download) == 0); Require(std::filesystem::is_directory(downloadData) && std::filesystem::is_empty(downloadData)); @@ -58,6 +61,7 @@ int main() { { std::ofstream file(downloadData / "cache" / "old.bin"); file << "downloaded"; } { std::ofstream file(downloadData / "index.dat"); file << "downloaded"; } Require(sceAppContentDownloadDataFormat(nullptr) == ErrorParameter); + Require(sceAppContentDownloadDataFormat(&invalidMount) == ErrorNotMounted); Require(std::filesystem::exists(downloadData / "index.dat")); Require(sceAppContentDownloadDataFormat(&download) == 0); Require(std::filesystem::is_directory(downloadData) && std::filesystem::is_empty(downloadData)); diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 5639476d66..bf943bde0e 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -3,7 +3,7 @@ ### Unknown function info - [sceAppContentDownloadDataGetAvailableSpaceKb](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - a title whose `param.json` declares `downloadDataSize` 0 gets 0 KB and OK; the console's behaviour for a title without a download data area is unverified (it may return an error instead). KytyPS5 does not special-case it, and shadPS4 stubs 1048576 KB -- [sceAppContentDownloadDataFormat](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - empties the `/download0` directory, creating it if needed. Imported by PPSA01317. A null mount point returns `PARAMETER` as `sceAppContentTemporaryDataFormat` does, and another mount point throws as in `sceAppContentDownloadDataGetAvailableSpaceKb`; the console's error codes for both and its behaviour with files still open are unverified +- [sceAppContentDownloadDataFormat](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - empties the `/download0` directory, creating it if needed. Imported by PPSA01317. A null mount point returns `PARAMETER` and another mount point returns `NOT_MOUNTED` as in `sceAppContentTemporaryDataFormat` and `sceAppContentDownloadDataGetAvailableSpaceKb`; the console's error codes for both and its behaviour with files still open are unverified ### Functional From 9895d130c4bbd83d9a4677f1534e8b2db9f46e62 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Sat, 17 Oct 2026 13:11:12 -0300 Subject: [PATCH 017/167] fix(libSceAgc): accept register range values at any byte address, as the console does --- .../libs/prx/libSceAgc/Command/src/Packet.cpp | 6 ++-- core/libs/prx/libSceAgc/tests/Commands.cpp | 10 ++++-- .../prx/libSceAgc/tests/RegisterCommands.cpp | 33 +++++++++++++++++-- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgc/Command/src/Packet.cpp b/core/libs/prx/libSceAgc/Command/src/Packet.cpp index 743fed53f2..6ec0e12165 100644 --- a/core/libs/prx/libSceAgc/Command/src/Packet.cpp +++ b/core/libs/prx/libSceAgc/Command/src/Packet.cpp @@ -1,7 +1,9 @@ #include "prx/libSceAgc/Command/include/Packet.hpp" #include +#include #include +#include #include #include #include @@ -96,8 +98,8 @@ std::uint32_t* WriteRegisterRange(CommandBuffer* buffer, std::uint32_t opcode, s Require(count != 0 && count <= 0x3fffu && offset <= 0xffffu && count <= 0x10000u - offset, function, "invalid register range"); std::vector snapshot; if (values != nullptr) { - CheckAddress(reinterpret_cast(values), 4, function); - snapshot.assign(values, values + count); + snapshot.resize(count); + std::memcpy(snapshot.data(), values, static_cast(count) * sizeof(std::uint32_t)); } auto* packet = Allocate(buffer, count + 2u, function); packet[0] = Header(opcode, count + 2u); diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 1f86645fe8..ba4e85f5b9 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -462,8 +462,14 @@ void testRegisterRange() { check(packet == storage.words.data() + 6 && storage.buffer.cursor_up == storage.words.data() + 12, "incorrect populated register range allocation"); check(storage.words == expected, "register values were not copied correctly"); const auto* misaligned = reinterpret_cast(reinterpret_cast(values.data()) + 1); - expectFailure([&] { sceAgcCbSetShRegisterRangeDirect(&storage.buffer, 0x8c, misaligned, 4); }); - check(storage.words == expected && storage.buffer.cursor_up == storage.words.data() + 12, "misaligned register values modified command buffer"); + packet = sceAgcCbSetShRegisterRangeDirect(&storage.buffer, 0x8c, misaligned, 3); + expected[12] = 0xc0037600u; + expected[13] = 0x8c; + expected[14] = 0x02000000u; + expected[15] = 0x03000000u; + expected[16] = 0x04000000u; + check(packet == storage.words.data() + 12 && storage.buffer.cursor_up == storage.words.data() + 17, "incorrect misaligned register range allocation"); + check(storage.words == expected, "misaligned register values were not read byte for byte"); } void testPacketPayloadAddress() { diff --git a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp index a1e0295daf..5e6be0c709 100644 --- a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp +++ b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp @@ -8,6 +8,8 @@ extern "C" { std::uint32_t* APS5_VABI sceAgcDcbSetCfRegisterDirect(CommandBuffer*, ShaderRegister); std::uint32_t* APS5_VABI sceAgcDcbSetCfRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t); +std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t); +std::uint32_t* APS5_VABI sceAgcCbSetUcRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t); std::uint32_t* APS5_VABI sceAgcDcbSetCxRegisterDirect(CommandBuffer*, ShaderRegister); std::uint32_t* APS5_VABI sceAgcDcbSetShRegisterDirect(CommandBuffer*, ShaderRegister); std::uint32_t* APS5_VABI sceAgcDcbSetUcRegisterDirect(CommandBuffer*, ShaderRegister); @@ -92,11 +94,37 @@ void testConfig() { expectFailure([&] { sceAgcDcbSetCfRegisterDirect(nullptr, {0, 1}); }); expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0x100u, values.data(), 0); }); expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0xffffu, values.data(), 2); }); - const auto* misaligned = reinterpret_cast(reinterpret_cast(values.data()) + 1); - expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0x100u, misaligned, 1); }); check(storage.words == before && storage.buffer.cursor_up == reserved + 4, "invalid config register write modified buffer"); } +void testRangeFromAnyByte() { + const std::array writers{sceAgcCbSetShRegisterRangeDirect, sceAgcCbSetUcRegisterRangeDirect, sceAgcDcbSetCfRegisterRangeDirect}; + const std::array headers{0xc0047600u, 0xc0047900u, 0xc0046800u}; + alignas(16) std::array bytes{}; + for (std::size_t i = 0; i < bytes.size(); ++i) bytes[i] = static_cast(0x10u + i); + struct Case { + std::size_t offset; + std::array values; + }; + const std::array cases{ + Case{1, {0x14131211u, 0x18171615u, 0x1c1b1a19u, 0x201f1e1du}}, + Case{2, {0x15141312u, 0x19181716u, 0x1d1c1b1au, 0x21201f1eu}}, + Case{3, {0x16151413u, 0x1a191817u, 0x1e1d1c1bu, 0x2221201fu}}, + Case{13, {0x201f1e1du, 0x24232221u, 0x28272625u, 0x2c2b2a29u}}, + }; + for (std::size_t i = 0; i < writers.size(); ++i) { + for (const auto& entry : cases) { + Storage storage; + storage.words.fill(0xabcdef01u); + const auto* values = reinterpret_cast(bytes.data() + entry.offset); + auto* packet = writers[i](&storage.buffer, 0x40u, values, 4); + check(packet == storage.words.data() && storage.buffer.cursor_up == packet + 6, "unaligned register range allocation mismatch"); + check(packet[0] == headers[i] && packet[1] == 0x40u && std::equal(entry.values.begin(), entry.values.end(), packet + 2), "unaligned register values were not read byte for byte"); + check(packet[6] == 0xabcdef01u, "unaligned register range overwrote the following word"); + } + } +} + void testIndirect() { const std::array writers{sceAgcDcbSetCxRegistersIndirect, sceAgcDcbSetShRegistersIndirect, sceAgcDcbSetUcRegistersIndirect}; const std::array sizes{sceAgcDcbSetCxRegistersIndirectGetSize, sceAgcDcbSetShRegistersIndirectGetSize, sceAgcDcbSetUcRegistersIndirectGetSize}; @@ -210,6 +238,7 @@ int main() { try { testDirect(); testConfig(); + testRangeFromAnyByte(); testIndirect(); testIndirectPlaceholder(); testDirectList(); From dd4f79332f2539c604dd6b4cabd5538366511eab Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:00:43 -0300 Subject: [PATCH 018/167] fix(tools): run the oracle comparison test cases in process hardware_oracle_compare started compare_results.py in a new Python process for every case, 77 interpreter starts. That takes 5-16 s on the macOS relinker runners against the test's 20 s timeout, and the x86_64 job timed out at 20.14 s in run 39103831688 on a pull request that does not touch the oracle tools. Call compare_results.main() in the test process with sys.argv patched and stdout and stderr captured, and keep the identical and differing cases as real subprocesses so the exit status of the script entry point stays covered. The test drops to under 1 s. --- tools/hw-oracle/test_compare_results.py | 26 ++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/tools/hw-oracle/test_compare_results.py b/tools/hw-oracle/test_compare_results.py index c2f18c7f6c..4197e8ff40 100644 --- a/tools/hw-oracle/test_compare_results.py +++ b/tools/hw-oracle/test_compare_results.py @@ -10,6 +10,7 @@ import unittest from pathlib import Path from unittest.mock import patch +import compare_results import hw_oracle @@ -41,16 +42,28 @@ class CompareResultsTests(unittest.TestCase): ], } - def cli(self, before=None, after=None): + def cli(self, before=None, after=None, process=False): self.before.write_text(json.dumps(self.report if before is None else before), encoding="utf-8") self.after.write_text(json.dumps(self.report if after is None else after), encoding="utf-8") - return subprocess.run([sys.executable, str(SCRIPT), str(self.before), str(self.after)], - capture_output=True, text=True, timeout=10) + if process: + return subprocess.run([sys.executable, str(SCRIPT), str(self.before), str(self.after)], + capture_output=True, text=True, timeout=10) + return self.compare() + + def compare(self): + argv = [str(SCRIPT), str(self.before), str(self.after)] + stdout, stderr = io.StringIO(), io.StringIO() + with patch("sys.argv", argv), contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): + try: + returncode = compare_results.main() + except SystemExit as exit: + returncode = exit.code + return subprocess.CompletedProcess(argv, returncode, stdout.getvalue(), stderr.getvalue()) def test_identical_outputs_across_assembly_targets(self): after = copy.deepcopy(self.report) after["assembly_target"] = "gfx1100" - result = self.cli(after=after) + result = self.cli(after=after, process=True) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("gfx1030 -> gfx1100", result.stdout) self.assertIn("2 rows x 2 registers: 0 differing words", result.stdout) @@ -60,7 +73,7 @@ class CompareResultsTests(unittest.TestCase): after = copy.deepcopy(self.report) after["rows"][0]["output"][0] = 0x80000000 after["rows"][1]["output"] = [0x7fc00002, 0] - result = self.cli(after=after) + result = self.cli(after=after, process=True) self.assertEqual(result.returncode, 1, result.stderr) self.assertIn("3 differing words", result.stdout) self.assertIn("row 1, v10: 0x00000000 -> 0x80000000", result.stdout) @@ -147,8 +160,7 @@ class CompareResultsTests(unittest.TestCase): with self.subTest(content=content): if content is not None: self.after.write_bytes(content) - result = subprocess.run([sys.executable, str(SCRIPT), str(self.before), str(self.after)], - capture_output=True, text=True, timeout=10) + result = self.compare() self.assertEqual(result.returncode, 2) self.assertNotIn("Traceback", result.stderr) self.assertEqual(result.stdout, "") From e5b4bd72dde2ea97670259961be935b56a54f1a4 Mon Sep 17 00:00:00 2001 From: HQasper <52220032+HQasper@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:15:06 +0200 Subject: [PATCH 019/167] fix(recompiler): write a coherent dwordx2 as one 64-bit atomic store A glc or dlc buffer_store_dwordx2 is one 64-bit write on the GPU, so a workgroup that polls a {flag, value} pair never sees the flag without its value. It was emitted as two dword stores, and the coherent dwordx2 load was one 64-bit read only in programs with 64-bit buffer atomics, the only ones that had the u64 storage-buffer view. GTA V builds each BLAS with a single-pass decoupled look-back scan that publishes its tiles' pairs with buffer_store_dwordx2 and has no 64-bit atomics. A tile read an earlier tile's new flag with its old value 0, the scan came out short, the BVH got overlapping nodes and the refit kernel looped until the device was lost. A program with a coherent raw two-dword buffer load or store now has the u64 view whenever the device has shaderBufferInt64Atomics. Such a store whose dwords are in bounds, adjacent and 8-byte aligned is one device-scope OpAtomicStore. The load takes the same condition, which now also checks the second dword's bounds and that the dwords are adjacent. All other accesses keep the per-dword path. LookBackScan also runs its kernel with the two buffer_atomic_swap_x2 replaced by glc buffer_store_dwordx2. Without this change that variant fails its first whole scan. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/LookBackScan.cpp | 39 +++++++++---- .../include/SpirvBackend/SpirvAnalysis.hpp | 2 + .../SpirvBackend/src/SpirvAnalysis.cpp | 2 + .../SpirvBackend/src/SpirvEmitter.cpp | 2 + .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 2 +- .../src/SpirvMemory/SpirvModuleSetup.cpp | 2 + .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 55 +++++++++++++++---- 7 files changed, 80 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp index 210d8e681d..8ec6a58bd2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp @@ -24,6 +24,13 @@ alignas(256) constexpr std::array ScanCode{ 0xe0701000, 0x80030802, 0xbf810000, }; +constexpr auto StoreScanCode = [] { + auto code = ScanCode; + code[16] = 0xe0745000; + code[38] = 0xe0745000; + return code; +}(); + constexpr std::uint32_t Lanes = 64; constexpr std::uint32_t Blocks = 4096; @@ -41,9 +48,17 @@ std::array BufferDescriptor(const void* data, std::uint32_t by return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; } -void Run(AgcDriver::VulkanDevice& device, std::uint32_t groups) { - alignas(256) static std::array code{}; - code = ScanCode; +struct Variant { + const char* name; + const std::array& words; +}; + +constexpr std::array Variants{{{"buffer_atomic_swap_x2", ScanCode}, {"buffer_store_dwordx2", StoreScanCode}}}; + +void Run(AgcDriver::VulkanDevice& device, std::size_t variant, std::uint32_t groups) { + alignas(256) static std::array, Variants.size()> programs{}; + auto& code = programs[variant]; + code = Variants[variant].words; std::vector userData; for (const auto& descriptor : {BufferDescriptor(Counter.data(), sizeof(Counter)), BufferDescriptor(Aggregates.data(), sizeof(Aggregates)), BufferDescriptor(Pairs.data(), sizeof(Pairs)), BufferDescriptor(Prefixes.data(), sizeof(Prefixes))}) { userData.insert(userData.end(), descriptor.begin(), descriptor.end()); @@ -64,8 +79,8 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t groups) { device.WaitIdle(); } -void CheckLookBack(AgcDriver::VulkanDevice& device, std::uint32_t block) { - const auto name = "look-back from block " + std::to_string(block); +void CheckLookBack(AgcDriver::VulkanDevice& device, std::size_t variant, std::uint32_t block) { + const auto name = std::string(Variants[variant].name) + " look-back from block " + std::to_string(block); Pairs.fill(0u); std::uint32_t prefix = 0; for (std::uint32_t b = 0; b < Blocks; ++b) Aggregates[b] = Value(b, block); @@ -77,20 +92,20 @@ void CheckLookBack(AgcDriver::VulkanDevice& device, std::uint32_t block) { Counter.fill(0u); Counter[0] = block; Prefixes.fill(0xdeadbeefu); - Run(device, 1); + Run(device, variant, 1); Require(Counter[0] == block + 1, name + ": the counter is " + std::to_string(Counter[0])); Require(Prefixes[block] == prefix, name + ": its prefix is " + std::to_string(Prefixes[block]) + ", expected " + std::to_string(prefix)); Require(Pairs[2 * block] == 2u, name + ": its flag is " + std::to_string(Pairs[2 * block]) + ", expected 2"); Require(Pairs[2 * block + 1] == prefix + Aggregates[block], name + ": it published " + std::to_string(Pairs[2 * block + 1]) + ", expected " + std::to_string(prefix + Aggregates[block])); } -void CheckWhole(AgcDriver::VulkanDevice& device, std::uint32_t seed) { - const auto name = "whole scan " + std::to_string(seed); +void CheckWhole(AgcDriver::VulkanDevice& device, std::size_t variant, std::uint32_t seed) { + const auto name = std::string(Variants[variant].name) + " whole scan " + std::to_string(seed); for (std::uint32_t b = 0; b < Blocks; ++b) Aggregates[b] = Value(b, seed); Counter.fill(0u); Pairs.fill(0u); Prefixes.fill(0xdeadbeefu); - Run(device, Blocks); + Run(device, variant, Blocks); std::uint32_t running = 0; for (std::uint32_t b = 0; b < Blocks; ++b) { Require(Prefixes[b] == running, name + ": block " + std::to_string(b) + "'s prefix is " + std::to_string(Prefixes[b]) + ", expected " + std::to_string(running)); @@ -113,8 +128,10 @@ int main() { std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); return VulkanTestSkipped; } - for (const std::uint32_t block : {0u, 1u, 2u, 63u, 64u, 1000u, Blocks - 1}) CheckLookBack(*device, block); - for (std::uint32_t seed = 0; seed < 50; ++seed) CheckWhole(*device, seed); + for (std::size_t variant = 0; variant < Variants.size(); ++variant) { + for (const std::uint32_t block : {0u, 1u, 2u, 63u, 64u, 1000u, Blocks - 1}) CheckLookBack(*device, variant, block); + for (std::uint32_t seed = 0; seed < 50; ++seed) CheckWhole(*device, variant, seed); + } std::puts("look-back scan tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp index ebfd8888e3..52ac6c6940 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp @@ -25,6 +25,8 @@ struct SpirvRequirements { bool functionScratch = false; bool pixelValidMask = false; bool bufferInt64Atomics = false; + bool coherentBufferPairs = false; + bool bufferU64View = false; bool imageInt64Atomics = false; bool sharedInt64Atomics = false; bool float64 = false; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 60295387c7..89178ce940 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -373,6 +373,8 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) { } const auto& memory = program.Resources().memoryInfo.at(memoryIndex); requirements.coherentBuffers = requirements.coherentBuffers || (memory.coherent && !memory.gpuDescriptor); + const bool pair = inst->Opcode() == IrOpcode::LoadBufferU32x2 || inst->Opcode() == IrOpcode::StoreBufferU32x2; + requirements.coherentBufferPairs = requirements.coherentBufferPairs || (pair && memory.coherent && !memory.gpuDescriptor); if (!memory.planningOnly) { requirements.subgroupLocalInvocationId = requirements.subgroupLocalInvocationId || program.Resources().stage == IrShaderStage::Compute; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 050316a6f7..0018960a3f 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -266,6 +266,8 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.requirements.ldsLock = false; state.requirements.ldsBufferInt64Atomics = true; } + const bool int64Atomics = std::ranges::find(state.supportedCapabilities, static_cast(spv::CapabilityInt64Atomics)) != state.supportedCapabilities.end(); + state.requirements.bufferU64View = state.requirements.bufferInt64Atomics || (state.requirements.coherentBufferPairs && int64Atomics); if (HostSubgroupSingleLane(state)) { state.requirements.subgroupBallot = false; state.requirements.subgroupShuffle = false; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 7a704ea52d..d4b27836aa 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -440,7 +440,7 @@ void DefineDescriptors(SpirvEmitterState& state) { case DescriptorBindingKind::Buffers: state.storageBufferVariable = Define(ArrayType(StorageBufferBlockType(state)), "buffers"); if (state.requirements.coherentBufferAlias) state.storageBufferCoherentVariable = Define(ArrayType(StorageBufferBlockType(state)), "buffers_coherent"); - if (state.requirements.bufferInt64Atomics) state.storageBufferU64Variable = Define(ArrayType(StorageBufferU64BlockType(state)), "buffers_u64"); + if (state.requirements.bufferU64View) state.storageBufferU64Variable = Define(ArrayType(StorageBufferU64BlockType(state)), "buffers_u64"); if (state.storageBufferU64Variable != 0u || state.storageBufferCoherentVariable != 0u) { for (const auto variable : {state.storageBufferVariable, state.storageBufferCoherentVariable, state.storageBufferU64Variable}) { if (variable != 0u) state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationAliased); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index ef3e1ac42d..c419a928c6 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -259,6 +259,8 @@ void DefineModule(SpirvEmitterState& state) { if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityInt64Atomics)) == state.supportedCapabilities.end()) { FailEmit("64-bit buffer atomics need shaderBufferInt64Atomics"); } + } + if (state.requirements.bufferU64View) { state.module.EmitCapability(spv::CapabilityInt64); state.module.EmitCapability(spv::CapabilityInt64Atomics); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 7820a803c3..c127beb77f 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -723,6 +723,27 @@ std::uint32_t formattedGpuCall(SpirvValueEmitContext& context, const IrValue& in return value; } +struct CoherentPair { + MemoryResourceAccess wide; + std::uint32_t index; + std::uint32_t usable; +}; + +std::optional coherentPair(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t components) { + auto& state = context.state; + const auto& memory = context.Memory(instruction); + if (components != 2u || !memory.coherent || memory.gpuDescriptor || memory.dataBits != 32u || state.storageBufferU64Variable == 0u) return std::nullopt; + const auto low = address(context, instruction, resource, 0u, 4u); + const auto high = address(context, instruction, resource, 4u, 4u); + const auto wide = directAccess(context, instruction, true); + const auto byte = EmitAddU32(state, low.guest, wide.byteOffset); + const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byte, ConstantU32(state, 3u)); + const auto aligned = both(state, equal(state, baseMisalignment(context, instruction), ConstantU32(state, 0u)), equal(state, EmitAndConstant(state, byte, 7u), ConstantU32(state, 0u))); + const auto adjacent = equal(state, high.guest, EmitAddU32(state, low.guest, ConstantU32(state, 4u))); + const auto inBounds = both(state, both(state, low.inBounds, high.inBounds), EmitMemoryElementInBounds(state, wide, index)); + return CoherentPair{wide, index, both(state, both(state, aligned, adjacent), inBounds)}; +} + } std::uint32_t EmitRuntimeBufferLoad(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t components) { @@ -742,15 +763,10 @@ std::uint32_t EmitRuntimeBufferLoad(SpirvValueEmitContext& context, const IrValu } return composite(state, components, values); }; - if (components != 2u || !memory.coherent || memory.gpuDescriptor || state.storageBufferU64Variable == 0u) return readComponents(); - const auto runtime = address(context, instruction, resource, 0u, 8u); - const auto wide = directAccess(context, instruction, true); - const auto byte = EmitAddU32(state, runtime.guest, wide.byteOffset); - const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byte, ConstantU32(state, 3u)); - const auto aligned = both(state, equal(state, baseMisalignment(context, instruction), ConstantU32(state, 0u)), equal(state, EmitAndConstant(state, byte, 7u), ConstantU32(state, 0u))); - const auto inBounds = both(state, runtime.inBounds, EmitMemoryElementInBounds(state, wide, index)); - return EmitValueIfElse(state, both(state, aligned, inBounds), type, [&] { - const auto pointer = EmitStorageBufferElementPointer(state, wide, index, TypeStorageBufferU64ElementPointer(state)); + const auto pair = coherentPair(context, instruction, resource, components); + if (!pair) return readComponents(); + return EmitValueIfElse(state, pair->usable, type, [&] { + const auto pointer = EmitStorageBufferElementPointer(state, pair->wide, pair->index, TypeStorageBufferU64ElementPointer(state)); const auto value = state.module.AllocateId(); state.module.AddFunction(spv::OpAtomicLoad, TypeScalarU64(state), value, pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsMaskNone)); const auto high = Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), value, BdaConstant(state, 32u)); @@ -769,10 +785,25 @@ void EmitRuntimeBufferStore(SpirvValueEmitContext& context, const IrValue& instr return; } const auto values = storeValues(state, storeData(context, instruction), components); - for (std::uint32_t component = 0; component < components; ++component) { - const auto access = address(context, instruction, resource, component * 4u, memory.dataBits / 8u); - EmitIfCondition(state, access.inBounds, [&] { writeBuffer(context, instruction, access.guest, values[component], memory.dataBits); }); + const auto writeComponents = [&] { + for (std::uint32_t component = 0; component < components; ++component) { + const auto access = address(context, instruction, resource, component * 4u, memory.dataBits / 8u); + EmitIfCondition(state, access.inBounds, [&] { writeBuffer(context, instruction, access.guest, values[component], memory.dataBits); }); + } + }; + const auto pair = coherentPair(context, instruction, resource, components); + if (!pair) { + writeComponents(); + return; } + EmitIfCondition(state, pair->usable, [&] { + const auto u64 = TypeScalarU64(state); + const auto high = Binary(state, spv::OpShiftLeftLogical, u64, Unary(state, spv::OpUConvert, u64, values[1]), BdaConstant(state, 32u)); + const auto value = Binary(state, spv::OpBitwiseOr, u64, Unary(state, spv::OpUConvert, u64, values[0]), high); + const auto pointer = EmitStorageBufferElementPointer(state, pair->wide, pair->index, TypeStorageBufferU64ElementPointer(state)); + state.module.AddFunction(spv::OpAtomicStore, pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsMaskNone), value); + }); + EmitIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), pair->usable), writeComponents); }); } From 1f9ac40808daa04c7f8576c1c92cf2a48811a56d Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 18 Oct 2026 16:54:31 +0000 Subject: [PATCH 020/167] fix(shader): print the shader hash of a SPIR-V emission failure in hex The SPIR-V emission failures printed the shader hash with std::to_string after "hash=0x", so the hash read as a hex number that was the decimal one. Shader 0x1173ae1000 was reported as hash=0x74955231232. Both now print it in hex, as the driver's own messages print the shader address. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 13 ++++ .../tests/SpirvFailureText.cpp | 75 +++++++++++++++++++ .../SpirvBackend/src/SpirvEmitter.cpp | 11 ++- 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/SpirvFailureText.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 5480263227..99413270d6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -741,6 +741,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_spirv_failure_text_tests EXCLUDE_FROM_ALL tests/SpirvFailureText.cpp) +target_link_libraries(agc_spirv_failure_text_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_spirv_failure_text_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_spirv_failure_text_tests) +if(BUILD_TESTING) + set_target_properties(agc_spirv_failure_text_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_spirv_failure_text COMMAND agc_spirv_failure_text_tests) + set_tests_properties(agc_spirv_failure_text PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_spirv_failure_text PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_spirv_lds_atomic_lock_tests EXCLUDE_FROM_ALL tests/SpirvLdsAtomicLock.cpp) target_link_libraries(agc_spirv_lds_atomic_lock_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_spirv_lds_atomic_lock_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvFailureText.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvFailureText.cpp new file mode 100644 index 0000000000..10559775ea --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvFailureText.cpp @@ -0,0 +1,75 @@ +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/ShaderStageInputInfo.hpp" +#include "SpirvBackend/SpirvEmitter.hpp" +#include "PipelineSpecialization.hpp" +#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); +} + +enum class Failure { MissingComputeInput, UnloweredInstruction }; + +std::string emissionFailure(Failure failure) { + IrProgram program; + program.SetWaveSize(32); + program.Resources().stage = IrShaderStage::Compute; + program.Resources().shaderHash = 0x1173ae1000ull; + program.Resources().srtPlanComplete = true; + program.Resources().resourceTrackingComplete = true; + program.Metadata().shaderInfoComplete = true; + program.Metadata().bindingLayoutComplete = true; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + program.Metadata().blockInfo.emplace_back(); + if (failure == Failure::UnloweredInstruction) block.AppendInstruction(&program.CreateValue(IrOpcode::Unreachable, IrType::Void)); + ShaderComputeInputInfo compute; + compute.threadsNum[0] = 32; + compute.threadsNum[1] = 1; + compute.threadsNum[2] = 1; + compute.hostSubgroupSize = 32; + compute.waveSize = 32; + ShaderStageInputInfo inputs; + if (failure != Failure::MissingComputeInput) inputs.compute = &compute; + CompiledBindingLayout bindings; + bindings.layout = program.Metadata().bindings; + SpirvTargetOptions target{}; + target.vulkanVersion = 0x00401000; + target.spirvVersion = 0x00010300; + target.subgroupSize = 32; + try { + static_cast(SpirvEmitter().Emit(program, inputs, bindings, target)); + } catch (const std::runtime_error& error) { + return error.what(); + } + throw std::runtime_error("the program emitted without a failure"); +} + +void requireFailure(Failure failure, std::string_view prefix, std::string_view reason) { + const auto text = emissionFailure(failure); + require(text.starts_with(prefix) && text.find(reason) != std::string::npos, "expected \"" + std::string(prefix) + "...\" with \"" + std::string(reason) + "\", got \"" + text + "\""); +} + +} + +int main() { + try { + requireFailure(Failure::MissingComputeInput, "SPIR-V emission failed: hash=0x1173ae1000 stage=", " reason=compute input info is missing"); + requireFailure(Failure::UnloweredInstruction, "SPIR-V emission failed: hash=0x1173ae1000 stage=", " opcode=Unreachable reason="); + } catch (const std::exception& error) { + std::fprintf(stderr, "SpirvFailureText: %s\n", error.what()); + return 1; + } + std::puts("SpirvFailureText: emission failures name the shader hash in hex"); + return 0; +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 050316a6f7..1f1f6d76b8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -18,8 +19,14 @@ namespace ShaderRecompiler { namespace { +std::string FailureText(const IrProgram& program) { + char hash[24]; + std::snprintf(hash, sizeof(hash), "%llx", static_cast(program.Resources().shaderHash)); + return std::string("SPIR-V emission failed: hash=0x") + hash + " stage=" + std::to_string(static_cast(program.Resources().stage)); +} + [[noreturn]] void FailProgram(const IrProgram& program, const char* reason) { - throw std::runtime_error("SPIR-V emission failed: hash=0x" + std::to_string(program.Resources().shaderHash) + " stage=" + std::to_string(static_cast(program.Resources().stage)) + " reason=" + reason); + throw std::runtime_error(FailureText(program) + " reason=" + reason); } const ShaderWorkgroupInputInfo* ShaderWorkgroupInputFor(const SpirvEmitterState& state) { @@ -233,7 +240,7 @@ std::uint32_t SpirvValueEmitContext::Label(const IrBlock* block) const { } [[noreturn]] void SpirvValueEmitContext::Fail(const IrValue& inst, const char* reason) const { - throw std::runtime_error("SPIR-V emission failed: hash=0x" + std::to_string(state.program.Resources().shaderHash) + " stage=" + std::to_string(static_cast(state.program.Resources().stage)) + " opcode=" + std::string(IrOpcodeName(inst.Opcode())) + " reason=" + reason); + throw std::runtime_error(FailureText(state.program) + " opcode=" + std::string(IrOpcodeName(inst.Opcode())) + " reason=" + reason); } std::vector SpirvEmitter::Emit(const IrProgram& program, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target) const { From be1287b66c01891f9f1a6f24a58f4ec94b42c7b9 Mon Sep 17 00:00:00 2001 From: WallisSkill Date: Mon, 19 Oct 2026 01:02:01 +0700 Subject: [PATCH 021/167] fix(libSceVideoOut): coalesce controller output updates Every change of the pad output state sent a rumble, light bar and trigger effect update to SDL, and the trigger effect was sent again even when it had not changed. Titles that animate the light bar or drive rumble from haptic audio change that state every frame, so at 120 Hz about 110 reports a second were queued while SDL's HIDAPI rumble thread sends at most one per 10 ms. Different report kinds do not merge in its queue, so the backlog grew: rumble arrived ever later, and disconnecting the controller blocked the presenter until the queue drained. Updates now go out at most every 30 ms with the latest state, and the trigger effect is sent only when it changes or a controller connects. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceVideoOut/include/PadInput.hpp | 4 ++++ core/libs/prx/libSceVideoOut/src/PadInput.cpp | 15 +++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index de1c799e0e..c7216c2587 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -5,6 +5,7 @@ #include "SDL_gamecontroller.h" #include "prx/libScePad/include/KeyboardMouse.hpp" #include "prx/libScePad/include/PadState.hpp" +#include #include class DisplayWindow; @@ -35,6 +36,9 @@ private: PadOutputState outputState{}; bool outputPending = false; std::chrono::steady_clock::time_point nextRumbleRefresh{}; + std::chrono::steady_clock::time_point nextOutputAllowed{}; + std::array sentTriggerEffect{}; + bool triggerEffectSent = false; }; #endif diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 5478ece435..e7e3f553c6 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -59,6 +59,7 @@ void PadInput::openController(int deviceIndex) { SDL_GameControllerGetNumTouchpads(controller), SDL_GameControllerHasLED(controller) == SDL_TRUE, SDL_GameControllerHasRumbleTriggers(controller) == SDL_TRUE); enableSensors(); outputPending = true; + triggerEffectSent = false; } void PadInput::enableSensors() { @@ -94,8 +95,10 @@ void PadInput::applyOutput() { if ((rumbling || (triggerRumble && !isPs5)) && now >= nextRumbleRefresh) outputPending = true; else return; } + if (now < nextOutputAllowed) return; outputPending = false; nextRumbleRefresh = now + std::chrono::milliseconds(700); + nextOutputAllowed = now + std::chrono::milliseconds(30); constexpr Uint32 rumbleMs = 2000; SDL_GameControllerRumble(controller, static_cast(large * 257), static_cast(small * 257), rumbling ? rumbleMs : 0); if (SDL_GameControllerHasLED(controller) == SDL_TRUE) { @@ -104,11 +107,15 @@ void PadInput::applyOutput() { } if (outputState.triggerTouched) { if (isPs5) { - Uint8 effect[47] = {}; + std::array effect{}; effect[0] = 0x04 | 0x08; - std::memcpy(effect + 10, outputState.trigger[1].effect, 11); - std::memcpy(effect + 21, outputState.trigger[0].effect, 11); - SDL_GameControllerSendEffect(controller, effect, sizeof(effect)); + std::memcpy(effect.data() + 10, outputState.trigger[1].effect, 11); + std::memcpy(effect.data() + 21, outputState.trigger[0].effect, 11); + if (!triggerEffectSent || effect != sentTriggerEffect) { + SDL_GameControllerSendEffect(controller, effect.data(), static_cast(effect.size())); + sentTriggerEffect = effect; + triggerEffectSent = true; + } } else if (SDL_GameControllerHasRumbleTriggers(controller) == SDL_TRUE) { SDL_GameControllerRumbleTriggers(controller, static_cast(outputState.trigger[0].fallback * 257), static_cast(outputState.trigger[1].fallback * 257), triggerRumble ? rumbleMs : 0); } From eaa6d89ff981f558f8213ac1b3dd5f9abd51eb6f Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sun, 18 Oct 2026 13:59:56 -0400 Subject: [PATCH 022/167] fix(shader): follow scheduled jump table index writes --- .../prx/libSceAgcDriver/tests/ControlFlow.cpp | 26 +++++++++++++++++++ .../tests/execution/ScalarSetpc.cpp | 24 +++++++++++++++++ .../ControlFlow/src/GraphBuilder.cpp | 13 +++++++--- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index a5d0d015da..92a22769c4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -471,6 +471,31 @@ void verifyLongBranch(const std::string& name, std::span co require(source != graph.blocks.end() && source->terminator.kind == TerminatorKind::Branch && graph.FindBlock(source->terminator.trueBlock).startProgramCounter == target, name + ": the jump does not branch to " + std::to_string(target)); } +void verifyScheduledJumpTable() { + const std::vector code{ + 0x83888208u, 0x7e020280u, 0x8f088308u, 0x7e040280u, 0xbe8c1f00u, 0x800cff0cu, 0x00000048u, + 0x820d800du, 0xf4040386u, 0x10000000u, 0xbf8cc07fu, 0xbe901f00u, 0x80100e10u, 0x82110f11u, + 0xbe802010u, 0x7e020281u, 0xbf820003u, 0x7e020282u, 0xbf820001u, 0x7e020283u, 0xe0700000u, + 0x80000100u, 0xbf810000u, 0x0000000cu, 0u, 0x00000014u, 0u, 0x0000001cu, 0u, + }; + verifyJumpTable("scheduled two-dword table", code, 0x20u, {0xcu, 0x14u, 0x1cu}, {0x3cu, 0x44u, 0x4cu}); + static_cast(recompile("scheduled two-dword table", code, 32u)); + static_cast(recompile("scheduled two-dword table", code, 64u)); + auto vcc = patched(code, 0, {0x83ea826au}); + vcc = patched(std::move(vcc), 2, {0x8f6a836au}); + vcc = patched(std::move(vcc), 9, {0xd4000000u}); + verifyJumpTable("scheduled two-dword table through vcc", vcc, 0x20u, {0xcu, 0x14u, 0x1cu}, {0x3cu, 0x44u, 0x4cu}); + static_cast(recompile("scheduled two-dword table through vcc", vcc, 32u)); + static_cast(recompile("scheduled two-dword table through vcc", vcc, 64u)); + const std::vector> refused{ + patched(code, 1, {0xbe880380u}), + patched(code, 3, {0xbe880380u}), + patched(code, 1, {0xbf820001u}), + patched(code, 3, {0xbf850000u}), + }; + for (const auto& rejected : refused) require(refusal(rejected).find("unsupported dynamic s_setpc_b64") != std::string::npos, "a rewritten or conditional two-dword table index was accepted"); +} + void verifyNullSwappc() { const std::array jump{0xbefd210cu, 0xbf810000u}; const auto swappc = RdnaInstructionDecoder{}.Decode(jump).instructions.front(); @@ -1369,6 +1394,7 @@ done: } try { verifyNullSwappc(); + verifyScheduledJumpTable(); VerifyUserDataTail(); VerifyConstantHighTail(); verifyLargeLoopExitFallback(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSetpc.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSetpc.cpp index 669e125986..41069b2013 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSetpc.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSetpc.cpp @@ -274,6 +274,22 @@ void Run(AgcDriver::VulkanDevice& device, std::span code, s device.WaitIdle(); } +std::vector Scheduled(std::span original) { + std::vector code(original.begin(), original.end()); + code.insert(code.begin() + 25, 0x7e3c0280u); + code.insert(code.begin() + 24, 0x7e3e0280u); + const auto relocated = [](std::int32_t word) { return word + (word >= 24 ? 1 : 0) + (word >= 25 ? 1 : 0); }; + for (std::int32_t word = 0; word < static_cast(original.size()); ++word) { + const auto instruction = original[static_cast(word)]; + const auto opcode = instruction & 0xffff0000u; + if (opcode != 0xbf820000u && opcode != 0xbf850000u) continue; + const auto target = word + 1 + static_cast(instruction & 0xffffu); + const auto offset = relocated(target) - relocated(word) - 1; + code[static_cast(relocated(word))] = opcode | (static_cast(offset) & 0xffffu); + } + return code; +} + void CheckReversedSubtractRefused(AgcDriver::VulkanDevice& device) { std::string refusal; try { @@ -306,6 +322,14 @@ int main() { Check(Expected64); Run(*device, Wave64Code, 64, device->ComputeTarget(32)); Check(Expected64); + const auto scheduled32 = Scheduled(Wave32Code); + const auto scheduled64 = Scheduled(Wave64Code); + Run(*device, scheduled32, 32, device->ComputeTarget(32)); + Check(Expected32); + Run(*device, scheduled64, 64, device->Target()); + Check(Expected64); + Run(*device, scheduled64, 64, device->ComputeTarget(32)); + Check(Expected64); CheckReversedSubtractRefused(*device); std::puts("setpc tests passed"); return 0; diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index fe2c3565a3..73e98b1341 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -195,6 +195,8 @@ bool addsImmediate(const RdnaInstruction& instruction, std::uint32_t reg, std::u (isRegister(instruction.source1, RdnaOperandKind::ScalarRegister, reg) && isImmediate(instruction.source0, immediate))); } +bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer, std::uint32_t waveSize = 0u, std::uint32_t skipBranchesPast = 0u); + bool resolveJumpTable(const RdnaProgram& program, std::uint32_t index, BoundedJumpTable& result) { if (index < 9u) return false; const auto& branch = program.instructions[index]; @@ -223,8 +225,13 @@ bool resolveJumpTable(const RdnaProgram& program, std::uint32_t index, BoundedJu const auto& basePc = program.instructions[loadIndex - 3u]; const auto& baseLow = program.instructions[loadIndex - 2u]; const auto& baseHigh = program.instructions[loadIndex - 1u]; - const auto& shift = program.instructions[loadIndex - 4u]; - const auto& bound = program.instructions[loadIndex - 5u]; + const auto offsetReg = scalarIndex(load.source1); + std::uint32_t shiftIndex = 0, boundIndex = 0; + if (offsetReg == NoScalarRegister || + !findLastWriter(program, loadIndex - 3u, {offsetReg}, shiftIndex) || + !findLastWriter(program, shiftIndex, {offsetReg}, boundIndex)) return false; + const auto& shift = program.instructions[shiftIndex]; + const auto& bound = program.instructions[boundIndex]; const auto baseReg = load.source0.reg; std::uint32_t displacement = 0, carry = 0, shiftAmount = 0, maximum = 0; if (basePc.op != RdnaOpcode::SGetpcB64 || !isRegister(basePc.destination, RdnaOperandKind::ScalarRegister, baseReg) || @@ -294,7 +301,7 @@ bool writesScalar(const RdnaInstruction& instruction, std::uint32_t index, std:: return covers(instruction.destination) || covers(instruction.destination2); } -bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer, std::uint32_t waveSize = 0u, std::uint32_t skipBranchesPast = 0u) { +bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer, std::uint32_t waveSize, std::uint32_t skipBranchesPast) { for (auto position = end; position-- > 0u;) { const auto& instruction = program.instructions[position]; const bool skipped = skipBranchesPast != 0u && IsDirectBranchOpcode(instruction.op) && instruction.op != RdnaOpcode::SBranch && instruction.branchTarget > skipBranchesPast; From ffa63e8fec5b7d0037474f8673c56981267b5ecc Mon Sep 17 00:00:00 2001 From: WallisSkill Date: Mon, 19 Oct 2026 01:09:43 +0700 Subject: [PATCH 023/167] fix(libSceVideoOut): rumble a DualSense from haptic audio without a sound card AudioOut2 turns controller haptic PCM into motor rumble only when no controller sound card plays it, which is the case for a DualSense over Bluetooth. PadInput then dropped that rumble for every DualSense, so titles that vibrate through haptic audio never rumbled one over Bluetooth. A DualSense now takes it like any other controller; with a sound card no haptic rumble is submitted, so nothing plays twice. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceVideoOut/src/PadInput.cpp | 4 ++-- docs/dev/TechnicalDebt.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 5478ece435..5cb167e2f4 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -86,8 +86,8 @@ void PadInput::applyOutput() { if (controller == nullptr) return; const auto now = std::chrono::steady_clock::now(); const bool isPs5 = SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_PS5; - const auto large = isPs5 ? outputState.vibrationLarge : std::max(outputState.vibrationLarge, outputState.audioVibrationLarge); - const auto small = isPs5 ? outputState.vibrationSmall : std::max(outputState.vibrationSmall, outputState.audioVibrationSmall); + const auto large = std::max(outputState.vibrationLarge, outputState.audioVibrationLarge); + const auto small = std::max(outputState.vibrationSmall, outputState.audioVibrationSmall); const bool rumbling = large != 0 || small != 0; const bool triggerRumble = outputState.trigger[0].fallback != 0 || outputState.trigger[1].fallback != 0; if (!outputPending) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index cfbf2e14f0..be43a0d2e8 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -24,6 +24,6 @@ Throughout the project, every function at every stage either **does exactly what ### Functional -- Controller haptic PCM drives Xbox-style motor rumble from its per-channel RMS (large/small motor, scale 2x, below 0.01 ignored, 80 ms hold); the mapping approximates waveform energy and is not measured against console hardware +- Controller haptic PCM that no controller sound card plays (a DualSense over Bluetooth, or any other controller) drives motor rumble from its per-channel RMS (large/small motor, scale 2x, below 0.01 ignored, 80 ms hold); the mapping approximates waveform energy and is not measured against console hardware - [Cancellation coverage](ARCHITECTURE.md#thread-cancellation) checks deferred request handling and recovery, not full asynchronous support or console ABI compatibility. Cancellation during file/socket I/O, sleep interruption, cancellation versus exit/detach, condition signal conservation under cancellation, and safe rejection of non-null invalid or reclaimed thread handles remain unverified. No console title or homebrew establishes the cleanup NIDs or buffer layout. Accepted asynchronous mode currently has no diagnostic for its deferred behavior. - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. From 8e2f71263c7c16612dd36f04052a1ab555a24449 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 18 Oct 2026 13:49:15 +0000 Subject: [PATCH 024/167] fix(libkernel): return ENOENT for measured sysctl MIBs --- .../System/src/SystemConfiguration.cpp | 21 +++++++++ core/libs/tests/GuestSystemConfiguration.cpp | 44 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 66 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index 3205d75fce..dd16dd67d9 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -47,6 +47,22 @@ bool IsLeafMib(const int* name) { return (name[0] == 6 && (name[1] == 2 || name[1] == 3 || name[1] == 5 || name[1] == 7)) || (name[0] == 1 && name[1] == 37); } +bool IsMeasuredAbsentMib(const int* name, std::uint32_t nameLength) { + static constexpr struct { + int mib[3]; + std::uint32_t length; + } absent[] = { + {{1, 2147482816, 2147482814}, 3}, + {{1, 2147483151}, 2}, + {{1, 2147483182, 2147483181}, 3}, + {{1, 2147482838, 2147482834}, 3}, + }; + for (const auto& entry : absent) { + if (nameLength == entry.length && std::memcmp(name, entry.mib, entry.length * sizeof(int)) == 0) return true; + } + return false; +} + } extern "C" { @@ -100,6 +116,11 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *__error_nid_postfix() = 14; return -1; } + if (IsMeasuredAbsentMib(name, nameLength)) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": writing an unmeasured absent MIB"); + *__error_nid_postfix() = 2; + return -1; + } if (nameLength == 2 && name[0] == 6 && name[1] == 7) { if (newValue != nullptr) { *__error_nid_postfix() = 1; diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 5c3f0922f4..e22964c6ea 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -209,6 +209,49 @@ static void CheckLeafWithTrailingComponent() { Require(length == sizeof(buffer)); } } +static void CheckMeasuredAbsentSysctlMibs() { + static constexpr struct { + int mib[3]; + std::uint32_t length; + } cases[] = { + {{1, 2147482816, 2147482814}, 3}, + {{1, 2147483151}, 2}, + {{1, 2147483182, 2147483181}, 3}, + {{1, 2147482838, 2147482834}, 3}, + }; + for (const auto& entry : cases) { + unsigned char buffer[64]; + std::memset(buffer, 0xaa, sizeof(buffer)); + std::size_t length = sizeof(buffer); + *__error_nid_postfix() = 77; + Require(sysctl_nid_postfix(entry.mib, entry.length, buffer, &length, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2 && length == sizeof(buffer)); + for (const auto byte : buffer) Require(byte == 0xaa); + + length = 77; + *__error_nid_postfix() = 77; + Require(sysctl_nid_postfix(entry.mib, entry.length, nullptr, &length, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2 && length == 77); + + std::memset(buffer, 0xaa, sizeof(buffer)); + *__error_nid_postfix() = 77; + Require(sysctl_nid_postfix(entry.mib, entry.length, buffer, nullptr, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2); + for (const auto byte : buffer) Require(byte == 0xaa); + + *__error_nid_postfix() = 77; + Require(sysctl_nid_postfix(entry.mib, entry.length, nullptr, nullptr, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2); + + bool threw = false; + try { + sysctl_nid_postfix(entry.mib, entry.length, buffer, &length, buffer, sizeof(buffer)); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + } +} static void CheckSystemSwVersion() { *__error_nid_postfix() = 13; Require(sceKernelGetSystemSwVersion_nid_postfix(nullptr) == SCE_KERNEL_ERROR_EFAULT && *__error_nid_postfix() == 13); @@ -427,6 +470,7 @@ int main() { CheckMachdepCachelineSizeSysctl(); CheckPathnameSysctl(); CheckLeafWithTrailingComponent(); + CheckMeasuredAbsentSysctlMibs(); CheckSystemSwVersion(); CheckPseudoRandomSysctl(); CheckSdkVersionSysctl(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b4adfa1c64..fbaf065c1f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -210,3 +210,4 @@ - [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. - [app0.zar files](../../../core/libs/prx/libkernel/File/src/ArchivedFile.cpp) (libkernel) - `sceKernelOpen`, `open`, `sceKernelStat` and `sceKernelCheckReachability` resolve a path missing from `app0/` in `app0.zar`. An archived file is a read-only host null device whose reads, positioned and vectored reads, AIO reads, seeks, `fstat` and `FIONREAD` are served from the archive, with the position shared by `dup`, `dup2` and `F_DUPFD`; writes fail with `EBADF` as on a read-only descriptor, opening one for writing or truncation fails with `EROFS` and `O_CREAT | O_EXCL` with `EEXIST`. `fstat` and `sceKernelStat` report mode `0444` (`0555` for a directory), the node index as inode and the archive file's device and times. Opening an archived directory or a directory present both on disk and in the archive throws, as `getdents` would miss entries. On an archived descriptor `mmap`, `flock`, record locks, `fstatfs`, `ftruncate`, `fsync`, `fdatasync`, `getdirentries`, `fchmod` and `futimes` throw instead of acting on the null device; `mmap` is `noexcept`, so its refusal terminates the process. `lstat` and the other path calls on archived files are not implemented - [param.sfo](../../../core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp) (libkernel) is read when `param.json` is absent, for `TITLE`, `TITLE_ID`, `DOWNLOAD_DATA_SIZE` and `USER_DEFINED_PARAM_1` to `_4`, and mounts `/data` and `/download0` like `param.json`; the localized `TITLE_00` to `TITLE_30` are not selected by the console language. +- [sysctl MIBs for kern.smp.maxcpus, kern.stackprot, kern.sched.name and kern.timecounter.hardware](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - on a PS5 (firmware 12.70), a payload process resolves these names to {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}; `sysctl` returns -1 with `ENOENT` for reads and size queries by those exact MIBs, leaving the buffer and length unchanged, including when either or both output pointers are null. Writes were not measured and throw. The names' `sysctlnametomib` mappings are not implemented here. From b42de6cf9e83e8435389d16b36836e7bc67172af Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 18 Oct 2026 14:45:59 +0000 Subject: [PATCH 025/167] docs: clarify sysctlnametomib MIB coverage --- 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 fbaf065c1f..1e6a2f3143 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -210,4 +210,4 @@ - [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. - [app0.zar files](../../../core/libs/prx/libkernel/File/src/ArchivedFile.cpp) (libkernel) - `sceKernelOpen`, `open`, `sceKernelStat` and `sceKernelCheckReachability` resolve a path missing from `app0/` in `app0.zar`. An archived file is a read-only host null device whose reads, positioned and vectored reads, AIO reads, seeks, `fstat` and `FIONREAD` are served from the archive, with the position shared by `dup`, `dup2` and `F_DUPFD`; writes fail with `EBADF` as on a read-only descriptor, opening one for writing or truncation fails with `EROFS` and `O_CREAT | O_EXCL` with `EEXIST`. `fstat` and `sceKernelStat` report mode `0444` (`0555` for a directory), the node index as inode and the archive file's device and times. Opening an archived directory or a directory present both on disk and in the archive throws, as `getdents` would miss entries. On an archived descriptor `mmap`, `flock`, record locks, `fstatfs`, `ftruncate`, `fsync`, `fdatasync`, `getdirentries`, `fchmod` and `futimes` throw instead of acting on the null device; `mmap` is `noexcept`, so its refusal terminates the process. `lstat` and the other path calls on archived files are not implemented - [param.sfo](../../../core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp) (libkernel) is read when `param.json` is absent, for `TITLE`, `TITLE_ID`, `DOWNLOAD_DATA_SIZE` and `USER_DEFINED_PARAM_1` to `_4`, and mounts `/data` and `/download0` like `param.json`; the localized `TITLE_00` to `TITLE_30` are not selected by the console language. -- [sysctl MIBs for kern.smp.maxcpus, kern.stackprot, kern.sched.name and kern.timecounter.hardware](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - on a PS5 (firmware 12.70), a payload process resolves these names to {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}; `sysctl` returns -1 with `ENOENT` for reads and size queries by those exact MIBs, leaving the buffer and length unchanged, including when either or both output pointers are null. Writes were not measured and throw. The names' `sysctlnametomib` mappings are not implemented here. +- [sysctl MIBs for kern.smp.maxcpus, kern.stackprot, kern.sched.name and kern.timecounter.hardware](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - on a PS5 (firmware 12.70), a payload process resolves these names to {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}; `sysctl` returns -1 with `ENOENT` for reads and size queries by those exact MIBs, leaving the buffer and length unchanged, including when either or both output pointers are null. Writes were not measured and throw. `sysctlnametomib` still throws for these four names; #7541 implements the mappings for nine of the other names. 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 026/167] 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 854032843172a09e96323aadd39539a712ebafc3 Mon Sep 17 00:00:00 2001 From: WallisSkill Date: Mon, 19 Oct 2026 02:14:44 +0700 Subject: [PATCH 027/167] fix(libSceVideoOut): pace only the light bar and resend refused effects The 30 ms limit gated all controller output, so a rumble pulse shorter than the interval could be lost and a stop could wait up to 30 ms. Rumble goes out on every output change again, as on main; SDL already skips repeated values and merges consecutive rumble reports. Only the light bar is paced: a changed colour is sent at most every 30 ms, always the latest one, and a colour SDL refuses is retried after the interval. The trigger effect is cached only when SDL_GameControllerSendEffect succeeds, so a refused effect is sent again on the next update as it was before. LightBarPacer and TriggerEffectCache hold these decisions; pad_output_pacing covers changes, retries, a new controller and a light bar animated every frame at 120 Hz. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceVideoOut/CMakeLists.txt | 9 ++ .../prx/libSceVideoOut/include/PadInput.hpp | 7 +- .../include/PadOutputPacing.hpp | 63 +++++++++++++ core/libs/prx/libSceVideoOut/src/PadInput.cpp | 23 +++-- .../libSceVideoOut/tests/PadOutputPacing.cpp | 89 +++++++++++++++++++ 5 files changed, 175 insertions(+), 16 deletions(-) create mode 100644 core/libs/prx/libSceVideoOut/include/PadOutputPacing.hpp create mode 100644 core/libs/prx/libSceVideoOut/tests/PadOutputPacing.cpp diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index b8e4ba2743..4528be4350 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -140,6 +140,15 @@ if(BUILD_TESTING) set_tests_properties(pad_input_mapping PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(pad_output_pacing_tests tests/PadOutputPacing.cpp) + target_include_directories(pad_output_pacing_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(pad_output_pacing_tests PRIVATE libc) + configure_windows_unwind(pad_output_pacing_tests) + add_test(NAME pad_output_pacing COMMAND pad_output_pacing_tests) + if(WIN32) + set_tests_properties(pad_output_pacing PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(BUILD_TESTING) diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index c7216c2587..03c2a49f59 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -5,7 +5,7 @@ #include "SDL_gamecontroller.h" #include "prx/libScePad/include/KeyboardMouse.hpp" #include "prx/libScePad/include/PadState.hpp" -#include +#include "prx/libSceVideoOut/include/PadOutputPacing.hpp" #include class DisplayWindow; @@ -36,9 +36,8 @@ private: PadOutputState outputState{}; bool outputPending = false; std::chrono::steady_clock::time_point nextRumbleRefresh{}; - std::chrono::steady_clock::time_point nextOutputAllowed{}; - std::array sentTriggerEffect{}; - bool triggerEffectSent = false; + LightBarPacer lightBar; + TriggerEffectCache triggerEffect; }; #endif diff --git a/core/libs/prx/libSceVideoOut/include/PadOutputPacing.hpp b/core/libs/prx/libSceVideoOut/include/PadOutputPacing.hpp new file mode 100644 index 0000000000..6061fcf618 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/include/PadOutputPacing.hpp @@ -0,0 +1,63 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_PADOUTPUTPACING_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_PADOUTPUTPACING_HPP + +#include +#include +#include + +class LightBarPacer { +public: + using Clock = std::chrono::steady_clock; + using Color = std::array; + static constexpr std::chrono::milliseconds Interval{30}; + + bool Pending(const Color& color) const { + return !sent || color != last; + } + + bool Due(const Color& color, Clock::time_point now) const { + return Pending(color) && now >= next; + } + + void Result(const Color& color, Clock::time_point now, bool succeeded) { + next = now + Interval; + if (!succeeded) return; + last = color; + sent = true; + } + + void Reset() { + sent = false; + next = {}; + } + +private: + Color last{}; + bool sent = false; + Clock::time_point next{}; +}; + +class TriggerEffectCache { +public: + using Effect = std::array; + + bool NeedsSend(const Effect& effect) const { + return !sent || effect != last; + } + + void Result(const Effect& effect, bool succeeded) { + if (!succeeded) return; + last = effect; + sent = true; + } + + void Reset() { + sent = false; + } + +private: + Effect last{}; + bool sent = false; +}; + +#endif diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index e7e3f553c6..2b71356e45 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -59,7 +59,8 @@ void PadInput::openController(int deviceIndex) { SDL_GameControllerGetNumTouchpads(controller), SDL_GameControllerHasLED(controller) == SDL_TRUE, SDL_GameControllerHasRumbleTriggers(controller) == SDL_TRUE); enableSensors(); outputPending = true; - triggerEffectSent = false; + lightBar.Reset(); + triggerEffect.Reset(); } void PadInput::enableSensors() { @@ -91,30 +92,28 @@ void PadInput::applyOutput() { const auto small = isPs5 ? outputState.vibrationSmall : std::max(outputState.vibrationSmall, outputState.audioVibrationSmall); const bool rumbling = large != 0 || small != 0; const bool triggerRumble = outputState.trigger[0].fallback != 0 || outputState.trigger[1].fallback != 0; + if (SDL_GameControllerHasLED(controller) == SDL_TRUE) { + const LightBarPacer::Color color = outputState.lightBarValid + ? LightBarPacer::Color{outputState.lightBar[0], outputState.lightBar[1], outputState.lightBar[2]} + : LightBarPacer::Color{0, 64, 255}; + if (lightBar.Due(color, now)) lightBar.Result(color, now, SDL_GameControllerSetLED(controller, color[0], color[1], color[2]) == 0); + } if (!outputPending) { if ((rumbling || (triggerRumble && !isPs5)) && now >= nextRumbleRefresh) outputPending = true; else return; } - if (now < nextOutputAllowed) return; outputPending = false; nextRumbleRefresh = now + std::chrono::milliseconds(700); - nextOutputAllowed = now + std::chrono::milliseconds(30); constexpr Uint32 rumbleMs = 2000; SDL_GameControllerRumble(controller, static_cast(large * 257), static_cast(small * 257), rumbling ? rumbleMs : 0); - if (SDL_GameControllerHasLED(controller) == SDL_TRUE) { - if (outputState.lightBarValid) SDL_GameControllerSetLED(controller, outputState.lightBar[0], outputState.lightBar[1], outputState.lightBar[2]); - else SDL_GameControllerSetLED(controller, 0, 64, 255); - } if (outputState.triggerTouched) { if (isPs5) { - std::array effect{}; + TriggerEffectCache::Effect effect{}; effect[0] = 0x04 | 0x08; std::memcpy(effect.data() + 10, outputState.trigger[1].effect, 11); std::memcpy(effect.data() + 21, outputState.trigger[0].effect, 11); - if (!triggerEffectSent || effect != sentTriggerEffect) { - SDL_GameControllerSendEffect(controller, effect.data(), static_cast(effect.size())); - sentTriggerEffect = effect; - triggerEffectSent = true; + if (triggerEffect.NeedsSend(effect)) { + triggerEffect.Result(effect, SDL_GameControllerSendEffect(controller, effect.data(), static_cast(effect.size())) == 0); } } else if (SDL_GameControllerHasRumbleTriggers(controller) == SDL_TRUE) { SDL_GameControllerRumbleTriggers(controller, static_cast(outputState.trigger[0].fallback * 257), static_cast(outputState.trigger[1].fallback * 257), triggerRumble ? rumbleMs : 0); diff --git a/core/libs/prx/libSceVideoOut/tests/PadOutputPacing.cpp b/core/libs/prx/libSceVideoOut/tests/PadOutputPacing.cpp new file mode 100644 index 0000000000..ab624ef5a4 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/PadOutputPacing.cpp @@ -0,0 +1,89 @@ +#include "prx/libSceVideoOut/include/PadOutputPacing.hpp" + +#include +#include +#include + +namespace { + +using namespace std::chrono_literals; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "pad_output_pacing: %s\n", message); + std::exit(1); +} + +void LightBarSendsChangesOnly() { + LightBarPacer pacer; + const auto start = LightBarPacer::Clock::time_point{} + 1s; + const LightBarPacer::Color blue{0, 64, 255}; + Require(pacer.Due(blue, start), "the first colour is sent at once"); + pacer.Result(blue, start, true); + Require(!pacer.Pending(blue), "a colour that was sent is not sent again"); + Require(!pacer.Due(blue, start + 1s), "an unchanged colour stays quiet"); + const LightBarPacer::Color red{255, 0, 0}; + Require(pacer.Pending(red) && !pacer.Due(red, start + 10ms), "a new colour waits for the interval"); + Require(pacer.Due(red, start + LightBarPacer::Interval), "a new colour goes out once the interval has passed"); +} + +void LightBarRetriesFailures() { + LightBarPacer pacer; + const auto start = LightBarPacer::Clock::time_point{} + 1s; + const LightBarPacer::Color green{0, 255, 0}; + pacer.Result(green, start, false); + Require(pacer.Pending(green), "a colour SDL refused is still pending"); + Require(!pacer.Due(green, start + 1ms), "a refused colour is not retried on the next frame"); + Require(pacer.Due(green, start + LightBarPacer::Interval), "a refused colour is retried after the interval"); + pacer.Result(green, start + LightBarPacer::Interval, true); + Require(!pacer.Pending(green), "a retried colour that was sent is done"); + pacer.Reset(); + Require(pacer.Due(green, start + LightBarPacer::Interval), "a new controller gets the current colour"); +} + +void LightBarAnimationIsPaced() { + LightBarPacer pacer; + const auto start = LightBarPacer::Clock::time_point{} + 1s; + constexpr int frames = 120; + int sent = 0; + LightBarPacer::Color color{}; + for (int frame = 0; frame < frames; ++frame) { + const auto now = start + std::chrono::microseconds(frame * 8333); + color = {static_cast(frame), 32, 32}; + if (!pacer.Due(color, now)) continue; + pacer.Result(color, now, true); + ++sent; + } + Require(sent >= 30 && sent <= 34, "a colour that changes every frame at 120 Hz goes out about 33 times a second"); + const auto end = start + 1s; + Require(pacer.Pending(color), "the last colour of the animation is still owed"); + Require(pacer.Due(color, end + LightBarPacer::Interval), "the last colour of the animation is sent when the interval allows"); +} + +void TriggerEffectRetriesFailures() { + TriggerEffectCache cache; + TriggerEffectCache::Effect effect{}; + effect[0] = 0x0C; + effect[10] = 0x21; + Require(cache.NeedsSend(effect), "the first effect is sent"); + cache.Result(effect, false); + Require(cache.NeedsSend(effect), "an effect SDL refused is sent again"); + cache.Result(effect, true); + Require(!cache.NeedsSend(effect), "an effect that was sent is not repeated"); + auto changed = effect; + changed[21] = 0x26; + Require(cache.NeedsSend(changed), "a changed effect is sent"); + cache.Reset(); + Require(cache.NeedsSend(effect), "a new controller gets the current effect"); +} + +} + +int main() { + LightBarSendsChangesOnly(); + LightBarRetriesFailures(); + LightBarAnimationIsPaced(); + TriggerEffectRetriesFailures(); + std::puts("pad_output_pacing: ok"); + return 0; +} From 2e363d375a50af6e77c5c61ecba7c581ed0f4449 Mon Sep 17 00:00:00 2001 From: billythemenace2 Date: Sun, 18 Oct 2026 21:24:44 +0100 Subject: [PATCH 028/167] fix(libkernel): redirect a raise only once the target leaves host code On Windows, sceKernelRaiseException suspends the target and redirects it to the guest handler wherever it stopped, except inside NtContinue or winpthreads. A target stopped inside host code (a C runtime call, a host library) can hold a host lock there; a handler that then waits, as a collector's thread-stop handler does, keeps it held, and every host thread that needs the lock stops with it. SONIC SUPERSTARS stopped at frame 65 this way: its "UDS" thread was redirected inside the stderr write of a trophy message and parked in sceKernelWaitEventFlag holding the stream lock, and the driver's logging threads blocked on it. A target running code of a module other than the executable or a guest module is now resumed and suspended again until it is back in guest code, for up to the existing retry limit; a target blocked in an ntdll wait is still redirected there, as before. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libkernel/CMakeLists.txt | 7 ++ .../prx/libkernel/System/src/Exception.cpp | 38 +++++++++- core/libs/tests/GuestRaiseInHostCode.cpp | 69 +++++++++++++++++++ 3 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 core/libs/tests/GuestRaiseInHostCode.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 1bb3b681c8..bed3d86510 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1597,6 +1597,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_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) + configure_windows_unwind(guest_raise_in_host_code_tests) + add_test(NAME guest_raise_in_host_code COMMAND guest_raise_in_host_code_tests) + set_tests_properties(guest_raise_in_host_code PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") add_test_executable(guest_mutex_contention_tests ${LIBS_INCLUDE_DIR}/tests/GuestMutexContention.cpp) target_include_directories(guest_mutex_contention_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_mutex_contention_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 6dbef36c89..be680ca0f0 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -4,6 +4,8 @@ #include "prx/libc/include/General.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" +#include +#include #include #include #include @@ -12,6 +14,7 @@ #include #include #ifdef _WIN32 +#include #include #else #include @@ -249,6 +252,36 @@ bool InWinpthread(DWORD64 rip) { return winpthread != nullptr && VirtualQuery(reinterpret_cast(rip), &info, sizeof(info)) == sizeof(info) && info.AllocationBase == winpthread; } +bool InHostModule(DWORD64 rip) { + HMODULE module = nullptr; + if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(rip), &module) || module == nullptr) return false; + if (module == GetModuleHandleW(nullptr)) return false; + wchar_t path[MAX_PATH]{}; + const auto length = GetModuleFileNameW(module, path, MAX_PATH); + const std::wstring_view name(path, length); + constexpr std::wstring_view guestSuffix = L".guest.prx"; + return !(name.size() >= guestSuffix.size() && name.substr(name.size() - guestSuffix.size()) == guestSuffix); +} + +bool BlockedInWait(DWORD64 rip) { + static const std::vector waits = [] { + std::vector stubs; + const HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); + for (const char* name : {"NtWaitForSingleObject", "NtWaitForMultipleObjects", "NtWaitForAlertByThreadId", "NtDelayExecution", "NtRemoveIoCompletion", "NtRemoveIoCompletionEx", "NtWaitForKeyedEvent", "NtSignalAndWaitForSingleObject", "NtWaitForWorkViaWorkerFactory"}) { + if (const auto address = reinterpret_cast(GetProcAddress(ntdll, name)); address != 0) stubs.push_back(address); + } + if (const HMODULE win32u = GetModuleHandleW(L"win32u.dll"); win32u != nullptr) { + if (const auto address = reinterpret_cast(GetProcAddress(win32u, "NtUserMsgWaitForMultipleObjectsEx")); address != 0) stubs.push_back(address); + } + return stubs; + }(); + return std::any_of(waits.begin(), waits.end(), [rip](DWORD64 stub) { return rip - stub < 0x20; }); +} + +bool RunningHostCode(DWORD64 rip) { + return InHostModule(rip) && !BlockedInWait(rip); +} + bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { if (thread == scePthreadSelf()) { CONTEXT context{}; @@ -281,9 +314,10 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { queued.release(); return true; } - if (!RestoringContext(delivery.context.Rip) && !InWinpthread(delivery.context.Rip)) break; + const bool restoring = RestoringContext(delivery.context.Rip) || InWinpthread(delivery.context.Rip); + if (!restoring && (!RunningHostCode(delivery.context.Rip) || std::chrono::steady_clock::now() >= retryDeadline)) break; ResumeThread(native); - if (std::chrono::steady_clock::now() >= retryDeadline) throw std::runtime_error("sceKernelRaiseException: the target thread stayed inside NtContinue or winpthreads for 1 s"); + if (restoring && std::chrono::steady_clock::now() >= retryDeadline) throw std::runtime_error("sceKernelRaiseException: the target thread stayed inside NtContinue or winpthreads for 1 s"); SwitchToThread(); } const DWORD64 slot = (delivery.context.Rsp - RedZone - sizeof(Delivery)) & ~static_cast(15); diff --git a/core/libs/tests/GuestRaiseInHostCode.cpp b/core/libs/tests/GuestRaiseInHostCode.cpp new file mode 100644 index 0000000000..a3f728545d --- /dev/null +++ b/core/libs/tests/GuestRaiseInHostCode.cpp @@ -0,0 +1,69 @@ +#include "SceTypes.hpp" +#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 = 20; +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 started{false}; + +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(1u << 20u, 'x'); + started.store(true); + while (!stop.load()) { + Require(std::fwrite(block.data(), 1, block.size(), shared) == block.size()); + Require(std::fseek(shared, 0, SEEK_SET) == 0); + 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 < 20000 && !done.load(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (!done.load()) std::abort(); + }); + shared = std::tmpfile(); + Require(shared != nullptr); + Require(sceKernelInstallExceptionHandler(SigUsr1, reinterpret_cast(&Handler)) == 0); + Pthread writer = nullptr; + Require(scePthreadCreate(&writer, nullptr, Writer, nullptr, "host writer") == 0); + while (!started.load()) 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); + Require(scePthreadJoin(writer, nullptr) == 0); + Require(sceKernelRemoveExceptionHandler(SigUsr1) == 0); + std::fclose(shared); + done.store(true); + watchdog.join(); +} From 388527d6b25c7a8353f3217295dbe4fb3fa347b9 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 18 Oct 2026 20:29:54 +0000 Subject: [PATCH 029/167] fix(agc): reimport a multisampled target recreated with another pipe-bank XOR RADV gives each image its own pipe-bank XOR when it creates it. When Ruffle recreates a 4x render target on the same memory, the next draw names the same surface, DCC and CMASK with another XOR, and the resident target refused it as a changed layout ("multisampled DCC target layout or metadata changed"). A use that differs from a resident DCC target only in that XOR now keeps the target's image, views and cached textures, takes the new layout, and reimports its contents from the current DCC and CMASK metadata the way a first use does, clearing the image to that color after the work already recorded against it. Any other layout change is still refused. Reusing the image keeps memory flat: Ruffle's cache_as_bitmap__avm2_button recreates its 864x864 4x target 28 times in 32 s. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/MultisampleTarget.cpp | 33 +++++++++++++ .../tests/execution/AlphaToCoverage.cpp | 46 +++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 4ca7a22552..e05bea8676 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -29,6 +29,10 @@ bool sameTarget(const ColorTarget& a, const ColorTarget& b) { return a.surfaceAddress == b.surfaceAddress && a.format == b.format && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.samples == b.samples; } +bool recreated(const ColorTarget& resident, const ColorTarget& use) { + return use.dccAddress != 0 && use.pipeBankXor != resident.pipeBankXor && use.address == resident.address && use.dccAddress == resident.dccAddress && use.dccControl == resident.dccControl && use.dccAlphaOnMsb == resident.dccAlphaOnMsb && use.dccPipeAligned == resident.dccPipeAligned && use.cmaskAddress == resident.cmaskAddress && use.cmaskBytes == resident.cmaskBytes && use.tileMode == resident.tileMode && use.elementBytes == resident.elementBytes; +} + VkClearColorValue fastClearColor(const ColorTarget& target) { Require(target.elementBytes != 0 && target.elementBytes <= sizeof(target.clearWords) && 16u % target.elementBytes == 0, "the fast clear of a multisampled target over 64-bit texels is not modeled"); std::array texels{}; @@ -189,6 +193,24 @@ public: }); } + void Recreate(const ColorTarget& use) { + SettleStorageWrites(); + target = use; + cmask = MultisampledCmask(use.cmaskAddress, use.cmaskBytes); + colorFill = MultisampledColorFill(use); + fromTexture = false; + dccRanges.clear(); + metadataValid = std::make_shared>(true); + const auto initial = initialDccClear(use); + recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &initial, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + }); + } + void SettleStorageWrites() { if (!storageWritten) return; storageWritten = false; @@ -442,6 +464,17 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { for (const auto& target : targets()) { if (!target->active->load(std::memory_order_acquire)) continue; if (target->context.device != context.device || !sameTarget(target->target, use)) continue; + if (recreated(target->target, use)) { + { + std::unique_lock lock(guardedMutex()); + std::erase_if(guarded(), [&](const auto& range) { return range.active == target->active; }); + anyGuarded().store(!guarded().empty(), std::memory_order_release); + } + target->Recreate(use); + guard(context, use, target->active, target->metadataValid); + target->ApplyFastClear(use); + return *target; + } if (target->fromTexture && use.cmaskAddress != 0) { target->fromTexture = false; target->cmask = MultisampledCmask(use.cmaskAddress, use.cmaskBytes); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index 4f6288141b..c7de927072 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -599,6 +599,52 @@ void DccFastClearTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSa } } } + for (const auto samples : {2u, 4u, 8u}) { + if (selectedSamples != 0 && samples != selectedSamples) continue; + blocks.push_back(std::make_unique(layout.Bytes() * samples)); + auto& source = *blocks.back(); + std::memset(source.data, 0, source.bytes); + blocks.push_back(std::make_unique(BlockBytes)); + auto& metadata = *blocks.back(); + std::memset(metadata.data, 0, BlockBytes); + auto first = DecodeTarget(source, samples); + first.dccAddress = metadata.Address(); + first.dccPipeAligned = true; + const Color written{0.25f, 0.25f, 0.25f, 0.25f}; + Draw(device, first, false, Float32Export, Program(written, Float32Export, 1u), false, true); + const std::vector ones(AgcDriver::Graphics::MultisampledDccBytes(first), std::byte{0xc0}); + AgcDriver::GuestMemory::Write(metadata.Address(), ones); + auto second = first; + second.pipeBankXor = 0x4000u; + Draw(device, second, false, Float32Export, Program(written, Float32Export, 1u), false, true); + blocks.push_back(std::make_unique(layout.Bytes())); + auto& resolved = *blocks.back(); + std::memset(resolved.data, 0, resolved.bytes); + AgcDriver::Graphics::ColorMetadataPass pass{}; + pass.mode = AgcDriver::Graphics::ColorMetadataPass::Mode::Resolve; + pass.source = second; + pass.targets = {DecodeTarget(resolved)}; + device.ColorMetadataPass(pass); + const auto stored = ReadBack(device, resolved); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto expected = x < ScissorWidth ? 1.0f + (written[channel] - 1.0f) / static_cast(samples) : 1.0f; + Require(FromHalf(stored[layout.Offset(x, y) / 2u + channel]) == expected, "a multisampled DCC target recreated with another pipe-bank XOR did not start from its new fast clear"); + } + } + } + auto third = second; + third.dccAlphaOnMsb = true; + bool refused = false; + try { + Draw(device, third, false, Float32Export, Program(written, Float32Export, 1u), false, true); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("layout or metadata changed") != std::string_view::npos; + } + Require(refused, "a resident multisampled DCC target accepted a use with other DCC metadata"); + programCount = 0; + } for (const auto samples : {2u, 4u, 8u}) { if (selectedSamples != 0 && samples != selectedSamples) continue; for (const auto mode : {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u}) { From 45a2e3dd4a99e7a9c4d34c73c5ab17943c14992f Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Sun, 18 Oct 2026 23:35:50 +0300 Subject: [PATCH 030/167] fix(relinker): resume scans after undecodable bytes between PS4 unwind functions In PS4 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, such as a lone REX byte before a 15-byte NOP. The syscall and call-site scans decode the executable segment linearly, so such a slot failed the relink with "Cannot decode instruction". For images with a PT_SCE_DYNLIBDATA segment the scans now receive the unwind function ranges. Outside every range an undecodable byte is skipped and decoding resumes at the next byte, and a decoding that would cross into a range ends at its start, so gap bytes cannot change how a function is decoded. Inside a range a decode failure still fails the relink, and images without unwind ranges are scanned as before. Co-Authored-By: Claude Opus 5.5 --- core/relinker/CMakeLists.txt | 2 + .../include/codegen/IInstructionScanner.hpp | 2 +- .../codegen/src/InstructionScanner.cpp | 43 +++++++-- .../codegen/tests/Amd64OnlyConverterTests.cpp | 13 +++ core/relinker/domain/include/domain/Types.hpp | 6 ++ .../relinker/analysis/SceCodeExtent.hpp | 1 + .../relinker/domain/ICallSiteResolver.hpp | 3 +- .../relinker/domain/ISyscallScanner.hpp | 2 +- .../include/relinker/domain/Types.hpp | 1 + .../src/analysis/CallSiteResolver.cpp | 10 ++- .../relinker/src/analysis/SceCodeExtent.cpp | 8 ++ .../relinker/src/analysis/SyscallScanner.cpp | 8 +- .../relinker/src/guest/GuestModuleBuilder.cpp | 3 +- .../src/pipeline/RelinkerPipeline.cpp | 15 ++-- .../relinker/tests/test_sce_code_gaps.py | 88 +++++++++++++++++++ docs/dev/debt/relinker.md | 1 + 16 files changed, 182 insertions(+), 24 deletions(-) create mode 100644 core/relinker/relinker/tests/test_sce_code_gaps.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index fb33a63019..aba9318a4d 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -611,6 +611,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_relro.py $) add_test(NAME sce_module_code_extent COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_module_code_extent.py $) + add_test(NAME sce_code_gaps COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_code_gaps.py $) add_test(NAME call_registry COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_call_registry.py $) add_test(NAME jmprel_table_bounds COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/codegen/include/codegen/IInstructionScanner.hpp b/core/relinker/codegen/include/codegen/IInstructionScanner.hpp index 2ea39a4e4f..e5314b276a 100644 --- a/core/relinker/codegen/include/codegen/IInstructionScanner.hpp +++ b/core/relinker/codegen/include/codegen/IInstructionScanner.hpp @@ -17,7 +17,7 @@ public: virtual void VisitCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize, const std::function& visit) const = 0; }; -std::unique_ptr MakeInstructionScanner(std::function progress = {}); +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 328e1e532b..6a6c0018b1 100644 --- a/core/relinker/codegen/src/InstructionScanner.cpp +++ b/core/relinker/codegen/src/InstructionScanner.cpp @@ -8,28 +8,50 @@ 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; }); + std::vector merged; + for (const auto& range : ranges) { + if (!merged.empty() && range.Begin <= merged.back().End) + merged.back().End = std::max(merged.back().End, range.End); + else + merged.push_back(range); + } + return merged; +} + +} + class InstructionScanner : public IInstructionScanner { public: - explicit InstructionScanner(std::function progress) : progress(std::move(progress)) {} + InstructionScanner(std::function progress, std::vector unwindRanges) : progress(std::move(progress)), unwindRanges(MergeCodeRanges(std::move(unwindRanges))) {} [[nodiscard]] std::vector ScanCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize) const override; void VisitCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize, const std::function& visit) const override; private: std::function progress; + std::vector unwindRanges; }; template -void ScanCodeSection(const std::vector& codeSection, const Domain::FileByteOffset codeSectionOffset, const Domain::FileByteOffset codeSectionSize, const std::function& progress, TVisitor&& visit) { +void ScanCodeSection(const std::vector& codeSection, const Domain::FileByteOffset codeSectionOffset, const Domain::FileByteOffset codeSectionSize, const std::function& progress, const std::vector& unwindRanges, TVisitor&& visit) { if (codeSectionSize > codeSection.size()) throw CodegenException("Code section exceeds the input buffer", codeSectionOffset); if (codeSectionSize > std::numeric_limits::max() - codeSectionOffset) throw CodegenException("Code section offset overflow", codeSectionOffset); const auto limit = static_cast(codeSectionSize); const X64InstructionDecoder decoder; std::size_t i = 0; + std::size_t range = 0; if (progress) progress(0, limit); std::size_t lastReport = 0; while (i < limit) { + while (range < unwindRanges.size() && unwindRanges[range].End <= i) ++range; + const bool outsideUnwindRanges = !unwindRanges.empty() && (range == unwindRanges.size() || i < unwindRanges[range].Begin); + const std::size_t nextUnwindRange = outsideUnwindRanges && range < unwindRanges.size() ? unwindRanges[range].Begin : limit; const std::uint8_t* cursor = codeSection.data() + i; const std::size_t available = limit - i; @@ -38,7 +60,14 @@ void ScanCodeSection(const std::vector& codeSection, const Domain: length = decoder.Decode(cursor, available); } catch (const CodegenException& e) { if (std::all_of(cursor, cursor + available, [](const std::uint8_t b) { return b == 0; })) break; - throw CodegenException(std::string("Cannot decode instruction: ") + e.what(), codeSectionOffset + i); + if (!outsideUnwindRanges) throw CodegenException(std::string("Cannot decode instruction: ") + e.what(), codeSectionOffset + i); + ++i; + continue; + } + + if (length > nextUnwindRange - i) { + i = nextUnwindRange; + continue; } const InstructionMatch match{codeSectionOffset + i, length}; @@ -56,16 +85,16 @@ void ScanCodeSection(const std::vector& codeSection, const Domain: std::vector InstructionScanner::ScanCodeSection(const std::vector& codeSection, const Domain::FileByteOffset codeSectionOffset, const Domain::FileByteOffset codeSectionSize) const { std::vector matches; - Codegen::ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize, progress, [&](const InstructionMatch& match) { matches.push_back(match); }); + Codegen::ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize, progress, unwindRanges, [&](const InstructionMatch& match) { matches.push_back(match); }); return matches; } void InstructionScanner::VisitCodeSection(const std::vector& codeSection, const Domain::FileByteOffset codeSectionOffset, const Domain::FileByteOffset codeSectionSize, const std::function& visit) const { - Codegen::ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize, progress, visit); + Codegen::ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize, progress, unwindRanges, visit); } -std::unique_ptr MakeInstructionScanner(std::function progress) { - return std::make_unique(std::move(progress)); +std::unique_ptr MakeInstructionScanner(std::function progress, std::vector unwindRanges) { + return std::make_unique(std::move(progress), std::move(unwindRanges)); } } diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 89bc80f07d..23faa55e9b 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -1792,6 +1792,18 @@ void scannerZeroTail() { requireFailure([&] { (void)scanner->ScanCodeSection(truncated, 0, truncated.size()); }, "Truncated non-zero tail must still fail"); } +void scannerUnwindRanges() { + const Bytes slot{0x48, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x2E, 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC3}; + const auto gap = Codegen::MakeInstructionScanner({}, {{16, 17}})->ScanCodeSection(slot, 0x1000, slot.size()); + require(gap.size() == 2 && gap[0].Offset == 0x1001 && gap[0].Length == 15 && gap[1].Offset == 0x1010 && gap[1].Length == 1, "An undecodable byte outside every unwind range must resume the scan at the next byte"); + requireFailure([&] { (void)Codegen::MakeInstructionScanner({}, {{0, 17}})->ScanCodeSection(slot, 0, slot.size()); }, "An undecodable byte inside an unwind range must still fail"); + requireFailure([&] { (void)Codegen::MakeInstructionScanner()->ScanCodeSection(slot, 0, slot.size()); }, "An undecodable byte without unwind ranges must still fail"); + const Bytes crossing{0xB8, 0x0F, 0x05, 0xC3, 0x90}; + const auto cut = Codegen::MakeInstructionScanner({}, {{3, 4}, {1, 3}})->ScanCodeSection(crossing, 0, crossing.size()); + require(cut.size() == 3 && cut[0].Offset == 1 && cut[0].Length == 2 && cut[1].Offset == 3 && cut[2].Offset == 4, "A gap instruction that crosses into an unwind range must end at the range"); + require(Codegen::MakeInstructionScanner()->ScanCodeSection(crossing, 0, crossing.size()).front().Length == 5, "Without unwind ranges the crossing instruction must decode whole"); +} + int main() { try { decoderLengths(); @@ -1837,6 +1849,7 @@ int main() { linuxPlacement(); linuxRipRelativePlacement(); scannerZeroTail(); + scannerUnwindRanges(); std::cout << "AMD64-only converter tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/relinker/domain/include/domain/Types.hpp b/core/relinker/domain/include/domain/Types.hpp index f505333ec7..dd181853e0 100644 --- a/core/relinker/domain/include/domain/Types.hpp +++ b/core/relinker/domain/include/domain/Types.hpp @@ -16,6 +16,12 @@ using FileByteOffset = std::uint64_t; using VirtualAddress = std::uint64_t; using ByteCount = std::uint64_t; +struct CodeRange { + ByteCount Begin; + ByteCount End; + bool operator==(const CodeRange&) const = default; +}; + struct ElfHeader { std::uint16_t Machine; std::uint16_t Type; diff --git a/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp b/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp index c43fd18cd6..4e897a7a0b 100644 --- a/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp +++ b/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp @@ -8,6 +8,7 @@ namespace Relinker { std::optional FindSceCodeEnd(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions, VirtualAddress pltGot, ByteCount pltRelocationsSize); +std::vector SceUnwindRanges(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions); } diff --git a/core/relinker/relinker/include/relinker/domain/ICallSiteResolver.hpp b/core/relinker/relinker/include/relinker/domain/ICallSiteResolver.hpp index 93e53570cd..8a3e5df7dc 100644 --- a/core/relinker/relinker/include/relinker/domain/ICallSiteResolver.hpp +++ b/core/relinker/relinker/include/relinker/domain/ICallSiteResolver.hpp @@ -14,7 +14,8 @@ public: const std::vector& textSection, FileByteOffset textSectionVAddr, VirtualAddress targetGotOrPltAddress, - ByteCount targetGotOrPltSize + ByteCount targetGotOrPltSize, + const std::vector& unwindRanges = {} ) = 0; }; diff --git a/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp b/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp index 71a9160da0..a4adf4a77c 100644 --- a/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp +++ b/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp @@ -12,7 +12,7 @@ class ISyscallScanner { public: virtual ~ISyscallScanner() = default; - virtual std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input = {}) = 0; + virtual std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input = {}, const std::vector& unwindRanges = {}) = 0; }; } diff --git a/core/relinker/relinker/include/relinker/domain/Types.hpp b/core/relinker/relinker/include/relinker/domain/Types.hpp index 0cae3ed174..856d6a0154 100644 --- a/core/relinker/relinker/include/relinker/domain/Types.hpp +++ b/core/relinker/relinker/include/relinker/domain/Types.hpp @@ -8,6 +8,7 @@ namespace Relinker { using FileByteOffset = Domain::FileByteOffset; using VirtualAddress = Domain::VirtualAddress; using ByteCount = Domain::ByteCount; +using CodeRange = Domain::CodeRange; using ElfHeader = Domain::ElfHeader; using ProgramHeader = Domain::ProgramHeader; using SectionHeader = Domain::SectionHeader; diff --git a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp index 63a1b2450c..91fe531f9c 100644 --- a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp +++ b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp @@ -51,21 +51,22 @@ std::optional ripRelativeDispOffset(const std::vector class CallSiteResolver : public ICallSiteResolver { public: - std::vector ResolveCallSites(const std::vector& textSection, FileByteOffset textSectionVAddr, VirtualAddress targetGotOrPltAddress, ByteCount targetGotOrPltSize) override; + std::vector ResolveCallSites(const std::vector& textSection, FileByteOffset textSectionVAddr, VirtualAddress targetGotOrPltAddress, ByteCount targetGotOrPltSize, const std::vector& unwindRanges) override; private: const std::vector* _cachedTextPtr = nullptr; std::vector> _cachedTargetToInstr; FileByteOffset _cachedTextVAddr = 0; + std::vector _cachedUnwindRanges; }; -std::vector CallSiteResolver::ResolveCallSites(const std::vector& textSection, const FileByteOffset textSectionVAddr, const VirtualAddress targetGotOrPltAddress, const ByteCount targetGotOrPltSize) { +std::vector CallSiteResolver::ResolveCallSites(const std::vector& textSection, const FileByteOffset textSectionVAddr, const VirtualAddress targetGotOrPltAddress, const ByteCount targetGotOrPltSize, const std::vector& unwindRanges) { if (targetGotOrPltSize > std::numeric_limits::max() - targetGotOrPltAddress) throw RelinkerException("Relocation target range exceeds the address space"); if (textSection.size() > std::numeric_limits::max() - textSectionVAddr) throw RelinkerException("Text section range exceeds the address space"); if (textSection.empty()) return {}; - if (_cachedTextPtr != &textSection || _cachedTextVAddr != textSectionVAddr) { - auto scanner = Codegen::MakeInstructionScanner(); + if (_cachedTextPtr != &textSection || _cachedTextVAddr != textSectionVAddr || _cachedUnwindRanges != unwindRanges) { + auto scanner = Codegen::MakeInstructionScanner({}, unwindRanges); _cachedTargetToInstr.clear(); const auto candidateCount = std::count_if(textSection.begin(), textSection.end(), [](std::uint8_t b) { return b == OneByteGrp5Rm || b == OneByteModRmRangeJMax; @@ -83,6 +84,7 @@ std::vector CallSiteResolver::ResolveCallSites(const std::vector std::sort(_cachedTargetToInstr.begin(), _cachedTargetToInstr.end()); _cachedTextPtr = &textSection; _cachedTextVAddr = textSectionVAddr; + _cachedUnwindRanges = unwindRanges; } std::vector sites; diff --git a/core/relinker/relinker/src/analysis/SceCodeExtent.cpp b/core/relinker/relinker/src/analysis/SceCodeExtent.cpp index 106528e542..0c53fee8c8 100644 --- a/core/relinker/relinker/src/analysis/SceCodeExtent.cpp +++ b/core/relinker/relinker/src/analysis/SceCodeExtent.cpp @@ -37,4 +37,12 @@ std::optional FindSceCodeEnd(std::span segme return segmentVAddr + *plt + pltSize; } +std::vector SceUnwindRanges(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions) { + std::vector ranges; + for (const auto& function : functions) + if (function.Begin >= segmentVAddr && function.Begin - segmentVAddr < segment.size() && function.End > function.Begin) + ranges.push_back({function.Begin - segmentVAddr, std::min(function.End - segmentVAddr, segment.size())}); + return ranges; +} + } diff --git a/core/relinker/relinker/src/analysis/SyscallScanner.cpp b/core/relinker/relinker/src/analysis/SyscallScanner.cpp index 77ee6dd08c..86b3647863 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -173,19 +173,19 @@ class SyscallScanner : public ISyscallScanner { public: SyscallScanner(Domain::IProgress& progress, const bool trapSdkStubs) : progress(progress), trapSdkStubs(trapSdkStubs) {} - std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input) override; + std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input, const std::vector& unwindRanges) override; private: Domain::IProgress& progress; bool trapSdkStubs; }; -std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::vector& codeSection, const FileByteOffset codeSectionOffset, const FileByteOffset codeSectionSize, std::string_view input) { +std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::vector& codeSection, const FileByteOffset codeSectionOffset, const FileByteOffset codeSectionSize, std::string_view input, const std::vector& unwindRanges) { if (codeSectionSize > codeSection.size()) throw RelinkerException("Code section exceeds the input buffer", codeSectionOffset); const auto limit = static_cast(codeSectionSize); const std::string source(input); - const auto scanner = Codegen::MakeInstructionScanner([&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Decode, source, completed, total}); }); + const auto scanner = Codegen::MakeInstructionScanner([&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Decode, source, completed, total}); }, unwindRanges); RecentInstructions recent; std::vector trapped; scanner->VisitCodeSection(codeSection, codeSectionOffset, codeSectionSize, [&](const Codegen::InstructionMatch& match) { @@ -228,7 +228,7 @@ std::unique_ptr MakeSyscallScanner(Domain::IProgress& progress, class NullSyscallScanner : public ISyscallScanner { public: - std::vector ScanCodeSectionForSyscalls(const std::vector&, FileByteOffset, FileByteOffset, std::string_view) override { return {}; } + std::vector ScanCodeSectionForSyscalls(const std::vector&, FileByteOffset, FileByteOffset, std::string_view, const std::vector&) override { return {}; } }; std::unique_ptr MakeNullSyscallScanner() { diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 2d94dcf5dc..2a09e1a55e 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -345,7 +345,8 @@ 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); - for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, code.size(), Io::Utf8Path(image.SourcePath))) { + const auto unwindRanges = SceUnwindRanges(code, header.MappedAddress, unwindFunctions); + 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); image.Bytes[header.Offset + offset] = 0x0F; diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index cf7f7a80ae..51cfffb403 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -362,18 +362,22 @@ RelinkResult RelinkerPipeline::Relink() { for (const auto& ref : nidRefs) validationPolicy->ValidateRelocationTypeSupported(ref.RelocationTypeValue, ref.RelocationTableOffset); + std::vector> unwindRanges(executableSegments.size()); if (std::any_of(programHeaders.begin(), programHeaders.end(), [](const ProgramHeader& ph) { return ph.Type == PT_SCE_DYNLIBDATA; })) { const auto functions = UnusedNidFilter::ReadExceptionFunctions(raw, programHeaders, {}, {}); - for (auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { + for (std::size_t index = 0; index < executableSegments.size(); ++index) { + auto& [segment, segmentVAddr, segmentFileOffset] = executableSegments[index]; static_cast(segmentFileOffset); if (const auto codeEnd = FindSceCodeEnd(segment, segmentVAddr, functions, gotVAddr, dynJmpRelSize)) segment.resize(*codeEnd - segmentVAddr); + unwindRanges[index] = SceUnwindRanges(segment, segmentVAddr, functions); } } std::vector syscallPatches; - for (auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { - for (const auto offset : syscallScanner->ScanCodeSectionForSyscalls(segment, segmentVAddr, segment.size())) { + for (std::size_t index = 0; index < executableSegments.size(); ++index) { + auto& [segment, segmentVAddr, segmentFileOffset] = executableSegments[index]; + for (const auto offset : syscallScanner->ScanCodeSectionForSyscalls(segment, segmentVAddr, segment.size(), {}, unwindRanges[index])) { if (offset > segment.size() || segment.size() - offset < 2u) throw RelinkerException("Syscall trap patch exceeds executable segment", segmentVAddr + offset); segment[offset] = 0x0F; @@ -536,10 +540,11 @@ RelinkResult RelinkerPipeline::Relink() { entries.push_back(std::move(entry)); } - for (const auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { + for (std::size_t index = 0; index < executableSegments.size(); ++index) { + const auto& [segment, segmentVAddr, segmentFileOffset] = executableSegments[index]; static_cast(segmentFileOffset); for (auto& entry : entries) { - auto segmentSites = callSiteResolver->ResolveCallSites(segment, segmentVAddr, entry.TargetOffset, 8); + auto segmentSites = callSiteResolver->ResolveCallSites(segment, segmentVAddr, entry.TargetOffset, 8, unwindRanges[index]); entry.CallSites.insert(entry.CallSites.end(), segmentSites.begin(), segmentSites.end()); entry.CallSitesResolved = !entry.CallSites.empty(); } diff --git a/core/relinker/relinker/tests/test_sce_code_gaps.py b/core/relinker/relinker/tests/test_sce_code_gaps.py new file mode 100644 index 0000000000..5dc8164f13 --- /dev/null +++ b/core/relinker/relinker/tests/test_sce_code_gaps.py @@ -0,0 +1,88 @@ +from pathlib import Path +import json +import struct +import subprocess +import sys +import tempfile + +from test_guest_symbol_names import importing_executable +from test_sce_code_extent import FUNCTION, FUNCTION_SIZE, fixture +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") +GAP = FUNCTION + FUNCTION_SIZE +TARGETS = (("linux", []), ("windows", ["--windows"]), ("macos", ["--macos"])) + + +def executable(gap_bytes=b"", sce_block=True, function=b"", before_function=b""): + image, plt = fixture(sce_block=sce_block, gap=0x20) + image[GAP:GAP + len(gap_bytes)] = gap_bytes + image[FUNCTION:FUNCTION + len(function)] = function + image[FUNCTION - len(before_function):FUNCTION] = before_function + return image, plt + + +def module(slot_in_function): + image = sce_module() + text = memoryview(image)[0x400:0x500] + text[0:len(SLOT)] = SLOT + if not slot_in_function: + text[0x10:0x10 + FUNCTION_SIZE] = b"\xc3" + b"\x90" * (FUNCTION_SIZE - 1) + unwind_table(text, 0x1000, 0x1010, FUNCTION_SIZE, 0x1080, 0x10A0) + struct.pack_into(" Date: Sun, 18 Oct 2026 23:15:36 +0200 Subject: [PATCH 031/167] 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 0fe9f61d01a19b2e5e005e3187d021a4c29e4292 Mon Sep 17 00:00:00 2001 From: teepean Date: Fri, 16 Oct 2026 21:53:43 +0300 Subject: [PATCH 032/167] perf(agc): draws that only store to a buffer continue one render pass A guest buffer the recompiler classifies as only stored to (bufferWritten, not bufferRead, not bufferAtomic), which no other allocation of the draw overlaps, is a write-only range of the draw. It conflicts with every read and every other write of the open render pass, but not with the write-only stores of other draws to the same range: the guest GPU leaves stores of different draws unordered as well, since no barrier separates them. Before, such a buffer counted as both read and written, so consecutive draws storing to it each began a new pass with two full barriers. APS5_NO_WRITE_ONLY_PASSES=1 restores that. Unreal Engine 4 stores virtual texture feedback from every base pass material that samples a virtual texture into one buffer per frame: in Stray (PPSA02100) about 90 % of the pass restarts. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Graphics/include/PassHazards.hpp | 2 + .../Graphics/include/ShaderResources.hpp | 3 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 31 +++++++++- .../Graphics/src/PassHazards.cpp | 19 +++--- .../Graphics/src/ShaderResources.cpp | 10 +++ .../prx/libSceAgcDriver/tests/PassHazards.cpp | 61 ++++++++++++++++++- 6 files changed, 116 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp index 9ce1cb70fc..89ef9964e0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp @@ -22,6 +22,7 @@ struct PassAccess { std::span> attachments; bool anyReads = false; bool anyWrites = false; + GuestRangeList writeOnly = {}; }; enum class PassHazard : std::uint8_t { None, ReadAfterWrite, WriteAfterWrite, WriteAfterRead, Image, UnknownRead, UnknownWrite }; @@ -55,6 +56,7 @@ private: bool overlapsAny(const GuestRangeSet& set, GuestRangeList ranges) const; GuestRangeSet reads; GuestRangeSet writes; + GuestRangeSet writeOnly; std::vector> images; std::vector attachments; std::uint64_t pass = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index a0ffcb295a..dd5a7fad5f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -216,6 +216,7 @@ public: // The recorder's hazard tracker inputs (Recorder::NoteAccess): the written elements' guest // ranges, the regions read in place, and the storage images with whether each is written. const std::vector>& GpuWrites() const { return guestMemory.Writes(); } + const std::vector>& WriteOnlyRanges() const { return writeOnlyRanges; } std::vector> InPlaceReads() const { return guestMemory.InPlaceReads(); } std::vector> StorageImages() const; std::vector> DeviceReads() const { return guestMemory.DeviceReads(); } @@ -332,6 +333,7 @@ private: ShaderRecompiler::DescriptorRole role = ShaderRecompiler::DescriptorRole::ShaderData; // Guest buffers: whether the shader may store to the range (see addGuestBuffer). bool written = true; + bool writeOnly = false; // Data buffers: the words the buffer holds once the recorded work ran (see RefreshData). std::vector dataWords; std::uint32_t adjustment = 0; @@ -557,6 +559,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::vector> writeOnlyRanges; bool drawBuild = false; std::uint64_t dispatchThreads = 0; std::vector> textures; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 9aaa239642..e6ad04936c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -14,6 +14,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include "ThreadOwned.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "Optimization/include/Optimization/ShaderStageInputInfo.hpp" @@ -1754,6 +1755,11 @@ void captureInputs(const Context& context, Recorder& recorder, VkCommandBuffer c }); } +bool WriteOnlyPasses() { + static const bool enabled = std::getenv("APS5_NO_WRITE_ONLY_PASSES") == nullptr; + return enabled; +} + void recordDraw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, DrawInputs& inputs, RecordedDraw& record, DrawOutcome& outcome, DrawTimer& timer, double& ownWaitedMs) { auto* recorder = record.recorder; auto& resources = *record.resources; @@ -1790,6 +1796,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam const bool meshIndirect = state.stages.mesh && args != nullptr; const bool addressBased = resources.HoldsLease(); auto passReads = addressBased ? std::vector>{} : resources.DeviceReads(); + const auto deviceReads = passReads.size(); if (gpuIndirect) { passReads.emplace_back(args->arguments, args->arguments + args->RangeBytes()); if (args->countIndirect) passReads.emplace_back(args->countAddress, args->countAddress + 4); @@ -1805,7 +1812,29 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam const bool readOnlyBda = addressReadPasses && resources.BdaReadOnly(); outcome.addressDraw = addressBased; const bool unknownWrites = ((addressBased || resources.UsesAddressTable()) && !readOnlyBda) || resources.HasCopiedWrites(); - const PassAccess passAccess{passReads, resources.GpuWrites(), passImages, passAttachments, unknownMemory, unknownWrites}; + GuestRangeList passWrites = resources.GpuWrites(); + GuestRangeList passWriteOnly{}; + thread_local std::vector>* hazardWritesSlot = nullptr; + if (const auto& writeOnly = resources.WriteOnlyRanges(); !writeOnly.empty() && !unknownMemory && WriteOnlyPasses()) { + const auto listed = [&](const std::pair& range, bool aligned) { + return std::any_of(writeOnly.begin(), writeOnly.end(), [&](const auto& only) { + if (!aligned) return only == range; + const auto begin = only.first & ~std::uint64_t{3}; + const auto end = begin == only.first ? only.second : (only.second + 3) & ~std::uint64_t{3}; + return range.first == begin && range.second == end; + }); + }; + const auto ownReads = passReads.begin() + static_cast(deviceReads); + passReads.erase(std::remove_if(passReads.begin(), ownReads, [&](const auto& range) { return listed(range, true); }), ownReads); + auto& hazardWrites = ShaderRecompiler::ThreadOwned(hazardWritesSlot); + hazardWrites.clear(); + for (const auto& range : resources.GpuWrites()) { + if (!listed(range, false)) hazardWrites.push_back(range); + } + passWrites = hazardWrites; + passWriteOnly = writeOnly; + } + const PassAccess passAccess{passReads, passWrites, passImages, passAttachments, unknownMemory, unknownWrites, passWriteOnly}; const bool fenced = state.depth.has_value() || !record.proxies.empty(); const auto start = recorder->StartDrawPass(passKey, capture || readsTarget || gpuIndirect || meshIndirect, fenced, passAccess); const bool continued = start.continued; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PassHazards.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PassHazards.cpp index a3a4c97382..9e54e2d663 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PassHazards.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PassHazards.cpp @@ -34,15 +34,16 @@ bool PassHazards::overlapsAny(const GuestRangeSet& set, GuestRangeList ranges) c PassHazard PassHazards::Check(const PassAccess& access, bool samePass) const { const auto touchesMemory = [](const PassAccess& use) { const auto nonEmpty = [](GuestRangeList ranges) { return std::any_of(ranges.begin(), ranges.end(), [](const auto& range) { return range.first < range.second; }); }; - return use.anyReads || use.anyWrites || nonEmpty(use.reads) || nonEmpty(use.writes); + return use.anyReads || use.anyWrites || nonEmpty(use.reads) || nonEmpty(use.writes) || nonEmpty(use.writeOnly); }; - const bool earlierTouches = anyReads || anyWrites || !reads.Empty() || !writes.Empty(); + const bool earlierTouches = anyReads || anyWrites || !reads.Empty() || !writes.Empty() || !writeOnly.Empty(); if ((access.anyWrites && earlierTouches) || (anyWrites && touchesMemory(access))) return PassHazard::UnknownWrite; - const bool writesRanges = std::any_of(access.writes.begin(), access.writes.end(), [](const auto& range) { return range.first < range.second; }); - if ((access.anyReads && !writes.Empty()) || (anyReads && writesRanges)) return PassHazard::UnknownRead; - if (overlapsAny(writes, access.reads)) return PassHazard::ReadAfterWrite; - if (overlapsAny(writes, access.writes)) return PassHazard::WriteAfterWrite; - if (overlapsAny(reads, access.writes)) return PassHazard::WriteAfterRead; + const auto nonEmpty = [](GuestRangeList ranges) { return std::any_of(ranges.begin(), ranges.end(), [](const auto& range) { return range.first < range.second; }); }; + const bool writesRanges = nonEmpty(access.writes) || nonEmpty(access.writeOnly); + if ((access.anyReads && (!writes.Empty() || !writeOnly.Empty())) || (anyReads && writesRanges)) return PassHazard::UnknownRead; + if (overlapsAny(writes, access.reads) || overlapsAny(writeOnly, access.reads)) return PassHazard::ReadAfterWrite; + if (overlapsAny(writes, access.writes) || overlapsAny(writeOnly, access.writes) || overlapsAny(writes, access.writeOnly)) return PassHazard::WriteAfterWrite; + if (overlapsAny(reads, access.writes) || overlapsAny(reads, access.writeOnly)) return PassHazard::WriteAfterRead; const auto conflicts = [](bool earlier, bool later) { return earlier || later; }; for (const auto& [image, written] : access.images) { if (image == VK_NULL_HANDLE) continue; @@ -60,6 +61,7 @@ PassHazard PassHazards::Check(const PassAccess& access, bool samePass) const { void PassHazards::Add(const PassAccess& access) { for (const auto& [begin, end] : access.reads) reads.Insert(begin, end); for (const auto& [begin, end] : access.writes) writes.Insert(begin, end); + for (const auto& [begin, end] : access.writeOnly) writeOnly.Insert(begin, end); for (const auto& [image, written] : access.images) { if (image == VK_NULL_HANDLE) continue; const auto it = std::find_if(images.begin(), images.end(), [&](const auto& seen) { return seen.first == image; }); @@ -79,13 +81,14 @@ void PassHazards::Add(const PassAccess& access) { void PassHazards::Clear() { reads.Clear(); writes.Clear(); + writeOnly.Clear(); images.clear(); attachments.clear(); anyReads = anyWrites = false; } bool PassHazards::Empty() const { - return reads.Empty() && writes.Empty() && images.empty() && attachments.empty() && !anyReads && !anyWrites; + return reads.Empty() && writes.Empty() && writeOnly.Empty() && images.empty() && attachments.empty() && !anyReads && !anyWrites; } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 3f54d9efc0..4f9df12fdd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1821,6 +1821,8 @@ void ShaderResources::buildPrepare(std::span shaders, cons const bool atomic = element < binding.bufferAtomic.size() && binding.bufferAtomic[element]; const bool read = element < binding.bufferRead.size() && binding.bufferRead[element]; const auto index = addGuestBuffer(std::span(binding.guestDescriptor).subspan(static_cast(element) * 4, 4), target, indexAddress, indexBytes, written, atomic, read); + const bool classified = element < binding.bufferWritten.size() && element < binding.bufferRead.size() && element < binding.bufferAtomic.size(); + allocations[index].writeOnly = classified && allocations[index].guest && allocations[index].written && binding.bufferWritten[element] && !read && !atomic; const auto& push = shader.program->pushConstants; if (!push.empty()) { const auto position = shader.program->memoryOffsetDword * 4u + element; @@ -2012,6 +2014,14 @@ void ShaderResources::buildPrepare(std::span shaders, cons Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &_set), "vkAllocateDescriptorSets"); } } + writeOnlyRanges.clear(); + for (const auto& item : allocations) { + if (!item.writeOnly || item.size == 0) continue; + const auto begin = item.address; + const auto end = item.address + item.size; + const bool shared = std::any_of(allocations.begin(), allocations.end(), [&](const Allocation& other) { return !other.writeOnly && other.size != 0 && other.address < end && begin < other.address + other.size; }); + if (!shared) writeOnlyRanges.emplace_back(begin, end); + } timing.descriptorsMs = phase(BuildPhase::Descriptors); if (BuildProfiled()) { timing.prepareMs = std::chrono::duration(std::chrono::steady_clock::now() - stageStart).count(); diff --git a/core/libs/prx/libSceAgcDriver/tests/PassHazards.cpp b/core/libs/prx/libSceAgcDriver/tests/PassHazards.cpp index 7689e5d90b..21f2f16582 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PassHazards.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PassHazards.cpp @@ -34,8 +34,9 @@ struct Use { Images attachments; bool anyReads = false; bool anyWrites = false; + Ranges writeOnly; - PassAccess View() const { return {reads, writes, images, attachments, anyReads, anyWrites}; } + PassAccess View() const { return {reads, writes, images, attachments, anyReads, anyWrites, writeOnly}; } }; void testRangeSet() { @@ -184,12 +185,70 @@ void testAttachments() { } +void testWriteOnly() { + PassHazards hazards; + hazards.BeginPass(); + Use feedback; + feedback.writeOnly = {{0x1000, 0x8f80}}; + hazards.Add(feedback.View()); + Expect(hazards.Check(feedback.View(), true) == PassHazard::None, "draws only storing to the same buffer continue the pass"); + Use reader; + reader.reads = {{0x2000, 0x2100}}; + Expect(hazards.Check(reader.View(), true) == PassHazard::ReadAfterWrite, "a read of write-only stores of the pass"); + Use writer; + writer.writes = {{0x8000, 0x9000}}; + Expect(hazards.Check(writer.View(), true) == PassHazard::WriteAfterWrite, "a read-write use after write-only stores"); + Use unknownReader; + unknownReader.anyReads = true; + Expect(hazards.Check(unknownReader.View(), true) == PassHazard::UnknownRead, "unknown reads after write-only stores"); + Use elsewhere; + elsewhere.writeOnly = {{0x9000, 0xa000}}; + elsewhere.reads = {{0xa000, 0xb000}}; + Expect(hazards.Check(elsewhere.View(), true) == PassHazard::None, "write-only stores and reads elsewhere"); + hazards.Clear(); + hazards.BeginPass(); + Use readWrite; + readWrite.reads = {{0x1000, 0x2000}}; + readWrite.writes = {{0x1000, 0x2000}}; + hazards.Add(readWrite.View()); + Use storesOver; + storesOver.writeOnly = {{0x1800, 0x1900}}; + Expect(hazards.Check(storesOver.View(), true) == PassHazard::WriteAfterWrite, "write-only stores after a read-write use"); + hazards.Clear(); + hazards.BeginPass(); + Use readOnly; + readOnly.reads = {{0x1000, 0x2000}}; + hazards.Add(readOnly.View()); + Expect(hazards.Check(storesOver.View(), true) == PassHazard::WriteAfterRead, "write-only stores after a read"); + hazards.Clear(); + hazards.BeginPass(); + Use unknownWriter; + unknownWriter.anyWrites = true; + unknownWriter.anyReads = true; + hazards.Add(feedback.View()); + Expect(hazards.Check(unknownWriter.View(), true) == PassHazard::UnknownWrite, "unknown writes after write-only stores"); + hazards.Clear(); + hazards.Add(unknownWriter.View()); + Expect(hazards.Check(feedback.View(), true) == PassHazard::UnknownWrite, "write-only stores after unknown writes"); + hazards.Clear(); + Use unknownRead; + unknownRead.anyReads = true; + hazards.Add(unknownRead.View()); + Expect(hazards.Check(feedback.View(), true) == PassHazard::UnknownRead, "write-only stores after unknown reads"); + hazards.Clear(); + hazards.Add(feedback.View()); + Expect(!hazards.Empty(), "write-only stores are tracked"); + hazards.Clear(); + Expect(hazards.Empty(), "cleared write-only stores"); +} + int main() { testRangeSet(); testRanges(); testUnknown(); testImages(); testAttachments(); + testWriteOnly(); if (failures != 0) { std::fprintf(stderr, "%d pass hazard checks failed\n", failures); return 1; From 12d720f7437ba16ce7dfb852d1cbbf6f35b0b3de Mon Sep 17 00:00:00 2001 From: teepean Date: Sun, 18 Oct 2026 05:35:58 +0300 Subject: [PATCH 033/167] fix(agc): seal a pass's write-only stores at guest flushes Queue 0 skips partial and cache flush EVENT_WRITE packets and ACQUIRE_MEM, and nothing ends the open render pass on them, so the pass hazard tracker is all that orders two draws inside one pass. A write-only store, a flush and a second write-only store to the same bytes stayed in one pass with no barrier between them, although the flush orders them on the guest GPU. At such a packet, the write-only ranges of the open pass now count as written, so a later write-only store to them is a WriteAfterWrite and begins a new pass. The seal is queued behind the draws in the draw pipeline (or made at once when it is idle), once per run of flushes after a draw. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Execution/include/VulkanDevice.hpp | 1 + .../src/Driver/Packets/PacketExecution.cpp | 26 +++++++++++++++++++ .../Execution/src/VulkanDevice.cpp | 4 +++ .../Graphics/include/PassHazards.hpp | 2 ++ .../Graphics/include/Recorder.hpp | 1 + .../Graphics/src/PassHazards.cpp | 5 ++++ .../libSceAgcDriver/Graphics/src/Recorder.cpp | 4 +++ .../prx/libSceAgcDriver/tests/PassHazards.cpp | 12 +++++++++ 8 files changed, 55 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index cc6d4a0e28..90db9196ce 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -103,6 +103,7 @@ public: // Pending-label table lookup and open-batch overlap test for WAIT_REG_MEM (see Recorder). std::optional PendingLabel(std::uint64_t address, std::size_t bytes, std::uint64_t afterStamp, Graphics::Recorder::LabelRefusal* refusal = nullptr) const; bool OpenWriteOverlaps(std::uint64_t address, std::size_t bytes) const; + void SealPassWriteOnly(); bool RecordedWritesSettled(std::uint64_t address, std::size_t bytes) const; bool StoresPendingOver(std::uint64_t address, std::size_t bytes) const; // Fills [address, address + bytes) of host-imported guest memory with a repeating 16-byte pattern, diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 0597878160..d448b1b279 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -14,6 +14,7 @@ #include #include #include +#include namespace AgcDriver::DriverDetail { @@ -59,6 +60,20 @@ bool keepsDrawPipeline(std::span packet, std::uint32_t head } } +bool sealsWriteOnly(std::span packet, std::uint32_t opcode) { + if (opcode == 0x58) return true; + if (opcode != 0x46 || packet.size() < 2) return false; + switch (packet[1] & 0x3fu) { + case 0x07: case 0x0f: case 0x10: case 0x16: case 0x2a: case 0x2c: case 0x2e: case 0x31: return true; + default: return false; + } +} + +bool& drawnSinceSeal() { + static thread_local bool drawn = false; + return drawn; +} + } void Driver::deliverGuestEopInterrupt(std::uint32_t queue) { @@ -384,6 +399,7 @@ void Driver::execute(const Submission& submission) { } else if (drawPacket) { landPendingWaits(submission.queue); packetCounters.CountDraw(); + drawnSinceSeal() = true; bool drawn = false; timed(&WorkerProfile::drawMs, [&] { static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr; @@ -426,6 +442,16 @@ void Driver::execute(const Submission& submission) { Pm4::Execute(packet, queue); } else if (sampleDump && !wroteOnGpu) { dumpSampleCounters(Pm4::SampleDumpAddress(packet)); + } else if (submission.queue == 0 && sealsWriteOnly(packet, opcode)) { + if (std::exchange(drawnSinceSeal(), false)) { + const auto seal = [this] { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + if (const auto localDevice = device.Load()) localDevice->SealPassWriteOnly(); + }; + if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Enqueue(seal, {}); + else seal(); + } } else if (opcode != 0x42 && opcode != 0x46 && opcode != 0x58) { if (!wroteOnGpu) { if (Pm4::IndirectRegisterOpcode(opcode)) Pm4::ExecuteIndirectRegisters(packet, submission.registerLists.at(cursor), queue); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 24bff306e4..d8a5ff952c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2167,6 +2167,10 @@ bool VulkanDevice::OpenWriteOverlaps(std::uint64_t address, std::size_t bytes) c return state->recorder && state->recorder->OpenWriteOverlaps(address, bytes); } +void VulkanDevice::SealPassWriteOnly() { + if (state->recorder) state->recorder->SealPassWriteOnly(); +} + bool VulkanDevice::RecordedWritesSettled(std::uint64_t address, std::size_t bytes) const { if (!state->recorder) return true; const auto& recorder = *state->recorder; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp index 89ef9964e0..298ca9c49e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp @@ -33,6 +33,7 @@ public: bool Overlaps(std::uint64_t begin, std::uint64_t end) const; bool Empty() const { return ranges.empty(); } std::size_t Size() const { return ranges.size(); } + GuestRangeList Ranges() const { return ranges; } void Clear() { ranges.clear(); } private: @@ -44,6 +45,7 @@ public: PassHazard Check(const PassAccess& access, bool samePass) const; void Add(const PassAccess& access); void BeginPass() { ++pass; } + void SealWriteOnly(); void Clear(); bool Empty() const; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index f62ba12556..a199bf888c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -68,6 +68,7 @@ public: }; DrawPassStart StartDrawPass(std::uint64_t key, bool forced, bool fenced, const PassAccess& access); void LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool fenced, const PassAccess& access, std::function afterPass = {}); + void SealPassWriteOnly(); // DCC "uncompressed" key stores (DccMetadata.cpp StoreUncompressedOnGpu): queued on the open // batch and recorded as one run (one barrier pair for every queued fill) at Submit, before a // label store (RecordStore), or before a command that writes or reads a queued range (the diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PassHazards.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PassHazards.cpp index 9e54e2d663..58673799a3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PassHazards.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PassHazards.cpp @@ -78,6 +78,11 @@ void PassHazards::Add(const PassAccess& access) { anyWrites = anyWrites || access.anyWrites; } +void PassHazards::SealWriteOnly() { + for (const auto& [begin, end] : writeOnly.Ranges()) writes.Insert(begin, end); + writeOnly.Clear(); +} + void PassHazards::Clear() { reads.Clear(); writes.Clear(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 43b90a9f1c..fe8ff6f7d1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1409,6 +1409,10 @@ void Recorder::LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool open->passHazards.Add(access); } +void Recorder::SealPassWriteOnly() { + if (open != nullptr) open->passHazards.SealWriteOnly(); +} + void Recorder::endOpenRenderPass(bool barrier) { auto& pass = open->renderPass; if (open->segmentActive) endSegment(*open); diff --git a/core/libs/prx/libSceAgcDriver/tests/PassHazards.cpp b/core/libs/prx/libSceAgcDriver/tests/PassHazards.cpp index 21f2f16582..25332fe672 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PassHazards.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PassHazards.cpp @@ -236,6 +236,18 @@ void testWriteOnly() { hazards.Add(unknownRead.View()); Expect(hazards.Check(feedback.View(), true) == PassHazard::UnknownRead, "write-only stores after unknown reads"); hazards.Clear(); + hazards.BeginPass(); + hazards.Add(feedback.View()); + hazards.SealWriteOnly(); + Expect(hazards.Check(feedback.View(), true) == PassHazard::WriteAfterWrite, "write-only stores over stores sealed by a flush"); + Use afterSeal; + afterSeal.writeOnly = {{0x9000, 0xa000}}; + Expect(hazards.Check(afterSeal.View(), true) == PassHazard::None, "write-only stores elsewhere after a seal"); + hazards.Add(afterSeal.View()); + Expect(hazards.Check(afterSeal.View(), true) == PassHazard::None, "write-only stores after a seal continue the pass among themselves"); + hazards.Clear(); + Expect(hazards.Check(feedback.View(), true) == PassHazard::None, "a cleared seal"); + hazards.Clear(); hazards.Add(feedback.View()); Expect(!hazards.Empty(), "write-only stores are tracked"); hazards.Clear(); From d840b10d2fd308a73efba15ac59fec5de04195b4 Mon Sep 17 00:00:00 2001 From: teepean Date: Mon, 19 Oct 2026 00:55:02 +0300 Subject: [PATCH 034/167] docs(agc): list APS5_NO_WRITE_ONLY_PASSES in the environment reference Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/dev/ENVIRONMENT.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index b921d608dc..f51ffca0ea 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -304,6 +304,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_WORKER_AFFINITY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp` | | leaves the queue workers and presenter unpinned (affinity mask 0) | | `APS5_NO_WRITER_KEYED_EVIDENCE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | records written-element evidence without keying it by writer | | `APS5_NO_WRITE_EVIDENCE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | disables the written-element evidence that lets a read skip the flush hook | +| `APS5_NO_WRITE_ONLY_PASSES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | counts a buffer a draw only stores to as read and written, so the next draw storing to it begins a new render pass | | `APS5_NO_WRITTEN_SHADOW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | binds written staging elements in place instead of through the written shadow | | `APS5_PACKET_EPOCH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitStatistics.cpp` | | makes queue workers bump the collect epoch before every packet | | `APS5_PIPELINED_DRAWS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp` | 8 | depth of the queue-0 draw pipeline: 0 records draws inline, 1 selects the default depth of 8, and any other value is the depth capped at 256 | From b0a7695b57cd4f882ad4a6aad5158632b51bbfc2 Mon Sep 17 00:00:00 2001 From: gohansharma79-svg <240928239+gohansharma79-svg@users.noreply.github.com> Date: Mon, 19 Oct 2026 03:45:14 +0530 Subject: [PATCH 035/167] fix(avplayer): deliver the first paused picture after audio starts --- core/libs/prx/libSceAvPlayer/CMakeLists.txt | 7 ++++++ core/libs/prx/libSceAvPlayer/src/Source.cpp | 2 +- core/libs/tests/GuestAvPlayer.cpp | 27 +++++++++++++++++++++ docs/dev/debt/libSceAvPlayer.md | 2 ++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAvPlayer/CMakeLists.txt b/core/libs/prx/libSceAvPlayer/CMakeLists.txt index 0acb2eeeb0..d8a652d7ff 100644 --- a/core/libs/prx/libSceAvPlayer/CMakeLists.txt +++ b/core/libs/prx/libSceAvPlayer/CMakeLists.txt @@ -12,6 +12,11 @@ if(BUILD_TESTING) file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/guest_avplayer) add_test(NAME guest_avplayer COMMAND guest_avplayer_tests) add_test(NAME guest_avplayer_callback_close COMMAND guest_avplayer_tests --callback-close) + add_test(NAME guest_avplayer_paused_first_video COMMAND guest_avplayer_tests --paused-first-video) + set_tests_properties(guest_avplayer_paused_first_video PROPERTIES + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/guest_avplayer_paused_first_video + TIMEOUT 20) + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/guest_avplayer_paused_first_video) set_tests_properties(guest_avplayer PROPERTIES WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/guest_avplayer TIMEOUT 60) @@ -21,6 +26,8 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(guest_avplayer PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(guest_avplayer_paused_first_video PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") set_tests_properties(guest_avplayer_callback_close PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libSceAvPlayer/src/Source.cpp b/core/libs/prx/libSceAvPlayer/src/Source.cpp index b9c1e162f3..466b6ceae8 100644 --- a/core/libs/prx/libSceAvPlayer/src/Source.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Source.cpp @@ -507,7 +507,7 @@ private: while (!frames.empty() && frames.front().epoch < stale) recycleFront(video); if (frames.empty()) return false; const auto& front = frames.front(); - if (paused && front.epoch <= clockEpoch) return false; + if (paused && presented && front.epoch <= clockEpoch) return false; if (front.epoch > clockEpoch) { if (synced && audioDriving && speed == NormalSpeed) return false; if (synced && front.seamless && presented && presentationClock.Now() < static_cast(lastPresented + frameDuration())) return false; diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index 5e4b500e60..d4f5b46f38 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -1262,6 +1262,26 @@ void TestRestartAfterPausedJumpKeepsClockRunning() { Check(sceAvPlayerClose(player) == 0, "close failed"); } +void TestPausedPreparationAfterAudioStarts(std::uint32_t syncMode) { + Events events; + auto init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source failed"); + Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "enable video failed"); + Check(sceAvPlayerEnableStream(player, EnglishAudioStream) == 0, "enable audio failed"); + Check(sceAvPlayerSetAvSyncMode(player, syncMode) == 0, "sync mode failed"); + Check(sceAvPlayerStart(player) == 0, "start failed"); + AvPlayerFrameInfo audio{}; + Check(WaitFor([&] { return sceAvPlayerGetAudioData(player, &audio) != 0; }), "no initial audio frame"); + Check(sceAvPlayerPause(player) == 0, "pause failed"); + AvPlayerFrameInfoEx frame{}; + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "first video frame blocked during paused preparation after audio started"); + CheckVideoFrame(frame); + Check(!sceAvPlayerGetVideoDataEx(player, &frame), "paused preparation delivered more than the initial video frame"); + Check(sceAvPlayerClose(player) == 0, "close failed"); +} + void TestUnsyncedVideoKeepsUpWithAudio() { Events events; AvPlayerInitData init = InitData(&events); @@ -1561,6 +1581,13 @@ int main(int argc, char** argv) { std::puts("AvPlayer callback close tests passed"); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--paused-first-video") == 0) { + WriteMovie(); + TestPausedPreparationAfterAudioStarts(0); + TestPausedPreparationAfterAudioStarts(1); + std::puts("AvPlayer paused first video tests passed"); + return 0; + } Check(argc == 1, "unknown AvPlayer test arguments"); WriteMovie(); TestLoopWarningAtPlayback(); diff --git a/docs/dev/debt/libSceAvPlayer.md b/docs/dev/debt/libSceAvPlayer.md index 148f5db253..eeb178edd4 100644 --- a/docs/dev/debt/libSceAvPlayer.md +++ b/docs/dev/debt/libSceAvPlayer.md @@ -7,6 +7,8 @@ ### Unknown function info +- [Paused initial video](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) keeps the first picture available when audio has already established the playback epoch before the caller pauses. Further pictures of that epoch stay blocked, as after a paused jump. The console's exact initial preparation ordering has not been measured. + - [AvPlayer loop warnings](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) are queued when the first audio or video frame of a loop is delivered, following [KytyPS5's playback boundary tracking](https://github.com/KytyPS5/KytyPS5/blob/a6f4e8e7f456b8697d3102823d3f16400c3e121e/src/libs/avPlayer.cpp#L1038-L1043). The console's exact callback timing is unverified. Decode-ahead does not announce a loop while the caller is consuming the previous pass. - [sceAvPlayerAddSource](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - requests up to 4 additional decode-ahead video framebuffers from the memory replacement beyond `num_output_video_framebuffers` when memory is available; the console's internal buffering behaviour is unknown From a7ab0a0b9b953ae78d91ad3716c3c18d0d09d0b4 Mon Sep 17 00:00:00 2001 From: gohansharma79-svg <240928239+gohansharma79-svg@users.noreply.github.com> Date: Mon, 19 Oct 2026 03:51:36 +0530 Subject: [PATCH 036/167] docs(avplayer): avoid the pending metadata note conflict --- docs/dev/debt/libSceAvPlayer.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceAvPlayer.md b/docs/dev/debt/libSceAvPlayer.md index eeb178edd4..2874d437d3 100644 --- a/docs/dev/debt/libSceAvPlayer.md +++ b/docs/dev/debt/libSceAvPlayer.md @@ -7,8 +7,6 @@ ### Unknown function info -- [Paused initial video](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) keeps the first picture available when audio has already established the playback epoch before the caller pauses. Further pictures of that epoch stay blocked, as after a paused jump. The console's exact initial preparation ordering has not been measured. - - [AvPlayer loop warnings](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) are queued when the first audio or video frame of a loop is delivered, following [KytyPS5's playback boundary tracking](https://github.com/KytyPS5/KytyPS5/blob/a6f4e8e7f456b8697d3102823d3f16400c3e121e/src/libs/avPlayer.cpp#L1038-L1043). The console's exact callback timing is unverified. Decode-ahead does not announce a loop while the caller is consuming the previous pass. - [sceAvPlayerAddSource](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - requests up to 4 additional decode-ahead video framebuffers from the memory replacement beyond `num_output_video_framebuffers` when memory is available; the console's internal buffering behaviour is unknown @@ -17,6 +15,8 @@ - [sceAvPlayerInitEx](../../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - the PS5 init data layout (`this_size` 0x230: auto start at 0x74, nine 0x30-byte thread parameter blocks from 0x78, output video frame buffer count at 0x228) follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/avPlayer.cpp), [prosper](https://github.com/mattias800/prosper/blob/main/prosper/src/hle/video/avplayer.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Media/AvPlayer.cs) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs), which agree on it. PPSA25063 fills the same fields, writes auto start 0 and starts playback with `sceAvPlayerStart`; a title that sets auto start has not been seen. A smaller `this_size` is read with the PS4 offsets. The thread parameters are ignored - [AvPlayer auto start](../../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - the auto start that `sceAvPlayerAddSource` queues for a player with auto start or without an event callback starts the player only while it is still ready: a title that starts the player itself before the auto start runs keeps its playback and the frames it holds. Not confirmed on a title. +- [Paused initial video](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) keeps the first picture available when audio has already established the playback epoch before the caller pauses. Further pictures of that epoch stay blocked, as after a paused jump. The console's exact initial preparation ordering has not been measured. + ### Functional - [sceAvPlayerGetVideoData](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) in sync mode `SCE_AVPLAYER_AV_SYNC_MODE_NONE` skips queued frames that the audio clock has already passed while the title pulls audio, so a title that requests video slower than the film's frame rate keeps the picture with the sound. Whether the console hands out every frame in this mode is unknown. PPSA12544 requests 7-19 frames per second for a 30 fps cinematic From 15649b702fe569dcb947ea576f16cbd5212678e9 Mon Sep 17 00:00:00 2001 From: Hannah Lyons <7751607+babyhuey@users.noreply.github.com> Date: Sun, 18 Oct 2026 09:33:10 -0400 Subject: [PATCH 037/167] feat(libkernel_sys): implement fstatat --- .../File/include/DirectoryDescriptor.hpp | 3 ++ core/libs/prx/libkernel/File/src/Open.cpp | 30 ++++++++++++++----- core/libs/prx/libkernel_sys/Export.cpp | 13 ++++++++ core/libs/tests/GuestKernelSys.cpp | 25 ++++++++++++++++ docs/dev/debt/libkernel_sys.md | 1 + 5 files changed, 64 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp index f6ab25f51a..99e1104145 100644 --- a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -5,8 +5,11 @@ #include #include +struct FileStat; + extern "C" const char* GetDirectoryDescriptorPath_nid_no_patch(int fd, int* error); extern "C" int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std::uint16_t mode); +extern "C" int StatInDirectory_nid_no_patch(int directory, const char* path, FileStat* sb, bool follow); namespace File { diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 47217858f2..b3ea220255 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -339,16 +339,12 @@ std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { constexpr int GuestEfault = 14; -int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { - if (path == nullptr) return SceErrorFromErrno(GuestEfault); - if (*path == '\0') return SceErrorFromErrno(2); - std::filesystem::path native; - if (const int error = ResolvePathError_nid_no_patch(path, native, true, true)) return SceKernelError(error); +static int StatNative(const std::filesystem::path& native, bool directory, FileStat* sb) { std::error_code error; constexpr int GuestEnotdir = 20; FileStat archived{}; if (File::FillArchivedStat(native, &archived)) { - if (std::string_view(path).back() == '/' && (archived.st_mode & 0170000) != 0040000) return SceErrorFromErrno(GuestEnotdir); + if (directory && (archived.st_mode & 0170000) != 0040000) return SceErrorFromErrno(GuestEnotdir); if (sb == nullptr) return SceErrorFromErrno(GuestEfault); *sb = archived; return 0; @@ -363,13 +359,31 @@ int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { if (!std::filesystem::exists(status)) { return SceErrorFromErrno(2); } - const std::string_view guestPath(path); - if (!guestPath.empty() && guestPath.back() == '/' && !std::filesystem::is_directory(status)) return SceErrorFromErrno(GuestEnotdir); + if (directory && !std::filesystem::is_directory(status)) return SceErrorFromErrno(GuestEnotdir); if (sb == nullptr) return SceErrorFromErrno(GuestEfault); File::FillFileStat(metadataPath, sb); return 0; } +int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { + if (path == nullptr) return SceErrorFromErrno(GuestEfault); + if (*path == '\0') return SceErrorFromErrno(2); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native, true, true)) return SceKernelError(error); + return StatNative(native, std::string_view(path).back() == '/', sb); +} + +int StatInDirectory_nid_no_patch(int directory, const char* path, FileStat* sb, bool follow) { + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(directory, &error); + if (base == nullptr) return SceErrorFromErrno(error); + const auto native = std::filesystem::path(reinterpret_cast(base)) / std::filesystem::path(reinterpret_cast(path)); + if (follow) return StatNative(native, std::string_view(path).back() == '/', sb); + if (sb == nullptr) return SceErrorFromErrno(GuestEfault); + if (!File::FillLinkStat(native, sb)) return SceErrorFromErrno(errno); + return 0; +} + int APS5_VABI sceKernelUnlink(const char* path) { if (path == nullptr) return SceErrorFromErrno(GuestEfault); if (*path == '\0') return SceErrorFromErrno(2); diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index a8746955a5..3e11501482 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -536,6 +536,19 @@ int APS5_VABI fchown_nid_postfix(int fd, std::uint32_t uid, std::uint32_t gid) { return 0; } +int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb); +int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb); + +int APS5_VABI fstatat_nid_postfix(int fd, const char* path, FileStat* sb, int flag) { + if (flag & ~GuestAtSymlinkNofollow) return static_cast(Fail(GuestEinval)); + if (path == nullptr) return static_cast(Fail(GuestEfault)); + if (*path == '\0') return static_cast(Fail(GuestEnoent)); + if (fd == GuestAtFdcwd || *path == '/') return flag ? lstat_nid_postfix(path, sb) : stat_nid_postfix(path, sb); + const int result = StatInDirectory_nid_no_patch(fd, path, sb, flag == 0); + if (result < 0) return static_cast(Fail(result & 0xffff)); + return 0; +} + int APS5_VABI unlink_nid_postfix(const char* path); int APS5_VABI rmdir_nid_postfix(const char* path); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 14e3137646..cc5ed2cdfc 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -1,3 +1,4 @@ +#include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "SceTypes.hpp" @@ -54,6 +55,7 @@ int APS5_VABI mkdirat_nid_postfix(int, const char*, std::uint16_t); int APS5_VABI renameat_nid_postfix(int, const char*, int, const char*); int APS5_VABI fchmodat_nid_postfix(int, const char*, int, int); int APS5_VABI fchown_nid_postfix(int, std::uint32_t, std::uint32_t); +int APS5_VABI fstatat_nid_postfix(int, const char*, FileStat*, int); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t); int* APS5_VABI __error_nid_postfix(); @@ -576,6 +578,29 @@ int main() { fchownThrew = false; try { fchown_nid_postfix(base, 0, 1); } catch (const std::exception&) { fchownThrew = true; } Require(fchownThrew); + FileStat stat{}; + Require(fstatat_nid_postfix(base, "file.txt", &stat, 0) == 0 && (stat.st_mode & 0170000) == 0100000 && stat.st_size == 1); + Require(fstatat_nid_postfix(base, "other_dir", &stat, 0) == 0 && (stat.st_mode & 0170000) == 0040000); + Require(fstatat_nid_postfix(base, "other_dir/", &stat, 0) == 0 && (stat.st_mode & 0170000) == 0040000); + Require(fstatat_nid_postfix(base, "file.txt/", &stat, 0) == -1 && Error() == 20); + Require(fstatat_nid_postfix(base, "file.txt", &stat, 0x0200) == 0 && (stat.st_mode & 0170000) == 0100000); + Require(fstatat_nid_postfix(-100, file.c_str(), &stat, 0) == 0 && stat.st_size == 1); + Require(fstatat_nid_postfix(-1, guestFile.c_str(), &stat, 0) == 0 && stat.st_size == 1); + Require(fstatat_nid_postfix(base, "missing.bin", &stat, 0) == -1 && Error() == 2); + Require(fstatat_nid_postfix(base, "missing.bin", &stat, 0x0200) == -1 && Error() == 2); + Require(fstatat_nid_postfix(base, nullptr, &stat, 0) == -1 && Error() == 14); + Require(fstatat_nid_postfix(base, "", &stat, 0) == -1 && Error() == 2); + Require(fstatat_nid_postfix(base, "file.txt", &stat, 0x4000) == -1 && Error() == 22); + Require(fstatat_nid_postfix(-1, "file.txt", &stat, 0) == -1 && Error() == 9); + const int notDirectory = sceKernelOpen(file.c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(notDirectory >= 0); + Require(fstatat_nid_postfix(notDirectory, "file.txt", &stat, 0) == -1 && Error() == 20); + Require(sceKernelClose(notDirectory) == 0); +#ifndef _WIN32 + Require(symlink_nid_postfix("file.txt", (root / "link.txt").string().c_str()) == 0); + Require(fstatat_nid_postfix(base, "link.txt", &stat, 0) == 0 && (stat.st_mode & 0170000) == 0100000 && stat.st_size == 1); + Require(fstatat_nid_postfix(base, "link.txt", &stat, 0x0200) == 0 && (stat.st_mode & 0170000) == 0120000); +#endif Require(sceKernelClose(base) == 0); #ifdef _WIN32 const auto junctionTarget = root / "junction-target"; diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 2c7e801edc..efb75bb8d5 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -12,6 +12,7 @@ - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified - [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, stays a throw, because the name left after the prefix is not a drive path. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link - [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified +- [fstatat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `stat`, or to `lstat` with `AT_SYMLINK_NOFOLLOW` (0x200). Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory, and flags other than `AT_SYMLINK_NOFOLLOW` return `EINVAL`. PPSA74126 imports it. Console behaviour is unverified - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified - [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [fork](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask From 0500362ff289ad9e1d6c392ed0d1fe8d814a0bbe Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Sat, 17 Oct 2026 21:26:05 +0530 Subject: [PATCH 038/167] 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 039/167] 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 41c75d265879667b97f9ec58328405ee3312d655 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 19 Oct 2026 02:54:37 +0000 Subject: [PATCH 040/167] fix(libSceFont): say once where system fonts were looked for when a set has none Without system font files, sceFontOpenFontSet returns SCE_FONT_ERROR_FONT_OPEN_FAILED and logs `file=(none)` for each call. Some titles then fail far from the cause: Astro's Playroom (PPSA01325) lays out text with the failed set and stops on an out-of-heap or invalid texture assertion (#7714). The first such open now prints the font set and the directory searched to stderr, with how to supply the fonts. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceFont/src/Open.cpp | 9 +++++ core/libs/tests/GuestSystemFont.cpp | 50 +++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Open.cpp b/core/libs/prx/libSceFont/src/Open.cpp index ef118bd83e..8e21cdb937 100644 --- a/core/libs/prx/libSceFont/src/Open.cpp +++ b/core/libs/prx/libSceFont/src/Open.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include #include #include @@ -227,6 +228,13 @@ std::shared_ptr> SystemFontBytes(const std::fil return bytes; } +void WarnMissingSystemFont(std::uint32_t fontSetType) { + static std::once_flag warned; + std::call_once(warned, [&] { + std::fprintf(stderr, "[libSceFont] no file for system font set 0x%08X in %s. Titles that lay out text in system fonts can fail far from here without them: put the console's fonts or the Noto substitutes from docs/user/USAGE.md there, or set ANYPS5_SYSTEM_FONTS to their directory.\n", fontSetType, SystemFontDirectory().string().c_str()); + }); +} + void CopyStyleToCache(FontHandleNative* font) { std::memcpy(&font->cached_style.state, &font->style, sizeof(StyleStateBlock)); } @@ -407,6 +415,7 @@ int APS5_VABI sceFontOpenFontSet(FontLibrary library, std::uint32_t fontSetType, if (!pFontHandle) return SCE_FONT_ERROR_INVALID_PARAMETER; const std::optional file = FindSystemFontFile(fontSetType); APS5_LOG_OUT("fontSetType=0x%08X file=%s", fontSetType, file ? file->path.string().c_str() : "(none)"); + if (!file && IsSystemFontSet(fontSetType)) WarnMissingSystemFont(fontSetType); std::uint32_t libraryLock = 0; if (!AcquireLibraryLock(lib, libraryLock)) { *pFontHandle = nullptr; diff --git a/core/libs/tests/GuestSystemFont.cpp b/core/libs/tests/GuestSystemFont.cpp index ec2f807855..5f374153a7 100644 --- a/core/libs/tests/GuestSystemFont.cpp +++ b/core/libs/tests/GuestSystemFont.cpp @@ -5,11 +5,17 @@ #include #include #include +#include #include #include #include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif extern "C" { int APS5_VABI sceFontMemoryInit(FontMemory*, void*, std::uint32_t, const FontMemoryInterface*, void*, FontMemoryDestroyFunction, void*); @@ -118,6 +124,41 @@ static void SetFontDirectory(const std::filesystem::path& directory) { #endif } +template +static std::string CaptureStderr(const std::filesystem::path& path, TRun run) { + std::fflush(stderr); +#ifdef _WIN32 + const int saved = _dup(2); +#else + const int saved = dup(2); +#endif + std::FILE* file = std::fopen(path.string().c_str(), "wb"); + Require(saved >= 0 && file != nullptr); +#ifdef _WIN32 + Require(_dup2(_fileno(file), 2) == 0); +#else + Require(dup2(fileno(file), 2) == 2); +#endif + run(); + std::fflush(stderr); +#ifdef _WIN32 + Require(_dup2(saved, 2) == 0); + _close(saved); +#else + Require(dup2(saved, 2) == 2); + close(saved); +#endif + std::fclose(file); + std::ifstream input(path, std::ios::binary); + return std::string(std::istreambuf_iterator(input), std::istreambuf_iterator()); +} + +static std::size_t Occurrences(const std::string& text, const std::string& pattern) { + std::size_t count = 0; + for (auto at = text.find(pattern); at != std::string::npos; at = text.find(pattern, at + pattern.size())) ++count; + return count; +} + static float AdvanceOf(FontHandle font, std::uint32_t code) { FontGlyphMetrics metrics{}; Require(sceFontGetCharGlyphMetrics(font, code, &metrics) == SCE_FONT_OK); @@ -155,8 +196,13 @@ int main() { FontHandle font = nullptr; SetFontDirectory(empty); - Require(sceFontOpenFontSet(library, EuropeanBold, 1, nullptr, &font) == SCE_FONT_ERROR_FONT_OPEN_FAILED && font == nullptr); - Require(sceFontOpenFontSet(library, 0x18070046u, 1, nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FONTSET && font == nullptr); + const std::string warnings = CaptureStderr(root / "stderr.txt", [&] { + Require(sceFontOpenFontSet(library, EuropeanBold, 1, nullptr, &font) == SCE_FONT_ERROR_FONT_OPEN_FAILED && font == nullptr); + Require(sceFontOpenFontSet(library, EuropeanLight, 1, nullptr, &font) == SCE_FONT_ERROR_FONT_OPEN_FAILED && font == nullptr); + Require(sceFontOpenFontSet(library, 0x18070046u, 1, nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FONTSET && font == nullptr); + }); + Require(Occurrences(warnings, "[libSceFont] no file for system font set") == 1); + Require(Occurrences(warnings, "0x180700C7 in " + empty.string()) == 1 && Occurrences(warnings, "ANYPS5_SYSTEM_FONTS") == 1); SetFontDirectory(root / "missing"); bool threw = false; try { From 7fdb261e673d49cd87646fd8997a11c403350c7a Mon Sep 17 00:00:00 2001 From: Leonardo Date: Mon, 19 Oct 2026 05:12:36 +0200 Subject: [PATCH 041/167] 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 c72ccac3e6406f971429f54d36528098b573ebc7 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Mon, 19 Oct 2026 07:58:16 +0400 Subject: [PATCH 042/167] feat(pad): queue timed controller button sequences --- CMakeLists.txt | 7 + .../libScePad/include/InputCommandFile.hpp | 82 +++++++++-- .../prx/libScePad/tests/InputCommandFile.cpp | 111 ++++++++++++++ docs/user/INPUT_MAPPING.md | 16 ++ docs/user/USAGE.md | 2 + tools/pad_timeline.py | 99 +++++++++++++ tools/tests/test_pad_timeline.py | 137 ++++++++++++++++++ 7 files changed, 439 insertions(+), 15 deletions(-) create mode 100644 core/libs/prx/libScePad/tests/InputCommandFile.cpp create mode 100644 tools/pad_timeline.py create mode 100644 tools/tests/test_pad_timeline.py diff --git a/CMakeLists.txt b/CMakeLists.txt index bc59d5e48c..601b03f040 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -108,6 +108,10 @@ if(BUILD_TESTING) core/shader/recompiler/IntermediateRepresentation/include) add_test(NAME shader_ir_use_rewrite COMMAND shader_ir_use_rewrite_tests) set_tests_properties(shader_ir_use_rewrite PROPERTIES TIMEOUT 20) + add_test_executable(pad_command_file_tests core/libs/prx/libScePad/tests/InputCommandFile.cpp) + target_include_directories(pad_command_file_tests PRIVATE core/libs) + add_test(NAME pad_command_file COMMAND pad_command_file_tests) + set_tests_properties(pad_command_file PROPERTIES TIMEOUT 20) if(Python3_Interpreter_FOUND) add_test(NAME shader_cache_native_paths COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/test_shader_cache_native_paths.py @@ -198,6 +202,9 @@ if(BUILD_TESTING) add_test(NAME pad_input COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_input.py) set_tests_properties(pad_input PROPERTIES TIMEOUT 20) + add_test(NAME pad_timeline COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_timeline.py) + set_tests_properties(pad_timeline PROPERTIES TIMEOUT 20) add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py) add_test(NAME conventions_bom_shebang COMMAND ${Python3_EXECUTABLE} diff --git a/core/libs/prx/libScePad/include/InputCommandFile.hpp b/core/libs/prx/libScePad/include/InputCommandFile.hpp index 5ee3b1fb73..e372a1f3f7 100644 --- a/core/libs/prx/libScePad/include/InputCommandFile.hpp +++ b/core/libs/prx/libScePad/include/InputCommandFile.hpp @@ -1,13 +1,18 @@ #ifndef CORE_LIBS_PRX_LIBSCEPAD_INPUTCOMMANDFILE_HPP #define CORE_LIBS_PRX_LIBSCEPAD_INPUTCOMMANDFILE_HPP +#include #include #include #include #include +#include +#include #include #include #include +#include +#include namespace Pad { class InputCommandFile { @@ -29,24 +34,36 @@ public: Input Poll(std::uint64_t now) { if (path.empty()) return {}; if (now >= nextPoll) { - nextPoll = now + 100000; - const auto command = read(); + nextPoll = now > std::numeric_limits::max() - 100000 + ? std::numeric_limits::max() : now + 100000; + auto command = read(); if (command.sequence > lastSequence) { lastSequence = command.sequence; - input = command.input; - until = now + command.durationMs * 1000ull; + events = std::move(command.events); + started = now; } } - return now < until ? input : Input{}; + if (events.empty() || now < started) return {}; + const auto elapsed = now - started; + const auto next = std::upper_bound(events.begin(), events.end(), elapsed, + [](std::uint64_t time, const Event& event) { return time < event.start; }); + if (next == events.begin()) return {}; + const auto& event = *std::prev(next); + return elapsed < event.end ? event.input : Input{}; } std::uint32_t Buttons(std::uint64_t now) { return Poll(now).buttons; } private: + struct Event { + std::uint64_t start = 0; + std::uint64_t end = 0; + Input input; + }; + struct Command { std::uint64_t sequence = 0; - Input input; - std::uint32_t durationMs = 0; + std::vector events; }; Command read() const { @@ -56,6 +73,40 @@ private: const std::string text(line); if (text.find('-') != std::string::npos) return {}; std::istringstream fields(text); + std::string first; + if (!(fields >> first)) return {}; + if (first == "APS5_PAD_TIMELINE") { + Command command; + std::string extra; + std::uint64_t version = 0, count = 0; + if (!(fields >> version >> command.sequence >> count) || fields >> extra) return {}; + if (version != 1 || command.sequence == 0 || count == 0 || count > 4096) return {}; + if (command.sequence <= lastSequence) return command; + command.events.reserve(static_cast(count)); + std::uint64_t previousEnd = 0; + for (std::uint64_t index = 0; index < count; ++index) { + if (!file.getline(line, sizeof(line))) return {}; + const std::string eventText(line); + if (eventText.find('-') != std::string::npos) return {}; + fields.clear(); + fields.str(eventText); + std::uint64_t start = 0, duration = 0, buttons = 0; + if (!(fields >> start >> duration >> buttons) || fields >> extra) return {}; + if ((buttons & ~(0x10fffeull | TouchLeftBit | TouchRightBit)) != 0 || duration == 0 || duration > 5000 || + start < previousEnd || start > 3600000 - duration) return {}; + previousEnd = start + duration; + Input input; + input.buttons = static_cast(buttons & 0x10fffeull); + input.touchLeft = (buttons & TouchLeftBit) != 0; + input.touchRight = (buttons & TouchRightBit) != 0; + command.events.push_back({start * 1000, previousEnd * 1000, input}); + } + file >> std::ws; + if (file.peek() != std::char_traits::eof()) return {}; + return command; + } + fields.clear(); + fields.str(text); std::string tokens[8]; int count = 0; while (count < 8 && fields >> tokens[count]) ++count; @@ -70,15 +121,16 @@ private: if (values[0] == 0 || (values[1] & ~(0x10fffeull | TouchLeftBit | TouchRightBit)) != 0 || values[2] == 0 || values[2] > 5000) return {}; Command command; command.sequence = values[0]; - command.input.buttons = static_cast(values[1] & 0x10fffeull); - command.input.touchLeft = (values[1] & TouchLeftBit) != 0; - command.input.touchRight = (values[1] & TouchRightBit) != 0; - command.durationMs = static_cast(values[2]); + Input input; + input.buttons = static_cast(values[1] & 0x10fffeull); + input.touchLeft = (values[1] & TouchLeftBit) != 0; + input.touchRight = (values[1] & TouchRightBit) != 0; for (int i = 3; i < count; ++i) { if (values[i] > 255) return {}; - command.input.sticks[i - 3] = static_cast(values[i]); + input.sticks[i - 3] = static_cast(values[i]); } - command.input.hasSticks = count == 7; + input.hasSticks = count == 7; + command.events.push_back({0, values[2] * 1000, input}); return command; } @@ -88,8 +140,8 @@ private: std::filesystem::path path; std::uint64_t lastSequence = 0; std::uint64_t nextPoll = 0; - std::uint64_t until = 0; - Input input; + std::uint64_t started = 0; + std::vector events; }; } diff --git a/core/libs/prx/libScePad/tests/InputCommandFile.cpp b/core/libs/prx/libScePad/tests/InputCommandFile.cpp new file mode 100644 index 0000000000..64309a4892 --- /dev/null +++ b/core/libs/prx/libScePad/tests/InputCommandFile.cpp @@ -0,0 +1,111 @@ +#include "prx/libScePad/include/InputCommandFile.hpp" +#include +#include +#include +#include +#include +#include + +void Check(bool condition) { + if (!condition) throw std::runtime_error("input command assertion failed"); +} + +int main() { + const auto path = std::filesystem::temp_directory_path() / + ("anyps5-pad-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + const auto write = [&](const std::string& text) { std::ofstream(path) << text; }; + try { + write("1 16384 300\n"); + Pad::InputCommandFile input(path); + Check(input.Buttons(0) == 0); + write("APS5_PAD_TIMELINE 1 2 3\n200 100 18432\n500 200 16\n800 100 0\n"); + Check(input.Buttons(100000) == 0); + Check(input.Buttons(299999) == 0); + Check(input.Buttons(300000) == 18432); + Check(input.Buttons(399999) == 18432); + Check(input.Buttons(400000) == 0); + Check(input.Buttons(600000) == 16); + Check(input.Buttons(800000) == 0); + Check(input.Buttons(1100000) == 0); + write("APS5_PAD_TIMELINE 1 3 2\n0 500 32768\n300 100 16\n"); + Check(input.Buttons(1200000) == 0); + write("APS5_PAD_TIMELINE 1 3 2\n0 500 32768\n600 100 16\n"); + Check(input.Buttons(1300000) == 32768); + write("APS5_PAD_TIMELINE 1 4 1\n0 6000 16\n"); + Check(input.Buttons(1400000) == 32768); + write("APS5_PAD_TIMELINE 1 4 1\n0 500 16\nextra\n"); + Check(input.Buttons(1500000) == 32768); + write("APS5_PAD_TIMELINE 1 4 2\n0 100 16\n"); + Check(input.Buttons(1600000) == 32768); + write("APS5_PAD_TIMELINE 2 4 1\n0 100 16\n"); + Check(input.Buttons(1700000) == 32768); + Check(input.Buttons(1800000) == 0); + write("4 8 200\n"); + Check(input.Buttons(1900000) == 8); + Check(input.Buttons(2100000) == 0); + write("3 16 200\n"); + Check(input.Buttons(2200000) == 0); + write("5 0 1\n"); + Check(input.Buttons(2300000) == 0); + const std::string bad[] = { + "APS5_PAD_TIMELINE 1 6 0\n", "APS5_PAD_TIMELINE 1 6 4097\n", + "APS5_PAD_TIMELINE 1 6 1\n0 1 1\n", "APS5_PAD_TIMELINE 1 6 1\n-1 1 16\n", + "APS5_PAD_TIMELINE 1 6 1\n3600000 1 16\n", + "APS5_PAD_TIMELINE 1 18446744073709551616 1\n0 1 16\n", + "APS5_PAD_TIMELINE 1 6 1\n18446744073709551615 1 16\n", + "APS5_PAD_TIMELINE 1 6 1\n0 1 4294967296\n" + }; + std::uint64_t clock = 2400000; + for (const auto& text : bad) { + write(text); + Check(input.Buttons(clock) == 0); + clock += 100000; + } + write("APS5_PAD_TIMELINE 1 6 1\n3599999 1 16384\n"); + Check(input.Buttons(clock) == 0); + Check(input.Buttons(clock + 3599999000ull) == 16384); + Check(input.Buttons(clock + 3600000000ull) == 0); + write("APS5_PAD_TIMELINE 1 7 1\n0 100 16\n"); + const auto maximum = std::numeric_limits::max(); + Check(input.Buttons(maximum - 10) == 16); + Check(input.Buttons(maximum) == 16); + Pad::InputCommandFile stale(path); + Check(stale.Buttons(0) == 0); + Pad::InputCommandFile disabled(std::filesystem::path{}); + Check(disabled.Buttons(0) == 0); + write("10 16384 300 0 255 128 64\n"); + Pad::InputCommandFile sticks(path); + Check(sticks.Poll(0) == Pad::InputCommandFile::Input{}); + write("11 16 200 0 255 128 64\n"); + Check(sticks.Poll(100000) == Pad::InputCommandFile::Input{16, true, {0, 255, 128, 64}}); + write("12 16 200 0 256 128 64\n"); + Check(sticks.Poll(200000) == Pad::InputCommandFile::Input{16, true, {0, 255, 128, 64}}); + Check(sticks.Poll(300000) == Pad::InputCommandFile::Input{}); + write("12 16 200 0 255 128 64 1\n"); + Check(sticks.Poll(400000) == Pad::InputCommandFile::Input{}); + write("APS5_PAD_TIMELINE 1 12 1\n0 500 16384\n"); + Check(sticks.Poll(500000) == Pad::InputCommandFile::Input{16384}); + write("13 8 200 255 0 64 128\n"); + Check(sticks.Poll(600000) == Pad::InputCommandFile::Input{8, true, {255, 0, 64, 128}}); + write("14 16 100\n"); + Check(sticks.Poll(700000) == Pad::InputCommandFile::Input{16}); + Check(sticks.Poll(800000) == Pad::InputCommandFile::Input{}); + write("15 2113536 100 0 255 128 64\n"); + Check(sticks.Poll(900000) == Pad::InputCommandFile::Input{16384, true, {0, 255, 128, 64}, true, false}); + Check(sticks.Poll(1000000) == Pad::InputCommandFile::Input{}); + write("APS5_PAD_TIMELINE 1 16 3\n0 100 2113536\n100 100 4194304\n200 100 6291456\n"); + Check(sticks.Poll(1100000) == Pad::InputCommandFile::Input{16384, false, {}, true, false}); + Check(sticks.Poll(1200000) == Pad::InputCommandFile::Input{0, false, {}, false, true}); + Check(sticks.Poll(1300000) == Pad::InputCommandFile::Input{0, false, {}, true, true}); + Check(sticks.Poll(1400000) == Pad::InputCommandFile::Input{}); + write("APS5_PAD_TIMELINE 1 17 1\n0 100 8388608\n"); + Check(sticks.Poll(1500000) == Pad::InputCommandFile::Input{}); + std::filesystem::remove(path); + std::cout << "Controller timeline runtime cases passed\n"; + return 0; + } catch (const std::exception& error) { + std::filesystem::remove(path); + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 74221f95ab..f0af904131 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -79,6 +79,22 @@ RightStickDeadzone = 8 MouseSensitivity = 2.5 ``` +## Controller timelines + +Set `APS5_PAD_INPUT_FILE` as described in [Controller input for test runs](USAGE.md#controller-input-for-test-runs), then queue an entire button sequence after the application begins reading its controller. Save a JSON file such as `menu.json`: + +```json +{"version":1,"events":[{"at_ms":200,"hold_ms":300,"buttons":["cross","r1"]},{"at_ms":1000,"hold_ms":300,"buttons":["down"]}]} +``` + +```sh +python3 tools/pad_timeline.py menu.json /tmp/anyps5-pad --repeat 3 --gap-ms 500 +``` + +Events start relative to when the application first observes the new command. Button lists support every name in `pad_input.py`; simultaneous names are combined. An empty list or `["release"]` injects no buttons. Events must be ordered and non-overlapping, hold for 1–5000 ms, and finish within one hour. Repetition adds the specified gap after the last event; at most 4096 events are accepted after repetition. The helper validates the entire sequence before atomically replacing the command file. + +The runtime evaluates scheduled events at each controller read, including their automatic releases. A new pulse or timeline replaces the remaining sequence. Physical controller buttons still add to injected buttons. Existing commands at the application's first controller read are ignored. This schedules wall-time input: events can be missed while the application is not reading its controller, and it does not reproduce frame timing or game state. Use gaps and holds long enough for the title's input loop. + ## Analog keyboard input `StickModifier` has no built-in binding. While a key or mouse button bound to it is held, the keyboard stick bindings tilt the stick by `StickModifierScale` of its range instead of fully, for walking or fine aim. Controller sticks and the mouse stick are not affected, and opposite keys still cancel each other. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 6f299ab99b..4922e64b55 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -209,6 +209,8 @@ python3 tools/pad_input.py /tmp/anyps5-pad cross --left-stick 255 128 --right-st python3 tools/pad_input.py /tmp/anyps5-pad touch-right ``` +For timed button sequences, see [Controller timelines](INPUT_MAPPING.md#controller-timelines). + Run the helper in a second terminal. Commands are polled at most every 100 ms during controller reads, and their buttons add to physical controller input. A press lasts 300 ms by default, starting when the application reads it, and releases automatically; `--hold-ms` accepts 1–5000 ms. `--left-stick X Y` and `--right-stick X Y` hold the sticks for the same time: each axis is 0-255, 128 is the centre, X grows to the right and Y grows downward. While such a command is active both sticks come from it instead of the physical controller, and a stick that was not named stays centred. `touch-left` and `touch-right` press the touchpad with a finger on its left or right half, as the `TouchLeft` and `TouchRight` keys do; in the command file they are bits `0x200000` and `0x400000` of the button field. Wait for the application to observe a release before sending another press. The file holds only the latest command; rapid commands can replace one another. On POSIX, replacing an existing command file retains its permission mode; a privileged helper also retains its owner and group so the game user can still read it. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled. ### System fonts diff --git a/tools/pad_timeline.py b/tools/pad_timeline.py new file mode 100644 index 0000000000..84dd938fd8 --- /dev/null +++ b/tools/pad_timeline.py @@ -0,0 +1,99 @@ +import argparse +import json +import os +import stat +from pathlib import Path +import tempfile +import time + +from pad_input import BUTTONS + + +def integer(value, field, minimum, maximum): + if type(value) is not int or not minimum <= value <= maximum: + raise ValueError(f"{field} must be an integer between {minimum} and {maximum}") + return value + + +def compile_timeline(document, sequence, repeat=1, gap_ms=200): + integer(sequence, "sequence", 1, 2**64 - 1) + integer(repeat, "repeat", 1, 4096) + integer(gap_ms, "gap_ms", 0, 3600000) + if not isinstance(document, dict) or set(document) != {"version", "events"}: + raise ValueError("timeline must contain version and events") + if type(document["version"]) is not int or document["version"] != 1: + raise ValueError("unsupported timeline version") + source = document["events"] + if not isinstance(source, list) or not 1 <= len(source) * repeat <= 4096: + raise ValueError("timeline must contain 1 to 4096 events after repetition") + events = [] + previous_end = 0 + for index, event in enumerate(source): + prefix = f"events[{index}]" + if not isinstance(event, dict) or set(event) != {"at_ms", "hold_ms", "buttons"}: + raise ValueError(f"{prefix} must contain at_ms, hold_ms and buttons") + start = integer(event["at_ms"], f"{prefix}.at_ms", 0, 3600000) + duration = integer(event["hold_ms"], f"{prefix}.hold_ms", 1, 5000) + if start < previous_end or start + duration > 3600000: + raise ValueError(f"{prefix} overlaps an earlier event or exceeds one hour") + names = event["buttons"] + if not isinstance(names, list) or not all(isinstance(name, str) and name in BUTTONS for name in names): + raise ValueError(f"{prefix}.buttons must be a list of known button names") + if "release" in names and len(names) != 1: + raise ValueError(f"{prefix}: release cannot be combined with another button") + buttons = 0 + for name in names: + buttons |= BUTTONS[name] + events.append((start, duration, buttons)) + previous_end = start + duration + period = previous_end + gap_ms + if (repeat - 1) * period + previous_end > 3600000: + raise ValueError("repeated timeline exceeds one hour") + rows = [f"APS5_PAD_TIMELINE 1 {sequence} {len(events) * repeat}"] + for iteration in range(repeat): + rows.extend(f"{start + iteration * period} {duration} {buttons}" for start, duration, buttons in events) + return "\n".join(rows) + "\n" + + +def queue_timeline(path, text): + metadata = None + if os.name == "posix": + try: + metadata = path.stat() + except FileNotFoundError: + pass + temporary = None + try: + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=path.parent, delete=False) as output: + temporary = Path(output.name) + if metadata is not None: + if os.geteuid() == 0: + os.fchown(output.fileno(), metadata.st_uid, metadata.st_gid) + os.fchmod(output.fileno(), stat.S_IMODE(metadata.st_mode)) + output.write(text) + os.replace(temporary, path) + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + + +def main(): + parser = argparse.ArgumentParser(description="Queue a timed controller button sequence for APS5_PAD_INPUT_FILE.") + parser.add_argument("timeline", type=Path) + parser.add_argument("path", type=Path) + parser.add_argument("--repeat", type=int, default=1) + parser.add_argument("--gap-ms", type=int, default=200) + args = parser.parse_args() + try: + if args.timeline.resolve() == args.path.resolve() or (args.path.exists() and os.path.samefile(args.timeline, args.path)): + raise ValueError("the command file must be separate from the timeline JSON") + document = json.loads(args.timeline.read_text(encoding="utf-8")) + text = compile_timeline(document, time.monotonic_ns(), args.repeat, args.gap_ms) + queue_timeline(args.path, text) + except (OSError, ValueError) as error: + parser.exit(2, f"pad_timeline: {error}\n") + print(f"Queued {len(text.splitlines()) - 1} timed events in {args.path}") + + +if __name__ == "__main__": + main() diff --git a/tools/tests/test_pad_timeline.py b/tools/tests/test_pad_timeline.py new file mode 100644 index 0000000000..33d10b2548 --- /dev/null +++ b/tools/tests/test_pad_timeline.py @@ -0,0 +1,137 @@ +import copy +import json +import os +import stat +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest import mock + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import pad_timeline + + +class TimelineTests(unittest.TestCase): + def setUp(self): + self.document = {"version": 1, "events": [ + {"at_ms": 200, "hold_ms": 100, "buttons": ["cross", "r1"]}, + {"at_ms": 500, "hold_ms": 200, "buttons": ["down"]}, + ]} + + def test_compilation_and_repetition(self): + text = pad_timeline.compile_timeline(self.document, 42, 2, 300) + self.assertEqual(text, "APS5_PAD_TIMELINE 1 42 4\n200 100 18432\n500 200 64\n1200 100 18432\n1500 200 64\n") + + def test_touchpad_half_names_keep_their_command_bits(self): + self.document["events"][0]["buttons"] = ["cross", "touch-left", "touch-right"] + self.assertIn("200 100 6307840", pad_timeline.compile_timeline(self.document, 1)) + + def test_empty_buttons_release(self): + self.document["events"][0]["buttons"] = [] + self.assertIn("200 100 0", pad_timeline.compile_timeline(self.document, 1)) + + def test_release_cannot_be_combined(self): + self.document["events"][0]["buttons"] = ["release", "cross"] + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(self.document, 1) + + def test_unknown_buttons(self): + for buttons in [["typo"], "cross", [True]]: + with self.subTest(buttons=buttons): + self.document["events"][0]["buttons"] = buttons + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(self.document, 1) + + def test_overlaps(self): + self.document["events"][1]["at_ms"] = 250 + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(self.document, 1) + + def test_integer_bounds(self): + for field, values in {"at_ms": [-1, True, 1.5, 3600001], "hold_ms": [0, True, 5001]}.items(): + for value in values: + with self.subTest(field=field, value=value): + document = copy.deepcopy(self.document) + document["events"][0][field] = value + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(document, 1) + + def test_schema_validation(self): + for document in [None, [], {}, {"version": True, "events": []}, {"version": 2, "events": []}, + {"version": 1, "events": []}, {"version": 1, "events": [{}]}, + dict(self.document, unknown=1)]: + with self.subTest(document=document): + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(document, 1) + + def test_repetition_bounds(self): + for repeat, gap in [(0, 0), (4097, 0), (True, 0), (1, -1), (4096, 3600000)]: + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(self.document, 1, repeat, gap) + + def test_sequence_bounds(self): + for sequence in [0, -1, True, 2**64]: + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(self.document, sequence) + + def test_maximum_duration(self): + document = {"version": 1, "events": [{"at_ms": 3599999, "hold_ms": 1, "buttons": ["cross"]}]} + self.assertIn("3599999 1", pad_timeline.compile_timeline(document, 2**64 - 1)) + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(document, 1, 2) + + @unittest.skipUnless(os.name == "posix", "POSIX file permissions") + def test_replacement_preserves_shared_command_permissions(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "command" + path.write_text("previous\n", encoding="utf-8") + path.chmod(0o640) + metadata = path.stat() + pad_timeline.queue_timeline(path, "new\n") + self.assertEqual(path.read_bytes(), b"new\n") + self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o640) + self.assertEqual((path.stat().st_uid, path.stat().st_gid), (metadata.st_uid, metadata.st_gid)) + + def test_atomic_replace_and_cleanup(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "command" + pad_timeline.queue_timeline(path, "new\n") + self.assertEqual(path.read_bytes(), b"new\n") + with mock.patch("pad_timeline.os.replace", side_effect=OSError("denied")): + with self.assertRaises(OSError): + pad_timeline.queue_timeline(path, "wrong\n") + self.assertEqual(path.read_bytes(), b"new\n") + self.assertEqual(list(Path(directory).iterdir()), [path]) + + def run_cli(self, timeline, output): + return subprocess.run([sys.executable, str(Path(pad_timeline.__file__)), str(timeline), str(output)], + capture_output=True, text=True, timeout=5) + + def test_cli_roundtrip_and_invalid_input_preservation(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + timeline, output = root / "sequence.json", root / "command" + timeline.write_text(json.dumps(self.document), encoding="utf-8") + self.assertEqual(self.run_cli(timeline, output).returncode, 0) + original = output.read_bytes() + timeline.write_text('{"version":2}', encoding="utf-8") + self.assertEqual(self.run_cli(timeline, output).returncode, 2) + self.assertEqual(output.read_bytes(), original) + + def test_input_alias_protection(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + timeline = root / "sequence.json" + original = json.dumps(self.document) + timeline.write_text(original, encoding="utf-8") + self.assertEqual(self.run_cli(timeline, timeline).returncode, 2) + linked = root / "hardlink" + os.link(timeline, linked) + self.assertEqual(self.run_cli(timeline, linked).returncode, 2) + self.assertEqual(timeline.read_text(encoding="utf-8"), original) + + +if __name__ == "__main__": + unittest.main() From 9f4c27190e089970897944078437e51d9498e7b6 Mon Sep 17 00:00:00 2001 From: Werner Date: Thu, 15 Oct 2026 16:48:56 +0000 Subject: [PATCH 043/167] fix(relinker): keep zero-filled PT_LOAD memory out of the Windows load image WindowsLoadImage backed each PT_LOAD up to its memory size, so the zero-filled tail of a segment was allocated and copied into its section before the writer dropped it again: 64 MiB of zero-filled memory cost 131 MiB and 0.2 s for --windows and --macos. Only the file-backed pages are stored now; a pointer written into zero-filled memory backs just the pages it touches, and the section keeps the rest as virtual size. A relocation into the end of a zero-filled area no longer pulls the whole area into the executable file. --- .../elfpatcher/windows/WindowsLoadImage.hpp | 2 +- .../elfpatcher/windows/WindowsPeFormat.hpp | 10 ++++ .../elfpatcher/src/macos/MacOsImage.cpp | 4 +- .../src/windows/WindowsGuestModuleWriter.cpp | 2 +- .../src/windows/WindowsLoadImage.cpp | 50 ++++++++++++------- .../src/windows/WindowsPePatcher.cpp | 2 +- .../relinker/tests/test_windows_bss.py | 28 +++++++++++ 7 files changed, 75 insertions(+), 23 deletions(-) diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp index 3f2fe28f13..fa2ef59297 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp @@ -24,7 +24,7 @@ private: std::vector Bytes; }; - Block& findBlock(std::uint64_t offset); + Block& backBlock(std::uint64_t offset, std::uint64_t length); std::vector segments; std::vector blocks; diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp index fec5214e01..790dfdf3ae 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp @@ -27,6 +27,16 @@ struct PeSection { std::uint64_t MemorySize() const { return std::max(Data.size(), VirtualSize); } + + bool Writable(const std::uint32_t rva, const std::uint64_t size) { + if (rva < Rva || rva - Rva > MemorySize() || size > MemorySize() - (rva - Rva)) + return false; + if (rva - Rva + size > Data.size()) { + VirtualSize = static_cast(MemorySize()); + Data.resize(static_cast(rva - Rva + size)); + } + return true; + } }; struct PeDirectory { diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index 99fc475e5b..210858aa4e 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -166,7 +166,9 @@ std::pair Locate(const std::vector& segmen Segment& SegmentAt(std::vector& segments, const std::uint64_t rva, const std::uint64_t size) { for (auto& segment : segments) { - if (rva >= segment.Rva && rva - segment.Rva + size <= segment.Data.size()) return segment; + if (rva < segment.Rva || rva - segment.Rva + size > segment.MemorySize) continue; + if (rva - segment.Rva + size > segment.Data.size()) segment.Data.resize(rva - segment.Rva + size); + return segment; } throw Domain::RelinkerException("Mach-O image patch is outside the image data", rva); } diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index 85fea36fc2..4cf1aa7521 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -94,7 +94,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI if (tlsIndex == 0) throw Domain::RelinkerException("Guest TLS relocation has no TLS block", target); bool written = false; for (auto& section : sections) { - if (rva < section.Rva || rva - section.Rva > section.Data.size() || section.Data.size() - (rva - section.Rva) < 8) continue; + if (!section.Writable(rva, 8)) continue; Io::WriteU64(section.Data, rva - section.Rva, ImageBase + tlsIndex); written = true; break; diff --git a/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp b/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp index 6fa30dcc54..ef5a9056a6 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp @@ -39,13 +39,10 @@ WindowsLoadImage::WindowsLoadImage(const std::vector& source, cons pageFlags.resize(static_cast(size / SectionAlignment)); for (const auto& segment : segments) { const auto offset = static_cast(segment.MappedAddress - firstAddress); - const auto blockStart = offset / SectionAlignment * SectionAlignment; - const auto blockEnd = static_cast(AlignRva(offset + segment.MemorySize)); - if (blocks.empty() || blocks.back().Offset + blocks.back().Bytes.size() < blockStart) - blocks.push_back({blockStart, {}}); - auto& block = blocks.back(); - block.Bytes.resize(blockEnd - block.Offset); - std::copy_n(source.begin() + static_cast(segment.Offset), static_cast(segment.FileSize), block.Bytes.begin() + static_cast(offset - block.Offset)); + if (segment.FileSize != 0) { + auto& block = backBlock(offset, segment.FileSize); + std::copy_n(source.begin() + static_cast(segment.Offset), static_cast(segment.FileSize), block.Bytes.begin() + static_cast(offset - block.Offset)); + } std::uint32_t flags = 0; if ((segment.Flags & 1) != 0) flags |= SectionExecute | SectionRead; @@ -91,16 +88,30 @@ std::uint64_t WindowsLoadImage::GetImageBase() const { return imageBase; } -WindowsLoadImage::Block& WindowsLoadImage::findBlock(const std::uint64_t offset) { - const auto next = std::upper_bound(blocks.begin(), blocks.end(), offset, [](const std::uint64_t value, const Block& block) { return value < block.Offset; }); - if (next == blocks.begin() || offset - std::prev(next)->Offset >= std::prev(next)->Bytes.size()) - throw Domain::RelinkerException("ELF image offset is not backed by PT_LOAD memory", offset); - return *std::prev(next); +WindowsLoadImage::Block& WindowsLoadImage::backBlock(const std::uint64_t offset, const std::uint64_t length) { + const auto start = offset / SectionAlignment * SectionAlignment; + const auto end = AlignRva(offset + length); + auto index = static_cast(std::upper_bound(blocks.begin(), blocks.end(), offset, [](const std::uint64_t value, const Block& block) { return value < block.Offset; }) - blocks.begin()); + if (index == 0 || blocks[index - 1].Offset + blocks[index - 1].Bytes.size() < start) + blocks.insert(blocks.begin() + static_cast(index), Block{start, {}}); + else + --index; + auto& block = blocks[index]; + if (block.Offset + block.Bytes.size() < end) + block.Bytes.resize(end - block.Offset); + while (index + 1 < blocks.size() && blocks[index + 1].Offset <= block.Offset + block.Bytes.size()) { + const auto& next = blocks[index + 1]; + const auto nextOffset = static_cast(next.Offset - block.Offset); + block.Bytes.resize(std::max(block.Bytes.size(), nextOffset + next.Bytes.size())); + std::copy(next.Bytes.begin(), next.Bytes.end(), block.Bytes.begin() + static_cast(nextOffset)); + blocks.erase(blocks.begin() + static_cast(index + 1)); + } + return block; } void WindowsLoadImage::WritePointer(const std::uint64_t address, const std::uint64_t value) { const auto offset = GetRva(address, 8) - LoadRva; - auto& block = findBlock(offset); + auto& block = backBlock(offset, 8); Io::WriteU64(block.Bytes, offset - block.Offset, value); } @@ -119,18 +130,19 @@ std::vector WindowsLoadImage::BuildSections() const { while (block != blocks.end() && block->Offset + block->Bytes.size() <= first * SectionAlignment) ++block; const auto backed = block != blocks.end() && block->Offset <= first * SectionAlignment; - const auto runEnd = backed ? (block->Offset + block->Bytes.size()) / SectionAlignment : block != blocks.end() ? block->Offset / SectionAlignment : pageFlags.size(); + const auto dataEnd = backed ? (block->Offset + block->Bytes.size()) / SectionAlignment : first; + const auto following = backed ? std::next(block) : block; + const auto stop = following != blocks.end() ? following->Offset / SectionAlignment : pageFlags.size(); auto last = first + 1; - while (last < runEnd && pageFlags[last] == pageFlags[first]) + while (last < stop && pageFlags[last] == pageFlags[first]) ++last; const auto characteristics = pageFlags[first] | ((pageFlags[first] & SectionExecute) != 0 ? 0x20u : 0x40u); - const auto rva = CheckedRva(LoadRva + first * SectionAlignment); + PeSection section{".elf" + std::to_string(sections.size()), CheckedRva(LoadRva + first * SectionAlignment), characteristics, {}, CheckedRva((last - first) * SectionAlignment)}; if (backed) { const auto begin = block->Bytes.begin() + static_cast(first * SectionAlignment - block->Offset); - sections.push_back({".elf" + std::to_string(sections.size()), rva, characteristics, {begin, begin + static_cast((last - first) * SectionAlignment)}}); - } else { - sections.push_back({".elf" + std::to_string(sections.size()), rva, characteristics, {}, CheckedRva((last - first) * SectionAlignment)}); + section.Data.assign(begin, begin + static_cast((std::min(last, dataEnd) - first) * SectionAlignment)); } + sections.push_back(std::move(section)); first = last; } return sections; diff --git a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp index 605d26ab6a..9102c6e15f 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -18,7 +18,7 @@ namespace { void writeGotStub(std::vector& sections, const std::uint32_t targetRva, const std::uint32_t stubRva, const std::uint64_t imageBase) { for (auto& section : sections) { - if (section.Data.size() < 8 || targetRva < section.Rva || targetRva - section.Rva > section.Data.size() - 8) + if (!section.Writable(targetRva, 8)) continue; Io::WriteU64(section.Data, targetRva - section.Rva, imageBase + stubRva); return; diff --git a/core/relinker/relinker/tests/test_windows_bss.py b/core/relinker/relinker/tests/test_windows_bss.py index 7cb1dac6ad..6c0b149ae0 100644 --- a/core/relinker/relinker/tests/test_windows_bss.py +++ b/core/relinker/relinker/tests/test_windows_bss.py @@ -65,8 +65,36 @@ def main(): if os.name == "nt": executed = subprocess.run([str(output)], capture_output=True, text=True, timeout=20) assert executed.returncode == 42, f"exit code {executed.returncode:#x}" + check_relocated_zero_fill(relinker, work) print("Windows zero-filled memory tests passed") +def check_relocated_zero_fill(relinker, work): + image = fixture() + file_size = struct.unpack_from(" Date: Mon, 19 Oct 2026 05:22:58 +0000 Subject: [PATCH 044/167] fix(relinker): compare page indices of one type when building Windows sections dataEnd was a std::uint64_t and last a std::size_t, which are different types on macOS, so std::min did not compile with Apple clang. --- core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp b/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp index ef5a9056a6..9f100e655c 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp @@ -130,7 +130,7 @@ std::vector WindowsLoadImage::BuildSections() const { while (block != blocks.end() && block->Offset + block->Bytes.size() <= first * SectionAlignment) ++block; const auto backed = block != blocks.end() && block->Offset <= first * SectionAlignment; - const auto dataEnd = backed ? (block->Offset + block->Bytes.size()) / SectionAlignment : first; + const auto dataEnd = backed ? static_cast((block->Offset + block->Bytes.size()) / SectionAlignment) : first; const auto following = backed ? std::next(block) : block; const auto stop = following != blocks.end() ? following->Offset / SectionAlignment : pageFlags.size(); auto last = first + 1; From 9babdd83fbbb69fe3fa4cb9a4fe7dbdbf35542d9 Mon Sep 17 00:00:00 2001 From: Werner Date: Mon, 19 Oct 2026 05:55:26 +0000 Subject: [PATCH 045/167] fix(relinker): merge zero-filled Mach-O segments past the dyld segment limit Backing only the pages a pointer touches splits a zero-filled run into one section per backed block. Each becomes a Mach-O segment, and dyld's bind and rebase opcodes address at most 16 segments, so a module with pointer slots on many separate zero-filled pages converted on main but failed with --macos. When the segments would pass that limit, adjacent sections with the same protection are merged again, smallest gap first. Reported by jotitadev in the review. --- .../elfpatcher/src/macos/MacOsImage.cpp | 28 ++++++++++++++++++ .../relinker/tests/test_windows_bss.py | 29 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index 210858aa4e..ad8c61308a 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -154,6 +154,33 @@ void PadTo(std::vector& out, const std::size_t alignment) { while (out.size() % alignment != 0) out.push_back(0); } +void FitDyldSegmentLimit(std::vector& sections, const std::size_t limit) { + while (sections.size() > limit) { + auto best = sections.end(); + std::uint64_t bestGap = 0; + for (auto section = sections.begin(); std::next(section) != sections.end(); ++section) { + const auto next = std::next(section); + if (section->Name == ".amdstub" || next->Name == ".amdstub" || section->Characteristics != next->Characteristics || + section->Rva + section->MemorySize() != next->Rva) + continue; + const auto gap = next->Rva - section->Rva - section->Data.size(); + if (best == sections.end() || gap < bestGap) { + best = section; + bestGap = gap; + } + } + if (best == sections.end()) return; + const auto next = std::next(best); + const auto memorySize = next->Rva + next->MemorySize() - best->Rva; + if (!next->Data.empty()) { + best->Data.resize(next->Rva - best->Rva); + best->Data.insert(best->Data.end(), next->Data.begin(), next->Data.end()); + } + best->VirtualSize = static_cast(memorySize); + sections.erase(next); + } +} + std::pair Locate(const std::vector& segments, const std::uint64_t rva, const std::uint64_t size) { for (std::size_t index = 0; index < segments.size(); ++index) { const auto& segment = segments[index]; @@ -420,6 +447,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { auto stubRva = Windows::AlignRva(image.GetEndRva()); Windows::WindowsTrampolineBuilder().Build(*input.Trampolines, image, sections, stubRva); } + FitDyldSegmentLimit(sections, 16 - guestSegments - 2); std::uint64_t guestEnd = image.GetEndRva(); for (auto& section : sections) { std::uint32_t protection = 0; diff --git a/core/relinker/relinker/tests/test_windows_bss.py b/core/relinker/relinker/tests/test_windows_bss.py index 6c0b149ae0..088ebc5a33 100644 --- a/core/relinker/relinker/tests/test_windows_bss.py +++ b/core/relinker/relinker/tests/test_windows_bss.py @@ -66,6 +66,7 @@ def main(): executed = subprocess.run([str(output)], capture_output=True, text=True, timeout=20) assert executed.returncode == 42, f"exit code {executed.returncode:#x}" check_relocated_zero_fill(relinker, work) + check_scattered_relocations_macos(relinker, work) print("Windows zero-filled memory tests passed") @@ -96,5 +97,33 @@ def check_relocated_zero_fill(relinker, work): assert executed.returncode == 42, f"exit code {executed.returncode:#x}" +def check_scattered_relocations_macos(relinker, work): + image = fixture() + file_size = struct.unpack_from(" Date: Mon, 19 Oct 2026 08:01:14 +0200 Subject: [PATCH 046/167] 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 ae662c873daf9920e98d840f4e85a64dbcbb686a Mon Sep 17 00:00:00 2001 From: ionut27001 <194206737+ionut27001@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:52:36 +0300 Subject: [PATCH 047/167] fix(libc): parse NaN payloads in strtod as measured on a PS5 In a payload process on a PS5 (firmware 12.70), the strtod and wcstod of libSceLibcInternal.sprx keep the payload of a NAN(...) sequence, read as FreeBSD 11's gdtoa hexnan reads it, under the quiet bit and with the sign. strtof, wcstof, strtold and wcstold return the positive default quiet NaN whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. Here every NaN lost its payload, and strtof and strtold kept the sign. The model reproduces the 64 NaN inputs that p11, p14, p27 and p33 measured, including blanks and other characters inside the parentheses and unterminated sequences, and the test now pins them. It also pins two p71 cases measured through strtod: nan(1 2), and "nan( " followed by NUL and ")". In hexnan, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console reads past it (p71); that case throws here. A blank right after "nan(" is skipped with a NUL check, and the console stopped at the NUL there. AI-assisted: yes Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Strings.cpp | 145 +++++++++++++++++++++++---- core/libs/tests/GuestMath.cpp | 132 ++++++++++++++++++++---- core/libs/tests/GuestWideStrings.cpp | 6 +- docs/dev/debt/libc.md | 2 +- 4 files changed, 243 insertions(+), 42 deletions(-) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 26f4d08bc3..8e1393c7e2 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include #include "prx/libc/include/General.hpp" #include "prx/libc/include/SonyCrt.hpp" @@ -37,31 +39,130 @@ std::string AsciiPrefix(const char16_t* text) { return prefix; } -bool IsNanSubject(const char* text) { +struct NanSubject { + const char* tail; + bool negative; +}; + +NanSubject FindNanSubject(const char* text) { while (*text == ' ' || (*text >= '\t' && *text <= '\r')) ++text; - if (*text == '+' || *text == '-') ++text; - return *text == 'n' || *text == 'N'; + bool negative = false; + if (*text == '+' || *text == '-') negative = *text++ == '-'; + if ((text[0] | 0x20) == 'n' && (text[1] | 0x20) == 'a' && (text[2] | 0x20) == 'n') return {text + 3, negative}; + return {nullptr, negative}; } -double QuietNan(double value, const char* text) { - const auto bits = std::bit_cast(value); - if ((bits & 0x7ff0000000000000u) != 0x7ff0000000000000u || !IsNanSubject(text)) return value; - return std::bit_cast((bits & 0x8000000000000000u) | 0x7ff8000000000000u); +unsigned HexNanDigit(unsigned char c) { + if (c >= '0' && c <= '9') return c - '0' + 0x10; + if (c >= 'a' && c <= 'f') return c - 'a' + 0x1a; + if (c >= 'A' && c <= 'F') return c - 'A' + 0x1a; + return 0; } -float QuietNan(float value, const char* text) { - const auto bits = std::bit_cast(value); - if ((bits & 0x7f800000u) != 0x7f800000u || !IsNanSubject(text)) return value; - return std::bit_cast((bits & 0x80000000u) | 0x7fc00000u); +void ShiftNanWords(std::uint32_t* words, int from, int to, int digits) { + const int shift = (8 - digits) * 4; + const int carry = 32 - shift; + int index = from; + do { + words[index] |= words[index + 1] << carry; + words[index + 1] >>= shift; + } while (++index < to); } -long double QuietNan(long double value, const char* text) { - std::uint16_t exponent; - std::memcpy(&exponent, reinterpret_cast(&value) + sizeof(std::uint64_t), sizeof(exponent)); - if ((exponent & 0x7fffu) != 0x7fffu || !IsNanSubject(text)) return value; - const std::uint64_t significand = 0xc000000000000000u; - std::memcpy(&value, &significand, sizeof(significand)); - return value; +std::uint64_t HexNanPayload(const char* open, const char*& end) { + std::uint32_t words[2] = {}; + constexpr int high = 1; + int top = high; + int group = high; + int digits = 0; + int groupStart = 0; + int inWord = 0; + const auto* s = reinterpret_cast(open); + const auto skipBlanksAndPrefix = [&s](bool inside) { + while (s[1] != 0 && s[1] <= ' ') ++s; + if (inside && s[1] == 0) throw std::invalid_argument("strtod: blanks after a NAN payload digit reach the terminating NUL, where the console reads past it"); + if (s[1] == '0' && (s[2] == 'x' || s[2] == 'X') && s[3] > ' ') s += 2; + }; + skipBlanksAndPrefix(false); + for (unsigned char c; (c = *++s) != 0;) { + const unsigned digit = HexNanDigit(c); + if (digit == 0) { + if (c <= ' ') { + if (groupStart < digits) { + if (top < group && inWord < 8) ShiftNanWords(words, top, group, inWord); + if (top <= 0) { + inWord = 8; + continue; + } + groupStart = digits; + words[--top] = 0; + group = top; + inWord = 0; + } + skipBlanksAndPrefix(true); + continue; + } + if (c == ')' && digits != 0) { + end = reinterpret_cast(s + 1); + break; + } + for (; c != 0; c = *++s) { + if (c == ')') { + end = reinterpret_cast(s + 1); + break; + } + } + return 0; + } + ++digits; + if (++inWord > 8) { + if (top <= 0) continue; + inWord = 1; + words[--top] = 0; + } + words[top] = (words[top] << 4) | (digit & 0xf); + } + if (digits == 0) return 0; + if (top < group && inWord < 8) ShiftNanWords(words, top, group, inWord); + if (top > 0) { + int to = 0; + int from = top; + do words[to++] = words[from++]; + while (from <= high); + do words[to++] = 0; + while (to <= high); + } else { + words[high] &= 0xffffffffu >> 12; + } + if (words[0] == 0 && words[1] == 0) words[0] = 1; + return (static_cast(words[1]) << 32) | words[0]; +} + +const char* NanSequenceEnd(const char* tail) { + if (*tail != '(') return tail; + const char* cursor = tail + 1; + while ((*cursor >= '0' && *cursor <= '9') || ((*cursor | 0x20) >= 'a' && (*cursor | 0x20) <= 'z') || *cursor == '_') ++cursor; + return *cursor == ')' ? cursor + 1 : tail; +} + +template +TFloat NanValue(const NanSubject& subject, const char*& end) { + if constexpr (std::is_same_v) { + end = subject.tail; + const std::uint64_t payload = *subject.tail == '(' ? HexNanPayload(subject.tail, end) : 0; + return std::bit_cast((subject.negative ? 0x8000000000000000ull : 0) | 0x7ff8000000000000ull | payload); + } else if constexpr (std::is_same_v) { + end = NanSequenceEnd(subject.tail); + return std::bit_cast(0x7fc00000u); + } else { + end = NanSequenceEnd(subject.tail); + TFloat value{}; + const std::uint64_t significand = 0xc000000000000000u; + const std::uint16_t signExponent = 0x7fff; + std::memcpy(&value, &significand, sizeof(significand)); + std::memcpy(reinterpret_cast(&value) + sizeof(significand), &signExponent, sizeof(signExponent)); + return value; + } } template @@ -75,10 +176,16 @@ auto ParseAsciiPrefix(const char16_t* text, char16_t** end, TParse parse) { template TFloat ParseFloat(const char* text, char** end, TFloat (*parse)(const char*, char**)) { + if (const NanSubject subject = FindNanSubject(text); subject.tail != nullptr) { + const char* stop = subject.tail; + const TFloat value = NanValue(subject, stop); + if (end != nullptr) *end = const_cast(stop); + return value; + } char* parsedEnd = nullptr; const TFloat value = parse(text, &parsedEnd); if (end != nullptr) *end = parsedEnd; - return parsedEnd == text ? TFloat{0} : QuietNan(value, text); + return parsedEnd == text ? TFloat{0} : value; } } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 176ac81a25..064db8fdea 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -315,42 +315,136 @@ static bool ExtendedBits(long double value, std::uint64_t mantissa, std::uint16_ } static void CheckNanConversions() { - const struct { const char* text; std::size_t consumed; bool negative; } cases[] = { - {"nan(0x123)", 10, false}, {" -NaN(1)!", 8, true}, {"nan(0x800000)", 13, false}, - {"nan(0x10000000000000)", 21, false}, {"nan(0x8000000000000000)", 23, false}, + struct MeasuredNan { + const char* text; + std::uint64_t bits; + std::size_t used; + std::size_t sequenceUsed; + bool extendedMeasured; + bool wideDoubleMeasured; + bool wideOtherMeasured; + }; + static constexpr MeasuredNan cases[] = { + {"nan", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"NAN", 0x7ff8000000000000ull, 3, 3, false, false, false}, + {"nan()", 0x7ff8000000000000ull, 5, 5, false, false, false}, + {"nan(1)", 0x7ff8000000000001ull, 6, 6, true, true, true}, + {"nan(0x10)", 0x7ff8000000000010ull, 9, 9, false, false, false}, + {"nan(abc)", 0x7ff8000000000abcull, 8, 8, false, false, false}, + {"nan(", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"-nan", 0xfff8000000000000ull, 4, 4, true, true, true}, + {"+nan(5)", 0x7ff8000000000005ull, 7, 7, false, false, false}, + {"nan(10)", 0x7ff8000000000010ull, 7, 7, false, false, false}, + {"nan(xyz)", 0x7ff8000000000000ull, 8, 8, false, false, false}, + {"nan(_)", 0x7ff8000000000000ull, 6, 6, false, false, false}, + {"nan(12", 0x7ff8000000000012ull, 3, 3, false, false, false}, + {"-nan(5)", 0xfff8000000000005ull, 7, 7, false, false, false}, + {"nan(0x8000000000000)", 0x7ff8000000000000ull, 20, 20, false, false, false}, + {"nan(0x10000000000000)", 0x7ff8000000000001ull, 21, 21, true, true, false}, + {"nan(0x400000)", 0x7ff8000000400000ull, 13, 13, false, false, false}, + {"NaN(0X1F)", 0x7ff800000000001full, 9, 9, false, false, false}, + {"nan(0x)", 0x7ff8000000000000ull, 7, 7, false, false, false}, + {"nan(123abc)", 0x7ff8000000123abcull, 11, 11, true, true, false}, + {"nan(1x2)", 0x7ff8000000000000ull, 8, 8, true, true, false}, + {"nan(0x1g)", 0x7ff8000000000000ull, 9, 9, true, true, false}, + {"nan(g1)", 0x7ff8000000000000ull, 7, 7, true, true, false}, + {"nan(0x0x1)", 0x7ff8000000000000ull, 10, 10, true, true, false}, + {"nan(fffffffffffff)", 0x7fffffffffffffffull, 18, 18, true, true, false}, + {"nan(1fffffffffffff)", 0x7fffffffffffffffull, 19, 19, true, true, false}, + {"nan(10000000000000)", 0x7ff8000000000001ull, 19, 19, true, true, false}, + {"nan(20000000000000)", 0x7ff8000000000001ull, 19, 19, true, true, false}, + {"nan(0x20000000000000)", 0x7ff8000000000001ull, 21, 21, true, true, false}, + {"nan(ffffffffffffffff)", 0x7fffffffffffffffull, 21, 21, true, true, false}, + {"nan(10000000000000000)", 0x7ff8000000000001ull, 22, 22, true, true, false}, + {"nan(0X)", 0x7ff8000000000000ull, 7, 7, true, true, false}, + {"nan(-1)", 0x7ff8000000000000ull, 7, 3, true, true, false}, + {"nan(+1)", 0x7ff8000000000000ull, 7, 3, true, true, false}, + {"nan( 1)", 0x7ff8000000000001ull, 7, 3, true, true, false}, + {"nan(1 )", 0x7ff8000100000000ull, 7, 3, true, true, false}, + {"nan(0)", 0x7ff8000000000001ull, 6, 6, true, true, false}, + {"nan(00001)", 0x7ff8000000000001ull, 10, 10, true, true, false}, + {"nan(0x0001)", 0x7ff8000000000001ull, 11, 11, true, true, false}, + {"nan(8000000000000)", 0x7ff8000000000000ull, 18, 18, true, true, false}, + {"nan(7ffffffffffff)", 0x7fffffffffffffffull, 18, 18, true, true, false}, + {"nan(123", 0x7ff8000000000123ull, 3, 3, true, true, false}, + {"nan(1)x", 0x7ff8000000000001ull, 6, 6, true, true, false}, + {"-nan(1)", 0xfff8000000000001ull, 7, 7, true, true, false}, + {"nan(abcdefABCDEF)", 0x7ff8abcdefabcdefull, 17, 17, true, true, false}, + {"nan(0xABCDEF)", 0x7ff8000000abcdefull, 13, 13, true, true, false}, + {"nan(9)", 0x7ff8000000000009ull, 6, 6, true, true, false}, + {"nan(1_2)", 0x7ff8000000000000ull, 8, 8, true, true, false}, + {"nan(_1)", 0x7ff8000000000000ull, 7, 7, true, true, false}, + {"nan(1)(2)", 0x7ff8000000000001ull, 6, 6, true, true, false}, + {"+nan", 0x7ff8000000000000ull, 4, 4, true, true, true}, + {"NaN", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"-NAN(5)", 0xfff8000000000005ull, 7, 7, true, true, true}, + {"nan(5)", 0x7ff8000000000005ull, 6, 6, true, true, true}, + {"-nan(abc)", 0xfff8000000000abcull, 9, 9, true, true, true}, + {" -nan", 0xfff8000000000000ull, 6, 6, true, true, true}, + {"nan(12 x", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"nan(12x", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"nan(1", 0x7ff8000000000001ull, 3, 3, true, true, true}, + {"nan(a b)", 0x7ff8000a0000000bull, 8, 3, true, true, true}, + {"nan(ab)", 0x7ff80000000000abull, 7, 7, true, true, true}, + {"nanx", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"-nan()", 0xfff8000000000000ull, 6, 6, true, true, true}, + {"nan(0x8000000000001)", 0x7ff8000000000001ull, 20, 20, true, true, true}, }; bool correct = true; for (const auto& test : cases) { - const char* expectedEnd = test.text + test.consumed; - const std::uint64_t expectedDouble = test.negative ? 0xfff8000000000000ull : 0x7ff8000000000000ull; char* end = nullptr; const auto value = std::bit_cast(strtod_nid_postfix(test.text, &end)); - if (value != expectedDouble || end != expectedEnd) { + if (value != test.bits || end != test.text + test.used) { std::fprintf(stderr, "strtod(\"%s\") = 0x%016llx\n", test.text, static_cast(value)); correct = false; } - const auto ascii = std::bit_cast(atof_nid_postfix(test.text)); - if (ascii != expectedDouble) { - std::fprintf(stderr, "atof(\"%s\") = 0x%016llx\n", test.text, static_cast(ascii)); + if (std::bit_cast(atof_nid_postfix(test.text)) != test.bits) { + std::fprintf(stderr, "atof(\"%s\")\n", test.text); correct = false; } const auto single = std::bit_cast(strtof_nid_postfix(test.text, &end)); - if (single != (test.negative ? 0xffc00000u : 0x7fc00000u) || end != expectedEnd) { + if (single != 0x7fc00000u || end != test.text + test.sequenceUsed) { std::fprintf(stderr, "strtof(\"%s\") = 0x%08x\n", test.text, static_cast(single)); correct = false; } - const long double extended = strtold_nid_postfix(test.text, &end); - if (!ExtendedBits(extended, 0xc000000000000000ull, test.negative ? 0xffff : 0x7fff) || end != expectedEnd) { - std::uint64_t significand = 0; - std::uint16_t signExponent = 0; - std::memcpy(&significand, &extended, sizeof(significand)); - std::memcpy(&signExponent, reinterpret_cast(&extended) + sizeof(significand), sizeof(signExponent)); - std::fprintf(stderr, "strtold(\"%s\") = 0x%04x'%016llx\n", test.text, static_cast(signExponent), - static_cast(significand)); - correct = false; + if (test.extendedMeasured) { + const long double extended = strtold_nid_postfix(test.text, &end); + if (!ExtendedBits(extended, 0xc000000000000000ull, 0x7fff) || end != test.text + test.sequenceUsed) { + std::fprintf(stderr, "strtold(\"%s\")\n", test.text); + correct = false; + } + } + char16_t wide[64] = {}; + for (std::size_t i = 0; test.text[i] != 0; ++i) wide[i] = static_cast(test.text[i]); + char16_t* wideEnd = nullptr; + if (test.wideDoubleMeasured) { + const auto wideValue = std::bit_cast(wcstod_nid_postfix(wide, &wideEnd)); + if (wideValue != test.bits || wideEnd != wide + test.used) { + std::fprintf(stderr, "wcstod(\"%s\") = 0x%016llx\n", test.text, static_cast(wideValue)); + correct = false; + } + } + if (test.wideOtherMeasured) { + const auto wideSingle = std::bit_cast(wcstof_nid_postfix(wide, &wideEnd)); + const bool singleOk = wideSingle == 0x7fc00000u && wideEnd == wide + test.sequenceUsed; + const long double wideExtended = wcstold_nid_postfix(wide, &wideEnd); + if (!singleOk || !ExtendedBits(wideExtended, 0xc000000000000000ull, 0x7fff) || wideEnd != wide + test.sequenceUsed) { + std::fprintf(stderr, "wcstof/wcstold(\"%s\")\n", test.text); + correct = false; + } } } Require(correct); + for (const char* text : {"nan(1 ", "nan(12 \t"}) { + bool threw = false; + try { strtod_nid_postfix(text, nullptr); } catch (...) { threw = true; } + Require(threw); + } + struct PlainNan { const char* text; std::uint64_t bits; std::size_t used; }; + for (const PlainNan& plain : {PlainNan{"nan(1 2)", 0x7ff8000100000002ull, 8}, PlainNan{"nan( \0)", 0x7ff8000000000000ull, 3}}) { + char* end = nullptr; + Require(std::bit_cast(strtod_nid_postfix(plain.text, &end)) == plain.bits && end == plain.text + plain.used); + } } static void CheckHypotSpecialValue() { diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index abbba3196d..809d47fe3e 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -322,12 +322,12 @@ int main() { const char16_t* half = u"-1.5é"; require(wcstold_nid_postfix(half, &end) == -1.5L && end == half + 4); const char16_t* wideNan = u" -nan(0x123)é"; - require(std::bit_cast(wcstod_nid_postfix(wideNan, &end)) == 0xfff8000000000000u && end == wideNan + 12); - require(std::bit_cast(wcstof_nid_postfix(wideNan, &end)) == 0xffc00000u && end == wideNan + 12); + require(std::bit_cast(wcstod_nid_postfix(wideNan, &end)) == 0xfff8000000000123u && end == wideNan + 12); + require(std::bit_cast(wcstof_nid_postfix(wideNan, &end)) == 0x7fc00000u && end == wideNan + 12); const long double wideExtended = wcstold_nid_postfix(wideNan, &end); std::uint64_t significand; std::memcpy(&significand, &wideExtended, sizeof(significand)); - require(std::isnan(wideExtended) && std::signbit(wideExtended) && significand == 0xc000000000000000u && end == wideNan + 12); + require(std::isnan(wideExtended) && !std::signbit(wideExtended) && significand == 0xc000000000000000u && end == wideNan + 12); require(wcscoll_nid_postfix(u"a", u"b") < 0); require(wcscoll_nid_postfix(u"￿", u"a") > 0); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 9b31a4e2a0..35e52a3953 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -61,7 +61,7 @@ - [fgetln](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) throws: in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5FE80) the export is a `hlt` instruction, which faults in user mode and ends the process - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. -- [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `NAN(n-char-sequence)` converts to the quiet NaN that `nan` and `nanf` return, negated after a minus sign, and ignores the sequence as they ignore their tag. This is inferred from C17 7.12.11.2p2, which makes `nan("n")` equivalent to `strtod("NAN(n)", NULL)`, and not observed on the console. No console `nanl` was observed, so `strtold` returns the x87 default quiet NaN (exponent `0x7fff`, significand `0xc000000000000000`) +- [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. From 55a3eeb074eae9d50db090ef3584b04ab1066c4d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 06:27:51 +0000 Subject: [PATCH 048/167] fix(agc): retain shader memory region vectors --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 63 ++++++++++++------- .../tests/execution/ImageTableCache.cpp | 2 +- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 0627a647d2..866e19e8a3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -826,7 +826,8 @@ void verifyOutOfRangeConstantReads() { require(plan->info.images.size() == 2u && plan->info.samplers.size() == 1u, "out-of-range s_buffer_load: the two T#s were not planned as one image each"); AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(request); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; static_cast(Recompile(request, *capture)); const auto holds = [&](const std::array& words) { return std::ranges::any_of(capture->snapshot.images, [&](const DescriptorValue& value) { @@ -1001,8 +1002,10 @@ void verifyGuardedDescriptorSources() { fill(valid, mode); AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(storeRequest); - storeRequest.context.memory = memory.Regions(); - return Recompile(storeRequest, *capture); + const auto memoryRegions = memory.Regions(); + auto compileRequest = storeRequest; + compileRequest.context.memory = memoryRegions; + return Recompile(compileRequest, *capture); }; const auto storePlan = GetResourcePlan(storeRequest); require(storePlan->info.images.size() == 2u && storePlan->descriptorSources.size() == 1u, "guarded images: the two stores were not planned as two images of one T#"); @@ -3506,7 +3509,8 @@ void verifyDescriptorPhis() { require(plan->info.images.size() == images && plan->info.samplers.size() == samplers && plan->info.sampledPairs.size() == 2u, "descriptor Phi: the edges were not given one resource each"); AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(request); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; const auto compiled = Recompile(request, *capture); require(countOps(compiled->spirv, OpImageSampleExplicitLod) == 2u, "descriptor Phi: the specialized SPIR-V does not sample once per edge"); #if ANYPS5_ENABLE_SPIRV_TOOLS @@ -3537,7 +3541,8 @@ void verifyDescriptorPhis() { auto twoLane = makeRequest(samplerCode, 64u); AgcDriver::ShaderMemory twoLaneMemory({}); const auto twoLaneCapture = twoLaneMemory.Capture(twoLane); - twoLane.context.memory = twoLaneMemory.Regions(); + const auto twoLaneRegions = twoLaneMemory.Regions(); + twoLane.context.memory = twoLaneRegions; require(countOps(Recompile(twoLane, *twoLaneCapture)->spirv, OpImageSampleExplicitLod) == 4u, "descriptor Phi: the specialized two-lane SPIR-V does not sample once per edge and half"); auto dynamic = makeRequest(dynamicCode); @@ -3559,7 +3564,8 @@ void verifyDescriptorPhis() { require(sequentialPlan->info.images.size() == 2u && sequentialPlan->info.samplers.size() == 2u, "descriptor Phi: a T# and an S# chosen in separate blocks were not split into their SRT descriptors"); AgcDriver::ShaderMemory sequentialMemory({}); const auto sequentialCapture = sequentialMemory.Capture(sequential); - sequential.context.memory = sequentialMemory.Regions(); + const auto sequentialRegions = sequentialMemory.Regions(); + sequential.context.memory = sequentialRegions; static_cast(Recompile(sequential, *sequentialCapture)); const auto planRestoredSampler = [&](std::span code, const char* message) { @@ -3568,7 +3574,8 @@ void verifyDescriptorPhis() { require(plan->info.samplers.size() == 1u && plan->info.sampledPairs.size() == 1u, message); AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(request); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; static_cast(Recompile(request, *capture)); }; std::vector restoredSamplerCode{0xf40c0200u, 0xfa000000u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbe930380u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20400u, 0xbe900380u, 0xbe910380u, 0xbe9c0414u, 0xbe9e0416u, 0xbf068011u, 0xbf850001u, 0x80128112u, 0xbe94041cu, 0xbe96041eu, 0x80118111u, 0xbf0a8211u, 0xbf85fff6u, 0xf09c0f08u, 0x00a20000u, 0x80108110u, 0xbf0a8210u, 0xbf85fff0u, 0x80138113u, 0xbf0a8213u, 0xbf85ffecu, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xe0700000u, 0x80060400u, 0x7d880090u, 0xbea23c6au, 0x4a020081u, 0xe0700000u, 0x80060100u, 0xbf810000u}; @@ -3742,7 +3749,8 @@ void verifyLaneSelectedDescriptors() { require(plan->info.images.size() == 2u && plan->info.samplers.size() == 1u, "lane-selected T#: the T#s selected per lane were not planned as one image each"); AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(request); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; static_cast(Recompile(request, *capture)); const auto holds = [&](const std::array& words) { return std::ranges::any_of(capture->snapshot.images, [&](const DescriptorValue& value) { @@ -3788,7 +3796,8 @@ void verifyProgramCounterRelativeData() { }; AgcDriver::ShaderMemory memory({}); static_cast(memory.Capture(request)); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; const auto first = Recompile(request); require(dataBase(first) == codeAddress + 56u, "program counter data: the V# does not name the data at the shader's address"); auto relocated = request; @@ -5457,7 +5466,8 @@ void verifyComputedTexelOffsets() { request.useCache = false; AgcDriver::ShaderMemory memory({}); static_cast(memory.Capture(request)); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; return Recompile(request).spirv; }; const auto sampleOperands = [](std::span words) { @@ -5507,7 +5517,8 @@ void verifyShaderClockScopes() { request.useCache = false; AgcDriver::ShaderMemory memory({}); static_cast(memory.Capture(request)); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; const auto words = Recompile(request).spirv; std::map constants; std::vector scopeIds; @@ -5821,7 +5832,8 @@ void verifyUnnormalizedSamplers() { request.layout.pushConstantSizeBytes = 128; AgcDriver::ShaderMemory memory({}); static_cast(memory.Capture(request)); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; return Recompile(request); }; const auto flags = [](const RecompileResult& result, DescriptorRole role) { @@ -6244,7 +6256,8 @@ void verifyTwoLaneUniformValues() { request.layout.pushConstantSizeBytes = 128; AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(request); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; const auto words = Recompile(request, *capture)->spirv; std::size_t count = 0; for (std::size_t cursor = 5; cursor < words.size();) { @@ -6825,7 +6838,8 @@ void verifyGuardedNullPointers() { const auto valid = validMemory.Capture(branch.request); require(valid->snapshot.srtPoison.empty() && valid->plan->guardedSrtSlots.size() == 1u && slotPc(*valid->plan, valid->plan->guardedSrtSlots[0]) == 0x2cu && GuardRecords(*valid), "guarded pointer: a mapped nested pointer was poisoned, or its read has no guard"); require(valid->snapshot.flattenedSrt.at(valid->plan->guardedSrtSlots[0]) == payload, "guarded pointer: a mapped nested read does not hold its word"); - branch.request.context.memory = validMemory.Regions(); + const auto validRegions = validMemory.Regions(); + branch.request.context.memory = validRegions; 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"); @@ -6867,20 +6881,23 @@ void verifyGuardedNullPointers() { const bool throughPointer = own == 0x38u && poison.address == unmapped; require(poison.pc == 0x2cu && (pointerWord || throughPointer), "guarded pointer: a derived poison does not name the inaccessible read"); } - chain.request.context.memory = chainMemory.Regions(); + const auto chainRegions = chainMemory.Regions(); + chain.request.context.memory = chainRegions; const auto chainResult = Recompile(chain.request, *chained); indirect = 0; point(static_cast(reinterpret_cast(&indirect))); AgcDriver::ShaderMemory nullChainMemory({}); const auto nullChain = nullChainMemory.Capture(chain.request); require(nullChain->snapshot.srtPoison.size() == 1u && nullChain->snapshot.srtPoison[0].pc == 0x38u && nullChain->snapshot.srtPoison[0].address == 0u && GuardRecords(*nullChain), "guarded pointer: a null pointer read from memory did not poison the read through it"); - chain.request.context.memory = nullChainMemory.Regions(); + const auto nullChainRegions = nullChainMemory.Regions(); + chain.request.context.memory = nullChainRegions; const auto nullChainResult = Recompile(chain.request, *nullChain); indirect = payloadAddress; AgcDriver::ShaderMemory mappedChainMemory({}); const auto mappedChain = mappedChainMemory.Capture(chain.request); require(mappedChain->snapshot.srtPoison.empty() && mappedChain->plan->guardedSrtSlots.size() == 3u && GuardRecords(*mappedChain), "guarded pointer: a mapped pointer chain was poisoned, or its reads have no guards"); - chain.request.context.memory = mappedChainMemory.Regions(); + const auto mappedChainRegions = mappedChainMemory.Regions(); + chain.request.context.memory = mappedChainRegions; 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"); require(samePipeline(*chainResult, *mappedChainResult) && samePipeline(*nullChainResult, *mappedChainResult), "guarded pointer: a chain's poison changed the variant, the specialization or the module"); @@ -6903,7 +6920,8 @@ void verifyGuardedNullPointers() { AgcDriver::ShaderMemory zeroMemory({}); const auto zeroCapture = zeroMemory.Capture(descriptor.request); require(zeroCapture->snapshot.srtPoison.empty() && zeroBuffer(*zeroCapture) && GuardRecords(*zeroCapture), "guarded pointer: a zero V# read from mapped memory was poisoned or not read"); - descriptor.request.context.memory = zeroMemory.Regions(); + const auto zeroRegions = zeroMemory.Regions(); + descriptor.request.context.memory = zeroRegions; const auto zeroResult = Recompile(descriptor.request, *zeroCapture); for (const auto pointer : {std::uint64_t{0}, unmapped}) { point(pointer); @@ -6959,7 +6977,8 @@ void verifyGuardedNullPointers() { const auto rootlessCapture = rootlessMemory.Capture(rootless.request); require(rootlessCapture->snapshot.nullRootReads != 0u && GuardRecords(*rootlessCapture), "guarded pointer: reads through a null user-data pointer were not counted"); require(std::all_of(rootlessCapture->snapshot.buffers.begin(), rootlessCapture->snapshot.buffers.end(), [](const DescriptorValue& value) { return std::all_of(value.dwords.begin(), value.dwords.end(), [](std::uint32_t word) { return word == 0u; }); }), "guarded pointer: a V# read through a null user-data pointer is not zero"); - rootless.request.context.memory = rootlessMemory.Regions(); + const auto rootlessRegions = rootlessMemory.Regions(); + rootless.request.context.memory = rootlessRegions; const auto rootlessResult = Recompile(rootless.request, *rootlessCapture); require(rootlessResult->poisonedSrtReads >= rootlessCapture->snapshot.nullRootReads, "guarded pointer: a capture through a null user-data pointer is cacheable"); point(0u); @@ -7449,7 +7468,8 @@ int main(int argc, char** argv) { const auto changedCapture = changedMemory.Capture(request); auto changed = updated; changed.useCache = true; - changed.context.memory = changedMemory.Regions(); + const auto changedRegions = changedMemory.Regions(); + changed.context.memory = changedRegions; auto before = updated; before.useCache = true; before.context.memory = updatedRegions; @@ -7512,7 +7532,8 @@ int main(int argc, char** argv) { AgcDriver::ShaderMemory nullRoot({}); const auto nullRootCapture = nullRoot.Capture(request); require(nullRootCapture->snapshot.nullRootReads != 0u, "reads through a null user-data pointer were not counted"); - request.context.memory = nullRoot.Regions(); + const auto nullRootRegions = nullRoot.Regions(); + request.context.memory = nullRootRegions; require(Recompile(request, *nullRootCapture)->poisonedSrtReads >= nullRootCapture->snapshot.nullRootReads, "a capture through a null user-data pointer is cacheable"); std::cout << "Shader memory capture, strict validation and deterministic replay passed\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCache.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCache.cpp index 9a112123fe..2e2bf16ed9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCache.cpp @@ -131,7 +131,7 @@ Captured Capture(std::byte* table) { result.memory = std::make_unique(std::span{}); result.capture = result.memory->Capture(request); result.regions = result.memory->TakeRecentRegions(); - request.context.memory = result.memory->Regions(); + request.context.memory = result.regions; result.compiled = Recompile(request, *result.capture); return result; } 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 049/167] 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 050/167] 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 4bd0f1d450441de88b96d48156b03a2772a5b5c1 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sun, 18 Oct 2026 13:36:09 -0400 Subject: [PATCH 051/167] fix(agc): preserve infinite depth bounds --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + .../libSceAgcDriver/Graphics/src/State.cpp | 10 +++++-- .../prx/libSceAgcDriver/tests/Graphics.cpp | 28 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index b8674a1f48..c136baa6e1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1804,6 +1804,7 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_undithered_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --undithered-alpha-to-coverage-state) + add_test(NAME agc_driver_depth_bounds_state COMMAND agc_driver_graphics_tests --depth-bounds-state) add_test(NAME agc_driver_msaa_dcc_state COMMAND agc_driver_graphics_tests --multisample-dcc-state) add_test(NAME agc_driver_unused_viewport_depth COMMAND agc_driver_graphics_tests --unused-viewport-depth) set_tests_properties(agc_driver_unused_viewport_depth PROPERTIES TIMEOUT 30) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 82a25682d1..af7ce6271c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -53,6 +53,12 @@ float readFloat(const Registers& registers, std::uint32_t offset) { return value; } +float readDepthBound(const Registers& registers, std::uint32_t offset) { + const auto value = std::bit_cast(read(registers, offset)); + if (std::isnan(value)) Require(false, "NaN depth bound at DWORD " + std::to_string(offset)); + return value; +} + std::string zeroMessage(std::uint32_t offset, std::uint32_t value, const char* name) { char detail[64]; std::snprintf(detail, sizeof(detail), " (register 0x%x = 0x%08x)", offset, value); @@ -349,8 +355,8 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx result.depthBoundsTest = (depthControl & 8u) != 0; if (result.depthBoundsTest) { Require(zFormat != 0, "depth bounds without a depth plane"); - result.minDepthBounds = readFloat(cx, 0x008); - result.maxDepthBounds = readFloat(cx, 0x009); + result.minDepthBounds = readDepthBound(cx, 0x008); + result.maxDepthBounds = readDepthBound(cx, 0x009); } if (depthClear) { result.depthTest = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 21bdee2218..fbda3b3ffa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -51,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -2577,6 +2578,28 @@ void DepthBoundsBiasTests() { queue.context[0x009] = bits(0.75f); auto state = AgcDriver::Graphics::DecodeState(queue); Require(state.depthTest && state.depthBoundsTest && state.minDepthBounds == 0.25f && state.maxDepthBounds == 0.75f, "depth bounds decode changed"); + const auto infinity = std::numeric_limits::infinity(); + for (const auto bounds : {std::array{0.0f, infinity}, std::array{-infinity, 1.0f}, std::array{-infinity, infinity}}) { + queue.context[0x008] = bits(bounds[0]); + queue.context[0x009] = bits(bounds[1]); + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depthBoundsTest && state.minDepthBounds == bounds[0] && state.maxDepthBounds == bounds[1], "infinite depth bounds changed"); + AgcDriver::Graphics::Context context{}; + expectFailure([&] { AgcDriver::Graphics::ValidateDepthBounds(context, state); }, "depth bounds outside [0, 1]"); + context.depthRangeUnrestricted = true; + AgcDriver::Graphics::ValidateDepthBounds(context, state); + } + for (const auto offset : {0x008u, 0x009u}) { + queue.context[0x008] = bits(0.25f); + queue.context[0x009] = bits(0.75f); + queue.context[offset] = bits(std::numeric_limits::quiet_NaN()); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "NaN depth bound"); + } + queue.context[0x008] = bits(0.25f); + queue.context[0x009] = bits(0.75f); + queue.context[0x00b] = bits(infinity); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "non-finite register"); + queue.context[0x00b] = bits(0.0f); queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); @@ -9815,6 +9838,11 @@ int main(int argc, char** argv) { std::cout << "Multisampled DCC state checks passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--depth-bounds-state") { + DepthBoundsBiasTests(); + std::cout << "Depth bounds state checks passed\n"; + return 0; + } { const AgcDriver::Graphics::Context context{}; const AgcDriver::Graphics::State state{}; From 8bd880ceb1b358e6e58bb5e9337eff01e34388a3 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sun, 18 Oct 2026 14:49:51 -0400 Subject: [PATCH 052/167] test(agc): configure depth bounds state runtime environment --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index c136baa6e1..0eef97b9eb 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1808,7 +1808,7 @@ if(BUILD_TESTING) add_test(NAME agc_driver_msaa_dcc_state COMMAND agc_driver_graphics_tests --multisample-dcc-state) add_test(NAME agc_driver_unused_viewport_depth COMMAND agc_driver_graphics_tests --unused-viewport-depth) set_tests_properties(agc_driver_unused_viewport_depth PROPERTIES TIMEOUT 30) - set_tests_properties(agc_driver_undithered_alpha_to_coverage_state PROPERTIES + set_tests_properties(agc_driver_undithered_alpha_to_coverage_state agc_driver_depth_bounds_state PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_driver_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --alpha-to-coverage-state) set_tests_properties(agc_driver_alpha_to_coverage_state PROPERTIES @@ -1826,7 +1826,7 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_dithered_alpha_to_coverage 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_alpha_to_coverage_state agc_driver_undithered_alpha_to_coverage_state agc_driver_msaa_dcc_state agc_driver_unused_viewport_depth PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_alpha_to_coverage_state agc_driver_undithered_alpha_to_coverage_state agc_driver_depth_bounds_state agc_driver_msaa_dcc_state agc_driver_unused_viewport_depth PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") set_tests_properties(agc_driver_dithered_alpha_to_coverage ${multisampledDccClearTests} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") From a20968ee254bda131c4112c4435c1d1e11cf3b6f Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 14 Oct 2026 20:56:33 -0400 Subject: [PATCH 053/167] fix(shader): retain adjacent anchor byte loads on RADV --- core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp | 3 +++ core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp | 2 +- docs/dev/debt/shader.md | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp index 113991ac43..54e68718f7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp @@ -307,6 +307,9 @@ static void RunBdaAnchorTests(const Context& context) { for (std::uint32_t lane = 0; lane < 32u; ++lane) lows.push_back(lane * 0x24u); std::vector strided; for (std::uint32_t lane = 0; lane < lanes; ++lane) strided.push_back(lane * 16u); + auto halfword = build({{{-1, 16, 1, 1}}, false, false, false, lanes}); + compare(*halfword, {guest + 0x100}, {1u}); + compare(*halfword, {guest + 0x100}, {0u, 1u}); auto continuing = build({reads, false, false, false, lanes}); compare(*continuing, anchors, lows); compare(*continuing, {guest + 0x400}, strided); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index 3d4f5d80bd..e38e1ffb1a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -607,7 +607,7 @@ std::array EmitAnchoredBdaRead(SpirvEmitterState& state, const const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpConvertUToPtr, bytePointer, pointer, element); const auto loaded = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, byteType, loaded, pointer, BdaAccessMask(coherent), 1u); + state.module.AddFunction(spv::OpLoad, byteType, loaded, pointer, BdaAccessMask(coherent || bits == 16u), 1u); value = Binary(state, spv::OpBitwiseOr, u32, value, Binary(state, spv::OpShiftLeftLogical, u32, Unary(state, spv::OpUConvert, u32, loaded), ConstantU32(state, byte * 8u))); } directValues[0] = value; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 2559d355f3..334e0a452c 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -155,6 +155,7 @@ - [Static scalar calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair is free to use wherever no return address can reach: a dataflow over the decoded instructions follows each dword of the pair from the calls that write it until a scalar load, SALU or VALU result is known to overwrite it (a VALU mask counts as one SGPR unless the request says wave64), and an instruction there that may read a dword still holding a return address throws; SOPC compares are exempt, as they only set SCC. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. - [Relative vector addressing](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in zero-initialised function-scope storage per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window. [`IndexedWindowSlots`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) bounds each relative index through the IR (bit-field extracts, masks, shifts, adds, selects, phis, `ReadFirstLane` and the window stores that reach a load); the storage is an array of the slots below the largest bound that is under half the window, and one function variable per slot after it. A constant index uses the array element or the slot's variable; a relative access whose index has no such bound, or lands past the array at run time, indexes the array below its size and otherwise branches through an `OpSwitch` over the slot variables, so the bound only decides speed. When no index has a bound under half the window, the whole window is the array, as with `APS5_NO_WINDOW_NARROWING=1` (a recompiler switch in the shader cache key). Measured on an NVIDIA GeForce RTX 3060 (driver 615.78.08, Linux) with a compute program whose 167-register window an ALU loop reads and writes in full and whose relative accesses take `m0 = s8 & 3`: a 2048-group dispatch took 70.2 ms in wave32 and 123.3 ms in wave64 with the whole window in the array (as before narrowing) and 1.9-2.1 ms and 7.8 ms narrowed; with a second relative access whose index has no bound and lands past the array at run time (index 50 or 166), 113 ms and 5.8-6.0 ms in wave32. In the PPSA08427 (Killing Floor 3) hub it made no measurable difference to presents or to the GPU time of the eight most expensive programs. Whether the host compiler keeps the array and the slot variables in registers or spills them to scratch is not measured. A register in the window the program never wrote reads 0, as an unwritten SSA register does; the console's value is undefined. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this. Pixel programs whose relative span contains the packed ancillary input instead keep that span in SSA and implement indexed operations with compare/select chains. This keeps ancillary definitions visible to field lowering and masked-write proofs; storing the unmodelled whole word in a private array would require composing unknown bits. A fold after masked-select elimination lowers fields whose consumers become visible only after redundant copies are removed. A whole-word use that the masked-select pass keeps gets the composed word described under the packed pixel ancillary word. Both representations include implicit entry inputs in the inferred register limit and retain unsigned M0, inactive-write and DPP update rules; wave32/wave64 native Vulkan readbacks cover relative reads, writes, split-offset swaps, inactive writes, DPP masks and out-of-range accesses, but console comparison and the performance cost of selecting over this span are unverified. - [s_call_b64](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Measured on RDNA2 (gfx1036) in wave32 and wave64 with SIMM16 = 3: the link pair receives `PC + 4`, the branch goes to `PC + 4 + signext(SIMM16) * 4`, and `s_setpc_b64` of the link returns to the instruction after the call. Execution on the PS5 (gfx1013) and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation. +- [BDA anchor halfword reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) retain volatile byte loads even for noncoherent memory. On RX 6650 XT / RADV Mesa 26.2.4, the valid SPIR-V for two adjacent nonvolatile byte loads returned `0x80a0` for bytes `0xa0, 0xa1`, where lookup-based reads returned `0xa1a0`. Adding the volatile operand makes the same GPU exactness fixture pass, including mixed direct/fallback lanes. The driver transformation responsible has not been isolated; removing this restriction needs equivalent readback coverage on the affected driver. Single-byte reads keep their original access mask. - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. - [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests From 9dd208fb9ade8ff6b21c84316b2c8cdfcbf22fc0 Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Mon, 19 Oct 2026 11:37:06 +0200 Subject: [PATCH 054/167] fix(tools): strip the NID disambiguation suffix from declared names The NID patcher and check_conventions resolve fstat_nid_disambig1_nid_postfix to fstat. import_audit hashed it as fstat_nid_disambig1, so a throwing stub with the suffix was keyed under the wrong NID, and nid_names reported fstat and pwrite as rename-ready instead of already-implemented. --- tools/import_audit.py | 3 +-- tools/nid_names.py | 10 ++++++---- tools/tests/test_import_audit.py | 13 +++++++++++++ tools/tests/test_nid_names.py | 10 ++++++++++ 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index 320db9d959..fa1f3d15b6 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -13,7 +13,6 @@ import progress ROOT = Path(__file__).resolve().parent.parent PRX = ROOT / "core" / "libs" / "prx" -NID_POSTFIX = "_nid_postfix" MODULE_SUFFIX = "#" CLASSES = ("implemented", "stub", "absent", "module") PE_DIRECTORY_OFFSET = {0x20b: 112, 0x10b: 96} @@ -447,7 +446,7 @@ def read_modules(directories): def nids_of(name, aliases): if name in aliases: return aliases[name] - return {nid_names.compute_nid(name.removesuffix(NID_POSTFIX))} + return {nid_names.compute_nid(nid_names.strip_nid_postfix(name))} def source_stubs(source): diff --git a/tools/nid_names.py b/tools/nid_names.py index 6ca6262fcb..21a078ff77 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -43,6 +43,10 @@ def compute_nid(symbol): return "".join(out) +def strip_nid_postfix(name): + return re.sub(r"_nid_disambig[0-9]+$", "", name.removesuffix(NID_POSTFIX)) + + def load_db(path): if not path.exists(): path.parent.mkdir(parents=True, exist_ok=True) @@ -126,12 +130,10 @@ def collect_real_names(): for match in source_definitions(text): name = match.group(1) names.add(name) - if name.endswith(NID_POSTFIX): - names.add(name[:-len(NID_POSTFIX)]) + names.add(strip_nid_postfix(name)) for name in source_assembly_definitions(text): names.add(name) - if name.endswith(NID_POSTFIX): - names.add(name[:-len(NID_POSTFIX)]) + names.add(strip_nid_postfix(name)) return names diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 39c0e5e614..a5ab5dc9c7 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -1047,6 +1047,19 @@ class ImportAuditTests(unittest.TestCase): self.assertNotIn(("libSceTwin.native.prx", twin), stubs) self.assertNotEqual(nid_names.compute_nid("printf"), nid_names.compute_nid("printf_nid_postfix")) + def test_disambiguated_stubs_use_the_nid_of_their_symbol(self): + source = self.work.source + write_source(source, "libkernel", STUB_SOURCE.format(name="fstat_nid_disambig1_nid_postfix") + + STUB_SOURCE.format(name="pwrite_nid_disambig12") + STUB_SOURCE.format(name="sceKeep_nid_disambigA")) + stubs = import_audit.source_stubs(source) + fstat = nid_names.compute_nid("fstat") + self.assertEqual(stubs, { + ("libkernel.prx", fstat): "fstat_nid_disambig1_nid_postfix", + ("libkernel.prx", nid_names.compute_nid("pwrite")): "pwrite_nid_disambig12", + ("libkernel.prx", nid_names.compute_nid("sceKeep_nid_disambigA")): "sceKeep_nid_disambigA"}) + records, _ = import_audit.audit([(fstat, "libkernel.prx", False)], {fstat: ["libkernel.prx"]}, stubs, {"libkernel.prx"}, set()) + self.assertEqual((records[0]["class"], records[0]["name"]), ("stub", "fstat_nid_disambig1_nid_postfix")) + def test_stub_in_one_library_is_not_hidden_by_another(self): source = self.work.source write_source(source, "libA", DONE_SOURCE.format(name="sceTwinCall")) diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index 5054e58516..20cb9fb060 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -282,6 +282,16 @@ auto example = "APS5_ASM_FUNCTION(\"sceQuoted_nid_postfix\")"; "sigAsmCall", "sigAsmCall_nid_postfix", }) + def test_disambiguated_definitions_are_real_names_of_their_symbol(self): + self.write_source('int APS5_VABI fstat_nid_disambig1_nid_postfix(int fd) { return 0; }\n' + 'int APS5_VABI pwrite_nid_disambig12(int fd) { return 0; }\n' + 'int APS5_VABI sceKeep_nid_disambigA() { return 0; }\n') + self.assertEqual(nid_names.collect_real_names(), { + "fstat", "fstat_nid_disambig1_nid_postfix", + "pwrite", "pwrite_nid_disambig12", + "sceKeep_nid_disambigA", + }) + def test_formatted_alias_macro_is_detected(self): self.write_source('APS5_EXPORT (\n "AAAAAAAAAAA" ,\n exampleUnknown00\n );\n') self.assertEqual(nid_names.collect_unknowns(), {"AAAAAAAAAAA": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown00"}) From 67575aae8126ce3101eac4e7c378c1b52987a456 Mon Sep 17 00:00:00 2001 From: Alexandre Sanchez Date: Sun, 18 Oct 2026 20:27:14 +0200 Subject: [PATCH 055/167] fix(agc): guard cached images against multisample color targets Check described image ranges before either resource reuse proof, preserving the combined color and resident-depth guard for existing callers. Add standalone draw and compute regressions for sampled and storage images, disjoint targets, guarded fresh builds, and target retirement. --- .../Graphics/include/MultisampleTarget.hpp | 1 + .../Graphics/src/MultisampleTarget.cpp | 4 + .../Graphics/src/ShaderResources.cpp | 1 + .../execution/CachedImageMultisampleGuard.cpp | 184 ++++++++++++++++++ 4 files changed, 190 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 7f4ca5d9eb..ae817a5039 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -70,6 +70,7 @@ void ClearMultisampleTargets(VkDevice device); void InstallResidentImageReadGuard_nid_no_patch(); void UpdateResidentImageUnmapObserver_nid_no_patch(); void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access); +void RequireNoMultisampleColorTarget(std::uint64_t address, std::size_t bytes, const char* access); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 14415abcc6..080c2f2f49 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -907,6 +907,10 @@ void ClearMultisampleTargets(VkDevice device) { void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access) { RequireNoResidentDepthRead_nid_no_patch(address, bytes); + RequireNoMultisampleColorTarget(address, bytes, access); +} + +void RequireNoMultisampleColorTarget(std::uint64_t address, std::size_t bytes, const char* access) { if (bytes == 0 || !anyGuarded().load(std::memory_order_acquire)) return; std::shared_lock lock(guardedMutex()); for (const auto& range : guarded()) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 7d3a64775f..dcddd9b65f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3117,6 +3117,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR 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& range : describedRanges) { + RequireNoMultisampleColorTarget(range.address, static_cast(range.bytes), "a cached image access"); CompletePendingCpuWrites(context, range.address, static_cast(range.bytes)); if (range.dccAddress != 0) CompletePendingCpuWrites(context, range.dccAddress, DccKeyBytes(range.bytes)); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp new file mode 100644 index 0000000000..c23656718c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp @@ -0,0 +1,184 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using namespace AgcDriver::Graphics; + +constexpr std::uint32_t Side = 64; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t AllocationBytes = 2 * BlockBytes; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + data = GuestArena::GuestArenaAllocate_nid_postfix(AllocationBytes, BlockBytes); + if (data != nullptr) GuestArena::GuestArenaCommit_nid_postfix(data, AllocationBytes, PAGE_READWRITE, AllocationBytes); +#else + void* mapped = mmap(nullptr, AllocationBytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + AllocationBytes != begin + AllocationBytes + BlockBytes) munmap(reinterpret_cast(aligned + AllocationBytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, AllocationBytes); + } +#endif + Require(data != nullptr, "cannot allocate tracked image memory"); + std::memset(data, 0, AllocationBytes); + GuestAllocations::Mutation().Add(data, AllocationBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(data); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(data, AllocationBytes); + GuestArena::GuestArenaRelease_nid_postfix(data, AllocationBytes); +#else + munmap(data, AllocationBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, AllocationBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + +private: + void* data = nullptr; +}; + +template +void RequireColorGuard(TCall&& call) { + bool guarded = false; + try { + call(); + } catch (const std::runtime_error& error) { + guarded = std::string_view(error.what()).find("multisampled color target") != std::string_view::npos; + if (!guarded) throw; + } + Require(guarded, "an image access accepted memory owned by a multisampled color target"); +} + +void CheckCachedImage(AgcDriver::VulkanDevice& device, bool sampled, bool draw) { + std::printf("checking %s %s image reuse\n", draw ? "draw" : "compute", sampled ? "sampled" : "storage"); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + GuestBlock block; + struct Cleanup { + VkDevice device; + ~Cleanup() { + ClearMultisampleTargets(device); + ClearCachedTextures(device); + } + } cleanup{context.device}; + const auto address = block.Address(); + ShaderRecompiler::DescriptorBinding binding{}; + binding.kind = sampled ? ShaderRecompiler::DescriptorKind::SampledImage : ShaderRecompiler::DescriptorKind::StorageImage; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.binding = (sampled ? ShaderRecompiler::RuntimeAbi::FirstImageBinding : ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding) + PlaceholderDimensionsPerClass + 2u; + binding.count = 1; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + binding.imageWritten = {false}; + binding.imageSamplers = {0u}; + binding.guestDescriptor = {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (20u << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u}; + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult program; + program.bindings.push_back(binding); + const CompiledShader shader{draw ? ShaderRecompiler::ShaderStage::Fragment : ShaderRecompiler::ShaderStage::Compute, &program, 0}; + const std::array stages{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, shader}}; + const auto build = [&] { + if (draw) return std::make_unique(context, stages, ColorTarget{}, 0, 0, 0); + return std::make_unique(context, shader); + }; + auto resources = build(); + Require(resources->Reusable(), "the ordinary image resources are not reusable"); + const auto revalidate = [&](ShaderResources::ProofReport* report = nullptr) { + return draw ? resources->Revalidate(stages, report) : resources->Revalidate(shader, report); + }; + ShaderResources::ProofReport proof; + Require(revalidate(&proof), "the unchanged image resources failed their initial proof"); + if (std::getenv("APS5_NO_FAST_REVALIDATE") == nullptr) Require(proof.path == ShaderResources::ProofPath::Fast, "the fixture did not exercise the fast image proof"); + ColorTarget target{}; + target.address = address + BlockBytes; + target.surfaceAddress = target.address; + target.extent = {Side, Side}; + target.surfaceExtent = target.extent; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.tileMode = ColorTileMode::RenderTarget; + target.elementBytes = 4; + target.bytes = Side * Side * 4u; + target.samples = 4; + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the disjoint multisample target has no view"); + device.SubmitRecorded(true); + device.WaitIdle(); + Require(revalidate(), "a disjoint multisample target prevented image reuse"); + target.address = address; + target.surfaceAddress = address; + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the overlapping multisample target has no view"); + device.SubmitRecorded(true); + device.WaitIdle(); + RequireColorGuard([&] { revalidate(); }); + RequireColorGuard([&] { build(); }); + ClearMultisampleTargets(context.device); + Require(revalidate(), "retiring the multisample targets did not restore image reuse"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (!AgcDriver::GuestMemory::WriteWatched() || (context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "cached image guard tests require write tracking and 4x color targets"); + std::puts("skipped, cached image guard tests require write tracking and 4x color targets"); + return VulkanTestSkipped; + } + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + for (const bool draw : {false, true}) { + for (const bool sampled : {false, true}) CheckCachedImage(*device, sampled, draw); + } + std::puts("cached image multisample guard tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 603e20bc117f2d0fdcb7ebe85ea932818db317a2 Mon Sep 17 00:00:00 2001 From: Alexandre Sanchez Date: Sun, 18 Oct 2026 20:44:18 +0200 Subject: [PATCH 056/167] fix(agc): guard fast sampled image lookups against MSAA targets Apply the color-target guard before the precollected texture shortcut can reuse an ordinary sampled image. Distinguish cached and fresh access failures in the regression and unregister tracked memory before unmapping it. --- .../libSceAgcDriver/Graphics/src/ShaderResources.cpp | 1 + .../tests/execution/CachedImageMultisampleGuard.cpp | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index dcddd9b65f..3948eb772a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4366,6 +4366,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); + RequireNoMultisampleColorTarget(address, bytes != 0 ? bytes : 1u, "a texture read"); if (DepthSurfaceAt(address)) return nullptr; if (GuestMemory::CollectWrites(address, bytes) == 0) return nullptr; if (PendingStorageOverlaps(address, bytes, record.source.get())) return nullptr; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp index c23656718c..4c43b86058 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp @@ -71,8 +71,8 @@ public: GuestArena::GuestArenaReset_nid_postfix(data, AllocationBytes); GuestArena::GuestArenaRelease_nid_postfix(data, AllocationBytes); #else - munmap(data, AllocationBytes); GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, AllocationBytes); + munmap(data, AllocationBytes); #endif } @@ -85,7 +85,7 @@ private: }; template -void RequireColorGuard(TCall&& call) { +void RequireColorGuard(TCall&& call, const char* failure) { bool guarded = false; try { call(); @@ -93,7 +93,7 @@ void RequireColorGuard(TCall&& call) { guarded = std::string_view(error.what()).find("multisampled color target") != std::string_view::npos; if (!guarded) throw; } - Require(guarded, "an image access accepted memory owned by a multisampled color target"); + Require(guarded, failure); } void CheckCachedImage(AgcDriver::VulkanDevice& device, bool sampled, bool draw) { @@ -153,8 +153,8 @@ void CheckCachedImage(AgcDriver::VulkanDevice& device, bool sampled, bool draw) Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the overlapping multisample target has no view"); device.SubmitRecorded(true); device.WaitIdle(); - RequireColorGuard([&] { revalidate(); }); - RequireColorGuard([&] { build(); }); + RequireColorGuard([&] { revalidate(); }, "a cached image access accepted memory owned by a multisampled color target"); + RequireColorGuard([&] { build(); }, "a fresh image access accepted memory owned by a multisampled color target"); ClearMultisampleTargets(context.device); Require(revalidate(), "retiring the multisample targets did not restore image reuse"); } From 67c7190223f79937473864710d6820831e7b3b05 Mon Sep 17 00:00:00 2001 From: Alexandre Sanchez Date: Sun, 18 Oct 2026 21:33:10 +0200 Subject: [PATCH 057/167] fix(agc): preserve image table semantics in cached MSAA guards Mark described image-table ranges explicitly and apply the eager color-target guard only to ordinary image bindings. Cover native multisample table validation and record the separate table registration gap. --- .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/src/ShaderResources.cpp | 6 +- .../execution/CachedImageMultisampleGuard.cpp | 61 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 33918639b7..9e2815639b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -342,6 +342,7 @@ private: std::uint32_t format = 0; int tileMode = 0; std::uint64_t dccAddress = 0; + bool imageTable = false; }; std::vector describedRanges; struct Allocation { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 3948eb772a..4a9c9191d2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3117,7 +3117,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR 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& range : describedRanges) { - RequireNoMultisampleColorTarget(range.address, static_cast(range.bytes), "a cached image access"); + if (!range.imageTable) RequireNoMultisampleColorTarget(range.address, static_cast(range.bytes), "a cached image access"); CompletePendingCpuWrites(context, range.address, static_cast(range.bytes)); if (range.dccAddress != 0) CompletePendingCpuWrites(context, range.dccAddress, DccKeyBytes(range.bytes)); } @@ -4668,7 +4668,7 @@ void ShaderResources::resolveImageTables() { const auto place = [&](std::uint32_t element, std::shared_ptr texture, const GuestTextureResource& resource, std::uint64_t guestBytes, bool firstLayer) { textures.push_back(std::move(texture)); textureFirstLayer.push_back(firstLayer); - describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); + describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress, true}); table.elementTextures[element] = textures.size() - 1u; item->imageAllocations[static_cast(table.elementBase) + element] = textures.size() - 1u; }; @@ -4787,7 +4787,7 @@ void ShaderResources::resolveImageTables() { storageWritten.push_back(true); storageAtomic.push_back(false); storageAtomic64.push_back(false); - describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); + describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress, true}); bindings[storageBinding].imageAllocations[absolute] = storageTextures.size() - 1u; continue; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp index 4c43b86058..08a51e2bb6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp @@ -8,6 +8,7 @@ #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" #include "VulkanTestDevice.hpp" +#include "ImageTableAbi.hpp" #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX @@ -159,6 +160,65 @@ void CheckCachedImage(AgcDriver::VulkanDevice& device, bool sampled, bool draw) Require(revalidate(), "retiring the multisample targets did not restore image reuse"); } +void CheckMultisampleTable(AgcDriver::VulkanDevice& device) { + namespace Abi = ShaderRecompiler::ImageTableAbi; + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (context.imageTableCapacity == 0) { + std::puts("skipped, native multisample table check needs image table support"); + return; + } + GuestBlock block; + struct Cleanup { + VkDevice device; + ~Cleanup() { ClearMultisampleTargets(device); } + } cleanup{context.device}; + ColorTarget target{}; + target.address = block.Address(); + target.surfaceAddress = target.address; + target.extent = {Side, Side}; + target.surfaceExtent = target.extent; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.tileMode = ColorTileMode::RenderTarget; + target.elementBytes = 4; + target.bytes = Side * Side * 4u; + target.samples = 4; + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the native multisample table target has no view"); + device.SubmitRecorded(true); + device.WaitIdle(); + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding map{}; + map.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + map.role = ShaderRecompiler::DescriptorRole::ImageTableMap; + map.binding = static_cast(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap); + map.count = 1; + map.guestDescriptor.resize(Abi::HeaderWords + 1u); + map.guestDescriptor[Abi::HeaderVersion] = Abi::Version; + map.guestDescriptor[Abi::HeaderElementCount] = 1u; + map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords; + map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords + 1u; + program.bindings.push_back(map); + ShaderRecompiler::DescriptorBinding image{}; + image.kind = ShaderRecompiler::DescriptorKind::SampledImage; + image.role = ShaderRecompiler::DescriptorRole::ImageTable; + image.binding = ShaderRecompiler::RuntimeAbi::ImageTableBinding; + image.count = 1; + image.imageShapes = {ShaderRecompiler::DescriptorImageShape::Image2DMultisample}; + image.imageDepthCompare = {false}; + image.imageWritten = {false}; + image.imageSamplers = {0u}; + const auto address = block.Address(); + image.guestDescriptor = {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (56u << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), 0xfacu | (2u << 16u) | (0x1bu << 20u) | (14u << 28u), 0u, 2u << 4u, 0u, 0u}; + program.bindings.push_back(image); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + ShaderResources resources(context, shader); + Require(resources.SampledTextures().size() == 1u, "the native multisample table entry was rejected during construction"); + Require(resources.Reusable(), "the native multisample table did not reach resource validation"); + static_cast(resources.Revalidate(shader)); + device.SubmitRecorded(true); + device.WaitIdle(); + std::puts("native multisample table validation did not reject its color target"); +} + } int main() { @@ -175,6 +235,7 @@ int main() { for (const bool draw : {false, true}) { for (const bool sampled : {false, true}) CheckCachedImage(*device, sampled, draw); } + CheckMultisampleTable(*device); std::puts("cached image multisample guard tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index fb15c34db5..85a1d0b3a0 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -135,7 +135,7 @@ - [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill. - [Multisampled texture no draw rendered to](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - sampling a multisampled texture whose surface no draw rendered to creates the host target from the T# and clears it to the color a single 32-bit value repeated over the whole footprint encodes, padding up to whole tile blocks included. Inferred, not measured on the console: that the samples of such a surface are the guest color memory, as a title that clears its canvas on the CPU (ps5-opengl `ps5_clear_msaa4_color`) relies on. Memory that is not one repeated value, tile modes other than Standard4KB, Standard64KB and RenderTarget, unmapped memory and values with no exact clear color throw. CPU writes to the surface after its host target exists are not seen by the host. - [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. -- [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, index buffer read, fixed-function vertex buffer read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Live targets remain cached per device, surface address, format, extent and sample count. Retired cache owners are removed on the next target, sampled or storage lookup; outstanding recorded work and sampled views retain their images. +- [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, index buffer read, fixed-function vertex buffer read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Live targets remain cached per device, surface address, format, extent and sample count. Retired cache owners are removed on the next target, sampled or storage lookup; outstanding recorded work and sampled views retain their images. Ordinary sampled and storage bindings check the color-target guard before resource reuse, including precollected sampled lookups. Reused image tables do not invalidate on multisample color-target registration yet; their per-element fault handling and native multisample sampling require a separate reuse proof. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. PA_SC_AA_CONFIG is only compared with CB0 while PA_SC_MODE_CNTL_0.MSAA_ENABLE is set: PPSA08270 resolves a 4x 512x512 target with MSAA_ENABLE off and PA_SC_AA_CONFIG still at 2x (0x00108001) from earlier draws. - [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2. On other devices every pixel takes the pattern's pixel (0,0) locations (`PA_SC_AA_SAMPLE_LOCS_PIXEL_X0Y0_*`, its first registers and the corner the grid is anchored at), and the first such pipeline of each sample count logs `[gpu] no 2x2 sample location grid`. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each; an RTX 3070 Laptop GPU (driver 617.42) reports 1x1 for every count. With that stand-in the top-left pixel of every 2x2 quad matches the console and the other three use its positions. A pattern that only reorders one set of positions between pixels, as a 2x checkerboard pattern puts its samples at x = +4/16 and -4/16 and swaps them on odd rows, keeps every position and the number of samples a primitive covers in each pixel, so a resolve that averages a pixel's samples, or takes their minimum or maximum, sees the console's values. Sample i of an odd row then holds what the console stores in sample 1 - i: a pass that reads the samples by index, as checkerboard reconstruction does, exchanges the two half-pixels of odd rows, which can show as combing along vertical edges, and per-sample shading, sample-mask exports and coverage inputs see the swapped indices too. A pattern whose pixels hold different positions draws pixel (0,0)'s positions in each pixel whose own differ. A device's pipelines and render pass begins take the same stand-in for a given pattern, so a sample-location-compatible depth image is not drawn with one set of locations and transitioned or tested with another. A multisampled rasterizer drawing into one-sample color targets, and a one-sample depth target if any, with MSAA_ENABLE set, no alpha-to-coverage and no coverage pixel input is drawn with one sample at sample 0's location in each pixel of the grid, as the targets keep only sample 0's coverage. Without PS_ITER_SAMPLE such a draw also refuses centroid inputs, and keeps PERSP_SAMPLE, LINEAR_SAMPLE and ANCILLARY: a pixel shaded once is taken to get sample 0's barycentrics and sample index 0. With PS_ITER_SAMPLE only sample 0's invocation reaches the targets, so its sample and centroid barycentrics are taken to be sample 0's and the draw is shaded once. Both are inferred, not measured. The pixel shader runs once per pixel with sample number 0, so ANCILLARY reads sample 0 and POS_X_FLOAT and POS_Y_FLOAT keep the position `SPI_BARYC_CNTL` names. PERSP_CENTER and LINEAR_CENTER inputs on this path are interpolated at sample 0's location on the host, where the console uses the pixel center; one-sample depth images are created with `SAMPLE_LOCATIONS_COMPATIBLE_DEPTH` when the device supports it, as multisampled ones already were; this is how VK_AMD_mixed_attachment_samples defines the case, and it is not measured on PS5 hardware. Other mixed sample counts throw. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. - [Single-sample scan into a multisampled depth target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` = 0 and `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear into a multisampled depth target rasterizes the depth target's sample count with every sample at the pixel center, as a single-sample scan into a multisampled color target already does. radeonsi programs this `MSAA_ENABLE` and `PA_SC_AA_CONFIG` with the depth buffer's own sample count when GL multisampling is disabled on a multisampled framebuffer ([L1066-L1068](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L1066-L1068), [L3485-L3512](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3485-L3512)), although its comment lists Z samples as at most the coverage samples ([L3439](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3439)) and it sets `DB_EQAA.MAX_ANCHOR_SAMPLES` to the Z sample count, where PPSA18216 sets `DB_EQAA` to 0. GL then lets every sample take the single-sample depth ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190)) and the fragment be treated as in multisampling with full coverage ([L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)), which is what this draws: each depth and stencil sample is tested and written with the depth at the pixel center. GL also lets an implementation test only a centermost sample and set every sample from it ([L425-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L425-L434)), which differs when a pixel's stored samples differ; which one the console does, whether it instead evaluates the depth plane at each sample position, and how its occlusion queries count such a draw, is not measured on hardware. Points, lines, alpha-to-coverage, `DB_EQAA` Z interpolation or overrasterization, a pixel program reading the sample index, a sample-mask export and a single-sample color target next to the multisampled depth target throw From 56d183dc946700720fa8eb71b36d56ebd6d27ab7 Mon Sep 17 00:00:00 2001 From: Alexandre Sanchez Date: Mon, 19 Oct 2026 01:40:25 +0200 Subject: [PATCH 058/167] test(agc): accept compared imports in the MSAA guard fixture Require the fast image proof only for write-watched image memory while retaining every guard check for compared imports. --- .../tests/execution/CachedImageMultisampleGuard.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp index 08a51e2bb6..c60b857816 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp @@ -134,7 +134,10 @@ void CheckCachedImage(AgcDriver::VulkanDevice& device, bool sampled, bool draw) }; ShaderResources::ProofReport proof; Require(revalidate(&proof), "the unchanged image resources failed their initial proof"); - if (std::getenv("APS5_NO_FAST_REVALIDATE") == nullptr) Require(proof.path == ShaderResources::ProofPath::Fast, "the fixture did not exercise the fast image proof"); + if (std::getenv("APS5_NO_FAST_REVALIDATE") == nullptr) { + if (AgcDriver::GuestMemory::Watched(address, Side * Side * 4u)) Require(proof.path == ShaderResources::ProofPath::Fast, "the fixture did not exercise the fast image proof"); + else std::puts("image memory is compared, not watched: fast image proof not tested; checking the image guards through the full proof"); + } ColorTarget target{}; target.address = address + BlockBytes; target.surfaceAddress = target.address; From 2ac433e3cb5458677bb1109cf91f072e78860915 Mon Sep 17 00:00:00 2001 From: Werner Date: Mon, 19 Oct 2026 13:24:02 +0200 Subject: [PATCH 059/167] feat(libc): list stack return address candidates in the hang dump The Windows crash report walks every other thread's rbp chain only, so a thread waiting inside ntdll or in code built without frame pointers showed no frames at all. For a thread whose chain yields fewer than two frames, scan the 32 KiB above its rsp while it is suspended and print up to 24 words that point at executable memory. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01T86KdkUbEv7m5hrCbPbkAp --- .../src/specifics/windows/CrashReport.cpp | 25 +++++++++++++++++-- docs/user/USAGE.md | 2 ++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 3d685d640a..a5d0a728bf 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -503,7 +503,17 @@ std::size_t CaptureFrames(const CONTEXT* context, std::uint64_t* frames, std::si return count; } -void ReportThreadContext(const CONTEXT* context, DWORD threadId, const char* name, const std::uint64_t* frames, std::size_t frameCount) { +std::size_t CaptureStackCandidates(std::uint64_t rsp, std::uint64_t* candidates, std::size_t capacity) { + std::size_t count = 0; + for (std::uint64_t slot = rsp; count < capacity && slot < rsp + 0x8000; slot += 8) { + if (!IsReadable(slot)) break; + const auto value = *reinterpret_cast(slot); + if (IsExecutable(value)) candidates[count++] = value; + } + return count; +} + +void ReportThreadContext(const CONTEXT* context, DWORD threadId, const char* name, const std::uint64_t* frames, std::size_t frameCount, const std::uint64_t* candidates, std::size_t candidateCount) { char line[MAX_PATH + 64]; Report(" thread %lu '%s': rip ", static_cast(threadId), name ? name : ""); DescribeAddress(context->Rip, line, sizeof(line)); @@ -512,6 +522,11 @@ void ReportThreadContext(const CONTEXT* context, DWORD threadId, const char* nam DescribeAddress(frames[depth], line, sizeof(line)); Report(" #%llu %s\n", static_cast(depth), line); } + if (candidateCount != 0) Report(" stack return address candidates:\n"); + for (std::size_t index = 0; index < candidateCount; ++index) { + DescribeAddress(candidates[index], line, sizeof(line)); + Report(" %s\n", line); + } } void ReportAllThreads() { @@ -527,6 +542,8 @@ void ReportAllThreads() { CONTEXT context; std::uint64_t frames[12]; std::size_t frameCount; + std::uint64_t candidates[24]; + std::size_t candidateCount; }; static CapturedThread captured[128]; std::size_t capturedCount = 0; @@ -549,6 +566,8 @@ void ReportAllThreads() { const BOOL haveContext = GetThreadContext(thread, &context); std::uint64_t frames[12] = {}; const std::size_t frameCount = haveContext ? CaptureFrames(&context, frames, sizeof(frames) / sizeof(frames[0])) : 0; + std::uint64_t candidates[24] = {}; + const std::size_t candidateCount = haveContext && frameCount < 2 ? CaptureStackCandidates(context.Rsp, candidates, sizeof(candidates) / sizeof(candidates[0])) : 0; ResumeThread(thread); char name[128] = ""; PWSTR description = nullptr; @@ -563,13 +582,15 @@ void ReportAllThreads() { captured[capturedCount].context = context; std::memcpy(captured[capturedCount].frames, frames, sizeof(captured[capturedCount].frames)); captured[capturedCount].frameCount = frameCount; + std::memcpy(captured[capturedCount].candidates, candidates, sizeof(captured[capturedCount].candidates)); + captured[capturedCount].candidateCount = candidateCount; ++capturedCount; } while (Thread32Next(snapshot, &entry)); } CloseHandle(snapshot); Report(" all threads:\n"); for (std::size_t i = 0; i < capturedCount; ++i) { - ReportThreadContext(&captured[i].context, captured[i].id, captured[i].name, captured[i].frames, captured[i].frameCount); + ReportThreadContext(&captured[i].context, captured[i].id, captured[i].name, captured[i].frames, captured[i].frameCount, captured[i].candidates, captured[i].candidateCount); } if (skippedCount != 0) Report(" %llu further thread(s) left out\n", static_cast(skippedCount)); } diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 722df8e305..2cf941aabb 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -326,6 +326,8 @@ Set `APS5_CRASH_INDIRECT_MEMORY=1` before launching the executable to include up Set `APS5_CRASH_STACK_BYTES=` to print, after the stack return address candidates, the faulting thread's stack words from `rsp` for `n` bytes (at most 1048576, decimal or `0x` hexadecimal), four words per line with their offset from `rsp`. Below them the report prints eight words at every distinct stack word that points at readable, non-executable memory outside the thread's stack, labelled with the word's offset and the address it holds, up to 256 addresses. This shows the objects the caller frames kept, such as the `this` of a destructor or the buffer of a container. The dump stops at the first unreadable stack page, and nothing is printed when the variable is unset, empty or 0. +The report lists every other thread of the process with its `rip`, `rsp`, `rbp` and the return addresses of its `rbp` frame chain. A thread whose chain yields fewer than two frames, such as one waiting inside `ntdll.dll` or in code built without frame pointers, also gets up to 24 stack return address candidates: the words in the 32 KiB above its `rsp` that point at executable memory, read while the thread is suspended. Like the candidates of the faulting thread they are a scan, not an unwind, so stale return addresses of finished calls can appear among them. + ### GPU submission rings and stall dumps Each GPU queue keeps its last 16 command buffer submissions: when each was submitted, its serial, size and guest address, and a copy of its first 512 dwords after indirect buffers are inlined. `APS5_PM4_RING=` keeps the last `n` instead, from 0 to 4096; 0 keeps none, and any other value stops the title at its first submission. The rings are printed to stderr, one line per submission and one per decoded packet, when the Windows crash report runs: an unhandled exception, `abort()`, an invalid CRT parameter or the timed dump of `APS5_HANG_DUMP_SECS`. From dca67ae477ebc49d4c25e08ba6979fdf4ac647a7 Mon Sep 17 00:00:00 2001 From: ionut27001 <194206737+ionut27001@users.noreply.github.com> Date: Sun, 18 Oct 2026 02:34:49 +0300 Subject: [PATCH 060/167] fix(libkernel): return kern.rng_pseudo in 64-byte blocks as measured on a PS5 In a payload process on a PS5 (firmware 12.70), sysctlbyname("kern.rng_pseudo") answers a size query with 64 and gives at most 64 bytes per call, up to a 65536-byte buffer. A buffer smaller than 64 bytes gets as many bytes as its length and a null length gets none, both with -1 and ENOMEM; a null buffer with a null length returns 0, and errno is kept when the call succeeds. Here the whole buffer was filled, a size query threw and the length was never changed. The 64 random bytes are now drawn only for a read that passes both a buffer and a length. AI-assisted: yes Co-Authored-By: Claude Opus 5.5 --- .../System/src/SystemConfiguration.cpp | 8 +-- core/libs/tests/GuestSystemConfiguration.cpp | 55 ++++++++++++++----- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index a9af1155db..33c2ce2d8f 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -251,11 +251,11 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si *__error_nid_postfix() = 1; return -1; } - if (oldValue == nullptr || oldLength == nullptr) throw std::runtime_error(std::string(__func__) + ": kern.rng_pseudo without an output buffer"); static std::random_device device; - auto* bytes = static_cast(oldValue); - for (std::size_t i = 0; i < *oldLength; ++i) bytes[i] = static_cast(device()); - return 0; + std::uint8_t block[64]{}; + if (oldValue != nullptr && oldLength != nullptr) + for (auto& byte : block) byte = static_cast(device()); + return CopyOut(block, sizeof(block), oldValue, oldLength); } if (std::strcmp(name, "vm.overcommit") == 0) { if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": setting vm.overcommit"); diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index a13c41c46f..635a215ee8 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -289,23 +289,50 @@ static void CheckSystemSwVersion() { } } static void CheckPseudoRandomSysctl() { + unsigned char first[128]; + unsigned char second[128]; + const auto untouchedFrom = [&first](std::size_t from) { + for (std::size_t i = from; i < sizeof(first); ++i) + if (first[i] != 0xaa) return false; + return true; + }; *__error_nid_postfix() = 13; - unsigned char first[64] = {}; - unsigned char second[64] = {}; - std::size_t length = sizeof(first); - Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, nullptr, 0) == 0 && length == sizeof(first)); - length = sizeof(second); - Require(sysctlbyname_nid_postfix("kern.rng_pseudo", second, &length, nullptr, 0) == 0 && length == sizeof(second)); - Require(std::memcmp(first, second, sizeof(first)) != 0); + std::memset(first, 0xaa, sizeof(first)); + std::memset(second, 0xaa, sizeof(second)); + std::size_t length = 64; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, nullptr, 0) == 0 && length == 64 && untouchedFrom(64) && !untouchedFrom(0)); + length = 64; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", second, &length, nullptr, 0) == 0 && length == 64); + Require(std::memcmp(first, second, 64) != 0); Require(*__error_nid_postfix() == 13); - Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, first, sizeof(first)) == -1 && *__error_nid_postfix() == 1); - bool threw = false; - try { - sysctlbyname_nid_postfix("kern.rng_pseudo", nullptr, &length, nullptr, 0); - } catch (const std::runtime_error&) { - threw = true; + static constexpr std::size_t larger[] = {65, 128}; + for (std::size_t asked : larger) { + std::memset(first, 0xaa, sizeof(first)); + length = asked; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, nullptr, 0) == 0 && length == 64 && untouchedFrom(64) && !untouchedFrom(0)); } - Require(threw); + Require(*__error_nid_postfix() == 13); + static constexpr std::size_t smaller[] = {0, 1, 63}; + for (std::size_t asked : smaller) { + std::memset(first, 0xaa, sizeof(first)); + length = asked; + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == asked && untouchedFrom(asked)); + if (asked == 63) Require(!untouchedFrom(0)); + } + static constexpr std::size_t presets[] = {0, 1, 64, 100, 4096}; + for (std::size_t preset : presets) { + length = preset; + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", nullptr, &length, nullptr, 0) == 0 && length == 64 && *__error_nid_postfix() == 13); + } + std::memset(first, 0xaa, sizeof(first)); + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 12 && untouchedFrom(0)); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", nullptr, nullptr, nullptr, 0) == 0 && *__error_nid_postfix() == 13); + length = 64; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, first, 64) == -1 && *__error_nid_postfix() == 1); } static void CheckOvercommitSysctl() { *__error_nid_postfix() = 13; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 0e26b387d8..35bb92cda5 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -15,7 +15,7 @@ - [sceKernelError](../../../core/libs/prx/libkernel/System/src/Error.cpp) follows [shadPS4's kernel implementation](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/kernel/kernel.cpp): zero returns zero; other guest POSIX error values receive the `0x80020000` bias without modifying errno. Arithmetic is defined modulo 32 bits to avoid signed overflow for inputs outside the errno domain. Console behavior for those inputs and PS5 equivalence have not been measured. - [getlogin_r](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. - [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` fills the whole output buffer with bytes from the host's `std::random_device` and leaves the length unchanged; a write returns -1 with `EPERM` and a call without an output buffer throws. 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, its per-call maximum and its answer to a size query are unknown +- [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` for the name and `sysctl` {1, 50} throw - [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 From c92f09d1d5f650cda01f60daf870bf3ee575c23b Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Mon, 19 Oct 2026 14:53:58 +0300 Subject: [PATCH 061/167] docs(debt): name no title in the SCE code gap entry Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/relinker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index cd67822bf6..9ca49b8361 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -17,7 +17,7 @@ - In Windows output, a guest instruction that addresses memory through GS becomes `UD2` and NOP padding of its own length ([TLS builder](../../../core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp)): the host owns GS, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`. - 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 (CUSA03312). 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. +- 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. - 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)). - In macOS output, a guest instruction with a GS segment prefix, including one with no memory operand (`65 90`, `65 48 89 c8`), which does not touch GS, becomes `UD2` and NOP padding of its own length ([macOS image writer](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp)): the host owns GS, which holds the pthread TSD base the guest TLS stubs read, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`. From 6206c230c425c19f78dcb3f39729ddf8f82ad9dc Mon Sep 17 00:00:00 2001 From: Lindamea1 <322051987+Lindamea1@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:34:40 +0100 Subject: [PATCH 062/167] fix(libSceSaveData.native): count PS5 save data in 64 KiB blocks, at most 16384 The PS5 references count save data differently from the PS4: SharpEmu uses a 64 KiB block, and SharpEmu and KytyPS5 allow 48 to 16384 blocks per save, which at 64 KiB a block is 3 MiB to 1 GiB, the PS4's byte range. The module used the PS4 values, 32 KiB blocks and a 32768-block maximum. At 64 KiB a block, sceSaveDataDirNameSearch and sceSaveDataGetMountInfo therefore counted up to twice the blocks a save's files use, and a save without a recorded size reported 32768 blocks, which is 2 GiB. The search test now expects 64 KiB blocks and the 16384 maximum, and the block accounting gets its own technical debt entry. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceSaveData.native/Export.cpp | 2 +- .../prx/libSceSaveData.native/SaveData.hpp | 2 +- core/libs/tests/GuestSaveDataSearch.cpp | 45 ++++++++++++------- docs/dev/debt/libSceSaveData.native.md | 3 +- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 925cabeb13..2143f6e129 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -360,7 +360,7 @@ int APS5_VABI sceSaveDataDeleteTransactionResource(int32_t resource) { return SAVE_DATA_OK; } -static constexpr std::uint64_t SAVE_DATA_BLOCK_BYTES = 32768; +static constexpr std::uint64_t SAVE_DATA_BLOCK_BYTES = 65536; static std::uint64_t save_blocks(const std::string& real_path) { std::ifstream file(blocks_path(real_path)); diff --git a/core/libs/prx/libSceSaveData.native/SaveData.hpp b/core/libs/prx/libSceSaveData.native/SaveData.hpp index ceffb09c01..1a8681cb38 100644 --- a/core/libs/prx/libSceSaveData.native/SaveData.hpp +++ b/core/libs/prx/libSceSaveData.native/SaveData.hpp @@ -25,7 +25,7 @@ constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_RDWR = 2; constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_CREATE = 4; constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_CREATE2 = 32; -constexpr std::uint64_t SAVE_DATA_BLOCKS_MAX = 32768; +constexpr std::uint64_t SAVE_DATA_BLOCKS_MAX = 16384; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_ALL = 0; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_TITLE = 1; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_SUB_TITLE = 2; diff --git a/core/libs/tests/GuestSaveDataSearch.cpp b/core/libs/tests/GuestSaveDataSearch.cpp index 6cea92d921..ed1e11b463 100644 --- a/core/libs/tests/GuestSaveDataSearch.cpp +++ b/core/libs/tests/GuestSaveDataSearch.cpp @@ -187,7 +187,7 @@ static void SearchAndMount() { for (std::size_t i = 0; i < 2; ++i) { SaveDataSearchInfo info{}; std::memcpy(&info, infos.data() + i * 48, sizeof(info)); - Require(info.blocks == 32768 && info.free_blocks == 32768, "search info must report the save's blocks and free blocks"); + Require(info.blocks == 16384 && info.free_blocks == 16384, "search info must report the save's blocks and free blocks"); for (const auto byte : info.reserved) Require(byte == 0, "search info reserved bytes must be zero"); } std::array found{}; @@ -246,20 +246,9 @@ static std::vector SizeSearch(std::uint32_t key, std::vector return {infos.begin(), infos.end()}; } -static void SaveSize() { - CreateSized("SIZE_A", 200, 0); - CreateSized("SIZE_B", 96, 40000); - std::vector found; - auto infos = SizeSearch(0, found); - Require(found == std::vector{"SIZE_A", "SIZE_B"}, "the sized saves must sort by name"); - Require(infos[0].blocks == 200 && infos[0].free_blocks == 200, "an empty save must report all its blocks free"); - Require(infos[1].blocks == 96 && infos[1].free_blocks == 94, "a save must report the blocks it was created with, less those its files take"); - SizeSearch(2, found); - Require(found == std::vector{"SIZE_B", "SIZE_A"}, "saves must sort by blocks"); - infos = SizeSearch(5, found); - Require(found == std::vector{"SIZE_B", "SIZE_A"} && infos[0].free_blocks < infos[1].free_blocks, "saves must sort by free blocks"); +static SaveDataMountInfo MountInfo(const char* name) { SceSaveDataDirName dirName{}; - std::snprintf(dirName.data, sizeof(dirName.data), "SIZE_B"); + std::snprintf(dirName.data, sizeof(dirName.data), "%s", name); SaveDataMount3 mount{}; mount.user_id = 1; mount.dir_name = &dirName; @@ -268,8 +257,33 @@ static void SaveSize() { Require(sceSaveDataMount3(&mount, &mounted) == 0, "mounting the sized save failed"); SaveDataMountInfo info{}; Require(sceSaveDataGetMountInfo(&mounted.mount_point, &info) == 0, "mount info failed"); - Require(info.blocks == 96 && info.free_blocks == 94, "mount info must report the save's blocks and free blocks"); Require(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "unmount failed"); + return info; +} + +static void SaveSize() { + CreateSized("SIZE_A", 200, 0); + CreateSized("SIZE_B", 96, 40000); + std::vector found; + auto infos = SizeSearch(0, found); + Require(found == std::vector{"SIZE_A", "SIZE_B"}, "the sized saves must sort by name"); + Require(infos[0].blocks == 200 && infos[0].free_blocks == 200, "an empty save must report all its blocks free"); + Require(infos[1].blocks == 96 && infos[1].free_blocks == 95, "a save must report the blocks it was created with, less those its files take"); + SizeSearch(2, found); + Require(found == std::vector{"SIZE_B", "SIZE_A"}, "saves must sort by blocks"); + infos = SizeSearch(5, found); + Require(found == std::vector{"SIZE_B", "SIZE_A"} && infos[0].free_blocks < infos[1].free_blocks, "saves must sort by free blocks"); + const auto info = MountInfo("SIZE_B"); + Require(info.blocks == 96 && info.free_blocks == 95, "mount info must report the save's blocks and free blocks"); +} + +static void BlockSize() { + CreateSized("BLOCK_A", 48, 70000); + auto info = MountInfo("BLOCK_A"); + Require(info.blocks == 48 && info.free_blocks == 46, "a file must take whole 64 KiB blocks"); + CreateSized("BLOCK_B", 32768, 0); + info = MountInfo("BLOCK_B"); + Require(info.blocks == 16384 && info.free_blocks == 16384, "a save must hold at most 16384 blocks"); } int main() { @@ -285,6 +299,7 @@ int main() { OtherTitle(); TrailingWildcards(); SaveSize(); + BlockSize(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); status = 1; diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index ad4a4bf0b4..1009270362 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -8,7 +8,8 @@ - [sceSaveDataConvert](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the parameter block (user id at `+0x00`, `SceSaveDataDirName` pointer at `+0x08`, 48 opaque bytes after it) is read from PPSA28416's single call, not confirmed by KytyPS5, shadPS4, fpPS4 or SharpProspero, which have no body for it. Host saves have no size limit, so an existing save returns `SCE_OK` without touching its files and no event is queued; what the console returns for a save that is mounted, already converted or needs no conversion is unknown. The directory name is validated as `sceSaveDataDelete` does - [sceSaveDataGetConvertProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA16064; signature and error codes follow the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-get-convert-progress.md) (`float* progress`). Save data conversion on host filesystems completes synchronously, so the progress query returns `SAVE_DATA_OK` with the last conversion's progress (`0.0f` before any conversion or when conversion fails, `1.0f` after successful conversion), `SAVE_DATA_ERROR_PARAMETER` if `progress` is null, or `SAVE_DATA_ERROR_NOT_INITIALIZED` when the library is uninitialized; intermediate asynchronous conversion progress on console is unmeasured - [sceSaveDataCancel](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA23126, which fails to load without it; added without an implementation because its signature is unknown. Every save data call here finishes before it returns, so nothing is ever in flight to cancel, but what the console returns then is unknown -- [sceSaveDataDirNameSearch](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. A save's size is the `blocks` of the `sceSaveDataMount3` call that created it, kept beside the save in `.blocks`; a save without a kept size takes that of its next mount that names one, and otherwise reports `SAVE_DATA_BLOCKS_MAX`. The search infos (blocks, free blocks and 32 reserved bytes per returned name, following shadPS4's `OrbisSaveDataSearchInfo`), `sceSaveDataGetMountInfo` and the two block keys use that size and, as free, what the save's files leave of it, each file rounded up to 32 KiB blocks; the console's file system overhead is not known and not counted. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it. In the directory name pattern `%` matches any run of characters, none included, and `_` exactly one, as the comments of shadPS4's matcher describe; also not confirmed on hardware +- [sceSaveDataDirNameSearch](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. The search infos (blocks, free blocks and 32 reserved bytes per returned name) follow shadPS4's `OrbisSaveDataSearchInfo`. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it. In the directory name pattern `%` matches any run of characters, none included, and `_` exactly one, as the comments of shadPS4's matcher describe; also not confirmed on hardware +- [sceSaveDataDirNameSearch, sceSaveDataGetMountInfo, sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the 64 KiB block of [SharpEmu](https://github.com/sharpemu/sharpemu/blob/c6d304cc3328bad62cb671327ace181110cf2ddb/src/SharpEmu.Libs/SaveData/SaveDataExports.cs#L82-L84) and the 48 to 16384-block range of SharpEmu and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L30-L31) are not measured on a console, and file system overhead is not counted. What the console does with a `blocks` outside that range is unknown: KytyPS5 rejects the creating mount and SharpEmu clamps it; here a larger one is kept as 16384 and a smaller non-zero one as given. A save's size is the `blocks` of the mount that created it, kept in `.blocks`, else that of its next mount that names one, else 16384; its free blocks are that size less its files, each rounded up to 64 KiB blocks - [sceSaveDataInitialize3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown ### Functional From cb7ee022cf7c9d99630ad727fd0a557ee8a9b0c2 Mon Sep 17 00:00:00 2001 From: mazen Date: Mon, 19 Oct 2026 19:52:21 +0400 Subject: [PATCH 063/167] fix(agc): require the pass-input barrier only for an in-place read Windows NVIDIA copies index and vertex ranges, so an open pass does not read the guest indices. The hazard check stays for devices that do read them in place. --- .../prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp | 1 + core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 4 ++++ .../prx/libSceAgcDriver/tests/execution/PassInputHazard.cpp | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 4b8f170a2e..26fc002b8b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -314,6 +314,7 @@ public: // `recipe`, when given, receives the DrawRecipe a recorded, cacheable, reusable, direct draw // built for its draw-cache entry (design_cpu_final M8); null otherwise. void Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots = {}, std::shared_ptr* recipe = nullptr); + bool DrawInputsInPlace() const; std::optional KnownDrawRejection(const Graphics::State& graphics, std::span shaders) const; void ColorMetadataPass(const Graphics::ColorMetadataPass& pass); // A draw-cache hit recorded from its recipe (Graphics::DrawWithRecipe), under the mutex after diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 1ebaafcc12..b979a85024 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3799,6 +3799,10 @@ void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParamete if (trace) APS5_LOG_CHARS_OUT("VulkanDevice::Draw complete"); } +bool VulkanDevice::DrawInputsInPlace() const { + return state->drawInputsInPlace; +} + namespace { // The resource cache serves dispatches unless disabled; Revalidate proves a hit by texture identity, diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PassInputHazard.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PassInputHazard.cpp index 66e6aff3eb..cbe0f54f42 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PassInputHazard.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PassInputHazard.cpp @@ -223,7 +223,7 @@ int main() { std::memcpy(indices, triangle.data(), sizeof(triangle)); const auto variant = std::string(readDepth == nullptr ? "without depth" : writeDepth == nullptr ? "with depth on the reader" : "with a depth target each"); Draw(*device, reader, read, readDepth, indices); - Require(WriterFenced(inputs.Address(), sizeof(triangle)), "pass input hazard " + variant + ": a pass writing the indices an open pass reads in place took no barrier"); + if (device->DrawInputsInPlace()) Require(WriterFenced(inputs.Address(), sizeof(triangle)), "pass input hazard " + variant + ": a pass writing the indices an open pass reads in place took no barrier"); Draw(*device, reader, read, readDepth, indices); Draw(*device, writer, written, writeDepth, nullptr); Require(Covered(*device, read) == all, "pass input hazard " + variant + ": the draw reading its indices in place did not cover its target, a later pass overwrote them first"); From d2657b975001ae0258e2520506e75b872df2fe89 Mon Sep 17 00:00:00 2001 From: billythemenace2 Date: Mon, 19 Oct 2026 17:22:50 +0100 Subject: [PATCH 064/167] fix(libkernel): classify the raise target's code without loader calls while it is suspended GetModuleHandleExW and GetModuleFileNameW take loader locks the suspended target may hold. While the target is suspended only VirtualQuery and a lock-free cache of image bases are consulted; an image not yet cached is classified after the target is resumed, and the target is sampled again. The statics the checks use are initialized before the first suspend. The host-code wait is recorded in the libkernel debt file, and the test raises 100 times so it fails on every run without the change. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../prx/libkernel/System/src/Exception.cpp | 82 ++++++++++++++++--- core/libs/tests/GuestRaiseInHostCode.cpp | 2 +- docs/dev/debt/libkernel.md | 1 + 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index be680ca0f0..377c194661 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -252,15 +252,59 @@ bool InWinpthread(DWORD64 rip) { return winpthread != nullptr && VirtualQuery(reinterpret_cast(rip), &info, sizeof(info)) == sizeof(info) && info.AllocationBase == winpthread; } -bool InHostModule(DWORD64 rip) { - HMODULE module = nullptr; - if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(rip), &module) || module == nullptr) return false; - if (module == GetModuleHandleW(nullptr)) return false; - wchar_t path[MAX_PATH]{}; - const auto length = GetModuleFileNameW(module, path, MAX_PATH); - const std::wstring_view name(path, length); - constexpr std::wstring_view guestSuffix = L".guest.prx"; - return !(name.size() >= guestSuffix.size() && name.substr(name.size() - guestSuffix.size()) == guestSuffix); +enum class CodeKind : std::uint8_t { Unknown, Guest, Host }; + +struct ImageKinds { + static constexpr std::size_t Capacity = 256; + std::array, Capacity> bases{}; + std::array, Capacity> kinds{}; + std::atomic count{0}; +}; + +ImageKinds& Images() { + static ImageKinds images; + return images; +} + +DWORD64 ImageBase(DWORD64 rip) { + MEMORY_BASIC_INFORMATION info{}; + if (VirtualQuery(reinterpret_cast(rip), &info, sizeof(info)) != sizeof(info) || info.Type != MEM_IMAGE) return 0; + return reinterpret_cast(info.AllocationBase); +} + +CodeKind CachedKind(DWORD64 base) { + if (base == 0) return CodeKind::Guest; + auto& images = Images(); + const auto count = std::min(images.count.load(std::memory_order_acquire), ImageKinds::Capacity); + for (std::size_t index = 0; index < count; ++index) { + if (images.bases[index].load(std::memory_order_acquire) == base) return static_cast(images.kinds[index].load(std::memory_order_acquire)); + } + return CodeKind::Unknown; +} + +DWORD64 ExecutableBase() { + static const auto executable = reinterpret_cast(GetModuleHandleW(nullptr)); + return executable; +} + +CodeKind ClassifyImage(DWORD64 base) { + if (const auto known = CachedKind(base); known != CodeKind::Unknown) return known; + auto kind = CodeKind::Host; + if (base == ExecutableBase()) { + kind = CodeKind::Guest; + } else { + wchar_t path[MAX_PATH]{}; + const auto length = GetModuleFileNameW(reinterpret_cast(base), path, MAX_PATH); + const std::wstring_view name(path, length); + constexpr std::wstring_view guestSuffix = L".guest.prx"; + if (name.size() >= guestSuffix.size() && name.substr(name.size() - guestSuffix.size()) == guestSuffix) kind = CodeKind::Guest; + } + auto& images = Images(); + if (const auto slot = images.count.fetch_add(1, std::memory_order_acq_rel); slot < ImageKinds::Capacity) { + images.kinds[slot].store(static_cast(kind), std::memory_order_release); + images.bases[slot].store(base, std::memory_order_release); + } + return kind; } bool BlockedInWait(DWORD64 rip) { @@ -278,8 +322,11 @@ bool BlockedInWait(DWORD64 rip) { return std::any_of(waits.begin(), waits.end(), [rip](DWORD64 stub) { return rip - stub < 0x20; }); } -bool RunningHostCode(DWORD64 rip) { - return InHostModule(rip) && !BlockedInWait(rip); +CodeKind RunningCode(DWORD64 rip, DWORD64& base) { + base = ImageBase(rip); + auto kind = CachedKind(base); + if (kind == CodeKind::Unknown && Images().count.load(std::memory_order_acquire) >= ImageKinds::Capacity) kind = CodeKind::Host; + return kind == CodeKind::Host && BlockedInWait(rip) ? CodeKind::Guest : kind; } bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { @@ -293,6 +340,11 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { 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; + static_cast(RestoringContext(0)); + static_cast(InWinpthread(0)); + static_cast(BlockedInWait(0)); + static_cast(ExecutableBase()); + static_cast(Images()); for (;;) { if (SuspendThread(native) == static_cast(-1)) { if (Exited(native)) return false; @@ -315,8 +367,14 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { return true; } const bool restoring = RestoringContext(delivery.context.Rip) || InWinpthread(delivery.context.Rip); - if (!restoring && (!RunningHostCode(delivery.context.Rip) || std::chrono::steady_clock::now() >= retryDeadline)) break; + 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; ResumeThread(native); + if (running == CodeKind::Unknown) { + static_cast(ClassifyImage(image)); + continue; + } if (restoring && std::chrono::steady_clock::now() >= retryDeadline) throw std::runtime_error("sceKernelRaiseException: the target thread stayed inside NtContinue or winpthreads for 1 s"); SwitchToThread(); } diff --git a/core/libs/tests/GuestRaiseInHostCode.cpp b/core/libs/tests/GuestRaiseInHostCode.cpp index a3f728545d..955d4605b6 100644 --- a/core/libs/tests/GuestRaiseInHostCode.cpp +++ b/core/libs/tests/GuestRaiseInHostCode.cpp @@ -16,7 +16,7 @@ int APS5_VABI sceKernelRaiseException(Pthread, int); } static constexpr int SigUsr1 = 30; -static constexpr int Raises = 20; +static constexpr int Raises = 100; static void Require(bool value) { if (!value) std::abort(); } static std::FILE* shared = nullptr; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b4adfa1c64..f0a295835a 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -138,6 +138,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) 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 13fa14e7dd2ef6aefc747eda1de346676cce31ab Mon Sep 17 00:00:00 2001 From: andreamatt Date: Sun, 18 Oct 2026 22:28:27 +0200 Subject: [PATCH 065/167] fix(progress_history): take history days in UTC daily_commits grouped commits by %cs, the committer date in the committer's own offset. With merges from +13:00 and +02:00 interleaved on main, the newest commit labelled with the later date can be older than the newest commit labelled with the earlier one, so the chart ended on an older tree and showed a drop that did not happen. Co-Authored-By: Claude Opus 5.5 --- docs/dev/PROGRESS.md | 2 +- tools/progress_history.py | 10 +++++----- tools/tests/test_progress_history.py | 15 +++++++++++++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/docs/dev/PROGRESS.md b/docs/dev/PROGRESS.md index 7602f54f0f..a1257f85a9 100644 --- a/docs/dev/PROGRESS.md +++ b/docs/dev/PROGRESS.md @@ -14,7 +14,7 @@ A library with no scanned definitions and no shared sources, such as one whose ` ## History -[`tools/progress_history.py`](../../tools/progress_history.py) measures the first-parent history of `main` with the same rules as `progress.py` and writes `history.json`, `history.svg` and `history.html` (the chart with a tooltip per day plus a table, linked from the README and the end of the progress page). The chart plots counts, not percentages, so it shows how much is added over time: one panel for library functions and one for shader instructions, each on its own scale with the implemented count as a solid line and the declared total as a dashed one (for shaders the declared total is the instruction list in `tools/rdna_isa.txt`); the current percentage sits next to each panel's implemented label. It takes one point per day, at the last commit of the day, and skips commits whose tree lacks the scanned paths. The chart compacts as days accumulate: up to 14 days it labels every day, up to 150 days one label per Monday, beyond that one per month; points are drawn up to 45 days, and every day keeps a hover tooltip, while the table and `history.json` keep every day. Old trees are measured with today's counting rules and instruction list, so the series is comparable but not what the page showed on that day. The workflow checks out the full history for it. +[`tools/progress_history.py`](../../tools/progress_history.py) measures the first-parent history of `main` with the same rules as `progress.py` and writes `history.json`, `history.svg` and `history.html` (the chart with a tooltip per day plus a table, linked from the README and the end of the progress page). The chart plots counts, not percentages, so it shows how much is added over time: one panel for library functions and one for shader instructions, each on its own scale with the implemented count as a solid line and the declared total as a dashed one (for shaders the declared total is the instruction list in `tools/rdna_isa.txt`); the current percentage sits next to each panel's implemented label. It takes one point per UTC day, at the last commit of the day, and skips commits whose tree lacks the scanned paths. The chart compacts as days accumulate: up to 14 days it labels every day, up to 150 days one label per Monday, beyond that one per month; points are drawn up to 45 days, and every day keeps a hover tooltip, while the table and `history.json` keep every day. Old trees are measured with today's counting rules and instruction list, so the series is comparable but not what the page showed on that day. The workflow checks out the full history for it. ## Comparisons diff --git a/tools/progress_history.py b/tools/progress_history.py index 5c5efe2f1b..f042791443 100644 --- a/tools/progress_history.py +++ b/tools/progress_history.py @@ -4,7 +4,7 @@ import json import subprocess import tarfile import tempfile -from datetime import date +from datetime import date, datetime, timezone from html import escape from pathlib import Path @@ -22,9 +22,9 @@ def git(*args): def daily_commits(ref): seen = {} - for line in git("log", "--first-parent", "--format=%H %cs", ref).decode().splitlines(): - sha, day = line.split() - seen.setdefault(day, sha) + for line in git("log", "--first-parent", "--format=%H %ct", ref).decode().splitlines(): + sha, timestamp = line.split() + seen.setdefault(datetime.fromtimestamp(int(timestamp), timezone.utc).date().isoformat(), sha) return sorted(seen.items()) @@ -147,7 +147,7 @@ def page(points): f'

Each panel has its own scale: the solid line is what is implemented, the dashed line what is declared. Library ' 'functions are declared in the prx sources; the shader total is the AMD RDNA 1 + RDNA 2 instruction list ' f'(tools/rdna_isa.txt). ' - 'One point per day with commits on main, taken at the last commit of the day. Every point is measured ' + 'One point per UTC day with commits on main, taken at the last commit of the day. Every point is measured ' f'with the current tools/progress.py rules and instruction list, ' 'so the series is comparable across days. Hover a day for the exact numbers. Raw numbers: history.json. ' 'Back to the current progress.

', diff --git a/tools/tests/test_progress_history.py b/tools/tests/test_progress_history.py index b467e9af88..9244d9fc79 100644 --- a/tools/tests/test_progress_history.py +++ b/tools/tests/test_progress_history.py @@ -14,13 +14,13 @@ import progress_history OPCODES = Path("core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp") -def commit(root, day, message, body): +def commit(root, day, message, body, time="12:00:00Z"): library = root / "core/libs/prx/libSceExample" library.mkdir(parents=True, exist_ok=True) (library / "Export.cpp").write_text(body) (root / OPCODES).parent.mkdir(parents=True, exist_ok=True) (root / OPCODES).write_text("enum class RdnaOpcode : int {\n SEndpgm,\n};\n") - env = {**os.environ, "GIT_AUTHOR_DATE": f"{day}T12:00:00Z", "GIT_COMMITTER_DATE": f"{day}T12:00:00Z"} + env = {**os.environ, "GIT_AUTHOR_DATE": f"{day}T{time}", "GIT_COMMITTER_DATE": f"{day}T{time}"} subprocess.run(["git", "-C", str(root), "add", "-A"], check=True) subprocess.run(["git", "-C", str(root), "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "-m", message], check=True, env=env) @@ -43,6 +43,17 @@ class ProgressHistoryTests(unittest.TestCase): self.assertIn(" Date: Sun, 18 Oct 2026 22:50:18 +0100 Subject: [PATCH 066/167] fix(agc): reread an invalid indirect register list where its packet executes SET_*_REG_INDIRECT lists are read when the command buffer is submitted. When that copy holds an offset dword that is no register offset, the queue worker now reads the list again where the packet executes (as the console PFP does) and uses that copy if it is valid. PPSA23566 (UFC 6) intermittently submits a SET_CONTEXT_REG_INDIRECT whose first entry is 0x038f038f or 0x03900390 in both copies, followed by 180 valid pairs, and the title stopped with "extended register semantics are not implemented". With APS5_SKIP_INVALID_INDIRECT_REGISTERS=1 the invalid entries are skipped and the other registers are written; without it the packet throws as before. --- .../libSceAgcDriver/Execution/include/Pm4.hpp | 3 +- .../src/Driver/Packets/PacketExecution.cpp | 29 ++++++++++++++++-- .../prx/libSceAgcDriver/Execution/src/Pm4.cpp | 22 +++++++++++--- core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 30 +++++++++++++++++++ docs/dev/ENVIRONMENT.md | 1 + docs/dev/debt/libSceAgcDriver.md | 2 +- 6 files changed, 79 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp index f902632e1a..909b403e26 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp @@ -89,7 +89,8 @@ void Validate(std::span packet, std::uint32_t queue); void Execute(std::span packet, QueueState& queue); inline bool IndirectRegisterOpcode(std::uint32_t opcode) { return opcode == 0x63 || opcode == 0x64 || opcode == 0x9f; } std::vector ReadIndirectRegisters(std::span packet); -void ExecuteIndirectRegisters(std::span packet, std::span pairs, QueueState& queue); +bool IndirectRegistersValid(std::span pairs); +void ExecuteIndirectRegisters(std::span packet, std::span pairs, QueueState& queue, bool skipInvalid = false); bool AccessesMemory(std::uint32_t header); // Whether an ACQUIRE_MEM packet asks only for GPU cache actions (no CPU-visible memory // synchronization): such a packet needs a pipeline barrier, not a device drain. diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 077fef550a..2a6537d79e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -11,6 +11,8 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Eq/include/Event.hpp" +#include +#include #include #include #include @@ -440,8 +442,31 @@ void Driver::execute(const Submission& submission) { dumpSampleCounters(Pm4::SampleDumpAddress(packet)); } else if (opcode != 0x42 && opcode != 0x46 && opcode != 0x58) { if (!wroteOnGpu) { - if (Pm4::IndirectRegisterOpcode(opcode)) Pm4::ExecuteIndirectRegisters(packet, submission.registerLists.at(cursor), queue); - else Pm4::Execute(packet, queue); + if (Pm4::IndirectRegisterOpcode(opcode)) { + const auto& listed = submission.registerLists.at(cursor); + if (Pm4::IndirectRegistersValid(listed)) { + Pm4::ExecuteIndirectRegisters(packet, listed, queue); + } else { + landPendingWaits(submission.queue); + recordQueuedLabelsBeforeRead(submission.queue); + const auto current = Pm4::ReadIndirectRegisters(packet); + static const bool skipInvalid = std::getenv("APS5_SKIP_INVALID_INDIRECT_REGISTERS") != nullptr; + static std::atomic reported{false}; + if (!reported.exchange(true)) { + std::uint32_t invalid = 0; + for (std::size_t i = 0; i < listed.size(); i += 2) { + if (!Pm4::IndirectRegistersValid(std::span(&listed[i], 1))) { + invalid = listed[i]; + break; + } + } + std::fprintf(stderr, "[gpu] queue 0x%x %s at DWORD %zu: register list entry with offset dword 0x%08x read again where the packet executes%s\n", submission.queue, Pm4::Name(packet[0]).c_str(), cursor, invalid, skipInvalid ? "; invalid entries are skipped (APS5_SKIP_INVALID_INDIRECT_REGISTERS)" : ""); + } + Pm4::ExecuteIndirectRegisters(packet, Pm4::IndirectRegistersValid(current) ? std::span(current) : std::span(listed), queue, skipInvalid); + } + } else { + Pm4::Execute(packet, queue); + } if (opcode == 0x49 || opcode == 0x37) { if (const auto label = Pm4::DecodeLabelWrite(packet)) noteLabelStore(label->address, label->Bytes(), ++eventSerial, submission.queue); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index aec2b3069b..369be6360f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -943,15 +943,29 @@ void NoteDispatchRegisters(std::span packet, QueueState& qu } } -void ExecuteIndirectRegisters(std::span packet, std::span pairs, QueueState& queue) { +bool IndirectRegistersValid(std::span pairs) { + for (std::size_t i = 0; i < pairs.size(); i += 2) { + if (pairs[i] == 0xffffffffu || (pairs[i] & ~0x70000000u) > 0xffffu) return false; + } + return true; +} + +void ExecuteIndirectRegisters(std::span packet, std::span pairs, QueueState& queue, bool skipInvalid) { const auto opcode = (packet[0] >> 8u) & 0xffu; require(packet.size() == 5 && IndirectRegisterOpcode(opcode), "expected indirect register packet"); require(pairs.size() == static_cast(packet[4]) * 2, "indirect register list does not match its packet"); - for (std::size_t i = 0; i < pairs.size(); i += 2) registerOffset(pairs[i]); - for (std::size_t i = 0; i < pairs.size(); i += 2) writeRegister(queue, opcode, registerOffset(pairs[i]), pairs[i + 1]); + const auto usable = [&](std::uint32_t value) { return !skipInvalid || IndirectRegistersValid(std::span(&value, 1)); }; + for (std::size_t i = 0; i < pairs.size(); i += 2) { + if (usable(pairs[i])) registerOffset(pairs[i]); + } + for (std::size_t i = 0; i < pairs.size(); i += 2) { + if (usable(pairs[i])) writeRegister(queue, opcode, registerOffset(pairs[i]), pairs[i + 1]); + } if (queue.savedContext.has_value() && TraceContextState()) { std::fprintf(stderr, "[context] indirect 0x%x:", opcode); - for (std::size_t i = 0; i < pairs.size(); i += 2) std::fprintf(stderr, " %x", registerOffset(pairs[i])); + for (std::size_t i = 0; i < pairs.size(); i += 2) { + if (usable(pairs[i])) std::fprintf(stderr, " %x", registerOffset(pairs[i])); + } std::fprintf(stderr, "\n"); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index d2e9b4c2aa..f18ad73c3a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -1012,6 +1012,34 @@ void testRegisterListsReadAtSubmission() { check(std::atomic_ref(done).load() == 1, "register list rewritten after submission was read by the worker"); } +void testInvalidRegisterListEntries() { + std::array pairs{0x038f038fu, 0xc8e9037eu, 0x10, 41, 0x11, 42}; + const auto packet = makePacket(0x9f, {low(pairs.data()), high(pairs.data()), 0x80000000, 3}); + check(!AgcDriver::Pm4::IndirectRegistersValid(pairs), "an offset dword with bits 16-27 set was taken as a register offset"); + check(!AgcDriver::Pm4::IndirectRegistersValid(std::array{0xffffffffu, 0}), "the sentinel was taken as a register offset"); + check(AgcDriver::Pm4::IndirectRegistersValid(std::array{0x10, 1, 0x70000011u, 2}), "a valid register list was rejected"); + AgcDriver::QueueState state; + expectFailure([&] { AgcDriver::Pm4::ExecuteIndirectRegisters(packet, pairs, state); }, "extended register"); + check(!state.context.contains(0x10) && !state.context.contains(0x11), "a rejected register list partially changed state"); + AgcDriver::Pm4::ExecuteIndirectRegisters(packet, pairs, state, true); + check(state.context.at(0x10) == 41 && state.context.at(0x11) == 42 && !state.context.contains(0x38f), "skipping an invalid entry lost the valid ones or wrote the invalid one"); +} + +void testRegisterListReadAgainAtExecution() { + alignas(8) static std::uint32_t gate = 0; + static std::uint32_t done = 0; + static std::array registers{0x038f038fu, 74}; + auto words = joinPackets({ + makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x19}), + makePacket(0x9f, {low(registers.data()), high(registers.data()), 0x80000000, 1}), + writeWord(done, 1)}); + submitWords(words); + registers[0] = 0x10; + std::atomic_ref(gate).store(1); + AgcDriverWaitIdle_nid_postfix(); + check(std::atomic_ref(done).load() == 1, "a register list invalid at submission and valid at execution was not read again"); +} + void testIndexAttributesReadAtExecution() { alignas(8) static std::uint32_t gate = 0; static std::uint32_t done = 0; @@ -1854,6 +1882,8 @@ int main(int argc, char** argv) { testUnwrittenUserData(); testDriverSubmission(); testRegisterListsReadAtSubmission(); + testInvalidRegisterListEntries(); + testRegisterListReadAgainAtExecution(); testIndexAttributesReadAtExecution(); testPredicatedSubmission(); testConditionalSubmission(); diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 222ee12c79..a3cc4b3b7e 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -350,6 +350,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SHADOW_VERIFY` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | | verifies that the shadow-published bytes equal the guest bytes | | `APS5_SHARED_COLLECT_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | uses the tracker's shared collect-memo ring under its mutex instead of the per-thread ring | | `APS5_SHARED_MEMORY_COUNTERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | accumulates the memory counters into one shared set with atomic RMWs | +| `APS5_SKIP_INVALID_INDIRECT_REGISTERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | skips the entries of a SET_*_REG_INDIRECT list whose offset dword is no register offset in both the submitted and the executed copy, and writes the others, instead of stopping | | `APS5_SKIP_REQUEST_DUMPS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp` | 8 | caps RecompileRequest dumps per shader-skip reason | | `APS5_SKIP_SHADER` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | 0 | hex program address of a shader to skip preparing | | `APS5_SNAPSHOT_CHECK_FATAL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | throws on the first snapshot that differs from registered memory instead of only counting them | diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index b1117bd00d..2034de35bf 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -111,7 +111,7 @@ - A [label or store](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`) into guest memory the device has no host import of waits for all recorded work and is then written by the CPU, so the queue worker stalls once per such label. The console's CP stores it in stream order without that stall. - [WRITE_DATA to thread-trace user-data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) updates the queue's UCONFIG values for register destinations `0xc340..0xc347`, including repeated writes to one address; these are not guest-memory labels. The range follows AMD's GFX10 `SQ_THREAD_TRACE_USERDATA_0..7` definitions in [the Linux register header](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/include/asic_reg/gc/gc_10_1_0_offset.h) and the packets emitted by PPSA21564's bundled AGC driver before its EOP flip. Other register destinations throw. GPU thread-trace capture and its user-data stream are not emitted; only the final register state is retained. Console trace behavior is unverified. - [WRITE_DATA with DST_SEL 3](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) writes guest memory like DST_SEL 5. AMD's GFX10 header names 3 the GDS, but Killzone: Liberation (PPSA11142) writes its frame fence with `sceAgcDcbWriteData(dst 6, cache policy 3, write confirm)`, which the SPRX-confirmed encoding ([OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/cb_builders.c)) turns into DST_SEL 3 with a 40-bit label address in the title's own memory that `RELEASE_MEM` and `WAIT_REG_MEM` then use; the console must write that label or the title would wait on it forever. Whether the console routes it through the GDS or a cache is unknown -- [SET_*_REG_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write. +- [SET_*_REG_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write. When the list read at submission holds an offset dword that is not a register offset (bits 16-27 set, or the 0xffffffff sentinel), the worker reads the list again where the packet executes and uses that copy if it is valid. PPSA23566 (UFC 6) intermittently submits a SET_CONTEXT_REG_INDIRECT whose first entry is 0x038f038f or 0x03900390 in both copies; what the PFP does with such an entry is unknown, so the packet still throws unless `APS5_SKIP_INVALID_INDIRECT_REGISTERS=1`, which skips the invalid entries and writes the others. - The [shader float mode](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (`FLOAT_MODE`, `DX10_CLAMP`, `IEEE_MODE` and `FP16_OVFL` of `COMPUTE_PGM_RSRC1` and `SPI_SHADER_PGM_RSRC1_PS/GS/HS`, bit positions from Mesa's `gfx10.json` and `gfx103.json`) is decoded into `GuestContext::floatMode` and is part of the shader cache keys and the serialized request (#981). `IEEE_MODE` is read by `v_min_f32`, `v_max_f32`, `v_min3_f32`, `v_max3_f32` and `v_med3_f32`, their f16 forms, `v_min_f64` and `v_max_f64`, `IEEE_MODE` and the `FLOAT_MODE` output denormal bits by the output modifier, `DX10_CLAMP` by the clamp modifier, `FP16_OVFL` by every instruction with an f16 result (see its entry), and the whole mode by the 16-bit interpolation instructions (see their entries). With IEEE=1 a signaling NaN operand returns that NaN quieted (the first operand checked first, min3/max3 as two nested pairs, med3 as min3 when any operand is NaN), measured on RDNA2 (gfx1035) over every combination of ±0, ±1, ±inf, denormals, quiet and signaling NaNs with f32 denormals kept and flushed. IEEE=0 and unknown modes keep the previous f32 lowering, which matches IEEE=0. The f16 forms choose their NaN results on the bits: a NaN operand returns the other operand unchanged (src0 when both are NaN), and with IEEE=1 a signaling NaN operand returns that NaN quieted, in the same order as f32. `v_min_f64` and `v_max_f64` quiet a signaling NaN operand the same way with IEEE=1; with IEEE=0 they return src0 unchanged when src1 is NaN, otherwise src1 when src0 is NaN. Both were measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f32 denormals kept and flushed and f16/f64 denormals kept; RDNA2 has no f64 min3, max3 or med3. Unknown modes follow IEEE=0. `v_min_f16` and `v_max_f16` flush each denormal input to its signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3 preserve inputs, even when output flushing is enabled. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1, including signed zeros, NaNs and neg/abs modifiers. Unknown modes keep f16 denormals. `v_min_f64` and `v_max_f64` flush subnormal inputs to signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3, and an unknown mode, preserve inputs. The fully flushed and preserved f64 behavior was [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/2036); split-mode selection follows [LLVM's denormal-mode enumeration](https://llvm.org/docs/AMDGPUUsage.html#floating-point-denorm-mode-enumeration-values). Other f64 arithmetic does not model these denormal controls. NaN quieting in f16 arithmetic (the unary instructions and the f32 arithmetic follow it, see below) and `FLOAT_MODE` (f32 denormals flushed before the min/max compare in Astro's Playroom's 0xc0) still follow a single mode, as the entries below record. The mode comes from the registered shader header's `sh_registers`, the same source at registration and at draw/dispatch so the prepared artifact keys match. A title that rewrites RSRC1 at run time, or a raw compute program with no header, compiles with that mode or none. - A [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) (libSceAgcDriver) that a compute queue passes on another queue's recorded label store, before the GPU has executed it, is landed before the queue's next dispatch, draw, COND_EXEC or predicated packet reads guest memory: the worker polls until memory satisfies the wait or no unfinished batch writes the label. The graphics queue (queue 0) is exempt, as landing its waits costs PPSA01341 about a quarter of its frame rate; a title that fills memory a graphics command buffer reads after it submits the buffer behind such a wait reads it early there. `APS5_LAND_WAITS_ALL_QUEUES=1` lands the graphics queue's waits too. - [CPU reads behind an address-based build](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp) (libSceAgcDriver) - an address-based store (FLAT/GLOBAL, or through a descriptor the recompiler cannot resolve to a static buffer) is not recorded as a pending write: its batch holds a lease on the registered allocations instead, and the flush hook does not wait for it. A DISPATCH_INDIRECT whose arguments are read on the CPU first finishes the batches recorded with a lease (`Graphics::FinishLeaseWork`). These reads do not, so they can see guest memory from before such a build ran: the indirect draw arguments and draw count of a draw resolved on the CPU (`Pm4::ReadDrawArguments`, `Pm4::ReadDrawCount`), the count record of the [copy kernel](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp) HLE (guarded only by its shape check), and the checks in [Submission.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) that decide whether a queue awaits a label only the title writes (`awaitsTitle`, `orderReleased`). No test covers them and no title has been seen to read such memory there. From ae67e2b7f9addf92f3ddb0ea0737644dc20dc273 Mon Sep 17 00:00:00 2001 From: Longziip Date: Sun, 18 Oct 2026 23:50:09 +0100 Subject: [PATCH 067/167] fix(agc): write the reread indirect register list when skipping its invalid entries When the list read at submission is invalid, the worker now always executes the copy read again where the packet runs. With APS5_SKIP_INVALID_INDIRECT_REGISTERS=1 only that copy's invalid entries are skipped, so the valid entries of a partly updated list are written instead of the stale ones from submission. testPartlyUpdatedRegisterListSkipsOnlyItsInvalidEntries submits a list that is invalid at submission, updates it to a valid entry plus an invalid one before its packet runs, and reads the register back with COPY_DATA. --- .../src/Driver/Packets/PacketExecution.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 22 +++++++++++++++++++ docs/dev/ENVIRONMENT.md | 2 +- docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 2a6537d79e..1f5b338694 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -462,7 +462,7 @@ void Driver::execute(const Submission& submission) { } std::fprintf(stderr, "[gpu] queue 0x%x %s at DWORD %zu: register list entry with offset dword 0x%08x read again where the packet executes%s\n", submission.queue, Pm4::Name(packet[0]).c_str(), cursor, invalid, skipInvalid ? "; invalid entries are skipped (APS5_SKIP_INVALID_INDIRECT_REGISTERS)" : ""); } - Pm4::ExecuteIndirectRegisters(packet, Pm4::IndirectRegistersValid(current) ? std::span(current) : std::span(listed), queue, skipInvalid); + Pm4::ExecuteIndirectRegisters(packet, current, queue, skipInvalid); } } else { Pm4::Execute(packet, queue); diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index f18ad73c3a..23279cc92d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -1025,6 +1026,26 @@ void testInvalidRegisterListEntries() { check(state.context.at(0x10) == 41 && state.context.at(0x11) == 42 && !state.context.contains(0x38f), "skipping an invalid entry lost the valid ones or wrote the invalid one"); } +void testPartlyUpdatedRegisterListSkipsOnlyItsInvalidEntries() { +#ifdef _WIN32 + check(_putenv_s("APS5_SKIP_INVALID_INDIRECT_REGISTERS", "1") == 0, "cannot enable skipping invalid register list entries"); +#else + check(setenv("APS5_SKIP_INVALID_INDIRECT_REGISTERS", "1", 1) == 0, "cannot enable skipping invalid register list entries"); +#endif + alignas(8) static std::uint32_t gate = 0; + alignas(8) static std::uint32_t copied = 0; + static std::array registers{0x038f038fu, 5, 0x11, 9}; + auto words = joinPackets({ + makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x19}), + makePacket(0x9f, {low(registers.data()), high(registers.data()), 0x80000000, 2}), + makePacket(0x40, {0x100, 0xa010, 0, low(&copied), high(&copied)})}); + submitWords(words); + registers = {0x10, 77, 0x03900390u, 6}; + std::atomic_ref(gate).store(1); + AgcDriverWaitIdle_nid_postfix(); + check(std::atomic_ref(copied).load() == 77, "a register list partly updated before its packet ran lost the current valid entries"); +} + void testRegisterListReadAgainAtExecution() { alignas(8) static std::uint32_t gate = 0; static std::uint32_t done = 0; @@ -1883,6 +1904,7 @@ int main(int argc, char** argv) { testDriverSubmission(); testRegisterListsReadAtSubmission(); testInvalidRegisterListEntries(); + testPartlyUpdatedRegisterListSkipsOnlyItsInvalidEntries(); testRegisterListReadAgainAtExecution(); testIndexAttributesReadAtExecution(); testPredicatedSubmission(); diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index a3cc4b3b7e..52774db276 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -350,7 +350,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SHADOW_VERIFY` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | | verifies that the shadow-published bytes equal the guest bytes | | `APS5_SHARED_COLLECT_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | uses the tracker's shared collect-memo ring under its mutex instead of the per-thread ring | | `APS5_SHARED_MEMORY_COUNTERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | accumulates the memory counters into one shared set with atomic RMWs | -| `APS5_SKIP_INVALID_INDIRECT_REGISTERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | skips the entries of a SET_*_REG_INDIRECT list whose offset dword is no register offset in both the submitted and the executed copy, and writes the others, instead of stopping | +| `APS5_SKIP_INVALID_INDIRECT_REGISTERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | skips the entries whose offset dword is no register offset in a SET_*_REG_INDIRECT list read again where its packet executes, and writes its valid entries, instead of stopping | | `APS5_SKIP_REQUEST_DUMPS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp` | 8 | caps RecompileRequest dumps per shader-skip reason | | `APS5_SKIP_SHADER` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | 0 | hex program address of a shader to skip preparing | | `APS5_SNAPSHOT_CHECK_FATAL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | throws on the first snapshot that differs from registered memory instead of only counting them | diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 2034de35bf..cc5e298218 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -111,7 +111,7 @@ - A [label or store](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`) into guest memory the device has no host import of waits for all recorded work and is then written by the CPU, so the queue worker stalls once per such label. The console's CP stores it in stream order without that stall. - [WRITE_DATA to thread-trace user-data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) updates the queue's UCONFIG values for register destinations `0xc340..0xc347`, including repeated writes to one address; these are not guest-memory labels. The range follows AMD's GFX10 `SQ_THREAD_TRACE_USERDATA_0..7` definitions in [the Linux register header](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/include/asic_reg/gc/gc_10_1_0_offset.h) and the packets emitted by PPSA21564's bundled AGC driver before its EOP flip. Other register destinations throw. GPU thread-trace capture and its user-data stream are not emitted; only the final register state is retained. Console trace behavior is unverified. - [WRITE_DATA with DST_SEL 3](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) writes guest memory like DST_SEL 5. AMD's GFX10 header names 3 the GDS, but Killzone: Liberation (PPSA11142) writes its frame fence with `sceAgcDcbWriteData(dst 6, cache policy 3, write confirm)`, which the SPRX-confirmed encoding ([OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/cb_builders.c)) turns into DST_SEL 3 with a 40-bit label address in the title's own memory that `RELEASE_MEM` and `WAIT_REG_MEM` then use; the console must write that label or the title would wait on it forever. Whether the console routes it through the GDS or a cache is unknown -- [SET_*_REG_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write. When the list read at submission holds an offset dword that is not a register offset (bits 16-27 set, or the 0xffffffff sentinel), the worker reads the list again where the packet executes and uses that copy if it is valid. PPSA23566 (UFC 6) intermittently submits a SET_CONTEXT_REG_INDIRECT whose first entry is 0x038f038f or 0x03900390 in both copies; what the PFP does with such an entry is unknown, so the packet still throws unless `APS5_SKIP_INVALID_INDIRECT_REGISTERS=1`, which skips the invalid entries and writes the others. +- [SET_*_REG_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write. When the list read at submission holds an offset dword that is not a register offset (bits 16-27 set, or the 0xffffffff sentinel), the worker reads the list again where the packet executes and writes that copy. PPSA23566 (UFC 6) intermittently submits a SET_CONTEXT_REG_INDIRECT whose first entry is 0x038f038f or 0x03900390 in both copies; what the PFP does with such an entry is unknown, so a copy that still holds one throws unless `APS5_SKIP_INVALID_INDIRECT_REGISTERS=1`, which skips its invalid entries and writes its valid ones. - The [shader float mode](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (`FLOAT_MODE`, `DX10_CLAMP`, `IEEE_MODE` and `FP16_OVFL` of `COMPUTE_PGM_RSRC1` and `SPI_SHADER_PGM_RSRC1_PS/GS/HS`, bit positions from Mesa's `gfx10.json` and `gfx103.json`) is decoded into `GuestContext::floatMode` and is part of the shader cache keys and the serialized request (#981). `IEEE_MODE` is read by `v_min_f32`, `v_max_f32`, `v_min3_f32`, `v_max3_f32` and `v_med3_f32`, their f16 forms, `v_min_f64` and `v_max_f64`, `IEEE_MODE` and the `FLOAT_MODE` output denormal bits by the output modifier, `DX10_CLAMP` by the clamp modifier, `FP16_OVFL` by every instruction with an f16 result (see its entry), and the whole mode by the 16-bit interpolation instructions (see their entries). With IEEE=1 a signaling NaN operand returns that NaN quieted (the first operand checked first, min3/max3 as two nested pairs, med3 as min3 when any operand is NaN), measured on RDNA2 (gfx1035) over every combination of ±0, ±1, ±inf, denormals, quiet and signaling NaNs with f32 denormals kept and flushed. IEEE=0 and unknown modes keep the previous f32 lowering, which matches IEEE=0. The f16 forms choose their NaN results on the bits: a NaN operand returns the other operand unchanged (src0 when both are NaN), and with IEEE=1 a signaling NaN operand returns that NaN quieted, in the same order as f32. `v_min_f64` and `v_max_f64` quiet a signaling NaN operand the same way with IEEE=1; with IEEE=0 they return src0 unchanged when src1 is NaN, otherwise src1 when src0 is NaN. Both were measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f32 denormals kept and flushed and f16/f64 denormals kept; RDNA2 has no f64 min3, max3 or med3. Unknown modes follow IEEE=0. `v_min_f16` and `v_max_f16` flush each denormal input to its signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3 preserve inputs, even when output flushing is enabled. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1, including signed zeros, NaNs and neg/abs modifiers. Unknown modes keep f16 denormals. `v_min_f64` and `v_max_f64` flush subnormal inputs to signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3, and an unknown mode, preserve inputs. The fully flushed and preserved f64 behavior was [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/2036); split-mode selection follows [LLVM's denormal-mode enumeration](https://llvm.org/docs/AMDGPUUsage.html#floating-point-denorm-mode-enumeration-values). Other f64 arithmetic does not model these denormal controls. NaN quieting in f16 arithmetic (the unary instructions and the f32 arithmetic follow it, see below) and `FLOAT_MODE` (f32 denormals flushed before the min/max compare in Astro's Playroom's 0xc0) still follow a single mode, as the entries below record. The mode comes from the registered shader header's `sh_registers`, the same source at registration and at draw/dispatch so the prepared artifact keys match. A title that rewrites RSRC1 at run time, or a raw compute program with no header, compiles with that mode or none. - A [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) (libSceAgcDriver) that a compute queue passes on another queue's recorded label store, before the GPU has executed it, is landed before the queue's next dispatch, draw, COND_EXEC or predicated packet reads guest memory: the worker polls until memory satisfies the wait or no unfinished batch writes the label. The graphics queue (queue 0) is exempt, as landing its waits costs PPSA01341 about a quarter of its frame rate; a title that fills memory a graphics command buffer reads after it submits the buffer behind such a wait reads it early there. `APS5_LAND_WAITS_ALL_QUEUES=1` lands the graphics queue's waits too. - [CPU reads behind an address-based build](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp) (libSceAgcDriver) - an address-based store (FLAT/GLOBAL, or through a descriptor the recompiler cannot resolve to a static buffer) is not recorded as a pending write: its batch holds a lease on the registered allocations instead, and the flush hook does not wait for it. A DISPATCH_INDIRECT whose arguments are read on the CPU first finishes the batches recorded with a lease (`Graphics::FinishLeaseWork`). These reads do not, so they can see guest memory from before such a build ran: the indirect draw arguments and draw count of a draw resolved on the CPU (`Pm4::ReadDrawArguments`, `Pm4::ReadDrawCount`), the count record of the [copy kernel](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp) HLE (guarded only by its shape check), and the checks in [Submission.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) that decide whether a queue awaits a label only the title writes (`awaitsTitle`, `orderReleased`). No test covers them and no title has been seen to read such memory there. 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 068/167] 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 069/167] 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 d37221a00ac191ba3eb90bb8f091336767139ea1 Mon Sep 17 00:00:00 2001 From: mopgobopot Date: Mon, 19 Oct 2026 20:01:41 +0300 Subject: [PATCH 070/167] 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 84546771871f9cc9b9a2e510951c5ad408d4338f Mon Sep 17 00:00:00 2001 From: primesoul-web Date: Mon, 19 Oct 2026 17:29:10 +0000 Subject: [PATCH 071/167] feat(libs): the sceImeOpen text and external-keyboard filters The text filter follows KytyPS5's contract; the external keyboard filter is stored on the session and is not called for the host input path, which delivers no key data. --- core/libs/prx/libSceIme/Export.cpp | 75 +++++++++++-- core/libs/tests/GuestImeOpen.cpp | 165 +++++++++++++++++++++++++++-- docs/dev/debt/libSceIme.md | 3 +- 3 files changed, 225 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 575463cf73..49e108d0da 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceIme/include/ImeInput.hpp" @@ -59,6 +60,7 @@ constexpr uint32_t CaretMoveRight = 2; struct Session { Param param{}; + ImeExtKeyboardFilter extKeyboardFilter = nullptr; std::u16string text; std::uint32_t caret = 0; std::deque events; @@ -157,6 +159,64 @@ void QueueCaretSteps(Session& session, uint32_t direction, std::size_t steps) { } } +struct EditDelta { + std::uint32_t index = 0; + std::int32_t length = 0; +}; + +EditDelta ComputeEditDelta(const std::u16string& before, const std::u16string& after) { + std::size_t prefix = 0; + while (prefix < before.size() && prefix < after.size() && before[prefix] == after[prefix]) ++prefix; + std::size_t beforeTail = before.size(); + std::size_t afterTail = after.size(); + while (beforeTail > prefix && afterTail > prefix && before[beforeTail - 1] == after[afterTail - 1]) { + --beforeTail; + --afterTail; + } + return {static_cast(prefix), + static_cast(afterTail - prefix) - static_cast(beforeTail - prefix)}; +} + +void NormalizeCaret(Session& session) { + if (session.caret > session.text.size()) session.caret = static_cast(session.text.size()); + if (session.caret > 0 && session.caret < session.text.size() && + IsHighSurrogate(session.text[session.caret - 1]) && IsLowSurrogate(session.text[session.caret])) { + ++session.caret; + } +} + +bool RunTextFilter(const Session& session, std::u16string* filtered) { + std::vector buffer(session.param.max_text_length + 1, u'\0'); + std::uint32_t length = session.param.max_text_length; + if (session.param.filter(buffer.data(), &length, session.text.c_str(), + static_cast(session.text.size())) != 0) { + return false; + } + if (length > session.param.max_text_length || + !ValidText(buffer.data(), length, (session.param.option & OptionMultiline) != 0)) { + return false; + } + filtered->assign(buffer.data(), buffer.data() + length); + return true; +} + +void CommitEdit(Session& session, const std::u16string& oldText, std::uint32_t plainIndex, std::int32_t plainDelta) { + if (session.param.filter) { + std::u16string filtered; + if (RunTextFilter(session, &filtered)) { + session.text = std::move(filtered); + NormalizeCaret(session); + } + if (session.text == oldText) return; + SyncBuffers(session); + const EditDelta edit = ComputeEditDelta(oldText, session.text); + QueueText(session, EventUpdateText, edit.index, edit.length); + return; + } + SyncBuffers(session); + QueueText(session, EventUpdateText, plainIndex, plainDelta); +} + void Insert(Session& session, std::u16string units) { const bool multiline = (session.param.option & OptionMultiline) != 0; std::u16string accepted; @@ -169,10 +229,10 @@ void Insert(Session& session, std::u16string units) { if (count > 0 && count < accepted.size() && IsHighSurrogate(accepted[count - 1])) --count; if (count == 0) return; const uint32_t index = session.caret; + const std::u16string oldText = session.text; session.text.insert(index, accepted, 0, count); session.caret += static_cast(count); - SyncBuffers(session); - QueueText(session, EventUpdateText, index, static_cast(count)); + CommitEdit(session, oldText, index, static_cast(count)); } std::size_t UnitsBefore(const Session& session) { @@ -198,18 +258,18 @@ void Apply(Session& session, const ImeHostInput& input) { case ImeHostInputKind::Backspace: { const std::size_t units = UnitsBefore(session); if (units == 0) return; + const std::u16string oldText = session.text; session.caret -= static_cast(units); session.text.erase(session.caret, units); - SyncBuffers(session); - QueueText(session, EventUpdateText, session.caret, -static_cast(units)); + CommitEdit(session, oldText, session.caret, -static_cast(units)); return; } case ImeHostInputKind::Delete: { const std::size_t units = UnitsAfter(session); if (units == 0) return; + const std::u16string oldText = session.text; session.text.erase(session.caret, units); - SyncBuffers(session); - QueueText(session, EventUpdateText, session.caret, -static_cast(units)); + CommitEdit(session, oldText, session.caret, -static_cast(units)); return; } case ImeHostInputKind::Left: { @@ -317,10 +377,9 @@ int APS5_VABI sceImeOpen_nid_postfix(const Param* param, const ExtendedParam* ex std::lock_guard lock(g_imeMutex); if (g_session) return ErrorBusy; if (const int error = ValidateOpen(param, extended)) return error; - if (param->filter) NotImplemented_nid_no_patch("sceImeOpen: text filter"); - if (extended && extended->ext_keyboard_filter) NotImplemented_nid_no_patch("sceImeOpen: external keyboard filter"); Session session; session.param = *param; + session.extKeyboardFilter = extended ? extended->ext_keyboard_filter : nullptr; session.text.assign(param->input_text_buffer, TextLength(param->input_text_buffer, param->max_text_length)); session.caret = static_cast(session.text.size()); SyncBuffers(session); diff --git a/core/libs/tests/GuestImeOpen.cpp b/core/libs/tests/GuestImeOpen.cpp index 15029e736b..a1ea6fdc87 100644 --- a/core/libs/tests/GuestImeOpen.cpp +++ b/core/libs/tests/GuestImeOpen.cpp @@ -73,6 +73,58 @@ void APS5_VABI Other(void*, const ImeEvent*) { std::abort(); } +int32_t UpperFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + for (uint32_t i = 0; i < sourceTextLength; ++i) { + const char16_t unit = sourceText[i]; + outText[i] = unit >= u'a' && unit <= u'z' ? static_cast(unit - u'a' + u'A') : unit; + } + *outTextLength = sourceTextLength; + return 0; +} + +int32_t FailingFilter(char16_t*, uint32_t*, const char16_t*, uint32_t) { + return 1; +} + +int32_t StripXFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + uint32_t length = 0; + for (uint32_t i = 0; i < sourceTextLength; ++i) { + if (sourceText[i] != u'x') outText[length++] = sourceText[i]; + } + *outTextLength = length; + return 0; +} + +int32_t InvalidOutputFilter(char16_t* outText, uint32_t* outTextLength, const char16_t*, uint32_t) { + outText[0] = 0xdc00; + *outTextLength = 1; + return 0; +} + +int32_t TooLongFilter(char16_t*, uint32_t* outTextLength, const char16_t*, uint32_t) { + *outTextLength = 9; + return 0; +} + +int32_t ShortenFilter(char16_t* outText, uint32_t* outTextLength, const char16_t*, uint32_t) { + outText[0] = u'a'; + *outTextLength = 1; + return 0; +} + +int32_t PairAtCaretFilter(char16_t* outText, uint32_t* outTextLength, const char16_t*, uint32_t) { + outText[0] = u'a'; + outText[1] = u'b'; + outText[2] = 0xd83d; + outText[3] = 0xde00; + *outTextLength = 4; + return 0; +} + +int ExtNoop(const ImeKeycode*, std::uint16_t*, std::uint32_t*, void*) { + return 0; +} + void Require(bool value, const char* message) { if (value) return; std::fprintf(stderr, "%s\n", message); @@ -174,15 +226,17 @@ void CheckValidation() { Require(sceImeClose_nid_postfix() == 0, "close after over-2K open"); param = MakeParam(buffers, 16); - param.filter = reinterpret_cast(&Record); - bool threw = false; - try { - sceImeOpen_nid_postfix(¶m, nullptr); - } catch (const std::exception&) { - threw = true; - } - Require(threw, "text filter must throw"); - Require(!ImeCapturesKeyboard_nid_postfix(), "failed open left a session"); + param.filter = &UpperFilter; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a text filter"); + Require(ImeCapturesKeyboard_nid_postfix(), "text filter session not open"); + Require(sceImeClose_nid_postfix() == 0, "close the text filter session"); + + param = MakeParam(buffers, 16); + ExtendedParam extFilters{}; + extFilters.ext_keyboard_filter = &ExtNoop; + Require(sceImeOpen_nid_postfix(¶m, &extFilters) == 0, "open with an external keyboard filter"); + Require(ImeCapturesKeyboard_nid_postfix(), "external keyboard filter session not open"); + Require(sceImeClose_nid_postfix() == 0, "close the external keyboard filter session"); } void CheckEditing() { @@ -304,6 +358,98 @@ void CheckMultiline() { Require(sceImeClose_nid_postfix() == 0, "multiline close"); } +void CheckTextFilter() { + Buffers buffers; + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + Param param = MakeParam(buffers, 8); + param.filter = &UpperFilter; + ExtendedParam extFilters{}; + extFilters.ext_keyboard_filter = &ExtNoop; + Require(sceImeOpen_nid_postfix(¶m, &extFilters) == 0, "open with both filters"); + Drain(); + + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"ABC", 3, 0, 1); + Require(std::u16string(buffers.text) == u"ABC", "filtered input buffer"); + Require(std::u16string(reinterpret_cast(buffers.work)) == u"ABC", "filtered work copy"); + + Publish(ImeHostInputKind::Backspace); + RequireText(Drain().at(0), EventUpdateText, u"AB", 2, 2, -1); + + Publish(ImeHostInputKind::Left); + RequireCaretSteps(Drain(), 1, 1); + Publish(ImeHostInputKind::Delete); + RequireText(Drain().at(0), EventUpdateText, u"A", 1, 1, -1); + + Require(sceImeSetText(u"xy", 2) == 0, "set text with a filter"); + Require(std::u16string(buffers.text) == u"xy", "set text is not filtered"); + Require(Drain().empty(), "set text with a filter queued an event"); + Require(sceImeClose_nid_postfix() == 0, "close the filtered session"); + + buffers = Buffers{}; + param = MakeParam(buffers, 8); + param.filter = &StripXFilter; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a stripping filter"); + Drain(); + Publish(ImeHostInputKind::Text, "x"); + Require(Drain().empty(), "a stripped edit sends no event"); + Require(std::u16string(buffers.text).empty(), "a stripped edit leaves the buffers"); + Publish(ImeHostInputKind::Text, "b"); + RequireText(Drain().at(0), EventUpdateText, u"b", 1, 0, 1); + Publish(ImeHostInputKind::Text, "x"); + Require(Drain().empty(), "a stripped edit sends no event again"); + Require(std::u16string(buffers.text) == u"b", "buffers after a stripped edit"); + Require(sceImeClose_nid_postfix() == 0, "close the stripping session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = &FailingFilter; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a failing filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"abc", 3, 2, 1); + Require(std::u16string(buffers.text) == u"abc", "a failed filter keeps the edit"); + Require(sceImeClose_nid_postfix() == 0, "close the failing session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = &InvalidOutputFilter; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with an invalid-output filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"abc", 3, 2, 1); + Require(std::u16string(buffers.text) == u"abc", "an invalid filter output keeps the edit"); + Require(sceImeClose_nid_postfix() == 0, "close the invalid-output session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = &TooLongFilter; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a too-long filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"abc", 3, 2, 1); + Require(std::u16string(buffers.text) == u"abc", "an overlong filter output keeps the edit"); + Require(sceImeClose_nid_postfix() == 0, "close the too-long session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = &ShortenFilter; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a shortening filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"a", 1, 1, -1); + Require(sceImeClose_nid_postfix() == 0, "close the shortening session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = &PairAtCaretFilter; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a pair filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"ab\U0001F600", 4, 2, 2); + Require(sceImeClose_nid_postfix() == 0, "close the pair session"); +} + void CheckHostEvents() { Buffers buffers; Param param = MakeParam(buffers, 16); @@ -346,5 +492,6 @@ int main() { CheckValidation(); CheckEditing(); CheckMultiline(); + CheckTextFilter(); CheckHostEvents(); } diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 76178899b3..0bbee7c5b0 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -7,10 +7,11 @@ ### Unknown function info - [sceImeGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills. While a panel is open it reports the type, position and alignments passed to `sceImeOpen` and the size `sceImeGetPanelSize` gives; the console's values for a panel moved by its own position adjustment are unknown. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title -- [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. A text filter or an external keyboard filter throws, as their contract is unknown. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn +- [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn ### Functional - [sceImeKeyboardOpen, sceImeKeyboardClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - opening a keyboard that is already open for the same user returns `0x80bc0001` (busy) and closing one that is not open returns `0x80bc0002`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/ime.cpp) does; it tracks one keyboard for all users, while the open state is kept per user id here, and the PS5 result is unverified - [sceImeKeyboardGetInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. - [sceImeKeyboardGetResourceId](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard +- [sceImeOpen text filter](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) runs on the text edits of host input - typing, backspace, forward delete and a multiline Enter - following [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/ime.cpp), not confirmed on a PS5. The filter is called with the edited text, a buffer of `maxTextLength` UTF-16 units and its length; a nonzero return, an output longer than the capacity, an unpaired surrogate or, without the multiline option, a line break keeps the edit unfiltered. An accepted output replaces the text, the caret stays where the edit put it and is clamped to the text and normalized past a surrogate pair, and one UpdateText is sent with the minimal edit delta of the whole old and new text, as KytyPS5 computes it; an output equal to the previous text sends no event. Caret moves, PressEnter without multiline, PressClose, `sceImeSetText` and `sceImeSetCaret` do not run the filter, as in KytyPS5. An external keyboard filter is accepted and stored on the session: it is called only for keyboard events that carry key data, which the host input path does not produce - it delivers text and actions, as KytyPS5's host API queues external inputs with a zeroed key and skips the filter for them - so it is never called From 6dc7c9fbb2febea719d8f2b55f4cf791443b6e6a Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Mon, 19 Oct 2026 19:39:24 +0200 Subject: [PATCH 072/167] 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 df6b81f83b7b55264cf7fc1b17c717f3f278f606 Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Mon, 19 Oct 2026 14:56:52 -0300 Subject: [PATCH 073/167] feat(libSceAudioOut): export missing firmware audio output interfaces --- core/libs/prx/libSceAudioOut/CMakeLists.txt | 1 + .../prx/libSceAudioOut/src/Unimplemented.cpp | 548 ++++++++++++++++++ docs/dev/debt/libSceAudioOut.md | 108 ++++ 3 files changed, 657 insertions(+) create mode 100644 core/libs/prx/libSceAudioOut/src/Unimplemented.cpp diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 59f2383f50..665bb433be 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -9,6 +9,7 @@ add_library(libSceAudioOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2User.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Mastering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2System.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Unimplemented.cpp ) target_include_directories(libSceAudioOut PRIVATE diff --git a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp new file mode 100644 index 0000000000..99e026d61e --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp @@ -0,0 +1,548 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAudioOut2UserCreateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortSetAttributes(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextAdvance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextPush(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextGetQueueLevel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortSetAttributesEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortGetAttributes(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetSystemDebugState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetSystemState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortGetState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetHandleStatusInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetSimulatedHandleStatusInfo2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutChangeAppModuleState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetSimulatedHandleStatusInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetSimulatedBusUsableStatusByBusType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutAttachToApplicationByPid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutDetachFromApplicationByPid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutPtOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutPtClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutPtGetLastOutputTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExPtOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExPtClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExPtGetLastOutputTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExGetLastOutputTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetInfoOpenNum(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutMasteringInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutMasteringSetParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutMasteringGetState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutMasteringTerm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortCreateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetPadSpkVolume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetPadJackVolume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetUsbVolume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetCaesarVolume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetHeadphoneOutMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetFocusEnablePid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetMainOutput(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetVolumeDown(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetChatBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetRecMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetAllMute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetSparkEQ(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutArbitrationInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2ArbitrationInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnable3DAudioByArbitration(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutAssignHRTF(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnable3DAudioForUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableMonoHpOutput(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableTVVirtual(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableTVCorrection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableVrControllerLowPowerMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableCinematicVrRotation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutCustomHrtfIsExist(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExConfigureOutputMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExSystemInfoIsSupportedAudioOutExMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExGetSystemInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExGetOutputInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysConfigureOutputMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysGetMonitorInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysHdmiMonitorInfoIsSupportedAudioOutMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysGetSystemInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysGetOutputInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextQueryMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextQueryMemoryEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextGetAttributes(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextCreateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextSetAttributes(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetPortConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetPortStatuses(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetHeadphoneOutMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusUserLoginFor3DAudio(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetDeviceConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetMorpheusParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetCaesarParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetSparkParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2AllAudioMainToHeadphoneForDiag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortGetStateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2ReportRegisterHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2ReportUnregisterHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetTvCorrectionInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2SetTvCorrectionInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetIpcHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetSystemStateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetSystemDebugState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MasteringInitEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2SetSpeakerInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2SetDebugHeadphoneMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2DebugStateCtrl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2ContextBedWrite(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2PortCreateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2PortSetAttributesEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2InitializeEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2Terminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2PortRegister(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2PortUnregister(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetAmbisonicsEncodeCoefficients(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetHrtfIdForCronos(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2SetHrtfIdForCronos(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 43372d1a15..ec058e7f31 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -24,6 +24,114 @@ - [sceAudioOutDeviceIdOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA01317, which fails to load without it; declared without parameters, signature unknown. It opens a port on a given audio device; which device ids exist and how they map to host outputs is unknown, so it throws - [sceAudioOut2PortGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) and [sceAudioOut2GetSpeakerInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - the port state reports the channel count of the port's data format, output main, volume 127 and a zero reroute counter and flags. Taking the channel count from the data format follows [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs), another emulator. A payload process on a PS5 (firmware 13.60), which is not the foreground application, read output 0, volume -1 and reroute counter 5 for its main ports, and 6 channels for an 8-channel port on a 5.1 system, so a title's values are unverified, including the output of a pad port, the volume after `sceAudioOut2PortSetAttributes` and the reroute counter on a device change. The speaker info reports type 0 with available bits `0x3`, the front left speaker at +30 degrees and the front right one at -30, and -360 for the azimuth and elevation of every other slot. On that console the type followed the HDMI device type and the available bits its channels: type 0 with the device set to TV, 1 to AV amplifier and 2 to sound bar, which [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) names television, AV receiver and sound bar (3 is headphones there), and available bits `0x3` for 2 channels and `0x3F` for 5.1 (centre at 0, surround pair at +110 and -110, -360 for the LFE). The front pair and the empty slots read the same in every setting; 7.1 on the AV amplifier reported 2 channels on that console's HDMI chain. The host output is always reported as a stereo television here. The console also returns `0x80268006` before `sceAudioOut2Initialize`, `0x80268001` for flags above 1 and `0x8026800C` for a null pointer, which are not modelled. ASTRO BOT and ASTRO's PLAYROOM reportedly stop on an assertion that depends on these values (#4317), but which field matters has not been isolated +- [sceAudioOut2UserCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortDestroy](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextDestroy](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutOpenEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortSetAttributes](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextAdvance](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextPush](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextGetQueueLevel](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortSetAttributesEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortGetAttributes](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetSystemState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortGetState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetHandleStatusInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetSimulatedHandleStatusInfo2](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutChangeAppModuleState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetSimulatedHandleStatusInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetSimulatedBusUsableStatusByBusType](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutAttachToApplicationByPid](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutDetachFromApplicationByPid](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutPtOpen](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutPtClose](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutPtGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExPtOpen](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExPtClose](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExPtGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetInfoOpenNum](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutMasteringInit](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutMasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutMasteringGetState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutMasteringTerm](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetPadSpkVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetPadJackVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetUsbVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetCaesarVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetHeadphoneOutMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetFocusEnablePid](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetMainOutput](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetVolumeDown](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetChatBalance](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetRecMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetAllMute](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetSparkEQ](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutArbitrationInitialize](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2ArbitrationInitialize](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnable3DAudioByArbitration](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutAssignHRTF](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnable3DAudioForUser](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableMonoHpOutput](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableTVVirtual](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableTVCorrection](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableVrControllerLowPowerMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableCinematicVrRotation](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutCustomHrtfIsExist](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExConfigureOutputMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExSystemInfoIsSupportedAudioOutExMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExGetSystemInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExGetOutputInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysConfigureOutputMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysHdmiMonitorInfoIsSupportedAudioOutMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysGetSystemInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysGetOutputInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextQueryMemory](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextQueryMemoryEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextGetAttributes](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextCreate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextSetAttributes](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusInit](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetPortConnections](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetConnections](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetPortStatuses](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetHeadphoneOutMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusUserLoginFor3DAudio](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetDeviceConnections](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetMorpheusParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetCaesarParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetSparkParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2AllAudioMainToHeadphoneForDiag](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortCreate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortGetStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2ReportRegisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2ReportUnregisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetTvCorrectionInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2SetTvCorrectionInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetIpcHandle](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetSystemStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MasteringInitEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2SetSpeakerInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2SetDebugHeadphoneMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2DebugStateCtrl](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoInit](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2ContextBedWrite](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2PortCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2PortSetAttributesEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2InitializeEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2Terminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2PortRegister](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2PortUnregister](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetAmbisonicsEncodeCoefficients](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetHrtfIdForCronos](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2SetHrtfIdForCronos](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature ### Functional From 7109c4b4af277b2f2fc100b6791e0042f056a742 Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Mon, 19 Oct 2026 15:04:11 -0300 Subject: [PATCH 074/167] feat(libSceHmd2): export missing firmware HMD2 interfaces --- core/libs/prx/libSceHmd2/Export.cpp | 495 ++++++++++++++++++++++++++++ docs/dev/debt/libSceHmd2.md | 99 ++++++ 2 files changed, 594 insertions(+) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index f2ade7c5ed..21fcb8e5b0 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -381,4 +381,499 @@ int APS5_VABI sceHmd2SetIpdOffset() { return 0; } +int APS5_VABI sceHmd2ReprojectionClearEventGpuEnd(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSlamTraceStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2ReprojectionClearEventGpuStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalRelocalizerSetResultData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetSlamCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetBudgetMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalRelocalizerOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetPortStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalPowerEventInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalRelocalizerClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalTrackingStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalLedDetectorStartBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGetDeviceInformation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageStartBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetPortConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetCameraSeeThroughMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetFakeLensSeparationDistance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalResetViewPosition(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2GazeGetEnabledEye(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalDfuSend(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalTrackingReadData2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGetVrMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalLedDetectorOpenBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageOpenBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGetFwVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageReadData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGetTrackerFtsData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalTrackingOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGazeReadStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalRelocalizerStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetPowerOff(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalCrashReportOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalTrackingClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGazeStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalCrashReportClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalDfuOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageSetConfigBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetCameraSeeThroughParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalLedDetectorStopBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetDeviceConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGazeOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalRelocalizerGetVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGenDataStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGetFakeLensSeparationDistance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalDecryptData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalUnregisterVibrationCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalRelocalizerStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalClearTrackerFts(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetHomeCalibration(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalLedDetectorCloseBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalTrackingStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalLedDetectorOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalDfuClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGenDataStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalIsGameVr2d(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetSidetone(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageGetFrameData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSlamTraceClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalLedDetectorClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetPositionResetIcon(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalIsCommonDlgVr2d(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalIsMiniAppVr2d(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGazeClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalLedDetectorSetPriorityConfigBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalDfuStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageSetConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGenDataClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGetDeviceId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetForceIcon(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalLedDetectorStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetVolume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGetSlamCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageGetAverageLuminance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalTrackingReadData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetBrightness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalDfuGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageStopBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageGetMemoryLayout(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetVibrationMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGenDataOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGetVr2dData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGazeStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalRelocalizerReadDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalMapSharedMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSlamTraceStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalLedDetectorSetIrCameraConfigBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalIsCommonDlgMiniAppVr2d(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGetMirroringMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalRegisterVibrationCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageCloseBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGetOpticalParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalPushVr2dData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSlamTraceOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index a18b12954b..35eebf6978 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -13,6 +13,105 @@ - 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 +- [sceHmd2ReprojectionClearEventGpuEnd](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSlamTraceStop](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2ReprojectionClearEventGpuStart](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalRelocalizerSetResultData](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetSlamCommand](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetBudgetMode](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalRelocalizerOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetPortStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalPowerEventInitialize](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalRelocalizerClose](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalTrackingStart](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalLedDetectorStartBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGetDeviceInformation](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageStartBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetPortConnection](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetCameraSeeThroughMode](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetFakeLensSeparationDistance](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalResetViewPosition](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2GazeGetEnabledEye](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalDfuSend](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalTrackingReadData2](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGetVrMode](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalLedDetectorOpenBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageOpenBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGetFwVersion](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageReadData](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGetTrackerFtsData](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalTrackingOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGazeReadStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalRelocalizerStart](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGetStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetPowerOff](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalCrashReportOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalTrackingClose](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageClose](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGazeStop](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalCrashReportClose](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalDfuOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageSetConfigBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetCameraSeeThroughParam](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalLedDetectorStopBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetDeviceConnection](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGazeOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalRelocalizerGetVersion](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGenDataStop](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGetFakeLensSeparationDistance](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalDecryptData](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalUnregisterVibrationCallback](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalRelocalizerStop](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalClearTrackerFts](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetHomeCalibration](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalLedDetectorCloseBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalTrackingStop](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalLedDetectorOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalDfuClose](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGenDataStart](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalIsGameVr2d](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetSidetone](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageGetFrameData](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSlamTraceClose](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalLedDetectorClose](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetPositionResetIcon](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalIsCommonDlgVr2d](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalIsMiniAppVr2d](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGazeClose](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalLedDetectorSetPriorityConfigBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalDfuStart](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageSetConfig](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGenDataClose](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGetDeviceId](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetForceIcon](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalLedDetectorStart](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetVolume](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGetSlamCommand](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageGetAverageLuminance](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalTrackingReadData](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetBrightness](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalDfuGetStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageStopBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageGetMemoryLayout](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetVibrationMode](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGenDataOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageStart](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGetVr2dData](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGazeStart](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalRelocalizerReadDataAsync](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalMapSharedMemory](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSlamTraceStart](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalLedDetectorSetIrCameraConfigBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageStop](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalIsCommonDlgMiniAppVr2d](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGetMirroringMode](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalRegisterVibrationCallback](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageCloseBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGetOpticalParam](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalPushVr2dData](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSlamTraceOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature ### Functional From aabbd10e3695e83a263d9fe4cac7179ca5aaea97 Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Mon, 19 Oct 2026 15:14:45 -0300 Subject: [PATCH 075/167] feat(libSceIme): implement sceImeFilterText Port the firmware filter for every text type, with the two codepage sets derived from the firmware converters, and declare sceImeCheckFilterText as unimplemented. --- core/libs/prx/libSceIme/CMakeLists.txt | 10 + core/libs/prx/libSceIme/Export.cpp | 10 + .../libSceIme/include/ImeFilterCodepages.hpp | 299 ++++++++++++++++++ .../prx/libSceIme/include/ImeFilterText.hpp | 177 +++++++++++ core/libs/tests/GuestImeFilterText.cpp | 106 +++++++ docs/dev/debt/libSceIme.md | 1 + 6 files changed, 603 insertions(+) create mode 100644 core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp create mode 100644 core/libs/prx/libSceIme/include/ImeFilterText.hpp create mode 100644 core/libs/tests/GuestImeFilterText.cpp diff --git a/core/libs/prx/libSceIme/CMakeLists.txt b/core/libs/prx/libSceIme/CMakeLists.txt index 5f45b0907c..460b1fed31 100644 --- a/core/libs/prx/libSceIme/CMakeLists.txt +++ b/core/libs/prx/libSceIme/CMakeLists.txt @@ -23,6 +23,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_ime_filter_text_tests ${LIBS_INCLUDE_DIR}/tests/GuestImeFilterText.cpp) + target_include_directories(guest_ime_filter_text_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ime_filter_text_tests PRIVATE libSceIme libc) + configure_windows_unwind(guest_ime_filter_text_tests) + add_test(NAME guest_ime_filter_text COMMAND guest_ime_filter_text_tests) + if(WIN32) + set_tests_properties(guest_ime_filter_text PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_ime_open_tests ${LIBS_INCLUDE_DIR}/tests/GuestImeOpen.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libSceVideoOut/src/ImeTextInput.cpp) target_include_directories(guest_ime_open_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 575463cf73..b9ab20dd06 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -11,6 +11,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceIme/include/ImeFilterText.hpp" #include "prx/libSceIme/include/ImeInput.hpp" namespace { @@ -400,6 +401,15 @@ int APS5_VABI sceImeKeyboardSetMode(int32_t user_id, uint32_t mode) { return (mode & ~ValidKeyboardModes) == 0 ? 0 : ErrorInvalidMode; } +int APS5_VABI sceImeFilterText(char16_t* dst, int32_t* out_length, uint32_t capacity, + const ime_filter::FilterParam* param, const char16_t* src, uint32_t src_length) { + return static_cast(ime_filter::FilterText(dst, out_length, capacity, param, src, src_length)); +} + +int APS5_VABI sceImeCheckFilterText(const void* param) { + NotImplemented_nid_no_patch(__func__); +} + void APS5_VABI sceImeParamInit(Param* param) { if (!param) return; std::memset(param, 0, sizeof(*param)); diff --git a/core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp b/core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp new file mode 100644 index 0000000000..4249c9b1d7 --- /dev/null +++ b/core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp @@ -0,0 +1,299 @@ +#pragma once +#include + +namespace ime_filter { + +inline constexpr std::uint64_t kCodepageABitmap[1024] = { + 0xffffffffffffffffull, 0xffffffffffffffffull, 0xffffffff00000000ull, 0xffffffffffffffffull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0xfffe03fbfffe0000ull, 0x00000000000003fbull, + 0xffffffffffff0002ull, 0x000000000002ffffull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x480d006333710000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000080200400008ull, 0x03ff03ff00000000ull, 0x00000000000f0000ull, 0x0000000000140000ull, + 0x20305fa1e406098dull, 0x00000cc300040000ull, 0x80000020000000ccull, 0x0000000000000000ull, + 0x0000000000040000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x000fffff00000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x999999393999900full, 0x0000000000000804ull, 0x300c000300000000ull, 0x000080000000c8c0ull, + 0x0000000000000060ull, 0x0000a40000000005ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x00000000b03fffefull, 0xfffffffffffffffeull, 0xfffffffe780fffffull, 0x787fffffffffffffull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0206000000000000ull, 0x0000000000000000ull, 0x000001f000000000ull, 0x0000000000000000ull, + 0x084008cc01102008ull, 0x7800000000822600ull, 0x000000027000c000ull, 0x0000000000002010ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x9b46254243f36f8bull, 0x400a0004e3e0e82cull, 0x04497977db365f65ull, 0x18c5603ae3f0ecd7ull, + 0x375180003403e60bull, 0x986982007eebe0c8ull, 0x8060e8032d56ad48ull, 0xc568c03aad93661cull, + 0x02403f7ec656aa60ull, 0x21751020146183cdull, 0x40bc300007122021ull, 0x0a3060a84562a624ull, + 0x9c84040285740217ull, 0x11e27f3414157ffbull, 0x60ff1f7522efb665ull, 0x676336c338403a70ull, + 0x0fc946b020b24dd9ull, 0xa03f86384850bc98ull, 0x5232be4998162388ull, 0xc72c00ddeba422abull, + 0x8f0a841b26e1a1e7ull, 0x89bfc241559e27ebull, 0x084d636185480014ull, 0x05cfff3eaad07f0cull, + 0x7b407a41a803ff1aull, 0x38eb050080024745ull, 0x710c9b341005dc51ull, 0xa404636601000397ull, + 0x430ac000005180d0ull, 0x5800000830c89071ull, 0x00415f80f7000ed9ull, 0x62800018941000b0ull, + 0x0156820009d00240ull, 0x05101d1008015004ull, 0x10504025001084c1ull, 0xa60d40094d8a410full, + 0x098121c0914cab19ull, 0x800006720203c485ull, 0x0009141d00080b04ull, 0x16900009905c49c9ull, + 0x2433841222200c65ull, 0x42250a0447960c03ull, 0x4f0c4900d0880028ull, 0x3e87d830d3aa14a2ull, + 0x41867ea41f618e04ull, 0x211857ad2dbbc390ull, 0x4e0411382a48241eull, 0x88400d60161b0a40ull, + 0x106082219502020aull, 0x8000144404000243ull, 0x700000000c040000ull, 0x0c00024a00c11a06ull, + 0x4045140400401a00ull, 0x052b0a78bdf30029ull, 0x8379407cbfa0bba9ull, 0xc5695bf6e91d12fdull, + 0xff022115444aeff6ull, 0x0242d033402bed63ull, 0x5dca1b4200131000ull, 0x2c61a703020000a0ull, + 0x000002848ff24880ull, 0x0048b200100d5804ull, 0x3780500420011894ull, 0x68be49ea684d3200ull, + 0x21c9a8202e42184cull, 0xff7c001e80b050b9ull, 0x01e028c114e0849aull, 0xdddb130fac49870eull, + 0x51b2a2e289fbbe1aull, 0x928b3ec632ca5522ull, 0x32986703438f1dbfull, 0xa923081173c03028ull, + 0x04028fe33a65c000ull, 0x00a1bf3da6252c4eull, 0x317c06c98cd43e3aull, 0x0edf018bd52a00e0ull, + 0xf09111838c22e34bull, 0x40fbc9aca7287d94ull, 0x44445a9007534484ull, 0xf5d4004800013fc8ull, + 0x891dc442ec5f7701ull, 0xd242410949286b83ull, 0x3a22184059fe061dull, 0xc0eaf0033b9fb7e4ull, + 0xe400898082021386ull, 0x0cc44b8010a1b200ull, 0x48341faf8944d309ull, 0x0470420a0c458259ull, + 0x4450314010c8a040ull, 0x0540828101004004ull, 0x1a056a30642c0108ull, 0x645690cf051460a6ull, + 0xcbf09c1831000021ull, 0x01b5104c63e2e120ull, 0x3281b8b29a83538cull, 0x0c0233e70a84987aull, + 0x9872e1b1d038d6cdull, 0x0459c3f4e2848a1eull, 0xd314484523c2439aull, 0xffbd024136400292ull, + 0xa5d27dc0e8f0eb09ull, 0xd0afa47fd24bc242ull, 0x0bd8824734a11aa0ull, 0xc83ad294651bc453ull, + 0x33140e0640c8001eull, 0xc0d00088b21f615full, 0x166ba1c5a898a02aull, 0x0604c08b85b4af50ull, + 0xa251056e1e04f933ull, 0x73b8ed0776380400ull, 0xc816408119324406ull, 0xaa04298463097c8aull, + 0x27614e0eca9c1c24ull, 0xc10c0846830009d0ull, 0x0908540d10816011ull, 0x0c000514cc0a000eull, + 0x6784008ba0440430ull, 0x8b18865e8a195288ull, 0x9cbe8c1041602e59ull, 0x00089800895c6861ull, + 0xc1900018089a8100ull, 0x640d8505f4a14007ull, 0xff0a48060e4d314eull, 0x000b852e2ea81632ull, + 0x696c0e20ca841810ull, 0x0390d65816000032ull, 0x112490001a6851a0ull, 0x1fae5d52432698e1ull, + 0x5700fafbae280fa0ull, 0xc044c88099406408ull, 0xa4c48424b1419005ull, 0xc1949000603a1a34ull, + 0xc106180d003a8246ull, 0x1511e05099100022ull, 0x022a041a00824157ull, 0x446ad8138930004full, + 0x400511c0ed228aa2ull, 0x3101880801021000ull, 0x0f08f80002044620ull, 0x22020000a2008900ull, + 0x1040004216108210ull, 0x200052f4126052c0ull, 0x4202110082308510ull, 0xda2070e180b5430aull, + 0xfc65350008012040ull, 0x62140286ab0419c1ull, 0x4246908500440087ull, 0x338032070a85405cull, + 0xc0d0ce30b8c00400ull, 0x0da505080080c030ull, 0x280c020000400a90ull, 0x4122642940446705ull, + 0x847c4664000002e8ull, 0x4049861dde200002ull, 0x20010084c0000a08ull, 0x01c742cd10108400ull, + 0x1d8f9968d52a703aull, 0x81d9aef53e12be50ull, 0x732e08282412cec4ull, 0xd41d020c4b3424acull, + 0x0811009780002a02ull, 0x7d451786114411c4ull, 0x879140405e4949ddull, 0x491444bad8c4254cull, + 0x15800271c8001b92ull, 0xc200096a0c0000c1ull, 0xba49302140024800ull, 0x1008e2ac1c802080ull, + 0x841400e300341004ull, 0x1414981020004020ull, 0x5420868804aa70c2ull, 0x2010918004130c62ull, + 0x54011c4002064082ull, 0x84802125e4e90383ull, 0xe60944c02810e433ull, 0x080112da81260a03ull, + 0xf886400197906901ull, 0xa6510a0e0081e24dull, 0x8441c60081ec011aull, 0x8741acefb62eadb8ull, + 0x026811614b028d54ull, 0x043350a02057bb60ull, 0x01122402f7b4a8c0ull, 0x00c8227120009ad3ull, + 0xe1800c8a809e2081ull, 0x402810318151b009ull, 0x620e69b689a52a0eull, 0x4d548085d1444425ull, + 0x862dd8071fb12c75ull, 0x226e414e5841d97cull, 0xedb7f80d9e088200ull, 0x0814931375668c80ull, + 0x6ea6484ec8040e32ull, 0xba0126c066742c4aull, 0x00000000185dd70cull, 0x0000000000000000ull, + 0x0540000000000000ull, 0x03a54f81813370a0ull, 0x2344c31a641055ecull, 0x1a090a4300341462ull, + 0xa848010213a5187bull, 0xe2dd8106c5440440ull, 0x0416b6262d481af0ull, 0x311280326e405058ull, + 0x420a82080c0007e4ull, 0x87134860803b4840ull, 0xe52903193428850dull, 0x5c1825a9870a2345ull, + 0x03e85e00d9c577a6ull, 0x41c6cd54a7000081ull, 0x2b0ab860a2042800ull, 0x0e1a08eada9e0020ull, + 0x0376890811c0427eull, 0x98a8000401058621ull, 0x20220d05c44846a0ull, 0x28d78a01914854a2ull, + 0x3122160500087898ull, 0x06a2fa4e08804340ull, 0x9b14200292110814ull, 0x9010500016432e52ull, + 0x2020304285ba0041ull, 0x40802f0807a84f0bull, 0x0601df501a930591ull, 0x4e8006303021a202ull, + 0x8001a00404c80cc4ull, 0x0a020880d4316000ull, 0x00418e1800281c00ull, 0x4b00f210ca106ad0ull, + 0x889002201506274dull, 0x8150454982a85a00ull, 0x2c08880480002004ull, 0x4ac48001000508d1ull, + 0x0a42008e0062e0a0ull, 0xe0a5090e6a8c3055ull, 0x80b3481442c42906ull, 0x733c0102b330803eull, + 0x09400c20700d1494ull, 0xc094a451c040301aull, 0xa40c96c205c88dcaull, 0x011000c834040001ull, + 0x1cda2428a9cd550dull, 0x120f7a4d48370142ull, 0xd20531fb452a32b4ull, 0x45ca68d7dc44b894ull, + 0x420819432ed15097ull, 0xa09798409d48d202ull, 0x00000000064d5409ull, 0x0000000000000000ull, + 0x8480000000000000ull, 0x17001c0604215542ull, 0xb9ddff8761107624ull, 0x3c11245d5c0a659full, + 0x00000000005dadb0ull, 0x00db28d000000000ull, 0x4408010802000422ull, 0x90288d0aac409804ull, + 0x00310400e0018700ull, 0x1054001982211794ull, 0x40039c02021a2cb2ull, 0x7900080c8804bd60ull, + 0xcb088640ba3c1628ull, 0x0000001e90807274ull, 0x9c87e188d8000000ull, 0x2791ae6404124034ull, + 0x5366408fe6fbe86bull, 0xb5e4e3ab537feea6ull, 0x012285480002869full, 0x20a0211648004402ull, + 0x0005208002240004ull, 0x01ac162c01547e00ull, 0x05308c1410852a84ull, 0x906060fafdc3fbc3ull, + 0x9690120040336440ull, 0x418200d44e834b31ull, 0x028020801d6a0129ull, 0x9f0c269102ad8000ull, + 0x0c24d96f67018044ull, 0x5021500118d02910ull, 0x0201709004d01000ull, 0x0100013261c30148ull, + 0x0562080207190088ull, 0xf0a104054c0e0132ull, 0x0000000000000002ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0080000000000000ull, 0x5a0421bd035e8e8dull, 0x0000002611703488ull, + 0x8804c50210000000ull, 0x25ed147cf801b815ull, 0x1bd785893bb0ed60ull, 0x0ac50d0c1a627af3ull, + 0x6b0d0490524ae5d1ull, 0x16122b575266a35cull, 0x001829491101a872ull, 0x886c600010080948ull, + 0x39903012058f916eull, 0x001b88a049b0f840ull, 0x0042850000000000ull, 0x7014ea0498000058ull, + 0x60005193611d1628ull, 0x0000000000a71a24ull, 0x1018712043c00000ull, 0x89066004a9270172ull, + 0x40810900020cc022ull, 0x00000e348ca0602dull, 0x1101210000000000ull, 0x0892ec4cd31a8011ull, + 0x1806c7ac85000040ull, 0x003480000512e03eull, 0x0a126d0180cec008ull, 0x0027011e08568641ull, + 0x4e05e032083d3751ull, 0x01400081048401c0ull, 0x0000000000000000ull, 0x00591aa000000000ull, + 0xc8001d48882443c8ull, 0x0405981372030152ull, 0x0d148a1004008280ull, 0x2704a04002088056ull, + 0x000000004e000000ull, 0xa320000000000000ull, 0xdf002660a0ae1902ull, 0x3ad081217b17f010ull, + 0x4800100300284180ull, 0x00c414cf8014cc00ull, 0x0000000130202000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000020000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000010000000ull, + 0x00003fffffffc000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0xfffffffffffffffeull, 0xfffffffe7fffffffull, 0x00000000ffffffffull, 0x0000003f00000000ull, +}; + +struct CodepageRange { + std::uint16_t first; + std::uint16_t last; +}; + +inline constexpr CodepageRange kCodepageBRanges[151] = { + {0x0000, 0x00ff}, {0x0101, 0x0101}, {0x0113, 0x0113}, {0x011b, 0x011b}, {0x012b, 0x012b}, {0x0144, 0x0144}, + {0x0148, 0x0148}, {0x014d, 0x014d}, {0x016b, 0x016b}, {0x01ce, 0x01ce}, {0x01d0, 0x01d0}, {0x01d2, 0x01d2}, + {0x01d4, 0x01d4}, {0x01d6, 0x01d6}, {0x01d8, 0x01d8}, {0x01da, 0x01da}, {0x01dc, 0x01dc}, {0x01f9, 0x01f9}, + {0x0251, 0x0251}, {0x0261, 0x0261}, {0x02c7, 0x02c7}, {0x02c9, 0x02cb}, {0x02d9, 0x02d9}, {0x0391, 0x03a1}, + {0x03a3, 0x03a9}, {0x03b1, 0x03c1}, {0x03c3, 0x03c9}, {0x0401, 0x0401}, {0x0410, 0x044f}, {0x0451, 0x0451}, + {0x2010, 0x2010}, {0x2013, 0x2016}, {0x2018, 0x2019}, {0x201c, 0x201d}, {0x2025, 0x2026}, {0x2030, 0x2030}, + {0x2032, 0x2033}, {0x2035, 0x2035}, {0x203b, 0x203b}, {0x20ac, 0x20ac}, {0x2103, 0x2103}, {0x2105, 0x2105}, + {0x2109, 0x2109}, {0x2116, 0x2116}, {0x2121, 0x2121}, {0x2160, 0x216b}, {0x2170, 0x2179}, {0x2190, 0x2193}, + {0x2196, 0x2199}, {0x2208, 0x2208}, {0x220f, 0x220f}, {0x2211, 0x2211}, {0x2215, 0x2215}, {0x221a, 0x221a}, + {0x221d, 0x2220}, {0x2223, 0x2223}, {0x2225, 0x2225}, {0x2227, 0x222b}, {0x222e, 0x222e}, {0x2234, 0x2237}, + {0x223d, 0x223d}, {0x2248, 0x2248}, {0x224c, 0x224c}, {0x2252, 0x2252}, {0x2260, 0x2261}, {0x2264, 0x2267}, + {0x226e, 0x226f}, {0x2295, 0x2295}, {0x2299, 0x2299}, {0x22a5, 0x22a5}, {0x22bf, 0x22bf}, {0x2312, 0x2312}, + {0x2460, 0x2469}, {0x2474, 0x249b}, {0x2500, 0x254b}, {0x2550, 0x2573}, {0x2581, 0x258f}, {0x2593, 0x2595}, + {0x25a0, 0x25a1}, {0x25b2, 0x25b3}, {0x25bc, 0x25bd}, {0x25c6, 0x25c7}, {0x25cb, 0x25cb}, {0x25ce, 0x25cf}, + {0x25e2, 0x25e5}, {0x2605, 0x2606}, {0x2609, 0x2609}, {0x2640, 0x2640}, {0x2642, 0x2642}, {0x2e84, 0x2e84}, + {0x2e88, 0x2e88}, {0x2e8b, 0x2e8c}, {0x2e97, 0x2e97}, {0x2ea7, 0x2ea7}, {0x2eaa, 0x2eaa}, {0x2eb6, 0x2eb7}, + {0x2ebb, 0x2ebb}, {0x2ff0, 0x2ffb}, {0x3000, 0x3003}, {0x3005, 0x3017}, {0x301d, 0x301e}, {0x3021, 0x3029}, + {0x303e, 0x303e}, {0x3041, 0x3093}, {0x309b, 0x309e}, {0x30a1, 0x30f6}, {0x30fc, 0x30fe}, {0x3105, 0x3129}, + {0x3220, 0x3229}, {0x3231, 0x3231}, {0x32a3, 0x32a3}, {0x338e, 0x338f}, {0x339c, 0x339e}, {0x33a1, 0x33a1}, + {0x33c4, 0x33c4}, {0x33ce, 0x33ce}, {0x33d1, 0x33d2}, {0x33d5, 0x33d5}, {0x3400, 0x3917}, {0x3919, 0x39ce}, + {0x39d0, 0x39de}, {0x39e0, 0x415e}, {0x4160, 0x4336}, {0x4338, 0x4728}, {0x472a, 0x477b}, {0x477d, 0x478c}, + {0x478e, 0x4c9e}, {0x4ca1, 0x4ca2}, {0x4ca4, 0x4db5}, {0x4e00, 0x9fa5}, {0xe766, 0xe864}, {0xf92c, 0xf92c}, + {0xf979, 0xf979}, {0xf995, 0xf995}, {0xf9e7, 0xf9e7}, {0xf9f1, 0xf9f1}, {0xfa0c, 0xfa0f}, {0xfa11, 0xfa11}, + {0xfa13, 0xfa14}, {0xfa18, 0xfa18}, {0xfa1f, 0xfa21}, {0xfa23, 0xfa24}, {0xfa27, 0xfa29}, {0xfe30, 0xfe31}, + {0xfe33, 0xfe44}, {0xfe49, 0xfe52}, {0xfe54, 0xfe57}, {0xfe59, 0xfe66}, {0xfe68, 0xfe6b}, {0xff01, 0xff5e}, + {0xffe0, 0xffe5}, +}; + +} diff --git a/core/libs/prx/libSceIme/include/ImeFilterText.hpp b/core/libs/prx/libSceIme/include/ImeFilterText.hpp new file mode 100644 index 0000000000..6c465fa07e --- /dev/null +++ b/core/libs/prx/libSceIme/include/ImeFilterText.hpp @@ -0,0 +1,177 @@ +#pragma once + +#include +#include + +#include "ImeFilterCodepages.hpp" + +namespace ime_filter { + +struct FilterParam { + std::uint64_t mask; + std::uint32_t type; + std::uint32_t version; + std::uint32_t variant; +}; + +static_assert(sizeof(FilterParam) == 24); + +constexpr std::uint32_t kErrorInvalidParam = 0x80bc0030u; +constexpr std::uint32_t kErrorInvalidAddress = 0x80bc0031u; + +constexpr std::uint64_t kClassCyrillic = 0x200000400ull; +constexpr std::uint64_t kClassVietnamese = 0x40000000ull; +constexpr std::uint64_t kClassRomanian = 0x80000000ull; +constexpr std::uint64_t kClassThai = 0x4000000ull; + +constexpr std::uint64_t kAbsentMasks[8] = {0x188000000ull, 0x40000000ull, 0x10000000ull, 0x1000000ull, + 0x2000ull, 0x18000ull, 0x6000ull, 0x4000ull}; + +constexpr char16_t kPunctuationLatin[] = { + 0xa1, 0xbf, 0xb4, 0x2018, 0x2019, 0x201a, 0x201c, 0x201d, 0x201e, 0xb0, 0x2039, 0x203a, + 0xab, 0xbb, 0xaa, 0xba, 0xd7, 0xf7, 0xa4, 0xa2, 0x20ac, 0xa3, 0xa5, 0x20a9, + 0xa7, 0xa6, 0xb5, 0xac, 0xb7, 0xb9, 0xb2, 0xb3, 0xbc, 0xbd, 0xbe, 0x2116}; + +constexpr char16_t kPunctuationFullwidth[] = { + 0x3001, 0x3002, 0x30fc, 0x30fb, 0x300c, 0x300d, 0xffe5, 0xff01, 0x201d, 0xff03, 0xff04, 0xff05, 0xff06, + 0x2019, 0xff08, 0xff09, 0xff0a, 0xff0b, 0xff0c, 0xff0d, 0xff0e, 0xff0f, 0xff1a, 0xff1b, 0xff1c, 0xff1d, + 0xff1e, 0xff1f, 0xff20, 0xff3b, 0xff3c, 0xff3d, 0xff3e, 0xff3f, 0xff40, 0xff5b, 0xff5c, 0xff5d, 0xff5e}; + +constexpr char16_t kPunctuationFullwidthNoProlong[] = { + 0x3001, 0x3002, 0x300c, 0x300d, 0xff01, 0x201d, 0xff03, 0xff04, 0xff05, 0xff06, 0x2019, 0xff08, + 0xff09, 0xff0a, 0xff0b, 0xff0c, 0xff0d, 0xff0e, 0xff0f, 0xff1a, 0xff1b, 0xff1c, 0xff1d, 0xff1e, + 0xff1f, 0xff20, 0xff3b, 0xff3c, 0xff3d, 0xff3e, 0xff3f, 0xff40, 0xff5b, 0xff5c, 0xff5d, 0xff5e}; + +constexpr char16_t kVietnameseLetters[] = {0x1a0, 0x1a1, 0x1af, 0x1b0, 0x20ab}; +constexpr char16_t kModifierLetters[] = {0x2c7, 0x2d8, 0x2d9, 0x2db, 0x2dd}; +constexpr char16_t kNumberSeparators[] = {0x2c, 0x2e, 0x2d, 0x0a}; + +template +constexpr bool In(const char16_t (&set)[N], std::uint32_t c) { + for (char16_t v : set) { + if (v == c) return true; + } + return false; +} + +constexpr bool InCodepageA(std::uint32_t c) { + return ((kCodepageABitmap[c >> 6] >> (c & 63)) & 1u) != 0; +} + +constexpr bool InCodepageB(std::uint32_t c) { + constexpr std::size_t count = sizeof(kCodepageBRanges) / sizeof(kCodepageBRanges[0]); + std::size_t low = 0; + std::size_t high = count; + while (low < high) { + const std::size_t middle = (low + high) / 2; + if (c > kCodepageBRanges[middle].last) { + low = middle + 1; + } else { + high = middle; + } + } + return low < count && c >= kCodepageBRanges[low].first; +} + +constexpr bool IsVietnameseExtension(std::uint32_t c) { + return static_cast(c - 0x300) < 0x24 || (c & 0xff00) == 0x1e00 || In(kVietnameseLetters, c); +} + +constexpr std::uint64_t DerivedMask(std::uint32_t version, std::uint32_t variant) { + const std::uint32_t second = variant == 1 ? 0x10000000u : 0x6000000u; + const std::uint64_t flag = (variant == 0 || version >= 0x2000000u) ? (1u << 24) : 0; + std::uint64_t low = flag + 0x1fffffu; + const std::uint64_t high = flag | 0x21fffffu; + if (version >= 0x2500000u || variant == 0) low = high; + const std::uint64_t wide = 0x200000000ull + low - 0x4000000ull; + if (version >= 0x5000000u || variant == 0) low = wide; + return version < second ? low : (low | 0x200000000ull); +} + +constexpr bool ValidMask(std::uint64_t mask, std::uint32_t version, std::uint32_t variant, std::uint32_t* limit) { + const bool none = variant == 0; + const bool wide = none || version >= 0x5000000u; + const std::uint64_t shifted = (none || version >= 0x2500000u ? 1u : 0u) << 25; + const std::uint64_t first = wide ? 0x200000000ull + shifted - 0x17e1e401ull : (shifted | 0x1e1bffull); + if ((mask & first) != 0) { + *limit = 0x17f; + return true; + } + *limit = 0x7f; + const std::uint32_t second = variant == 1 ? 0x10000000u : 0x6000000u; + const std::uint64_t flag = static_cast(none || version >= 0x2000000u ? 1u : 0u) << 24; + const std::uint64_t base = (wide ? 0x1401e400ull : 0x1e400ull) | flag; + const std::uint64_t allowed = version < second ? base : (base | 0x200000000ull); + return (mask & allowed) != 0; +} + +constexpr bool HighBitSet(std::uint64_t mask) { + return (mask & 0x80000000ull) != 0; +} + +constexpr bool KeepClassified(std::uint32_t c, std::uint64_t mask, std::uint32_t limit, const bool (&absent)[8]) { + if (c < limit) return true; + if (In(kPunctuationLatin, c)) return true; + if (!absent[7] && (static_cast(c - 0x3130) < 0x60 || static_cast(c + 0x5400) < 0x2bb0)) { + return true; + } + if ((kClassCyrillic & mask) != 0 && (c & 0xff00) == 0x400) return true; + if (!absent[6] && (static_cast(c + 0xf0) <= 9 || In(kPunctuationFullwidth, c))) return true; + if (!absent[5] && (static_cast(c + 0xf0) <= 9 || In(kPunctuationFullwidthNoProlong, c))) return true; + if (!absent[4] && InCodepageA(c)) return true; + if (!absent[5] && InCodepageB(c)) return true; + if (!absent[3] && ((c & 0xff00) == 0x600 || (c & 0xfffc) == 0x200c)) return true; + if ((mask & kClassThai) != 0 && (c & 0xff80) == 0xe00) return true; + if (!absent[2] && (static_cast(c - 0x370) < 0x72 || (c & 0xfff0) == 0x3f0)) return true; + if (!absent[1] && IsVietnameseExtension(c)) return true; + if (HighBitSet(mask) && (c & 0xfffc) == 0x218) return true; + if (!absent[0] && In(kModifierLetters, c)) return true; + return false; +} + +inline std::uint32_t FilterText(char16_t* dst, std::int32_t* out_length, std::uint32_t capacity, + const FilterParam* param, const char16_t* src, std::uint32_t src_length) { + if (dst == nullptr || out_length == nullptr || param == nullptr || src == nullptr) return kErrorInvalidAddress; + if (capacity < 2) return kErrorInvalidParam; + const std::uint32_t type = param->type; + if (type >= 5 && type - 0x100 > 3) return kErrorInvalidParam; + + std::uint64_t mask = param->mask; + if (mask == 0) mask = DerivedMask(param->version, param->variant); + std::uint32_t limit = 0; + if (!ValidMask(mask, param->version, param->variant, &limit)) return kErrorInvalidParam; + + bool absent[8]; + for (int i = 0; i < 8; ++i) absent[i] = (mask & kAbsentMasks[i]) == 0; + + std::uint32_t room = capacity - 1; + std::uint32_t count = 0; + for (std::uint64_t i = 0; i < src_length && room != 0; ++i) { + const std::uint32_t c = src[i]; + if (c == 0) break; + bool keep; + if (type == 4) { + keep = static_cast(c - 0x30) <= 9 || In(kNumberSeparators, c); + } else if (type == 1 || type == 3 || type == 0x102) { + keep = c <= 0x7e; + } else if (type == 0x100) { + keep = c < 0x17f; + if (!keep && (mask & kClassVietnamese) != 0) { + const bool listed = static_cast(c - 0x300) >= 0x24 && (c & 0xff00) != 0x1e00; + keep = !listed || In(kVietnameseLetters, c); + } + if (!keep && HighBitSet(mask)) keep = (c & 0xfffc) == 0x218; + } else { + keep = KeepClassified(c, mask, limit, absent); + } + if (keep) { + dst[count++] = static_cast(c); + --room; + } + } + dst[count] = 0; + *out_length = static_cast(count); + return 0; +} + +} diff --git a/core/libs/tests/GuestImeFilterText.cpp b/core/libs/tests/GuestImeFilterText.cpp new file mode 100644 index 0000000000..4bc3daf1b9 --- /dev/null +++ b/core/libs/tests/GuestImeFilterText.cpp @@ -0,0 +1,106 @@ +#include "SceTypes.hpp" +#include "prx/libSceIme/include/ImeFilterText.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceImeFilterText(char16_t* dst, int32_t* out_length, uint32_t capacity, + const ime_filter::FilterParam* param, const char16_t* src, uint32_t src_length); +} + +namespace { + +constexpr int kInvalidParam = static_cast(0x80bc0030u); +constexpr int kInvalidAddress = static_cast(0x80bc0031u); + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +struct Result { + int code; + std::vector text; +}; + +Result Run(uint64_t mask, uint32_t type, uint32_t capacity, std::initializer_list input, + uint32_t length = UINT32_MAX) { + std::vector src(input); + src.push_back(0); + if (length == UINT32_MAX) length = static_cast(input.size()); + std::vector dst(capacity + 4, 0xcccc); + int32_t count = -1; + const ime_filter::FilterParam param{mask, type, 0, 0}; + const int code = sceImeFilterText(dst.data(), &count, capacity, ¶m, src.data(), length); + Result result{code, {}}; + if (code == 0) { + Require(count >= 0 && static_cast(count) < capacity, "length must leave room for the terminator"); + Require(dst[static_cast(count)] == 0, "output must be terminated"); + result.text.assign(dst.begin(), dst.begin() + count); + } + return result; +} + +void Expect(const Result& result, std::initializer_list expected, const char* message) { + Require(result.code == 0, message); + Require(result.text == std::vector(expected), message); +} + +void CheckArguments() { + char16_t dst[4]; + int32_t count = 0; + const char16_t src[2] = {u'a', 0}; + const ime_filter::FilterParam param{0, 1, 0, 0}; + Require(sceImeFilterText(nullptr, &count, 4, ¶m, src, 1) == kInvalidAddress, "null destination"); + Require(sceImeFilterText(dst, nullptr, 4, ¶m, src, 1) == kInvalidAddress, "null length"); + Require(sceImeFilterText(dst, &count, 4, nullptr, src, 1) == kInvalidAddress, "null parameter"); + Require(sceImeFilterText(dst, &count, 4, ¶m, nullptr, 1) == kInvalidAddress, "null source"); + Require(Run(0, 1, 1, {u'a'}).code == kInvalidParam, "capacity below 2"); + Require(Run(0, 5, 16, {u'a'}).code == kInvalidParam, "type 5"); + Require(Run(0, 0xff, 16, {u'a'}).code == kInvalidParam, "type 0xff"); + Require(Run(0, 0x104, 16, {u'a'}).code == kInvalidParam, "type 0x104"); +} + +void CheckSimpleTypes() { + Expect(Run(0, 1, 16, {0x41, 0x7f, 0x42, 0xe9, 0x20ac, 0x7e}), {0x41, 0x42, 0x7e}, "ascii type"); + Expect(Run(0, 3, 16, {0x41, 0x7f, 0x42}), {0x41, 0x42}, "type 3"); + Expect(Run(0, 0x102, 16, {0x41, 0xe9, 0x7e}), {0x41, 0x7e}, "type 0x102"); + Expect(Run(0, 4, 16, {0x31, 0x2c, 0x41, 0x2e, 0x2d, 0x0a, 0xff11}), {0x31, 0x2c, 0x2e, 0x2d, 0x0a}, "number type"); +} + +void CheckLatinType() { + Expect(Run(0, 0x100, 16, {0x41, 0x17e, 0x17f, 0x1a0, 0x218}), {0x41, 0x17e, 0x1a0, 0x218}, "latin type"); + Expect(Run(0x4000001f, 0x100, 16, {0x41, 0x17f, 0x1a0, 0x1af, 0x20ab, 0x300, 0x323, 0x1ea0, 0x218}), + {0x41, 0x1a0, 0x1af, 0x20ab, 0x300, 0x323, 0x1ea0}, "vietnamese latin"); + Expect(Run(0x8000001f, 0x100, 16, {0x41, 0x218, 0x219, 0x21a, 0x21b, 0x21c}), + {0x41, 0x218, 0x219, 0x21a, 0x21b}, "romanian latin"); +} + +void CheckClassifiedType() { + Expect(Run(0, 0, 32, {0x31, 0x2c, 0x42, 0x39, 0xe9, 0x3b1, 0x430, 0x4e2d, 0x3042, 0xe01, 0x627, 0xac00}), + {0x31, 0x2c, 0x42, 0x39, 0xe9, 0x3b1, 0x430, 0x4e2d, 0x3042, 0xe01, 0x627, 0xac00}, "default classes"); + Expect(Run(0x400001f, 0, 16, {0x41, 0xe01, 0xe5b, 0xe80, 0x627}), {0x41, 0xe01, 0xe5b}, "thai class"); +} + +void CheckBounds() { + Expect(Run(0, 1, 4, {0x41, 0x42, 0x43, 0x44, 0x45}), {0x41, 0x42, 0x43}, "capacity limit"); + Expect(Run(0, 1, 16, {0x41, 0, 0x42}), {0x41}, "stops at terminator"); + Expect(Run(0, 1, 16, {0x41, 0x42, 0x43}, 2), {0x41, 0x42}, "source length limit"); + Expect(Run(0, 1, 16, {0x41, 0x42, 0x43}, 0), {}, "empty source"); +} + +} + +int main() { + CheckArguments(); + CheckSimpleTypes(); + CheckLatinType(); + CheckClassifiedType(); + CheckBounds(); + return 0; +} diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 76178899b3..172ac8123f 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -8,6 +8,7 @@ - [sceImeGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills. While a panel is open it reports the type, position and alignments passed to `sceImeOpen` and the size `sceImeGetPanelSize` gives; the console's values for a panel moved by its own position adjustment are unknown. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title - [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. A text filter or an external keyboard filter throws, as their contract is unknown. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn +- [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 (`sceImeFilterText`, 2272 bytes) and is differential-tested against that function run in an x86-64 emulator; it is not measured on a console. The two codepage sets it tests (the helpers at 0x4850 and 0x48f0, which ask two ICU converters compiled into the library whether a UTF-16 unit converts) were derived by running those helpers over all 65536 units and are stored in [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp); which character encodings they are is not established. The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` are the two `u32` values from which the mask is derived when it is 0 and which select the accepted classes; their meaning is unknown. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown ### Functional From 2e0d8d7ab52ecbba54daa071c896054a0e219e30 Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Mon, 19 Oct 2026 15:15:25 -0300 Subject: [PATCH 076/167] docs(libSceIme): shorten the sceImeFilterText debt entry --- docs/dev/debt/libSceIme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 172ac8123f..2262faf807 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -8,7 +8,7 @@ - [sceImeGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills. While a panel is open it reports the type, position and alignments passed to `sceImeOpen` and the size `sceImeGetPanelSize` gives; the console's values for a panel moved by its own position adjustment are unknown. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title - [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. A text filter or an external keyboard filter throws, as their contract is unknown. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn -- [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 (`sceImeFilterText`, 2272 bytes) and is differential-tested against that function run in an x86-64 emulator; it is not measured on a console. The two codepage sets it tests (the helpers at 0x4850 and 0x48f0, which ask two ICU converters compiled into the library whether a UTF-16 unit converts) were derived by running those helpers over all 65536 units and are stored in [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp); which character encodings they are is not established. The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` are the two `u32` values from which the mask is derived when it is 0 and which select the accepted classes; their meaning is unknown. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown +- [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 (`sceImeFilterText`, 2272 bytes) and is differential-tested against that function in an emulator; it is not measured on a console. The two codepage sets it tests (helpers at 0x4850 and 0x48f0, which ask two compiled-in converters whether a UTF-16 unit converts) were derived by running those helpers over all 65536 units and are stored in [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp); the encodings are not identified. The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` are the two `u32` values from which the mask is derived when it is 0 and which select the accepted classes. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown ### Functional From 3eee2b281faa95b4e409d7c13e819251e74f76b2 Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Mon, 19 Oct 2026 15:17:06 -0300 Subject: [PATCH 077/167] fix(libSceAudioOut): leave sceAudioOutGetSystemState to #5965 --- core/libs/prx/libSceAudioOut/src/Unimplemented.cpp | 5 ----- docs/dev/debt/libSceAudioOut.md | 1 - 2 files changed, 6 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp index 99e026d61e..78fd125308 100644 --- a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp +++ b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp @@ -60,11 +60,6 @@ int APS5_VABI sceAudioOutSetSystemDebugState(void) { return 0; } -int APS5_VABI sceAudioOutGetSystemState(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2LoPortGetState(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index ec058e7f31..4ce0e79bfb 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -35,7 +35,6 @@ - [sceAudioOut2LoPortSetAttributesEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoPortGetAttributes](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutSetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOutGetSystemState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoPortGetState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutGetHandleStatusInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutGetSimulatedHandleStatusInfo2](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature From f8545e961e8583c8f74ea0ac6e910f012f5f27a9 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Sun, 18 Oct 2026 08:58:50 -0300 Subject: [PATCH 078/167] fix(tests): compare the Bessel exports with fixed reference values libc++ does not provide std::cyl_bessel_j or std::cyl_neumann, so GuestMath.cpp stopped compiling on macOS after the Bessel exports were added. The three checks now compare against the values of J0(2.5), J3(4.0) and Y2(3.0), which glibc and macOS libm agree on to 1e-16. --- core/libs/tests/GuestMath.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 98eb2d3d10..00bdc28b09 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -875,11 +875,11 @@ static void CheckLongDoubleFunctions() { Require(hypot3_nid_postfix(0x1p1000, 0x1p1000, 0x1p1000) == std::hypot(0x1p1000, std::hypot(0x1p1000, 0x1p1000))); Require(j0f_nid_postfix(0.0f) == 1.0f && j1f_nid_postfix(0.0f) == 0.0f && jnf_nid_postfix(2, 0.0f) == 0.0f); - Require(std::fabs(j0f_nid_postfix(2.5f) - static_cast(std::cyl_bessel_j(0.0, 2.5))) < 1e-6f); - Require(std::fabs(jnf_nid_postfix(3, 4.0f) - static_cast(std::cyl_bessel_j(3.0, 4.0))) < 1e-6f); + Require(std::fabs(j0f_nid_postfix(2.5f) - static_cast(-0.048383776468198)) < 1e-6f); + Require(std::fabs(jnf_nid_postfix(3, 4.0f) - static_cast(0.430171473875622)) < 1e-6f); Require(y0f_nid_postfix(0.0f) == -std::numeric_limits::infinity() && y1f_nid_postfix(0.0f) == -std::numeric_limits::infinity()); Require(ynf_nid_postfix(2, 0.0f) == -std::numeric_limits::infinity()); - Require(std::fabs(ynf_nid_postfix(2, 3.0f) - static_cast(std::cyl_neumann(2.0, 3.0))) < 1e-6f); + Require(std::fabs(ynf_nid_postfix(2, 3.0f) - static_cast(-0.160400393484924)) < 1e-6f); Require(std::isnan(y0f_nid_postfix(-1.0f))); } 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 079/167] 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 bf608ca8bedc8c769b18d9358f6d538cbd63690b Mon Sep 17 00:00:00 2001 From: zaryabzip <110787259+zaryabzip@users.noreply.github.com> Date: Fri, 16 Oct 2026 02:47:07 +0500 Subject: [PATCH 080/167] fix(agc): reserve the shader preparation thread stacks on Windows instead of committing them The registration preparation and library prebuild pools ask for 64 MiB stacks through pthread_attr_setstacksize. With winpthreads that size reaches _beginthreadex as the initial commit, so every pool thread commits its whole 64 MiB up front: 7 threads and 448 MiB on a 12-thread job, 9 threads and 576 MiB on the 16-thread i7-11800H, before any shader is prepared. On Windows the pool threads are now created with STACK_SIZE_PARAM_IS_A_RESERVATION, which keeps the 64 MiB reservation and commits pages as the stack grows. The pool code is host code built with stack probes, so on-demand growth is safe there. Guest threads are unchanged. agc_driver_registration_prepare_stack measures a pool thread's stack from inside a task: at least 64 MiB reserved everywhere, and at most 8 MiB committed on Windows. On main it fails with "registration preparation thread commits 64 MiB of its 64 MiB stack up front". --- .../src/Driver/Shaders/ShaderRegistry.cpp | 28 ++++- .../execution/RegistrationPrepareStack.cpp | 104 ++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/RegistrationPrepareStack.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index f8214382bf..3d92e38173 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -32,6 +32,13 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#endif #ifdef __GLIBC__ #include #endif @@ -1204,10 +1211,21 @@ public: } private: + static constexpr std::size_t StackBytes = 64u << 20u; + explicit RegistrationPreparePool(unsigned count) { +#ifdef _WIN32 + for (unsigned index = 0; index < count; ++index) { + const auto thread = _beginthreadex(nullptr, static_cast(StackBytes), &RegistrationPreparePool::WindowsMain, this, STACK_SIZE_PARAM_IS_A_RESERVATION, nullptr); + if (thread != 0) { + CloseHandle(reinterpret_cast(thread)); + ++threads; + } + } +#else pthread_attr_t attributes; pthread_attr_init(&attributes); - pthread_attr_setstacksize(&attributes, 64u << 20u); + pthread_attr_setstacksize(&attributes, StackBytes); for (unsigned index = 0; index < count; ++index) { pthread_t thread; if (pthread_create(&thread, &attributes, &RegistrationPreparePool::Main, this) == 0) { @@ -1217,8 +1235,16 @@ private: } } pthread_attr_destroy(&attributes); +#endif } +#ifdef _WIN32 + static unsigned __stdcall WindowsMain(void* argument) { + Main(argument); + return 0; + } +#endif + static void* Main(void* argument) { auto& pool = *static_cast(argument); if (PerformanceTrace::Configured()) PerformanceTrace::BindThread("shader prepare", TraceThread::DefaultCapacity / 8); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RegistrationPrepareStack.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RegistrationPrepareStack.cpp new file mode 100644 index 0000000000..c28a98d697 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RegistrationPrepareStack.cpp @@ -0,0 +1,104 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif + +namespace { + +constexpr std::uint64_t PoolStackBytes = 64ull << 20u; +constexpr std::uint64_t MostCommittedBytes = 8ull << 20u; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error("registration prepare stack: " + message); +} + +struct StackUse { + std::thread::id thread; + std::uint64_t reserved = 0; + std::uint64_t committed = 0; +}; + +StackUse MeasureCurrentStack() { + StackUse use; + use.thread = std::this_thread::get_id(); +#ifdef _WIN32 + MEMORY_BASIC_INFORMATION here{}; + const int local = 0; + Require(VirtualQuery(&local, &here, sizeof(here)) != 0, "VirtualQuery of the stack failed"); + auto cursor = reinterpret_cast(here.AllocationBase); + for (;;) { + MEMORY_BASIC_INFORMATION info{}; + if (VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == 0 || info.AllocationBase != here.AllocationBase) break; + use.reserved += info.RegionSize; + if (info.State == MEM_COMMIT) use.committed += info.RegionSize; + cursor = reinterpret_cast(info.BaseAddress) + info.RegionSize; + } +#elif defined(__APPLE__) + use.reserved = pthread_get_stacksize_np(pthread_self()); +#else + pthread_attr_t attributes; + Require(pthread_getattr_np(pthread_self(), &attributes) == 0, "pthread_getattr_np failed"); + void* base = nullptr; + std::size_t size = 0; + pthread_attr_getstack(&attributes, &base, &size); + pthread_attr_destroy(&attributes); + use.reserved = size; +#endif + return use; +} + +StackUse MeasureOn(const std::function)>& submit) { + std::mutex mutex; + std::condition_variable done; + bool measured = false; + StackUse use; + submit([&] { + const auto result = MeasureCurrentStack(); + std::lock_guard lock(mutex); + use = result; + measured = true; + done.notify_all(); + }); + std::unique_lock lock(mutex); + done.wait(lock, [&] { return measured; }); + return use; +} + +void CheckPool(const char* name, const std::function)>& submit) { + const auto use = MeasureOn(submit); + Require(use.thread != std::this_thread::get_id(), std::string(name) + " ran the task on the caller's thread"); + Require(use.reserved >= PoolStackBytes, std::string(name) + " thread reserves " + std::to_string(use.reserved >> 20u) + " MiB of stack, less than 64 MiB"); +#ifdef _WIN32 + Require(use.committed <= MostCommittedBytes, std::string(name) + " thread commits " + std::to_string(use.committed >> 20u) + " MiB of its " + std::to_string(use.reserved >> 20u) + " MiB stack up front"); +#endif +} + +} + +int main() { + try { + AgcDriver::DriverDetail::Driver::Get(); + CheckPool("registration preparation", [](std::function task) { AgcDriver::DriverDetail::SubmitRegistrationPreparation(std::move(task)); }); + CheckPool("library prebuild", [](std::function task) { AgcDriver::DriverDetail::SubmitLibraryPrebuild(std::move(task)); }); + std::puts("registration prepare stack tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From afcfe2d9e2ddf11186141b35dad1de7431630645 Mon Sep 17 00:00:00 2001 From: zaryabzip <110787259+zaryabzip@users.noreply.github.com> Date: Tue, 20 Oct 2026 02:42:16 +0500 Subject: [PATCH 081/167] fix(agc): join and count the Windows shader preparation threads The Windows pool closed each thread handle at creation, so Close() joined no thread and liveWorkers was never raised before each worker lowered it. Keep the handle and thread id, wait for the handle in Close(), and raise liveWorkers like the POSIX path. The stack test now checks the live count before and after CloseRegistrationPreparation(). --- .../src/Driver/Shaders/ShaderRegistry.cpp | 32 +++++++++++++++---- .../execution/RegistrationPrepareStack.cpp | 8 +++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 3d92e38173..cf60184e86 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1197,7 +1197,7 @@ public: } void Close() { - std::vector joined; + std::vector joined; { std::unique_lock lock(mutex); closed = true; @@ -1205,21 +1205,39 @@ public: joined.swap(workers); } ready.notify_all(); - for (const auto worker : joined) { - if (!pthread_equal(worker, pthread_self())) pthread_join(worker, nullptr); - } + for (const auto& worker : joined) Join(worker); } private: static constexpr std::size_t StackBytes = 64u << 20u; +#ifdef _WIN32 + struct Worker { + HANDLE handle; + unsigned id; + }; + + static void Join(const Worker& worker) { + if (worker.id != GetCurrentThreadId()) WaitForSingleObject(worker.handle, INFINITE); + CloseHandle(worker.handle); + } +#else + using Worker = pthread_t; + + static void Join(Worker worker) { + if (!pthread_equal(worker, pthread_self())) pthread_join(worker, nullptr); + } +#endif + explicit RegistrationPreparePool(unsigned count) { #ifdef _WIN32 for (unsigned index = 0; index < count; ++index) { - const auto thread = _beginthreadex(nullptr, static_cast(StackBytes), &RegistrationPreparePool::WindowsMain, this, STACK_SIZE_PARAM_IS_A_RESERVATION, nullptr); + unsigned id = 0; + const auto thread = _beginthreadex(nullptr, static_cast(StackBytes), &RegistrationPreparePool::WindowsMain, this, STACK_SIZE_PARAM_IS_A_RESERVATION, &id); if (thread != 0) { - CloseHandle(reinterpret_cast(thread)); + workers.push_back({reinterpret_cast(thread), id}); ++threads; + liveWorkers.fetch_add(1, std::memory_order_acq_rel); } } #else @@ -1289,7 +1307,7 @@ private: std::condition_variable ready; std::condition_variable idle; std::deque tasks; - std::vector workers; + std::vector workers; unsigned threads = 0; inline static std::atomic liveWorkers{0}; unsigned running = 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RegistrationPrepareStack.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RegistrationPrepareStack.cpp index c28a98d697..20c3fd9389 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/RegistrationPrepareStack.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RegistrationPrepareStack.cpp @@ -1,8 +1,10 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include #include #include #include +#include #include #include #include @@ -95,6 +97,12 @@ int main() { AgcDriver::DriverDetail::Driver::Get(); CheckPool("registration preparation", [](std::function task) { AgcDriver::DriverDetail::SubmitRegistrationPreparation(std::move(task)); }); CheckPool("library prebuild", [](std::function task) { AgcDriver::DriverDetail::SubmitLibraryPrebuild(std::move(task)); }); + const char* prebuildThreads = std::getenv("APS5_PREBUILD_THREADS"); + const auto started = std::max(2u, std::thread::hardware_concurrency() / 2u) + (prebuildThreads != nullptr ? std::max(1u, static_cast(std::strtoul(prebuildThreads, nullptr, 10))) : 1u); + const auto live = AgcDriver::DriverDetail::RegistrationPreparationThreads(); + Require(live == started, "the preparation and prebuild pools count " + std::to_string(live) + " live threads of the " + std::to_string(started) + " they started"); + AgcDriver::DriverDetail::CloseRegistrationPreparation(); + Require(AgcDriver::DriverDetail::RegistrationPreparationThreads() == 0, "closing the preparation and prebuild pools left " + std::to_string(AgcDriver::DriverDetail::RegistrationPreparationThreads()) + " live threads"); std::puts("registration prepare stack tests passed"); return 0; } catch (const std::exception& error) { From b4cd843324bc82f7d9806848da66015dab45ce7f Mon Sep 17 00:00:00 2001 From: Joey Fisher Date: Mon, 19 Oct 2026 12:06:03 -0600 Subject: [PATCH 082/167] 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 083/167] 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 084/167] 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 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 085/167] 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 086/167] 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 087/167] 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 088/167] 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 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 089/167] 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 090/167] 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 091/167] 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 086e98a42a3423ddd467e07b89b5ae6b742a4710 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Mon, 19 Oct 2026 23:11:53 -0300 Subject: [PATCH 092/167] fix(agc): decode cube descriptors with slices past their last whole cube # Conflicts: # docs/dev/debt/libSceAgcDriver.md --- .../Graphics/src/GuestTextureResource.cpp | 4 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 8 +-- .../tests/GuestTextureResource.cpp | 11 +++- .../prx/libSceAgcDriver/tests/Recorder.cpp | 66 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 5 files changed, 81 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index c07a32acc8..3c81c9c97e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -203,8 +203,8 @@ GuestTextureResource DecodeTextureResource(std::span words) Require(width == height, "guest cube texture descriptor is not square"); Require(baseArray <= depth, "guest cube texture descriptor has a base array past its last array slice"); if (baseArray == depth) depth += 5u; - if ((depth - baseArray + 1u) % 6u != 0) { - Require(false, "guest cube texture descriptor does not contain a multiple of 6 array slices" + + if (depth - baseArray + 1u < 6u) { + Require(false, "guest cube texture descriptor holds fewer than 6 array slices" + describe() + " (base array " + std::to_string(baseArray) + ")"); } break; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 1c4afad236..588b810ba5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -525,7 +525,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { - Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; @@ -632,7 +632,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { - Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; @@ -747,7 +747,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { - Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; @@ -1014,7 +1014,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; const auto viewLayerCount = std::min(geometry.imageLayers, source->ImageLayers()) - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { - Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = source->Image(); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 8410c5d9cd..db38d183f5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -290,9 +290,14 @@ void RunGuestTextureResourceTests() { Fields cubeBadArray = cube; cubeBadArray.baseArray = 6; rejectFields(cubeBadArray, "base array past its last array slice"); - Fields cubeNotMultiple = cube; - cubeNotMultiple.depth = 4; - rejectFields(cubeNotMultiple, "multiple of 6"); + Fields cubeExtraSlice = cube; + cubeExtraSlice.depth = 6; + result = DecodeTextureResource(pack(cubeExtraSlice)); + Require(result.baseArray == 0 && result.depthOrLastArray == 6, "a cube descriptor with a slice past its last whole cube lost a slice"); + Require(DescribeSurface(result).imageLayers == 7, "a cube allocation with a slice past its last whole cube omitted it"); + Fields cubePartial = cube; + cubePartial.depth = 4; + rejectFields(cubePartial, "fewer than 6 array slices"); Fields multisampled = base; multisampled.typeRaw = 14; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index f3b0ad345c..4fca55c01d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -8360,6 +8360,70 @@ void singleCubeTests(const Device& device, Recorder& recorder) { } } +void cubeExtraSliceTests(const Device& device, Recorder& recorder) { + auto context = device.GetContext(); + TextureDetiler detiler(context); + context.detiler = &detiler; + SampleProgram program(context, recorder, SAMPLE_ARRAY_SPV); + const VkComponentMapping identity{}; + std::array words{0x1000u, (56u << 20u) | (3u << 30u), 15u | (63u << 14u), 0xb0000facu, 6u, 0, 0, 0}; + auto resource = DecodeTextureResource(words); + const auto geometry = DescribeSurface(resource); + Require(geometry.imageLayers == 7u, "a cube with a slice past its last whole cube does not allocate it"); + std::vector memory(static_cast(geometry.guestBytes) + 256); + auto* surface = reinterpret_cast((reinterpret_cast(memory.data()) + 255) & ~std::uintptr_t{255}); + for (std::uint32_t layer = 0; layer < geometry.imageLayers; ++layer) { + std::memset(surface + geometry.GuestLayerOffset(layer), 16 * (layer + 1), static_cast(geometry.layerBytes)); + } + resource.baseAddress = reinterpret_cast(surface); + const std::span snapshot(surface, static_cast(geometry.guestBytes)); + Texture texture(context, detiler, resource, identity, snapshot); + auto storage = StorageTexture::Create(context, detiler, resource, 0); + recorder.Keep(storage); + Texture storageView(context, storage, resource, identity); + const auto wordCube = [&words](std::vector& backing, std::uint32_t format, const std::function& word) { + auto cubeWords = words; + cubeWords[1] = (format << 20u) | (3u << 30u); + auto described = DecodeTextureResource(cubeWords); + const auto layout = DescribeSurface(described); + backing.assign(static_cast(layout.guestBytes) + 256, std::byte{}); + auto* base = reinterpret_cast((reinterpret_cast(backing.data()) + 255) & ~std::uintptr_t{255}); + for (std::uint32_t layer = 0; layer < layout.imageLayers; ++layer) { + const auto value = word(layer); + for (std::uint64_t offset = 0; offset < layout.layerBytes; offset += sizeof(value)) std::memcpy(base + layout.GuestLayerOffset(layer) + offset, &value, sizeof(value)); + } + described.baseAddress = reinterpret_cast(base); + return described; + }; + std::vector depthMemory; + const auto depth = wordCube(depthMemory, 22u, [](std::uint32_t layer) { + const float value = static_cast(layer + 1) / 8.0f; + std::uint32_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + return bits; + }); + auto depthStorage = StorageTexture::Create(context, detiler, depth, 0); + recorder.Keep(depthStorage); + Require(Texture::CanDepthCopyFrom(*depthStorage, depth), "a cube with a slice past its last whole cube cannot take a depth copy"); + Texture depthCopy(context, depthStorage, depth, identity, Texture::DepthCopyTag{}); + depthCopy.RefreshDepthCopy(); + constexpr std::array unsignedFloats{960u, 1024u, 1056u, 1088u, 1104u, 1120u, 1136u}; + std::vector repackMemory; + const auto repacked = wordCube(repackMemory, 43u, [&unsignedFloats](std::uint32_t layer) { return unsignedFloats[layer] << 21u; }); + auto repackStorage = StorageTexture::Create(context, detiler, repacked, 0); + recorder.Keep(repackStorage); + Require(Texture::CanRepackCopyFrom(*repackStorage, repacked), "a cube with a slice past its last whole cube cannot take a repacked copy"); + Texture repackCopy(context, repackStorage, repacked, identity, Texture::RepackCopyTag{}); + repackCopy.RefreshDepthCopy(); + for (std::uint32_t layer = 0; layer < geometry.imageLayers; ++layer) { + const auto expected = 16.0f * (layer + 1) / 255.0f; + expectRed(program.Red(texture.View(), texture.Layout(), 0, static_cast(layer)), expected, "a cube snapshot with a slice past its last whole cube sampled the wrong slice"); + expectRed(program.Red(storageView.View(), storageView.Layout(), 0, static_cast(layer)), expected, "a cube storage view with a slice past its last whole cube sampled the wrong slice"); + expectRed(program.Red(depthCopy.View(), depthCopy.Layout(), 0, static_cast(layer)), static_cast(layer + 1) / 8.0f, "a cube depth copy with a slice past its last whole cube sampled the wrong slice"); + expectRed(program.Red(repackCopy.View(), repackCopy.Layout(), 0, static_cast(layer)), static_cast(layer + 1), "a cube repacked copy with a slice past its last whole cube sampled the wrong slice"); + } +} + struct SampledViewProbe { Context& context; PFN_vkGetDeviceProcAddr resolver; @@ -9705,6 +9769,7 @@ int main(int argc, char** argv) { } if (argc == 2 && std::string_view(argv[1]) == "--cube-only") { singleCubeTests(device, recorder); + cubeExtraSliceTests(device, recorder); std::cout << "Single cube snapshot and storage sampling tests passed\n"; return 0; } @@ -9980,6 +10045,7 @@ int main(int argc, char** argv) { firstLayerViewTests(device, recorder); sampledStorageBaseLevelUsageTests(device, recorder); singleCubeTests(device, recorder); + cubeExtraSliceTests(device, recorder); atomicViewTests(device, recorder); metadataPassTests(device, recorder); cmaskPassTests(device, recorder); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 259f6530b9..3e18ef4b7a 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -138,7 +138,7 @@ - [HTILE written by other kernels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - when a depth target is bound and its HTILE was written since it was last read, the words over its whole pipe-aligned metadata (`HtileMetadataBytes`, the extent `HtileFillCovers` takes) are read, outside the surfaces mutex since the read waits for recorded work: when every one marks the depth (ZMask 0) or the stencil (SMem 0) cleared, the surface takes that fast clear and the words are stored back as expanded (depth only: ZMask 15 over the full Z range; with stencil: ZMask 15, Z range and SMem, SR1 and SR0 3), so only the title's next clear clears it again. PPSA29141 clears its depth every frame with a compute kernel the fill hooks do not recognize (`InvokeHTileClearComputeShader` in its `ClearMRT` pass); without this its depth kept every earlier frame and moving objects left black trails. PPSA25344 (Unreal Engine 5) reads its HTILE after the depth prepass in its Nanite depth export, which takes a tile with ZMask 0 as holding the clear value; without the expanded words the export put Nanite depth over the prepass and the Nanite material pass painted over the meshes in front. The word layouts are Mesa radv's for GFX10. Metadata with some tiles cleared is not modelled, and the compression state draws leave in HTILE is not tracked, so a title reading its HTILE after a draw sees the stored expanded words. A recognized HTILE fill or clear makes the next bind read the words again. A slice other than 0 of a depth array is read at its own HTILE (`DB_HTILE_DATA_BASE` plus the slice times `HtileMetadataBytes`) but the HTILE word layout of slices 1 and up is not modelled (PPSA05684's tile masks need X8 and Y8 to take Z0 and Z1 as well, slices 4 and up unmeasured), which neither the check that every word is cleared nor the store-back of each word in place needs, so such a slice takes the clear like slice 0. A depth-only HTILE whose tiles all have ZMask 0 but different Min/Max Z (per-tile clear values, PPSA05684's cascades) is taken as a whole-surface clear to `DB_DEPTH_CLEAR`, the value the DB's depth test reads for a cleared tile (`HTILE_Z_CLEAR_REG` in Mesa's `src/amd/common/ac_descriptors.h`: the clear word "only clears HiZ", "the DB register contain[s] the full clear values"); the HiZ test before it, which rejects against each tile's own Min/Max Z range (the HTILE definitions in the same file), is not modelled, so a draw that HiZ would reject over a tile cleared with Min/Max Z 0 is tested against `DB_DEPTH_CLEAR` instead. A multisampled depth target, and any target whose HTILE overlaps a resident native depth guard, skips this read and store-back and goes to the native multisample depth owner, which keeps its own refusal for a fast-clear transition. - [RADV fill-memory kernel](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) (libSceAgcDriver) - the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes RADV's 16-byte `fill_memory` compute kernel (`MatchClampedGlobalFill`: 11 GFX10.3 words, 64x1x1 groups, six user SGPRs with the address in `user[2..3]`, the largest store offset in `user[4]` and the value in `user[5]`; each thread stores 16 bytes at `min(thread * 16, user[4])`) as a fill of `min(threads * 16, user[4] + 16)` bytes. RADV initializes and fast-clears DCC keys and other metadata with it, so the key and metadata tracking sees such a fill where it is in the command stream; run as a dispatch, its global stores name no range, and a key fill was only seen once it had landed, after later writes to the surface. Only a 16-byte aligned address and largest offset are taken. RADV's 4-byte variant (fills below 16 bytes), other Mesa versions' encodings and other compilers still run as dispatches. The kernel words are the ones vkQuake's PS5 RADV build (Mesa 25.2) submitted; they are not compared with the NIR builder's output for other targets. - [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import: the largest aligned power-of-two block, from twice the import alignment up to the 8 MiB window, that holds the requested address and that they fill exactly, so a block only grows by doubling as its window fills. A block that would exceed the budget, would replace an import a build holds, or fails (a refused Windows alias, `vkAllocateMemory`) steps down to the next smaller block; a failed block is not tried again until one of its ranges changes. Ranges larger than the window are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound exactly over adjacent ranges of any size (from the first one's start to the last one's end), joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer is bound exactly over all of its pieces, or when a sparse descriptor heap over it is bound: a heap a shader binds whole while only the pieces from its base are mapped is bound in place from the import of that run, its views ending at the import's end as the copy's do, once the previous bind of that address saw the same last mapped byte (a heap that grows between every two of its binds stays copied; one bound more often than it grows is joined again after each growth) and at most 64 times per joined run base; past that, with a message, it is not joined again: it is bound in place while an existing import covers its run and copied otherwise, until no registered range starts at that base or the device changes. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. -- [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. +- [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces still throw: hardware load bounds and filtered-sample addressing differ. A range of six faces or more that does not end on a whole cube keeps every slice up to `DEPTH`, the bound loads use on hardware; sampling the cube it cuts short is out-of-range cube-array sampling. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Array layer views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (libSceAgcDriver) - `ArrayLayersServeView` lets a 2D texture at a 2D array image's base address, or a 2D array texture over its first layers, view that image (`Texture::CanCopyFrom`). Mesa-built homebrew (ps5-opengl) writes the view's last slice plus one to `CB_COLOR_ATTRIB3.MIP0_DEPTH`, so a slice-0 render target is a two-layer array image, and the title samples it as a plain 2D texture (SuperTuxKart PPSA69421 drew a black 3D scene and minimap without this). The per-layer layout of a mipmapped array is assumed equal to a standalone surface's; it is not measured on the console. A 2D texture at a later slice's address still gets an image of its own. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. From 2dc56f37c3322075bba5718a6e1b4519b41313d9 Mon Sep 17 00:00:00 2001 From: alosta Date: Sun, 18 Oct 2026 06:05:24 +0100 Subject: [PATCH 093/167] feat(libs): add unimplemented PS5 system library stubs and debt docs Add boilerplate stubs for libSceAt9Enc, libSceAudiodecCpu, libSceAudiodecCpuHevag, libSceM4aacEnc, libSceSulpha, libSceCamera, and libSceDepth2, each exporting its known NIDs via NotImplemented_nid_no_patch. Include debt documentation with sysmodule IDs and (where available) orbital-derived signatures. Add sceSystemServiceShowDisplaySafeAreaSettings as a silent success stub and document it under Silent stubs. AI-assisted: yes --- core/libs/prx/libSceAt9Enc/Export.cpp | 34 ++++ core/libs/prx/libSceAudiodecCpu/Export.cpp | 29 +++ .../prx/libSceAudiodecCpuHevag/Export.cpp | 14 ++ core/libs/prx/libSceCamera/Export.cpp | 179 ++++++++++++++++++ core/libs/prx/libSceDepth2/Export.cpp | 69 +++++++ core/libs/prx/libSceM4aacEnc/Export.cpp | 94 +++++++++ core/libs/prx/libSceSulpha/Export.cpp | 49 +++++ core/libs/prx/libSceSystemService/Export.cpp | 4 + docs/dev/debt/libSceAt9Enc.md | 16 ++ docs/dev/debt/libSceAudiodecCpu.md | 22 +++ docs/dev/debt/libSceAudiodecCpuHevag.md | 7 + docs/dev/debt/libSceCamera.md | 42 ++++ docs/dev/debt/libSceDepth2.md | 20 ++ docs/dev/debt/libSceM4aacEnc.md | 32 ++++ docs/dev/debt/libSceSulpha.md | 16 ++ docs/dev/debt/libSceSystemService.md | 2 +- 16 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAt9Enc/Export.cpp create mode 100644 core/libs/prx/libSceAudiodecCpu/Export.cpp create mode 100644 core/libs/prx/libSceAudiodecCpuHevag/Export.cpp create mode 100644 core/libs/prx/libSceCamera/Export.cpp create mode 100644 core/libs/prx/libSceDepth2/Export.cpp create mode 100644 core/libs/prx/libSceM4aacEnc/Export.cpp create mode 100644 core/libs/prx/libSceSulpha/Export.cpp create mode 100644 docs/dev/debt/libSceAt9Enc.md create mode 100644 docs/dev/debt/libSceAudiodecCpu.md create mode 100644 docs/dev/debt/libSceAudiodecCpuHevag.md create mode 100644 docs/dev/debt/libSceCamera.md create mode 100644 docs/dev/debt/libSceDepth2.md create mode 100644 docs/dev/debt/libSceM4aacEnc.md create mode 100644 docs/dev/debt/libSceSulpha.md diff --git a/core/libs/prx/libSceAt9Enc/Export.cpp b/core/libs/prx/libSceAt9Enc/Export.cpp new file mode 100644 index 0000000000..e567ab6085 --- /dev/null +++ b/core/libs/prx/libSceAt9Enc/Export.cpp @@ -0,0 +1,34 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + + +extern "C" { + +int32_t APS5_VABI sceAt9EncClearContext(void *pAt9EncWorkArea, int32_t *pInternalResult) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceAt9EncCreateEncoder(void *pAt9EncWorkArea, void*pAt9EncParam, int32_t memSize, int32_t *pInternalResult) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceAt9EncEncode(void *pAt9EncWorkArea, const void *pInputBuf, void *pOutputBuf, uint32_t inputBufSize, uint32_t outputBufSize, uint32_t *pOutputDataByte, uint32_t *pUsedBufSize, int32_t *pInternalResult) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceAt9EncFlush(void *pAt9EncWorkArea, void *pOutputBuf, uint32_t outputBufSize, uint32_t *pOutputDataByte, int32_t *pInternalResult) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceAt9EncQueryMemSize(void*pAt9EncParam, int32_t *pMemSize, int32_t *pInternalResult) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAudiodecCpu/Export.cpp b/core/libs/prx/libSceAudiodecCpu/Export.cpp new file mode 100644 index 0000000000..2c05e334a4 --- /dev/null +++ b/core/libs/prx/libSceAudiodecCpu/Export.cpp @@ -0,0 +1,29 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + + +extern "C" { + +int32_t APS5_VABI sceAudiodecCpuClearContext(void*pRes, uint32_t uiCodecType) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceAudiodecCpuDecode(void*pCtrl, void*pRes, uint32_t uiCodecType) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceAudiodecCpuInitDecoder(void*pCtrl, void*pRes, uint32_t uiCodecType) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceAudiodecCpuQueryMemSize(void*pCtrl, void*pRes, uint32_t uiCodecType) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAudiodecCpuHevag/Export.cpp b/core/libs/prx/libSceAudiodecCpuHevag/Export.cpp new file mode 100644 index 0000000000..c3eff17036 --- /dev/null +++ b/core/libs/prx/libSceAudiodecCpuHevag/Export.cpp @@ -0,0 +1,14 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + + +extern "C" { + +int APS5_VABI sceAudiodecCpuHevagUnimplemented() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceCamera/Export.cpp b/core/libs/prx/libSceCamera/Export.cpp new file mode 100644 index 0000000000..4d1031cb13 --- /dev/null +++ b/core/libs/prx/libSceCamera/Export.cpp @@ -0,0 +1,179 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + + +extern "C" { + +int APS5_VABI sceCamera2Close() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2Finalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetAttribute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetAutoExposureGain() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetAutoWhiteBalance() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetCalibrationData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetContrast() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetExposureGain() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetFieldOfView() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetFrameData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetGamma() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetHue() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetSaturation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetSharpness() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetWhiteBalance() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2Initialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2IsAttached() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2IsValidFrameData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2Open() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetAttribute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetAutoExposureGain() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetAutoWhiteBalance() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetContrast() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetExposureGain() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetGamma() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetHue() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetSaturation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetSharpness() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetVideoSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetWhiteBalance() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2Start() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2Stop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceDepth2/Export.cpp b/core/libs/prx/libSceDepth2/Export.cpp new file mode 100644 index 0000000000..adc94f415a --- /dev/null +++ b/core/libs/prx/libSceDepth2/Export.cpp @@ -0,0 +1,69 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + + +extern "C" { + +int APS5_VABI sceDepth2GetImage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2GetRectifiedImage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2Initialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2InitializeVr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2QueryMemory() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2SaveInternalBuffers() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2SetCameraFactoryCalibrationData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2SetCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2SetRoi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2Submit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2Terminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2WaitAndExecutePostProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceM4aacEnc/Export.cpp b/core/libs/prx/libSceM4aacEnc/Export.cpp new file mode 100644 index 0000000000..e71f829cca --- /dev/null +++ b/core/libs/prx/libSceM4aacEnc/Export.cpp @@ -0,0 +1,94 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + + +extern "C" { + +int32_t APS5_VABI sceM4aacEncClearContext(int32_t m4aacEncHandle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncClearContextEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceM4aacEncCreateEncoder(int32_t channels, int32_t samplingRate, int32_t bitRate, int32_t inputFormat, int32_t outputFormat, int32_t *pInternalError) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncCreateEncoderEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceM4aacEncDeleteEncoder(int32_t m4aacEncHandle, int32_t *pInternalError) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncDeleteEncoderEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceM4aacEncEncode(int32_t m4aacEncHandle, void *pInput, uint32_t inputByte, void *pOutput, uint32_t *pOutputByte, int32_t *pInternalError) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncEncodeEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncEncodeWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncEncodeWithPriorityEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceM4aacEncFlush(int32_t m4aacEncHandle, void *pOutput, uint32_t *pOutputByte, int32_t *pInternalError) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncFlushEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncFlushWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncFlushWithPriorityEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncGetContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSulpha/Export.cpp b/core/libs/prx/libSceSulpha/Export.cpp new file mode 100644 index 0000000000..ac1f6bede2 --- /dev/null +++ b/core/libs/prx/libSceSulpha/Export.cpp @@ -0,0 +1,49 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + + +extern "C" { + +int APS5_VABI sceSulphaGetClientInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaGetConnectedClientCount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaGetDefaultConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaGetNeededMemory() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaMessage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaSetBookmark() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaShutdown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index f99a9cc62f..cc161de50a 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -262,6 +262,10 @@ int APS5_VABI sceSystemServiceGetDisplaySafeAreaInfo(SystemServiceDisplaySafeAre return SYSTEM_SERVICE_OK; } +int APS5_VABI sceSystemServiceShowDisplaySafeAreaSettings(void) { + return SYSTEM_SERVICE_OK; +} + int APS5_VABI sceSystemServiceGetHdrToneMapLuminance(SystemServiceHdrToneMapLuminance* luminance) { if (luminance == nullptr) { return SYSTEM_SERVICE_ERROR_PARAMETER; diff --git a/docs/dev/debt/libSceAt9Enc.md b/docs/dev/debt/libSceAt9Enc.md new file mode 100644 index 0000000000..742fdd3b1e --- /dev/null +++ b/docs/dev/debt/libSceAt9Enc.md @@ -0,0 +1,16 @@ +# libSceAt9Enc technical debt + +### Not implemented + +- [libSceAt9Enc](../../../core/libs/prx/libSceSysmodule/ModuleTable.hpp) (PS5 system library; name suggests ATRAC9 encoder but unconfirmed; sysmodule id `0x000000d1`) is not implemented in AnyPS5. Only its entry in the sysmodule table exists; there is no `core/libs/prx/libSceAt9Enc` implementation and no host `.prx` is shipped. Titles that link it directly (observed with PPSA03332) fail to load with `libSceAt9Enc.prx: cannot open shared object file`. + +### Related unimplemented system libraries + +The same load-time failure affects other firmware system libraries that are listed in the sysmodule table but have no implementation or shipped host `.prx` in AnyPS5, observed while running PPSA03332: `libSceAudiodecCpu`, `libSceAudiodecCpuHevag`, `libSceM4aacEnc`, `libSceSulpha`, `libSceCamera`, `libSceDepth2`. +## Signatures (orbital db_types.json) + +- `int32_t sceAt9EncClearContext(void *pAt9EncWorkArea, int32_t *pInternalResult)` +- `int32_t sceAt9EncCreateEncoder(void *pAt9EncWorkArea, struct *pAt9EncParam, int32_t memSize, int32_t *pInternalResult)` +- `int32_t sceAt9EncEncode(void *pAt9EncWorkArea, const void *pInputBuf, void *pOutputBuf, uint32_t inputBufSize, uint32_t outputBufSize, uint32_t *pOutputDataByte, uint32_t *pUsedBufSize, int32_t *pInternalResult)` +- `int32_t sceAt9EncFlush(void *pAt9EncWorkArea, void *pOutputBuf, uint32_t outputBufSize, uint32_t *pOutputDataByte, int32_t *pInternalResult)` +- `int32_t sceAt9EncQueryMemSize(struct *pAt9EncParam, int32_t *pMemSize, int32_t *pInternalResult)` diff --git a/docs/dev/debt/libSceAudiodecCpu.md b/docs/dev/debt/libSceAudiodecCpu.md new file mode 100644 index 0000000000..aa6736eded --- /dev/null +++ b/docs/dev/debt/libSceAudiodecCpu.md @@ -0,0 +1,22 @@ +# libSceAudiodecCpu technical debt + +### Not implemented + +- `libSceAudiodecCpu` (PS5 system library, sysmodule id `0x000000bd`) is not implemented in AnyPS5. Only its entry in the sysmodule table exists; there is no `core/libs/prx/libSceAudiodecCpu` implementation and no host `.prx` is shipped. Titles that link it directly (observed with PPSA03332) fail to load with `libSceAudiodecCpu.prx: cannot open shared object file`. + +Known public exports (from the aerolib NID database): +- `sceAudiodecCpuClearContext` +- `sceAudiodecCpuDecode` +- `sceAudiodecCpuInitDecoder` +- `sceAudiodecCpuInternalClearContext` +- `sceAudiodecCpuInternalDecode` +- `sceAudiodecCpuInternalInitDecoder` +- `sceAudiodecCpuInternalQueryMemSize` +- `sceAudiodecCpuQueryMemSize` + +## Signatures (orbital db_types.json) + +- `int32_t sceAudiodecCpuClearContext(SceAudiodecCpuResource *pRes, uint32_t uiCodecType)` +- `int32_t sceAudiodecCpuDecode(SceAudiodecCpuCtrl *pCtrl, SceAudiodecCpuResource *pRes, uint32_t uiCodecType)` +- `int32_t sceAudiodecCpuInitDecoder(SceAudiodecCpuCtrl *pCtrl, SceAudiodecCpuResource *pRes, uint32_t uiCodecType)` +- `int32_t sceAudiodecCpuQueryMemSize(SceAudiodecCpuCtrl *pCtrl, SceAudiodecCpuResource *pRes, uint32_t uiCodecType)` diff --git a/docs/dev/debt/libSceAudiodecCpuHevag.md b/docs/dev/debt/libSceAudiodecCpuHevag.md new file mode 100644 index 0000000000..cbf3809d2f --- /dev/null +++ b/docs/dev/debt/libSceAudiodecCpuHevag.md @@ -0,0 +1,7 @@ +# libSceAudiodecCpuHevag technical debt + +### Not implemented + +- `libSceAudiodecCpuHevag` (PS5 system library, sysmodule id `0x000000e1`) is not implemented in AnyPS5. Only its entry in the sysmodule table exists; there is no `core/libs/prx/libSceAudiodecCpuHevag` implementation and no host `.prx` is shipped. Titles that link it directly (observed with PPSA03332) fail to load with `libSceAudiodecCpuHevag.prx: cannot open shared object file`. + +Note: aerolib contains only internal/mangled symbols for this library, so the public API list is unknown. diff --git a/docs/dev/debt/libSceCamera.md b/docs/dev/debt/libSceCamera.md new file mode 100644 index 0000000000..08be576113 --- /dev/null +++ b/docs/dev/debt/libSceCamera.md @@ -0,0 +1,42 @@ +# libSceCamera technical debt + +### Not implemented + +- `libSceCamera` (PS5 system library, sysmodule id `0x8000001a`) is not implemented in AnyPS5. Only its entry in the sysmodule table exists; there is no `core/libs/prx/libSceCamera` implementation and no host `.prx` is shipped. Titles that link it directly (observed with PPSA03332) fail to load with `libSceCamera.prx: cannot open shared object file`. + +No public signatures found in the orbital NID database or online references (orbital only has PS4 `sceCamera*`, not PS5 `sceCamera2*`); needs disassembly or hardware measurement. +Known public exports (from the aerolib NID database): +- `sceCamera2Close` +- `sceCamera2Finalize` +- `sceCamera2GetAttribute` +- `sceCamera2GetAutoExposureGain` +- `sceCamera2GetAutoWhiteBalance` +- `sceCamera2GetCalibrationData` +- `sceCamera2GetConfig` +- `sceCamera2GetContrast` +- `sceCamera2GetExposureGain` +- `sceCamera2GetFieldOfView` +- `sceCamera2GetFrameData` +- `sceCamera2GetGamma` +- `sceCamera2GetHue` +- `sceCamera2GetSaturation` +- `sceCamera2GetSharpness` +- `sceCamera2GetWhiteBalance` +- `sceCamera2Initialize` +- `sceCamera2IsAttached` +- `sceCamera2IsValidFrameData` +- `sceCamera2Open` +- `sceCamera2SetAttribute` +- `sceCamera2SetAutoExposureGain` +- `sceCamera2SetAutoWhiteBalance` +- `sceCamera2SetConfig` +- `sceCamera2SetContrast` +- `sceCamera2SetExposureGain` +- `sceCamera2SetGamma` +- `sceCamera2SetHue` +- `sceCamera2SetSaturation` +- `sceCamera2SetSharpness` +- `sceCamera2SetVideoSync` +- `sceCamera2SetWhiteBalance` +- `sceCamera2Start` +- `sceCamera2Stop` diff --git a/docs/dev/debt/libSceDepth2.md b/docs/dev/debt/libSceDepth2.md new file mode 100644 index 0000000000..39864f4965 --- /dev/null +++ b/docs/dev/debt/libSceDepth2.md @@ -0,0 +1,20 @@ +# libSceDepth2 technical debt + +### Not implemented + +- `libSceDepth2` (PS5 system library, sysmodule id `0xunknown`) is not implemented in AnyPS5. Only its entry in the sysmodule table exists; there is no `core/libs/prx/libSceDepth2` implementation and no host `.prx` is shipped. Titles that link it directly (observed with PPSA03332) fail to load with `libSceDepth2.prx: cannot open shared object file`. + +No public signatures found in the orbital NID database or online references; needs disassembly or hardware measurement. +Known public exports (from the aerolib NID database): +- `sceDepth2GetImage` +- `sceDepth2GetRectifiedImage` +- `sceDepth2Initialize` +- `sceDepth2InitializeVr` +- `sceDepth2QueryMemory` +- `sceDepth2SaveInternalBuffers` +- `sceDepth2SetCameraFactoryCalibrationData` +- `sceDepth2SetCommand` +- `sceDepth2SetRoi` +- `sceDepth2Submit` +- `sceDepth2Terminate` +- `sceDepth2WaitAndExecutePostProcess` diff --git a/docs/dev/debt/libSceM4aacEnc.md b/docs/dev/debt/libSceM4aacEnc.md new file mode 100644 index 0000000000..dbf74bdba4 --- /dev/null +++ b/docs/dev/debt/libSceM4aacEnc.md @@ -0,0 +1,32 @@ +# libSceM4aacEnc technical debt + +### Not implemented + +- `libSceM4aacEnc` (PS5 system library, sysmodule id `0x000000bc`) is not implemented in AnyPS5. Only its entry in the sysmodule table exists; there is no `core/libs/prx/libSceM4aacEnc` implementation and no host `.prx` is shipped. Titles that link it directly (observed with PPSA03332) fail to load with `libSceM4aacEnc.prx: cannot open shared object file`. + +Known public exports (from the aerolib NID database): +- `sceM4aacEncClearContext` +- `sceM4aacEncClearContextEx` +- `sceM4aacEncCreateEncoder` +- `sceM4aacEncCreateEncoderEx` +- `sceM4aacEncDeleteEncoder` +- `sceM4aacEncDeleteEncoderEx` +- `sceM4aacEncEncode` +- `sceM4aacEncEncodeEx` +- `sceM4aacEncEncodeWithPriority` +- `sceM4aacEncEncodeWithPriorityEx` +- `sceM4aacEncFlush` +- `sceM4aacEncFlushEx` +- `sceM4aacEncFlushWithPriority` +- `sceM4aacEncFlushWithPriorityEx` +- `sceM4aacEncGetContext` +- `sceM4aacEncInitialize` +- `sceM4aacEncTerminate` + +## Signatures (orbital db_types.json) + +- `int32_t sceM4aacEncClearContext(int32_t m4aacEncHandle)` +- `int32_t sceM4aacEncCreateEncoder(int32_t channels, int32_t samplingRate, int32_t bitRate, int32_t inputFormat, int32_t outputFormat, int32_t *pInternalError)` +- `int32_t sceM4aacEncDeleteEncoder(int32_t m4aacEncHandle, int32_t *pInternalError)` +- `int32_t sceM4aacEncEncode(int32_t m4aacEncHandle, void *pInput, uint32_t inputByte, void *pOutput, uint32_t *pOutputByte, int32_t *pInternalError)` +- `int32_t sceM4aacEncFlush(int32_t m4aacEncHandle, void *pOutput, uint32_t *pOutputByte, int32_t *pInternalError)` diff --git a/docs/dev/debt/libSceSulpha.md b/docs/dev/debt/libSceSulpha.md new file mode 100644 index 0000000000..36c6d64488 --- /dev/null +++ b/docs/dev/debt/libSceSulpha.md @@ -0,0 +1,16 @@ +# libSceSulpha technical debt + +### Not implemented + +- `libSceSulpha` (PS5 system library, sysmodule id `0x0000009f`) is not implemented in AnyPS5. Only its entry in the sysmodule table exists; there is no `core/libs/prx/libSceSulpha` implementation and no host `.prx` is shipped. Titles that link it directly (observed with PPSA03332) fail to load with `libSceSulpha.prx: cannot open shared object file`. + +No public signatures found in the orbital NID database or online references; needs disassembly or hardware measurement. +Known public exports (from the aerolib NID database): +- `sceSulphaGetClientInfo` +- `sceSulphaGetConnectedClientCount` +- `sceSulphaGetDefaultConfig` +- `sceSulphaGetNeededMemory` +- `sceSulphaInit` +- `sceSulphaMessage` +- `sceSulphaSetBookmark` +- `sceSulphaShutdown` diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 9339410b2f..badd7b6541 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -5,7 +5,7 @@ - [libSceSystemService](../../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick`, `sceSystemServiceReportAbnormalTermination`, `sceSystemServiceDisableMusicPlayer`, `sceSystemServiceReenableMusicPlayer`, `sceSystemServiceDisableMediaPlay` and `sceSystemServiceReenableMediaPlay` return success and do nothing: the host has no auto power-off timer to reset, no system crash reporter to notify, no system music player to pause or resume and no system media playback to disable. PPSA01521 calls `sceSystemServiceDisableMediaPlay` during start-up, and its wrappers for both pass no arguments - [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 launches no web browser, so the call fails and nothing is shown. RommPS PPSA76677 then tells the player to scan its QR code instead - [sceSystemServiceShowControllerSettings](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without showing the controller settings: AnyPS5 has no system UI to open. Cat Quest III (PPSA15065) imports it -- [sceSystemServiceOpenChallengeActivity](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without opening the challenge activity: AnyPS5 has no system UI to open. Cat Quest III (PPSA15065) imports it +- [sceSystemServiceShowDisplaySafeAreaSettings](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without showing the display safe area settings dialog: AnyPS5 has no system UI to open. Observed with PPSA03332- [sceSystemServiceOpenChallengeActivity](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without opening the challenge activity: AnyPS5 has no system UI to open. Cat Quest III (PPSA15065) imports it ### Unknown function info From 73e29b9469169c5ce8444fad7cd2acea6ba9d47c Mon Sep 17 00:00:00 2001 From: alosta Date: Sun, 18 Oct 2026 06:28:34 +0100 Subject: [PATCH 094/167] fix(docs): correct debt-file link for libSceAt9Enc --- docs/dev/debt/libSceAt9Enc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAt9Enc.md b/docs/dev/debt/libSceAt9Enc.md index 742fdd3b1e..0acf307238 100644 --- a/docs/dev/debt/libSceAt9Enc.md +++ b/docs/dev/debt/libSceAt9Enc.md @@ -2,7 +2,7 @@ ### Not implemented -- [libSceAt9Enc](../../../core/libs/prx/libSceSysmodule/ModuleTable.hpp) (PS5 system library; name suggests ATRAC9 encoder but unconfirmed; sysmodule id `0x000000d1`) is not implemented in AnyPS5. Only its entry in the sysmodule table exists; there is no `core/libs/prx/libSceAt9Enc` implementation and no host `.prx` is shipped. Titles that link it directly (observed with PPSA03332) fail to load with `libSceAt9Enc.prx: cannot open shared object file`. +- [libSceAt9Enc](../../../core/libs/prx/libSceAt9Enc/Export.cpp) (PS5 system library; name suggests ATRAC9 encoder but unconfirmed; sysmodule id `0x000000d1`) is not implemented in AnyPS5. Only its entry in the sysmodule table exists; there is no `core/libs/prx/libSceAt9Enc` implementation and no host `.prx` is shipped. Titles that link it directly (observed with PPSA03332) fail to load with `libSceAt9Enc.prx: cannot open shared object file`. ### Related unimplemented system libraries From 38aa6aafe88c02da9412fd44b5ad28b766666676 Mon Sep 17 00:00:00 2001 From: alosta Date: Tue, 20 Oct 2026 03:03:29 +0100 Subject: [PATCH 095/167] refactor(libs): drop the stub libraries with unknown signatures SceCamera, SceDepth2, SceSulpha and SceAudiodecCpuHevag were declared without parameters because no reference gives their signatures, and the HEVAG stub exported an invented sceAudiodecCpuHevagUnimplemented symbol that matches no real export. SceAudiodecCpu overlaps #6847. Only the libraries whose real exports and signatures are known stay. AI-assisted: yes --- core/libs/prx/libSceAudiodecCpu/Export.cpp | 29 --- .../prx/libSceAudiodecCpuHevag/Export.cpp | 14 -- core/libs/prx/libSceCamera/Export.cpp | 179 ------------------ core/libs/prx/libSceDepth2/Export.cpp | 69 ------- core/libs/prx/libSceSulpha/Export.cpp | 49 ----- docs/dev/debt/libSceAudiodecCpu.md | 22 --- docs/dev/debt/libSceAudiodecCpuHevag.md | 7 - docs/dev/debt/libSceCamera.md | 42 ---- docs/dev/debt/libSceDepth2.md | 20 -- docs/dev/debt/libSceSulpha.md | 16 -- 10 files changed, 447 deletions(-) delete mode 100644 core/libs/prx/libSceAudiodecCpu/Export.cpp delete mode 100644 core/libs/prx/libSceAudiodecCpuHevag/Export.cpp delete mode 100644 core/libs/prx/libSceCamera/Export.cpp delete mode 100644 core/libs/prx/libSceDepth2/Export.cpp delete mode 100644 core/libs/prx/libSceSulpha/Export.cpp delete mode 100644 docs/dev/debt/libSceAudiodecCpu.md delete mode 100644 docs/dev/debt/libSceAudiodecCpuHevag.md delete mode 100644 docs/dev/debt/libSceCamera.md delete mode 100644 docs/dev/debt/libSceDepth2.md delete mode 100644 docs/dev/debt/libSceSulpha.md diff --git a/core/libs/prx/libSceAudiodecCpu/Export.cpp b/core/libs/prx/libSceAudiodecCpu/Export.cpp deleted file mode 100644 index 2c05e334a4..0000000000 --- a/core/libs/prx/libSceAudiodecCpu/Export.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - - -extern "C" { - -int32_t APS5_VABI sceAudiodecCpuClearContext(void*pRes, uint32_t uiCodecType) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int32_t APS5_VABI sceAudiodecCpuDecode(void*pCtrl, void*pRes, uint32_t uiCodecType) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int32_t APS5_VABI sceAudiodecCpuInitDecoder(void*pCtrl, void*pRes, uint32_t uiCodecType) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int32_t APS5_VABI sceAudiodecCpuQueryMemSize(void*pCtrl, void*pRes, uint32_t uiCodecType) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} diff --git a/core/libs/prx/libSceAudiodecCpuHevag/Export.cpp b/core/libs/prx/libSceAudiodecCpuHevag/Export.cpp deleted file mode 100644 index c3eff17036..0000000000 --- a/core/libs/prx/libSceAudiodecCpuHevag/Export.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - - -extern "C" { - -int APS5_VABI sceAudiodecCpuHevagUnimplemented() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} diff --git a/core/libs/prx/libSceCamera/Export.cpp b/core/libs/prx/libSceCamera/Export.cpp deleted file mode 100644 index 4d1031cb13..0000000000 --- a/core/libs/prx/libSceCamera/Export.cpp +++ /dev/null @@ -1,179 +0,0 @@ -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - - -extern "C" { - -int APS5_VABI sceCamera2Close() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2Finalize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2GetAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2GetAutoExposureGain() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2GetAutoWhiteBalance() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2GetCalibrationData() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2GetConfig() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2GetContrast() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2GetExposureGain() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2GetFieldOfView() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2GetFrameData() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2GetGamma() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2GetHue() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2GetSaturation() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2GetSharpness() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2GetWhiteBalance() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2Initialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2IsAttached() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2IsValidFrameData() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2Open() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2SetAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2SetAutoExposureGain() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2SetAutoWhiteBalance() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2SetConfig() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2SetContrast() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2SetExposureGain() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2SetGamma() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2SetHue() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2SetSaturation() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2SetSharpness() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2SetVideoSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2SetWhiteBalance() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2Start() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCamera2Stop() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} diff --git a/core/libs/prx/libSceDepth2/Export.cpp b/core/libs/prx/libSceDepth2/Export.cpp deleted file mode 100644 index adc94f415a..0000000000 --- a/core/libs/prx/libSceDepth2/Export.cpp +++ /dev/null @@ -1,69 +0,0 @@ -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - - -extern "C" { - -int APS5_VABI sceDepth2GetImage() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceDepth2GetRectifiedImage() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceDepth2Initialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceDepth2InitializeVr() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceDepth2QueryMemory() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceDepth2SaveInternalBuffers() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceDepth2SetCameraFactoryCalibrationData() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceDepth2SetCommand() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceDepth2SetRoi() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceDepth2Submit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceDepth2Terminate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceDepth2WaitAndExecutePostProcess() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} diff --git a/core/libs/prx/libSceSulpha/Export.cpp b/core/libs/prx/libSceSulpha/Export.cpp deleted file mode 100644 index ac1f6bede2..0000000000 --- a/core/libs/prx/libSceSulpha/Export.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - - -extern "C" { - -int APS5_VABI sceSulphaGetClientInfo() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSulphaGetConnectedClientCount() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSulphaGetDefaultConfig() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSulphaGetNeededMemory() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSulphaInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSulphaMessage() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSulphaSetBookmark() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSulphaShutdown() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} diff --git a/docs/dev/debt/libSceAudiodecCpu.md b/docs/dev/debt/libSceAudiodecCpu.md deleted file mode 100644 index aa6736eded..0000000000 --- a/docs/dev/debt/libSceAudiodecCpu.md +++ /dev/null @@ -1,22 +0,0 @@ -# libSceAudiodecCpu technical debt - -### Not implemented - -- `libSceAudiodecCpu` (PS5 system library, sysmodule id `0x000000bd`) is not implemented in AnyPS5. Only its entry in the sysmodule table exists; there is no `core/libs/prx/libSceAudiodecCpu` implementation and no host `.prx` is shipped. Titles that link it directly (observed with PPSA03332) fail to load with `libSceAudiodecCpu.prx: cannot open shared object file`. - -Known public exports (from the aerolib NID database): -- `sceAudiodecCpuClearContext` -- `sceAudiodecCpuDecode` -- `sceAudiodecCpuInitDecoder` -- `sceAudiodecCpuInternalClearContext` -- `sceAudiodecCpuInternalDecode` -- `sceAudiodecCpuInternalInitDecoder` -- `sceAudiodecCpuInternalQueryMemSize` -- `sceAudiodecCpuQueryMemSize` - -## Signatures (orbital db_types.json) - -- `int32_t sceAudiodecCpuClearContext(SceAudiodecCpuResource *pRes, uint32_t uiCodecType)` -- `int32_t sceAudiodecCpuDecode(SceAudiodecCpuCtrl *pCtrl, SceAudiodecCpuResource *pRes, uint32_t uiCodecType)` -- `int32_t sceAudiodecCpuInitDecoder(SceAudiodecCpuCtrl *pCtrl, SceAudiodecCpuResource *pRes, uint32_t uiCodecType)` -- `int32_t sceAudiodecCpuQueryMemSize(SceAudiodecCpuCtrl *pCtrl, SceAudiodecCpuResource *pRes, uint32_t uiCodecType)` diff --git a/docs/dev/debt/libSceAudiodecCpuHevag.md b/docs/dev/debt/libSceAudiodecCpuHevag.md deleted file mode 100644 index cbf3809d2f..0000000000 --- a/docs/dev/debt/libSceAudiodecCpuHevag.md +++ /dev/null @@ -1,7 +0,0 @@ -# libSceAudiodecCpuHevag technical debt - -### Not implemented - -- `libSceAudiodecCpuHevag` (PS5 system library, sysmodule id `0x000000e1`) is not implemented in AnyPS5. Only its entry in the sysmodule table exists; there is no `core/libs/prx/libSceAudiodecCpuHevag` implementation and no host `.prx` is shipped. Titles that link it directly (observed with PPSA03332) fail to load with `libSceAudiodecCpuHevag.prx: cannot open shared object file`. - -Note: aerolib contains only internal/mangled symbols for this library, so the public API list is unknown. diff --git a/docs/dev/debt/libSceCamera.md b/docs/dev/debt/libSceCamera.md deleted file mode 100644 index 08be576113..0000000000 --- a/docs/dev/debt/libSceCamera.md +++ /dev/null @@ -1,42 +0,0 @@ -# libSceCamera technical debt - -### Not implemented - -- `libSceCamera` (PS5 system library, sysmodule id `0x8000001a`) is not implemented in AnyPS5. Only its entry in the sysmodule table exists; there is no `core/libs/prx/libSceCamera` implementation and no host `.prx` is shipped. Titles that link it directly (observed with PPSA03332) fail to load with `libSceCamera.prx: cannot open shared object file`. - -No public signatures found in the orbital NID database or online references (orbital only has PS4 `sceCamera*`, not PS5 `sceCamera2*`); needs disassembly or hardware measurement. -Known public exports (from the aerolib NID database): -- `sceCamera2Close` -- `sceCamera2Finalize` -- `sceCamera2GetAttribute` -- `sceCamera2GetAutoExposureGain` -- `sceCamera2GetAutoWhiteBalance` -- `sceCamera2GetCalibrationData` -- `sceCamera2GetConfig` -- `sceCamera2GetContrast` -- `sceCamera2GetExposureGain` -- `sceCamera2GetFieldOfView` -- `sceCamera2GetFrameData` -- `sceCamera2GetGamma` -- `sceCamera2GetHue` -- `sceCamera2GetSaturation` -- `sceCamera2GetSharpness` -- `sceCamera2GetWhiteBalance` -- `sceCamera2Initialize` -- `sceCamera2IsAttached` -- `sceCamera2IsValidFrameData` -- `sceCamera2Open` -- `sceCamera2SetAttribute` -- `sceCamera2SetAutoExposureGain` -- `sceCamera2SetAutoWhiteBalance` -- `sceCamera2SetConfig` -- `sceCamera2SetContrast` -- `sceCamera2SetExposureGain` -- `sceCamera2SetGamma` -- `sceCamera2SetHue` -- `sceCamera2SetSaturation` -- `sceCamera2SetSharpness` -- `sceCamera2SetVideoSync` -- `sceCamera2SetWhiteBalance` -- `sceCamera2Start` -- `sceCamera2Stop` diff --git a/docs/dev/debt/libSceDepth2.md b/docs/dev/debt/libSceDepth2.md deleted file mode 100644 index 39864f4965..0000000000 --- a/docs/dev/debt/libSceDepth2.md +++ /dev/null @@ -1,20 +0,0 @@ -# libSceDepth2 technical debt - -### Not implemented - -- `libSceDepth2` (PS5 system library, sysmodule id `0xunknown`) is not implemented in AnyPS5. Only its entry in the sysmodule table exists; there is no `core/libs/prx/libSceDepth2` implementation and no host `.prx` is shipped. Titles that link it directly (observed with PPSA03332) fail to load with `libSceDepth2.prx: cannot open shared object file`. - -No public signatures found in the orbital NID database or online references; needs disassembly or hardware measurement. -Known public exports (from the aerolib NID database): -- `sceDepth2GetImage` -- `sceDepth2GetRectifiedImage` -- `sceDepth2Initialize` -- `sceDepth2InitializeVr` -- `sceDepth2QueryMemory` -- `sceDepth2SaveInternalBuffers` -- `sceDepth2SetCameraFactoryCalibrationData` -- `sceDepth2SetCommand` -- `sceDepth2SetRoi` -- `sceDepth2Submit` -- `sceDepth2Terminate` -- `sceDepth2WaitAndExecutePostProcess` diff --git a/docs/dev/debt/libSceSulpha.md b/docs/dev/debt/libSceSulpha.md deleted file mode 100644 index 36c6d64488..0000000000 --- a/docs/dev/debt/libSceSulpha.md +++ /dev/null @@ -1,16 +0,0 @@ -# libSceSulpha technical debt - -### Not implemented - -- `libSceSulpha` (PS5 system library, sysmodule id `0x0000009f`) is not implemented in AnyPS5. Only its entry in the sysmodule table exists; there is no `core/libs/prx/libSceSulpha` implementation and no host `.prx` is shipped. Titles that link it directly (observed with PPSA03332) fail to load with `libSceSulpha.prx: cannot open shared object file`. - -No public signatures found in the orbital NID database or online references; needs disassembly or hardware measurement. -Known public exports (from the aerolib NID database): -- `sceSulphaGetClientInfo` -- `sceSulphaGetConnectedClientCount` -- `sceSulphaGetDefaultConfig` -- `sceSulphaGetNeededMemory` -- `sceSulphaInit` -- `sceSulphaMessage` -- `sceSulphaSetBookmark` -- `sceSulphaShutdown` From e9cbbfb13ce4faa5e902aa921e5eaa149d4e5160 Mon Sep 17 00:00:00 2001 From: alosta Date: Tue, 20 Oct 2026 03:03:34 +0100 Subject: [PATCH 096/167] style(libs): space the libSceAt9Enc pointer parameters as elsewhere AI-assisted: yes --- core/libs/prx/libSceAt9Enc/Export.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAt9Enc/Export.cpp b/core/libs/prx/libSceAt9Enc/Export.cpp index e567ab6085..77fee077ec 100644 --- a/core/libs/prx/libSceAt9Enc/Export.cpp +++ b/core/libs/prx/libSceAt9Enc/Export.cpp @@ -11,7 +11,7 @@ int32_t APS5_VABI sceAt9EncClearContext(void *pAt9EncWorkArea, int32_t *pInterna return 0; } -int32_t APS5_VABI sceAt9EncCreateEncoder(void *pAt9EncWorkArea, void*pAt9EncParam, int32_t memSize, int32_t *pInternalResult) { +int32_t APS5_VABI sceAt9EncCreateEncoder(void *pAt9EncWorkArea, void *pAt9EncParam, int32_t memSize, int32_t *pInternalResult) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -26,7 +26,7 @@ int32_t APS5_VABI sceAt9EncFlush(void *pAt9EncWorkArea, void *pOutputBuf, uint32 return 0; } -int32_t APS5_VABI sceAt9EncQueryMemSize(void*pAt9EncParam, int32_t *pMemSize, int32_t *pInternalResult) { +int32_t APS5_VABI sceAt9EncQueryMemSize(void *pAt9EncParam, int32_t *pMemSize, int32_t *pInternalResult) { NotImplemented_nid_no_patch(__func__); return 0; } From 828a3582a3b3f94db5fe4a07512ba0c97a388e7e Mon Sep 17 00:00:00 2001 From: alosta Date: Tue, 20 Oct 2026 03:03:40 +0100 Subject: [PATCH 097/167] test(libs): cover the libSceAt9Enc and libSceM4aacEnc throwing stubs Register a guest test for each library and build it from the library's CMakeLists.txt, so the two libraries are built and registered like the other libraries that ship a CMakeLists.txt. The tests call every export and require the NotImplemented_nid_no_patch throw, so none of them can return a made-up success or write the caller's buffers. AI-assisted: yes --- core/libs/prx/libSceAt9Enc/CMakeLists.txt | 15 +++++ core/libs/prx/libSceM4aacEnc/CMakeLists.txt | 15 +++++ core/libs/tests/GuestAt9Enc.cpp | 42 +++++++++++++ core/libs/tests/GuestM4aacEnc.cpp | 66 +++++++++++++++++++++ 4 files changed, 138 insertions(+) create mode 100644 core/libs/prx/libSceAt9Enc/CMakeLists.txt create mode 100644 core/libs/prx/libSceM4aacEnc/CMakeLists.txt create mode 100644 core/libs/tests/GuestAt9Enc.cpp create mode 100644 core/libs/tests/GuestM4aacEnc.cpp diff --git a/core/libs/prx/libSceAt9Enc/CMakeLists.txt b/core/libs/prx/libSceAt9Enc/CMakeLists.txt new file mode 100644 index 0000000000..89880a7b92 --- /dev/null +++ b/core/libs/prx/libSceAt9Enc/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceAt9Enc SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceAt9Enc PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceAt9Enc) + +if(BUILD_TESTING) + add_test_executable(guest_at9_enc_tests ${LIBS_INCLUDE_DIR}/tests/GuestAt9Enc.cpp) + target_include_directories(guest_at9_enc_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_at9_enc_tests PRIVATE libSceAt9Enc libc) + configure_windows_unwind(guest_at9_enc_tests) + add_test(NAME guest_at9_enc COMMAND guest_at9_enc_tests) + if(WIN32) + set_tests_properties(guest_at9_enc PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceM4aacEnc/CMakeLists.txt b/core/libs/prx/libSceM4aacEnc/CMakeLists.txt new file mode 100644 index 0000000000..85a6f282a7 --- /dev/null +++ b/core/libs/prx/libSceM4aacEnc/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceM4aacEnc SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceM4aacEnc PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceM4aacEnc) + +if(BUILD_TESTING) + add_test_executable(guest_m4aac_enc_tests ${LIBS_INCLUDE_DIR}/tests/GuestM4aacEnc.cpp) + target_include_directories(guest_m4aac_enc_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_m4aac_enc_tests PRIVATE libSceM4aacEnc libc) + configure_windows_unwind(guest_m4aac_enc_tests) + add_test(NAME guest_m4aac_enc COMMAND guest_m4aac_enc_tests) + if(WIN32) + set_tests_properties(guest_m4aac_enc PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/tests/GuestAt9Enc.cpp b/core/libs/tests/GuestAt9Enc.cpp new file mode 100644 index 0000000000..2da60d41b3 --- /dev/null +++ b/core/libs/tests/GuestAt9Enc.cpp @@ -0,0 +1,42 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int32_t APS5_VABI sceAt9EncClearContext(void* pAt9EncWorkArea, int32_t* pInternalResult); +int32_t APS5_VABI sceAt9EncCreateEncoder(void* pAt9EncWorkArea, void* pAt9EncParam, int32_t memSize, int32_t* pInternalResult); +int32_t APS5_VABI sceAt9EncEncode(void* pAt9EncWorkArea, const void* pInputBuf, void* pOutputBuf, uint32_t inputBufSize, uint32_t outputBufSize, uint32_t* pOutputDataByte, uint32_t* pUsedBufSize, int32_t* pInternalResult); +int32_t APS5_VABI sceAt9EncFlush(void* pAt9EncWorkArea, void* pOutputBuf, uint32_t outputBufSize, uint32_t* pOutputDataByte, int32_t* pInternalResult); +int32_t APS5_VABI sceAt9EncQueryMemSize(void* pAt9EncParam, int32_t* pMemSize, int32_t* pInternalResult); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +template +bool Throws(Call call) { + try { + static_cast(call()); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +} + +int main() { + int32_t memSize = 0; + int32_t internalResult = 0; + + Require(Throws([&] { return sceAt9EncClearContext(nullptr, &internalResult); })); + Require(Throws([&] { return sceAt9EncCreateEncoder(nullptr, nullptr, 0, &internalResult); })); + Require(Throws([&] { return sceAt9EncEncode(nullptr, nullptr, nullptr, 0, 0, nullptr, nullptr, &internalResult); })); + Require(Throws([&] { return sceAt9EncFlush(nullptr, nullptr, 0, nullptr, &internalResult); })); + Require(Throws([&] { return sceAt9EncQueryMemSize(nullptr, &memSize, &internalResult); })); + + Require(memSize == 0); + Require(internalResult == 0); +} diff --git a/core/libs/tests/GuestM4aacEnc.cpp b/core/libs/tests/GuestM4aacEnc.cpp new file mode 100644 index 0000000000..d9f6f00888 --- /dev/null +++ b/core/libs/tests/GuestM4aacEnc.cpp @@ -0,0 +1,66 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int32_t APS5_VABI sceM4aacEncClearContext(int32_t m4aacEncHandle); +int APS5_VABI sceM4aacEncClearContextEx(); +int32_t APS5_VABI sceM4aacEncCreateEncoder(int32_t channels, int32_t samplingRate, int32_t bitRate, int32_t inputFormat, int32_t outputFormat, int32_t* pInternalError); +int APS5_VABI sceM4aacEncCreateEncoderEx(); +int32_t APS5_VABI sceM4aacEncDeleteEncoder(int32_t m4aacEncHandle, int32_t* pInternalError); +int APS5_VABI sceM4aacEncDeleteEncoderEx(); +int32_t APS5_VABI sceM4aacEncEncode(int32_t m4aacEncHandle, void* pInput, uint32_t inputByte, void* pOutput, uint32_t* pOutputByte, int32_t* pInternalError); +int APS5_VABI sceM4aacEncEncodeEx(); +int APS5_VABI sceM4aacEncEncodeWithPriority(); +int APS5_VABI sceM4aacEncEncodeWithPriorityEx(); +int32_t APS5_VABI sceM4aacEncFlush(int32_t m4aacEncHandle, void* pOutput, uint32_t* pOutputByte, int32_t* pInternalError); +int APS5_VABI sceM4aacEncFlushEx(); +int APS5_VABI sceM4aacEncFlushWithPriority(); +int APS5_VABI sceM4aacEncFlushWithPriorityEx(); +int APS5_VABI sceM4aacEncGetContext(); +int APS5_VABI sceM4aacEncInitialize(); +int APS5_VABI sceM4aacEncTerminate(); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +template +bool Throws(Call call) { + try { + static_cast(call()); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +} + +int main() { + uint32_t outputByte = 0; + int32_t internalError = 0; + + Require(Throws([&] { return sceM4aacEncClearContext(1); })); + Require(Throws([&] { return sceM4aacEncClearContextEx(); })); + Require(Throws([&] { return sceM4aacEncCreateEncoder(2, 48000, 128000, 0, 0, &internalError); })); + Require(Throws([&] { return sceM4aacEncCreateEncoderEx(); })); + Require(Throws([&] { return sceM4aacEncDeleteEncoder(1, &internalError); })); + Require(Throws([&] { return sceM4aacEncDeleteEncoderEx(); })); + Require(Throws([&] { return sceM4aacEncEncode(1, nullptr, 0, nullptr, &outputByte, &internalError); })); + Require(Throws([&] { return sceM4aacEncEncodeEx(); })); + Require(Throws([&] { return sceM4aacEncEncodeWithPriority(); })); + Require(Throws([&] { return sceM4aacEncEncodeWithPriorityEx(); })); + Require(Throws([&] { return sceM4aacEncFlush(1, nullptr, &outputByte, &internalError); })); + Require(Throws([&] { return sceM4aacEncFlushEx(); })); + Require(Throws([&] { return sceM4aacEncFlushWithPriority(); })); + Require(Throws([&] { return sceM4aacEncFlushWithPriorityEx(); })); + Require(Throws([&] { return sceM4aacEncGetContext(); })); + Require(Throws([&] { return sceM4aacEncInitialize(); })); + Require(Throws([&] { return sceM4aacEncTerminate(); })); + + Require(outputByte == 0); + Require(internalError == 0); +} From 76bbf68f7010f1835da891140e329749520943a5 Mon Sep 17 00:00:00 2001 From: alosta Date: Tue, 20 Oct 2026 03:03:45 +0100 Subject: [PATCH 098/167] docs(debt): record the remaining stubs under Unknown function info The debt entries only had the four index sections before; use Unknown function info for the imports that fail a title to load and for the signatures Orbital or the aerolib NID list do not give. Note that sceSystemServiceShowDisplaySafeAreaSettings takes its (void) signature from the sibling sceSystemServiceShowControllerSettings, not a reference. AI-assisted: yes --- docs/dev/debt/libSceAt9Enc.md | 16 +++----------- docs/dev/debt/libSceM4aacEnc.md | 32 +++------------------------- docs/dev/debt/libSceSystemService.md | 3 ++- 3 files changed, 8 insertions(+), 43 deletions(-) diff --git a/docs/dev/debt/libSceAt9Enc.md b/docs/dev/debt/libSceAt9Enc.md index 0acf307238..0ceb0e614c 100644 --- a/docs/dev/debt/libSceAt9Enc.md +++ b/docs/dev/debt/libSceAt9Enc.md @@ -1,16 +1,6 @@ # libSceAt9Enc technical debt -### Not implemented +### Unknown function info -- [libSceAt9Enc](../../../core/libs/prx/libSceAt9Enc/Export.cpp) (PS5 system library; name suggests ATRAC9 encoder but unconfirmed; sysmodule id `0x000000d1`) is not implemented in AnyPS5. Only its entry in the sysmodule table exists; there is no `core/libs/prx/libSceAt9Enc` implementation and no host `.prx` is shipped. Titles that link it directly (observed with PPSA03332) fail to load with `libSceAt9Enc.prx: cannot open shared object file`. - -### Related unimplemented system libraries - -The same load-time failure affects other firmware system libraries that are listed in the sysmodule table but have no implementation or shipped host `.prx` in AnyPS5, observed while running PPSA03332: `libSceAudiodecCpu`, `libSceAudiodecCpuHevag`, `libSceM4aacEnc`, `libSceSulpha`, `libSceCamera`, `libSceDepth2`. -## Signatures (orbital db_types.json) - -- `int32_t sceAt9EncClearContext(void *pAt9EncWorkArea, int32_t *pInternalResult)` -- `int32_t sceAt9EncCreateEncoder(void *pAt9EncWorkArea, struct *pAt9EncParam, int32_t memSize, int32_t *pInternalResult)` -- `int32_t sceAt9EncEncode(void *pAt9EncWorkArea, const void *pInputBuf, void *pOutputBuf, uint32_t inputBufSize, uint32_t outputBufSize, uint32_t *pOutputDataByte, uint32_t *pUsedBufSize, int32_t *pInternalResult)` -- `int32_t sceAt9EncFlush(void *pAt9EncWorkArea, void *pOutputBuf, uint32_t outputBufSize, uint32_t *pOutputDataByte, int32_t *pInternalResult)` -- `int32_t sceAt9EncQueryMemSize(struct *pAt9EncParam, int32_t *pMemSize, int32_t *pInternalResult)` +- [libSceAt9Enc](../../../core/libs/prx/libSceAt9Enc/Export.cpp) (libSceAt9Enc, sysmodule id `0x000000d1`) has no encoder, so all five exports throw: `sceAt9EncClearContext`, `sceAt9EncCreateEncoder`, `sceAt9EncEncode`, `sceAt9EncFlush` and `sceAt9EncQueryMemSize`. PPSA03332 imports them and fails to load (`libSceAt9Enc.prx: cannot open shared object file`) when the library is absent. The names are from the aerolib NID list, and the name reads as the ATRAC9 encoder, unconfirmed +- [sceAt9Enc*](../../../core/libs/prx/libSceAt9Enc/Export.cpp) (libSceAt9Enc) - the signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json), which gives `int32_t sceAt9EncClearContext(void *pAt9EncWorkArea, int32_t *pInternalResult)`, `int32_t sceAt9EncCreateEncoder(void *pAt9EncWorkArea, struct *pAt9EncParam, int32_t memSize, int32_t *pInternalResult)`, `int32_t sceAt9EncEncode(void *pAt9EncWorkArea, const void *pInputBuf, void *pOutputBuf, uint32_t inputBufSize, uint32_t outputBufSize, uint32_t *pOutputDataByte, uint32_t *pUsedBufSize, int32_t *pInternalResult)`, `int32_t sceAt9EncFlush(void *pAt9EncWorkArea, void *pOutputBuf, uint32_t outputBufSize, uint32_t *pOutputDataByte, int32_t *pInternalResult)` and `int32_t sceAt9EncQueryMemSize(struct *pAt9EncParam, int32_t *pMemSize, int32_t *pInternalResult)`. Its `struct *pAt9EncParam` is a placeholder, so that parameter's layout is unknown diff --git a/docs/dev/debt/libSceM4aacEnc.md b/docs/dev/debt/libSceM4aacEnc.md index dbf74bdba4..fbb1f5292f 100644 --- a/docs/dev/debt/libSceM4aacEnc.md +++ b/docs/dev/debt/libSceM4aacEnc.md @@ -1,32 +1,6 @@ # libSceM4aacEnc technical debt -### Not implemented +### Unknown function info -- `libSceM4aacEnc` (PS5 system library, sysmodule id `0x000000bc`) is not implemented in AnyPS5. Only its entry in the sysmodule table exists; there is no `core/libs/prx/libSceM4aacEnc` implementation and no host `.prx` is shipped. Titles that link it directly (observed with PPSA03332) fail to load with `libSceM4aacEnc.prx: cannot open shared object file`. - -Known public exports (from the aerolib NID database): -- `sceM4aacEncClearContext` -- `sceM4aacEncClearContextEx` -- `sceM4aacEncCreateEncoder` -- `sceM4aacEncCreateEncoderEx` -- `sceM4aacEncDeleteEncoder` -- `sceM4aacEncDeleteEncoderEx` -- `sceM4aacEncEncode` -- `sceM4aacEncEncodeEx` -- `sceM4aacEncEncodeWithPriority` -- `sceM4aacEncEncodeWithPriorityEx` -- `sceM4aacEncFlush` -- `sceM4aacEncFlushEx` -- `sceM4aacEncFlushWithPriority` -- `sceM4aacEncFlushWithPriorityEx` -- `sceM4aacEncGetContext` -- `sceM4aacEncInitialize` -- `sceM4aacEncTerminate` - -## Signatures (orbital db_types.json) - -- `int32_t sceM4aacEncClearContext(int32_t m4aacEncHandle)` -- `int32_t sceM4aacEncCreateEncoder(int32_t channels, int32_t samplingRate, int32_t bitRate, int32_t inputFormat, int32_t outputFormat, int32_t *pInternalError)` -- `int32_t sceM4aacEncDeleteEncoder(int32_t m4aacEncHandle, int32_t *pInternalError)` -- `int32_t sceM4aacEncEncode(int32_t m4aacEncHandle, void *pInput, uint32_t inputByte, void *pOutput, uint32_t *pOutputByte, int32_t *pInternalError)` -- `int32_t sceM4aacEncFlush(int32_t m4aacEncHandle, void *pOutput, uint32_t *pOutputByte, int32_t *pInternalError)` +- [libSceM4aacEnc](../../../core/libs/prx/libSceM4aacEnc/Export.cpp) (libSceM4aacEnc, sysmodule id `0x000000bc`) has no encoder, so all seventeen exports throw. PPSA03332 imports them and fails to load (`libSceM4aacEnc.prx: cannot open shared object file`) when the library is absent. The names are from the aerolib NID list: `sceM4aacEncClearContext`, `sceM4aacEncClearContextEx`, `sceM4aacEncCreateEncoder`, `sceM4aacEncCreateEncoderEx`, `sceM4aacEncDeleteEncoder`, `sceM4aacEncDeleteEncoderEx`, `sceM4aacEncEncode`, `sceM4aacEncEncodeEx`, `sceM4aacEncEncodeWithPriority`, `sceM4aacEncEncodeWithPriorityEx`, `sceM4aacEncFlush`, `sceM4aacEncFlushEx`, `sceM4aacEncFlushWithPriority`, `sceM4aacEncFlushWithPriorityEx`, `sceM4aacEncGetContext`, `sceM4aacEncInitialize` and `sceM4aacEncTerminate` +- [sceM4aacEnc*](../../../core/libs/prx/libSceM4aacEnc/Export.cpp) (libSceM4aacEnc) - five signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json): `int32_t sceM4aacEncClearContext(int32_t m4aacEncHandle)`, `int32_t sceM4aacEncCreateEncoder(int32_t channels, int32_t samplingRate, int32_t bitRate, int32_t inputFormat, int32_t outputFormat, int32_t *pInternalError)`, `int32_t sceM4aacEncDeleteEncoder(int32_t m4aacEncHandle, int32_t *pInternalError)`, `int32_t sceM4aacEncEncode(int32_t m4aacEncHandle, void *pInput, uint32_t inputByte, void *pOutput, uint32_t *pOutputByte, int32_t *pInternalError)` and `int32_t sceM4aacEncFlush(int32_t m4aacEncHandle, void *pOutput, uint32_t *pOutputByte, int32_t *pInternalError)`. Orbital has no entry for the other twelve, which are declared without parameters, so their signatures are unknown diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index badd7b6541..7ba6b3cc2c 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -5,7 +5,8 @@ - [libSceSystemService](../../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick`, `sceSystemServiceReportAbnormalTermination`, `sceSystemServiceDisableMusicPlayer`, `sceSystemServiceReenableMusicPlayer`, `sceSystemServiceDisableMediaPlay` and `sceSystemServiceReenableMediaPlay` return success and do nothing: the host has no auto power-off timer to reset, no system crash reporter to notify, no system music player to pause or resume and no system media playback to disable. PPSA01521 calls `sceSystemServiceDisableMediaPlay` during start-up, and its wrappers for both pass no arguments - [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 launches no web browser, so the call fails and nothing is shown. RommPS PPSA76677 then tells the player to scan its QR code instead - [sceSystemServiceShowControllerSettings](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without showing the controller settings: AnyPS5 has no system UI to open. Cat Quest III (PPSA15065) imports it -- [sceSystemServiceShowDisplaySafeAreaSettings](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without showing the display safe area settings dialog: AnyPS5 has no system UI to open. Observed with PPSA03332- [sceSystemServiceOpenChallengeActivity](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without opening the challenge activity: AnyPS5 has no system UI to open. Cat Quest III (PPSA15065) imports it +- [sceSystemServiceShowDisplaySafeAreaSettings](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without showing the display safe area settings dialog: AnyPS5 has no system UI to open. PPSA03332 imports it, and the no-argument signature is assumed: [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) has `sceSystemServiceShowControllerSettings` but not this call +- [sceSystemServiceOpenChallengeActivity](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without opening the challenge activity: AnyPS5 has no system UI to open. Cat Quest III (PPSA15065) imports it ### Unknown function info From 93a2bcba576d691410d245d092842b567ca5d976 Mon Sep 17 00:00:00 2001 From: alosta Date: Tue, 20 Oct 2026 03:04:02 +0100 Subject: [PATCH 099/167] test(libSceSystemService): cover the display safe area settings stub sceSystemServiceShowDisplaySafeAreaSettings returns success without showing anything, so check the result through the existing guest test. AI-assisted: yes --- core/libs/tests/GuestSystemService.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestSystemService.cpp b/core/libs/tests/GuestSystemService.cpp index 1ac756fba3..28df762dcd 100644 --- a/core/libs/tests/GuestSystemService.cpp +++ b/core/libs/tests/GuestSystemService.cpp @@ -55,6 +55,7 @@ extern "C" int APS5_VABI sceSystemServiceDisableMediaPlay(void); extern "C" int APS5_VABI sceSystemServiceReenableMediaPlay(void); extern "C" int APS5_VABI sceSystemServiceLaunchWebBrowser(const char* uri, void* param); extern "C" int APS5_VABI sceSystemServiceShowControllerSettings(void); +extern "C" int APS5_VABI sceSystemServiceShowDisplaySafeAreaSettings(void); extern "C" int APS5_VABI sceSystemServiceOpenChallengeActivity(void); extern "C" int APS5_VABI sceSystemServiceGetStatus(SystemServiceStatus* status); extern "C" int APS5_VABI sceSystemServiceGetDisplaySafeAreaInfo(SystemServiceDisplaySafeAreaInfo* info); @@ -79,6 +80,7 @@ int main() { Require(sceSystemServiceReenableMediaPlay() == SYSTEM_SERVICE_OK); Require(sceSystemServiceReenableMediaPlay() == SYSTEM_SERVICE_OK); Require(sceSystemServiceShowControllerSettings() == SYSTEM_SERVICE_OK); + Require(sceSystemServiceShowDisplaySafeAreaSettings() == SYSTEM_SERVICE_OK); Require(sceSystemServiceOpenChallengeActivity() == SYSTEM_SERVICE_OK); Require(sceSystemServiceLaunchWebBrowser("http://127.0.0.1:8780/video?v=0", nullptr) == browserUnavailable); unsigned char browserParam[64]; From 5c1eba11af123fd0e30168020aab62dda0391c4e Mon Sep 17 00:00:00 2001 From: blu <131330772+ycbuzi@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:23:35 -0700 Subject: [PATCH 100/167] fix(libc): honour the rounding mode in rint and rintf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MathExact.cpp was built without -frounding-math, so GCC compiled std::rint as an add and subtract of 2^52 on the magnitude, which is exact only when rounding to nearest: under FE_DOWNWARD rint(-1.5) returned -1 and rintf(-1.5f) -1, where C and FreeBSD's libm give -2. lrint and llrint were right: they call the host libm's llrint, which honours the mode. MathExact.cpp now builds with -frounding-math. GuestMathExact checks rint, rintf, rintl, lrint and llrint of -1.5 and 1.5 in all four rounding modes and that the mode is unchanged. The failure was first caught by the rounding test of boykopovar/AnyPS5#2235 (Привалов Александр Владимирович). Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/CMakeLists.txt | 3 +++ core/libs/tests/GuestMathExact.cpp | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index db26a5c925..2e9300dddd 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -86,6 +86,9 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/exception/Exports.cpp ) +set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/src/MathExact.cpp + TARGET_DIRECTORY libc PROPERTIES COMPILE_OPTIONS "-frounding-math") + target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/BinaryWordIo.cpp) target_include_directories(libc PRIVATE diff --git a/core/libs/tests/GuestMathExact.cpp b/core/libs/tests/GuestMathExact.cpp index b3afbca1ac..e601e88973 100644 --- a/core/libs/tests/GuestMathExact.cpp +++ b/core/libs/tests/GuestMathExact.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include @@ -22,6 +23,10 @@ long double APS5_VABI remainderl_nid_postfix(long double, long double); double APS5_VABI drem_nid_postfix(double, double); float APS5_VABI dremf_nid_postfix(float, float); std::int64_t APS5_VABI lrint_nid_postfix(double); +double APS5_VABI rint_nid_postfix(double); +float APS5_VABI rintf_nid_postfix(float); +long double APS5_VABI rintl_nid_postfix(long double); +long long APS5_VABI llrint_nid_postfix(double); int APS5_VABI finite_nid_postfix(double); int APS5_VABI __fpclassifyl_nid_postfix(long double); int APS5_VABI __isnanl_nid_postfix(long double); @@ -96,6 +101,25 @@ static void CheckConsoleQuirks() { Require(finite_nid_postfix(1.0) == 1 && finite_nid_postfix(INFINITY) == 0); } +static void CheckRoundingModes() { + struct Case { + int mode; + double negative; + double positive; + }; + const Case cases[] = {{FE_TONEAREST, -2.0, 2.0}, {FE_DOWNWARD, -2.0, 1.0}, {FE_UPWARD, -1.0, 2.0}, {FE_TOWARDZERO, -1.0, 1.0}}; + const int saved = std::fegetround(); + for (const auto& test : cases) { + Require(std::fesetround(test.mode) == 0); + Require(rint_nid_postfix(-1.5) == test.negative && rint_nid_postfix(1.5) == test.positive); + Require(rintf_nid_postfix(-1.5f) == static_cast(test.negative) && rintf_nid_postfix(1.5f) == static_cast(test.positive)); + Require(rintl_nid_postfix(-1.5L) == test.negative && rintl_nid_postfix(1.5L) == test.positive); + Require(lrint_nid_postfix(-1.5) == static_cast(test.negative) && llrint_nid_postfix(1.5) == static_cast(test.positive)); + Require(std::fegetround() == test.mode); + } + Require(std::fesetround(saved) == 0); +} + static void CheckClassification() { const long double pseudoDenormal = Extended(0x0000, 0x8000000000000000ull); Require(_LDtest_nid_postfix(&pseudoDenormal) == -1 && __fpclassifyl_nid_postfix(pseudoDenormal) == -2); @@ -112,6 +136,7 @@ int main() { CheckSignalingNanPassThrough(); CheckErrno(); CheckConsoleQuirks(); + CheckRoundingModes(); CheckClassification(); return 0; } From eb78be80366816b800cad41a7fc9ec4e1431b17e Mon Sep 17 00:00:00 2001 From: primesoul-web Date: Tue, 20 Oct 2026 07:24:09 +0000 Subject: [PATCH 101/167] fix(libSceIme): drop the filtered commit when the session changed while the filter ran --- core/libs/prx/libSceIme/Export.cpp | 7 ++++++- core/libs/tests/GuestImeOpen.cpp | 24 ++++++++++++++++++++++++ docs/dev/debt/libSceIme.md | 6 +++--- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index b2d0f2a4a6..ea355e802e 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -66,6 +66,7 @@ struct Session { std::deque events; std::deque hostInputs; std::uint64_t id = 0; + std::uint64_t revision = 0; }; struct DeferredFilter { @@ -478,6 +479,7 @@ int APS5_VABI sceImeSetCaret(const Caret* caret) { if (!caret) return ErrorInvalidAddress; if (caret->index > g_session->text.size()) return ErrorInvalidParam; g_session->caret = caret->index; + ++g_session->revision; return 0; } @@ -495,6 +497,7 @@ int APS5_VABI sceImeSetText(const char16_t* text, uint32_t length) { const std::size_t count = TextLength(text, std::min(length, session.param.max_text_length)); if (!ValidText(text, count, (session.param.option & OptionMultiline) != 0)) return ErrorInvalidText; session.text.assign(text, count); + ++session.revision; if (session.caret > session.text.size()) session.caret = static_cast(session.text.size()); SyncBuffers(session); return 0; @@ -521,6 +524,7 @@ int APS5_VABI sceImeUpdate(EventHandler handler) { std::uint32_t filterMaxLength = 0; bool filterMultiline = false; std::uint64_t sessionId = 0; + std::uint64_t sessionRevision = 0; { std::lock_guard lock(g_imeMutex); if (!g_session || g_session->param.handler != handler) return 0; @@ -535,6 +539,7 @@ int APS5_VABI sceImeUpdate(EventHandler handler) { filterMaxLength = g_session->param.max_text_length; filterMultiline = (g_session->param.option & OptionMultiline) != 0; sessionId = g_session->id; + sessionRevision = g_session->revision; } } else if (!g_session->events.empty()) { event = g_session->events.front(); @@ -549,7 +554,7 @@ int APS5_VABI sceImeUpdate(EventHandler handler) { std::u16string filtered; const bool accepted = RunTextFilter(filter, filterText, filterMaxLength, filterMultiline, &filtered); std::lock_guard lock(g_imeMutex); - if (g_session && g_session->id == sessionId) { + if (g_session && g_session->id == sessionId && g_session->revision == sessionRevision) { CommitFilteredEdit(*g_session, accepted ? &filtered : nullptr, filterOldText); } continue; diff --git a/core/libs/tests/GuestImeOpen.cpp b/core/libs/tests/GuestImeOpen.cpp index 1437400528..a1ce465687 100644 --- a/core/libs/tests/GuestImeOpen.cpp +++ b/core/libs/tests/GuestImeOpen.cpp @@ -126,6 +126,7 @@ int APS5_VABI ExtNoop(const ImeKeycode*, std::uint16_t*, std::uint32_t*, void*) } bool g_sawCaptureInsideFilter = false; +int g_setInsideRc = -1; int32_t APS5_VABI LockingFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { g_sawCaptureInsideFilter = ImeCapturesKeyboard_nid_postfix(); @@ -136,6 +137,15 @@ int32_t APS5_VABI LockingFilter(char16_t* outText, uint32_t* outTextLength, cons return 0; } +int32_t APS5_VABI SetInsideFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + g_setInsideRc = sceImeSetText(u"zz", 2); + for (uint32_t i = 0; i < sourceTextLength; ++i) { + outText[i] = sourceText[i]; + } + *outTextLength = sourceTextLength; + return 0; +} + void Require(bool value, const char* message) { if (value) return; std::fprintf(stderr, "%s\n", message); @@ -470,6 +480,20 @@ void CheckTextFilter() { RequireText(Drain().at(0), EventUpdateText, u"c", 1, 0, 1); Require(g_sawCaptureInsideFilter, "the filter can call back into sceIme*"); Require(sceImeClose_nid_postfix() == 0, "close the locking session"); + + buffers = Buffers{}; + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = reinterpret_cast(&SetInsideFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a filter that sets the text"); + Drain(); + g_setInsideRc = -1; + Publish(ImeHostInputKind::Text, "c"); + Require(Drain().empty(), "an edit whose filter set the text sends no event"); + Require(g_setInsideRc == 0, "sceImeSetText inside the filter returns 0"); + Require(std::u16string(buffers.text) == u"zz", "the setText inside the filter wins"); + Require(std::u16string(reinterpret_cast(buffers.work)) == u"zz", "the setText inside the filter wins in the work copy"); + Require(sceImeClose_nid_postfix() == 0, "close the set-inside session"); } void CheckHostEvents() { diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 0b1ac53637..bcce37a189 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -2,7 +2,7 @@ ### Silent stubs -- [sceImeOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) shows no panel: the text is typed on the host keyboard, which [ImeTextInput](../../../core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp) forwards from the game window (SDL text input, Backspace, Delete, Left, Right, Home, End, Enter and Escape), and the controller cannot type. While a panel is open, key presses do not reach the keyboard-to-pad bindings of [PadInput](../../../core/libs/prx/libSceVideoOut/src/PadInput.cpp), as the console's panel takes the controller; key releases still do. Minecraft Preview (PPSA19639, #1277) opens it when entering a world +- [sceImeOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) shows no panel: the text is typed on the host keyboard, which [ImeTextInput](../../../core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp) forwards from the game window (SDL text input, Backspace, Delete, Left, Right, Home, End, Enter and Escape), and the controller cannot type. While a panel is open, key presses do not reach the keyboard-to-pad bindings of [PadInput](../../../core/libs/prx/libSceVideoOut/src/PadInput.cpp), as the console's panel takes the controller; key releases still do. Minecraft Preview (PPSA19639, #1277) opens it when entering a world. The external keyboard filter is accepted and never called because no keycode events are produced ### Unknown function info @@ -55,7 +55,7 @@ ### Functional -- [sceImeKeyboardOpen, sceImeKeyboardClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - opening a keyboard that is already open for the same user returns `0x80bc0001` (busy) and closing one that is not open returns `0x80bc0002`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/ime.cpp) does; it tracks one keyboard for all users, while the open state is kept per user id here, and the PS5 result is unverified +- [sceImeKeyboardOpen, sceImeKeyboardClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - opening a keyboard that is already open for the same user returns `0x80bc0001` (busy) and closing one that is not open returns `0x80bc0002`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/1211958b4eb38256cc0c5c7ba92d668820c05389/src/libs/ime.cpp) does; it tracks one keyboard for all users, while the open state is kept per user id here, and the PS5 result is unverified - [sceImeKeyboardGetInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. - [sceImeKeyboardGetResourceId](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard -- [sceImeOpen text filter](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) runs on the text edits of host input - typing, backspace, forward delete and a multiline Enter - following [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/1211958b4eb38256cc0c5c7ba92d668820c05389/src/libs/ime.cpp), not confirmed on a PS5. The host input thread only queues the edit under the IME mutex; `sceImeUpdate` applies it on the title's thread, where the filter also runs with the mutex released - like the event handler, so a filter that calls back into `sceIme*` cannot deadlock - and the structural edit and the filtered commit are serialized under the mutex, with a commit after the session closed or reopened dropped by a session id. The filter is guest code and `ImeTextFilter` and `ImeExtKeyboardFilter` are declared `APS5_VABI` like `EventHandler`. The filter is called with the edited text, a buffer of `maxTextLength` UTF-16 units and its length; a nonzero return, an output longer than the capacity, an unpaired surrogate or, without the multiline option, a line break keeps the edit unfiltered. An accepted output replaces the text, the caret stays where the edit put it and is clamped to the text and normalized past a surrogate pair, and one UpdateText is sent with the minimal edit delta of the whole old and new text, as KytyPS5 computes it; an output equal to the previous text sends no event. Caret moves, PressEnter without multiline, PressClose, `sceImeSetText` and `sceImeSetCaret` do not run the filter, as in KytyPS5. An external keyboard filter is accepted and stored on the session: it is called only for keyboard events that carry key data, which the host input path does not produce - it delivers text and actions, as KytyPS5's host API queues external inputs with a zeroed key and skips the filter for them - so it is never called +- [sceImeOpen text filter](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) runs on the text edits of host input - typing, backspace, forward delete and a multiline Enter - following [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/1211958b4eb38256cc0c5c7ba92d668820c05389/src/libs/ime.cpp), not confirmed on a PS5. The host input thread only queues the edit under the IME mutex; `sceImeUpdate` applies it on the title's thread, where the filter also runs with the mutex released - like the event handler, so a filter that calls back into `sceIme*` cannot deadlock - and the structural edit and the filtered commit are serialized under the mutex, with a commit after the session closed or reopened dropped by a session id. The filter is guest code and `ImeTextFilter` and `ImeExtKeyboardFilter` are declared `APS5_VABI` like `EventHandler`. The filter is called with the edited text, a buffer of `maxTextLength` UTF-16 units and its length; a nonzero return, an output longer than the capacity, an unpaired surrogate or, without the multiline option, a line break keeps the edit unfiltered. An accepted output replaces the text, the caret stays where the edit put it and is clamped to the text and normalized past a surrogate pair, and one UpdateText is sent with the minimal edit delta of the whole old and new text, as KytyPS5 computes it; an output equal to the previous text sends no event. Caret moves, PressEnter without multiline, PressClose, `sceImeSetText` and `sceImeSetCaret` do not run the filter, as in KytyPS5; a `sceImeSetText` or `sceImeSetCaret` that lands while the filter runs bumps a revision beside the session id, and the filtered commit is dropped when either changed, as KytyPS5 drops the commit when its revision changed, so the title's own text or caret setting wins and the edit sends no event. An external keyboard filter is accepted and stored on the session: it is called only for keyboard events that carry key data, which the host input path does not produce - it delivers text and actions, as KytyPS5's host API queues external inputs with a zeroed key and skips the filter for them - so it is never called From 36b30824e0ca7c1351ee458e7fd7565d8891a74c Mon Sep 17 00:00:00 2001 From: thuanquang Date: Tue, 20 Oct 2026 02:15:49 +0700 Subject: [PATCH 102/167] test(shader): check that s_subvector_loop_begin tests an empty exec mask with one compare agc_spirv_subvector_loop_begin recompiles the exec-kept program of agc_driver_subvector_loop at wave64 in both robustBufferAccess2 modes and requires exactly one branch on (exec_lo | exec_hi) == 0 and none on exec_lo == 0 && exec_hi == 0. It needs no GPU, so CI fails if the begin instruction goes back to two compares. The program moves to SubvectorLoopFixture.hpp so both tests use one copy. Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 13 ++ .../tests/SpirvSubvectorLoopBegin.cpp | 172 ++++++++++++++++++ .../tests/SubvectorLoopFixture.hpp | 21 +++ .../tests/execution/SubvectorLoop.cpp | 12 +- 4 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/SpirvSubvectorLoopBegin.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/SubvectorLoopFixture.hpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index fbd9b9d9c6..603ce15bc1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -817,6 +817,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_spirv_subvector_loop_begin_tests EXCLUDE_FROM_ALL tests/SpirvSubvectorLoopBegin.cpp) +target_link_libraries(agc_spirv_subvector_loop_begin_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_spirv_subvector_loop_begin_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_spirv_subvector_loop_begin_tests) +if(BUILD_TESTING) + set_target_properties(agc_spirv_subvector_loop_begin_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_spirv_subvector_loop_begin COMMAND agc_spirv_subvector_loop_begin_tests) + set_tests_properties(agc_spirv_subvector_loop_begin PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_spirv_subvector_loop_begin PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_layer_input_tests EXCLUDE_FROM_ALL tests/LayerInput.cpp) target_link_libraries(agc_layer_input_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_layer_input_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvSubvectorLoopBegin.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvSubvectorLoopBegin.cpp new file mode 100644 index 0000000000..b0d590b7d7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvSubvectorLoopBegin.cpp @@ -0,0 +1,172 @@ +#define SPV_ENABLE_UTILITY_CODE +#include +#undef SPV_ENABLE_UTILITY_CODE +#include "Recompiler.hpp" +#include "SubvectorLoopFixture.hpp" +#include +#include +#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); +} + +std::vector recompile(bool robustBufferAccess2) { + const std::array userData{0x10000000u, 0u, 0x100u, 0x31016facu, 0x10001000u, 0u, 0x900u, 0x31016facu}; + const std::array extensions{"SPV_KHR_storage_buffer_storage_class"}; + const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, SubvectorLoopFixture::ExecKeptCode, 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 = capabilities; + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.target.robustBufferAccess2 = robustBufferAccess2; + 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(); +} + +enum class EmptyExecTest { + Other, + OneCompare, + TwoCompares +}; + +class BranchConditions { +public: + explicit BranchConditions(std::vector module) : words(std::move(module)) { + require(words.size() >= 5, "the SPIR-V header is missing"); + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + require(count != 0 && count <= words.size() - offset, "a SPIR-V instruction is truncated"); + const auto opcode = static_cast(words[offset] & spv::OpCodeMask); + bool result = false; + bool type = false; + spv::HasResultAndType(opcode, &result, &type); + if (result) definitions.emplace(words[offset + (type ? 2 : 1)], offset); + if (opcode == spv::OpBranchConditional) conditions.push_back(words[offset + 1]); + offset += count; + } + } + + std::vector Tests() const { + std::vector tests; + for (const auto condition : conditions) tests.push_back(classify(condition)); + return tests; + } + +private: + std::vector words; + std::vector conditions; + std::map definitions; + + std::span instruction(std::uint32_t id, spv::Op opcode, std::size_t count) const { + const auto found = definitions.find(id); + if (found == definitions.end()) return {}; + const auto offset = found->second; + if ((words[offset] & spv::OpCodeMask) != static_cast(opcode) || (words[offset] >> spv::WordCountShift) != count) return {}; + return std::span(words).subspan(offset, count); + } + + bool zero(std::uint32_t id) const { + const auto constant = instruction(id, spv::OpConstant, 4); + return !constant.empty() && constant[3] == 0u; + } + + std::optional comparedWithZero(std::uint32_t id) const { + const auto compare = instruction(id, spv::OpIEqual, 5); + if (compare.empty()) return std::nullopt; + if (zero(compare[4])) return compare[3]; + if (zero(compare[3])) return compare[4]; + return std::nullopt; + } + + std::optional> execWord(std::uint32_t id) const { + const auto extract = instruction(id, spv::OpCompositeExtract, 5); + if (extract.empty() || instruction(extract[3], spv::OpGroupNonUniformBallot, 5).empty()) return std::nullopt; + return std::pair{extract[3], extract[4]}; + } + + bool execWords(std::uint32_t first, std::uint32_t second) const { + const auto low = execWord(first); + const auto high = execWord(second); + return low && high && low->first == high->first && low->second <= 1u && high->second <= 1u && low->second != high->second; + } + + EmptyExecTest classify(std::uint32_t condition) const { + if (const auto negation = instruction(condition, spv::OpLogicalNot, 4); !negation.empty()) return classify(negation[3]); + if (const auto operand = comparedWithZero(condition)) { + const auto combined = instruction(*operand, spv::OpBitwiseOr, 5); + if (!combined.empty() && execWords(combined[3], combined[4])) return EmptyExecTest::OneCompare; + } + const auto both = instruction(condition, spv::OpLogicalAnd, 5); + if (!both.empty()) { + const auto first = comparedWithZero(both[3]); + const auto second = comparedWithZero(both[4]); + if (first && second && execWords(*first, *second)) return EmptyExecTest::TwoCompares; + } + return EmptyExecTest::Other; + } +}; + +int failures = 0; + +void fail(const std::string& message) { + std::fprintf(stderr, "SpirvSubvectorLoopBegin: %s\n", message.c_str()); + ++failures; +} + +void check(bool robustBufferAccess2) { + const std::string mode = robustBufferAccess2 ? "robustBufferAccess2 on" : "robustBufferAccess2 off"; + std::uint32_t oneCompare = 0; + std::uint32_t twoCompares = 0; + for (const auto test : BranchConditions(recompile(robustBufferAccess2)).Tests()) { + if (test == EmptyExecTest::OneCompare) ++oneCompare; + if (test == EmptyExecTest::TwoCompares) ++twoCompares; + } + if (twoCompares != 0u) { + fail(mode + ": branches that compare exec_lo and exec_hi with 0 separately: " + std::to_string(twoCompares) + ", expected 0"); + } + if (oneCompare != 1u) { + fail(mode + ": branches that compare (exec_lo | exec_hi) with 0: " + std::to_string(oneCompare) + ", expected 1 (s_subvector_loop_begin)"); + } +} + +} + +int main() { + try { + check(false); + check(true); + } catch (const std::exception& error) { + fail(error.what()); + } + if (failures != 0) return 1; + std::puts("SpirvSubvectorLoopBegin: s_subvector_loop_begin tests an empty exec mask as (exec_lo | exec_hi) == 0 in both robustBufferAccess2 modes"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/SubvectorLoopFixture.hpp b/core/libs/prx/libSceAgcDriver/tests/SubvectorLoopFixture.hpp new file mode 100644 index 0000000000..2438f79b12 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SubvectorLoopFixture.hpp @@ -0,0 +1,21 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SUBVECTORLOOPFIXTURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SUBVECTORLOOPFIXTURE_HPP + +#include +#include + +namespace SubvectorLoopFixture { + +alignas(256) constexpr std::array ExecKeptCode{ + 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, + 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000a, 0x80148114, 0x4a141481, + 0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850001, 0xbe26fff6, + 0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000, 0x80010a01, + 0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400, 0x80010e01, + 0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800, 0x80011201, + 0xbf810000, +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp index 4944237df7..bb378bac87 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "../SubvectorLoopFixture.hpp" #include "Recompiler.hpp" #include "VulkanTestDevice.hpp" #include @@ -17,6 +18,7 @@ namespace { using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; +using SubvectorLoopFixture::ExecKeptCode; constexpr std::uint32_t Threads = 64; constexpr std::uint32_t Columns = 9; @@ -25,16 +27,6 @@ constexpr std::uint32_t Marker = 0x5a5a5a5au; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array ExecKeptCode{ - 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, - 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000a, 0x80148114, 0x4a141481, - 0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850001, 0xbe26fff6, - 0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000, 0x80010a01, - 0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400, 0x80010e01, - 0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800, 0x80011201, - 0xbf810000, -}; - alignas(256) constexpr std::array ExecResetCode{ 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000b, 0x80148114, 0x4a141481, From a8f47b33221f4e4b4ac8bfe01839ee407208e990 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Tue, 20 Oct 2026 14:52:11 +0700 Subject: [PATCH 103/167] test(agc): remove the large cache file test directory when the test fails runLargeFileWrite writes a 2 GiB pipelines.bin into a randomly named temporary directory, and only the passing path removed it. A failing require after the write and the 200 s hang limit (std::_Exit) left the file behind, and the two skips left the empty directory. A local Cleanup now removes the directory when the function returns or throws, and the hang path removes it before std::_Exit and prints the error when it cannot, as on Windows while the write still holds the file open. Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 1796f83acb..7ffc5e1952 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -793,6 +793,13 @@ int runLargeFileWrite() { std::error_code error; std::filesystem::remove_all(directory, error); std::filesystem::create_directories(directory, error); + struct Cleanup { + std::filesystem::path directory; + ~Cleanup() { + std::error_code error; + std::filesystem::remove_all(directory, error); + } + } cleanup{directory}; const auto space = std::filesystem::space(directory, error); if (error || space.available < size + (std::uintmax_t{1} << 30u)) { std::cout << "skipping: not enough free disk space for a file of more than 2 GiB\n"; @@ -819,6 +826,8 @@ int runLargeFileWrite() { }).detach(); if (result.wait_for(std::chrono::seconds(200)) != std::future_status::ready) { std::cerr << "WriteFileAtomically did not return within 200 s for a file of more than 2 GiB\n"; + std::filesystem::remove_all(directory, error); + if (error) std::cerr << "cannot remove the large file test directory: " << error.message() << '\n'; std::cerr.flush(); std::_Exit(1); } From 1868043281d72c2103aee75a085bb49e4e6ff96a Mon Sep 17 00:00:00 2001 From: thuanquang Date: Tue, 20 Oct 2026 14:56:50 +0700 Subject: [PATCH 104/167] test(agc): skip only the device-dependent f64 denormal mode results On a device whose target reports no f64 DenormPreserve, the f64 rounding and add/mul denormal mode tests skipped every mismatch in the modes that keep f64 denormal inputs (1, 3 and no known float mode) and checked modes 0 and 2 strictly. A recompiler regression in modes 1 and 3 never failed there, such as dropping the v_fract_f64 result flush that only mode 1 shows, while mode 2 failed where a driver may flush a denormal result of normal inputs (v_add_f64 and v_mul_f64 rows 4, 9, 11 and 15). A result is now skipped only when its expected value changes if the device flushes f64 denormal inputs, results or both, and only until the device shows that it keeps them: when the run without a known float mode, which uses the native instructions, matches every row, the other modes are checked strictly. Co-Authored-By: Claude --- .../execution/F64AddMulDenormalModes.cpp | 30 ++++----- .../execution/F64RoundingDenormalModes.cpp | 61 +++++++++++-------- 2 files changed, 50 insertions(+), 41 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F64AddMulDenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F64AddMulDenormalModes.cpp index 71de71a46b..fa57f2f3ec 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F64AddMulDenormalModes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F64AddMulDenormalModes.cpp @@ -167,15 +167,23 @@ void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { device.WaitIdle(); } -void Check(const Mode& mode) { +std::uint32_t Check(const Mode& mode, bool keepsDenormals) { constexpr const char* Names[2] = {"v_add_f64", "v_mul_f64"}; + const std::array deviceFlushes{mode.field & 2u, mode.field & 1u, 0u}; + std::uint32_t skipped = 0; for (std::uint32_t tid = 0; tid < Threads; ++tid) { for (std::uint32_t form = 0; form < 2u; ++form) { const std::uint64_t actual = Output[tid * Results + form * 2u] | (static_cast(Output[tid * Results + form * 2u + 1u]) << 32u); const std::uint64_t expected = Expected[mode.field][tid][form]; - Require(actual == expected, std::string(Names[form]) + " " + mode.name + " of " + Hex(Pairs[tid][0]) + " and " + Hex(Pairs[tid][1]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + if (actual == expected) continue; + const std::string message = std::string(Names[form]) + " " + mode.name + " of " + Hex(Pairs[tid][0]) + " and " + Hex(Pairs[tid][1]) + " is " + Hex(actual) + ", expected " + Hex(expected); + const bool deviceDependent = std::any_of(deviceFlushes.begin(), deviceFlushes.end(), [&](std::uint32_t flushed) { return Expected[flushed][tid][form] != expected; }); + Require(deviceDependent && !keepsDenormals, message); + std::printf("skipped %s, the device flushes f64 denormals without DenormPreserve support\n", message.c_str()); + ++skipped; } } + return skipped; } } @@ -195,21 +203,15 @@ int main() { {"f64 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, 3u}, {"f64 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, 3u}, }; - const bool canPreserveDenormals = device->Target().float64DenormPreserve; + bool keepsDenormals = device->Target().float64DenormPreserve; + std::uint32_t skipped = 0; for (const Mode& mode : modes) { Run(*device, mode); - const bool keepsDenormals = !mode.value.has_value() || ((mode.value->floatMode >> 6u) & 1u) != 0u; - if (!keepsDenormals || canPreserveDenormals) { - Check(mode); - continue; - } - try { - Check(mode); - } catch (const std::exception& error) { - std::printf("skipped %s, the device flushes f64 denormals without DenormPreserve support: %s\n", mode.name, error.what()); - } + const std::uint32_t modeSkipped = Check(mode, keepsDenormals); + keepsDenormals = keepsDenormals || (!mode.value.has_value() && modeSkipped == 0u); + skipped += modeSkipped; } - std::puts("f64 add and mul denormal mode tests passed"); + std::printf("f64 add and mul denormal mode tests passed, %u device-dependent results skipped\n", skipped); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F64RoundingDenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F64RoundingDenormalModes.cpp index 60ee60dd0b..363303b53d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F64RoundingDenormalModes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F64RoundingDenormalModes.cpp @@ -94,28 +94,41 @@ std::uint64_t Bits(double value) { return std::bit_cast(value); } -void Check(const Mode& mode) { +std::array Expected(std::uint64_t source, std::uint32_t field) { + const double value = std::bit_cast(Flush(source, (field & 1u) == 0u)); + const double below = std::bit_cast(0x3fefffffffffffffull); + int exponent = 0; + const double mantissa = std::frexp(value, &exponent); + return { + Bits(std::floor(value)), + Bits(std::ceil(value)), + Flush(Bits(std::min(value - std::floor(value), below)), (field & 2u) == 0u), + Bits(mantissa), + static_cast(exponent), + }; +} + +std::uint32_t Check(const Mode& mode, bool keepsDenormals) { const std::uint32_t field = mode.value.has_value() ? mode.value->floatMode >> 6u : 3u; + const std::array deviceFlushes{field & 2u, field & 1u, 0u}; + constexpr const char* Names[5] = {"v_floor_f64", "v_ceil_f64", "v_fract_f64", "v_frexp_mant_f64", "v_frexp_exp_i32_f64"}; + std::uint32_t skipped = 0; for (std::uint32_t tid = 0; tid < Threads; ++tid) { const std::uint64_t source = ValueAt(tid); - const double value = std::bit_cast(Flush(source, (field & 1u) == 0u)); - const double below = std::bit_cast(0x3fefffffffffffffull); - int exponent = 0; - const double mantissa = std::frexp(value, &exponent); - const std::array expected{ - Bits(std::floor(value)), - Bits(std::ceil(value)), - Flush(Bits(std::min(value - std::floor(value), below)), (field & 2u) == 0u), - Bits(mantissa), - }; - constexpr const char* Names[4] = {"v_floor_f64", "v_ceil_f64", "v_fract_f64", "v_frexp_mant_f64"}; + const auto expected = Expected(source, field); const std::uint32_t* out = &Output[tid * Results]; for (std::uint32_t index = 0; index < expected.size(); ++index) { - const std::uint64_t actual = out[index * 2u] | (static_cast(out[index * 2u + 1u]) << 32u); - Require(actual == expected[index], std::string(Names[index]) + " " + mode.name + " of " + Hex(source) + " is " + Hex(actual) + ", expected " + Hex(expected[index])); + const std::uint64_t actual = index < 4u ? out[index * 2u] | (static_cast(out[index * 2u + 1u]) << 32u) : out[8]; + if (actual == expected[index]) continue; + const auto text = [index](std::uint64_t bits) { return index < 4u ? Hex(bits) : std::to_string(static_cast(bits)); }; + const std::string message = std::string(Names[index]) + " " + mode.name + " of " + Hex(source) + " is " + text(actual) + ", expected " + text(expected[index]); + const bool deviceDependent = std::any_of(deviceFlushes.begin(), deviceFlushes.end(), [&](std::uint32_t flushed) { return Expected(source, flushed)[index] != expected[index]; }); + Require(deviceDependent && !keepsDenormals, message); + std::printf("skipped %s, the device flushes f64 denormals without DenormPreserve support\n", message.c_str()); + ++skipped; } - Require(out[8] == static_cast(exponent), std::string("v_frexp_exp_i32_f64 ") + mode.name + " of " + Hex(source) + " is " + std::to_string(static_cast(out[8])) + ", expected " + std::to_string(exponent)); } + return skipped; } } @@ -135,21 +148,15 @@ int main() { {"f64 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}}, {"f64 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}}, }; - const bool canPreserveDenormals = device->Target().float64DenormPreserve; + bool keepsDenormals = device->Target().float64DenormPreserve; + std::uint32_t skipped = 0; for (const Mode& mode : modes) { Run(*device, mode); - const bool keepsDenormals = !mode.value.has_value() || ((mode.value->floatMode >> 6u) & 1u) != 0u; - if (!keepsDenormals || canPreserveDenormals) { - Check(mode); - continue; - } - try { - Check(mode); - } catch (const std::exception& error) { - std::printf("skipped %s, the device flushes f64 denormals without DenormPreserve support: %s\n", mode.name, error.what()); - } + const std::uint32_t modeSkipped = Check(mode, keepsDenormals); + keepsDenormals = keepsDenormals || (!mode.value.has_value() && modeSkipped == 0u); + skipped += modeSkipped; } - std::puts("f64 rounding denormal mode tests passed"); + std::printf("f64 rounding denormal mode tests passed, %u device-dependent results skipped\n", skipped); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From a5e1f83417cda5ee0d8ce2fa1d4323a97dc050fe Mon Sep 17 00:00:00 2001 From: thuanquang Date: Tue, 20 Oct 2026 15:18:13 +0700 Subject: [PATCH 105/167] fix(shader): keep the 24-bit factor reads of 16-bit multiplies #7018 reads the low-half factors of v_mul_lo_u16 and v_pk_mul_lo_u16 as 24-bit fields because AMD's Windows driver stores the full 32-bit product when it knows both factors fit in 16 bits. #7673's fold of BitFieldUExtract(x, 0, n) into x removed those reads when the program had masked the factors to 16 bits, so agc_driver_mul_lo_u16_high_half failed again on that driver. The folder now keeps a field read that a multiply uses. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ConstantBitTest.cpp | 22 +++++++++++++++++++ .../Optimization/src/ConstantFolder.cpp | 7 +++++- docs/dev/debt/shader.md | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp index 0c44fbf067..06d73e6578 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp @@ -125,6 +125,26 @@ bool halvesRejoin() { return rejoined == &word && fieldBack; } +bool factorFieldKept(bool multiplied) { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& sum = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(1u), &lane}); + auto& masked = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&sum, &builder.Constant(0xffffu)}); + auto& field = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&masked, &builder.Constant(0u), &builder.Constant(24u)}); + auto& used = multiplied ? builder.IMul(field, field) : field; + auto& user = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&used}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder().Fold(program); + const IrValue* result = user.Argument(0)->Resolve(); + if (!multiplied) return result == &masked; + return result->Opcode() == IrOpcode::IMul32 && result->Argument(0)->Resolve() == &field && field.Opcode() == IrOpcode::BitFieldUExtract && field.Argument(0)->Resolve() == &masked; +} + bool reversesTo(std::uint32_t word, std::uint32_t expected) { IrProgram program; auto& block = program.CreateBlock(); @@ -220,6 +240,8 @@ int main() { passed &= run("a mask under a merge of the halves reads through it", mergedHalfMask()); passed &= run("half extractions read the half that was merged in", halfExtractions()); passed &= run("the two halves of one word rejoin into the word", halvesRejoin()); + passed &= run("a 24-bit field read of a value masked to 16 bits folds into the value", factorFieldKept(false)); + passed &= run("a 24-bit field read that a multiply uses is kept, even for a factor masked to 16 bits", factorFieldKept(true)); return passed ? 0 : 1; } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index 1511315ce1..1eb0dd3650 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -224,6 +224,11 @@ bool foldRedundantMask(IrValue& value) { return false; } +bool feedsMultiply(const IrValue& value) { + const auto& uses = value.Uses(); + return std::any_of(uses.begin(), uses.end(), [](const IrValue* use) { return use->Opcode() == IrOpcode::IMul32; }); +} + bool isImmediateU32(const IrValue& value, std::uint32_t immediate) { return isImmediate(value, IrType::U32) && value.ImmediateU32() == immediate; } @@ -903,7 +908,7 @@ bool tryFoldValue(IrProgram& program, IrValue& value, OperandFacts& facts) { return true; } } - if (value.Opcode() == IrOpcode::BitFieldUExtract && isImmediateU32(offset, 0u) && isImmediate(count, IrType::U32) && count.ImmediateU32() < 32u && !isImmediate(source, IrType::U32) && (possibleBits(source) >> count.ImmediateU32()) == 0u) { + if (value.Opcode() == IrOpcode::BitFieldUExtract && isImmediateU32(offset, 0u) && isImmediate(count, IrType::U32) && count.ImmediateU32() < 32u && !isImmediate(source, IrType::U32) && (possibleBits(source) >> count.ImmediateU32()) == 0u && !feedsMultiply(value)) { replaceWith(value, source); return true; } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 2dee38e74e..fc987190ec 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -115,7 +115,7 @@ - [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. - [v_log_f32 and v_log_f16](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler -- [16-bit integer multiplies](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`v_mul_lo_u16`, `v_pk_mul_lo_u16`) (shader recompiler) - a factor taken from the low half of its source is read as the low 24 bits of the register, not the low 16; the low 16 bits of the product are the same for every input. The AMD Windows driver 32.0.21045.5002 (RX 6700 XT, RDNA 2) stores the full 32-bit product when two values it knows fit in 16 bits are multiplied, the product is reduced to 16 bits and nothing else is merged into the stored value: `v_mul_lo_u16` into a register whose high half is zero, also with factors masked to 16 bits by the program, and `v_pk_mul_lo_u16` whose high lanes are zero wrote 0xfffe0001 for 0xffff * 0xffff where the GPU writes 0x00000001. Reducing the product with `v_and_b32`, `v_bfe_u32` or a shift pair loses the same bits; factors read as 24-bit fields do not. Measured against the same instructions run through the [hardware oracle](../HW_ORACLE.md) on that GPU; other drivers are not checked +- [16-bit integer multiplies](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`v_mul_lo_u16`, `v_pk_mul_lo_u16`) (shader recompiler) - a factor taken from the low half of its source is read as the low 24 bits of the register, not the low 16; the low 16 bits of the product are the same for every input. The AMD Windows driver 32.0.21045.5002 (RX 6700 XT, RDNA 2) stores the full 32-bit product when two values it knows fit in 16 bits are multiplied, the product is reduced to 16 bits and nothing else is merged into the stored value: `v_mul_lo_u16` into a register whose high half is zero, also with factors masked to 16 bits by the program, and `v_pk_mul_lo_u16` whose high lanes are zero wrote 0xfffe0001 for 0xffff * 0xffff where the GPU writes 0x00000001. Reducing the product with `v_and_b32`, `v_bfe_u32` or a shift pair loses the same bits; factors read as 24-bit fields do not. The [constant folder](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) keeps a field read that a multiply uses, even when the program already masked the factor to 16 bits, since folding it away brings the full product back (#8271). Measured against the same instructions run through the [hardware oracle](../HW_ORACLE.md) on that GPU; other drivers are not checked - [f32 compares in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_cmp_X_f32` and `v_cmpx_X_f32` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f32 denormal inputs by bit 0 of the `FLOAT_MODE` f32 denormal field: mode 1 keeps them as mode 3 does, and mode 2 flushes them as mode 0 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 484 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3, f32 round to nearest even, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0. The PS5's gfx1013 is not measured. - [f16 compares](../../../core/shader/recompiler/Translation/src/CompareInstructions.cpp) (`v_cmp_X_f16` and `v_cmpx_X_f16` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f16 denormal inputs to a zero of the same sign by bit 0 of the `FLOAT_MODE` f16 denormal field: modes 0 and 2 flush them, 1 and 3 keep them. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 256 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f16 denormal mode 0 to 3, f16 round to nearest even, DX10_CLAMP=1, f32 denormals kept and FP16_OVFL=0. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the host pixel interlock spans the whole shader, not only up to `s_sendmsg` ORDERED_PS_DONE. From 251ca1c1008b21a41889246fd7ea9420cc1fb632 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Mon, 19 Oct 2026 17:12:01 -0500 Subject: [PATCH 106/167] 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 680ba6bf29ab82d5b4b6f5d24d4637cc1d388e36 Mon Sep 17 00:00:00 2001 From: nekomancer01 <5332082+nekomancer01@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:14:42 +1100 Subject: [PATCH 107/167] feat(libSceRudp): implement the poll exports Implement sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait and sceRudpPollDestroy with the PS Vita signatures and argument checks. sceRudpTerminate removes the context from every poll and sceRudpEnd destroys the polls and wakes their waiters. Co-Authored-By: Claude Opus 5.5 --- core/libs/CMakeLists.txt | 10 +++ core/libs/prx/libSceRudp/Export.cpp | 131 +++++++++++++++++++++++++--- core/libs/tests/GuestRudpPoll.cpp | 122 ++++++++++++++++++++++++++ docs/dev/debt/libSceRudp.md | 2 +- 4 files changed, 251 insertions(+), 14 deletions(-) create mode 100644 core/libs/tests/GuestRudpPoll.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 50373d1bf8..a6428b6825 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -478,6 +478,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_rudp_poll_tests tests/GuestRudpPoll.cpp) + target_include_directories(guest_rudp_poll_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_rudp_poll_tests PRIVATE libSceRudp libc) + configure_windows_unwind(guest_rudp_poll_tests) + add_test(NAME guest_rudp_poll COMMAND guest_rudp_poll_tests) + if(WIN32) + set_tests_properties(guest_rudp_poll PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_memory_pool_stats_tests tests/GuestMemoryPoolStats.cpp) target_include_directories(guest_memory_pool_stats_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_memory_pool_stats_tests PRIVATE libSceSsl libSceHttp2 libSceNet libkernel libc) diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index e1c283e658..f72bc2f121 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -1,10 +1,14 @@ #include +#include +#include #include #include #include #include +#include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -20,6 +24,13 @@ constexpr int RUDP_ERROR_MEMORY = static_cast(0x80770007u); constexpr int RUDP_ERROR_CONN_RESET = static_cast(0x80770009u); constexpr int RUDP_ERROR_CONN_REFUSED = static_cast(0x8077000Au); constexpr int RUDP_ERROR_NOT_ACCEPTABLE = static_cast(0x80770011u); +constexpr int RUDP_ERROR_INVALID_POLL_ID = static_cast(0x8077001Fu); +constexpr int RUDP_ERROR_TOO_MANY_CONTEXTS = static_cast(0x80770020u); + +constexpr int RUDP_POLL_OP_ADD = 1; +constexpr int RUDP_POLL_OP_MODIFY = 2; +constexpr int RUDP_POLL_OP_REMOVE = 3; +constexpr uint64_t RUDP_POLL_WAIT_MAX_USEC = static_cast(std::numeric_limits::max() / 2000); constexpr int RUDP_POOL_MIN_SIZE = 0xC58; constexpr std::uintptr_t RUDP_POOL_ALIGNMENT = 8; @@ -36,9 +47,31 @@ std::map g_ctx; // id -> bound GuestRudpEventHandler g_handler = nullptr; void* g_handler_arg = nullptr; +struct RudpPoll { + uint32_t capacity; + std::map entries; +}; + +struct RudpPollEvent { + int ctxId; + uint16_t reqEvents; + uint16_t rtnEvents; +}; +static_assert(sizeof(RudpPollEvent) == 8); + +int g_next_poll = 1; +std::map g_polls; +std::condition_variable g_polls_changed; + bool valid_ctx(int ctx) { return g_ctx.count(ctx) != 0; } + +void remove_ctx_from_polls(int ctx) { + for (auto& [id, poll] : g_polls) { + poll.entries.erase(ctx); + } +} } // namespace extern "C" { @@ -84,6 +117,8 @@ int APS5_VABI sceRudpEnd(void) { } g_inited = false; g_ctx.clear(); + g_polls.clear(); + g_polls_changed.notify_all(); return 0; } @@ -92,7 +127,11 @@ int APS5_VABI sceRudpTerminate(int ctx_id) { if (!g_inited) { return RUDP_ERROR_NOT_INITIALIZED; } - return g_ctx.erase(ctx_id) != 0 ? 0 : RUDP_ERROR_INVALID_CONTEXT_ID; + if (g_ctx.erase(ctx_id) == 0) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + remove_ctx_from_polls(ctx_id); + return 0; } int APS5_VABI sceRudpEnableInternalIOThread(uint32_t stack_size, uint32_t priority) { @@ -293,23 +332,89 @@ int APS5_VABI sceRudpGetMaxSegmentSize(uint16_t* size) { return 0; } -int APS5_VABI sceRudpPollCreate() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceRudpPollCreate(uint32_t size) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (size == 0) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + if (g_next_poll == std::numeric_limits::max()) { + throw std::runtime_error("sceRudpPollCreate: poll ids exhausted"); + } + const int id = g_next_poll++; + g_polls[id] = RudpPoll{size, {}}; + return id; +} + +int APS5_VABI sceRudpPollControl(int pollId, int op, int ctxId, uint16_t events) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (pollId < 0 || ctxId < 0 || (op != RUDP_POLL_OP_ADD && op != RUDP_POLL_OP_MODIFY && op != RUDP_POLL_OP_REMOVE)) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + const auto poll = g_polls.find(pollId); + if (poll == g_polls.end()) { + return RUDP_ERROR_INVALID_POLL_ID; + } + if (!valid_ctx(ctxId)) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + auto& entries = poll->second.entries; + const auto entry = entries.find(ctxId); + if (op == RUDP_POLL_OP_ADD) { + if (entry != entries.end()) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + if (entries.size() >= poll->second.capacity) { + return RUDP_ERROR_TOO_MANY_CONTEXTS; + } + entries.emplace(ctxId, events); + return 0; + } + if (entry == entries.end()) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + if (op == RUDP_POLL_OP_MODIFY) { + entry->second = events; + } else { + entries.erase(entry); + } return 0; } -int APS5_VABI sceRudpPollControl() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceRudpPollWait(int pollId, RudpPollEvent* events, uint32_t eventLen, uint64_t timeout) { + std::unique_lock lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (pollId < 0 || events == nullptr || eventLen == 0) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + if (g_polls.count(pollId) == 0) { + return RUDP_ERROR_INVALID_POLL_ID; + } + const auto closed = [pollId] { return !g_inited || g_polls.count(pollId) == 0; }; + if (timeout > RUDP_POLL_WAIT_MAX_USEC) { + g_polls_changed.wait(lk, closed); + } else if (!g_polls_changed.wait_for(lk, std::chrono::microseconds(timeout), closed)) { + return 0; + } + return g_inited ? RUDP_ERROR_INVALID_POLL_ID : RUDP_ERROR_NOT_INITIALIZED; } -int APS5_VABI sceRudpPollWait() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRudpPollDestroy() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceRudpPollDestroy(int pollId) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (g_polls.erase(pollId) == 0) { + return RUDP_ERROR_INVALID_POLL_ID; + } + g_polls_changed.notify_all(); return 0; } diff --git a/core/libs/tests/GuestRudpPoll.cpp b/core/libs/tests/GuestRudpPoll.cpp new file mode 100644 index 0000000000..1957a27f2f --- /dev/null +++ b/core/libs/tests/GuestRudpPoll.cpp @@ -0,0 +1,122 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include +#include + +extern "C" { +using GuestRudpEventHandler = void(APS5_VABI*)(int, int, int, void*); +struct GuestRudpPollEvent { + int ctxId; + std::uint16_t reqEvents; + std::uint16_t rtnEvents; +}; +int APS5_VABI sceRudpInit_nid_postfix(void*, int); +int APS5_VABI sceRudpCreateContext(GuestRudpEventHandler, void*, int*); +int APS5_VABI sceRudpTerminate(int); +int APS5_VABI sceRudpEnd(); +int APS5_VABI sceRudpPollCreate(std::uint32_t); +int APS5_VABI sceRudpPollControl(int, int, int, std::uint16_t); +int APS5_VABI sceRudpPollWait(int, GuestRudpPollEvent*, std::uint32_t, std::uint64_t); +int APS5_VABI sceRudpPollDestroy(int); +} + +static void Require(bool value) { + if (!value) std::abort(); +} + +static void APS5_VABI Handler(int, int, int, void*) {} + +int main() { + constexpr int notInitialized = static_cast(0x80770001u); + constexpr int invalidContext = static_cast(0x80770003u); + constexpr int invalidArgument = static_cast(0x80770004u); + constexpr int invalidPoll = static_cast(0x8077001Fu); + constexpr int tooManyContexts = static_cast(0x80770020u); + constexpr int add = 1; + constexpr int modify = 2; + constexpr int remove = 3; + constexpr std::uint16_t read = 0x0001; + constexpr std::uint16_t write = 0x0002; + constexpr std::uint64_t forever = std::numeric_limits::max(); + alignas(16) static std::array pool{}; + std::array events{}; + const auto eventCount = static_cast(events.size()); + + Require(sceRudpPollCreate(1) == notInitialized); + Require(sceRudpPollControl(1, add, 0, read) == notInitialized); + Require(sceRudpPollWait(1, events.data(), eventCount, 0) == notInitialized); + Require(sceRudpPollDestroy(1) == notInitialized); + Require(sceRudpInit_nid_postfix(pool.data(), static_cast(pool.size())) == 0); + + Require(sceRudpPollCreate(0) == invalidArgument); + const int poll = sceRudpPollCreate(1); + Require(poll > 0); + const int other = sceRudpPollCreate(1); + Require(other > 0 && other != poll); + + int first = -1; + int second = -1; + Require(sceRudpCreateContext(Handler, nullptr, &first) == 0); + Require(sceRudpCreateContext(Handler, nullptr, &second) == 0); + + Require(sceRudpPollControl(poll, 0, first, read) == invalidArgument); + Require(sceRudpPollControl(poll, 4, first, read) == invalidArgument); + Require(sceRudpPollControl(-1, add, first, read) == invalidArgument); + Require(sceRudpPollControl(poll, add, -1, read) == invalidArgument); + Require(sceRudpPollControl(12345, add, first, read) == invalidPoll); + Require(sceRudpPollControl(poll, add, 12345, read) == invalidContext); + Require(sceRudpPollControl(poll, modify, first, read) == invalidContext); + Require(sceRudpPollControl(poll, remove, first, read) == invalidContext); + + Require(sceRudpPollControl(poll, add, first, read) == 0); + Require(sceRudpPollControl(poll, add, first, read) == invalidContext); + Require(sceRudpPollControl(poll, add, second, read) == tooManyContexts); + Require(sceRudpPollControl(other, add, first, read) == 0); + Require(sceRudpPollControl(poll, modify, first, read | write) == 0); + Require(sceRudpPollControl(poll, remove, first, 0) == 0); + Require(sceRudpPollControl(poll, remove, first, 0) == invalidContext); + Require(sceRudpPollControl(poll, add, second, write) == 0); + + Require(sceRudpPollWait(-1, events.data(), eventCount, 0) == invalidArgument); + Require(sceRudpPollWait(poll, nullptr, eventCount, 0) == invalidArgument); + Require(sceRudpPollWait(poll, events.data(), 0, 0) == invalidArgument); + Require(sceRudpPollWait(12345, events.data(), eventCount, 0) == invalidPoll); + Require(sceRudpPollWait(poll, events.data(), eventCount, 0) == 0); + const auto start = std::chrono::steady_clock::now(); + Require(sceRudpPollWait(poll, events.data(), eventCount, 20000) == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(20)); + + Require(sceRudpTerminate(second) == 0); + Require(sceRudpPollControl(poll, add, first, read) == 0); + Require(sceRudpTerminate(first) == 0); + Require(sceRudpPollControl(poll, add, first, read) == invalidContext); + int third = -1; + Require(sceRudpCreateContext(Handler, nullptr, &third) == 0); + Require(sceRudpPollControl(poll, add, third, read) == 0); + Require(sceRudpPollControl(other, add, third, read) == 0); + + int destroyedResult = 0; + std::thread destroyedWaiter([&] { destroyedResult = sceRudpPollWait(poll, events.data(), eventCount, forever); }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(sceRudpPollDestroy(poll) == 0); + destroyedWaiter.join(); + Require(destroyedResult == invalidPoll); + Require(sceRudpPollDestroy(poll) == invalidPoll); + Require(sceRudpPollControl(poll, add, third, read) == invalidPoll); + + int endedResult = 0; + std::thread endedWaiter([&] { endedResult = sceRudpPollWait(other, events.data(), eventCount, forever); }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(sceRudpEnd() == 0); + endedWaiter.join(); + Require(endedResult == notInitialized); + + Require(sceRudpInit_nid_postfix(pool.data(), static_cast(pool.size())) == 0); + Require(sceRudpPollDestroy(other) == invalidPoll); + Require(sceRudpEnd() == 0); + return 0; +} diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index fd1fc4868a..217a1ebbd8 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -12,5 +12,5 @@ - [sceRudpGetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. The library keeps no configurable segment size (there is no `sceRudpSetMaxSegmentSize`), so 1410 is always reported. The `uint16_t*` parameter is inferred: the measurement does not show how many bytes the console writes, PPSA03490 passes a pointer to a 16-bit value and [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.cpp) declares the PS3 `cellRudpSetMaxSegmentSize(u16 mss)`, so only 16 bits are written - [sceRudpGetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) takes `(int ctx_id, int option, void* value, uint32_t len)`, taken from the bundled `Media/Plugins/SceRudp.prx` of PPSA06888: its two exported wrappers pass their first three arguments through unchanged and set `len` to 4, the same shape as its `sceRudpSetOption` call, which passes a 4-byte value. The option codes, the defaults and the error results are unknown, so it still throws - PPSA06888 imports with unknown signatures: [sceRudpGetRemoteInfo](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by its bundled `Media/Plugins/SceRudp.prx` and not called during start-up. The third argument is a pointer to a 4-byte buffer seeded from the byte its wrapper reads through the second argument, which does not show what the arguments mean. The managed `SceRudp.Context.GetRemoteInfo()` in the title's Unity metadata takes no parameters and returns a `SceRudp.AddrAndVport`; its native arguments are not visible there. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp), which declares them without parameters too -- PPSA31327 imports declared without parameters, signatures unknown: [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by Yakuza 0 Director's Cut and not called before the main menu. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) +- [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - imported by PPSA31327 (Yakuza 0 Director's Cut) and not called before the main menu. Signatures `int sceRudpPollCreate(uint32_t size)`, `int sceRudpPollControl(int pollId, int op, int ctxId, uint16_t events)` and `int sceRudpPollWait(int pollId, SceRudpPollEvent* events, uint32_t eventLen, uint64_t timeoutUsec)`, the 8-byte `SceRudpPollEvent` (`ctxId`, `reqEvents`, `rtnEvents`), the operations (1 add, 2 modify, 3 remove) and the argument checks follow the PS Vita library in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp); `RUDP_ERROR_INVALID_POLL_ID` (0x8077001F) and `RUDP_ERROR_TOO_MANY_CONTEXTS` (0x80770020) match the PS3 codes in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.h). Vita3K leaves `sceRudpPollDestroy` unimplemented: `int sceRudpPollDestroy(int pollId)` is inferred. No context can connect, so no context ever reports an event and `sceRudpPollWait` returns 0 when its timeout elapses; a timeout above 2^63/2000 microseconds waits until the poll is destroyed. A wait whose poll is destroyed returns `RUDP_ERROR_INVALID_POLL_ID`, and one interrupted by `sceRudpEnd` returns `RUDP_ERROR_NOT_INITIALIZED`. `sceRudpTerminate` removes the context from every poll. Poll ids start at 1. None of this is measured on a PS5 - [sceRudpAccept, sceRudpFlush, sceRudpGetLocalInfo, sceRudpListen, sceRudpPollCancel and sceRudpSetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRudp.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. From d2b9531517ecb0bf3cd5024efe6bcae519c0da0e Mon Sep 17 00:00:00 2001 From: nekomancer01 <5332082+nekomancer01@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:20:54 +1100 Subject: [PATCH 108/167] fix(libSceRudp): match the Vita3K check order and reject negative poll ids Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceRudp/Export.cpp | 9 ++++++--- core/libs/tests/GuestRudpPoll.cpp | 6 ++++-- docs/dev/debt/libSceRudp.md | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index f72bc2f121..87864f5971 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -366,12 +366,12 @@ int APS5_VABI sceRudpPollControl(int pollId, int op, int ctxId, uint16_t events) auto& entries = poll->second.entries; const auto entry = entries.find(ctxId); if (op == RUDP_POLL_OP_ADD) { - if (entry != entries.end()) { - return RUDP_ERROR_INVALID_CONTEXT_ID; - } if (entries.size() >= poll->second.capacity) { return RUDP_ERROR_TOO_MANY_CONTEXTS; } + if (entry != entries.end()) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } entries.emplace(ctxId, events); return 0; } @@ -411,6 +411,9 @@ int APS5_VABI sceRudpPollDestroy(int pollId) { if (!g_inited) { return RUDP_ERROR_NOT_INITIALIZED; } + if (pollId < 0) { + return RUDP_ERROR_INVALID_ARGUMENT; + } if (g_polls.erase(pollId) == 0) { return RUDP_ERROR_INVALID_POLL_ID; } diff --git a/core/libs/tests/GuestRudpPoll.cpp b/core/libs/tests/GuestRudpPoll.cpp index 1957a27f2f..d797fad444 100644 --- a/core/libs/tests/GuestRudpPoll.cpp +++ b/core/libs/tests/GuestRudpPoll.cpp @@ -55,7 +55,7 @@ int main() { Require(sceRudpPollCreate(0) == invalidArgument); const int poll = sceRudpPollCreate(1); Require(poll > 0); - const int other = sceRudpPollCreate(1); + const int other = sceRudpPollCreate(2); Require(other > 0 && other != poll); int first = -1; @@ -73,9 +73,10 @@ int main() { Require(sceRudpPollControl(poll, remove, first, read) == invalidContext); Require(sceRudpPollControl(poll, add, first, read) == 0); - Require(sceRudpPollControl(poll, add, first, read) == invalidContext); + Require(sceRudpPollControl(poll, add, first, read) == tooManyContexts); Require(sceRudpPollControl(poll, add, second, read) == tooManyContexts); Require(sceRudpPollControl(other, add, first, read) == 0); + Require(sceRudpPollControl(other, add, first, read) == invalidContext); Require(sceRudpPollControl(poll, modify, first, read | write) == 0); Require(sceRudpPollControl(poll, remove, first, 0) == 0); Require(sceRudpPollControl(poll, remove, first, 0) == invalidContext); @@ -106,6 +107,7 @@ int main() { destroyedWaiter.join(); Require(destroyedResult == invalidPoll); Require(sceRudpPollDestroy(poll) == invalidPoll); + Require(sceRudpPollDestroy(-1) == invalidArgument); Require(sceRudpPollControl(poll, add, third, read) == invalidPoll); int endedResult = 0; diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index 217a1ebbd8..e0b6e6ba33 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -12,5 +12,5 @@ - [sceRudpGetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. The library keeps no configurable segment size (there is no `sceRudpSetMaxSegmentSize`), so 1410 is always reported. The `uint16_t*` parameter is inferred: the measurement does not show how many bytes the console writes, PPSA03490 passes a pointer to a 16-bit value and [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.cpp) declares the PS3 `cellRudpSetMaxSegmentSize(u16 mss)`, so only 16 bits are written - [sceRudpGetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) takes `(int ctx_id, int option, void* value, uint32_t len)`, taken from the bundled `Media/Plugins/SceRudp.prx` of PPSA06888: its two exported wrappers pass their first three arguments through unchanged and set `len` to 4, the same shape as its `sceRudpSetOption` call, which passes a 4-byte value. The option codes, the defaults and the error results are unknown, so it still throws - PPSA06888 imports with unknown signatures: [sceRudpGetRemoteInfo](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by its bundled `Media/Plugins/SceRudp.prx` and not called during start-up. The third argument is a pointer to a 4-byte buffer seeded from the byte its wrapper reads through the second argument, which does not show what the arguments mean. The managed `SceRudp.Context.GetRemoteInfo()` in the title's Unity metadata takes no parameters and returns a `SceRudp.AddrAndVport`; its native arguments are not visible there. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp), which declares them without parameters too -- [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - imported by PPSA31327 (Yakuza 0 Director's Cut) and not called before the main menu. Signatures `int sceRudpPollCreate(uint32_t size)`, `int sceRudpPollControl(int pollId, int op, int ctxId, uint16_t events)` and `int sceRudpPollWait(int pollId, SceRudpPollEvent* events, uint32_t eventLen, uint64_t timeoutUsec)`, the 8-byte `SceRudpPollEvent` (`ctxId`, `reqEvents`, `rtnEvents`), the operations (1 add, 2 modify, 3 remove) and the argument checks follow the PS Vita library in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp); `RUDP_ERROR_INVALID_POLL_ID` (0x8077001F) and `RUDP_ERROR_TOO_MANY_CONTEXTS` (0x80770020) match the PS3 codes in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.h). Vita3K leaves `sceRudpPollDestroy` unimplemented: `int sceRudpPollDestroy(int pollId)` is inferred. No context can connect, so no context ever reports an event and `sceRudpPollWait` returns 0 when its timeout elapses; a timeout above 2^63/2000 microseconds waits until the poll is destroyed. A wait whose poll is destroyed returns `RUDP_ERROR_INVALID_POLL_ID`, and one interrupted by `sceRudpEnd` returns `RUDP_ERROR_NOT_INITIALIZED`. `sceRudpTerminate` removes the context from every poll. Poll ids start at 1. None of this is measured on a PS5 +- [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - imported by PPSA31327 (Yakuza 0 Director's Cut) and not called before the main menu. Signatures `int sceRudpPollCreate(uint32_t size)`, `int sceRudpPollControl(int pollId, int op, int ctxId, uint16_t events)` and `int sceRudpPollWait(int pollId, SceRudpPollEvent* events, uint32_t eventLen, uint64_t timeoutUsec)`, the 8-byte `SceRudpPollEvent` (`ctxId`, `reqEvents`, `rtnEvents`), the operations (1 add, 2 modify, 3 remove) and the argument checks follow the PS Vita library in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp); `RUDP_ERROR_INVALID_POLL_ID` (0x8077001F) and `RUDP_ERROR_TOO_MANY_CONTEXTS` (0x80770020) match the PS3 codes in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.h). Vita3K leaves `sceRudpPollDestroy` unimplemented: `int sceRudpPollDestroy(int pollId)` is inferred, and it answers a negative id with `RUDP_ERROR_INVALID_ARGUMENT` like the other poll calls. No context can connect, so no context ever reports an event and `sceRudpPollWait` returns 0 when its timeout elapses, also for a poll with no contexts, where Vita3K returns 0 at once; a timeout above 2^63/2000 microseconds waits until the poll is destroyed. A wait whose poll is destroyed returns `RUDP_ERROR_INVALID_POLL_ID`, and one interrupted by `sceRudpEnd` returns `RUDP_ERROR_NOT_INITIALIZED`. `sceRudpTerminate` removes the context from every poll. Poll ids start at 1. None of this is measured on a PS5 - [sceRudpAccept, sceRudpFlush, sceRudpGetLocalInfo, sceRudpListen, sceRudpPollCancel and sceRudpSetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRudp.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. From e4164098a8becd6463a4e164eb7ed17d7c30712e Mon Sep 17 00:00:00 2001 From: nekomancer01 <5332082+nekomancer01@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:10:42 +1100 Subject: [PATCH 109/167] docs(libSceRudp): state the poll gaps first and shorten the debt entry Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceRudp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index e0b6e6ba33..19757221c0 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -12,5 +12,5 @@ - [sceRudpGetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. The library keeps no configurable segment size (there is no `sceRudpSetMaxSegmentSize`), so 1410 is always reported. The `uint16_t*` parameter is inferred: the measurement does not show how many bytes the console writes, PPSA03490 passes a pointer to a 16-bit value and [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.cpp) declares the PS3 `cellRudpSetMaxSegmentSize(u16 mss)`, so only 16 bits are written - [sceRudpGetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) takes `(int ctx_id, int option, void* value, uint32_t len)`, taken from the bundled `Media/Plugins/SceRudp.prx` of PPSA06888: its two exported wrappers pass their first three arguments through unchanged and set `len` to 4, the same shape as its `sceRudpSetOption` call, which passes a 4-byte value. The option codes, the defaults and the error results are unknown, so it still throws - PPSA06888 imports with unknown signatures: [sceRudpGetRemoteInfo](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by its bundled `Media/Plugins/SceRudp.prx` and not called during start-up. The third argument is a pointer to a 4-byte buffer seeded from the byte its wrapper reads through the second argument, which does not show what the arguments mean. The managed `SceRudp.Context.GetRemoteInfo()` in the title's Unity metadata takes no parameters and returns a `SceRudp.AddrAndVport`; its native arguments are not visible there. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp), which declares them without parameters too -- [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - imported by PPSA31327 (Yakuza 0 Director's Cut) and not called before the main menu. Signatures `int sceRudpPollCreate(uint32_t size)`, `int sceRudpPollControl(int pollId, int op, int ctxId, uint16_t events)` and `int sceRudpPollWait(int pollId, SceRudpPollEvent* events, uint32_t eventLen, uint64_t timeoutUsec)`, the 8-byte `SceRudpPollEvent` (`ctxId`, `reqEvents`, `rtnEvents`), the operations (1 add, 2 modify, 3 remove) and the argument checks follow the PS Vita library in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp); `RUDP_ERROR_INVALID_POLL_ID` (0x8077001F) and `RUDP_ERROR_TOO_MANY_CONTEXTS` (0x80770020) match the PS3 codes in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.h). Vita3K leaves `sceRudpPollDestroy` unimplemented: `int sceRudpPollDestroy(int pollId)` is inferred, and it answers a negative id with `RUDP_ERROR_INVALID_ARGUMENT` like the other poll calls. No context can connect, so no context ever reports an event and `sceRudpPollWait` returns 0 when its timeout elapses, also for a poll with no contexts, where Vita3K returns 0 at once; a timeout above 2^63/2000 microseconds waits until the poll is destroyed. A wait whose poll is destroyed returns `RUDP_ERROR_INVALID_POLL_ID`, and one interrupted by `sceRudpEnd` returns `RUDP_ERROR_NOT_INITIALIZED`. `sceRudpTerminate` removes the context from every poll. Poll ids start at 1. None of this is measured on a PS5 +- [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - unverified on a PS5; imported by PPSA31327 and not called before the main menu. The signatures, the 8-byte `SceRudpPollEvent`, the operations (1 add, 2 modify, 3 remove) and the argument checks follow the PS Vita library in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp), and the error codes match [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.h). Vita3K leaves `sceRudpPollDestroy` unimplemented, so `int sceRudpPollDestroy(int pollId)` and its checks are inferred. No context can connect, so no event is ever reported: `sceRudpPollWait` returns 0 when its timeout elapses, also for an empty poll, where Vita3K returns at once - [sceRudpAccept, sceRudpFlush, sceRudpGetLocalInfo, sceRudpListen, sceRudpPollCancel and sceRudpSetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRudp.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. From 4b527adf0587aaa2973885fc11e724feaf24b47d Mon Sep 17 00:00:00 2001 From: hypermist Date: Tue, 20 Oct 2026 22:12:02 +1300 Subject: [PATCH 110/167] feat(libc): implement vfscanf, vscanf, scanf, the wide stream printf family and vswscanf vfscanf, vscanf and scanf scan through the same host path as fscanf; vfwprintf, fwprintf and vwprintf format like wprintf and write to the guest stream; vswscanf and vswscanf_s run the wide scanner on a va_list. The names hash to the NIDs libSceLibcInternal exports. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libc/CMakeLists.txt | 9 ++ core/libs/prx/libc/src/Formatting.cpp | 39 ++++++++ core/libs/prx/libc/src/FormattingWide.cpp | 26 +++++ core/libs/prx/libc/src/ScanningWide.cpp | 10 ++ core/libs/tests/GuestStdioVariadic.cpp | 114 ++++++++++++++++++++++ 5 files changed, 198 insertions(+) create mode 100644 core/libs/tests/GuestStdioVariadic.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index ca39154516..083cd31097 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -314,6 +314,15 @@ if(BUILD_TESTING) set_tests_properties(guest_integer_builtins PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_stdio_variadic_tests ${LIBS_INCLUDE_DIR}/tests/GuestStdioVariadic.cpp) + target_include_directories(guest_stdio_variadic_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_stdio_variadic_tests PRIVATE libc) + configure_windows_unwind(guest_stdio_variadic_tests) + add_test(NAME guest_stdio_variadic COMMAND guest_stdio_variadic_tests) + if(WIN32) + set_tests_properties(guest_stdio_variadic PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_execute_once_tests ${LIBS_INCLUDE_DIR}/tests/GuestExecuteOnce.cpp) target_include_directories(guest_execute_once_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_execute_once_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 0d883aa904..e78be1d4a6 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -565,4 +565,43 @@ int APS5_VABI puts_nid_postfix(const char* s) { return std::puts(s); } +extern FileStream* __stdinp_nid_postfix; + +int APS5_VABI vfscanf_nid_postfix(FileStream* stream, const char* format, VaList* args) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } + auto* native = GetNativeStream(stream); +#ifdef _WIN32 + const int result = LibcDetail::ScanFileWindows_nid_no_patch(native, format, args); +#else + RejectGuestWideScan(format); + std::va_list copy; + va_copy(copy, *reinterpret_cast(args)); + const int result = HostVfscanf(native, format, copy); + va_end(copy); +#endif + stream->SyncStatus(); + return result; +} + +int APS5_VABI vscanf_nid_postfix(const char* format, VaList* args) { + return vfscanf_nid_postfix(__stdinp_nid_postfix, format, args); +} + +int APS5_VABI scanf_nid_postfix(const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + const int result = vfscanf_nid_postfix(__stdinp_nid_postfix, format, reinterpret_cast(args)); +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; +} + } diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index 713a520591..1526f39fef 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -333,6 +333,32 @@ int APS5_VABI wprintf_nid_postfix(const char16_t* format, ...) { return result; } +int APS5_VABI vfwprintf_nid_postfix(FileStream* stream, const char16_t* format, VaList* args) { + if (stream == nullptr || format == nullptr) { errno = 22; return -1; } + if (!stream->IsOpen()) { errno = 9; return -1; } + int result = -1; + try { + const std::u16string text = FormatWide(format, args); + const std::string bytes = ToUtf8(text); + if (std::fwrite(bytes.data(), 1, bytes.size(), GetNativeStream(stream)) == bytes.size()) result = static_cast(text.size()); + } catch (const std::exception&) { + errno = 22; + } + stream->SyncStatus(); + return result; +} + +int APS5_VABI fwprintf_nid_postfix(FileStream* stream, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfwprintf_nid_postfix(stream, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + +int APS5_VABI vwprintf_nid_postfix(const char16_t* format, VaList* args) { + return vfwprintf_nid_postfix(__stdoutp_nid_postfix, format, args); +} + int APS5_VABI fputwc_nid_postfix(char16_t value, FileStream* stream) { if (stream == nullptr) { errno = 22; return -1; } try { diff --git a/core/libs/prx/libc/src/ScanningWide.cpp b/core/libs/prx/libc/src/ScanningWide.cpp index 6cccb21222..d040f9afe5 100644 --- a/core/libs/prx/libc/src/ScanningWide.cpp +++ b/core/libs/prx/libc/src/ScanningWide.cpp @@ -427,4 +427,14 @@ int APS5_VABI swscanf_s_nid_postfix(const char16_t* buffer, const char16_t* form } } +int APS5_VABI vswscanf_nid_postfix(const char16_t* buffer, const char16_t* format, VaList* args) { + if (buffer == nullptr || format == nullptr) return EOF; + return WideScanner("vswscanf", false, buffer, format, args).Run(); +} + +int APS5_VABI vswscanf_s_nid_postfix(const char16_t* buffer, const char16_t* format, VaList* args) { + if (buffer == nullptr || format == nullptr) return EOF; + return WideScanner("vswscanf_s", true, buffer, format, args).Run(); +} + } diff --git a/core/libs/tests/GuestStdioVariadic.cpp b/core/libs/tests/GuestStdioVariadic.cpp new file mode 100644 index 0000000000..62ead8be7f --- /dev/null +++ b/core/libs/tests/GuestStdioVariadic.cpp @@ -0,0 +1,114 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +extern FileStream* __stdinp_nid_postfix; +int APS5_VABI vfscanf_nid_postfix(FileStream*, const char*, void*); +int APS5_VABI vscanf_nid_postfix(const char*, void*); +int APS5_VABI scanf_nid_postfix(const char*, ...); +int APS5_VABI vfwprintf_nid_postfix(FileStream*, const char16_t*, void*); +int APS5_VABI fwprintf_nid_postfix(FileStream*, const char16_t*, ...); +int APS5_VABI vswscanf_nid_postfix(const char16_t*, const char16_t*, void*); +int APS5_VABI vswscanf_s_nid_postfix(const char16_t*, const char16_t*, void*); +} + +#ifdef _WIN32 +#define APS5_VA_BEGIN(last) __builtin_sysv_va_list args; __builtin_sysv_va_start(args, last) +#define APS5_VA_END() __builtin_sysv_va_end(args) +#else +#define APS5_VA_BEGIN(last) std::va_list args; va_start(args, last) +#define APS5_VA_END() va_end(args) +#endif + +static void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", what); + std::abort(); +} + +static int APS5_VABI ScanStream(FileStream* stream, const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfscanf_nid_postfix(stream, format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanInput(const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vscanf_nid_postfix(format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI PrintWide(FileStream* stream, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfwprintf_nid_postfix(stream, format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanWide(const char16_t* input, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vswscanf_nid_postfix(input, format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanWideSized(const char16_t* input, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vswscanf_s_nid_postfix(input, format, args); + APS5_VA_END(); + return result; +} + +static std::string Contents(FileStream& stream) { + std::fflush(stream.GetHandle()); + std::rewind(stream.GetHandle()); + std::string text; + for (int c; (c = std::fgetc(stream.GetHandle())) != EOF;) text.push_back(static_cast(c)); + return text; +} + +int main() { + FileStream scanned(std::tmpfile()); + Require(std::fputs("42 answer 7", scanned.GetHandle()) >= 0, "seed the scanned stream"); + std::rewind(scanned.GetHandle()); + int number = 0; + char word[16]{}; + Require(ScanStream(&scanned, "%d %15s", &number, word) == 2, "vfscanf assigns both conversions"); + Require(number == 42 && std::strcmp(word, "answer") == 0, "vfscanf values"); + Require(ScanStream(&scanned, "%d", &number) == 1 && number == 7, "vfscanf continues from the stream position"); + Require(ScanStream(&scanned, "%d", &number) == EOF && number == 7, "vfscanf at end of file"); + scanned.Close(); + + FileStream input(std::tmpfile()); + Require(std::fputs("-5 12", input.GetHandle()) >= 0, "seed the standard input stream"); + std::rewind(input.GetHandle()); + FileStream* const standardInput = __stdinp_nid_postfix; + __stdinp_nid_postfix = &input; + int first = 0, second = 0; + Require(ScanInput("%d", &first) == 1 && first == -5, "vscanf reads the guest standard input"); + Require(scanf_nid_postfix("%d", &second) == 1 && second == 12, "scanf reads the guest standard input"); + __stdinp_nid_postfix = standardInput; + input.Close(); + + FileStream written(std::tmpfile()); + Require(fwprintf_nid_postfix(&written, u"%d:%ls|", 7, u"été") == 6, "fwprintf counts wide characters"); + Require(PrintWide(&written, u"%5.1f", 2.25) == 5, "vfwprintf counts wide characters"); + Require(Contents(written) == "7:\xc3\xa9t\xc3\xa9| 2.2", "the stream holds the formatted text"); + written.Close(); + + std::int32_t a = 0, b = 0; + Require(ScanWide(u" 31 -4", u"%d%d", &a, &b) == 2 && a == 31 && b == -4, "vswscanf"); + Require(ScanWide(u"", u"%d", &a) == EOF, "vswscanf on empty input"); + char16_t letters[4]{}; + Require(ScanWideSized(u"abcdef", u"%3ls", letters, 4u) == 1 && std::u16string(letters) == u"abc", "vswscanf_s %ls with a size"); + Require(ScanWideSized(u"abcdef", u"%ls", letters, 4u) == 0 && letters[0] == 0, "vswscanf_s rejects a too small buffer"); + return 0; +} From 29e9ab36e19fe00308586801da9d03eb92423fee Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Tue, 20 Oct 2026 06:25:40 -0300 Subject: [PATCH 111/167] fix(libSceAudioOut): drop names missing from the pinned aerolib --- .../prx/libSceAudioOut/src/Unimplemented.cpp | 55 ------------------- docs/dev/debt/libSceAudioOut.md | 11 ---- 2 files changed, 66 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp index 78fd125308..d87292b4c0 100644 --- a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp +++ b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp @@ -45,16 +45,6 @@ int APS5_VABI sceAudioOut2LoContextGetQueueLevel(void) { return 0; } -int APS5_VABI sceAudioOut2LoPortSetAttributesEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioOut2LoPortGetAttributes(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOutSetSystemDebugState(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -165,11 +155,6 @@ int APS5_VABI sceAudioOutMasteringTerm(void) { return 0; } -int APS5_VABI sceAudioOut2LoPortCreateEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOutSetPadSpkVolume(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -335,26 +320,11 @@ int APS5_VABI sceAudioOut2LoContextQueryMemory(void) { return 0; } -int APS5_VABI sceAudioOut2LoContextQueryMemoryEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioOut2LoContextGetAttributes(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2LoContextCreate(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAudioOut2LoContextCreateEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2LoContextSetAttributes(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -440,11 +410,6 @@ int APS5_VABI sceAudioOut2GetTvCorrectionInfo(void) { return 0; } -int APS5_VABI sceAudioOut2SetTvCorrectionInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2GetIpcHandle(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -455,21 +420,11 @@ int APS5_VABI sceAudioOut2GetSystemStateEx(void) { return 0; } -int APS5_VABI sceAudioOut2GetSystemDebugState(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2MasteringInitEx(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAudioOut2SetSpeakerInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2SetDebugHeadphoneMode(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -500,11 +455,6 @@ int APS5_VABI sceAudioOut2PortCreateEx(void) { return 0; } -int APS5_VABI sceAudioOut2PortSetAttributesEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2InitializeEx(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -535,9 +485,4 @@ int APS5_VABI sceAudioOut2GetHrtfIdForCronos(void) { return 0; } -int APS5_VABI sceAudioOut2SetHrtfIdForCronos(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 4ce0e79bfb..905733011a 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -32,8 +32,6 @@ - [sceAudioOut2LoContextAdvance](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoContextPush](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoContextGetQueueLevel](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2LoPortSetAttributesEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2LoPortGetAttributes](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutSetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoPortGetState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutGetHandleStatusInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature @@ -56,7 +54,6 @@ - [sceAudioOutMasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutMasteringGetState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutMasteringTerm](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2LoPortCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutSetPadSpkVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutSetPadJackVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutSetUsbVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature @@ -90,10 +87,7 @@ - [sceAudioOutSysGetSystemInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutSysGetOutputInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoContextQueryMemory](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2LoContextQueryMemoryEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2LoContextGetAttributes](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoContextCreate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2LoContextCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoContextSetAttributes](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2MbusInit](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2MbusSetPortConnections](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature @@ -111,26 +105,21 @@ - [sceAudioOut2ReportRegisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2ReportUnregisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetTvCorrectionInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2SetTvCorrectionInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetIpcHandle](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetSystemStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2GetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2MasteringInitEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2SetSpeakerInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2SetDebugHeadphoneMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2DebugStateCtrl](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoInit](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2ContextBedWrite](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2PortCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2PortSetAttributesEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2InitializeEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2Terminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2PortRegister](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2PortUnregister](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetAmbisonicsEncodeCoefficients](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetHrtfIdForCronos](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2SetHrtfIdForCronos](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature ### Functional From 0ef3bed1ae26ff5afdead741d13cb74306f78642 Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Tue, 20 Oct 2026 06:27:25 -0300 Subject: [PATCH 112/167] fix(libSceHmd2): drop exports already declared elsewhere or absent from aerolib --- core/libs/prx/libSceHmd2/Export.cpp | 400 ---------------------------- docs/dev/debt/libSceHmd2.md | 80 ------ 2 files changed, 480 deletions(-) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index 94a23df038..ab5733971f 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -377,71 +377,16 @@ int APS5_VABI sceHmd2SetIpdOffset() { return 0; } -int APS5_VABI sceHmd2ReprojectionClearEventGpuEnd(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalSlamTraceStop(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionClearEventGpuStart(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalRelocalizerSetResultData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalSetSlamCommand(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalSetBudgetMode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalRelocalizerOpen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2InternalSetPortStatus(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2InternalPowerEventInitialize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalRelocalizerClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalTrackingStart(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2InternalLedDetectorStartBySlot(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2InternalGetDeviceInformation(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2InternalImageStartBySlot(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -462,31 +407,11 @@ int APS5_VABI sceHmd2InternalSetFakeLensSeparationDistance(void) { return 0; } -int APS5_VABI sceHmd2InternalResetViewPosition(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeGetEnabledEye(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2InternalDfuSend(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2InternalTrackingReadData2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalGetVrMode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2InternalLedDetectorOpenBySlot(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -497,86 +422,6 @@ int APS5_VABI sceHmd2InternalImageOpenBySlot(void) { return 0; } -int APS5_VABI sceHmd2InternalGetFwVersion(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalImageReadData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalGetTrackerFtsData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalTrackingOpen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalGazeReadStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalRelocalizerStart(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalGetStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalSetPowerOff(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalCrashReportOpen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalTrackingClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalImageClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalGazeStop(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalCrashReportClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalDfuOpen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalImageSetConfigBySlot(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalSetCameraSeeThroughParam(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2InternalLedDetectorStopBySlot(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -587,289 +432,44 @@ int APS5_VABI sceHmd2InternalSetDeviceConnection(void) { return 0; } -int APS5_VABI sceHmd2InternalGazeOpen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalRelocalizerGetVersion(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalImageOpen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalGenDataStop(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalGetFakeLensSeparationDistance(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalDecryptData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalUnregisterVibrationCallback(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalRelocalizerStop(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalClearTrackerFts(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalSetHomeCalibration(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2InternalLedDetectorCloseBySlot(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2InternalTrackingStop(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalLedDetectorOpen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalDfuClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalGenDataStart(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalIsGameVr2d(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2InternalSetSidetone(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2InternalImageGetFrameData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalSlamTraceClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalLedDetectorClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalSetPositionResetIcon(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalIsCommonDlgVr2d(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalIsMiniAppVr2d(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalGazeClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalLedDetectorSetPriorityConfigBySlot(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalDfuStart(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalImageSetConfig(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalGenDataClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2InternalGetDeviceId(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2InternalSetForceIcon(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalLedDetectorStart(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalSetVolume(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalGetSlamCommand(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalImageGetAverageLuminance(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalTrackingReadData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2InternalSetBrightness(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2InternalDfuGetStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2InternalImageStopBySlot(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2InternalImageGetMemoryLayout(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2InternalSetVibrationMode(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2InternalGenDataOpen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalImageStart(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2InternalGetVr2dData(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2InternalGazeStart(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalRelocalizerReadDataAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalMapSharedMemory(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalOpen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalSlamTraceStart(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalLedDetectorSetIrCameraConfigBySlot(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalImageStop(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalIsCommonDlgMiniAppVr2d(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalGetMirroringMode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalRegisterVibrationCallback(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2InternalImageCloseBySlot(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2InternalGetOpticalParam(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalPushVr2dData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2InternalSlamTraceOpen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index 00cde1c41d..4ebd9bb64e 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -12,105 +12,25 @@ - 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 -- [sceHmd2ReprojectionClearEventGpuEnd](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalSlamTraceStop](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2ReprojectionClearEventGpuStart](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalRelocalizerSetResultData](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalSetSlamCommand](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalSetBudgetMode](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalRelocalizerOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalSetPortStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalPowerEventInitialize](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalRelocalizerClose](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalTrackingStart](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalLedDetectorStartBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalGetDeviceInformation](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalImageStartBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalSetPortConnection](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalSetCameraSeeThroughMode](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalSetFakeLensSeparationDistance](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalResetViewPosition](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2GazeGetEnabledEye](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalDfuSend](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalTrackingReadData2](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalGetVrMode](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalLedDetectorOpenBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalImageOpenBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalGetFwVersion](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalImageReadData](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalGetTrackerFtsData](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalTrackingOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalGazeReadStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalRelocalizerStart](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalGetStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalSetPowerOff](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalCrashReportOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalTrackingClose](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalImageClose](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalGazeStop](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalCrashReportClose](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalDfuOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalImageSetConfigBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalSetCameraSeeThroughParam](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalLedDetectorStopBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalSetDeviceConnection](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalGazeOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalRelocalizerGetVersion](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalImageOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalGenDataStop](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalGetFakeLensSeparationDistance](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalDecryptData](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalUnregisterVibrationCallback](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalRelocalizerStop](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalClearTrackerFts](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalSetHomeCalibration](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalLedDetectorCloseBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalTrackingStop](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalLedDetectorOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalDfuClose](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalGenDataStart](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalIsGameVr2d](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalSetSidetone](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalImageGetFrameData](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalSlamTraceClose](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalLedDetectorClose](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalSetPositionResetIcon](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalIsCommonDlgVr2d](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalIsMiniAppVr2d](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalGazeClose](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalLedDetectorSetPriorityConfigBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalDfuStart](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalImageSetConfig](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalGenDataClose](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalGetDeviceId](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalSetForceIcon](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalLedDetectorStart](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalSetVolume](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalGetSlamCommand](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalImageGetAverageLuminance](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalTrackingReadData](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalSetBrightness](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalDfuGetStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalImageStopBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalImageGetMemoryLayout](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalSetVibrationMode](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalGenDataOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalImageStart](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalGetVr2dData](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalGazeStart](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalRelocalizerReadDataAsync](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalMapSharedMemory](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalSlamTraceStart](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalLedDetectorSetIrCameraConfigBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalImageStop](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalIsCommonDlgMiniAppVr2d](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalGetMirroringMode](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalRegisterVibrationCallback](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalImageCloseBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalGetOpticalParam](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalPushVr2dData](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature -- [sceHmd2InternalSlamTraceOpen](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature ### Functional From 4c849bcadfcc6f535a19c74d520c90e1f5846b98 Mon Sep 17 00:00:00 2001 From: primesoul-web Date: Tue, 20 Oct 2026 09:32:35 +0000 Subject: [PATCH 113/167] fix(libSceIme): discard the edit when a caret move drops the filtered commit --- core/libs/prx/libSceIme/Export.cpp | 9 ++++++-- core/libs/tests/GuestImeOpen.cpp | 34 ++++++++++++++++++++++++++++++ docs/dev/debt/libSceIme.md | 2 +- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index ea355e802e..a2979dea88 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -554,8 +554,13 @@ int APS5_VABI sceImeUpdate(EventHandler handler) { std::u16string filtered; const bool accepted = RunTextFilter(filter, filterText, filterMaxLength, filterMultiline, &filtered); std::lock_guard lock(g_imeMutex); - if (g_session && g_session->id == sessionId && g_session->revision == sessionRevision) { - CommitFilteredEdit(*g_session, accepted ? &filtered : nullptr, filterOldText); + if (g_session && g_session->id == sessionId) { + if (g_session->revision == sessionRevision) { + CommitFilteredEdit(*g_session, accepted ? &filtered : nullptr, filterOldText); + } else if (g_session->text == filterText) { + g_session->text = filterOldText; + NormalizeCaret(*g_session); + } } continue; } diff --git a/core/libs/tests/GuestImeOpen.cpp b/core/libs/tests/GuestImeOpen.cpp index a1ce465687..ce516d5530 100644 --- a/core/libs/tests/GuestImeOpen.cpp +++ b/core/libs/tests/GuestImeOpen.cpp @@ -146,6 +146,22 @@ int32_t APS5_VABI SetInsideFilter(char16_t* outText, uint32_t* outTextLength, co return 0; } +bool g_caretInsideFirstCall = true; +int g_caretInsideRc = -1; + +int32_t APS5_VABI SetCaretInsideFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + if (g_caretInsideFirstCall) { + g_caretInsideFirstCall = false; + const Caret caret{0.0f, 0.0f, 0, 0}; + g_caretInsideRc = sceImeSetCaret(&caret); + } + for (uint32_t i = 0; i < sourceTextLength; ++i) { + outText[i] = sourceText[i]; + } + *outTextLength = sourceTextLength; + return 0; +} + void Require(bool value, const char* message) { if (value) return; std::fprintf(stderr, "%s\n", message); @@ -494,6 +510,24 @@ void CheckTextFilter() { Require(std::u16string(buffers.text) == u"zz", "the setText inside the filter wins"); Require(std::u16string(reinterpret_cast(buffers.work)) == u"zz", "the setText inside the filter wins in the work copy"); Require(sceImeClose_nid_postfix() == 0, "close the set-inside session"); + + buffers = Buffers{}; + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = reinterpret_cast(&SetCaretInsideFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a filter that moves the caret"); + Drain(); + g_caretInsideFirstCall = true; + g_caretInsideRc = -1; + Publish(ImeHostInputKind::Text, "c"); + Require(Drain().empty(), "an edit whose filter moved the caret sends no event"); + Require(g_caretInsideRc == 0, "sceImeSetCaret inside the filter returns 0"); + Require(std::u16string(buffers.text) == u"ab", "the discarded edit is not in the buffer"); + Publish(ImeHostInputKind::Text, "d"); + RequireText(Drain().at(0), EventUpdateText, u"dab", 1, 0, 1); + Require(std::u16string(buffers.text) == u"dab", "the edit after the discard builds on the old text"); + Require(std::u16string(reinterpret_cast(buffers.work)) == u"dab", "the edit after the discard reaches the work copy"); + Require(sceImeClose_nid_postfix() == 0, "close the caret-inside session"); } void CheckHostEvents() { diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index bcce37a189..9d4d0abbdd 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -58,4 +58,4 @@ - [sceImeKeyboardOpen, sceImeKeyboardClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - opening a keyboard that is already open for the same user returns `0x80bc0001` (busy) and closing one that is not open returns `0x80bc0002`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/1211958b4eb38256cc0c5c7ba92d668820c05389/src/libs/ime.cpp) does; it tracks one keyboard for all users, while the open state is kept per user id here, and the PS5 result is unverified - [sceImeKeyboardGetInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. - [sceImeKeyboardGetResourceId](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard -- [sceImeOpen text filter](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) runs on the text edits of host input - typing, backspace, forward delete and a multiline Enter - following [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/1211958b4eb38256cc0c5c7ba92d668820c05389/src/libs/ime.cpp), not confirmed on a PS5. The host input thread only queues the edit under the IME mutex; `sceImeUpdate` applies it on the title's thread, where the filter also runs with the mutex released - like the event handler, so a filter that calls back into `sceIme*` cannot deadlock - and the structural edit and the filtered commit are serialized under the mutex, with a commit after the session closed or reopened dropped by a session id. The filter is guest code and `ImeTextFilter` and `ImeExtKeyboardFilter` are declared `APS5_VABI` like `EventHandler`. The filter is called with the edited text, a buffer of `maxTextLength` UTF-16 units and its length; a nonzero return, an output longer than the capacity, an unpaired surrogate or, without the multiline option, a line break keeps the edit unfiltered. An accepted output replaces the text, the caret stays where the edit put it and is clamped to the text and normalized past a surrogate pair, and one UpdateText is sent with the minimal edit delta of the whole old and new text, as KytyPS5 computes it; an output equal to the previous text sends no event. Caret moves, PressEnter without multiline, PressClose, `sceImeSetText` and `sceImeSetCaret` do not run the filter, as in KytyPS5; a `sceImeSetText` or `sceImeSetCaret` that lands while the filter runs bumps a revision beside the session id, and the filtered commit is dropped when either changed, as KytyPS5 drops the commit when its revision changed, so the title's own text or caret setting wins and the edit sends no event. An external keyboard filter is accepted and stored on the session: it is called only for keyboard events that carry key data, which the host input path does not produce - it delivers text and actions, as KytyPS5's host API queues external inputs with a zeroed key and skips the filter for them - so it is never called +- [sceImeOpen text filter](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) runs on the text edits of host input - typing, backspace, forward delete and a multiline Enter - following [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/1211958b4eb38256cc0c5c7ba92d668820c05389/src/libs/ime.cpp), not confirmed on a PS5. The host input thread only queues the edit under the IME mutex; `sceImeUpdate` applies it on the title's thread, where the filter also runs with the mutex released - like the event handler, so a filter that calls back into `sceIme*` cannot deadlock - and the structural edit and the filtered commit are serialized under the mutex, with a commit after the session closed or reopened dropped by a session id. The filter is guest code and `ImeTextFilter` and `ImeExtKeyboardFilter` are declared `APS5_VABI` like `EventHandler`. The filter is called with the edited text, a buffer of `maxTextLength` UTF-16 units and its length; a nonzero return, an output longer than the capacity, an unpaired surrogate or, without the multiline option, a line break keeps the edit unfiltered. An accepted output replaces the text, the caret stays where the edit put it and is clamped to the text and normalized past a surrogate pair, and one UpdateText is sent with the minimal edit delta of the whole old and new text, as KytyPS5 computes it; an output equal to the previous text sends no event. Caret moves, PressEnter without multiline, PressClose, `sceImeSetText` and `sceImeSetCaret` do not run the filter, as in KytyPS5; a `sceImeSetText` or `sceImeSetCaret` that lands while the filter runs bumps a revision beside the session id, and the filtered commit is dropped when either changed, as KytyPS5 drops the commit when its revision changed, so the title's own text or caret setting wins, the edit is discarded and sends no event. An external keyboard filter is accepted and stored on the session: it is called only for keyboard events that carry key data, which the host input path does not produce - it delivers text and actions, as KytyPS5's host API queues external inputs with a zeroed key and skips the filter for them - so it is never called From 2d1c18410f483de5a9afd432a230256745c5c051 Mon Sep 17 00:00:00 2001 From: mazen Date: Mon, 19 Oct 2026 12:20:31 +0400 Subject: [PATCH 114/167] fix(libkernel): keep host stderr off a guest dup2 onto descriptor 2 Guest dup2 onto 2 replaces the CRT descriptor fprintf(stderr) still uses, so host logs fill the 4 KiB guest pipe. --- core/libs/prx/libkernel/CMakeLists.txt | 9 +++ core/libs/prx/libkernel/File/src/Stdio.cpp | 21 +++++++ .../tests/GuestHostStandardErrorWindows.cpp | 59 +++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 4 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestHostStandardErrorWindows.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index fabbadfbda..7a32329e25 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -321,6 +321,15 @@ if(BUILD_TESTING) add_test(NAME guest_closed_standard_descriptors COMMAND guest_closed_standard_descriptors_tests) set_tests_properties(guest_closed_standard_descriptors PROPERTIES TIMEOUT 10) endif() + if(WIN32) + add_test_executable(guest_host_standard_error_windows_tests ${LIBS_INCLUDE_DIR}/tests/GuestHostStandardErrorWindows.cpp) + target_include_directories(guest_host_standard_error_windows_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_host_standard_error_windows_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_host_standard_error_windows_tests) + add_test(NAME guest_host_standard_error_windows COMMAND guest_host_standard_error_windows_tests) + set_tests_properties(guest_host_standard_error_windows PROPERTIES TIMEOUT 10 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_truncate_tests ${LIBS_INCLUDE_DIR}/tests/GuestTruncate.cpp) target_include_directories(guest_truncate_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_truncate_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 4d786eb038..7afd64bbd2 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -56,6 +56,7 @@ static constexpr std::uint64_t KERNEL_MNT_NOSUID = 0x8; #ifdef _WIN32 #include #include +#include #include #include #include @@ -710,6 +711,16 @@ int APS5_VABI _close_nid_postfix(int descriptor) { namespace { std::atomic g_hostStandardError{-1}; +#ifdef _WIN32 +void RetargetHostStandardError(int saved) { + std::fflush(stderr); + auto* const stream = __acrt_iob_func(2); + auto* const descriptor = reinterpret_cast(reinterpret_cast(stream) + 24); + if (*descriptor != 2) throw std::runtime_error("dup2: host stderr descriptor field is not 2"); + *descriptor = saved; +} +#endif + void PreserveHostStandardError() { if (g_hostStandardError.load() >= 0) return; #ifdef _WIN32 @@ -723,7 +734,17 @@ void PreserveHostStandardError() { #ifdef _WIN32 _close(saved); #endif + return; } +#ifdef _WIN32 + try { + RetargetHostStandardError(saved); + } catch (...) { + g_hostStandardError.store(-1); + _close(saved); + throw; + } +#endif } } diff --git a/core/libs/tests/GuestHostStandardErrorWindows.cpp b/core/libs/tests/GuestHostStandardErrorWindows.cpp new file mode 100644 index 0000000000..f04312454c --- /dev/null +++ b/core/libs/tests/GuestHostStandardErrorWindows.cpp @@ -0,0 +1,59 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#include +#endif + +extern "C" { +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI dup2_nid_postfix(int, int); +std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::size_t); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static DWORD Available(int descriptor, void* buffer, DWORD capacity, DWORD& read) { + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + DWORD available = 0; + Require(::PeekNamedPipe(handle, buffer, capacity, &read, &available, nullptr) != 0); + return available; +} + +int main() { + int capture[2]{-1, -1}; + Require(::_pipe(capture, 65536, _O_BINARY) == 0); + const int original = ::_dup(2); + Require(original >= 0 && ::_dup2(capture[1], 2) == 0); + + int guest[2]{-1, -1}; + Require(pipe_nid_postfix(guest) == 0); + Require(dup2_nid_postfix(guest[1], 2) == 2); + + std::string host(8192, 'h'); + host.back() = '\n'; + Require(std::fputs(host.c_str(), stderr) >= 0); + Require(std::fflush(stderr) == 0); + Require(write_nid_postfix(2, "guest", 5) == 5); + + char guestBytes[8]{}; + DWORD guestRead = 0; + Require(Available(guest[0], guestBytes, sizeof(guestBytes), guestRead) == 5); + Require(guestRead == 5 && std::string(guestBytes, 5) == "guest"); + char hostBytes[8]{}; + DWORD hostRead = 0; + Require(Available(capture[0], hostBytes, sizeof(hostBytes), hostRead) == host.size()); + Require(hostRead == sizeof(hostBytes) && std::string(hostBytes, hostRead) == host.substr(0, hostRead)); + + Require(::_dup2(original, 2) == 0); + ::_close(original); + ::_close(capture[0]); + ::_close(capture[1]); + ::_close(guest[0]); + ::_close(guest[1]); + return 0; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 9d2edf5790..ae3686ee08 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -158,7 +158,7 @@ - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified -- [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table, and on Windows duplicating a directory descriptor. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. +- [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table, and on Windows duplicating a directory descriptor. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. On Windows the UCRT `FILE` from `__acrt_iob_func(2)` is opaque: the descriptor word 24 bytes in matched `_fileno(stderr)` and an `_fdopen` duplicate on WinLibs GCC 15.2.0 UCRT, while a word at byte 32 held 2 on the original stream and 0 on the duplicate, so only byte 24 is written and only when it is 2. A runtime whose word is not 2 throws. Descriptor 1 is not retargeted, so host `stdout` still follows a guest `dup2` onto 1 (#7791) - [waitpid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns -1 with `ECHILD` (`EINVAL` for unknown options): nothing here creates a child process; child processes would need a real `waitpid`. - [execv](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it. - [execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws like `execvp`: replacing the process image is not supported. The console behavior for invalid arguments and executable path lookups is not modeled. From 715fe9ca265ac5401c598b8b5003f9241e52e6b7 Mon Sep 17 00:00:00 2001 From: mazen Date: Mon, 19 Oct 2026 12:36:01 +0400 Subject: [PATCH 115/167] fix(libkernel): keep host stdout off a guest dup2 onto descriptor 1 The same guest dup2 that fills the stderr pipe also replaces descriptor 1, so host stdout was still writing into the guest pipe. --- core/libs/prx/libc/include/General.hpp | 1 + core/libs/prx/libc/src/General.cpp | 26 ++++++++++++++++ core/libs/prx/libkernel/File/src/Stdio.cpp | 30 +++++++++++-------- core/libs/tests/GuestHostStandardError.cpp | 24 +++++++++++++++ .../tests/GuestHostStandardErrorWindows.cpp | 21 ++++++++----- docs/dev/debt/libkernel.md | 2 +- 6 files changed, 83 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 2db5852780..3c23138fcb 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -16,6 +16,7 @@ extern "C" bool StopAtPrefixLetter_nid_no_patch(const char* str, char** endptr, #ifndef _WIN32 extern "C" int KeepHostStandardError_nid_no_patch(); extern "C" int HostStandardError_nid_no_patch(); +extern "C" int KeepHostStandardOutput_nid_no_patch(); #endif extern "C" bool CopyGuestEnvironment_nid_no_patch(const char* name, std::string& value); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 7b86b2c269..d94f7e4509 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -422,6 +422,32 @@ extern "C" int HostStandardError_nid_no_patch() { return g_hostStandardError.load(); } +namespace { +std::mutex g_hostStandardOutputMutex; +std::atomic g_hostStandardOutput{-1}; +} + +extern "C" int KeepHostStandardOutput_nid_no_patch() { + const std::lock_guard lock(g_hostStandardOutputMutex); + if (const int kept = g_hostStandardOutput.load(); kept >= 0) return kept; + const int kept = ::fcntl(1, F_DUPFD_CLOEXEC, 3); + if (kept < 0) { + if (errno == EBADF) return -1; + throw std::system_error(errno, std::generic_category(), "Keeping the host standard output"); + } + std::FILE* const stream = ::fdopen(kept, "w"); + if (stream == nullptr) { + const int error = errno; + ::close(kept); + throw std::system_error(error, std::generic_category(), "Keeping the host standard output"); + } + std::setvbuf(stream, nullptr, _IONBF, 0); + std::fflush(stdout); + stdout = stream; + g_hostStandardOutput.store(kept); + return kept; +} + extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor) { return ::fcntl(descriptor, F_DUPFD_CLOEXEC, 3); } diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 7afd64bbd2..4ceba06865 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -710,27 +710,28 @@ int APS5_VABI _close_nid_postfix(int descriptor) { namespace { std::atomic g_hostStandardError{-1}; +std::atomic g_hostStandardOutput{-1}; #ifdef _WIN32 -void RetargetHostStandardError(int saved) { - std::fflush(stderr); - auto* const stream = __acrt_iob_func(2); +void RetargetHostStream(int standard, int saved) { + auto* const stream = __acrt_iob_func(standard); + std::fflush(stream); auto* const descriptor = reinterpret_cast(reinterpret_cast(stream) + 24); - if (*descriptor != 2) throw std::runtime_error("dup2: host stderr descriptor field is not 2"); + if (*descriptor != standard) throw std::runtime_error("dup2: host standard stream descriptor field is not " + std::to_string(standard)); *descriptor = saved; } #endif -void PreserveHostStandardError() { - if (g_hostStandardError.load() >= 0) return; +void PreserveHostStream(int standard, std::atomic& slot) { + if (slot.load() >= 0) return; #ifdef _WIN32 - const int saved = WithoutParameterHandler([] { return ::_dup(2); }); + const int saved = WithoutParameterHandler([standard] { return ::_dup(standard); }); #else - const int saved = KeepHostStandardError_nid_no_patch(); + const int saved = standard == 1 ? KeepHostStandardOutput_nid_no_patch() : KeepHostStandardError_nid_no_patch(); #endif - if (saved < 0) throw std::runtime_error("dup2: cannot keep the host standard error"); + if (saved < 0) throw std::runtime_error(standard == 1 ? "dup2: cannot keep the host standard output" : "dup2: cannot keep the host standard error"); int expected = -1; - if (!g_hostStandardError.compare_exchange_strong(expected, saved)) { + if (!slot.compare_exchange_strong(expected, saved)) { #ifdef _WIN32 _close(saved); #endif @@ -738,14 +739,17 @@ void PreserveHostStandardError() { } #ifdef _WIN32 try { - RetargetHostStandardError(saved); + RetargetHostStream(standard, saved); } catch (...) { - g_hostStandardError.store(-1); + slot.store(-1); _close(saved); throw; } #endif } + +void PreserveHostStandardError() { PreserveHostStream(2, g_hostStandardError); } +void PreserveHostStandardOutput() { PreserveHostStream(1, g_hostStandardOutput); } } int File::HostStandardError() { @@ -791,6 +795,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { #ifdef _WIN32 if (WithoutParameterHandler([from] { return ::_get_osfhandle(from) == -1 ? -1 : 0; }) != 0) return PosixFailure(GUEST_EBADF); if (from == to) return to; + if (to == 1) PreserveHostStandardOutput(); if (to == 2) PreserveHostStandardError(); auto* const standard = HostStandardHandle_nid_no_patch(to); const int duplicated = WithoutParameterHandler([from, to] { return ::_dup2(from, to); }); @@ -811,6 +816,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { if (::fcntl(from, F_GETFL) < 0) return PosixFailure(GUEST_EBADF); return to; } + if (to == 1) PreserveHostStandardOutput(); if (to == 2) PreserveHostStandardError(); const int result = ::dup2(from, to); if (result < 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); diff --git a/core/libs/tests/GuestHostStandardError.cpp b/core/libs/tests/GuestHostStandardError.cpp index bf4df157ce..de3237cc91 100644 --- a/core/libs/tests/GuestHostStandardError.cpp +++ b/core/libs/tests/GuestHostStandardError.cpp @@ -101,5 +101,29 @@ int main() { if (redirect == Redirect::Reopen) Require(Contents(probe).find("report kept") == std::string::npos); } std::filesystem::remove(probe); + + int host[2] = {-1, -1}; + Require(::pipe(host) == 0); + const pid_t child = ::fork(); + Require(child >= 0); + if (child == 0) { + ::close(host[0]); + if (::dup2(host[1], 1) != 1) ::_exit(70); + ::close(host[1]); + int guest[2] = {-1, -1}; + if (pipe_nid_postfix(guest) != 0 || dup2_nid_postfix(guest[1], 1) != 1) ::_exit(72); + std::fputs("host\n", stdout); + std::fflush(stdout); + ::_exit(0); + } + ::close(host[1]); + std::string output; + char buffer[64]; + for (ssize_t count; (count = ::read(host[0], buffer, sizeof(buffer))) > 0;) output.append(buffer, static_cast(count)); + ::close(host[0]); + int status = 0; + Require(::waitpid(child, &status, 0) == child); + Require(WIFEXITED(status) && WEXITSTATUS(status) == 0); + Require(output == "host\n"); return 0; } diff --git a/core/libs/tests/GuestHostStandardErrorWindows.cpp b/core/libs/tests/GuestHostStandardErrorWindows.cpp index f04312454c..28be899f90 100644 --- a/core/libs/tests/GuestHostStandardErrorWindows.cpp +++ b/core/libs/tests/GuestHostStandardErrorWindows.cpp @@ -24,21 +24,21 @@ static DWORD Available(int descriptor, void* buffer, DWORD capacity, DWORD& read return available; } -int main() { +static void Split(int standard, std::FILE* stream) { int capture[2]{-1, -1}; Require(::_pipe(capture, 65536, _O_BINARY) == 0); - const int original = ::_dup(2); - Require(original >= 0 && ::_dup2(capture[1], 2) == 0); + const int original = ::_dup(standard); + Require(original >= 0 && ::_dup2(capture[1], standard) == 0); int guest[2]{-1, -1}; Require(pipe_nid_postfix(guest) == 0); - Require(dup2_nid_postfix(guest[1], 2) == 2); + Require(dup2_nid_postfix(guest[1], standard) == standard); std::string host(8192, 'h'); host.back() = '\n'; - Require(std::fputs(host.c_str(), stderr) >= 0); - Require(std::fflush(stderr) == 0); - Require(write_nid_postfix(2, "guest", 5) == 5); + Require(std::fputs(host.c_str(), stream) >= 0); + Require(std::fflush(stream) == 0); + Require(write_nid_postfix(standard, "guest", 5) == 5); char guestBytes[8]{}; DWORD guestRead = 0; @@ -49,11 +49,16 @@ int main() { Require(Available(capture[0], hostBytes, sizeof(hostBytes), hostRead) == host.size()); Require(hostRead == sizeof(hostBytes) && std::string(hostBytes, hostRead) == host.substr(0, hostRead)); - Require(::_dup2(original, 2) == 0); + Require(::_dup2(original, standard) == 0); ::_close(original); ::_close(capture[0]); ::_close(capture[1]); ::_close(guest[0]); ::_close(guest[1]); +} + +int main() { + Split(2, stderr); + Split(1, stdout); return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index ae3686ee08..be9c7f181f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -158,7 +158,7 @@ - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified -- [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table, and on Windows duplicating a directory descriptor. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. On Windows the UCRT `FILE` from `__acrt_iob_func(2)` is opaque: the descriptor word 24 bytes in matched `_fileno(stderr)` and an `_fdopen` duplicate on WinLibs GCC 15.2.0 UCRT, while a word at byte 32 held 2 on the original stream and 0 on the duplicate, so only byte 24 is written and only when it is 2. A runtime whose word is not 2 throws. Descriptor 1 is not retargeted, so host `stdout` still follows a guest `dup2` onto 1 (#7791) +- [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table, and on Windows duplicating a directory descriptor. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. On Windows the UCRT `FILE` from `__acrt_iob_func` is opaque: the descriptor word 24 bytes in matched `_fileno` for stdout and for stderr, and an `_fdopen` duplicate, on WinLibs GCC 15.2.0 UCRT. A word at byte 32 held 2 on the original stderr and 0 on its duplicate, and 4096 on stdout, so only byte 24 is written, and only when it equals that stream's descriptor. A runtime whose word does not throws (#7791) - [waitpid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns -1 with `ECHILD` (`EINVAL` for unknown options): nothing here creates a child process; child processes would need a real `waitpid`. - [execv](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it. - [execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws like `execvp`: replacing the process image is not supported. The console behavior for invalid arguments and executable path lookups is not modeled. From c03168fb472737814d6f0cc81de6b270c6a8ea14 Mon Sep 17 00:00:00 2001 From: mazen Date: Mon, 19 Oct 2026 16:44:15 +0400 Subject: [PATCH 116/167] fix(libc): keep guest stdio on a dup2 of descriptors 1 and 2 Host logging opens its own FILE on the saved descriptor. The guest FILE stays on the replaced descriptor, so guest printf, puts and fprintf still reach the pipe. --- core/libs/CMakeLists.txt | 6 +++ core/libs/prx/libc/include/General.hpp | 5 +++ .../prx/libc/include/HostStandardStreams.hpp | 21 ++++++++++ .../prx/libc/include/WindowsFormatting.hpp | 3 +- core/libs/prx/libc/src/Formatting.cpp | 12 +++--- core/libs/prx/libc/src/FormattingWide.cpp | 2 +- core/libs/prx/libc/src/General.cpp | 26 ++++++++++++ core/libs/prx/libc/src/RuntimeSupport.cpp | 4 ++ core/libs/prx/libkernel/File/src/Stdio.cpp | 16 +------- core/libs/tests/GuestHostStandardError.cpp | 39 ++++++++++++++++++ .../tests/GuestHostStandardErrorWindows.cpp | 41 +++++++++++++++++-- docs/dev/debt/libkernel.md | 2 +- 12 files changed, 152 insertions(+), 25 deletions(-) create mode 100644 core/libs/prx/libc/include/HostStandardStreams.hpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 50373d1bf8..933041b732 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -82,6 +82,9 @@ add_library(libc SHARED EXCLUDE_FROM_ALL) set(LIBS_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) add_subdirectory(${LIBC_DIR} ${CMAKE_CURRENT_BINARY_DIR}/prx/libc) target_include_directories(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +if(WIN32) + target_compile_options(libc PRIVATE "SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/include/HostStandardStreams.hpp") +endif() configure_windows_unwind(libc) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(libc PRIVATE LINKER:-z,now) @@ -139,6 +142,9 @@ foreach(LIB_DIR ${LIBS_DIRS}) configure_windows_unwind(${LIB_NAME}) endif() + if(WIN32) + target_compile_options(${LIB_NAME} PRIVATE "SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/include/HostStandardStreams.hpp") + endif() if(LIB_NAME STREQUAL "libkernel") target_link_libraries(${LIB_NAME} PUBLIC libc) else() diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 3c23138fcb..333900b870 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "general/LogMacros.hpp" #include "general/VabiMacros.hpp" @@ -17,6 +18,10 @@ extern "C" bool StopAtPrefixLetter_nid_no_patch(const char* str, char** endptr, extern "C" int KeepHostStandardError_nid_no_patch(); extern "C" int HostStandardError_nid_no_patch(); extern "C" int KeepHostStandardOutput_nid_no_patch(); +#else +extern "C" bool AdoptHostStandardStream_nid_no_patch(int standard, int saved); +extern "C" std::FILE* HostStandardOutputFile_nid_no_patch(); +extern "C" std::FILE* HostStandardErrorFile_nid_no_patch(); #endif extern "C" bool CopyGuestEnvironment_nid_no_patch(const char* name, std::string& value); diff --git a/core/libs/prx/libc/include/HostStandardStreams.hpp b/core/libs/prx/libc/include/HostStandardStreams.hpp new file mode 100644 index 0000000000..9d0f14bf2e --- /dev/null +++ b/core/libs/prx/libc/include/HostStandardStreams.hpp @@ -0,0 +1,21 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_HOSTSTANDARDSTREAMS_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_HOSTSTANDARDSTREAMS_HPP + +#include + +#ifdef _WIN32 +#undef stdout +#undef stderr +#ifdef __cplusplus +extern "C" { +#endif +FILE* HostStandardOutputFile_nid_no_patch(void); +FILE* HostStandardErrorFile_nid_no_patch(void); +#ifdef __cplusplus +} +#endif +#define stdout HostStandardOutputFile_nid_no_patch() +#define stderr HostStandardErrorFile_nid_no_patch() +#endif + +#endif diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index c143d2a17e..0c8dd2861f 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -16,6 +16,7 @@ #include extern "C" int APS5_VABI strerror_r_nid_postfix(int error, char* buffer, std::size_t length); +extern "C" std::FILE* GuestStandardOutputFile_nid_no_patch(); namespace LibcDetail { @@ -413,7 +414,7 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo inline int PrintWindows(const char* format, const void* args) { std::string buffer; const int size = FormatWindows(nullptr, 0, format, args, &buffer); - if (std::fwrite(buffer.data(), 1, static_cast(size), stdout) != static_cast(size)) + if (std::fwrite(buffer.data(), 1, static_cast(size), GuestStandardOutputFile_nid_no_patch()) != static_cast(size)) throw std::runtime_error("Formatted output write failed"); return size; } diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 0d883aa904..2973bd7697 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -350,7 +350,7 @@ int APS5_VABI printf_nid_postfix(const char* format, ...) { if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } std::va_list args; va_start(args, format); - const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vfprintf(GuestStandardOutputFile_nid_no_patch(), format, args); va_end(args); return result; } @@ -359,7 +359,7 @@ int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } std::va_list args; va_start(args, format); - const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vfprintf(GuestStandardOutputFile_nid_no_patch(), format, args); va_end(args); return result; } @@ -445,7 +445,7 @@ int APS5_VABI vprintf_nid_postfix(const char* str, VaList* c) { if (RequiresGuestFormatting(str)) return LibcDetail::PrintWindows(str, c); std::va_list copy; va_copy(copy, *reinterpret_cast(c)); - const int result = std::vprintf(str, copy); + const int result = std::vfprintf(GuestStandardOutputFile_nid_no_patch(), str, copy); va_end(copy); return result; #endif @@ -553,7 +553,7 @@ int APS5_VABI snprintf_s_nid_postfix(char* buffer, size_t size, const char* form int APS5_VABI printf_s_nid_postfix(const char* format, ...) { std::va_list args; va_start(args, format); - const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vfprintf(GuestStandardOutputFile_nid_no_patch(), format, args); va_end(args); return result; } @@ -562,7 +562,9 @@ int APS5_VABI printf_s_nid_postfix(const char* format, ...) { int APS5_VABI puts_nid_postfix(const char* s) { if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } - return std::puts(s); + auto* native = GetNativeStream(&_Stdout_nid_postfix); + if (std::fputs(s, native) < 0 || std::fputc('\n', native) == EOF) return EOF; + return 0; } } diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index 713a520591..760ed04492 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -325,7 +325,7 @@ int APS5_VABI wprintf_nid_postfix(const char16_t* format, ...) { try { const std::u16string text = FormatWide(format, reinterpret_cast(args)); const std::string bytes = ToUtf8(text); - if (std::fwrite(bytes.data(), 1, bytes.size(), stdout) == bytes.size()) result = static_cast(text.size()); + if (std::fwrite(bytes.data(), 1, bytes.size(), GetNativeStream(__stdoutp_nid_postfix)) == bytes.size()) result = static_cast(text.size()); } catch (const std::exception&) { errno = 22; } diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index d94f7e4509..d3c8d0753c 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -453,6 +453,32 @@ extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor) { } #endif +#ifdef _WIN32 +namespace { +std::atomic g_hostStandardOutputFile{nullptr}; +std::atomic g_hostStandardErrorFile{nullptr}; +} + +extern "C" bool AdoptHostStandardStream_nid_no_patch(int standard, int saved) { + std::fflush(__acrt_iob_func(standard)); + std::FILE* stream = ::_fdopen(saved, "w"); + if (stream == nullptr) return false; + std::setvbuf(stream, nullptr, _IONBF, 0); + (standard == 1 ? g_hostStandardOutputFile : g_hostStandardErrorFile).store(stream, std::memory_order_release); + return true; +} + +extern "C" std::FILE* HostStandardOutputFile_nid_no_patch() { + if (std::FILE* kept = g_hostStandardOutputFile.load(std::memory_order_acquire)) return kept; + return __acrt_iob_func(1); +} + +extern "C" std::FILE* HostStandardErrorFile_nid_no_patch() { + if (std::FILE* kept = g_hostStandardErrorFile.load(std::memory_order_acquire)) return kept; + return __acrt_iob_func(2); +} +#endif + extern "C" void AddPathAlias_nid_no_patch(const char* guestPrefix, const char* hostPath) { if (guestPrefix == nullptr || hostPath == nullptr) { APS5_INVALID_ARG_EX; diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index 8fc522804a..02058dc4af 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -57,6 +57,10 @@ FileStream _Stderr_nid_postfix{stderr}; FileStream _Stdout_nid_postfix{stdout}; FileStream _Stdin_nid_postfix{stdin}; +std::FILE* GuestStandardOutputFile_nid_no_patch() { + return GetNativeStream(&_Stdout_nid_postfix); +} + int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dsoHandle) { std::lock_guard lock(g_exitMutex); g_exitDestructors.push_back({func, arg, dsoHandle}); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 4ceba06865..4623564598 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -712,16 +712,6 @@ namespace { std::atomic g_hostStandardError{-1}; std::atomic g_hostStandardOutput{-1}; -#ifdef _WIN32 -void RetargetHostStream(int standard, int saved) { - auto* const stream = __acrt_iob_func(standard); - std::fflush(stream); - auto* const descriptor = reinterpret_cast(reinterpret_cast(stream) + 24); - if (*descriptor != standard) throw std::runtime_error("dup2: host standard stream descriptor field is not " + std::to_string(standard)); - *descriptor = saved; -} -#endif - void PreserveHostStream(int standard, std::atomic& slot) { if (slot.load() >= 0) return; #ifdef _WIN32 @@ -738,12 +728,10 @@ void PreserveHostStream(int standard, std::atomic& slot) { return; } #ifdef _WIN32 - try { - RetargetHostStream(standard, saved); - } catch (...) { + if (!AdoptHostStandardStream_nid_no_patch(standard, saved)) { slot.store(-1); _close(saved); - throw; + throw std::runtime_error(standard == 1 ? "dup2: cannot keep the host standard output" : "dup2: cannot keep the host standard error"); } #endif } diff --git a/core/libs/tests/GuestHostStandardError.cpp b/core/libs/tests/GuestHostStandardError.cpp index de3237cc91..abca0c87eb 100644 --- a/core/libs/tests/GuestHostStandardError.cpp +++ b/core/libs/tests/GuestHostStandardError.cpp @@ -23,7 +23,11 @@ int APS5_VABI fflush_nid_postfix(FileStream*); int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI dup2_nid_postfix(int, int); std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::size_t); +int APS5_VABI printf_nid_postfix(const char*, ...); +int APS5_VABI puts_nid_postfix(const char*); +int APS5_VABI fprintf_nid_postfix(FileStream*, const char*, ...); extern FileStream* __stderrp_nid_postfix; +extern FileStream* __stdoutp_nid_postfix; } static void Require(bool value) { if (!value) std::abort(); } @@ -125,5 +129,40 @@ int main() { Require(::waitpid(child, &status, 0) == child); Require(WIFEXITED(status) && WEXITSTATUS(status) == 0); Require(output == "host\n"); + + for (const int standard : {1, 2}) { + int host[2] = {-1, -1}; + Require(::pipe(host) == 0); + const pid_t child = ::fork(); + Require(child >= 0); + if (child == 0) { + ::close(host[0]); + int guest[2] = {-1, -1}; + if (pipe_nid_postfix(guest) != 0 || dup2_nid_postfix(guest[1], standard) != standard) ::_exit(72); + const char* expected = "guest-fprintf\n"; + if (standard == 1) { + if (printf_nid_postfix("guest-printf\n") < 0 || puts_nid_postfix("guest-puts") < 0 || fflush_nid_postfix(__stdoutp_nid_postfix) != 0) ::_exit(73); + expected = "guest-printf\nguest-puts\n"; + } else if (fprintf_nid_postfix(__stderrp_nid_postfix, "guest-fprintf\n") < 0 || fflush_nid_postfix(__stderrp_nid_postfix) != 0) { + ::_exit(73); + } + std::string got; + char buffer[64]; + const auto length = std::char_traits::length(expected); + for (ssize_t count; got.size() < length && (count = ::read(guest[0], buffer, sizeof(buffer))) > 0;) got.append(buffer, static_cast(count)); + if (got != expected) ::_exit(75); + if (::write(host[1], "ok", 2) != 2) ::_exit(76); + ::_exit(0); + } + ::close(host[1]); + std::string marker; + char buffer[8]; + for (ssize_t count; (count = ::read(host[0], buffer, sizeof(buffer))) > 0;) marker.append(buffer, static_cast(count)); + ::close(host[0]); + int status = 0; + Require(::waitpid(child, &status, 0) == child); + Require(WIFEXITED(status) && WEXITSTATUS(status) == 0); + Require(marker == "ok"); + } return 0; } diff --git a/core/libs/tests/GuestHostStandardErrorWindows.cpp b/core/libs/tests/GuestHostStandardErrorWindows.cpp index 28be899f90..c66efe96eb 100644 --- a/core/libs/tests/GuestHostStandardErrorWindows.cpp +++ b/core/libs/tests/GuestHostStandardErrorWindows.cpp @@ -1,3 +1,5 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/HostStandardStreams.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -13,6 +15,12 @@ extern "C" { int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI dup2_nid_postfix(int, int); std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::size_t); +int APS5_VABI printf_nid_postfix(const char*, ...); +int APS5_VABI puts_nid_postfix(const char*); +int APS5_VABI fprintf_nid_postfix(FileStream*, const char*, ...); +int APS5_VABI fflush_nid_postfix(FileStream*); +extern FileStream _Stdout_nid_postfix; +extern FileStream _Stderr_nid_postfix; } static void Require(bool value) { if (!value) std::abort(); } @@ -24,7 +32,7 @@ static DWORD Available(int descriptor, void* buffer, DWORD capacity, DWORD& read return available; } -static void Split(int standard, std::FILE* stream) { +static void Split(int standard) { int capture[2]{-1, -1}; Require(::_pipe(capture, 65536, _O_BINARY) == 0); const int original = ::_dup(standard); @@ -33,6 +41,7 @@ static void Split(int standard, std::FILE* stream) { int guest[2]{-1, -1}; Require(pipe_nid_postfix(guest) == 0); Require(dup2_nid_postfix(guest[1], standard) == standard); + std::FILE* const stream = standard == 1 ? stdout : stderr; std::string host(8192, 'h'); host.back() = '\n'; @@ -57,8 +66,34 @@ static void Split(int standard, std::FILE* stream) { ::_close(guest[1]); } +static void GuestText(int standard, const std::string& text) { + int guest[2]{-1, -1}; + Require(pipe_nid_postfix(guest) == 0); + const int original = ::_dup(standard); + Require(original >= 0); + Require(dup2_nid_postfix(guest[1], standard) == standard); + if (standard == 1) { + Require(printf_nid_postfix("guest-printf\n") >= 0); + Require(puts_nid_postfix("guest-puts") >= 0); + Require(fflush_nid_postfix(&_Stdout_nid_postfix) == 0); + } else { + Require(fprintf_nid_postfix(&_Stderr_nid_postfix, "guest-fprintf\n") >= 0); + Require(fflush_nid_postfix(&_Stderr_nid_postfix) == 0); + } + std::string got(text.size(), '\0'); + DWORD read = 0; + Require(Available(guest[0], got.data(), static_cast(got.size()), read) == text.size()); + Require(read == text.size() && got == text); + Require(::_dup2(original, standard) == 0); + ::_close(original); + ::_close(guest[0]); + ::_close(guest[1]); +} + int main() { - Split(2, stderr); - Split(1, stdout); + Split(2); + Split(1); + GuestText(1, "guest-printf\nguest-puts\n"); + GuestText(2, "guest-fprintf\n"); return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index be9c7f181f..afa6dde3b9 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -158,7 +158,7 @@ - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified -- [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table, and on Windows duplicating a directory descriptor. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. On Windows the UCRT `FILE` from `__acrt_iob_func` is opaque: the descriptor word 24 bytes in matched `_fileno` for stdout and for stderr, and an `_fdopen` duplicate, on WinLibs GCC 15.2.0 UCRT. A word at byte 32 held 2 on the original stderr and 0 on its duplicate, and 4096 on stdout, so only byte 24 is written, and only when it equals that stream's descriptor. A runtime whose word does not throws (#7791) +- [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table, and on Windows duplicating a directory descriptor. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. On Windows a guest `dup2` onto descriptor 1 or 2 keeps host logging on an `_fdopen` of a duplicate, and the guest `FILE` stays on the descriptor that was replaced; opening that duplicate throws (#7791) - [waitpid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns -1 with `ECHILD` (`EINVAL` for unknown options): nothing here creates a child process; child processes would need a real `waitpid`. - [execv](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it. - [execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws like `execvp`: replacing the process image is not supported. The console behavior for invalid arguments and executable path lookups is not modeled. From fb0b5ca91966cd348f27435ac720a1e1ad8882d0 Mon Sep 17 00:00:00 2001 From: Maz Date: Tue, 20 Oct 2026 12:35:42 +0400 Subject: [PATCH 117/167] fix(libc): send perror through the guest stderr stream After a guest dup2 onto descriptor 2, host stderr is a separate FILE, so perror must use the guest stream like the other stdio writers. --- core/libs/prx/libc/src/Errors.cpp | 6 ++++-- core/libs/tests/GuestHostStandardError.cpp | 10 ++++++++-- core/libs/tests/GuestHostStandardErrorWindows.cpp | 6 +++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libc/src/Errors.cpp b/core/libs/prx/libc/src/Errors.cpp index 56a77382bb..4e2d3f08bb 100644 --- a/core/libs/prx/libc/src/Errors.cpp +++ b/core/libs/prx/libc/src/Errors.cpp @@ -1,3 +1,4 @@ +#include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -76,8 +77,9 @@ void APS5_VABI perror_nid_postfix(const char* prefix) { const int saved = *__error_nid_postfix(); char message[128]; strerror_r_nid_postfix(saved, message, sizeof(message)); - if (prefix && *prefix) std::fprintf(stderr, "%s: %s\n", prefix, message); - else std::fprintf(stderr, "%s\n", message); + auto* stream = GetNativeStream(&_Stderr_nid_postfix); + if (prefix && *prefix) std::fprintf(stream, "%s: %s\n", prefix, message); + else std::fprintf(stream, "%s\n", message); *__error_nid_postfix() = saved; } } diff --git a/core/libs/tests/GuestHostStandardError.cpp b/core/libs/tests/GuestHostStandardError.cpp index abca0c87eb..4c3fdc2309 100644 --- a/core/libs/tests/GuestHostStandardError.cpp +++ b/core/libs/tests/GuestHostStandardError.cpp @@ -26,6 +26,8 @@ std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::size_t); int APS5_VABI printf_nid_postfix(const char*, ...); int APS5_VABI puts_nid_postfix(const char*); int APS5_VABI fprintf_nid_postfix(FileStream*, const char*, ...); +void APS5_VABI perror_nid_postfix(const char*); +int* APS5_VABI __error_nid_postfix(); extern FileStream* __stderrp_nid_postfix; extern FileStream* __stdoutp_nid_postfix; } @@ -143,8 +145,12 @@ int main() { if (standard == 1) { if (printf_nid_postfix("guest-printf\n") < 0 || puts_nid_postfix("guest-puts") < 0 || fflush_nid_postfix(__stdoutp_nid_postfix) != 0) ::_exit(73); expected = "guest-printf\nguest-puts\n"; - } else if (fprintf_nid_postfix(__stderrp_nid_postfix, "guest-fprintf\n") < 0 || fflush_nid_postfix(__stderrp_nid_postfix) != 0) { - ::_exit(73); + } else { + if (fprintf_nid_postfix(__stderrp_nid_postfix, "guest-fprintf\n") < 0) ::_exit(73); + *__error_nid_postfix() = 2; + perror_nid_postfix("guest-perror"); + if (fflush_nid_postfix(__stderrp_nid_postfix) != 0) ::_exit(73); + expected = "guest-fprintf\nguest-perror: No such file or directory\n"; } std::string got; char buffer[64]; diff --git a/core/libs/tests/GuestHostStandardErrorWindows.cpp b/core/libs/tests/GuestHostStandardErrorWindows.cpp index c66efe96eb..24a2eff0e8 100644 --- a/core/libs/tests/GuestHostStandardErrorWindows.cpp +++ b/core/libs/tests/GuestHostStandardErrorWindows.cpp @@ -19,6 +19,8 @@ int APS5_VABI printf_nid_postfix(const char*, ...); int APS5_VABI puts_nid_postfix(const char*); int APS5_VABI fprintf_nid_postfix(FileStream*, const char*, ...); int APS5_VABI fflush_nid_postfix(FileStream*); +void APS5_VABI perror_nid_postfix(const char*); +int* APS5_VABI __error_nid_postfix(); extern FileStream _Stdout_nid_postfix; extern FileStream _Stderr_nid_postfix; } @@ -78,6 +80,8 @@ static void GuestText(int standard, const std::string& text) { Require(fflush_nid_postfix(&_Stdout_nid_postfix) == 0); } else { Require(fprintf_nid_postfix(&_Stderr_nid_postfix, "guest-fprintf\n") >= 0); + *__error_nid_postfix() = 2; + perror_nid_postfix("guest-perror"); Require(fflush_nid_postfix(&_Stderr_nid_postfix) == 0); } std::string got(text.size(), '\0'); @@ -94,6 +98,6 @@ int main() { Split(2); Split(1); GuestText(1, "guest-printf\nguest-puts\n"); - GuestText(2, "guest-fprintf\n"); + GuestText(2, "guest-fprintf\nguest-perror: No such file or directory\n"); return 0; } From 25db742794217b2387d7727c867ff4003c0c13f3 Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Tue, 20 Oct 2026 06:38:46 -0300 Subject: [PATCH 118/167] docs(libSceIme): name the sceImeFilterText codepages and their public sources --- docs/dev/debt/libSceIme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 26b012783e..b4e8be6c3f 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -9,7 +9,7 @@ - [sceImeSetConsecutiveClauseConversionEnabled](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - name from its NID (`row9sfnYNx4`) in [sce_symbols](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); neither [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0ebea15c24253b60f514fc37a348dabc8d0bcb90/src/core/libraries/ime/ime.cpp) nor [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/e5f0ee7fc2fb4ce88c6abb2fa02296404e2fe2a6/src/libs/ime.cpp) implements it, so it throws. PPSA24191 calls it with 1 as its only argument before each `sceImeOpen` and opens the panel only when it returns 0; the argument type, the result while a panel is open and the error codes are unknown - [sceImeGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills. While a panel is open it reports the type, position and alignments passed to `sceImeOpen` and the size `sceImeGetPanelSize` gives; the console's values for a panel moved by its own position adjustment are unknown. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title - [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. A text filter or an external keyboard filter throws, as their contract is unknown. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn -- [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 (`sceImeFilterText`, 2272 bytes) and is differential-tested against that function in an emulator; it is not measured on a console. The two codepage sets it tests (helpers at 0x4850 and 0x48f0, which ask two compiled-in converters whether a UTF-16 unit converts) were derived by running those helpers over all 65536 units and are stored in [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp); the encodings are not identified. The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` are the two `u32` values from which the mask is derived when it is 0 and which select the accepted classes. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown +- [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 and is differential-tested against that function in an emulator; it is not measured on a console. The two codepage checks (helpers at 0x4850 and 0x48f0) ask whether a UTF-16 unit converts. [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp) rebuilds them from Unicode's public [CP932](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT) and [CP936](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT) tables plus the units the firmware also accepts (Latin-1, CJK Extension A and a few more, listed in the PR); the encodings are identified as CP932 and GBK/GB18030 from that match. The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` derive it when it is 0; their meaning is unknown. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown - [sceImeVshOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeKeyboardUpdate](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshUpdate](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature From e8b564a974bd0bf9c4464bf215f7ebb02d85996d Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Tue, 20 Oct 2026 06:39:06 -0300 Subject: [PATCH 119/167] docs(libSceIme): shorten the sceImeFilterText debt entry --- docs/dev/debt/libSceIme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index b4e8be6c3f..206e25a538 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -9,7 +9,7 @@ - [sceImeSetConsecutiveClauseConversionEnabled](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - name from its NID (`row9sfnYNx4`) in [sce_symbols](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); neither [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0ebea15c24253b60f514fc37a348dabc8d0bcb90/src/core/libraries/ime/ime.cpp) nor [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/e5f0ee7fc2fb4ce88c6abb2fa02296404e2fe2a6/src/libs/ime.cpp) implements it, so it throws. PPSA24191 calls it with 1 as its only argument before each `sceImeOpen` and opens the panel only when it returns 0; the argument type, the result while a panel is open and the error codes are unknown - [sceImeGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills. While a panel is open it reports the type, position and alignments passed to `sceImeOpen` and the size `sceImeGetPanelSize` gives; the console's values for a panel moved by its own position adjustment are unknown. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title - [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. A text filter or an external keyboard filter throws, as their contract is unknown. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn -- [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 and is differential-tested against that function in an emulator; it is not measured on a console. The two codepage checks (helpers at 0x4850 and 0x48f0) ask whether a UTF-16 unit converts. [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp) rebuilds them from Unicode's public [CP932](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT) and [CP936](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT) tables plus the units the firmware also accepts (Latin-1, CJK Extension A and a few more, listed in the PR); the encodings are identified as CP932 and GBK/GB18030 from that match. The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` derive it when it is 0; their meaning is unknown. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown +- [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 and is differential-tested against that function in an emulator, not measured on a console. [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp) holds the two codepage checks (helpers at 0x4850 and 0x48f0), rebuilt from Unicode's public [CP932](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT) and [CP936](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT) tables plus the Latin-1, CJK Extension A and a few other units the firmware also accepts (listed in the PR). The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` derive it when it is 0. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown - [sceImeVshOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeKeyboardUpdate](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeVshUpdate](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature From e1095000cc609a14e2933b8512e0f88a88a6f64b Mon Sep 17 00:00:00 2001 From: Maz Date: Tue, 20 Oct 2026 13:39:19 +0400 Subject: [PATCH 120/167] fix(libc): keep MinGW ANSI stdio when forcing host stdout/stderr The Windows -include of HostStandardStreams.hpp pulled in stdio.h before libstdc++ could set __USE_MINGW_ANSI_STDIO, so long double formatting bound to UCRT. Define the MinGW flag and include cstdio for C++. --- core/libs/prx/libc/include/HostStandardStreams.hpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/libs/prx/libc/include/HostStandardStreams.hpp b/core/libs/prx/libc/include/HostStandardStreams.hpp index 9d0f14bf2e..5c01f269cf 100644 --- a/core/libs/prx/libc/include/HostStandardStreams.hpp +++ b/core/libs/prx/libc/include/HostStandardStreams.hpp @@ -1,7 +1,14 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_HOSTSTANDARDSTREAMS_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_HOSTSTANDARDSTREAMS_HPP +#if defined(__MINGW32__) && !defined(__USE_MINGW_ANSI_STDIO) +#define __USE_MINGW_ANSI_STDIO 1 +#endif +#ifdef __cplusplus +#include +#else #include +#endif #ifdef _WIN32 #undef stdout From 445d7192f9905fba74cbdf861297e80df3268a49 Mon Sep 17 00:00:00 2001 From: Cankiee <13728354+Cankiee@users.noreply.github.com> Date: Sun, 18 Oct 2026 10:06:53 +0200 Subject: [PATCH 121/167] fix(tools): handle git command failures gracefully --- tools/check_conventions.py | 21 +++++++++++++- tools/test_check_conventions_git.py | 45 +++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 tools/test_check_conventions_git.py diff --git a/tools/check_conventions.py b/tools/check_conventions.py index c55fea49e7..8c2ed750d8 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -66,11 +66,30 @@ SECTION = re.compile(r"^###(?!#)\s*(.+?)\s*$", re.M) KEY = re.compile(r"^( *)([A-Za-z0-9_-]+|'[^']*'|\"[^\"]*\"):(?:\s+(.*?))?\s*$") + def git(*args, text=True): - output = subprocess.run(["git", "-c", "core.quotePath=false", *args], capture_output=True, check=True).stdout + try: + result = subprocess.run( + ["git", "-c", "core.quotePath=false", *args], + capture_output=True, + check=True, + ) + except subprocess.CalledProcessError as error: + message = error.stderr.decode("utf-8", errors="replace").strip() + print( + f"Git command failed: {message or 'unknown Git error'}", + file=sys.stderr, + ) + raise SystemExit(2) from None + except FileNotFoundError: + print("Git executable not found. Please install Git.", file=sys.stderr) + raise SystemExit(2) from None + + output = result.stdout return output.decode("utf-8", errors="replace") if text else output + class Check: def __init__(self, base, head): self.head = head diff --git a/tools/test_check_conventions_git.py b/tools/test_check_conventions_git.py new file mode 100644 index 0000000000..bc18595532 --- /dev/null +++ b/tools/test_check_conventions_git.py @@ -0,0 +1,45 @@ + +import subprocess +import sys +import unittest +from pathlib import Path + + +class GitErrorHandlingTests(unittest.TestCase): + def test_invalid_base_branch(self): + script = Path(__file__).with_name("check_conventions.py") + result = subprocess.run( + [ + sys.executable, + str(script), + "--base", + "this-branch-does-not-exist", + ], + capture_output=True, + text=True, + ) + + self.assertEqual(result.returncode, 2) + self.assertIn("Git command failed:", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_invalid_head_branch(self): + script = Path(__file__).with_name("check_conventions.py") + result = subprocess.run( + [ + sys.executable, + str(script), + "--head", + "this-branch-does-not-exist", + ], + capture_output=True, + text=True, + ) + + self.assertEqual(result.returncode, 2) + self.assertIn("Git command failed:", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + +if __name__ == "__main__": + unittest.main() From 609017e51dc6f86f776aa0520ecc8207d9bbef2b Mon Sep 17 00:00:00 2001 From: Cankiee <13728354+Cankiee@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:10:40 +0200 Subject: [PATCH 122/167] test(tools): register git error regression tests with ctest --- CMakeLists.txt | 2 + tools/test_check_conventions_git.py | 45 ----------------------- tools/tests/test_check_conventions_git.py | 40 ++++++++++++++++++++ 3 files changed, 42 insertions(+), 45 deletions(-) delete mode 100644 tools/test_check_conventions_git.py create mode 100644 tools/tests/test_check_conventions_git.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 094bf25cf3..bfd144d963 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -170,6 +170,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py) add_test(NAME conventions_ci_gate COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_ci_gate.py) + add_test(NAME conventions_git_errors COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_git.py) add_test(NAME conventions_doc_links COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_doc_links.py) add_test(NAME conventions_unknown_nids COMMAND ${Python3_EXECUTABLE} diff --git a/tools/test_check_conventions_git.py b/tools/test_check_conventions_git.py deleted file mode 100644 index bc18595532..0000000000 --- a/tools/test_check_conventions_git.py +++ /dev/null @@ -1,45 +0,0 @@ - -import subprocess -import sys -import unittest -from pathlib import Path - - -class GitErrorHandlingTests(unittest.TestCase): - def test_invalid_base_branch(self): - script = Path(__file__).with_name("check_conventions.py") - result = subprocess.run( - [ - sys.executable, - str(script), - "--base", - "this-branch-does-not-exist", - ], - capture_output=True, - text=True, - ) - - self.assertEqual(result.returncode, 2) - self.assertIn("Git command failed:", result.stderr) - self.assertNotIn("Traceback", result.stderr) - - def test_invalid_head_branch(self): - script = Path(__file__).with_name("check_conventions.py") - result = subprocess.run( - [ - sys.executable, - str(script), - "--head", - "this-branch-does-not-exist", - ], - capture_output=True, - text=True, - ) - - self.assertEqual(result.returncode, 2) - self.assertIn("Git command failed:", result.stderr) - self.assertNotIn("Traceback", result.stderr) - - -if __name__ == "__main__": - unittest.main() diff --git a/tools/tests/test_check_conventions_git.py b/tools/tests/test_check_conventions_git.py new file mode 100644 index 0000000000..02a7d8f057 --- /dev/null +++ b/tools/tests/test_check_conventions_git.py @@ -0,0 +1,40 @@ + +import subprocess +import sys +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +CHECKER = REPO_ROOT / "tools" / "check_conventions.py" +INVALID_REVISION = "this-branch-does-not-exist" + + +class GitErrorHandlingTests(unittest.TestCase): + def check_invalid_revision(self, option): + result = subprocess.run( + [ + sys.executable, + str(CHECKER), + option, + INVALID_REVISION, + ], + cwd=REPO_ROOT, + capture_output=True, + text=True, + ) + + self.assertEqual(result.returncode, 2) + self.assertIn("Git command failed:", result.stderr) + self.assertIn(INVALID_REVISION, result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_invalid_base_branch(self): + self.check_invalid_revision("--base") + + def test_invalid_head_branch(self): + self.check_invalid_revision("--head") + + +if __name__ == "__main__": + unittest.main() From 4dbcc94e9c0b00af18877369184b1a999e714566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 20 Oct 2026 11:53:06 +0200 Subject: [PATCH 123/167] test(tools): pass a valid revision for the option not under test --- tools/tests/test_check_conventions_git.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/tests/test_check_conventions_git.py b/tools/tests/test_check_conventions_git.py index 02a7d8f057..5502d752d0 100644 --- a/tools/tests/test_check_conventions_git.py +++ b/tools/tests/test_check_conventions_git.py @@ -12,12 +12,13 @@ INVALID_REVISION = "this-branch-does-not-exist" class GitErrorHandlingTests(unittest.TestCase): def check_invalid_revision(self, option): + valid = "--head" if option == "--base" else "--base" + arguments = [valid, "HEAD", option, INVALID_REVISION] result = subprocess.run( [ sys.executable, str(CHECKER), - option, - INVALID_REVISION, + *arguments, ], cwd=REPO_ROOT, capture_output=True, From 8a47d68ba1cdd6dc22d30987f5bf1ce5433df487 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sun, 18 Oct 2026 17:34:43 +0300 Subject: [PATCH 124/167] test(agc): verify sampled-cache fixture device selection --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 8 ++ .../tests/execution/SampledCacheBudget.cpp | 83 +++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 61dd26a9a9..54c415d446 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2350,6 +2350,14 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) add_test(NAME agc_driver_sampled_cache_budget_without_storage_image_multisample COMMAND agc_driver_sampled_cache_budget_tests without-storage-image-multisample) set_tests_properties(agc_driver_sampled_cache_budget_without_storage_image_multisample PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_sampled_cache_device_selection COMMAND agc_driver_sampled_cache_budget_tests device-selection-cpu) + set_tests_properties(agc_sampled_cache_device_selection PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_sampled_cache_device_selection COMMAND agc_driver_sampled_cache_budget_tests device-selection) + set_tests_properties(agc_driver_sampled_cache_device_selection PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_sampled_cache_device_selection agc_driver_sampled_cache_device_selection PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() 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) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index c9dc46e856..ebeab9dcff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -17,7 +17,9 @@ #endif #include #endif +#include #include +#include #include #include #include @@ -26,8 +28,11 @@ #include #include #include +#include +#include #include #include +#include #include #include #include @@ -48,6 +53,67 @@ constexpr std::size_t AllocationBytes = BlockBytes + RegistrationGapBytes; constexpr std::uint64_t Budget = 1ull << 20u; constexpr std::uint64_t Ceiling = Budget + Budget / 4u; +std::optional selectDevice(std::span devices, std::string_view request) { + const auto rankDeviceType = [](VkPhysicalDeviceType type) { + switch (type) { + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 3; + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 2; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 1; + default: return 0; + } + }; + std::optional selected; + int selectedRank = -1; + for (std::size_t index = 0; index < devices.size(); ++index) { + const auto& candidate = devices[index]; + if (candidate.apiVersion < VK_API_VERSION_1_1) continue; + const std::string_view name(candidate.deviceName); + if (!request.empty() && std::search(name.begin(), name.end(), request.begin(), request.end(), [](char left, char right) { + return std::tolower(static_cast(left)) == std::tolower(static_cast(right)); + }) == name.end()) continue; + const int rank = request.empty() ? rankDeviceType(candidate.deviceType) : 4; + if (rank <= selectedRank) continue; + selected = index; + selectedRank = rank; + } + return selected; +} + +void deviceSelectionTests() { + const auto properties = [](const char* name, VkPhysicalDeviceType type) { + VkPhysicalDeviceProperties result{}; + result.apiVersion = VK_API_VERSION_1_1; + result.deviceType = type; + std::memcpy(result.deviceName, name, std::strlen(name) + 1u); + return result; + }; + std::array devices{ + properties("AMD Radeon Graphics", VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU), + properties("NVIDIA GeForce RTX 5080", VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU), + properties("Virtual GPU", VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU), + properties("CPU", VK_PHYSICAL_DEVICE_TYPE_CPU), + }; + Require(!selectDevice({}, {}).has_value(), "an empty device list selected a device"); + Require(selectDevice(devices, {}) == 1u, "default selection did not prefer the discrete GPU"); + Require(selectDevice(devices, "AMD") == 0u, "the AMD request did not select the integrated GPU"); + Require(selectDevice(devices, "NVIDIA") == 1u, "the NVIDIA request did not select the discrete GPU"); + Require(selectDevice(devices, "aMd rAdEoN") == 0u, "a requested integrated GPU lost to an unrelated discrete GPU"); + Require(selectDevice(devices, "rtx 5080") == 1u, "a partial GPU name did not match without regard to case"); + Require(selectDevice(devices, "Virtual") == 2u, "a requested virtual GPU was not selected"); + Require(selectDevice(devices, "cpu") == 3u, "a requested CPU device was not selected"); + Require(!selectDevice(devices, "missing").has_value(), "an unmatched request selected another GPU"); + Require(!selectDevice(devices, "NVIDIA GeForce RTX 5080 with a longer name").has_value(), "a request longer than the device name matched"); + Require(selectDevice(std::span(devices).subspan(2), {}) == 0u, "default selection did not prefer the virtual GPU to the CPU"); + devices[1].apiVersion = VK_API_VERSION_1_0; + Require(selectDevice(devices, {}) == 0u, "default selection did not prefer the integrated GPU to the virtual GPU"); + Require(!selectDevice(devices, "NVIDIA").has_value(), "a requested device without Vulkan 1.1 was selected"); + devices[1] = devices[0]; + Require(selectDevice(devices, {}) == 0u && selectDevice(devices, "AMD") == 0u, "equally ranked matches did not retain enumeration order"); + for (auto& device : devices) device.apiVersion = VK_API_VERSION_1_0; + Require(!selectDevice(devices, {}).has_value(), "a list without Vulkan 1.1 selected a device"); + std::puts("sampled-cache device selection CPU tests passed"); +} + class Device { public: explicit Device(bool enableStorageImageMultisample = true) { @@ -122,6 +188,7 @@ public: function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); VkPhysicalDeviceProperties properties{}; function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + std::printf("sampled-cache fixture Vulkan device: %s vendor=0x%04x device=0x%04x\n", properties.deviceName, properties.vendorID, properties.deviceID); context.limits = properties.limits; context.bufferDeviceAddress = true; context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); @@ -141,6 +208,14 @@ public: ~Device() { release(); } const Context& GetContext() const { return context; } + void VerifySelection() const { + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + const char* request = std::getenv("ANYPS5_GPU"); + Require(selectDevice(std::span(&properties, 1), request != nullptr ? request : "").has_value(), "the sampled-cache fixture physical device does not match ANYPS5_GPU"); + std::puts("sampled-cache fixture physical device selection verified"); + } + private: template TFunction function(const char* name) const { @@ -783,6 +858,10 @@ int main(int argc, char** argv) { try { const bool memory = argc > 1 && std::string(argv[1]) == "memory"; const std::string mode = argc > 1 ? argv[1] : ""; + if (mode == "device-selection-cpu") { + deviceSelectionTests(); + return 0; + } const bool resources = mode == "resource-admission" || mode == "resource-oversized" || mode == "resource-clear"; const bool disableStorageImageMultisample = mode == "without-storage-image-multisample"; #ifdef _WIN32 @@ -798,6 +877,10 @@ int main(int argc, char** argv) { std::printf("skipped, no usable Vulkan device: %s\n", error.what()); return VulkanTestSkipped; } + if (mode == "device-selection") { + device->VerifySelection(); + return 0; + } auto context = device->GetContext(); if (disableStorageImageMultisample) { PlaceholderImages placeholders(context); From b71f1b8000c2b481b33d3b63927e7e09985384cf Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sun, 18 Oct 2026 17:35:43 +0300 Subject: [PATCH 125/167] fix(agc): honor GPU selection in sampled-cache tests --- .../tests/execution/SampledCacheBudget.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index ebeab9dcff..e96d71cc14 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -137,7 +137,13 @@ public: Require(count != 0, "no Vulkan device"); std::vector devices(count); Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); - context.physical = devices.front(); + const auto physicalProperties = function("vkGetPhysicalDeviceProperties"); + std::vector deviceProperties(devices.size()); + for (std::size_t index = 0; index < devices.size(); ++index) physicalProperties(devices[index], &deviceProperties[index]); + const char* requestedName = std::getenv("ANYPS5_GPU"); + const auto selectedIndex = selectDevice(deviceProperties, requestedName != nullptr ? requestedName : ""); + Require(selectedIndex.has_value(), requestedName != nullptr && *requestedName != '\0' ? std::string("no Vulkan 1.1 device matching ANYPS5_GPU=\"") + requestedName + "\"" : "no Vulkan 1.1 device"); + context.physical = devices[*selectedIndex]; const auto extensions = function("vkEnumerateDeviceExtensionProperties"); Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); std::vector available(count); @@ -873,7 +879,8 @@ int main(int argc, char** argv) { try { device = std::make_unique(!disableStorageImageMultisample); } catch (const std::exception& error) { - if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + const char* requestedName = std::getenv("ANYPS5_GPU"); + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr || (requestedName != nullptr && *requestedName != '\0')) throw; std::printf("skipped, no usable Vulkan device: %s\n", error.what()); return VulkanTestSkipped; } From 55e4850fb7a7fc88a410b3953aa0147992c78990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 20 Oct 2026 12:10:59 +0200 Subject: [PATCH 126/167] test(agc): skip the CPU selection test when the execution fixture is stubbed Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 54c415d446..e3420a69ba 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2351,7 +2351,7 @@ if(BUILD_TESTING) add_test(NAME agc_driver_sampled_cache_budget_without_storage_image_multisample COMMAND agc_driver_sampled_cache_budget_tests without-storage-image-multisample) set_tests_properties(agc_driver_sampled_cache_budget_without_storage_image_multisample PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_sampled_cache_device_selection COMMAND agc_driver_sampled_cache_budget_tests device-selection-cpu) - set_tests_properties(agc_sampled_cache_device_selection PROPERTIES TIMEOUT 30) + set_tests_properties(agc_sampled_cache_device_selection PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_driver_sampled_cache_device_selection COMMAND agc_driver_sampled_cache_budget_tests device-selection) set_tests_properties(agc_driver_sampled_cache_device_selection PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) From 7b05271abb494c3577faaaa4ff2a186a1abd68d2 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 18 Oct 2026 13:31:50 +0000 Subject: [PATCH 127/167] fix(libkernel): support kern.rng_pseudo MIB access --- .../System/src/SystemConfiguration.cpp | 13 ++++- core/libs/tests/GuestSystemConfiguration.cpp | 52 ++++++++++++++++++- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index d02e41cc8d..74b358cb5d 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -5,6 +5,7 @@ #include "prx/libkernel/System/include/HostCpu.hpp" #include "SceTypes.hpp" #include +#include #include #include #include @@ -74,7 +75,7 @@ const SystemString* FindSystemString(const int* name) { } bool IsLeafMib(const int* name) { - return (name[0] == 6 && (name[1] == 2 || name[1] == 3 || name[1] == 5 || name[1] == 7)) || (name[0] == 1 && name[1] == 37); + return (name[0] == 6 && (name[1] == 2 || name[1] == 3 || name[1] == 5 || name[1] == 7)) || (name[0] == 1 && (name[1] == 37 || name[1] == 50)); } constexpr std::int32_t VmEntryDefaultType = 1; @@ -280,6 +281,15 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *oldLength = count; return 0; } + if (nameLength == 2 && name[0] == 1 && name[1] == 50) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": writing kern.rng_pseudo is unverified"); + std::array bytes{}; + if (oldValue != nullptr && oldLength != nullptr) { + static std::random_device device; + for (auto& byte : bytes) byte = static_cast(device()); + } + return CopyOut(bytes.data(), bytes.size(), oldValue, oldLength); + } if (nameLength > 2 && (IsLeafMib(name) || FindSystemString(name) != nullptr)) { *__error_nid_postfix() = 20; return -1; @@ -387,6 +397,7 @@ int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_ {"hw.physmem", {6, 5}, 2}, {"hw.realmem", {6, 12}, 2}, {"kern.arandom", {1, 37}, 2}, + {"kern.rng_pseudo", {1, 50}, 2}, {"kern.proc.pathname", {1, 14, 12}, 3}, }; constexpr std::size_t MaximumPath = 1024; diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 74bcc6a1a5..bebbab8f7b 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -258,7 +258,7 @@ static void CheckSystemStringNames() { } } static void CheckLeafWithTrailingComponent() { - const int leaves[][2] = {{6, 3}, {6, 2}, {6, 5}, {6, 7}, {1, 37}}; + const int leaves[][2] = {{6, 3}, {6, 2}, {6, 5}, {6, 7}, {1, 37}, {1, 50}}; for (const auto& leaf : leaves) { const int mib[] = {leaf[0], leaf[1], 0}; char buffer[8] = {}; @@ -467,6 +467,43 @@ static void CheckVmmapSysctl() { const int tooLong[] = {1, 14, 32, self, 0}; Require(sysctl_nid_postfix(tooLong, 5, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); } +static void CheckPseudoRandomMibSysctl() { + const int mib[] = {1, 50}; + *__error_nid_postfix() = 13; + std::size_t length = 7; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == 64); + Require(*__error_nid_postfix() == 13); + Require(sysctl_nid_postfix(mib, 2, nullptr, nullptr, nullptr, 0) == 0); + unsigned char bytes[80]; + std::memset(bytes, 0xaa, sizeof(bytes)); + length = 64; + Require(sysctl_nid_postfix(mib, 2, bytes, &length, nullptr, 0) == 0 && length == 64); + bool changed = false; + for (std::size_t i = 0; i < 64; ++i) changed = changed || bytes[i] != 0xaa; + Require(changed && bytes[64] == 0xaa && bytes[79] == 0xaa); + unsigned char other[64] = {}; + length = sizeof(other); + Require(sysctl_nid_postfix(mib, 2, other, &length, nullptr, 0) == 0 && length == sizeof(other)); + Require(std::memcmp(bytes, other, sizeof(other)) != 0); + + unsigned char shortBuffer[64]; + std::memset(shortBuffer, 0xaa, sizeof(shortBuffer)); + length = 63; + Require(sysctl_nid_postfix(mib, 2, shortBuffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 63 && shortBuffer[62] != 0xaa && shortBuffer[63] == 0xaa); + std::memset(shortBuffer, 0xaa, sizeof(shortBuffer)); + Require(sysctl_nid_postfix(mib, 2, shortBuffer, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + for (unsigned char byte : shortBuffer) Require(byte == 0xaa); + int value = 0; + length = sizeof(value); + bool threw = false; + try { + sysctl_nid_postfix(mib, 2, &value, &length, &value, sizeof(value)); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); +} static void CheckNameToMib() { struct Expected { const char* name; @@ -480,6 +517,7 @@ static void CheckNameToMib() { {"hw.physmem", {6, 5}, 2}, {"hw.realmem", {6, 12}, 2}, {"kern.arandom", {1, 37}, 2}, + {"kern.rng_pseudo", {1, 50}, 2}, {"kern.proc.pathname", {1, 14, 12}, 3}, }; *__error_nid_postfix() = 13; @@ -523,6 +561,17 @@ static void CheckNameToMib() { Require(sysctlnametomib_nid_postfix("hw.ncpu", truncated, &size) == -1 && *__error_nid_postfix() == 12); Require(size == 0 && truncated[0] == 6); + int rngMib[3] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + size = 3; + Require(sysctlnametomib_nid_postfix("kern.rng_pseudo", rngMib, &size) == 0 && size == 2); + Require(rngMib[0] == 1 && rngMib[1] == 50 && rngMib[2] == 0x5a5a5a5a); + size = 2; + Require(sysctl_nid_postfix(rngMib, static_cast(size), nullptr, &length, nullptr, 0) == 0 && length == 64); + rngMib[0] = rngMib[1] = rngMib[2] = 0x5a5a5a5a; + size = 1; + Require(sysctlnametomib_nid_postfix("kern.rng_pseudo", rngMib, &size) == -1 && *__error_nid_postfix() == 12); + Require(size == 1 && rngMib[0] == 1 && rngMib[1] == 0x5a5a5a5a); + size = 4; Require(sysctlnametomib_nid_postfix("", mib, &size) == -1 && *__error_nid_postfix() == 2); Require(sysctlnametomib_nid_postfix("machdep.cacheline_size", mib, &size) == -1 && *__error_nid_postfix() == 2); @@ -580,6 +629,7 @@ int main() { CheckOvercommitSysctl(); CheckArandomSysctl(); CheckVmmapSysctl(); + CheckPseudoRandomMibSysctl(); CheckNameToMib(); *__error_nid_postfix() = 13; Require(sysconf_nid_postfix(1) == 262144); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 880a0832bf..86d838ef0d 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -16,7 +16,7 @@ - [physhm_open, physhm_unlink](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel, NIDs `n371J5cP+uo` and `AUqJNkobQ1c`) - declared as throwing stubs: the PS5 physical shared memory API is not documented, and [shadPS4's module generator](https://github.com/shadps4-emu/moduleGenerator/blob/main/libraries/kernel.cpp) also declares both with no arguments and returns `ORBIS_OK`, so the signature, the handle a success would return and the console's behaviour are unmeasured. The no-argument `int` signature follows shadPS4. Forever Launcher (PPSA99030) imports both and stops at load on `physhm_open`, and the relinker resolves no call site for either in that title, so no handle is fabricated and both throw - [getlogin_r](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. - [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` for the name and `sysctl` {1, 50} throw +- [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 From 99e87ce36fa95723e726af65e29cfa73a46c883a Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:50:47 +0000 Subject: [PATCH 128/167] test(libkernel): avoid flaky random byte assertion --- core/libs/tests/GuestSystemConfiguration.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index bebbab8f7b..6627591d38 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -490,7 +490,9 @@ static void CheckPseudoRandomMibSysctl() { std::memset(shortBuffer, 0xaa, sizeof(shortBuffer)); length = 63; Require(sysctl_nid_postfix(mib, 2, shortBuffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); - Require(length == 63 && shortBuffer[62] != 0xaa && shortBuffer[63] == 0xaa); + bool shortChanged = false; + for (std::size_t i = 0; i < 63; ++i) shortChanged = shortChanged || shortBuffer[i] != 0xaa; + Require(length == 63 && shortChanged && shortBuffer[63] == 0xaa); std::memset(shortBuffer, 0xaa, sizeof(shortBuffer)); Require(sysctl_nid_postfix(mib, 2, shortBuffer, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 12); for (unsigned char byte : shortBuffer) Require(byte == 0xaa); From 34d3199601dca5b0282c88a9fe34656ec74b272f Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Sun, 18 Oct 2026 08:39:10 -0300 Subject: [PATCH 129/167] fix(tools): fail when a test_*.py script is never run check_test_registration.py only audits built test executables, so a Python test that no add_test or workflow names is silently skipped. Add a --scripts mode that lists the tracked test_*.py scripts and fails for any not run by an add_test of a CMakeLists or a python step of a workflow, and register it as a ctest so the relinker builds run it. --- CMakeLists.txt | 2 ++ tools/check_test_registration.py | 40 +++++++++++++++++++++++++++ tools/tests/test_test_registration.py | 37 +++++++++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index bfd144d963..b5e6fc8d8d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -180,6 +180,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_debt_length.py) add_test(NAME test_registration COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_test_registration.py) + add_test(NAME test_script_registration COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_test_registration.py --scripts) add_test(NAME build_eligibility COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_eligibility.py) add_test(NAME check_skips COMMAND ${Python3_EXECUTABLE} diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index 45faf7639a..c3da26c6be 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -11,6 +11,7 @@ LEXICAL = re.compile(r'\\.|"(?:\\.|[^"\\])*"|\[(?P=*)\[.*?\](?P= r'|(?P\#\[(?P=*)\[.*?\](?P=comment_equals)\]|\#[^\n]*)', re.S) KNOWN_UNRUN = set() +SCRIPT = re.compile(r"(?:[\w.-]+/)*test_\w+\.py") def built(build): @@ -125,11 +126,50 @@ def registered(build): return names +def listed(root): + output = subprocess.run(["git", "-c", "core.quotePath=false", "-C", str(root), "ls-files", "-z", "--cached", "--others", + "--exclude-standard"], capture_output=True, check=True).stdout.decode("utf-8", errors="replace") + return sorted(set(filter(None, output.split("\0")))) + + +def run_scripts(root, files): + run = set() + for name in files: + if name.endswith("CMakeLists.txt") or name.endswith(".cmake"): + content = (Path(root) / name).read_text(encoding="utf-8", errors="replace") + for command, arguments in commands(content): + if command == "add_test": + run.update(argument.rpartition("}")[2].lstrip("/") for argument in arguments) + elif name.startswith(".github/workflows/") and name.endswith((".yml", ".yaml")): + for line in (Path(root) / name).read_text(encoding="utf-8", errors="replace").splitlines(): + line = line.split(" #")[0] + if "python" in line: + run.update(token[2:] if token.startswith("./") else token for token in SCRIPT.findall(line)) + return {name for name in run if SCRIPT.fullmatch(name)} + + +def unrun_scripts(root): + files = listed(root) + run = run_scripts(root, files) + scripts = [name for name in files if re.match(r"^test_\w+\.py$", name.rpartition("/")[2])] + return scripts, [name for name in scripts if not any(name == path or name.endswith("/" + path) for path in run)] + + if __name__ == "__main__": parser = argparse.ArgumentParser(description="fail when a test executable is built that no ctest test runs") parser.add_argument("build", type=Path, nargs="?", default=Path("build"), help="configured build directory") parser.add_argument("--list", action="store_true", help="print every test executable and whether ctest runs it") + parser.add_argument("--scripts", action="store_true", help="instead check that an add_test or a workflow runs every test_*.py of the source tree") args = parser.parse_args() + if args.scripts: + scripts, unrun = unrun_scripts(Path(__file__).resolve().parents[1]) + for name in unrun: + print(f"error: {name} is a test script that no add_test or workflow runs") + if unrun: + print(f"{len(unrun)} test script(s) would never run: register each with add_test, or run it from a workflow") + raise SystemExit(1) + print(f"{len(scripts)} test script(s), each run by an add_test or a workflow") + raise SystemExit try: every = sorted(built(args.build)) run = registered(args.build) diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index 32bbbcfbc1..73f48964d8 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -294,5 +294,42 @@ class RegistrationTests(unittest.TestCase): self.assertIn("requires a Ninja generator", result.stderr) self.assertNotIn("Traceback", result.stderr) +class ScriptRegistrationTests(unittest.TestCase): + def unrun(self, files): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + check_test_registration.subprocess.run(["git", "init", "-q", str(root)], check=True) + for name, content in files.items(): + (root / name).parent.mkdir(parents=True, exist_ok=True) + (root / name).write_text(content, encoding="utf-8") + return check_test_registration.unrun_scripts(root)[1] + + def test_script_registered_by_a_nested_add_test_runs(self): + self.assertEqual(self.unrun({ + "core/relinker/CMakeLists.txt": "if(WIN32)\n add_test(NAME a COMMAND ${Python3_EXECUTABLE}\n ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_a.py)\nendif()\n", + "core/relinker/relinker/tests/test_a.py": ""}), []) + + def test_unregistered_script_is_reported(self): + self.assertEqual(self.unrun({ + "CMakeLists.txt": "add_test(NAME a COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_a.py)\n", + "tools/tests/test_a.py": "", "tools/tests/test_b.py": "", "tools/tests/helper.py": ""}), ["tools/tests/test_b.py"]) + + def test_commented_add_test_does_not_run_a_script(self): + self.assertEqual(self.unrun({ + "CMakeLists.txt": "#add_test(NAME a COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/test_a.py)\n#[[\nadd_test(NAME a COMMAND python test_a.py)\n]]\n", + "test_a.py": ""}), ["test_a.py"]) + + def test_registration_names_its_directory(self): + self.assertEqual(self.unrun({ + "CMakeLists.txt": "add_test(NAME a COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/a/tests/test_a.py)\n", + "a/tests/test_a.py": "", "b/tests/test_a.py": ""}), ["b/tests/test_a.py"]) + + def test_workflow_run_counts_and_a_paths_filter_does_not(self): + self.assertEqual(self.unrun({ + ".github/workflows/pages.yml": "jobs:\n web:\n steps:\n - run: python3 tools/test_web.py --build build-wasm\n", + ".github/workflows/web.yml": "on:\n pull_request:\n paths:\n - tools/test_paths.py\n", + "tools/test_web.py": "", "tools/test_paths.py": ""}), ["tools/test_paths.py"]) + + if __name__ == "__main__": unittest.main() From d502c75b5fb4dfe325885f03f125f5fffa847847 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Sun, 18 Oct 2026 11:27:06 -0300 Subject: [PATCH 130/167] fix(tools): skip commented workflow lines when finding test scripts A YAML comment line such as '# python3 tools/tests/test_x.py' counted as a run, because only a comment after a space was stripped. Skip lines whose first non-blank character is '#', also inside run blocks, and strip a trailing comment after any whitespace. --- tools/check_test_registration.py | 4 +++- tools/tests/test_test_registration.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index c3da26c6be..fcce86cbd1 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -142,7 +142,9 @@ def run_scripts(root, files): run.update(argument.rpartition("}")[2].lstrip("/") for argument in arguments) elif name.startswith(".github/workflows/") and name.endswith((".yml", ".yaml")): for line in (Path(root) / name).read_text(encoding="utf-8", errors="replace").splitlines(): - line = line.split(" #")[0] + if line.lstrip().startswith("#"): + continue + line = re.split(r"\s#", line)[0] if "python" in line: run.update(token[2:] if token.startswith("./") else token for token in SCRIPT.findall(line)) return {name for name in run if SCRIPT.fullmatch(name)} diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index 73f48964d8..106d3bc3ae 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -330,6 +330,18 @@ class ScriptRegistrationTests(unittest.TestCase): ".github/workflows/web.yml": "on:\n pull_request:\n paths:\n - tools/test_paths.py\n", "tools/test_web.py": "", "tools/test_paths.py": ""}), ["tools/test_paths.py"]) + def test_commented_workflow_line_does_not_run_a_script(self): + self.assertEqual(self.unrun({ + ".github/workflows/ci.yml": "# python3 tools/tests/test_f.py\njobs:\n t:\n steps:\n # python3 tools/tests/test_a.py\n" + " - run: |\n #python3 tools/tests/test_b.py\n" + " \t# python3 tools/tests/test_c.py\n" + " echo done\t# python3 tools/tests/test_d.py\n" + " python3 tools/tests/test_e.py\n", + "tools/tests/test_a.py": "", "tools/tests/test_b.py": "", "tools/tests/test_c.py": "", + "tools/tests/test_d.py": "", "tools/tests/test_e.py": "", "tools/tests/test_f.py": ""}), + ["tools/tests/test_a.py", "tools/tests/test_b.py", "tools/tests/test_c.py", "tools/tests/test_d.py", + "tools/tests/test_f.py"]) + if __name__ == "__main__": unittest.main() From e5de2a9b7d3808af7b38c0955f86ede5f77ada41 Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 07:39:27 -0300 Subject: [PATCH 131/167] test(tools): run the hw-oracle oracle.cmd fixture from non-ASCII paths Co-Authored-By: Claude Opus 5.5 --- tools/hw-oracle/test_hw_oracle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index 0f1238023c..58f66cf38c 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -402,13 +402,13 @@ class TargetOverrideTests(unittest.TestCase): encoding="utf-8") binary = root / ("oracle.cmd" if os.name == "nt" else "oracle") if os.name == "nt": - binary.write_text(f'@"{sys.executable}" "{runner}" %*\n', encoding="utf-8") + binary.write_text('@"%HW_ORACLE_TEST_PYTHON%" "%~dp0probe.py" %*\n', encoding="ascii") else: binary.write_text(f'#!/bin/sh\nexec {shlex.quote(sys.executable)} {shlex.quote(str(runner))} "$@"\n', encoding="utf-8") binary.chmod(0o755) with patch.object(hw_oracle, "oracle", return_value=binary), \ - patch.dict(os.environ, {"HW_ORACLE_TARGET": ""}): + patch.dict(os.environ, {"HW_ORACLE_TARGET": "", "HW_ORACLE_TEST_PYTHON": sys.executable}): os.environ["ROCR_VISIBLE_DEVICES"] = "0" self.assertEqual(hw_oracle.detected_target(binary, "1"), "gfx1100") for visible, expected in (("0", "gfx1030"), ("0", "gfx1030"), From 1529a5cf31ebb7b38045ded1eefce63b9c018684 Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 07:45:53 -0300 Subject: [PATCH 132/167] test(recompiler): read the cache fixture's build.ninja in the Ninja build-file encoding Co-Authored-By: Claude Opus 5.5 --- tools/tests/test_shader_cache_runtime_sources.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_shader_cache_runtime_sources.py b/tools/tests/test_shader_cache_runtime_sources.py index db2f418cfb..0c462bb7ea 100644 --- a/tools/tests/test_shader_cache_runtime_sources.py +++ b/tools/tests/test_shader_cache_runtime_sources.py @@ -7,6 +7,9 @@ import sys import tempfile import unittest +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import select_tests + SOURCE = Path(sys.argv.pop(1)) CMAKE = sys.argv.pop(1) @@ -74,7 +77,7 @@ file(WRITE "${{CMAKE_CURRENT_BINARY_DIR}}/runtime-sources.txt" "${{runtimeSource raise AssertionError('The generator fixture invoked a compiler: ' + (self.root / 'compiler-invoked').read_text(encoding='utf-8')) self.generated = self.build / 'shader_recompiler_probe_generated' self.listing = self.generated / 'sources.txt' - ninja = (self.build / 'build.ninja').read_text(encoding='utf-8') + ninja = (self.build / 'build.ninja').read_text(encoding=select_tests.Build(self.build).encoding) command = next(line for line in ninja.splitlines() if line.startswith(' COMMAND = ') and 'ShaderCacheVersion.cmake' in line) self.extra = re.search(r'"-DEXTRA=([^"]*)"', command).group(1) self.compiled = [] From 308848e1a6b6a60ced84d0c4697f4f865a5398db Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 12:49:56 +0200 Subject: [PATCH 133/167] fix(libc): resolve the rename source before the destination argument FreeBSD 11.4 kern_renameat looks up the source first (namei on fromnd), so a missing or dot source fails with ENOENT or EINVAL before the new name is touched. The guest call rejected a NULL, empty or dot destination first, so rename(missing, NULL) returned EFAULT and rename(missing, ".") returned EINVAL where FreeBSD returns ENOENT. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/src/Rename.cpp | 19 ++++++++++++++++--- core/libs/prx/libkernel/File/src/Stdio.cpp | 2 +- core/libs/tests/GuestFilesystem.cpp | 8 ++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/src/Rename.cpp b/core/libs/prx/libc/src/Rename.cpp index 89ef0a55a2..973c142ff6 100644 --- a/core/libs/prx/libc/src/Rename.cpp +++ b/core/libs/prx/libc/src/Rename.cpp @@ -94,13 +94,26 @@ static int RenamePath_nid_no_patch(const std::filesystem::path& source, const st return error ? FilesystemError(error) : 0; } +static int SourceLookupError_nid_no_patch(const std::filesystem::path& source) { + std::error_code status; + const auto sourceStatus = std::filesystem::symlink_status(source, status); + if (HasNonDirectoryPrefix_nid_no_patch(source)) return 20; + if (status && status != std::errc::no_such_file_or_directory) return FilesystemError(status); + return sourceStatus.type() == std::filesystem::file_type::not_found ? 2 : 0; +} + extern "C" int RenameGuestPath_nid_no_patch(const char* from, const char* to) { - if (!from || !to) { errno = 14; return -1; } - if (!*from || !*to) { errno = 2; return -1; } + if (!from) { errno = 14; return -1; } + if (!*from) { errno = 2; return -1; } if (const int dot = RenameDotError_nid_no_patch(from)) { errno = dot; return -1; } - if (const int dot = RenameDotError_nid_no_patch(to)) { errno = dot; return -1; } + const int destinationError = !to ? 14 : !*to ? 2 : RenameDotError_nid_no_patch(to); try { const auto source = ResolvePath_nid_no_patch(from); + if (destinationError) { + const int sourceError = SourceLookupError_nid_no_patch(source); + errno = sourceError ? sourceError : destinationError; + return -1; + } const auto destination = ResolvePath_nid_no_patch(to); int error = RenamePath_nid_no_patch(source, destination); if (error == 2) { diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 4d786eb038..5a7d73a4fa 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1379,7 +1379,7 @@ int64_t APS5_VABI pwritev_nid_postfix(int d, const KernelIovec* iov, int iovcnt, } int APS5_VABI sceKernelRename(const char* from, const char* to) { - if (!from || !to) return SceErrorFromErrno(GUEST_EFAULT); + if (!from) return SceErrorFromErrno(GUEST_EFAULT); const int result = RenameGuestPath_nid_no_patch(from, to); return result == 0 ? 0 : SceKernelError(errno); if (const int dot = RenameDotError_nid_no_patch(from)) return SceKernelError(dot); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 7818a4421c..07b86670f3 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -856,6 +856,14 @@ int main() { Require(sceKernelCheckReachability("") == static_cast(0x80020002u)); Require(sceKernelRename("", presentName.c_str()) == static_cast(0x80020002u)); Require(sceKernelRename(presentName.c_str(), "") == static_cast(0x80020002u)); + Require(sceKernelRename(missingName.c_str(), nullptr) == static_cast(0x80020002u)); + Require(sceKernelRename("", nullptr) == static_cast(0x80020002u)); + Require(sceKernelRename(".", nullptr) == static_cast(0x80020016u)); + Require(sceKernelRename(missingName.c_str(), ".") == static_cast(0x80020002u)); + Require(sceKernelRename(presentName.c_str(), nullptr) == static_cast(0x8002000Eu)); + Require(sceKernelRename(nullptr, presentName.c_str()) == static_cast(0x8002000Eu)); + Require(rename_nid_postfix(missingName.c_str(), nullptr) == -1 && *__error_nid_postfix() == 2); + Require(rename_nid_postfix(presentName.c_str(), nullptr) == -1 && *__error_nid_postfix() == 14); Require(sceKernelRmdir("") == static_cast(0x80020002u)); Require(sceKernelMkdir("", 0700) == static_cast(0x80020002u)); const auto cwdPermissions = std::filesystem::status(".").permissions(); From 187b1ab24726a34bb93143e27ccb44be66fc9187 Mon Sep 17 00:00:00 2001 From: thibaut Date: Tue, 20 Oct 2026 12:52:31 +0200 Subject: [PATCH 134/167] 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 3bb39053ae4065849ae08174702cb126519f73e7 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Mon, 19 Oct 2026 12:43:06 +0530 Subject: [PATCH 135/167] fix(savedata): return parameter error on null arguments in umount2, loadIcon and saveIcon --- core/libs/prx/libSceSaveData.native/Export.cpp | 6 +++--- core/libs/tests/GuestSaveDataMountStatus.cpp | 15 +++++++++++++++ docs/dev/debt/libSceSaveData.native.md | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 5bb5858274..0cb45e16be 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -591,7 +591,7 @@ int APS5_VABI sceSaveDataInitialize3(const void* init) { int APS5_VABI sceSaveDataLoadIcon(const SaveDataMountPoint* mount_point, SaveDataIcon* icon) { (void)icon; if (mount_point == nullptr) { - throw std::runtime_error("sceSaveDataLoadIcon: null mount_point"); + return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); if (find_slot_by_mount_point(mount_point->data) == -1) { @@ -673,7 +673,7 @@ int APS5_VABI sceSaveDataPrepare(const SaveDataMountPoint* mount_point, const Sa int APS5_VABI sceSaveDataSaveIcon(const SaveDataMountPoint* mount_point, const SaveDataIcon* icon) { (void)icon; if (mount_point == nullptr) { - throw std::runtime_error("sceSaveDataSaveIcon: null mount_point"); + return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); if (find_slot_by_mount_point(mount_point->data) == -1) { @@ -896,7 +896,7 @@ int APS5_VABI sceSaveDataTransferringMount(const SaveDataTransferringMount* moun static int umount2(uint32_t mode, const SaveDataMountPoint* mount_point) { (void)mode; if (mount_point == nullptr) { - throw std::runtime_error("sceSaveDataUmount2: null mount_point"); + return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); int slot = find_slot_by_mount_point(mount_point->data); diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index 4fb5ea120e..5c744255bc 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -15,6 +15,8 @@ int APS5_VABI sceSaveDataUmount2(std::uint32_t, const SaveDataMountPoint*); int APS5_VABI sceSaveDataGetMountInfo(const SaveDataMountPoint*, SaveDataMountInfo*); int APS5_VABI sceSaveDataGetParam(const SaveDataMountPoint*, std::uint32_t, void*, std::size_t, std::size_t*); int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint*, std::uint32_t, const void*, std::size_t); +int APS5_VABI sceSaveDataLoadIcon(const SaveDataMountPoint*, SaveDataIcon*); +int APS5_VABI sceSaveDataSaveIcon(const SaveDataMountPoint*, const SaveDataIcon*); } namespace { @@ -39,8 +41,15 @@ void CheckUnmountedCalls() { SaveDataMountPoint point{}; std::memcpy(point.data, "test", sizeof("test")); SaveDataMountInfo info{}; + SaveDataIcon icon{}; char buf[128]{}; std::size_t gotSize = 0; + Check(sceSaveDataUmount2(0, nullptr) == SaveDataErrorParameter, "umount2 null point is PARAMETER"); + Check(sceSaveDataUmount2(0, &point) == SaveDataErrorNotMounted, "umount2 unmounted point is NOT_MOUNTED"); + Check(sceSaveDataLoadIcon(nullptr, &icon) == SaveDataErrorParameter, "loadIcon null point is PARAMETER"); + Check(sceSaveDataLoadIcon(&point, &icon) == SaveDataErrorNotMounted, "loadIcon unmounted point is NOT_MOUNTED"); + Check(sceSaveDataSaveIcon(nullptr, &icon) == SaveDataErrorParameter, "saveIcon null point is PARAMETER"); + Check(sceSaveDataSaveIcon(&point, &icon) == SaveDataErrorNotMounted, "saveIcon unmounted point is NOT_MOUNTED"); Check(sceSaveDataGetMountInfo(nullptr, &info) == SaveDataErrorParameter, "getMountInfo null point is PARAMETER"); Check(sceSaveDataGetMountInfo(&point, nullptr) == SaveDataErrorParameter, "getMountInfo null info is PARAMETER"); Check(sceSaveDataGetMountInfo(&point, &info) == SaveDataErrorNotMounted, "getMountInfo unmounted point is NOT_MOUNTED"); @@ -54,6 +63,7 @@ void CheckUnmountedCalls() { void CheckMountInfoAndParam(const SaveDataMountPoint* point) { SaveDataMountInfo info{}; + SaveDataIcon icon{}; char buf[128]{}; char smallBuf[64]{}; std::size_t gotSize = 0; @@ -61,6 +71,11 @@ void CheckMountInfoAndParam(const SaveDataMountPoint* point) { std::memcpy(unmounted.data, "unmounted", sizeof("unmounted")); const char newTitle[] = "Mounted Title"; char readTitle[128]{}; + Check(sceSaveDataUmount2(0, nullptr) == SaveDataErrorParameter, "umount2 null point is PARAMETER"); + Check(sceSaveDataLoadIcon(nullptr, &icon) == SaveDataErrorParameter, "loadIcon null point is PARAMETER"); + Check(sceSaveDataLoadIcon(point, &icon) == 0, "loadIcon on mounted save succeeds"); + Check(sceSaveDataSaveIcon(nullptr, &icon) == SaveDataErrorParameter, "saveIcon null point is PARAMETER"); + Check(sceSaveDataSaveIcon(point, &icon) == 0, "saveIcon on mounted save succeeds"); Check(sceSaveDataGetMountInfo(nullptr, &info) == SaveDataErrorParameter, "getMountInfo null point is PARAMETER"); Check(sceSaveDataGetMountInfo(point, nullptr) == SaveDataErrorParameter, "getMountInfo null info is PARAMETER"); Check(sceSaveDataGetMountInfo(&unmounted, &info) == SaveDataErrorNotMounted, "getMountInfo unmounted point is NOT_MOUNTED"); diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 92ab2a4581..cbef030023 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -18,6 +18,6 @@ - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for `sceSaveDataPrepare`, its file and `sceSaveDataUmount2` - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated or empty directory name, `.`, `..`, or a name containing `/`, `\` or `:` returns `SAVE_DATA_ERROR_PARAMETER`, which the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-mount3.md#L196-L204) lists for an invalid parameter, and leaves the result as it was, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L652-L658) does. The [SDK](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-dir-name.md#L32-L36) lets an application use only letters, digits, `-`, `.` and `@` in a name, and KytyPS5 also accepts `_` ([`valid_path_component`](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L276-L287)); which other names the console's mount rejects is not checked on hardware -- [sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataSetParam](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup +- [sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup - [sceSaveDataSyncSaveDataMemory](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - validates library initialization and parameters; since in-memory saves flush synchronously on write, sync confirms existence and readiness of the memory blob 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 136/167] 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 becb78b704e5d888a4a542ba48237efc76edfe68 Mon Sep 17 00:00:00 2001 From: hypermist Date: Mon, 19 Oct 2026 23:07:49 +1300 Subject: [PATCH 137/167] feat(libc): implement gmtime_r, localtime_r, timegm and the Dinkumware xtime helpers Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/CMakeLists.txt | 11 ++ core/libs/prx/libc/src/TimeExtras.cpp | 93 ++++++++++++++ core/libs/tests/GuestTimeExtras.cpp | 170 ++++++++++++++++++++++++++ 3 files changed, 274 insertions(+) create mode 100644 core/libs/prx/libc/src/TimeExtras.cpp create mode 100644 core/libs/tests/GuestTimeExtras.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index ca39154516..bd95dc7bcd 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -49,6 +49,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Backtrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/DinkumwareAtomics.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Time.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/TimeExtras.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AtomicBuiltins.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileAndHeap.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/HostErrno.cpp @@ -185,6 +186,16 @@ if(BUILD_TESTING) set_tests_properties(guest_mktime PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_time_extras_tests ${LIBS_INCLUDE_DIR}/tests/GuestTimeExtras.cpp) + target_include_directories(guest_time_extras_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_time_extras_tests PRIVATE libc) + configure_windows_unwind(guest_time_extras_tests) + add_test(NAME guest_time_extras COMMAND guest_time_extras_tests) + set_tests_properties(guest_time_extras PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_time_extras PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(precise_wait_tests ${LIBS_INCLUDE_DIR}/tests/PreciseWait.cpp) target_include_directories(precise_wait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(precise_wait_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/TimeExtras.cpp b/core/libs/prx/libc/src/TimeExtras.cpp new file mode 100644 index 0000000000..ee2a225b25 --- /dev/null +++ b/core/libs/prx/libc/src/TimeExtras.cpp @@ -0,0 +1,93 @@ +#include + +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestTime.hpp" + +namespace { + +struct Xtime { + std::int64_t sec; + std::int64_t nsec; +}; + +constexpr int timeUtc = 1; +constexpr std::int64_t nanosPerSecond = 1000000000; +constexpr std::int64_t secondsPerDay = 86400; + +std::int64_t floorDivide(std::int64_t value, std::int64_t divisor) { + return value >= 0 ? value / divisor : -((-(value + 1)) / divisor + 1); +} + +std::int64_t daysFromCivil(std::int64_t year, std::int64_t month, std::int64_t day) { + year -= month <= 2 ? 1 : 0; + const std::int64_t era = floorDivide(year, 400); + const std::int64_t yearOfEra = year - era * 400; + const std::int64_t dayOfYear = (153 * (month > 2 ? month - 3 : month + 9) + 2) / 5 + day - 1; + const std::int64_t dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear; + return era * 146097 + dayOfEra - 719468; +} + +std::int64_t wrappingAdd(std::int64_t left, std::int64_t right) { + return static_cast(static_cast(left) + static_cast(right)); +} + +} // namespace + +extern "C" { + +GuestTm* APS5_VABI gmtime_s_nid_postfix(const std::int64_t* timer, GuestTm* result); +GuestTm* APS5_VABI localtime_s_nid_postfix(const std::int64_t* timer, GuestTm* result); +std::int64_t APS5_VABI _Xtime_get_ticks_nid_postfix(); + +GuestTm* APS5_VABI gmtime_r_nid_postfix(const std::int64_t* timer, GuestTm* result) { + return gmtime_s_nid_postfix(timer, result); +} + +GuestTm* APS5_VABI localtime_r_nid_postfix(const std::int64_t* timer, GuestTm* result) { + return localtime_s_nid_postfix(timer, result); +} + +std::int64_t APS5_VABI timegm_nid_postfix(GuestTm* time) { + const std::int64_t years = floorDivide(time->tm_mon, 12); + const std::int64_t month = time->tm_mon - years * 12 + 1; + const std::int64_t year = std::int64_t{time->tm_year} + 1900 + years; + const std::int64_t days = daysFromCivil(year, month, 1) + time->tm_mday - 1; + const std::int64_t seconds = days * secondsPerDay + std::int64_t{time->tm_hour} * 3600 + std::int64_t{time->tm_min} * 60 + time->tm_sec; + GuestTm normalized{}; + if (gmtime_s_nid_postfix(&seconds, &normalized) == nullptr) return -1; + *time = normalized; + return seconds; +} + +int APS5_VABI xtime_get_nid_postfix(Xtime* time, int base) { + if (time == nullptr || base != timeUtc) return 0; + const std::int64_t micros = _Xtime_get_ticks_nid_postfix(); + time->sec = floorDivide(micros, 1000000); + time->nsec = (micros - time->sec * 1000000) * 1000; + return timeUtc; +} + +Xtime APS5_VABI _Xtime_to_ts_nid_postfix(const Xtime* time) { + return *time; +} + +Xtime APS5_VABI _Xtime_diff_to_ts_nid_postfix(const Xtime* target) { + Xtime now{}; + xtime_get_nid_postfix(&now, timeUtc); + std::int64_t seconds = wrappingAdd(target->sec, target->nsec / nanosPerSecond); + std::int64_t nanos = target->nsec % nanosPerSecond; + if (nanos < 0) { + nanos += nanosPerSecond; + seconds = wrappingAdd(seconds, -1); + } + seconds = wrappingAdd(seconds, -now.sec); + if (nanos < now.nsec) { + nanos += nanosPerSecond; + seconds = wrappingAdd(seconds, -1); + } + nanos -= now.nsec; + if (seconds < 0 || (seconds == 0 && nanos == 0)) return {0, 0}; + return {seconds, nanos}; +} + +} diff --git a/core/libs/tests/GuestTimeExtras.cpp b/core/libs/tests/GuestTimeExtras.cpp new file mode 100644 index 0000000000..b3f818e843 --- /dev/null +++ b/core/libs/tests/GuestTimeExtras.cpp @@ -0,0 +1,170 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestTime.hpp" +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +struct Xtime { + std::int64_t sec; + std::int64_t nsec; +}; + +extern "C" { +GuestTm* APS5_VABI gmtime_s_nid_postfix(const std::int64_t*, GuestTm*); +GuestTm* APS5_VABI localtime_s_nid_postfix(const std::int64_t*, GuestTm*); +GuestTm* APS5_VABI gmtime_r_nid_postfix(const std::int64_t*, GuestTm*); +GuestTm* APS5_VABI localtime_r_nid_postfix(const std::int64_t*, GuestTm*); +std::int64_t APS5_VABI timegm_nid_postfix(GuestTm*); +int APS5_VABI xtime_get_nid_postfix(Xtime*, int); +Xtime APS5_VABI _Xtime_to_ts_nid_postfix(const Xtime*); +Xtime APS5_VABI _Xtime_diff_to_ts_nid_postfix(const Xtime*); +} + +static void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +static bool Equal(const GuestTm& left, const GuestTm& right) { + return left.tm_sec == right.tm_sec && left.tm_min == right.tm_min && left.tm_hour == right.tm_hour + && left.tm_mday == right.tm_mday && left.tm_mon == right.tm_mon && left.tm_year == right.tm_year + && left.tm_wday == right.tm_wday && left.tm_yday == right.tm_yday && left.tm_isdst == right.tm_isdst; +} + +struct TimegmCase { + int sec, min, hour, mday, mon, year; + std::int64_t expected; + int normalizedMday, normalizedMon, normalizedYear, wday, yday; +}; + +static void CheckTimegm(const TimegmCase& test) { + GuestTm time{}; + time.tm_sec = test.sec; + time.tm_min = test.min; + time.tm_hour = test.hour; + time.tm_mday = test.mday; + time.tm_mon = test.mon; + time.tm_year = test.year; + time.tm_wday = 9; + time.tm_yday = 999; + time.tm_isdst = 5; + time.tm_gmtoff = 77; + if (timegm_nid_postfix(&time) != test.expected) { + std::fprintf(stderr, "timegm(%d-%d-%d) returned the wrong time\n", test.year, test.mon, test.mday); + std::abort(); + } + Require(time.tm_mday == test.normalizedMday && time.tm_mon == test.normalizedMon && time.tm_year == test.normalizedYear + && time.tm_wday == test.wday && time.tm_yday == test.yday && time.tm_isdst == 0 && time.tm_gmtoff == 0, + "timegm did not normalize the calendar fields"); +} + +static void CheckReentrantConversions() { + constexpr std::int64_t timers[] = {0, -1, 86399, 951782400, 4107542400, 253402300799, -62135596800}; + for (const std::int64_t timer : timers) { + GuestTm expected{}; + GuestTm actual{}; + Require(gmtime_s_nid_postfix(&timer, &expected) == &expected, "gmtime_s rejected a representable time"); + Require(gmtime_r_nid_postfix(&timer, &actual) == &actual && Equal(actual, expected), "gmtime_r differs from gmtime_s"); + Require(localtime_s_nid_postfix(&timer, &expected) == &expected, "localtime_s rejected a representable time"); + Require(localtime_r_nid_postfix(&timer, &actual) == &actual && Equal(actual, expected) && actual.tm_gmtoff == 7200, + "localtime_r differs from localtime_s"); + } + const std::int64_t epoch = 0; + Require(gmtime_r_nid_postfix(&epoch, nullptr) == nullptr, "gmtime_r accepted a null result"); + Require(localtime_r_nid_postfix(&epoch, nullptr) == nullptr, "localtime_r accepted a null result"); + const std::int64_t underflow = std::numeric_limits::min(); + GuestTm scratch{}; + Require(gmtime_r_nid_postfix(&underflow, &scratch) == nullptr, "gmtime_r accepted an unrepresentable time"); +} + +static void CheckTimegmCases() { + constexpr TimegmCase cases[] = { + {0, 0, 0, 1, 0, 70, 0, 1, 0, 70, 4, 0}, + {59, 59, 23, 31, 11, 69, -1, 31, 11, 69, 3, 364}, + {7, 14, 3, 19, 9, 126, 1792379647, 19, 9, 126, 1, 291}, + {0, 0, 0, 0, 0, 70, -86400, 31, 11, 69, 3, 364}, + {0, 0, 0, 1, 12, 69, 0, 1, 0, 70, 4, 0}, + {0, 0, 0, 1, -1, 71, 28857600, 1, 11, 70, 2, 334}, + {0, 0, 0, 1, -13, 71, -2678400, 1, 11, 69, 1, 334}, + {0, 0, 0, 32, 1, 100, 952041600, 3, 2, 100, 5, 62}, + {31536000, 0, 0, 1, 0, 70, 31536000, 1, 0, 71, 5, 0}, + {0, 0, 0, 1, 2, 200, 4107542400, 1, 2, 200, 1, 59}, + {59, 59, 23, 31, 11, 8099, 253402300799, 31, 11, 8099, 5, 364}, + {0, 0, 0, 1, 0, -1899, -62135596800, 1, 0, -1899, 1, 0}, + }; + for (const auto& test : cases) CheckTimegm(test); + + GuestTm beyond{}; + beyond.tm_year = std::numeric_limits::max(); + beyond.tm_mon = 12; + beyond.tm_mday = 1; + const GuestTm before = beyond; + Require(timegm_nid_postfix(&beyond) == -1 && Equal(beyond, before), "timegm accepted a year past the int range"); + + std::mt19937_64 random(7); + for (int iteration = 0; iteration < 100000; ++iteration) { + const std::int64_t timer = static_cast(random() % 600000000000ull) - 300000000000; + GuestTm fields{}; + Require(gmtime_r_nid_postfix(&timer, &fields) == &fields, "gmtime_r rejected a representable time"); + GuestTm copy = fields; + Require(timegm_nid_postfix(©) == timer && Equal(copy, fields), "timegm did not invert gmtime_r"); + } +} + +static void CheckXtime() { + Xtime time{-5, -5}; + Require(xtime_get_nid_postfix(&time, 0) == 0 && time.sec == -5 && time.nsec == -5, "xtime_get accepted base 0"); + Require(xtime_get_nid_postfix(&time, 2) == 0 && time.sec == -5 && time.nsec == -5, "xtime_get accepted base 2"); + Require(xtime_get_nid_postfix(nullptr, 1) == 0, "xtime_get accepted a null xtime"); + const std::int64_t before = static_cast(std::time(nullptr)); + Require(xtime_get_nid_postfix(&time, 1) == 1, "xtime_get rejected TIME_UTC"); + Require(time.sec >= before - 1 && time.sec <= before + 2 && time.nsec >= 0 && time.nsec < 1000000000 && time.nsec % 1000 == 0, + "xtime_get did not return the wall clock in microseconds"); + + const Xtime unnormalized{7, -3}; + const Xtime copied = _Xtime_to_ts_nid_postfix(&unnormalized); + Require(copied.sec == 7 && copied.nsec == -3, "_Xtime_to_ts changed the time"); + + Xtime now{}; + Require(xtime_get_nid_postfix(&now, 1) == 1, "xtime_get rejected TIME_UTC"); + const Xtime past{now.sec - 1, now.nsec}; + const Xtime remaining = _Xtime_diff_to_ts_nid_postfix(&past); + Require(remaining.sec == 0 && remaining.nsec == 0, "_Xtime_diff_to_ts did not clamp a past time to zero"); + const Xtime epoch{0, 0}; + const Xtime none = _Xtime_diff_to_ts_nid_postfix(&epoch); + Require(none.sec == 0 && none.nsec == 0, "_Xtime_diff_to_ts did not clamp the epoch to zero"); + const Xtime later{now.sec + 100, now.nsec}; + const Xtime wait = _Xtime_diff_to_ts_nid_postfix(&later); + Require(wait.sec >= 98 && wait.sec <= 100 && wait.nsec >= 0 && wait.nsec < 1000000000, "_Xtime_diff_to_ts returned the wrong wait"); + const Xtime carried{now.sec, now.nsec + 5000000000}; + const Xtime carriedWait = _Xtime_diff_to_ts_nid_postfix(&carried); + Require(carriedWait.sec >= 3 && carriedWait.sec <= 5 && carriedWait.nsec >= 0 && carriedWait.nsec < 1000000000, + "_Xtime_diff_to_ts did not carry whole seconds out of nsec"); + const Xtime borrowed{now.sec + 10, -1}; + const Xtime borrowedWait = _Xtime_diff_to_ts_nid_postfix(&borrowed); + Require(borrowedWait.sec >= 8 && borrowedWait.sec <= 9 && borrowedWait.nsec >= 0 && borrowedWait.nsec < 1000000000, + "_Xtime_diff_to_ts did not borrow a second for a negative nsec"); +} + +int main() { +#ifdef _WIN32 + _putenv_s("TZ", "UTC-2"); + _tzset(); +#else + setenv("TZ", "UTC-2", 1); + tzset(); +#endif + CheckReentrantConversions(); + CheckTimegmCases(); + CheckXtime(); + return 0; +} From 6eb1168e698c554aab03780d2abd4f9c0c9d1c30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 20 Oct 2026 13:06:49 +0200 Subject: [PATCH 138/167] test(libc): set the zone in guest_time_extras through the guest setenv/tzset Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestTimeExtras.cpp | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/core/libs/tests/GuestTimeExtras.cpp b/core/libs/tests/GuestTimeExtras.cpp index b3f818e843..6fc9d4cde0 100644 --- a/core/libs/tests/GuestTimeExtras.cpp +++ b/core/libs/tests/GuestTimeExtras.cpp @@ -7,10 +7,6 @@ #include #include -#ifdef _WIN32 -#include -#endif - struct Xtime { std::int64_t sec; std::int64_t nsec; @@ -22,6 +18,8 @@ GuestTm* APS5_VABI localtime_s_nid_postfix(const std::int64_t*, GuestTm*); GuestTm* APS5_VABI gmtime_r_nid_postfix(const std::int64_t*, GuestTm*); GuestTm* APS5_VABI localtime_r_nid_postfix(const std::int64_t*, GuestTm*); std::int64_t APS5_VABI timegm_nid_postfix(GuestTm*); +void APS5_VABI tzset_nid_postfix(); +int APS5_VABI setenv_nid_postfix(const char*, const char*, int); int APS5_VABI xtime_get_nid_postfix(Xtime*, int); Xtime APS5_VABI _Xtime_to_ts_nid_postfix(const Xtime*); Xtime APS5_VABI _Xtime_diff_to_ts_nid_postfix(const Xtime*); @@ -156,13 +154,8 @@ static void CheckXtime() { } int main() { -#ifdef _WIN32 - _putenv_s("TZ", "UTC-2"); - _tzset(); -#else - setenv("TZ", "UTC-2", 1); - tzset(); -#endif + Require(setenv_nid_postfix("TZ", "UTC-2", 1) == 0, "Could not set the time zone"); + tzset_nid_postfix(); CheckReentrantConversions(); CheckTimegmCases(); CheckXtime(); From 321460a4ed6c88f46b4f28533594a5c99a7c0abd Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Mon, 19 Oct 2026 11:59:22 +0200 Subject: [PATCH 139/167] fix(agc): reserve only the sampled textures alive from the storage image budget StorageTextureBudget reserved the larger of the sampled textures alive and their 2 GiB floor. The sampled budget already reserves TextureCacheBudget, at least the storage cap, for the storage cache, so on an 8 GB card the two caches each kept the other's maximum: with 3936 MiB used outside the caches on a 7189 MiB budget and 49 MiB of sampled textures, the storage budget came out at 306 MiB and sat at its 512 MiB floor while about 2.7 GB of the budget stayed free. The storage budget now reserves the sampled textures alive and gives room back as they grow, at its next read. Where they hold 2 GiB or more the budget is unchanged. --- .../Graphics/src/ShaderResources.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 16 +++++++++++++--- .../tests/execution/SampledCacheBudget.cpp | 6 +++--- docs/dev/debt/libSceAgcDriver.md | 2 +- docs/user/USAGE.md | 2 +- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index f1055a3663..59bc877ae5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1202,7 +1202,7 @@ std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memor if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); const auto budget = reported->heapBudget[*heap]; const auto usage = reported->heapUsage[*heap]; - const auto reserved = usage - std::min(usage, sampledBytes + storageBytes) + std::max(2048ull << 20u, sampledBytes) + budget / 8u; + const auto reserved = usage - std::min(usage, sampledBytes + storageBytes) + sampledBytes + budget / 8u; const auto cap = std::min(TextureCacheBudget(memory), budget / 4u); return std::max(512ull << 20u, std::min(cap, budget > reserved ? budget - reserved : 0u)); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 6e1f5d23ee..9f562b1a79 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -6431,10 +6431,10 @@ void storageTextureBudgetTests() { Require(StorageTextureBudget(memory, &reported, 0, 0) == 2560 * MiB, "a 10 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget"); reported.heapUsage[2] = 5 * GiB; Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2560 * MiB, "2 GiB used outside the texture caches on a 10 GiB budget lowered the storage image cache below its quarter"); - reported.heapUsage[2] = 8 * GiB; - Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 1792 * MiB, "a 10 GiB budget with 5 GiB used outside the texture caches does not leave 1792 MiB after the sampled textures' 2 GiB floor and 1280 MiB of headroom"); + reported.heapUsage[2] = 8 * GiB + 512 * MiB; + Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2304 * MiB, "a 10 GiB budget with 5632 MiB used outside the texture caches does not leave 2304 MiB after 1 GiB of sampled textures and 1280 MiB of headroom"); reported.heapUsage[2] = 9 * GiB; - Require(StorageTextureBudget(memory, &reported, 3 * GiB, 2 * GiB) == 1792 * MiB, "3 GiB of sampled textures were not reserved past their 2 GiB floor"); + Require(StorageTextureBudget(memory, &reported, 3 * GiB, 2 * GiB) == 1792 * MiB, "3 GiB of sampled textures were not reserved in full"); reported.heapUsage[2] = 1 * GiB; Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2560 * MiB, "usage below the cached bytes is not read as no other device memory in use"); Require(StorageTextureBudget(memory, &reported, 7 * GiB, 1 * GiB) == 1792 * MiB, "7 GiB of sampled textures with the usage below the cached bytes do not leave 1792 MiB after their reservation and 1280 MiB of headroom"); @@ -6447,6 +6447,16 @@ void storageTextureBudgetTests() { Require(StorageTextureBudget(memory, &reported, 0, 0) == 512 * MiB, "a 1 GiB budget does not keep the 512 MiB floor"); reported.heapBudget[2] = 40 * GiB; Require(StorageTextureBudget(memory, &reported, 0, 0) == 4 * GiB, "a budget above the heap size raised the storage image cache past a quarter of the heap"); + memory.memoryHeaps[2].size = 8 * GiB; + reported.heapBudget[2] = 7189 * MiB; + reported.heapUsage[2] = 4491 * MiB; + Require(StorageTextureBudget(memory, &reported, 49 * MiB, 506 * MiB) == 7189 * MiB / 4u, "49 MiB of sampled textures on an 8 GiB heap with a 7189 MiB budget and 3936 MiB used outside the texture caches held the storage image cache below a quarter of the budget"); + reported.heapUsage[2] = 6490 * MiB; + Require(StorageTextureBudget(memory, &reported, 2 * GiB, 506 * MiB) == 512 * MiB, "2 GiB of sampled textures on an 8 GiB heap with a 7189 MiB budget and 3936 MiB used outside the texture caches did not take the storage image cache back to its 512 MiB floor"); + memory.memoryHeaps[2].size = 10 * GiB; + reported.heapBudget[2] = 8767 * MiB; + reported.heapUsage[2] = 7981 * MiB; + Require(StorageTextureBudget(memory, &reported, 1427 * MiB, 490 * MiB) == 512 * MiB, "a 10 GiB heap at 7981 of its 8767 MiB budget with 1427 MiB of sampled textures does not keep the storage image cache at its 512 MiB floor"); memory.memoryHeaps[0].flags = 0; memory.memoryHeaps[2].flags = 0; Require(StorageTextureBudget(memory, &reported, 0, 0) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of storage images"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index e96d71cc14..be30cdaff8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -812,7 +812,7 @@ std::uint64_t ReadBudget(const Context& context) { return SampledTextureCacheBudget(context); } -constexpr std::uint64_t PressedUsage = ReportedBudget - 11ull * (1ull << 30u); +constexpr std::uint64_t PressedUsage = ReportedBudget - 9ull * (1ull << 30u); void VKAPI_PTR ReportPressure(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { auto* reported = static_cast(properties->pNext); @@ -853,8 +853,8 @@ void storageMemoryTests(Context context) { Require(ReadStorageBudget(context) == quarter, "52 GiB free of a 64 GiB budget does not leave the storage image cache its quarter"); context.memoryProperties2 = &ReportPressure; const auto pressed = ReadStorageBudget(context); - const auto expected = GiB + SampledTextureMemory() + TextureCacheUsage().storageBytes; - Require(pressed == expected, "53 GiB in use of a 64 GiB budget leaves the storage image cache " + std::to_string(pressed >> 20u) + " MiB, not the " + std::to_string(expected >> 20u) + " MiB left after the use outside the texture caches, the sampled textures' 2 GiB floor and 8 GiB of headroom"); + const auto expected = GiB + TextureCacheUsage().storageBytes; + Require(pressed == expected, "55 GiB in use of a 64 GiB budget leaves the storage image cache " + std::to_string(pressed >> 20u) + " MiB, not the " + std::to_string(expected >> 20u) + " MiB left after the use outside the texture caches, the sampled textures and 8 GiB of headroom"); std::printf("reported heap budget %llu MiB with %llu MiB in use: storage image cache budget %llu MiB\n", static_cast(ReportedBudget >> 20u), static_cast(PressedUsage >> 20u), static_cast(pressed >> 20u)); } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 6ec4e45f5c..26633e1ed1 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -55,8 +55,8 @@ ### Functional - [Workgroup count split](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) - a direct dispatch over `maxComputeWorkGroupCount` runs as `vkCmdDispatch` chunks, each bound to a pipeline variant whose workgroup offset specialization constants add the chunk's base to `WorkgroupId` and `GlobalInvocationId`. A variant is built when its chunk base is first recorded, not when the dispatch is prepared. A split of a shader with device-memory LDS throws: its slot base reads the chunk's `NumWorkgroups`, not the dispatch's. Chunks are ordered by a compute barrier, which the console does not have between workgroups of one dispatch. -- [Texture cache budgets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - the storage image cache's budget is what the reported heap budget leaves after the heap's use other than the cached storage images, the unused part of the sampled textures' 2 GiB floor and an eighth of the budget, capped at a quarter of the budget and at `TextureCacheBudget` (a quarter of the heap's size, at least 2 GiB), never below 512 MiB; the sampled cache's budget reserves `TextureCacheBudget` for the storage cache. So the sampled budget falls first while the sampled textures alive stay within their floor, and both fall together above it; each cache trims only at its next insert, a storage image is allocated before its insert reads the budget, and both budgets read the heap at most once a second, so a burst between two reads can still exceed the reported budget. The 2 GiB floor and the eighth are the sampled formula's values reused and the 512 MiB floor and the caps the earlier ones; none was measured beyond one title (PPSA11386) on one 10 GB GPU, and a title whose storage working set exceeds the shrunk budget re-creates storage images at each use. - [Scan-resistant sampled texture admission](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - while the sampled texture cache is over its budget but within a quarter over it, the first use of a texture it does not hold in a present is refused (used for its draw, then released, never cached) when its least recently used entry was used in the current or previous present, and a later use in the same present is refused when that entry was used in the current present. So a working set larger than the budget keeps a stable subset: a texture used more than once in a present still evicts an entry not yet used in that present, so a working set with interleaved multi-use textures rotates as on main, and the textures that arrive after the cache fills are created again at each use for as long as the least recently used entry is from the current present, including in the transition present after a scene change. Past a quarter over the budget the entries are evicted as before, so after the budget falls the cache can sit at up to 125% of the reduced budget until entries stop being used. Entries unused for two presents are evicted as before. A later use can evict an entry from the previous present and then be refused when the next least recently used entry is from the current present, so that entry is created again in the next present. The "current or previous present" window is an unmeasured threshold: it was chosen, not derived. Measured once, as one A-B-A series on one title (loading phase about 33000 textures per frame against a 5.4 GiB budget) on one RTX 4090, 3600 s each: 932 / 968 / 941 frames, evictions after frame 620 2.72M -> 1.17M, texture creations unchanged (about 5.8M). Nothing is measured on other titles or GPUs, and a title whose per-present working set only slightly exceeds the budget now re-creates the excess textures at every use. +- [Texture cache budgets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - the storage image cache's budget is what the reported heap budget leaves after the heap's use other than the cached storage images (at least the sampled textures alive) and an eighth of the budget, capped at a quarter of the budget and at `TextureCacheBudget` (a quarter of the heap's size, at least 2 GiB), never below 512 MiB; the sampled cache's budget reserves `TextureCacheBudget` for the storage cache and never falls below 2 GiB. The storage budget keeps no room for the part of that 2 GiB floor the sampled textures alive do not use: it gives room back as they grow, at its next read. As the heap's other use grows the sampled budget reaches its floor first, then the storage budget falls. Each cache trims only at its next insert, a storage image is allocated before its insert reads the budget, and both budgets read the heap at most once a second, so a burst between two reads, sampled textures included, can still exceed the reported budget. The eighth is the sampled formula's value reused and the 512 MiB floor and the caps the earlier ones; none was measured beyond PPSA11386 on one 10 GB GPU and PPSA16064 on one 8 GB GPU, and a title whose storage working set exceeds the shrunk budget re-creates storage images at each use. - [Polygon primitive type](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_PRIMITIVE_TYPE` 21 (`PrimitiveType::Polygon`) decodes to `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN` and is treated as inactive for vertex point size, like triangle fans and strips. PPSA01317 (Gran Turismo 7) submits polygon draws; whether the console performs polygon-mode decomposition or custom vertex routing is unverified. - [Native multisampled DCC fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) import uniform 0000/0001/1110/1111 keys into a single-layer, level-zero SW_64KB_R_X color image with 2, 4 or 8 fragments. Color and DCC/CMASK ranges must stay write-watched and unchanged; expanded or mixed initial DCC, register CMASK clears, compressed payloads, color transforms, lossy modes, other metadata changes, guest publication and writes over a live owner throw. A whole-range DCC fill with one of those keys, landed or still recorded, is taken as its fast clear; a partial fill, or a CMASK changed without a whole fill, throws. Driver-mediated reads of the color and metadata stay guarded. The metadata extent follows [AMD Addrlib](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L347-L454) with the [PS5 Mesa model](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/winsys/ps5/radv_ps5_winsys.c): GFX10.1, 16 pipes, 256-byte interleave, two maximum compressed fragments. Console geometry, concurrent guest mutation and transitions outside this subset are unverified. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index e37f8c1ab0..ac055620f7 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -290,7 +290,7 @@ Besides the recompiled shaders, the shader cache directory holds `pipelines.jour ### Texture cache budget -Sampled textures and storage images stay in device memory in two caches that evict their least recently used entries when they grow past their budgets. The storage image budget is what the `VK_EXT_memory_budget` budget of the largest device-local heap leaves after the heap's use other than the cached storage images themselves, the part of the sampled textures' 2 GiB floor not yet in use, and an eighth of the budget; it is capped at a quarter of that budget, at most a quarter of the heap's size or 2 GiB, whichever is larger, and never below 512 MiB. The sampled texture budget is what the reported budget leaves after the heap's use other than cached textures, a quarter of the heap's size (at least 2 GiB) and an eighth of the budget, and never below 2 GiB. While the sampled textures alive stay within their floor the sampled budget falls first, to that floor, then the storage budget; above it both fall together. Each cache trims at its next insert. Both budgets are read again every second; `[gpu] sampled texture cache budget` lines report them at start-up and when either moves by more than a tenth, at most every 10 s. `APS5_TEXTURE_CACHE_MIB=` fixes both budgets at `n` MiB, and the lines are then not printed. +Sampled textures and storage images stay in device memory in two caches that evict their least recently used entries when they grow past their budgets. The storage image budget is what the `VK_EXT_memory_budget` budget of the largest device-local heap leaves after the heap's use other than the cached storage images themselves and an eighth of the budget; it is capped at a quarter of that budget, at most a quarter of the heap's size or 2 GiB, whichever is larger, and never below 512 MiB. The sampled texture budget is what the reported budget leaves after the heap's use other than cached textures, a quarter of the heap's size (at least 2 GiB) and an eighth of the budget, and never below 2 GiB. As the heap's other use grows the sampled budget falls first, to that floor, then the storage budget; the storage budget also gives back what the sampled textures alive take as they grow. Each cache trims at its next insert. Both budgets are read again every second; `[gpu] sampled texture cache budget` lines report them at start-up and when either moves by more than a tenth, at most every 10 s. `APS5_TEXTURE_CACHE_MIB=` fixes both budgets at `n` MiB, and the lines are then not printed. A fixed budget above the automatic one can exceed what the card holds next to the rest of the frame. Allocations then go to host memory (`[gpu] ... placed in host memory` lines), where the GPU reads them over PCIe. PPSA16064 on an RTX 4060 8 GB, whose automatic storage image budget was 1797 MiB under the earlier rule, placed 868 and 878 MiB in host memory with `APS5_TEXTURE_CACHE_MIB=3072` and walked through its first level at 4.46 and 4.83 FPS; with the variable unset it placed none and ran the same walk at 6.48 and 6.07 FPS. Leave it unset unless a measurement on the card shows otherwise. From 16daeffb65465c550eeb310dd274b52c5557a2f8 Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Mon, 19 Oct 2026 14:21:28 +0200 Subject: [PATCH 140/167] test(agc): sweep the storage image budget across heap sizes and pressure Checks the floor and caps, that more memory used outside the texture caches never raises the budget, and that a budget above the floor leaves room for the live sampled textures plus an eighth of the budget, on 2-32 GiB heaps. Co-authored-by: JJenkx <31495916+JJenkx@users.noreply.github.com> --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 9f562b1a79..e28a78b241 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -6457,6 +6457,25 @@ void storageTextureBudgetTests() { reported.heapBudget[2] = 8767 * MiB; reported.heapUsage[2] = 7981 * MiB; Require(StorageTextureBudget(memory, &reported, 1427 * MiB, 490 * MiB) == 512 * MiB, "a 10 GiB heap at 7981 of its 8767 MiB budget with 1427 MiB of sampled textures does not keep the storage image cache at its 512 MiB floor"); + for (const auto heapGiB : {2u, 4u, 6u, 8u, 16u, 32u}) { + memory.memoryHeaps[2].size = heapGiB * GiB; + for (std::uint64_t budget = GiB; budget <= heapGiB * GiB; budget += 512 * MiB) { + reported.heapBudget[2] = budget; + for (std::uint64_t sampled = 0; sampled <= budget; sampled += 256 * MiB) { + std::uint64_t previous = 0; + for (std::uint64_t outside = 0; outside <= budget + GiB; outside += 128 * MiB) { + reported.heapUsage[2] = outside + sampled + 512 * MiB; + const auto limit = StorageTextureBudget(memory, &reported, sampled, 512 * MiB); + Require(limit >= 512 * MiB, "pressure sweep lost the storage cache floor"); + Require(limit <= std::max(512 * MiB, budget / 4u), "pressure sweep exceeded a quarter of the reported budget above the floor"); + Require(limit <= std::max(2 * GiB, heapGiB * GiB / 4u), "pressure sweep exceeded the heap storage cache cap"); + if (outside != 0) Require(limit <= previous, "additional non-cache memory usage grew the storage cache budget"); + if (limit > 512 * MiB) Require(outside + sampled + budget / 8u + limit <= budget, "storage cache budget consumed live sampled memory or pressure headroom"); + previous = limit; + } + } + } + } memory.memoryHeaps[0].flags = 0; memory.memoryHeaps[2].flags = 0; Require(StorageTextureBudget(memory, &reported, 0, 0) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of storage images"); From 91aa7dc60574bf54da2f9ddc5633c1e347bde6f9 Mon Sep 17 00:00:00 2001 From: AnyPS5 Contributors Date: Thu, 15 Oct 2026 11:55:45 -0400 Subject: [PATCH 141/167] test(windows): reproduce stale write tracking on direct remaps --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../libSceAgcDriver/tests/WriteTracking.cpp | 82 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index f29b84cc0b..a448182645 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -648,7 +648,9 @@ if(BUILD_TESTING) if(WIN32) add_test(NAME agc_write_tracking_metadata_commit COMMAND agc_write_tracking_tests --metadata-commit) set_tests_properties(agc_write_tracking_metadata_commit PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) - set_tests_properties(agc_write_tracking agc_write_tracking_metadata_commit PROPERTIES ENVIRONMENT_MODIFICATION + add_test(NAME agc_resident_direct_remap COMMAND agc_write_tracking_tests --resident-direct-remap) + set_tests_properties(agc_resident_direct_remap PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 20) + set_tests_properties(agc_write_tracking agc_write_tracking_metadata_commit agc_resident_direct_remap PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 14d6423661..700455ddd0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -30,6 +31,7 @@ using namespace AgcDriver::GuestMemory; constexpr std::size_t Block = 65536; #ifdef _WIN32 constexpr wchar_t HostHoleMode[] = L"--host-hole"; +constexpr wchar_t ResidentRemapMode[] = L"--resident-direct-remap"; #endif void Require(bool condition, const std::string& message) { @@ -647,6 +649,82 @@ void CheckRewatch() { GuestArena::GuestArenaRelease_nid_postfix(memory, 4 * Block); } +void CheckResidentDirectMappingReuse() { + constexpr std::size_t bytes = 5 * Block; + auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block)); + Require(memory != nullptr, "cannot reserve the resident remap fixture"); + struct Cleanup { + std::uint8_t* memory; + std::array sections{}; + ~Cleanup() { + GuestArena::GuestArenaReset_nid_postfix(memory, bytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, bytes); + for (const auto section : sections) { + if (section == nullptr) continue; + GuestArena::GuestArenaForgetSection_nid_postfix(section); + CloseHandle(section); + } + } + } cleanup{memory}; + GuestArena::GuestArenaCommit_nid_postfix(memory, 3 * Block, PAGE_READWRITE, 3 * Block); + GuestArena::GuestArenaCommit_nid_postfix(memory + 3 * Block, 2 * Block, PAGE_READWRITE, 2 * Block); + Require(Watched(reinterpret_cast(memory), bytes), "the resident remap fixture is not watched"); + for (auto& section : cleanup.sections) { + section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, 3 * Block, nullptr); + Require(section != nullptr, "cannot create the resident remap backing"); + } + const auto base = reinterpret_cast(memory); + const auto nativeType = [&](std::size_t offset) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(memory + offset, &info, sizeof(info)) == sizeof(info), "cannot query the resident remap backing"); + return info.Type; + }; + for (std::size_t use = 0; use < 2; ++use) { + BumpCollectEpoch(); + const auto before = CollectWrites(base, 3 * Block); + Require(before != 0, "the backing before a resident remap was not collected"); + Unwatch(base + Block, Block); + Require(!Watched(base + Block, Block), "the resident remap exclusion was not established"); + const auto outside = CollectWrites(base + 3 * Block, 2 * Block); + const auto mapping = GuestArena::GuestArenaCommitGeneration_nid_postfix(); + GuestArena::GuestArenaMapDirect_nid_postfix(memory, 3 * Block, cleanup.sections[use], 0, PAGE_READWRITE); + Require(nativeType(0) == MEM_PRIVATE && nativeType(2 * Block) == MEM_PRIVATE, "the resident remap did not create private memory"); + Require(Watched(base, 3 * Block), "a resident direct remap retained an old import exclusion"); + Require(GuestArena::GuestArenaCommitGeneration_nid_postfix() > mapping, "a resident direct remap reused the mapping generation"); + Require(!UnchangedSince(base, Block, before) && !UnchangedSince(base + 2 * Block, Block, before), "a resident direct remap reused old neighbour stamps"); + Require(UnchangedSince(base + 3 * Block, 2 * Block, outside), "a resident direct remap invalidated memory outside its range"); + const auto after = CollectWritesUncached(base, 3 * Block); + Require(after > before && UnchangedSince(base, 3 * Block, after), "the resident remap could not establish a current generation"); + memory[Block + 9] = static_cast(0x51 + use); + CollectWritesUncached(base, 3 * Block); + Require(!UnchangedSince(base + Block, Block, after), "a CPU store after a resident direct remap was missed"); + std::cout << "resident direct remap " << use << ": private, watched, new mapping generation, CPU store collected\n"; + } + Unwatch(base, 3 * Block); + GuestArena::GuestArenaMap_nid_postfix(memory, 3 * Block, cleanup.sections[0], 0, PAGE_READWRITE); + Require(nativeType(0) == MEM_MAPPED, "the shared remap did not create mapped memory"); + for (std::size_t block = 0; block < 3; ++block) Require(!Watched(base + block * Block, Block), "an ordinary shared remap restored a private import exclusion"); + GuestArena::GuestArenaMapDirect_nid_postfix(memory + 3 * Block, 2 * Block, cleanup.sections[2], 0, PAGE_READWRITE); + GuestArena::GuestArenaMapDirect_nid_postfix(memory, 3 * Block, cleanup.sections[2], 0, PAGE_READWRITE); + Require(nativeType(0) == MEM_MAPPED && nativeType(Block) == MEM_MAPPED && nativeType(2 * Block) == MEM_PRIVATE, "the mixed direct remap did not establish shared and private spans"); + Require(!Watched(base, Block) && !Watched(base + Block, Block) && Watched(base + 2 * Block, Block), "the mixed direct remap restored an exclusion outside its new private span"); + std::cout << "resident direct remap controls: shared exclusions retained, only the new private span restored\n"; + Unwatch(base, 3 * Block); + bool refused = false; + try { + GuestArena::GuestArenaMapDirect_nid_postfix(memory, 3 * Block, cleanup.sections[1], 0, 0); + } catch (const std::system_error& error) { + Require(error.code().value() == ERROR_INVALID_PARAMETER, "the failed resident remap reported an unexpected error"); + refused = true; + } + std::uintptr_t createdBegin = 0; + std::uintptr_t createdEnd = 0; + Require(refused && GuestArena::GuestArenaResidentSpan_nid_postfix(base, 1, &createdBegin, &createdEnd), "the failing protection change did not leave a new private span"); + Require(createdBegin == base && createdEnd <= base + 3 * Block && Watched(base, createdEnd - base), "a failed resident remap did not report its completed private span"); + for (auto block = createdEnd; block < base + 3 * Block; block += Block) Require(!Watched(block, Block), "a failed resident remap restored an uncreated span"); + std::cout << "resident direct remap failure: completed private span watched, original mapping error retained\n"; +} + void CheckUntrackedHint() { constexpr std::size_t bytes = 4 * Block; auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block)); @@ -1331,6 +1409,10 @@ int main() { CheckHostHole(); return 0; } + if (std::wstring_view(GetCommandLineW()).ends_with(ResidentRemapMode)) { + CheckResidentDirectMappingReuse(); + return 0; + } #endif CheckSharedBlock(); CheckStoredAllSince(); From aea2ca9b781d74d471626bb437eee265af74294f Mon Sep 17 00:00:00 2001 From: AnyPS5 Contributors Date: Thu, 15 Oct 2026 12:13:58 -0400 Subject: [PATCH 142/167] fix(windows): restore write tracking for resident direct remaps --- .../libs/prx/libc/include/WindowsMappings.hpp | 7 ++++++- core/libs/prx/libc/src/GuestArena.cpp | 19 ++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 4437020a26..8b4ec7beb8 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -76,11 +76,12 @@ public: reset(reinterpret_cast(address), bytes); } - void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection, bool watched = false, bool resident = false, DWORD viewProtection = PAGE_EXECUTE_READWRITE, std::size_t sectionBytes = SIZE_MAX) { + void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection, bool watched = false, bool resident = false, DWORD viewProtection = PAGE_EXECUTE_READWRITE, std::size_t sectionBytes = SIZE_MAX, std::vector>* createdPrivate = nullptr) { if (sectionBytes < bytes && (resident || sectionBytes == 0 || bytes - sectionBytes >= pageBytes)) throw std::runtime_error("shared guest mapping extends a whole page past its section"); std::lock_guard lock(mutex); collectableRanges.clear(); auto cursor = reinterpret_cast(address); + if (createdPrivate != nullptr) createdPrivate->reserve(createdPrivate->size() + bytes / pageBytes); reset(cursor, bytes); if (resident) watchResident = watched; HANDLE duplicate = nullptr; @@ -111,6 +112,10 @@ public: while (done + runBytes < bytes && (base + runBytes) % chunkBytes != 0 && single(done + runBytes)) runBytes += pageBytes; split(base, runBytes); commitResident(base, runBytes); + if (createdPrivate != nullptr) { + if (!createdPrivate->empty() && createdPrivate->back().first + createdPrivate->back().second == base) createdPrivate->back().second += runBytes; + else createdPrivate->emplace_back(base, runBytes); + } residentRuns.emplace(base, Run{base + runBytes, {}, static_cast(runBytes >> 12)}); for (std::size_t at = 0; at < runBytes; at += pageBytes) { const auto slot = slotOf(done + at); diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 1fd9510565..6c971b0284 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #ifndef _WIN32 #include #include @@ -472,7 +473,23 @@ void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, void GuestArenaMapDirect_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection) { if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("direct memory mapping", pointer, bytes); - WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched(), true); + const auto generation = commitGeneration.fetch_add(1, std::memory_order_acq_rel) + 1; + std::vector> created; + std::exception_ptr failure; + try { + WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched(), true, PAGE_EXECUTE_READWRITE, SIZE_MAX, &created); + } catch (...) { + failure = std::current_exception(); + } + try { + if (const auto callback = privateMappingObserver.load(std::memory_order_acquire)) { + for (const auto& [address, size] : created) callback(address, size, generation); + } + } catch (...) { + if (failure != nullptr) std::rethrow_exception(failure); + throw; + } + if (failure != nullptr) std::rethrow_exception(failure); noteAccess(pointer, bytes); } From 9f6924cc1d59fa99cef97fcbea2f58be4b226c10 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Sun, 18 Oct 2026 01:17:20 -0500 Subject: [PATCH 143/167] feat(libScePlayerSelectionDialog): add scePlayerSelectionDialogParamInitialize stub and tests --- core/libs/prx/libScePlayerSelectionDialog/Export.cpp | 5 +++++ core/libs/tests/GuestPlayerSelectionDialog.cpp | 11 +++++++++++ docs/dev/debt/libScePlayerSelectionDialog.md | 4 ++++ 3 files changed, 20 insertions(+) diff --git a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp index d866a19fe2..4f9c50b9a7 100644 --- a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp +++ b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp @@ -63,6 +63,11 @@ int APS5_VABI scePlayerSelectionDialogGetResult(void* result) { return 0; } +int APS5_VABI scePlayerSelectionDialogParamInitialize(void* param) { + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + NotImplemented_nid_no_patch(__func__); +} + int APS5_VABI scePlayerSelectionDialogTerminate(void) { if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; return 0; diff --git a/core/libs/tests/GuestPlayerSelectionDialog.cpp b/core/libs/tests/GuestPlayerSelectionDialog.cpp index 639a8297fe..8814165e4f 100644 --- a/core/libs/tests/GuestPlayerSelectionDialog.cpp +++ b/core/libs/tests/GuestPlayerSelectionDialog.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include extern "C" { int APS5_VABI scePlayerSelectionDialogInitialize(void); @@ -8,6 +9,7 @@ int APS5_VABI scePlayerSelectionDialogOpen(const void* param); int APS5_VABI scePlayerSelectionDialogUpdateStatus(void); int APS5_VABI scePlayerSelectionDialogGetStatus(void); int APS5_VABI scePlayerSelectionDialogGetResult(void* result); +int APS5_VABI scePlayerSelectionDialogParamInitialize(void* param); int APS5_VABI scePlayerSelectionDialogClose(void); int APS5_VABI scePlayerSelectionDialogTerminate(void); } @@ -32,6 +34,15 @@ int main() { Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized); Require(scePlayerSelectionDialogOpen(¶m) == kErrNotInitialized); Require(scePlayerSelectionDialogClose() == kErrNotInitialized); + Require(scePlayerSelectionDialogParamInitialize(nullptr) == kErrArgNull); + bool threw = false; + try { + scePlayerSelectionDialogParamInitialize(¶m); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + Require(param == 0); Require(scePlayerSelectionDialogGetResult(result) == kErrNotInitialized); Require(scePlayerSelectionDialogGetStatus() == 0); Require(scePlayerSelectionDialogUpdateStatus() == 0); diff --git a/docs/dev/debt/libScePlayerSelectionDialog.md b/docs/dev/debt/libScePlayerSelectionDialog.md index c8920961b9..c4936f8740 100644 --- a/docs/dev/debt/libScePlayerSelectionDialog.md +++ b/docs/dev/debt/libScePlayerSelectionDialog.md @@ -3,3 +3,7 @@ ### Silent stubs - [libScePlayerSelectionDialog](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) - the player-selection dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled. `GetResult` writes only the first `int32` (`USER_CANCELED`); the rest of `ScePlayerSelectionDialogResult` (player list pointer and length) is not filled. EA SPORTS FC 27 (PPSA34015) fails to load `libScePlayerSelectionDialog.prx` without `Initialize`, `Open`, `Close`, `GetStatus`, `UpdateStatus` and `GetResult` (`unresolved ELF import` / `GetLastError: 127`) + +### Unknown function info + +- [scePlayerSelectionDialogParamInitialize](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) (libScePlayerSelectionDialog) - returns `COMMON_DIALOG_ERROR_ARG_NULL` on a null parameter and throws `NotImplemented_nid_no_patch` for non-null pointers because `ScePlayerSelectionDialogParam` struct layout and default fields remain unverified. From 648d19f91e54d1ef01c75bfd94c0ece39fb977f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 19 Oct 2026 19:26:38 +0200 Subject: [PATCH 144/167] fix(libScePlayerSelectionDialog): link libc into the test for NotImplemented_nid_no_patch --- core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt b/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt index 02e53fdc16..f61cf77ce5 100644 --- a/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt +++ b/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt @@ -5,7 +5,7 @@ configure_windows_unwind(libScePlayerSelectionDialog) if(BUILD_TESTING) add_test_executable(guest_player_selection_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestPlayerSelectionDialog.cpp) target_include_directories(guest_player_selection_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_player_selection_dialog_tests PRIVATE libScePlayerSelectionDialog) + target_link_libraries(guest_player_selection_dialog_tests PRIVATE libScePlayerSelectionDialog libc) add_test(NAME guest_player_selection_dialog COMMAND guest_player_selection_dialog_tests) if(WIN32) set_tests_properties(guest_player_selection_dialog PROPERTIES ENVIRONMENT_MODIFICATION From 32a3c596504016e854d9699e5c889adbd587c26b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 20 Oct 2026 13:30:29 +0200 Subject: [PATCH 145/167] fix(libScePlayerSelectionDialog): unwind the test exe on Windows so the NotImplemented throw is catchable Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt b/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt index f61cf77ce5..670b5b3b0a 100644 --- a/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt +++ b/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt @@ -6,6 +6,7 @@ if(BUILD_TESTING) add_test_executable(guest_player_selection_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestPlayerSelectionDialog.cpp) target_include_directories(guest_player_selection_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_player_selection_dialog_tests PRIVATE libScePlayerSelectionDialog libc) + configure_windows_unwind(guest_player_selection_dialog_tests) add_test(NAME guest_player_selection_dialog COMMAND guest_player_selection_dialog_tests) if(WIN32) set_tests_properties(guest_player_selection_dialog PROPERTIES ENVIRONMENT_MODIFICATION From eead108c3bf82866bfaca2ea1c61c45b6a38beea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Mon, 19 Oct 2026 22:20:41 +0200 Subject: [PATCH 146/167] fix(libkernel): report ELOOP and ENAMETOOLONG from mkdir and rmdir #6648 taught open, unlink and stat to report a symbolic link loop and an over-long name, but sceKernelMkdir and sceKernelRmdir still lost them: their std::filesystem queries fail with filename_too_long or too_many_symbolic_link_levels and the code went on to ENOENT, EIO or ENOTDIR. A PathLookupFailure check after those queries now returns the guest error (ELOOP 62, ENAMETOOLONG 63) as FreeBSD's namei does, and the POSIX wrappers set the same errno. guest_path_errors covers the open, unlink, stat, rename, mkdir and rmdir paths with a self-referencing link, a two-link cycle, a 300-byte name and each as a path prefix; it is UNIX-only because it creates symbolic links. --- core/libs/prx/libkernel/CMakeLists.txt | 5 + core/libs/prx/libkernel/File/src/Stdio.cpp | 7 ++ core/libs/tests/GuestPathErrors.cpp | 135 +++++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 core/libs/tests/GuestPathErrors.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 3b2ed8af91..8e3ea1b164 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -310,6 +310,11 @@ if(BUILD_TESTING) target_link_libraries(guest_record_lock_owners_tests PRIVATE libkernel libc) add_test(NAME guest_record_lock_owners COMMAND guest_record_lock_owners_tests) set_tests_properties(guest_record_lock_owners PROPERTIES TIMEOUT 20) + add_test_executable(guest_path_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestPathErrors.cpp) + target_include_directories(guest_path_errors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_path_errors_tests PRIVATE libkernel libc) + add_test(NAME guest_path_errors COMMAND guest_path_errors_tests) + set_tests_properties(guest_path_errors PROPERTIES TIMEOUT 20) add_test_executable(guest_host_standard_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestHostStandardError.cpp) target_include_directories(guest_host_standard_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_host_standard_error_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 4d786eb038..615cd257e9 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -445,6 +445,11 @@ static int SceErrorFromErrno(int error) { return static_cast(0x80020000u | static_cast(guest)); } +static int PathLookupFailure(const std::error_code& error) { + const bool lookup = error == std::errc::filename_too_long || error == std::errc::too_many_symbolic_link_levels; + return lookup ? SceErrorFromErrno(error.default_error_condition().value()) : 0; +} + extern "C" int* APS5_VABI __error_nid_postfix(); static int PosixFailure(int error) { @@ -1160,6 +1165,7 @@ int APS5_VABI sceKernelMkdir(const char* path, uint16_t mode) { while (!native.has_filename() && native.has_relative_path()) native = native.parent_path(); std::error_code error; if (std::filesystem::exists(native, error)) return SceErrorFromErrno(GUEST_EEXIST); + if (const int failure = PathLookupFailure(error)) return failure; if (!std::filesystem::is_directory(WithoutTrailingSeparator(native).parent_path(), error)) { return SceErrorFromErrno(HasNonDirectoryPrefix(native) ? GUEST_ENOTDIR : GUEST_ENOENT); } @@ -1393,6 +1399,7 @@ int APS5_VABI sceKernelRmdir(const char* path) { if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; if (std::filesystem::is_symlink(native, error)) return SceErrorFromErrno(GUEST_ENOTDIR); + if (const int failure = PathLookupFailure(error)) return failure; if (!std::filesystem::is_directory(native, error)) { const bool blockedByFile = HasNonDirectoryPrefix(native) || std::filesystem::exists(WithoutTrailingSeparator(native), error); return SceErrorFromErrno(blockedByFile ? GUEST_ENOTDIR : GUEST_ENOENT); diff --git a/core/libs/tests/GuestPathErrors.cpp b/core/libs/tests/GuestPathErrors.cpp new file mode 100644 index 0000000000..f593c761a7 --- /dev/null +++ b/core/libs/tests/GuestPathErrors.cpp @@ -0,0 +1,135 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/KernelErrors.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode); +int APS5_VABI sceKernelUnlink(const char* path); +int APS5_VABI sceKernelClose(int d); +int APS5_VABI open_nid_postfix(const char* path, int flags, int mode); +int APS5_VABI unlink_nid_postfix(const char* path); +int APS5_VABI sceKernelStat(const char* path, FileStat* sb); +int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb); +int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb); +int APS5_VABI sceKernelMkdir(const char* path, std::uint16_t mode); +int APS5_VABI sceKernelRmdir(const char* path); +int APS5_VABI mkdir_nid_postfix(const char* path, std::uint16_t mode); +int APS5_VABI rmdir_nid_postfix(const char* path); +int APS5_VABI sceKernelRename(const char* from, const char* to); +int APS5_VABI rename_nid_postfix(const char* from, const char* to); +int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t length); +int APS5_VABI truncate_nid_postfix(const char* path, std::int64_t length); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "path error check failed at line %d\n", line); + std::exit(1); + } +} +#define Check(value) Require((value), __LINE__) + +constexpr int GuestEloop = 62; +constexpr int GuestEnametoolong = 63; +constexpr int SceEloop = SceKernelError(GuestEloop); +constexpr int SceEnametoolong = SceKernelError(GuestEnametoolong); + +bool PosixFails(int result, int error) { + const bool failed = result == -1 && *__error_nid_postfix() == error; + *__error_nid_postfix() = 0; + return failed; +} + +std::filesystem::path EnterFreshRoot() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-path-errors-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + Check(::symlink("loop", "loop") == 0); + Check(::symlink("second", "first") == 0 && ::symlink("first", "second") == 0); + { std::ofstream file("plain.txt"); file << "plain"; } + return root; +} + +const std::string LongName(300, 'n'); + +void OpenAndUnlink() { + Check(sceKernelOpen("loop", SCE_KERNEL_O_RDONLY, 0) == SceEloop); + Check(sceKernelOpen("first", SCE_KERNEL_O_RDONLY, 0) == SceEloop); + Check(sceKernelOpen("loop/inside", SCE_KERNEL_O_RDONLY, 0) == SceEloop); + Check(sceKernelOpen(LongName.c_str(), SCE_KERNEL_O_RDONLY, 0) == SceEnametoolong); + Check(sceKernelOpen((LongName + "/inside").c_str(), SCE_KERNEL_O_RDONLY, 0) == SceEnametoolong); + Check(sceKernelOpen(LongName.c_str(), SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT, 0644) == SceEnametoolong); + Check(PosixFails(open_nid_postfix("loop", SCE_KERNEL_O_RDONLY, 0), GuestEloop)); + Check(PosixFails(open_nid_postfix(LongName.c_str(), SCE_KERNEL_O_RDONLY, 0), GuestEnametoolong)); + Check(sceKernelUnlink(LongName.c_str()) == SceEnametoolong); + Check(sceKernelUnlink("loop/inside") == SceEloop); + Check(PosixFails(unlink_nid_postfix(LongName.c_str()), GuestEnametoolong)); + const int plain = sceKernelOpen("plain.txt", SCE_KERNEL_O_RDONLY, 0); + Check(plain >= 0 && sceKernelClose(plain) == 0); +} + +void Status() { + FileStat status{}; + Check(sceKernelStat("loop", &status) == SceEloop); + Check(sceKernelStat("first", &status) == SceEloop); + Check(sceKernelStat("loop/inside", &status) == SceEloop); + Check(sceKernelStat(LongName.c_str(), &status) == SceEnametoolong); + Check(sceKernelStat((LongName + "/inside").c_str(), &status) == SceEnametoolong); + Check(PosixFails(stat_nid_postfix("loop", &status), GuestEloop)); + Check(PosixFails(stat_nid_postfix(LongName.c_str(), &status), GuestEnametoolong)); + Check(PosixFails(lstat_nid_postfix(LongName.c_str(), &status), GuestEnametoolong)); + Check(lstat_nid_postfix("loop", &status) == 0 && (status.st_mode & 0170000) == 0120000); + Check(sceKernelStat("plain.txt", &status) == 0 && status.st_size == 5); + Check(sceKernelStat("missing.txt", &status) == SceKernelError(2)); +} + +void Directories() { + Check(sceKernelMkdir("loop/inside", 0755) == SceEloop); + Check(sceKernelMkdir(LongName.c_str(), 0755) == SceEnametoolong); + Check(PosixFails(mkdir_nid_postfix("loop/inside", 0755), GuestEloop)); + Check(sceKernelRmdir("loop/inside") == SceEloop); + Check(sceKernelRmdir(LongName.c_str()) == SceEnametoolong); + Check(sceKernelRmdir((LongName + "/inside").c_str()) == SceEnametoolong); + Check(PosixFails(rmdir_nid_postfix(LongName.c_str()), GuestEnametoolong)); + Check(sceKernelRmdir("loop") == SceKernelError(20)); + Check(sceKernelMkdir("made", 0755) == 0 && sceKernelRmdir("made") == 0); + Check(sceKernelRmdir("made") == SceKernelError(2)); +} + +void Rename() { + Check(sceKernelRename(LongName.c_str(), "target.txt") == SceEnametoolong); + Check(sceKernelRename("loop/inside", "target.txt") == SceEloop); + Check(sceKernelRename("plain.txt", LongName.c_str()) == SceEnametoolong); + Check(sceKernelRename("plain.txt", "loop/inside") == SceEloop); + Check(PosixFails(rename_nid_postfix(LongName.c_str(), "target.txt"), GuestEnametoolong)); + Check(sceKernelRename("missing.txt", "target.txt") == SceKernelError(2)); + Check(sceKernelRename("plain.txt", "moved.txt") == 0 && sceKernelRename("moved.txt", "plain.txt") == 0); +} + +} + +int main() { + const auto root = EnterFreshRoot(); + OpenAndUnlink(); + Status(); + Directories(); + Rename(); + std::error_code error; + std::filesystem::current_path(std::filesystem::temp_directory_path(), error); + std::filesystem::remove_all(root, error); + return 0; +} From 5057a9e54d8680568e60a4ad61b399ad54e3819c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Mon, 19 Oct 2026 22:20:53 +0200 Subject: [PATCH 147/167] fix(libkernel): report ELOOP and ENAMETOOLONG from truncate sceKernelTruncate's status query fails with too_many_symbolic_link_levels for a link loop and filename_too_long for an over-long name, and the call answered ENOENT for both. It now returns ELOOP or ENAMETOOLONG through PathLookupFailure, and truncate sets the same errno. --- core/libs/prx/libkernel/File/src/Stdio.cpp | 1 + core/libs/tests/GuestPathErrors.cpp | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 615cd257e9..cf4e9db165 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1452,6 +1452,7 @@ int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t lengt if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; const auto status = std::filesystem::status(native, error); + if (const int failure = PathLookupFailure(error)) return failure; if (!std::filesystem::exists(status)) return SceErrorFromErrno(NotFoundError_nid_no_patch(native)); if (std::filesystem::is_directory(status)) return SceErrorFromErrno(GUEST_EISDIR); if (const int failure = NativeTruncate(native, length)) return SceErrorFromErrno(failure); diff --git a/core/libs/tests/GuestPathErrors.cpp b/core/libs/tests/GuestPathErrors.cpp index f593c761a7..3da76d9739 100644 --- a/core/libs/tests/GuestPathErrors.cpp +++ b/core/libs/tests/GuestPathErrors.cpp @@ -120,6 +120,15 @@ void Rename() { Check(sceKernelRename("plain.txt", "moved.txt") == 0 && sceKernelRename("moved.txt", "plain.txt") == 0); } +void Truncate() { + Check(sceKernelTruncate_nid_postfix("loop", 0) == SceEloop); + Check(sceKernelTruncate_nid_postfix(LongName.c_str(), 0) == SceEnametoolong); + Check(PosixFails(truncate_nid_postfix("loop", 0), GuestEloop)); + Check(PosixFails(truncate_nid_postfix(LongName.c_str(), 0), GuestEnametoolong)); + Check(sceKernelTruncate_nid_postfix("missing.txt", 0) == SceKernelError(2)); + Check(sceKernelTruncate_nid_postfix("plain.txt", 2) == 0 && std::filesystem::file_size("plain.txt") == 2); +} + } int main() { @@ -128,6 +137,7 @@ int main() { Status(); Directories(); Rename(); + Truncate(); std::error_code error; std::filesystem::current_path(std::filesystem::temp_directory_path(), error); std::filesystem::remove_all(root, error); From 92e136a129435e1ffe1500e5e28105c7ec55aaf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 12:23:13 +0200 Subject: [PATCH 148/167] feat(libkernel): report the write end of a pipe to equeue descriptor polls GuestSockets::Ready only knew the read end of a pipe, and WaitAny asked the host for POLLIN on every descriptor that is not a socket, so a write interest on a pipe could never be reported or wake a wait. PipeWriteState gives the free space of the host buffer (F_GETPIPE_SZ less FIONREAD on Linux, the write quota on Windows), EOF once the read end is closed, and whether the host takes a write now; Ready reports the write end when it is writable or at EOF, and WaitAny asks for POLLOUT on it. macOS has no call for the capacity of a pipe and throws. --- .../Socket/include/SocketRuntime.hpp | 1 + core/libs/prx/libkernel/Socket/src/Udp.cpp | 34 +++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp index 4e16028b10..d59cfc4304 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -35,6 +35,7 @@ std::uintptr_t CurrentWaker(); void Wake(std::uintptr_t waker); bool WaitAny(const std::vector& interests, std::uint64_t deadlineNanos); bool PipeReadState(int descriptor, std::int64_t* data, bool* eof); +bool PipeWriteState(int descriptor, std::int64_t* data, bool* eof, bool* writable); std::int64_t Readv(int descriptor, const Iovec* iov, int iovcnt); std::int64_t Writev(int descriptor, const Iovec* iov, int iovcnt); } diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 7b64c96aae..9860dd60f7 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -26,6 +26,7 @@ #endif #endif #include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/PreciseWait.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -376,7 +377,7 @@ bool GuestSockets::WaitAny(const std::vector& interests, std::uint64_t #ifdef _WIN32 pipes = true; #else - entries.push_back({interest.descriptor, ReadMask, 0}); + entries.push_back({interest.descriptor, interest.write ? WriteMask : ReadMask, 0}); #endif continue; } @@ -786,7 +787,11 @@ bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* e { std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); - if (found == sockets.end()) return !write && PipeReadState(descriptor, data, eof) && (*data > 0 || *eof); + if (found == sockets.end() && !write) return PipeReadState(descriptor, data, eof) && (*data > 0 || *eof); + if (found == sockets.end()) { + bool writable = false; + return PipeWriteState(descriptor, data, eof, &writable) && (writable || *eof); + } socket = found->second; } #ifdef _WIN32 @@ -840,6 +845,31 @@ bool GuestSockets::PipeReadState(int descriptor, std::int64_t* data, bool* eof) return true; } +bool GuestSockets::PipeWriteState(int descriptor, std::int64_t* data, bool* eof, bool* writable) { +#ifdef _WIN32 + ACCESS_MASK access = 0; + PipeLocalInformation pipe{}; + if (!QueryPipe(descriptor, "kevent", access, pipe) || !(access & FILE_WRITE_DATA)) return false; + *eof = pipe.state == PipeClosingState; + *writable = pipe.writeQuotaAvailable >= PipeAtomicBytes; + *data = *eof ? 0 : pipe.writeQuotaAvailable; +#elif defined(__linux__) + if (!IsPipe(descriptor, 0) || (::fcntl(descriptor, F_GETFL) & O_ACCMODE) == O_RDONLY) return false; + pollfd entry{descriptor, POLLOUT, 0}; + int queued = 0; + const int capacity = ::fcntl(descriptor, F_GETPIPE_SZ); + if (::poll(&entry, 1, 0) < 0 || ::ioctl(descriptor, FIONREAD, &queued) != 0 || capacity < 0) + throw std::runtime_error("kevent: querying pipe descriptor " + std::to_string(descriptor) + " failed"); + *eof = (entry.revents & POLLERR) != 0; + *writable = (entry.revents & POLLOUT) != 0; + *data = *eof ? 0 : std::max(0, capacity - queued); +#else + if (!IsPipe(descriptor, 0) || (::fcntl(descriptor, F_GETFL) & O_ACCMODE) == O_RDONLY) return false; + NotImplemented_nid_no_patch("kevent: EVFILT_WRITE on a pipe on macOS"); +#endif + return true; +} + int GuestSockets::Poll(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds) { std::vector> held; std::vector native; From bd8ee518c4c5674df658174b712cd483a615d461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 12:23:13 +0200 Subject: [PATCH 149/167] feat(libkernel): support EVFILT_WRITE on the write end of a pipe kevent threw for EVFILT_WRITE on any descriptor that is not a socket. On the write end of a pipe it now follows FreeBSD 11's filt_pipewrite: data is the free buffer space, and once the read end is closed the event reports EV_EOF with data 0. Adding the event after the read end is closed fails with EPIPE, as in pipe_kqfilter. EVFILT_WRITE on the read end still throws, because FreeBSD's pipe ends are bidirectional and host pipes are not. --- core/libs/prx/libkernel/Equeue/src/Kqueue.cpp | 9 ++- core/libs/tests/GuestKqueue.cpp | 70 ++++++++++++++++++- docs/dev/debt/libkernel.md | 4 +- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index 752caca239..8fed47f8ff 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -36,6 +36,7 @@ constexpr int GUEST_EFAULT = 14; constexpr int GUEST_EINVAL = 22; constexpr int GUEST_ENFILE = 23; constexpr int GUEST_EMFILE = 24; +constexpr int GUEST_EPIPE = 32; constexpr int KQ_NEVENTS = 8; constexpr std::uint32_t NOTE_FFCTRLMASK = 0xc0000000; @@ -205,9 +206,13 @@ int AddDescriptorKnote(const KernelEqueueRef& queue, KernelEvent& change, std::u const bool socket = GuestSockets::IsOpen(descriptor); std::int64_t data = 0; bool eof = false; - const bool pipe = !socket && change.filter == EVFILT_READ && GuestSockets::PipeReadState(descriptor, &data, &eof); + bool writable = false; + const bool pipe = !socket && (change.filter == EVFILT_READ ? GuestSockets::PipeReadState(descriptor, &data, &eof) : GuestSockets::PipeWriteState(descriptor, &data, &eof, &writable)); if (!socket && !pipe) { - NotImplemented_nid_no_patch(change.filter == EVFILT_READ ? "kevent: EVFILT_READ on a descriptor that is neither a socket nor the read end of a pipe" : "kevent: EVFILT_WRITE on a non-socket descriptor"); + NotImplemented_nid_no_patch(change.filter == EVFILT_READ ? "kevent: EVFILT_READ on a descriptor that is neither a socket nor the read end of a pipe" : "kevent: EVFILT_WRITE on a descriptor that is neither a socket nor the write end of a pipe"); + } + if (pipe && change.filter == EVFILT_WRITE && eof) { + return GUEST_EPIPE; } const auto lowWater = DescriptorLowWater(change); auto event = EqueueDescriptorEvent(descriptor, change.filter, flags, change.udata); diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index aeb8b759ed..0d581a27e7 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -22,6 +22,7 @@ int APS5_VABI kevent_nid_postfix(int, const KernelEvent*, int, KernelEvent*, int int APS5_VABI close_nid_postfix(int); int APS5_VABI dup_nid_postfix(int); int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI fcntl_nid_postfix(int, int, ...); int APS5_VABI socketpair_nid_postfix(int, int, int, int*); std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); @@ -44,6 +45,11 @@ int* APS5_VABI __error_nid_postfix(); static constexpr int GuestEnoent = 2; static constexpr int GuestEbadf = 9; static constexpr int GuestEinval = 22; +static constexpr int GuestEpipe = 32; +static constexpr int GuestEagain = 35; +static constexpr int SetStatusFlags = 4; +static constexpr int NonBlocking = 0x0004; +static constexpr std::int64_t PipeAtomic = 512; static constexpr int Unix = 1; static constexpr int Stream = 1; static constexpr int FirstSocket = 0x10000000; @@ -254,6 +260,62 @@ static void PipeRead() { Require(close_nid_postfix(kq) == 0); } +#ifndef __APPLE__ +static void PipeWrite() { + const int kq = kqueue_nid_postfix(); + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + const auto writeEnd = static_cast(pipe[1]); + int marker = 0; + KernelEvent events[4]{}; + Require(Apply(kq, Change(writeEnd, EvfiltWrite, EvAdd, 0, 0, &marker)) == 0); + Require(Poll(kq, events, 4) == 1); + Require(events[0].ident == writeEnd && events[0].filter == EvfiltWrite && events[0].udata == &marker && (events[0].flags & EvEof) == 0); + const std::int64_t empty = events[0].data; + Require(empty >= PipeAtomic); + Require(write_nid_postfix(pipe[1], "abc", 3) == 3); + Require(Poll(kq, events, 4) == 1 && events[0].data == empty - 3); + Require(fcntl_nid_postfix(pipe[1], SetStatusFlags, NonBlocking) == 0); + const std::string chunk(4096, 'x'); + std::int64_t queued = 3; + for (;;) { + const std::int64_t written = write_nid_postfix(pipe[1], chunk.data(), static_cast(chunk.size())); + if (written < 0) { + Require(*__error_nid_postfix() == GuestEagain); + break; + } + queued += written; + } + Require(queued > PipeAtomic); + Require(Poll(kq, events, 4) == 0); + std::thread reader([&pipe, queued] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + std::string drained(static_cast(queued), '\0'); + std::int64_t done = 0; + while (done < queued) { + const std::int64_t got = read_nid_postfix(pipe[0], drained.data() + done, static_cast(queued - done)); + Require(got > 0); + done += got; + } + }); + const KernelTimespec limit{5, 0}; + const auto start = std::chrono::steady_clock::now(); + Require(kevent_nid_postfix(kq, nullptr, 0, events, 4, &limit) == 1); + const auto waited = std::chrono::steady_clock::now() - start; + reader.join(); + Require(waited >= std::chrono::milliseconds(40) && waited < std::chrono::seconds(4)); + Require(events[0].ident == writeEnd && events[0].data >= PipeAtomic && (events[0].flags & EvEof) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == empty); + Require(close_nid_postfix(pipe[0]) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 0 && (events[0].flags & EvEof) != 0); + const int late = kqueue_nid_postfix(); + RequireFailure(Apply(late, Change(writeEnd, EvfiltWrite, EvAdd)), GuestEpipe); + Require(close_nid_postfix(late) == 0); + Require(close_nid_postfix(pipe[1]) == 0); + Require(close_nid_postfix(kq) == 0); +} +#endif + static void SocketPair() { const int kq = kqueue_nid_postfix(); int pair[2]; @@ -561,7 +623,10 @@ static void Errors() { int pipe[2]; Require(pipe_nid_postfix(pipe) == 0); Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[1]), EvfiltRead, EvAdd)); })); + Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[0]), EvfiltWrite, EvAdd)); })); +#ifdef __APPLE__ Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[1]), EvfiltWrite, EvAdd)); })); +#endif #ifndef __linux__ Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[0]), EvfiltRead, EvAdd | EvClear)); })); #endif @@ -878,7 +943,7 @@ static void StandardInput() { Require(Apply(kq, Change(0, EvfiltRead, EvAdd, 0, 0, &marker)) == 0); Require(Apply(kq, Change(0, EvfiltRead, EvDelete)) == 0); RequireFailure(Apply(kq, Change(0, EvfiltRead, EvDelete)), GuestEnoent); - Require(ThrownMessage([kq] { Apply(kq, Change(0, EvfiltWrite, EvAdd)); }).find("EVFILT_WRITE on a non-socket descriptor") != std::string::npos); + Require(ThrownMessage([kq] { Apply(kq, Change(0, EvfiltWrite, EvAdd)); }).find("EVFILT_WRITE on a descriptor that is neither a socket nor the write end of a pipe") != std::string::npos); Require(close_nid_postfix(kq) == 0); } @@ -887,6 +952,9 @@ int main() { CreateAndClose(); Timers(); PipeRead(); +#ifndef __APPLE__ + PipeWrite(); +#endif UserEvents(); SocketPair(); #ifdef __linux__ diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 55dfa98df6..ac4cb1a152 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 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 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 - [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 @@ -186,7 +186,7 @@ - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number - [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. `/dev/random` and `/dev/urandom` follow FreeBSD 11.4's [`randomdev_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/dev/random/randomdev.c#L274-L285) for a seeded device: a request with `POLLIN` or `POLLRDNORM` gets only those bits back, and any other request gets every requested bit. Open descriptors 0 to 2 follow the console model `select` uses: descriptor 0 never reports an event and descriptors 1 and 2 report `POLLOUT`, whatever the host attaches to them. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file, a directory, the random device nor descriptor 0 to 2 (other devices, a kqueue) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [kevent EV_CLEAR on a socket or a pipe](../../../core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp) (libkernel) - FreeBSD 11 activates an `EV_CLEAR` knote when `kqueue_register` finds it ready, which every `EV_ADD` or `EV_ENABLE` of an enabled knote does again, and on each `KNOTE` the socket or pipe raises (`sorwakeup`, `sowwakeup`, `pipeselwakeup`); [`kqueue_scan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c) checks the filter again before reporting, drops a knote that is no longer ready and deactivates a reported one until the next activation. On Linux each such knote registers the host socket or pipe with its own epoll instance in edge-triggered mode (`EPOLLIN | EPOLLRDHUP` for `EVFILT_READ`, `EPOLLOUT` for `EVFILT_WRITE`), and a wakeup of the host descriptor with that mask stands for a `KNOTE`: the knote is then checked with the same readiness test as a level-triggered one, `NOTE_LOWAT` mark included, and a wait blocks on the epoll descriptor instead of the socket, so a socket that stays readable does not wake it. The host's wakeups are not FreeBSD's: FreeBSD raises `sowwakeup` whenever send buffer space is freed, while Linux TCP wakes `EPOLLOUT` waiters only once the socket is writable again after a write found it full (`SOCK_NOSPACE`, [`tcp_check_space`](https://github.com/torvalds/linux/blob/v6.8/net/ipv4/tcp_input.c#L5639)), so a TCP writer that never fills its buffer sees fewer `EVFILT_WRITE` events than on FreeBSD; a host stack may also coalesce data wakeups, so the number of times a busy socket fires can differ. Windows and macOS throw, as the host there has no edge-triggered wait over these descriptors that is wired up; `EV_CLEAR` on the timer, user and vnode filters does not use this and works everywhere. -- [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on the read end of a pipe follows FreeBSD 11's [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1806): `data` is the number of buffered bytes, and `EV_EOF` is set once the write end is closed. Windows pipes cannot be waited on, so while a kqueue waits on a pipe it checks it with `NtQueryInformationFile` and waits on its sockets in slices of 1 ms; a wait with no pipe is not sliced. `EVFILT_READ` on the write end of a pipe throws: FreeBSD's `pipe` opens both ends for reading and writing ([`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422)), and host pipes are one-way. `EVFILT_WRITE` on a pipe throws too, and `NOTE_LOWAT` on a pipe is ignored, as `filt_piperead` ignores it. +- [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - host pipes are one-way, where FreeBSD's [`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422) opens both ends for reading and writing, so `EVFILT_READ` on the write end and `EVFILT_WRITE` on the read end throw; on macOS `EVFILT_WRITE` on a pipe throws, as the host has no call for a pipe's capacity. `EVFILT_WRITE` reports the free space of the host buffer, not of FreeBSD's 16 KiB one, and on Linux fires when the host says the pipe is writable rather than at `PIPE_BUF` (512) free bytes. Windows pipes cannot be waited on, so a kqueue waiting on one checks it every 1 ms. Otherwise [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1828) and `filt_pipewrite` are followed: `EV_EOF` once the other end closes, `EPIPE` for a write event added after the read end closed, and `NOTE_LOWAT` ignored. - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level, `TCP_NODELAY` and `IPV6_V6ONLY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. 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 `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. `IPV6_V6ONLY` (`IPPROTO_IPV6`, 27) on an `AF_INET6` socket is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL` ([L1572](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1572)), as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as FreeBSD's default `net.inet6.ip6.v6only` does, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The `IPPROTO_IPV6` level on an `AF_INET` or `AF_UNIX` socket fails with `EINVAL`, as `ip_ctloutput` and `uipc_ctloutput` answer it. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores, the low-water marks among them; PPSA99871 sets `SO_REUSEPORT` and carries on when it fails), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them), every other `IPPROTO_IPV6` option ([L1928-L1929](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1928-L1929), [L2158-L2159](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L2158-L2159)) and the `IPPROTO_IP` level fail with `ENOPROTOOPT`. None of this was measured on a console. - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) 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 From 99e56464a1ab1712d9f1ab97297f393626b76e44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 20 Oct 2026 13:35:07 +0200 Subject: [PATCH 150/167] fix(libkernel): keep the PipeWrite full-pipe case off Windows and start its clock before the reader Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestKqueue.cpp | 10 ++++++++-- docs/dev/debt/libkernel.md | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 0d581a27e7..aa199d6bd0 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -275,6 +275,7 @@ static void PipeWrite() { Require(empty >= PipeAtomic); Require(write_nid_postfix(pipe[1], "abc", 3) == 3); Require(Poll(kq, events, 4) == 1 && events[0].data == empty - 3); +#ifndef _WIN32 Require(fcntl_nid_postfix(pipe[1], SetStatusFlags, NonBlocking) == 0); const std::string chunk(4096, 'x'); std::int64_t queued = 3; @@ -288,6 +289,8 @@ static void PipeWrite() { } Require(queued > PipeAtomic); Require(Poll(kq, events, 4) == 0); + const KernelTimespec limit{5, 0}; + const auto start = std::chrono::steady_clock::now(); std::thread reader([&pipe, queued] { std::this_thread::sleep_for(std::chrono::milliseconds(50)); std::string drained(static_cast(queued), '\0'); @@ -298,14 +301,17 @@ static void PipeWrite() { done += got; } }); - const KernelTimespec limit{5, 0}; - const auto start = std::chrono::steady_clock::now(); Require(kevent_nid_postfix(kq, nullptr, 0, events, 4, &limit) == 1); const auto waited = std::chrono::steady_clock::now() - start; reader.join(); Require(waited >= std::chrono::milliseconds(40) && waited < std::chrono::seconds(4)); Require(events[0].ident == writeEnd && events[0].data >= PipeAtomic && (events[0].flags & EvEof) == 0); Require(Poll(kq, events, 4) == 1 && events[0].data == empty); +#else + char back[3]; + Require(read_nid_postfix(pipe[0], back, 3) == 3); + Require(Poll(kq, events, 4) == 1 && events[0].data == empty); +#endif Require(close_nid_postfix(pipe[0]) == 0); Require(Poll(kq, events, 4) == 1 && events[0].data == 0 && (events[0].flags & EvEof) != 0); const int late = kqueue_nid_postfix(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index ac4cb1a152..5b75b2ffcf 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -186,7 +186,7 @@ - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number - [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. `/dev/random` and `/dev/urandom` follow FreeBSD 11.4's [`randomdev_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/dev/random/randomdev.c#L274-L285) for a seeded device: a request with `POLLIN` or `POLLRDNORM` gets only those bits back, and any other request gets every requested bit. Open descriptors 0 to 2 follow the console model `select` uses: descriptor 0 never reports an event and descriptors 1 and 2 report `POLLOUT`, whatever the host attaches to them. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file, a directory, the random device nor descriptor 0 to 2 (other devices, a kqueue) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [kevent EV_CLEAR on a socket or a pipe](../../../core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp) (libkernel) - FreeBSD 11 activates an `EV_CLEAR` knote when `kqueue_register` finds it ready, which every `EV_ADD` or `EV_ENABLE` of an enabled knote does again, and on each `KNOTE` the socket or pipe raises (`sorwakeup`, `sowwakeup`, `pipeselwakeup`); [`kqueue_scan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c) checks the filter again before reporting, drops a knote that is no longer ready and deactivates a reported one until the next activation. On Linux each such knote registers the host socket or pipe with its own epoll instance in edge-triggered mode (`EPOLLIN | EPOLLRDHUP` for `EVFILT_READ`, `EPOLLOUT` for `EVFILT_WRITE`), and a wakeup of the host descriptor with that mask stands for a `KNOTE`: the knote is then checked with the same readiness test as a level-triggered one, `NOTE_LOWAT` mark included, and a wait blocks on the epoll descriptor instead of the socket, so a socket that stays readable does not wake it. The host's wakeups are not FreeBSD's: FreeBSD raises `sowwakeup` whenever send buffer space is freed, while Linux TCP wakes `EPOLLOUT` waiters only once the socket is writable again after a write found it full (`SOCK_NOSPACE`, [`tcp_check_space`](https://github.com/torvalds/linux/blob/v6.8/net/ipv4/tcp_input.c#L5639)), so a TCP writer that never fills its buffer sees fewer `EVFILT_WRITE` events than on FreeBSD; a host stack may also coalesce data wakeups, so the number of times a busy socket fires can differ. Windows and macOS throw, as the host there has no edge-triggered wait over these descriptors that is wired up; `EV_CLEAR` on the timer, user and vnode filters does not use this and works everywhere. -- [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - host pipes are one-way, where FreeBSD's [`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422) opens both ends for reading and writing, so `EVFILT_READ` on the write end and `EVFILT_WRITE` on the read end throw; on macOS `EVFILT_WRITE` on a pipe throws, as the host has no call for a pipe's capacity. `EVFILT_WRITE` reports the free space of the host buffer, not of FreeBSD's 16 KiB one, and on Linux fires when the host says the pipe is writable rather than at `PIPE_BUF` (512) free bytes. Windows pipes cannot be waited on, so a kqueue waiting on one checks it every 1 ms. Otherwise [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1828) and `filt_pipewrite` are followed: `EV_EOF` once the other end closes, `EPIPE` for a write event added after the read end closed, and `NOTE_LOWAT` ignored. +- [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - host pipes are one-way, where FreeBSD's [`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422) opens both ends for reading and writing, so `EVFILT_READ` on the write end and `EVFILT_WRITE` on the read end throw; on macOS `EVFILT_WRITE` on a pipe throws, as the host has no call for a pipe's capacity. `EVFILT_WRITE` reports the free space of the host buffer, not of FreeBSD's 16 KiB one, and on Linux fires when the host says the pipe is writable rather than at `PIPE_BUF` (512) free bytes. Windows pipes cannot be waited on, so a kqueue waiting on one checks it every 1 ms. Otherwise [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1828) and `filt_pipewrite` are followed: `EV_EOF` once the other end closes, `EPIPE` for a write event added after the read end closed, and `NOTE_LOWAT` ignored. On Windows `fcntl` cannot make a pipe write end non-blocking, so the full-pipe and drain case of `EVFILT_WRITE` is tested on Linux only and the `NtQueryInformationFile` write quota is checked there only for the free space, `EV_EOF` and `EPIPE`. - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level, `TCP_NODELAY` and `IPV6_V6ONLY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. 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 `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. `IPV6_V6ONLY` (`IPPROTO_IPV6`, 27) on an `AF_INET6` socket is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL` ([L1572](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1572)), as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as FreeBSD's default `net.inet6.ip6.v6only` does, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The `IPPROTO_IPV6` level on an `AF_INET` or `AF_UNIX` socket fails with `EINVAL`, as `ip_ctloutput` and `uipc_ctloutput` answer it. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores, the low-water marks among them; PPSA99871 sets `SO_REUSEPORT` and carries on when it fails), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them), every other `IPPROTO_IPV6` option ([L1928-L1929](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1928-L1929), [L2158-L2159](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L2158-L2159)) and the `IPPROTO_IP` level fail with `ENOPROTOOPT`. None of this was measured on a console. - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) 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 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 151/167] 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 152/167] 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 18da29b8988d4c2faa48d367e6d9462dbf589c29 Mon Sep 17 00:00:00 2001 From: Maz Date: Tue, 20 Oct 2026 16:01:02 +0400 Subject: [PATCH 153/167] fix(shader): keep recompiler diagnostics off guest stdio --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 3 ++ .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 41 +++++++++++++++++++ .../tests/GuestHostStandardErrorWindows.cpp | 2 + core/shader/recompiler/CMakeLists.txt | 3 ++ 4 files changed, 49 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 61dd26a9a9..5ebc5b19f3 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2101,6 +2101,9 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test(NAME agc_shader_disk_cache_long_paths COMMAND agc_shader_disk_cache_tests --long-host-paths) + add_test(NAME agc_shader_disk_cache_host_stderr COMMAND agc_shader_disk_cache_tests --host-stderr) + set_tests_properties(agc_shader_disk_cache_host_stderr PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") set_tests_properties(agc_shader_disk_cache_long_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index c86a452b79..d99784fab1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -41,6 +41,9 @@ #define WIN32_LEAN_AND_MEAN #endif #include +#include +#include +#include "prx/libc/include/General.hpp" #endif namespace { @@ -59,6 +62,40 @@ void setEnvironment(const char* name, const std::string& value) { #endif } +#ifdef _WIN32 +void verifyHostStandardError() { + const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-host-stderr-" + std::to_string(std::random_device{}())); + setEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); + setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + int capture[2], guest[2]; + require(::_pipe(capture, 65536, _O_BINARY) == 0, "cannot create host capture"); + require(::_pipe(guest, 65536, _O_BINARY) == 0, "cannot create guest capture"); + const int original = ::_dup(2); + require(original >= 0 && ::_dup2(capture[1], 2) == 0, "cannot capture initial stderr"); + const int saved = ::_dup(2); + require(saved >= 0 && AdoptHostStandardStream_nid_no_patch(2, saved), "cannot preserve host stderr"); + require(::_dup2(guest[1], 2) == 0, "cannot redirect guest stderr"); + require(ShaderDiskCache::Enabled(), "shader disk cache was disabled"); + ShaderDiskCache::Shutdown(); + std::fflush(stderr); + std::fflush(HostStandardErrorFile_nid_no_patch()); + require(::_dup2(original, 2) == 0, "cannot restore stderr"); + ::_close(original); + DWORD hostBytes = 0, guestBytes = 0; + require(::PeekNamedPipe(reinterpret_cast(::_get_osfhandle(capture[0])), nullptr, 0, nullptr, &hostBytes, nullptr) != 0, "cannot inspect host capture"); + require(::PeekNamedPipe(reinterpret_cast(::_get_osfhandle(guest[0])), nullptr, 0, nullptr, &guestBytes, nullptr) != 0, "cannot inspect guest capture"); + std::string host(hostBytes, '\0'); + if (hostBytes != 0) require(::_read(capture[0], host.data(), hostBytes) == static_cast(hostBytes), "cannot read host capture"); + ::_close(capture[0]); + ::_close(capture[1]); + ::_close(guest[0]); + ::_close(guest[1]); + std::filesystem::remove_all(directory); + require(guestBytes == 0, "shader cache diagnostic entered the guest stderr pipe"); + require(host.find("[shader-disk-cache]") != std::string::npos, "shader cache diagnostic did not reach host stderr"); +} +#endif + void removeCacheDirectory(const std::filesystem::path& directory) { std::error_code error; #ifdef _WIN32 @@ -2378,6 +2415,10 @@ int main(int argc, char** argv) { return 0; } #ifdef _WIN32 + if (argc == 2 && std::string_view(argv[1]) == "--host-stderr") { + verifyHostStandardError(); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--long-host-paths") { verifyLongHostPaths(); return 0; diff --git a/core/libs/tests/GuestHostStandardErrorWindows.cpp b/core/libs/tests/GuestHostStandardErrorWindows.cpp index 24a2eff0e8..f91c92d09e 100644 --- a/core/libs/tests/GuestHostStandardErrorWindows.cpp +++ b/core/libs/tests/GuestHostStandardErrorWindows.cpp @@ -39,6 +39,7 @@ static void Split(int standard) { Require(::_pipe(capture, 65536, _O_BINARY) == 0); const int original = ::_dup(standard); Require(original >= 0 && ::_dup2(capture[1], standard) == 0); + Require(std::fputs("host-before\n", standard == 1 ? stdout : stderr) >= 0); int guest[2]{-1, -1}; Require(pipe_nid_postfix(guest) == 0); @@ -49,6 +50,7 @@ static void Split(int standard) { host.back() = '\n'; Require(std::fputs(host.c_str(), stream) >= 0); Require(std::fflush(stream) == 0); + host.insert(0, "host-before\n"); Require(write_nid_postfix(standard, "guest", 5) == 5); char guestBytes[8]{}; diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 01ffcb201c..bb32aec198 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -168,6 +168,9 @@ function(add_shader_recompiler target runtimeTarget) ) target_compile_features(${target} PUBLIC cxx_std_20) + if(WIN32) + target_compile_options(${target} PRIVATE "SHELL:-include ${PROJECT_SOURCE_DIR}/core/libs/prx/libc/include/HostStandardStreams.hpp") + endif() if(ANYPS5_ENABLE_SPIRV_TOOLS) target_sources(${target} PRIVATE ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvOptimizer.cpp) From cd2fd50f0c8a56c669230905780c652137f0758f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 20 Oct 2026 14:02:05 +0200 Subject: [PATCH 154/167] fix(main): halve guest_raise_exception rounds to fit the 30 s budget on Windows #7652 made raises wait for host code to leave, adding ~3 s to a test that already took ~16 s on a fast machine. Main run 39414828108 timed out at 30 s on windows. Round counts halved; every scenario still runs. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestRaiseException.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index 1f882c248d..fd74e36771 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -41,7 +41,7 @@ static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003); static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003c); static constexpr int SIGUSR1 = 30; -static constexpr int Repeats = 100; +static constexpr int Repeats = 50; static constexpr std::size_t R12Offset = 0xa0; static constexpr std::size_t R13Offset = 0xa8; static constexpr std::size_t Xmm8Offset = 0x140 + 160 + 8 * 16; @@ -172,7 +172,7 @@ static void* APS5_VABI HostBlocked(void* arg) { return nullptr; } -static constexpr int LeavingRounds = 200; +static constexpr int LeavingRounds = 100; static std::atomic leavingRound{0}; static void* APS5_VABI Leaving(void* arg) { @@ -198,7 +198,7 @@ static LONG CALLBACK ContinueRaised(EXCEPTION_POINTERS* info) { return EXCEPTION_CONTINUE_EXECUTION; } -static constexpr int SelfingRounds = 100; +static constexpr int SelfingRounds = 50; static std::atomic selfSink{nullptr}; static void* APS5_VABI Selfing(void* arg) { @@ -239,7 +239,7 @@ static void* APS5_VABI Looping(void* arg) { return nullptr; } -static constexpr int VectorRounds = 100; +static constexpr int VectorRounds = 50; static constexpr std::size_t VectorBytes = 16 * 32; alignas(32) static std::uint8_t vectorPattern[VectorBytes]; alignas(32) static std::uint8_t vectorResult[VectorBytes]; @@ -405,7 +405,7 @@ static void NestedWaitKeepsOuterWake() { } #ifdef _WIN32 -static constexpr int AddressRounds = 100; +static constexpr int AddressRounds = 50; static std::uint32_t addressWord = 0; static std::uint32_t otherAddressWord = 0; static std::atomic holdingEntered{false}; From e0230e6a75444ed3a3b9d49c5b817fdd5859cd5f Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Tue, 20 Oct 2026 01:59:49 +0300 Subject: [PATCH 155/167] 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 156/167] 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:22:35 +0200 Subject: [PATCH 157/167] test(libkernel): expect ENOENT for a missing rename source with a NULL destination The rename source is now resolved before the destination argument, as in FreeBSD kern_renameat, so sceKernelRename("src", nullptr) fails with ENOENT when src does not exist. GuestPosixScalarIo still expected EFAULT. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestPosixScalarIo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/GuestPosixScalarIo.cpp b/core/libs/tests/GuestPosixScalarIo.cpp index 90cafe8ea9..ec96da483a 100644 --- a/core/libs/tests/GuestPosixScalarIo.cpp +++ b/core/libs/tests/GuestPosixScalarIo.cpp @@ -169,7 +169,7 @@ int main() { Require(sceKernelMkdir(nullptr, 0777) == static_cast(0x8002000eu)); Require(sceKernelRmdir(nullptr) == static_cast(0x8002000eu)); Require(sceKernelRename(nullptr, "dest") == static_cast(0x8002000eu)); - Require(sceKernelRename("src", nullptr) == static_cast(0x8002000eu)); + Require(sceKernelRename("src", nullptr) == static_cast(0x80020002u)); Require(sceKernelRename(nullptr, nullptr) == static_cast(0x8002000eu)); Require(close_nid_postfix(descriptor) == 0); const int readOnly = open_nid_postfix(fileName.c_str(), SCE_KERNEL_O_RDONLY, 0); From fb050223ddd2a51fb36d6e2fac16f45287d655f9 Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Tue, 20 Oct 2026 15:26:15 +0300 Subject: [PATCH 158/167] 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 1ea49c741f9b0f3e3f123036792a26952ef204ca Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Tue, 20 Oct 2026 15:48:42 +0300 Subject: [PATCH 159/167] 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 160/167] 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: Thu, 15 Oct 2026 18:00:50 +0200 Subject: [PATCH 161/167] feat(agc): run registered merged hull programs (binary type 3) A registered type-3 hull program holds the local and the hull part of a tessellation draw in one binary, addressed through SPI_SHADER_PGM_LO_LS (SH 0x148) with no SPI_SHADER_PGM_LO_HS: the local part runs on the lanes below s3[7:0] and writes LDS, an s_barrier follows, and the hull part runs on the lanes below s3[15:8] and may store the registers the local part filled. The draw decode asked 0x148 for a type-5 local half, and PPSA11386 (007 First Light) stopped in its level load with "registered program refers to an incompatible shader binary type". The draw decode now gives a registered type-3 program both the local and the hull role and marks the tessellation configuration mergedHull. The local stage only passes gl_VertexIndex and gl_InstanceIndex in a 16-byte record. The hull stage runs the whole program, one invocation per output control point: v2 and v5 come from the record of control point min(gl_InvocationID, inputs - 1), v3 is the relative vertex id, and the LDS becomes a per-control-point output array that a control point writes in its own record and reads in any record of its patch after the barrier. Registers live across the barrier stay in the invocation. The hull stage of a merged program runs at one lane per invocation on every device (HostSubgroupSize 1). Its lanes are gl_InvocationID, not host subgroup invocations, so the s_cbranch_execz around the local part tests the invocation's own lane. With the device's subgroup it took a subgroup ballot, and lavapipe, whose subgroup stages lack the tessellation control stage, rejected the draw ("subgroup capability 61 is unsupported for shader stage 2"). The lowering refuses more input than output control points, LDS writes outside the own record, reads outside the patch's records, barriers that some invocations do not reach or that sit in a loop, reads or writes of another lane's register, outputs beyond the device limits, and fetch subroutines. Type 3 is prepared with its stage group like type 5 and takes its float mode from RSRC1_HS. The hull user-data address (SH 0x102/0x103) is optional for a merged program: s[0:1] take it, unchecked, when the draw sets it and are zero otherwise. Both stages refer to one binary, so a draw lists each code and header region once (AppendProgramMemory); a request's memory regions must not overlap. An indirect draw's record words go into every program whose user data holds the register, so the hull stage of a merged program reads them too. mergedHull is part of the recompile cache key, the draw decode comparison and the request serialization (version 39). Co-Authored-By: Claude Opus 5.5 --- .../include/Driver/Draw/DrawCache.hpp | 4 + .../Execution/src/Driver/Draw/Draw.cpp | 35 ++-- .../Execution/src/Driver/Draw/DrawDecode.cpp | 43 +++- .../Execution/src/Driver/Draw/DrawLookup.cpp | 1 + .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../src/Driver/Shaders/ShaderRegistry.cpp | 6 +- .../libSceAgcDriver/tests/ShaderMemory.cpp | 42 +++- .../tests/execution/PreparedGraphics.cpp | 194 ++++++++++++++++++ .../tests/execution/Tessellation.cpp | 120 +++++++++++ core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 6 +- .../IrMetadata/ProgramMetadata.hpp | 1 + .../IrMetadata/StageIO.hpp | 3 +- .../Optimization/ShaderStageInputInfo.hpp | 1 + .../Optimization/TessellationLowering.hpp | 3 +- .../Optimization/src/ShaderInfoCollector.cpp | 8 +- .../Optimization/src/TessellationLowering.cpp | 97 ++++++++- core/shader/recompiler/Recompiler.cpp | 28 ++- core/shader/recompiler/Recompiler.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 2 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 18 +- .../Translation/src/InstructionTranslator.cpp | 19 ++ .../src/ShaderInputInfoBuilder.cpp | 1 + .../tests/RequestSerializerTests.cpp | 37 ++-- docs/dev/debt/shader.md | 1 + 25 files changed, 605 insertions(+), 69 deletions(-) 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..0ad9ba5903 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include "prx/libc/include/general/AtomicSharedPtr.hpp" @@ -45,6 +46,9 @@ void ReadUserWords(const QueueState& queue, DrawProgram& program, bool staticAbi void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32_t pointerBase, bool pointerRequired, bool staticAbi); void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const ShaderRegistry& registry, bool staticAbi, bool includeFragment); +void AppendProgramMemory(std::vector& memory, const std::vector& programs); +std::vector> UserDataWords(const std::vector& programs, const std::vector& roles, std::uint32_t location); + struct PreparedGraphicsStage { std::shared_ptr snapshot; PreparedShaders::Entry entry; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 1dcbe329eb..acc9d7ea26 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -216,13 +216,9 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe phaseTiming.Phase(DrawRowDecode); const auto locate = [&](std::uint32_t location) -> std::optional> { - if (location == 0x280u) return std::nullopt; - for (std::size_t i = 0; i < programs.size(); ++i) { - if (roles[i] == Role::Fragment || roles[i] == Role::GeometryBack || location < programs[i].userDataBase) continue; - const auto word = location - programs[i].userDataBase + (8u - programs[i].firstUserSgpr); - if (word < programs[i].userData.size()) return std::make_pair(i, static_cast(word)); - } - return std::nullopt; + const auto words = UserDataWords(programs, roles, location); + if (words.empty()) return std::nullopt; + return words.front(); }; if (drawParameters.indirect) { auto& indirect = *drawParameters.indirect; @@ -237,13 +233,11 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe } std::vector memory; memory.reserve(2 * programs.size() + 32); + AppendProgramMemory(memory, programs); std::vector linked; linked.reserve(programs.size()); for (std::size_t i = 0; i < programs.size(); ++i) { const auto& program = programs[i]; - for (const auto& region : program.memory) { - if (!region.bytes.empty()) memory.push_back(region); - } linked.push_back({roles[i], program.binary, program.userDataBase, program.firstUserSgpr, program.userData}); } timing.Mark("prepare"); @@ -544,9 +538,9 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe std::vector records; for (std::uint32_t record = 0; record < count; ++record) records.push_back(Pm4::ReadDrawArguments(indirect, record)); Graphics::CountIndirectDraw(*indirectCpu, std::chrono::duration(std::chrono::steady_clock::now() - readStart).count()); - const auto baseVertexWord = locate(indirect.baseVertexLocation); - const auto startInstanceWord = locate(indirect.startInstanceLocation); - const auto drawIndexWord = indirect.drawIndexEnabled ? locate(indirect.drawIndexLocation) : std::nullopt; + const auto baseVertexWords = UserDataWords(programs, roles, indirect.baseVertexLocation); + const auto startInstanceWords = UserDataWords(programs, roles, indirect.startInstanceLocation); + const auto drawIndexWords = indirect.drawIndexEnabled ? UserDataWords(programs, roles, indirect.drawIndexLocation) : std::vector>{}; const auto drawable = [&](const Pm4::DrawArguments& arguments) { return arguments.count != 0 && arguments.instances != 0 && (!drawParameters.indexed || arguments.firstVertexOrIndex < drawParameters.indexCount); }; std::size_t lastDrawable = records.size(); for (std::size_t record = 0; record < records.size(); ++record) { @@ -557,14 +551,15 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe if (traceIndirect) std::fprintf(stderr, "[draw] record %u: count %u instances %u first %u vertexOffset %u startInstance %u\n", record, arguments.count, arguments.instances, arguments.firstVertexOrIndex, arguments.vertexOffset, arguments.firstInstance); if (arguments.count == 0 || arguments.instances == 0) continue; std::set patched; - const auto patch = [&](const std::optional>& word, std::uint32_t value) { - if (!word) return; - writablePrograms[word->first].userData[word->second] = value; - patched.insert(word->first); + const auto patch = [&](const std::vector>& words, std::uint32_t value) { + for (const auto& [program, word] : words) { + writablePrograms[program].userData[word] = value; + patched.insert(program); + } }; - patch(baseVertexWord, indirect.recordBytes == 20 ? arguments.vertexOffset : arguments.firstVertexOrIndex); - patch(startInstanceWord, arguments.firstInstance); - patch(drawIndexWord, record); + patch(baseVertexWords, indirect.recordBytes == 20 ? arguments.vertexOffset : arguments.firstVertexOrIndex); + patch(startInstanceWords, arguments.firstInstance); + patch(drawIndexWords, record); if (graphics.stages.tessellation) { for (std::size_t i = 0; i < programs.size(); ++i) { if (programs[i].binary.stage == Stage::TessellationControl || programs[i].binary.stage == Stage::TessellationEvaluation) patched.insert(i); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index c9bc7f26bb..f17c3ce9df 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -46,11 +46,34 @@ void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32 const auto address = static_cast(low) | (static_cast(high) << 32u); require(address != 0 || !pointerRequired, "merged shader user-data address is null"); if (address == 0) return; - GuestMemory::CheckRange(reinterpret_cast(address), 8, 4); + if (pointerRequired) GuestMemory::CheckRange(reinterpret_cast(address), 8, 4); program.userData[0] = low; program.userData[1] = high; } +void AppendProgramMemory(std::vector& memory, const std::vector& programs) { + for (const auto& program : programs) { + for (const auto& region : program.memory) { + if (region.bytes.empty()) continue; + const bool listed = std::any_of(memory.begin(), memory.end(), [&](const ShaderRecompiler::MemoryRegion& known) { return known.guestAddress == region.guestAddress && known.bytes.data() == region.bytes.data() && known.bytes.size() == region.bytes.size(); }); + if (!listed) memory.push_back(region); + } + } +} + +std::vector> UserDataWords(const std::vector& programs, const std::vector& roles, std::uint32_t location) { + using Role = ShaderRecompiler::ProgramRole; + require(roles.size() == programs.size(), "draw program roles are incomplete"); + std::vector> words; + if (location == 0x280u) return words; + for (std::size_t i = 0; i < programs.size(); ++i) { + if (roles[i] == Role::Fragment || roles[i] == Role::GeometryBack || location < programs[i].userDataBase) continue; + const auto word = location - programs[i].userDataBase + (8u - programs[i].firstUserSgpr); + if (word < programs[i].userData.size()) words.emplace_back(i, static_cast(word)); + } + return words; +} + void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const ShaderRegistry& registry, bool staticAbi, bool includeFragment) { using Stage = ShaderRecompiler::ShaderStage; using Role = ShaderRecompiler::ProgramRole; @@ -95,11 +118,21 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const programs.push_back(prepare(programAddress(base), type, stage, resources, users)); roles.push_back(role); }; - const auto& graphics = product->state; + auto& graphics = product->state; if (graphics.stages.path == Graphics::ShaderPath::Tessellation) { - if (!staticAbi || programAddress(0x148) != 0) append(0x148, 5, Stage::Local, 0x10b, 0x10c, Role::Local); - append(0x108, 7, Stage::TessellationControl, 0x10b, 0x10c, Role::Hull); - InitializeMerged(queue, programs.back(), 0x102, true, staticAbi); + const auto localAddress = programAddress(0x148); + const auto local = !staticAbi || localAddress != 0 ? ProgramSnapshot(registry, localAddress, {3, 5}) : nullptr; + const bool mergedHull = local != nullptr && !local->header.empty() && local->type == 3; + require(graphics.stages.tessellation.has_value(), "tessellation draw has no tessellation configuration"); + graphics.stages.tessellation->mergedHull = mergedHull; + if (mergedHull) { + append(0x148, 3, Stage::Local, 0x10b, 0x10c, Role::Local); + append(0x148, 3, Stage::TessellationControl, 0x10b, 0x10c, Role::Hull); + } else { + if (local != nullptr) append(0x148, 5, Stage::Local, 0x10b, 0x10c, Role::Local); + append(0x108, 7, Stage::TessellationControl, 0x10b, 0x10c, Role::Hull); + } + InitializeMerged(queue, programs.back(), 0x102, !mergedHull, staticAbi); append(0x0c8, 2, Stage::TessellationEvaluation, 0x08b, 0x08c, Role::Domain); } else if (graphics.stages.path == Graphics::ShaderPath::Geometry) { const auto frontAddress = programAddress(0xc8); 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..b0ae591522 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -28,6 +28,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(tess.breakAtInstance)}) mix(value); + mix(tess.mergedHull); } mix(graphics.rectList); mix(programs.size()); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 7a4ba31d64..129e751733 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -241,7 +241,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { const auto sameTess = [](const std::optional& x, const std::optional& y) { if (x.has_value() != y.has_value()) return false; if (!x) return true; - return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology && x->patchesPerGroup == y->patchesPerGroup && x->primitiveGroupSize == y->primitiveGroupSize && x->breakAtInstance == y->breakAtInstance; + return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology && x->patchesPerGroup == y->patchesPerGroup && x->primitiveGroupSize == y->primitiveGroupSize && x->breakAtInstance == y->breakAtInstance && x->mergedHull == y->mergedHull; }; if (s.stages.path != t.stages.path || s.stages.registerValue != t.stages.registerValue || s.stages.vertexWaveSize != t.stages.vertexWaveSize || s.stages.fragmentWaveSize != t.stages.fragmentWaveSize || !sameMesh(s.stages.mesh, t.stages.mesh) || !sameTess(s.stages.tessellation, t.stages.tessellation)) return false; if (!sameColor(s.color, t.color) || s.colors.size() != t.colors.size() || s.blends.size() != t.blends.size()) return false; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index f8214382bf..1d6a215d55 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -803,7 +803,7 @@ std::optional DecodeFloatMode(const Registers case 0: rsrc1 = 0x212; fp16OverflowBit = 26; break; case 1: rsrc1 = 0x00a; fp16OverflowBit = 29; break; case 2: case 4: case 6: rsrc1 = 0x08a; fp16OverflowBit = 31; break; - case 5: case 7: rsrc1 = 0x10a; fp16OverflowBit = 30; break; + case 3: case 5: case 7: rsrc1 = 0x10a; fp16OverflowBit = 30; break; default: return std::nullopt; } const auto found = registers.find(rsrc1); @@ -901,7 +901,7 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap case 1: stage = Stage::Fragment; programRegister = 0x008; resourceRegister = 0x00b; break; case 2: stage = Stage::Vertex; programRegister = 0x0c8; resourceRegister = 0x08b; firstUser = 8; break; case 4: stage = Stage::Mesh; programRegister = 0x0c8; resourceRegister = 0x08b; break; - case 5: return {}; + case 3: case 5: return {}; case 6: stage = Stage::Mesh; programRegister = 0x088; resourceRegister = 0x08b; break; case 7: stage = Stage::TessellationControl; programRegister = 0x108; resourceRegister = 0x10b; break; default: @@ -1350,13 +1350,13 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod require(expected == decoded.roles, "graphics ABI linked programs do not match the stage routing"); std::vector linked; std::vector memory; + AppendProgramMemory(memory, decoded.programs); for (std::size_t index = 0; index < decoded.programs.size(); ++index) { const auto& program = decoded.programs[index]; require(program.snapshot != nullptr && program.codeOffset < program.snapshot->code.size(), "graphics ABI has an invalid registered entry point"); const auto code = std::span(program.snapshot->code).subspan(program.codeOffset); require(program.binary.code.data() == code.data() && program.binary.code.size() == code.size() && program.binary.codeAddress == program.snapshot->codeAddress + program.codeOffset * sizeof(std::uint32_t), "graphics ABI entry point differs from its snapshot"); linked.push_back({decoded.roles[index], program.binary, program.userDataBase, program.firstUserSgpr, program.userData}); - memory.insert(memory.end(), program.memory.begin(), program.memory.end()); } std::vector prepared; std::uint32_t pushOffset = 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index eef662d3fd..99fa863086 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4839,7 +4839,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQSYAAAA=", "new requests did not use serialization version 38"); + require(requestPrefix(encoded, 8u) == "NVNQQScAAAA=", "new requests did not use serialization version 39"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -4957,7 +4957,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=", "NVNQQScAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSgAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -5243,6 +5243,43 @@ void verifyDepthRemapRequests() { require(key != other, "the depth remap mode did not change the shader cache key"); } +void verifyMergedHullConfiguration() { + using namespace ShaderRecompiler; + static constexpr std::array code{0xbf810000u}; + const RequestSerializer serializer; + RecompileRequest request{}; + request.shader = {ShaderStage::TessellationControl, 0x10000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.vertex = ShaderVertexStageInfo{}; + request.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{3u, 3u, 1u, 2u, 2u}, {}}; + const auto separate = serializer.Serialize(request); + std::vector separateKey; + RecompileCacheKey::Build(request, separateKey); + request.graphics->tessellation->mergedHull = true; + const auto merged = serializer.Serialize(request); + std::vector mergedKey; + RecompileCacheKey::Build(request, mergedKey); + const auto replay = serializer.Deserialize(merged); + 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); + 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}) { + auto older = legacy; + older[4] = static_cast(version); + if (version < 38u) older.erase(older.end() - 31); + if (version < 36u) older.pop_back(); + if (version < 35u) older.resize(older.size() - 5u); + if (version < 32u) older.pop_back(); + const auto upgraded = serializer.Deserialize(encodeBase64(older)); + const auto what = "a main version " + std::to_string(version) + " tessellation request"; + require(upgraded.request.graphics.has_value() && upgraded.request.graphics->tessellation.has_value() && !upgraded.request.graphics->tessellation->mergedHull, (what + " read a merged hull flag").c_str()); + require(serializer.Serialize(upgraded.request) == separate, (what + " misread the fields after its tessellation configuration").c_str()); + } + require(mergedKey != separateKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(serializer.Deserialize(separate).request), "the cache keys ignore whether the hull program is merged"); +} + void verifyLegacyPixelRequests() { using namespace ShaderRecompiler; static constexpr std::array legacyPixelRequests{ @@ -7412,6 +7449,7 @@ int main(int argc, char** argv) { verifyPixelRequestSerialization(); verifyVertexInfoVersion19(); verifyDepthRemapRequests(); + verifyMergedHullConfiguration(); verifyPositionExportComponents(); verifyLegacyPixelRequests(); verifyPixelExportReplay(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp index 1242c30e33..56ea345d1f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp @@ -7,6 +7,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "Optimization/ResourceProgram.hpp" #include "CompiledVariant.hpp" +#include "Optimization/RequestMemoryView.hpp" #include #include #include @@ -481,6 +482,197 @@ void RetainedPixelMatchesNullProgram(AgcDriver::VulkanDevice& device) { } } +void CheckMergedHull(AgcDriver::VulkanDevice& device) { + using namespace AgcDriver::DriverDetail; + using namespace ShaderRecompiler; + Fixture merged; + Fixture domain; + Fixture fragment; + merged.Initialize(3u); + domain.Initialize(2u); + fragment.Initialize(1u); + ShaderRegistry registry; + for (const auto* fixture : {&merged, &domain, &fragment}) registry.emplace(fixture->snapshot->codeAddress, std::vector>{fixture->snapshot}); + AgcDriver::QueueState queue{}; + queue.context[0x8e] = 0xfu; + queue.context[0x8f] = 0xfu; + merged.Bind(queue, 0x148u, 0x10bu); + domain.Bind(queue, 0xc8u, 0x8bu); + fragment.Bind(queue, 0x8u, 0xbu); + const auto unset = queue; + const auto null = [&] { + auto state = queue; + state.shader[0x102u] = 0u; + state.shader[0x103u] = 0u; + return state; + }(); + alignas(8) const std::array pointer{}; + const auto pointerAddress = reinterpret_cast(pointer.data()); + queue.shader[0x102u] = static_cast(pointerAddress); + queue.shader[0x103u] = static_cast(pointerAddress >> 32u); + DrawDecode prepared{}; + prepared.state.stages.path = AgcDriver::Graphics::ShaderPath::Tessellation; + prepared.state.stages.vertexWaveSize = 64u; + prepared.state.stages.fragmentWaveSize = 32u; + prepared.state.stages.tessellation = TessellationConfiguration{3, 4, 1, 2, 2}; + prepared.pixel.wave32 = true; + prepared.pixel.targetOutputMode[0] = 9; + prepared.pixel.targetExportMapping.fill(0xe4u); + DecodeGraphicsPrograms(prepared, queue, registry, true, true); + DrawDecode draw{}; + draw.state = prepared.state; + draw.state.stages.tessellation->mergedHull = false; + draw.pixel = prepared.pixel; + DecodeGraphicsPrograms(draw, queue, registry, false, true); + for (const auto* decoded : {&prepared, &draw}) { + Require(decoded->state.stages.tessellation->mergedHull, "a registered type-3 hull program was not decoded as a merged hull"); + Require(decoded->roles == std::vector{ProgramRole::Local, ProgramRole::Hull, ProgramRole::Domain, ProgramRole::Fragment}, "a merged hull program did not give the local and hull roles"); + const auto& local = decoded->programs[0]; + const auto& hull = decoded->programs[1]; + Require(local.snapshot == merged.snapshot && hull.snapshot == merged.snapshot && local.codeOffset == 64u && hull.codeOffset == 64u, "the local and hull stages of a merged hull program refer to another program"); + Require(local.binary.stage == ShaderStage::Local && hull.binary.stage == ShaderStage::TessellationControl && local.firstUserSgpr == 8u && hull.firstUserSgpr == 0u && hull.userData.size() == local.userData.size() + 8u, "a merged hull program's stages lost the merged user SGPR layout"); + } + Require(draw.programs[1].userData[0] == static_cast(pointerAddress) && draw.programs[1].userData[1] == static_cast(pointerAddress >> 32u), "a merged hull draw did not pass its user-data address"); + Require(UserDataWords(draw.programs, draw.roles, 0x10eu) == std::vector>{{0u, 2u}, {1u, 10u}}, "an indirect draw's user-data register does not reach both stages of a merged hull program"); + std::vector programMemory; + AppendProgramMemory(programMemory, draw.programs); + Require(programMemory.size() == 6u, "a merged hull draw's program memory does not list each code and header region once"); + try { + const RequestMemoryView view(programMemory); + } catch (const std::exception& error) { + throw std::runtime_error(std::string("a merged hull draw's program memory is not a valid request memory: ") + error.what()); + } + for (const auto* state : {&unset, &null}) { + DrawDecode without{}; + without.state = prepared.state; + without.pixel = prepared.pixel; + DecodeGraphicsPrograms(without, *state, registry, false, true); + Require(without.roles == draw.roles && without.programs[1].userData.size() == draw.programs[1].userData.size() && without.programs[1].userData[0] == 0u && without.programs[1].userData[1] == 0u, "a merged hull draw without a user-data address was not decoded with zero s[0:1]"); + } + auto unmapped = queue; + unmapped.shader[0x102u] = 0x10u; + unmapped.shader[0x103u] = 0u; + DrawDecode stale{}; + stale.state = prepared.state; + stale.pixel = prepared.pixel; + DecodeGraphicsPrograms(stale, unmapped, registry, false, true); + Require(stale.programs[1].userData[0] == 0x10u && stale.programs[1].userData[1] == 0u, "a merged hull draw did not pass a user-data address it does not need unchecked"); + const auto stages = PrepareGraphicsStages(prepared, device.Target()); + Require(stages.size() == 4u && stages[0].entry.handle != stages[1].entry.handle, "a merged hull program was not prepared as separate local and hull stages"); + for (const auto& stage : stages) stage.snapshot->prepared->entries.push_back(stage.entry); + std::vector linked; + std::vector memory; + AppendProgramMemory(memory, draw.programs); + for (std::size_t index = 0; index < draw.programs.size(); ++index) { + const auto& program = draw.programs[index]; + linked.push_back({draw.roles[index], program.binary, program.userDataBase, program.firstUserSgpr, program.userData}); + } + const auto predicted = merged.snapshot->prepared->entries.size(); + for (std::size_t index = 0; index < 2u; ++index) { + const auto& program = draw.programs[index]; + const auto vertex = AgcDriver::Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true); + RecompileRequest request{program.binary, {64u, program.firstUserSgpr, program.userData, {}, std::nullopt, vertex, memory}, device.Target(), {0, 0, 0, 128u}, GraphicsCompileContext{program.firstUserSgpr, linked, std::nullopt, draw.state.stages.tessellation, {0, 3, 4, 1}}}; + static_cast(InvocationFor(*program.snapshot, program.codeOffset, request)); + Require(merged.snapshot->prepared->entries.size() == predicted, "a merged hull stage prepared at ABI resolution was prepared again at its draw"); + request.graphics->tessellation->mergedHull = false; + Require(!MatchesPreparedShader(request, *stages[index].entry.handle), "a separate hull configuration matched a merged hull artifact"); + } + Fixture front; + front.Initialize(5u); + registry.emplace(front.snapshot->codeAddress, std::vector>{front.snapshot}); + Fixture back; + back.Initialize(7u); + registry.emplace(back.snapshot->codeAddress, std::vector>{back.snapshot}); + auto separate = queue; + front.Bind(separate, 0x148u, 0x10bu); + back.Bind(separate, 0x108u, 0x10bu); + DrawDecode halves{}; + halves.state = prepared.state; + halves.pixel = prepared.pixel; + DecodeGraphicsPrograms(halves, separate, registry, false, true); + Require(!halves.state.stages.tessellation->mergedHull && halves.programs[0].snapshot == front.snapshot && halves.programs[1].snapshot == back.snapshot, "separate hull halves were decoded as a merged hull program"); + auto halvesUnset = unset; + front.Bind(halvesUnset, 0x148u, 0x10bu); + back.Bind(halvesUnset, 0x108u, 0x10bu); + DrawDecode halvesWithout{}; + halvesWithout.state = prepared.state; + halvesWithout.pixel = prepared.pixel; + Reject([&] { DecodeGraphicsPrograms(halvesWithout, halvesUnset, registry, false, true); }, "merged shader user-data address is null"); +} + +void CheckMergedHullVertexTables(AgcDriver::VulkanDevice& device) { + using namespace AgcDriver::DriverDetail; + using namespace ShaderRecompiler; + struct Header { + Shader shader{}; + ShaderUserData users{}; + std::array direct{}; + ShaderSemantic semantic{}; + } header; + alignas(256) std::array code{}; + code.fill(0xffffffffu); + code.back() = 0xbf810000u; + header.shader.type = 3; + header.shader.user_data = &header.users; + header.direct.fill(0xffffu); + header.direct[8] = 10u; + header.direct[10] = 12u; + header.users.direct_resource_offset = header.direct.data(); + header.users.direct_resource_count = static_cast(header.direct.size()); + header.semantic.size_in_elements = 4u; + header.shader.input_semantics = &header.semantic; + header.shader.num_input_semantics = 1u; + auto merged = std::make_shared(); + merged->type = 3; + merged->codeAddress = reinterpret_cast(code.data()); + merged->headerAddress = reinterpret_cast(&header); + merged->code.assign(code.begin(), code.end()); + merged->header.resize(sizeof(header)); + std::memcpy(merged->header.data(), &header, sizeof(header)); + Fixture domain; + Fixture fragment; + domain.Initialize(2u); + fragment.Initialize(1u); + ShaderRegistry registry; + registry.emplace(merged->codeAddress, std::vector>{merged}); + for (const auto* fixture : {&domain, &fragment}) registry.emplace(fixture->snapshot->codeAddress, std::vector>{fixture->snapshot}); + AgcDriver::QueueState queue{}; + queue.context[0x8e] = 0xfu; + queue.context[0x8f] = 0xfu; + const auto entry = merged->codeAddress + 256u; + queue.shader[0x148u] = static_cast(entry >> 8u); + queue.shader[0x149u] = static_cast(entry >> 40u); + queue.shader[0x10bu] = 16u << 1u; + domain.Bind(queue, 0xc8u, 0x8bu); + fragment.Bind(queue, 0x8u, 0xbu); + alignas(16) const std::array bufferTable{}; + alignas(16) const std::array attributeTable{}; + const auto bufferAddress = reinterpret_cast(bufferTable.data()); + const auto attributeAddress = reinterpret_cast(attributeTable.data()); + for (std::uint32_t slot = 2u; slot < 6u; ++slot) queue.shader[0x10cu + slot] = slot % 2u == 0u ? 0x12345600u : 0x00000077u; + queue.shader[0x10cu + 10u] = static_cast(bufferAddress); + queue.shader[0x10cu + 11u] = static_cast(bufferAddress >> 32u); + queue.shader[0x10cu + 12u] = static_cast(attributeAddress); + queue.shader[0x10cu + 13u] = static_cast(attributeAddress >> 32u); + DrawDecode draw{}; + draw.state.stages.path = AgcDriver::Graphics::ShaderPath::Tessellation; + draw.state.stages.vertexWaveSize = 64u; + draw.state.stages.fragmentWaveSize = 32u; + draw.state.stages.tessellation = TessellationConfiguration{3, 3, 1, 2, 2}; + draw.pixel.wave32 = true; + draw.pixel.targetOutputMode[0] = 9; + draw.pixel.targetExportMapping.fill(0xe4u); + DecodeGraphicsPrograms(draw, queue, registry, false, true); + Require(draw.state.stages.tessellation->mergedHull && draw.programs[1].firstUserSgpr == 0u, "the merged hull draw with vertex tables was not decoded as a merged hull"); + for (std::size_t index = 0; index < 2u; ++index) { + const auto& program = draw.programs[index]; + std::vector reads; + const auto vertex = AgcDriver::Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &reads, false, 8u - program.firstUserSgpr); + Require(vertex.fetchEmbedded && vertex.fetchAttribReg == 12u && vertex.fetchBufferReg == 10u && vertex.resourcesNum == 1u, "a merged hull stage did not decode its vertex tables"); + Require(reads.size() == 2u && reads[0].address == attributeAddress && reads[1].address == bufferAddress, "a merged hull stage read its vertex tables from user-data words other than its slots"); + } +} + void NullPixelMatchesRegistration(AgcDriver::VulkanDevice& device) { using namespace AgcDriver::DriverDetail; Fixture front; @@ -541,6 +733,8 @@ int main(int argc, char** argv) { Check(*device, AgcDriver::Graphics::ShaderPath::Vertex, dump); Check(*device, AgcDriver::Graphics::ShaderPath::Geometry, dump); Check(*device, AgcDriver::Graphics::ShaderPath::Tessellation, dump); + CheckMergedHull(*device); + CheckMergedHullVertexTables(*device); std::cout << "prepared graphics ABI tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index 9ed2c5c32c..e3725caa8e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -326,6 +326,90 @@ alignas(256) constexpr std::array QuadDomainCode{ 0x7e3402f2, 0xf80008cf, 0x1a191716, 0xf800020f, 0x1a19181a, 0xbf810000, }; +alignas(256) constexpr std::array MergedHullCode{ + 0xbefe04c1, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x80060000, 0xbf88001c, + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefe04c1, 0xbf8cc07f, 0xd7650000, 0x000100c1, + 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, + 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, 0x162c2890, + 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, 0xbe94047e, + 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80048, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, + 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, + 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array MergedHullRegisters{ + 0xbefe04c1, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x80060000, 0xbf88001c, + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefe04c1, 0xbf8cc07f, 0xd7650000, 0x000100c1, + 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, + 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, 0x162c2890, + 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0741000, 0x02020816, 0x7da82881, 0xbe94047e, + 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80048, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, + 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, + 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array MergedHullForeignRecord{ + 0xbefe04c1, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x80060000, 0xbf88001c, + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340020, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefe04c1, 0xbf8cc07f, 0xd7650000, 0x000100c1, + 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, + 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, 0x162c2890, + 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, 0xbe94047e, + 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80048, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, + 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, + 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array MergedHullDivergentBarrier{ + 0xbefe04c1, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x80060000, 0xbf88001d, + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8a0000, 0xbf8cc07f, 0xbefe04c1, 0xbf8cc07f, 0xd7650000, + 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, + 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, + 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, + 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80048, 0x06000013, 0xd8dc1008, 0x07000013, + 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, + 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array MergedHullOutsidePatch{ + 0xbefe04c1, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x80060000, 0xbf88001c, + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefe04c1, 0xbf8cc07f, 0xd7650000, 0x000100c1, + 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, + 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, 0x162c2890, + 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, 0xbe94047e, + 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80060, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, + 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, + 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array MergedHullReadLane{ + 0xbefe04c1, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x80060000, 0xbf88001c, + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefe04c1, 0xbf8cc07f, 0xd7650000, 0x000100c1, + 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, + 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, 0x162c2890, + 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, 0xbe94047e, + 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80048, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, + 0xd7600010, 0x02010106, 0x7e300210, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, + 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + alignas(256) constexpr std::array PixelCode{ 0xc8020002, 0xc8060102, 0xc80a0202, 0xc80e0302, 0xf800180f, 0x03020100, 0xbf810000, }; @@ -333,6 +417,7 @@ alignas(256) constexpr std::array PixelCode{ using ShaderRecompiler::TessellationConfiguration; constexpr TessellationConfiguration Patches{3u, 3u, 1u, 2u, 2u}; +constexpr TessellationConfiguration MergedPatches{3u, 3u, 1u, 2u, 2u, 256u, 0u, false, 0u, false, true}; constexpr std::array PatchLevels{1u, 0u, 5u}; constexpr std::uint16_t Corner(std::uint32_t patch, std::uint32_t x, std::uint32_t y) { @@ -379,6 +464,7 @@ alignas(256) constexpr std::array NextInputIndices{ const PatchDraw Triangles{Patches, Indices}; const PatchDraw NextInputTriangles{{4u, 3u, 1u, 2u, 2u}, NextInputIndices}; +const PatchDraw MergedTriangles{MergedPatches, Indices}; struct Stages { ShaderRecompiler::RecompileResult local; @@ -842,6 +928,31 @@ void RequireSingleControlPointHulls(const ShaderRecompiler::SpirvTarget& target) Require(!CompileHull(target, HullSingleOutput, {1u, 1u, 1u, 2u, 2u}).spirv.empty(), "a hull with one output control point and unguarded patch outputs was not compiled"); } +void RequireMergedHullWithoutSubgroups(const ShaderRecompiler::SpirvTarget& target) { + std::uint32_t cursor = 0; + const auto hull = Compile(target, ShaderStage::TessellationControl, MergedHullCode, cursor, MergedTriangles); + const auto words = hull.spirv.Words(); + for (std::size_t at = 5; at + 1u < words.size(); at += words[at] >> 16u) { + Require((words[at] >> 16u) != 0u, "the merged hull program compiled to a SPIR-V instruction without words"); + Require((words[at] & 0xffffu) != spv::OpCapability || words[at + 1u] != spv::CapabilityGroupNonUniform, "the hull stage of a merged hull program declares subgroup operations, which a device without tessellation control subgroups rejects"); + } +} + +void RequireRegisteredMergedStages() { + const auto low = [](const auto& code) { return static_cast(reinterpret_cast(code.data()) >> 8u); }; + const auto high = [](const auto& code) { return static_cast(reinterpret_cast(code.data()) >> 40u); }; + RegisteredStage merged; + RegisteredStage domain; + Describe(merged, MergedHullCode, 3u, {{0x148u, low(MergedHullCode)}, {0x149u, high(MergedHullCode)}, {0x10au, 0u}, {0x10bu, 16u}}, {{0x2dbu, 0x40049u}, {0x286u, 0x42800000u}, {0x287u, 0x3f800000u}, {0x2d6u, 0xc355u}}, 0x105u); + Describe(domain, DomainCode, 2u, {{0xc8u, low(DomainCode)}, {0xc9u, high(DomainCode)}, {0x8au, 0u}, {0x8bu, 16u}}, {}, 0x02002008u); + for (const auto* stage : {&merged, &domain}) AgcDriverRegisterShader_nid_postfix(&stage->shader); + const std::array stages{&merged.shader, &domain.shader}; + const std::array context{{{0x2d5u, 0x0200210du}, {0x29bu, 0u}}}; + const std::array primitive{{{0x25bu, 0u}, {0x262u, 0u}, {0x242u, 9u}}}; + AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, context, primitive); + AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, context, primitive); +} + } int main() { @@ -877,6 +988,14 @@ int main() { 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}); + ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullCode, "more input than output control points", {{4u, 3u, 1u, 2u, 2u, 256u, 0u, false, 0u, false, true}, Indices}); + ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullForeignRecord, "outside its own control point's record", MergedTriangles); + ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullDivergentBarrier, "s_barrier that some invocations do not reach", MergedTriangles); + ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullOutsidePatch, "reads LDS outside its patch's control point records", MergedTriangles); + ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullReadLane, "another lane's register", MergedTriangles); + RequireMergedHullWithoutSubgroups(target); + RequirePatches(*device, target, MergedHullCode, "merged hull program", MergedTriangles, MergedHullCode); + RequirePatches(*device, target, MergedHullRegisters, "merged hull program storing its local part's registers", MergedTriangles, MergedHullRegisters); RequireCounterClockwise(*device, target); RequireLevelClamps(*device, target); RequireIntegerPartitioning(*device, target); @@ -888,6 +1007,7 @@ int main() { RequireHullBdaReads(*device, target); RequireCompareBranches(target); device.reset(); + RequireRegisteredMergedStages(); RequireRegisteredStages(); std::puts("Tessellation tests passed"); return 0; diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index bc9219115d..19711b8ddc 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -131,6 +131,7 @@ private: append(key, tessellation != nullptr); if (tessellation == nullptr) return; for (const auto value : {tessellation->inputControlPoints, tessellation->outputControlPoints, tessellation->domain, tessellation->partitioning, tessellation->outputTopology}) append(key, value); + if (request.shader.stage != ShaderStage::TessellationEvaluation) append(key, tessellation->mergedHull); if (request.shader.stage == ShaderStage::Local) return; append(key, tessellation->patchesPerGroup); append(key, tessellation->primitiveGroupSize); diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 6ed339cbe5..76e0e11888 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -563,6 +563,7 @@ void writeTessellationConfiguration(Writer& writer, const TessellationConfigurat writer.WriteU32(configuration.patchesPerGroup); writer.WriteU32(configuration.primitiveGroupSize); writer.WriteBool(configuration.breakAtInstance); + writer.WriteBool(configuration.mergedHull); } TessellationConfiguration readTessellationConfiguration(Reader& reader, std::uint32_t version) { @@ -577,6 +578,7 @@ TessellationConfiguration readTessellationConfiguration(Reader& reader, std::uin configuration.primitiveGroupSize = reader.ReadU32(); configuration.breakAtInstance = reader.ReadBool(); } + if (version >= 39u) configuration.mergedHull = reader.ReadBool(); return configuration; } @@ -825,7 +827,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(38u); + writer.WriteU32(39u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -864,7 +866,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 > 38u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 39u) 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); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp index 1ed7da10a3..ae85d35ec0 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp @@ -33,6 +33,7 @@ struct IrProgramMetadata { bool vectorWindowIndexedWrites = false; std::uint32_t tessellationControlStride = 0; std::uint32_t tessellationPatchStride = 0; + std::uint32_t tessellationSharedStride = 0; }; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp index 83690a5aaf..bfa9785b83 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp @@ -16,7 +16,8 @@ enum class TessellationAttribute { Factor, PatchInput, InstanceOutput, - InstanceInput + InstanceInput, + ControlShared }; enum class TessellationBaseKind { diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index e87567a2c1..f2a9ff9323 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -123,6 +123,7 @@ struct ShaderTessellationInputInfo { bool breakAtInstance = false; std::uint32_t instancePatches = 0; bool drawParameters = false; + bool mergedHull = false; }; struct ShaderVertexInputInfo { diff --git a/core/shader/recompiler/Optimization/include/Optimization/TessellationLowering.hpp b/core/shader/recompiler/Optimization/include/Optimization/TessellationLowering.hpp index 9c524f45e0..7032578c3b 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/TessellationLowering.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/TessellationLowering.hpp @@ -3,12 +3,13 @@ #include "IntermediateRepresentation/IrProgram.hpp" #include "Optimization/ShaderStageInputInfo.hpp" +#include "Recompiler.hpp" namespace ShaderRecompiler { class TessellationLowering { public: - void Lower(IrProgram& program, const ShaderTessellationInputInfo& tessellation) const; + void Lower(IrProgram& program, const ShaderTessellationInputInfo& tessellation, const TessellationTargetLimits* limits = nullptr) const; }; } diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 5a97337b76..d94eaff86a 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -336,10 +336,16 @@ void CollectComputeInputs(const ShaderComputeInputInfo* compute, ShaderInfo& inf } } +bool OwnControlPointWrite(const IrValue& inst) { + if (inst.Opcode() != IrOpcode::SetTessellationAttribute) return false; + const auto kind = static_cast(inst.Argument(0)->ImmediateU32()); + return kind == TessellationAttribute::ControlOutput || kind == TessellationAttribute::ControlShared; +} + void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { for (const auto& block : program.Blocks()) { for (const IrValue* inst : block->Instructions()) { - if (program.Resources().stage == IrShaderStage::TessellationControl && inst->Opcode() == IrOpcode::LaneId) { + if (program.Resources().stage == IrShaderStage::TessellationControl && (inst->Opcode() == IrOpcode::LaneId || OwnControlPointWrite(*inst))) { AddInput(info, StageInputKind::InvocationId, 0, 1, "gl_InvocationID"); } if (inst->Opcode() != IrOpcode::GetBuiltin) { diff --git a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp index 3304659e91..36981ccf21 100644 --- a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp +++ b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp @@ -558,10 +558,16 @@ std::uint32_t ComponentCount(IrOpcode opcode) { class Lowering { public: - Lowering(IrProgram& program, const ShaderTessellationInputInfo& tessellation) - : program(program), tessellation(tessellation), stage(program.Resources().stage), evaluator(stage == IrShaderStage::TessellationControl ? tessellation.outputControlPoints : 0u, stage == IrShaderStage::Local ? LocalVertexIndexCount : static_cast(tessellation.patchesPerGroup)) {} + Lowering(IrProgram& program, const ShaderTessellationInputInfo& tessellation, const TessellationTargetLimits* limits) + : program(program), tessellation(tessellation), limits(limits), stage(program.Resources().stage), merged(stage == IrShaderStage::TessellationControl && tessellation.mergedHull), + evaluator(stage == IrShaderStage::TessellationControl ? tessellation.outputControlPoints : 0u, stage == IrShaderStage::Local ? LocalVertexIndexCount : static_cast(tessellation.patchesPerGroup)), localStride(program.Metadata().tessellationLocalStride) {} void Run() { + if (merged) { + if (tessellation.inputControlPoints > tessellation.outputControlPoints) Fail("a merged hull program with more input than output control points runs its local part on lanes the hull stage does not have", 0u); + requireUniformBarriers(); + requireOwnLanes(); + } if (stage == IrShaderStage::TessellationControl) lowerOwnLaneBits(); collect(); derive(); @@ -591,6 +597,8 @@ public: program.Metadata().tessellationLocalStride = localStride; program.Metadata().tessellationControlStride = controlStride; program.Metadata().tessellationPatchStride = patchStride; + program.Metadata().tessellationSharedStride = sharedStride; + if (merged) requireMergedLimits(); } private: @@ -626,6 +634,8 @@ private: if (access.components == 0u || (shared != SharedAccess::Read && shared != SharedAccess::Write)) Fail("only 32-bit LDS reads and writes are supported in tessellation stages", flags.pc); if (stage == IrShaderStage::Local && access.write) { access.kind = TessellationAttribute::LocalOutput; + } else if (merged) { + access.kind = TessellationAttribute::ControlShared; } else if (stage == IrShaderStage::TessellationControl && !access.write) { access.kind = TessellationAttribute::ControlInput; } else { @@ -678,6 +688,10 @@ private: if (access.kind == TessellationAttribute::ControlInput && (access.address.dynamic || access.address.vertex == 0) && access.address.relative > 0 && access.address.relative % inputs == 0) agree(localStride, access.address.relative / inputs, access.pc); if (access.kind == TessellationAttribute::ControlOutput && access.address.vertex != 0) agree(controlStride, access.address.vertex, access.pc); if (access.kind == TessellationAttribute::ControlOutput && soleControlPoint(access)) agree(controlStride, access.address.relative, access.pc); + if (access.kind == TessellationAttribute::ControlShared) { + if (access.address.relative <= 0 || access.address.relative % inputs != 0) Fail("the merged hull program accesses LDS outside its patch", access.pc); + agree(sharedStride, access.address.relative / inputs, access.pc); + } if (access.kind == TessellationAttribute::EvaluationInput) { 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) { @@ -734,6 +748,13 @@ private: if (evaluator.Lowest(address) < static_cast(tessellation.patchesPerGroup) * outputs * controlStride) Fail("the domain shader reads off-chip memory that is neither its patch's control points nor a patch-constant record", access.pc); record(access, end); break; + case TessellationAttribute::ControlShared: + if (access.write) { + if (address.vertex != sharedStride || address.constant < 0 || end > sharedStride) Fail("the merged hull program writes LDS outside its own control point's record", access.pc); + } else if ((address.vertex != 0 && address.vertex != sharedStride) || address.constant < 0 || end + address.vertex * (outputs - 1) > inputs * sharedStride) { + Fail("the merged hull program reads LDS outside its patch's control point records", access.pc); + } + break; case TessellationAttribute::PatchOutput: break; } @@ -841,6 +862,66 @@ private: } } + void requireUniformBarriers() const { + const auto& blocks = program.BlockOrder(); + for (IrBlock* block : blocks) { + if (std::none_of(block->Instructions().begin(), block->Instructions().end(), [](const IrValue* inst) { return inst->Opcode() == IrOpcode::Barrier; })) continue; + std::unordered_set seen{block}; + std::vector pending; + if (blocks.front() != block) pending.push_back(blocks.front()); + while (!pending.empty()) { + IrBlock* current = pending.back(); + pending.pop_back(); + if (!seen.insert(current).second) continue; + if (current->Successors().empty()) Fail("the merged hull program has an s_barrier that some invocations do not reach", 0u); + pending.insert(pending.end(), current->Successors().begin(), current->Successors().end()); + } + seen = {}; + pending.assign(block->Successors().begin(), block->Successors().end()); + while (!pending.empty()) { + IrBlock* current = pending.back(); + pending.pop_back(); + if (current == block) Fail("the merged hull program has an s_barrier inside a loop", 0u); + if (!seen.insert(current).second) continue; + pending.insert(pending.end(), current->Successors().begin(), current->Successors().end()); + } + } + } + + void requireOwnLanes() const { + for (IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + switch (inst->Opcode()) { + case IrOpcode::ReadLane: + case IrOpcode::WriteLane: + case IrOpcode::DppMoveU32: + case IrOpcode::DppUpdateU32: + case IrOpcode::Permlane16U32: + case IrOpcode::SwizzleU32: + case IrOpcode::PermuteU32: + case IrOpcode::BpermuteU32: + Fail("the merged hull program reads or writes another lane's register, which its hull stage does not hold at one lane per invocation", 0u); + default: + break; + } + } + } + } + + void requireMergedLimits() const { + if (limits == nullptr) Fail("the target has no tessellation limits for a merged hull program", 0u); + const auto vectors = [](std::uint32_t bytes) { return (bytes + 15u) / 16u; }; + const auto patch = vectors(patchStride); + const auto perVertex = vectors(controlStride) + vectors(sharedStride); + const auto locations = vectors(controlStride) + patch + vectors(sharedStride); + const auto total = 4ull * (static_cast(tessellation.outputControlPoints) * perVertex + patch); + if (4u * locations > limits->maxControlPerVertexOutputComponents || 4u * patch > limits->maxControlPerPatchOutputComponents || total > limits->maxControlTotalOutputComponents || 4u > limits->maxControlPerVertexInputComponents) { + char text[256]; + std::snprintf(text, sizeof(text), "the merged hull program's outputs exceed the device limits (%u per-vertex locations, %u patch vectors, %llu components)", locations, patch, static_cast(total)); + Fail(text, 0u); + } + } + void requireFirstInvocation(const Access& access) { if (tessellation.outputControlPoints == 1u) return; std::unordered_set path; @@ -895,7 +976,8 @@ private: auto address = access.address.constant + 4 * static_cast(component); if (access.kind == TessellationAttribute::PatchOutput || access.kind == TessellationAttribute::PatchInput) address -= FloorDiv(address, patchStride) * patchStride; IrValue& offset = constant(static_cast(address)); - if (access.kind != TessellationAttribute::ControlInput || access.address.vertex == 0) return offset; + const bool indexed = access.kind == TessellationAttribute::ControlInput || (access.kind == TessellationAttribute::ControlShared && !access.write); + if (!indexed || access.address.vertex == 0) return offset; IrValue& invocation = insert(access, IrOpcode::GetBuiltin, IrType::U32, {&constant(static_cast(StageInputKind::InvocationId)), &constant(0u)}); IrValue& scaled = insert(access, IrOpcode::IMul32, IrType::U32, {&invocation, &constant(static_cast(access.address.vertex))}); return insert(access, IrOpcode::IAdd32, IrType::U32, {&scaled, &offset}); @@ -954,20 +1036,23 @@ private: IrProgram& program; const ShaderTessellationInputInfo& tessellation; + const TessellationTargetLimits* limits; IrShaderStage stage; + bool merged; AddressEvaluator evaluator; std::vector accesses; - std::uint32_t localStride = 0; + std::uint32_t localStride; std::uint32_t controlStride = 0; std::uint32_t patchStride = 0; + std::uint32_t sharedStride = 0; }; } -void TessellationLowering::Lower(IrProgram& program, const ShaderTessellationInputInfo& tessellation) const { +void TessellationLowering::Lower(IrProgram& program, const ShaderTessellationInputInfo& tessellation, const TessellationTargetLimits* limits) const { const auto stage = program.Resources().stage; if (stage != IrShaderStage::Local && stage != IrShaderStage::TessellationControl && stage != IrShaderStage::TessellationEvaluation) return; - Lowering(program, tessellation).Run(); + Lowering(program, tessellation, limits).Run(); } } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 48c22f5a9f..3731da950d 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -86,11 +86,17 @@ ShaderStageKind toShaderStageKind(ShaderStage stage) { namespace { +bool MergedHullStage(const RecompileRequest& request) { + const auto stage = request.shader.stage; + return (stage == ShaderStage::Local || stage == ShaderStage::TessellationControl) && request.graphics && request.graphics->tessellation && request.graphics->tessellation->mergedHull; +} + // The host subgroup width wave64 programs are laid out for. Debug aid: APS5_SINGLE_LANE= keeps the listed programs at one guest lane per invocation. std::uint32_t HostSubgroupSize(const RecompileRequest& request) { const bool vertexStage = request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::Local; if (vertexStage && !request.target.vertexSubgroups) return 1u; + if (request.shader.stage == ShaderStage::TessellationControl && MergedHullStage(request)) return 1u; static const std::string list = [] { const char* text = std::getenv("APS5_SINGLE_LANE"); return text ? std::string(text) : std::string(); }(); if (!list.empty()) { if (list == "all") return 64u; @@ -101,10 +107,16 @@ std::uint32_t HostSubgroupSize(const RecompileRequest& request) { return request.target.subgroupSize; } -RdnaProgram DecodeStageProgram(ShaderStageKind stage, std::span code) { +RdnaProgram DecodeStageProgram(ShaderStageKind stage, std::span code, bool mergedHull) { + static constexpr std::array EndProgram{0xbf810000u}; + if (stage == ShaderStageKind::Local && mergedHull) { + RdnaProgram program; + program.code = EndProgram; + program.instructions.push_back(DecodeRdnaInstruction(0u, EndProgram, 0u)); + return program; + } if (stage != ShaderStageKind::Local) return RdnaInstructionDecoder{}.Decode(code); auto program = DecodeRdnaFrontProgram(code); - static constexpr std::array EndProgram{0xbf810000u}; program.instructions.back() = DecodeRdnaInstruction(program.instructions.back().programCounter, EndProgram, 0u); return program; } @@ -134,8 +146,8 @@ std::atomic frontEndBuilds{0}; struct PreparedControlFlow { PreparedControlFlow(const RecompileRequest& request, const SwappcInfo& swappcInfo) - : stage(request.shader.stage), fallbacks(swappcInfo.linkedCallFallbacks.begin(), swappcInfo.linkedCallFallbacks.end()), swappc(WithFallbacks(swappcInfo, fallbacks)), code(request.shader.code.begin(), request.shader.code.end()), - decoded(DecodeStageProgram(toShaderStageKind(request.shader.stage), code)), cfg(GraphBuilder{}.Build(decoded, &swappc)) { + : stage(request.shader.stage), mergedHull(MergedHullStage(request)), fallbacks(swappcInfo.linkedCallFallbacks.begin(), swappcInfo.linkedCallFallbacks.end()), swappc(WithFallbacks(swappcInfo, fallbacks)), code(request.shader.code.begin(), request.shader.code.end()), + decoded(DecodeStageProgram(toShaderStageKind(request.shader.stage), code, mergedHull)), cfg(GraphBuilder{}.Build(decoded, &swappc)) { Structurizer{}.Structurize(cfg); } @@ -143,7 +155,7 @@ struct PreparedControlFlow { PreparedControlFlow& operator=(const PreparedControlFlow&) = delete; bool Matches(const RecompileRequest& request, const SwappcInfo& swappcInfo) const { - return stage == request.shader.stage && swappc.fetchCallAllowed == swappcInfo.fetchCallAllowed && + return stage == request.shader.stage && mergedHull == MergedHullStage(request) && swappc.fetchCallAllowed == swappcInfo.fetchCallAllowed && swappc.userDataBaseRegister == swappcInfo.userDataBaseRegister && swappc.userDataCount == swappcInfo.userDataCount && std::ranges::equal(fallbacks, swappcInfo.linkedCallFallbacks) && std::ranges::equal(code, request.shader.code); } @@ -154,6 +166,7 @@ struct PreparedControlFlow { } ShaderStage stage; + bool mergedHull; std::vector fallbacks; SwappcInfo swappc; std::vector code; @@ -218,8 +231,9 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat constexpr InstructionTranslator translator; + if (frontend->mergedHull && cfg.hasFetchCall) throw std::runtime_error("a merged hull program that calls a fetch subroutine is unsupported"); EmbeddedFetchPlan embeddedFetch; - if ((stageKind == ShaderStageKind::Vertex || stageKind == ShaderStageKind::Local) && inputInfo.vertex != nullptr && inputInfo.vertex->fetchEmbedded) { + if ((stageKind == ShaderStageKind::Vertex || stageKind == ShaderStageKind::Local) && !frontend->mergedHull && inputInfo.vertex != nullptr && inputInfo.vertex->fetchEmbedded) { embeddedFetch = EmbeddedVertexFetchAnalyzer{}.Analyze(decoded, inputInfo.vertex->fetchAttribReg, inputInfo.vertex->fetchBufferReg, request.context.userDataBaseRegister, static_cast(request.context.userData.size()), request.context.waveSize); translateOptions.embeddedFetch = &embeddedFetch; } @@ -290,7 +304,7 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat if (inputInfo.vertex != nullptr) { constexpr TessellationLowering tessellationLowering; - tessellationLowering.Lower(program, inputInfo.vertex->tess); + tessellationLowering.Lower(program, inputInfo.vertex->tess, request.target.tessellation ? &*request.target.tessellation : nullptr); } constexpr SrtWalker srtWalker; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 5b1e075939..9ca828bf3d 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -340,6 +340,7 @@ struct TessellationConfiguration { bool breakAtInstance = false; std::uint32_t instancePatches = 0; bool drawParameters = false; + bool mergedHull = false; }; inline constexpr std::uint32_t MeshDrawPushOffsetBytes = 104; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 6f166d9f4b..3603bb59b8 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -94,7 +94,7 @@ struct SpirvEmitterState { SpirvModule module; const IrProgram& program; ShaderStageInputInfo inputInfo; - std::array tessVariables {}; + std::array tessVariables {}; std::uint32_t tessInnerVariable = 0; SpirvRequirements requirements; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 390c82cb30..e5fbb7f67c 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -793,7 +793,7 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins const auto address = ctx.Arg(inst, 1); const bool patch = kind == TessellationAttribute::PatchOutput || kind == TessellationAttribute::PatchInput; const bool local = kind == TessellationAttribute::LocalOutput || kind == TessellationAttribute::ControlInput; - const auto stride = local ? state.program.Metadata().tessellationLocalStride : state.program.Metadata().tessellationControlStride; + const auto stride = local ? state.program.Metadata().tessellationLocalStride : kind == TessellationAttribute::ControlShared ? state.program.Metadata().tessellationSharedStride : 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; @@ -801,9 +801,10 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins state.module.AddFunction(spv::OpExtInst, TypeU32(state), clamped, GlslStd450(state), GLSLstd450UMin, vertex, ConstantU32(state, state.inputInfo.vertex->tess.inputControlPoints - 1u)); return clamped; }; + const bool sharedRead = kind == TessellationAttribute::ControlShared && inst.Opcode() == IrOpcode::GetTessellationAttribute; const auto* sum = inst.Argument(1); const auto* scaled = sum->Opcode() == IrOpcode::IAdd32 ? sum->Argument(0) : nullptr; - if (kind == TessellationAttribute::ControlInput && scaled != nullptr && sum->Argument(1)->HasImmediate() && scaled->Opcode() == IrOpcode::IMul32 && scaled->Argument(1)->HasImmediate() && scaled->Argument(1)->ImmediateU32() == stride) { + if ((kind == TessellationAttribute::ControlInput || sharedRead) && scaled != nullptr && sum->Argument(1)->HasImmediate() && scaled->Opcode() == IrOpcode::IMul32 && scaled->Argument(1)->HasImmediate() && scaled->Argument(1)->ImmediateU32() == stride) { const auto constant = sum->Argument(1)->ImmediateU32(); const auto vertex = inPatch(EmitBinaryU32(state, spv::OpIAdd, ctx.Arg(*scaled, 0), ConstantU32(state, constant / stride))); state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, ConstantU32(state, constant % stride / 16u), ConstantU32(state, constant % stride % 16u / 4u)); @@ -815,7 +816,8 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins if (kind == TessellationAttribute::LocalOutput || patch) { state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, attribute, component); } else { - const auto vertex = kind == TessellationAttribute::ControlOutput ? EmitLaneId(state) : inPatch(EmitBinaryU32(state, spv::OpUDiv, address, ConstantU32(state, stride))); + const bool own = kind == TessellationAttribute::ControlOutput || (kind == TessellationAttribute::ControlShared && !sharedRead); + const auto vertex = own ? EmitLaneId(state) : inPatch(EmitBinaryU32(state, spv::OpUDiv, address, ConstantU32(state, stride))); state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, attribute, component); } return pointer; @@ -824,7 +826,7 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins } void DefineTessellationInterfaces(SpirvEmitterState& state) { - std::array used {}; + std::array used {}; for (const IrBlock* block : state.program.BlockOrder()) { for (const IrValue* inst : block->Instructions()) { if (inst->Opcode() != IrOpcode::GetTessellationAttribute && inst->Opcode() != IrOpcode::SetTessellationAttribute) { @@ -858,7 +860,7 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { type = array(TypeU32Vector(state, 4u), (state.program.Metadata().tessellationPatchStride + 15u) / 16u); } else { const bool local = kind == TessellationAttribute::LocalOutput || kind == TessellationAttribute::ControlInput; - const auto stride = local ? state.program.Metadata().tessellationLocalStride : state.program.Metadata().tessellationControlStride; + const auto stride = local ? state.program.Metadata().tessellationLocalStride : kind == TessellationAttribute::ControlShared ? state.program.Metadata().tessellationSharedStride : state.program.Metadata().tessellationControlStride; type = array(TypeU32Vector(state, 4u), (stride + 15u) / 16u); if (kind != TessellationAttribute::LocalOutput) { type = array(type, local ? tess.inputControlPoints : tess.outputControlPoints); @@ -874,7 +876,11 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { state.module.AddAnnotation(spv::OpDecorate, state.tessInnerVariable, spv::DecorationBuiltIn, spv::BuiltInTessLevelInner); state.module.AddAnnotation(spv::OpDecorate, state.tessInnerVariable, spv::DecorationPatch); } else { - state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation, patch ? (state.program.Metadata().tessellationControlStride + 15u) / 16u : 0u); + const auto controlLocations = (state.program.Metadata().tessellationControlStride + 15u) / 16u; + const bool patchUsed = used[static_cast(TessellationAttribute::PatchOutput)] || used[static_cast(TessellationAttribute::PatchInput)]; + const auto patchLocations = patchUsed ? (state.program.Metadata().tessellationPatchStride + 15u) / 16u : 0u; + const auto location = patch ? controlLocations : kind == TessellationAttribute::ControlShared ? controlLocations + patchLocations : 0u; + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation, location); } if (kind == TessellationAttribute::Factor || patch) { state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationPatch); diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index e9f11b2e9b..d73689d528 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -188,6 +188,10 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO const auto tessellationBase = [&entryIr](TessellationBaseKind kind) -> IrValue& { return entryIr.Emit(IrOpcode::TessellationBase, IrOpcodeType(IrOpcode::TessellationBase), {&entryIr.Constant(static_cast(kind))}); }; + constexpr std::uint32_t MergedHullRecordBytes = 16u; + const auto mergedHullRecord = [&entryIr](IrValue& address) -> IrValue& { + return entryIr.Emit(IrOpcode::GetTessellationAttribute, IrOpcodeType(IrOpcode::GetTessellationAttribute), {&entryIr.Constant(static_cast(TessellationAttribute::ControlInput)), &address, &entryIr.ConstantBool(true)}); + }; for (std::uint32_t index = 0; index < options.userDataCount; index++) { const auto reg = static_cast(options.userDataBaseRegister + index); @@ -325,6 +329,13 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO setVector(static_cast(2), drawIndex(StageInputKind::VertexIndex)); setVector(static_cast(3), tessellationBase(TessellationBaseKind::RelativeIndex)); setVector(static_cast(5), drawIndex(StageInputKind::InstanceIndex)); + if (options.inputInfo.vertex->tess.mergedHull) { + const auto kind = static_cast(TessellationAttribute::LocalOutput); + for (const auto [offset, index] : {std::pair{0u, StageInputKind::VertexIndex}, std::pair{4u, StageInputKind::InstanceIndex}}) { + static_cast(entryIr.Emit(IrOpcode::SetTessellationAttribute, IrType::Void, {&entryIr.Constant(kind), &entryIr.Constant(offset), &builtin(index), &entryIr.ConstantBool(true)})); + } + program.Metadata().tessellationLocalStride = MergedHullRecordBytes; + } } else if (options.stage == ShaderStageKind::TessellationControl) { const auto& tess = options.inputInfo.vertex->tess; entryIr.SetScalarReg(static_cast(2), tessellationBase(TessellationBaseKind::OffChip)); @@ -332,6 +343,14 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO entryIr.SetScalarReg(static_cast(3), entryIr.Constant(0x81010000u | tess.inputControlPoints | (tess.outputControlPoints << 8u))); setVector(static_cast(0), builtin(StageInputKind::PrimitiveId)); setVector(static_cast(1), entryIr.BitwiseOr(entryIr.ShiftLeftLogical(builtin(StageInputKind::InvocationId), entryIr.Constant(8u)), tessellationBase(TessellationBaseKind::RelativeIndex))); + if (tess.mergedHull) { + IrValue& point = entryIr.Emit(IrOpcode::UMin32, IrOpcodeType(IrOpcode::UMin32), {&builtin(StageInputKind::InvocationId), &entryIr.Constant(tess.inputControlPoints - 1u)}); + IrValue& record = entryIr.IMul(point, entryIr.Constant(MergedHullRecordBytes)); + setVector(static_cast(2), mergedHullRecord(record)); + setVector(static_cast(3), entryIr.IAdd(entryIr.IMul(tessellationBase(TessellationBaseKind::RelativeIndex), entryIr.Constant(tess.inputControlPoints)), builtin(StageInputKind::InvocationId))); + setVector(static_cast(5), mergedHullRecord(entryIr.IAdd(record, entryIr.Constant(4u)))); + program.Metadata().tessellationLocalStride = MergedHullRecordBytes; + } } else if (options.stage == ShaderStageKind::TessellationEvaluation) { entryIr.SetScalarReg(static_cast(3), entryIr.Constant(64u)); entryIr.SetScalarReg(static_cast(4), tessellationBase(TessellationBaseKind::OffChip)); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 4c5482b884..6c894d3e8c 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -206,6 +206,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues target.breakAtInstance = tessellation->breakAtInstance; target.instancePatches = tessellation->instancePatches; target.drawParameters = tessellation->drawParameters; + target.mergedHull = tessellation->mergedHull; } if (stage == ShaderStageKind::Mesh) { if (mesh == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: a mesh-stage program has no mesh configuration"); diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index be4b2a16c7..138602235a 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -214,11 +214,11 @@ 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] = 38u; + upgraded[4] = 39u; 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, 5u, 0u); + if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 6u, 0u); upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; @@ -254,11 +254,11 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 38u; + layered[4] = 39u; InsertMeshBarycentricFlag(layered); layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); - if (fixture.tessellation) layered.insert(layered.end() - 35u, 5u, 0u); + if (fixture.tessellation) layered.insert(layered.end() - 35u, 6u, 0u); layered.insert(layered.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } @@ -280,7 +280,7 @@ 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] = 38u; + bytes[4] = 39u; InsertMeshBarycentricFlag(bytes); bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); @@ -404,13 +404,23 @@ int main() { Require(!fixture.tessellation || (request.graphics->tessellation->patchesPerGroup == 63u && request.graphics->tessellation->primitiveGroupSize == 37u && request.graphics->tessellation->breakAtInstance)); 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] = 38u; - InsertMeshBarycentricFlag(upgraded); - if (fixture.pixel) upgraded.insert(upgraded.begin() + 259u, {0u, 0u}); - if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); + auto main38 = bytes; + main38[4] = 38u; + InsertMeshBarycentricFlag(main38); + if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); + if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38); + auto upgraded = main38; + upgraded[4] = 39u; + 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); 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); + Require(!fixture.tessellation || (!replay38.request.graphics->tessellation->mergedHull && replay38.request.graphics->tessellation->primitiveGroupSize == 37u && replay38.request.graphics->tessellation->breakAtInstance)); + Require(replay38.request.graphics->draw.indexAddress == 0xabcdef3400u && replay38.request.graphics->draw.indexCount == 57u && replay38.request.graphics->draw.indexElementBytes == 4u && replay38.request.graphics->draw.instanceCount == 9u); + Require(replay38.request.target.float16DenormPreserve == preserve); + Require(serializer.Serialize(replay38.request) == EncodeBase64(upgraded)); for (const bool dither : {false, true}) { if (!fixture.pixel && dither) continue; auto main25 = bytes; @@ -419,12 +429,13 @@ 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] = 38u; + main25[4] = 39u; 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); Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); } @@ -440,9 +451,9 @@ int main() { const auto replay = serializer.Deserialize(encoded); Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 38u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 39u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto future = bytes; - future[4] = 39u; + future[4] = 40u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index c2b4084d5f..9eabb677f9 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -169,3 +169,4 @@ - [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. - [Spanned wave64 compute](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) (shader recompiler) - `APS5_SPANNED_WAVE=1`, off by default, runs a single-wave wave64 compute program at one lane per invocation on a 32-lane host subgroup and exchanges ballots, shuffles and reductions between the two host subgroups through workgroup memory and barriers. The driver turns it on only where compute subgroups can be pinned at 32 full lanes (`subgroupSizeControl` and `computeFullSubgroups`), passes it as `SpirvTarget::spannedWaves` (part of the cache key and the serialized request), and creates the pipelines of spanned programs with `REQUIRE_FULL_SUBGROUPS`, since the lane number `SubgroupId * 32 + SubgroupLocalInvocationId` needs two full subgroups. Those barriers are correct only where every invocation of the workgroup reaches them, which holds for the guest's wave-uniform control flow. The emulated 64-bit LDS atomics ([LockedLdsUpdate](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp)) run only on the lanes the exec mask keeps, so their lock loop ballots within its own host subgroup, as its elect already does. Any other cross-lane exchange emitted inside per-lane control flow would hang a spanned wave in the same way, and the emitter does not reject one: there is no uniformity check. The only check so far is manual: disassemble the module (`spirv-dis`), list the selection and loop constructs that enclose each `OpControlBarrier`, and read the condition of each enclosing selection. In Ghost of Yōtei's program 0x80003af600 (PPSA26344), 108 of 111 barriers sat only in loops on wave-uniform conditions and 3 inside per-invocation conditionals, all in the lock loop. Other programs and titles have not been scanned. An earlier, cheaper ballot (one barrier instead of three) also lost the device for a reason that is not understood, which may be another barrier placement problem. Measured on an RTX 4090 (Windows): an offline replay of that program and two Yōtei runs that previously lost the device (NVIDIA TDR) now run past it, and [LdsMaskWaterfall.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp) runs both modes and covers a half-inactive wave. Stray (PPSA02101) on Intel ANV and a Demon's Souls (PPSA01342) request replay showed no fault, before this fix - [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - a guest load whose lane is inactive or whose address is out of bounds is still performed and its result replaced by 0 with `OpSelect`, instead of a selection per guard, when the load can be pointed at a location nothing writes: scratch reads its own element 0; storage buffers (direct buffers, GDS) read element 0xfffffffe, which `robustBufferAccess2` turns into a zero load with no memory access, so the extra read cannot race with a write the guarded program would not have raced with ([Vulkan memory model](https://docs.vulkan.org/spec/latest/appendices/memorymodel.html#data-race)). Without `robustBufferAccess2` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it) those loads keep their selections; LDS loads, stores, atomics, formatted and physical-address (BDA) accesses always do. LDS keeps the branch because NVIDIA's compiler predicates a branched LDS load at no cost, while the select form costs about 2.5 instructions per dword and splits a `ds_read_b128` into four loads (RTX 4060 measurements by @karanvora2599 in #4048); Intel's static counts favour the select only for wide reads, and run time was not measured on either. The aligned high word of a misaligned-capable buffer read re-reads the low word's own element. A selection guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. `agc_guarded_memory_loads` traces the emitted loads in both device modes. AMD's proprietary driver does not get `robustBufferAccess2` ([RobustBufferAccess.hpp](../../../core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp)), so its loads keep their selections. On an RX 6700 XT with driver 32.0.21045.5002, enabling the feature broke `agc_driver_layered_volume_draw` and `agc_driver_layered_volume_vertex`: the vertex draw left the volume untouched even with the selections kept. `agc_driver_subvector_loop` failed too, with the selected loads: the driver compiled the begin instruction's `exec_lo == 0` as a test of the whole 64-bit mask, so the high half of a wave64 skipped its loop. The begin instruction now tests for an empty mask as `(exec_lo | exec_hi) == 0` rather than `exec_lo == 0 && exec_hi == 0`, and that driver compiles it correctly. The cause of the layered-volume failures was not found. +- [Merged hull programs](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) (registered binary type 3) run whole in the hull stage, one invocation per output control point. On the console hull lane i keeps local lane i's registers, which with fewer input than output control points stop matching the patch's control points from i = inputs on; here invocation i holds control point min(i, inputs - 1) of its own patch. A wave-mask branch (`s_cbranch_exec[n]z`, `s_cbranch_vcc[n]z`) tests the invocation's own lane, so it can skip a block, scalar instructions included, that the console runs for the wave; `v_readfirstlane_b32` returns the invocation's own value, not the first active lane's, and another lane's register throws. The register layout (`s3`, `v0`-`v5`, as for the separate halves) is not measured on hardware. s[0:1] hold the hull user-data address (SH 0x102/0x103) when a draw sets it and zero otherwise, so a program reading it unset faults at the null address. From e5543aad64e48e8a8bcc6bd0c9f13e0c230898d9 Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Tue, 20 Oct 2026 11:20:19 -0300 Subject: [PATCH 162/167] chore(tools): add the sceImeFilterText codepage generator --- docs/dev/debt/libSceIme.md | 2 +- tools/gen_ime_codepages.py | 77 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 tools/gen_ime_codepages.py diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 743df9fe45..0254c3ce17 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -8,7 +8,7 @@ - [sceImeSetConsecutiveClauseConversionEnabled](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - name from its NID (`row9sfnYNx4`) in [sce_symbols](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); neither [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0ebea15c24253b60f514fc37a348dabc8d0bcb90/src/core/libraries/ime/ime.cpp) nor [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/e5f0ee7fc2fb4ce88c6abb2fa02296404e2fe2a6/src/libs/ime.cpp) implements it, so it throws. PPSA24191 calls it with 1 as its only argument before each `sceImeOpen` and opens the panel only when it returns 0; the argument type, the result while a panel is open and the error codes are unknown - [sceImeGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills. While a panel is open it reports the type, position and alignments passed to `sceImeOpen` and the size `sceImeGetPanelSize` gives; the console's values for a panel moved by its own position adjustment are unknown. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title -- [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 and is differential-tested against that function in an emulator, not measured on a console. [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp) holds the two codepage checks (helpers at 0x4850 and 0x48f0), rebuilt from Unicode's public [CP932](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT) and [CP936](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT) tables plus the Latin-1, CJK Extension A and a few other units the firmware also accepts (listed in the PR). The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` derive it when it is 0. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown +- [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 and is differential-tested against that function in an emulator, not measured on a console. [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp) holds the two codepage checks (helpers at 0x4850 and 0x48f0), rebuilt from Unicode's public [CP932](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT) and [CP936](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT) tables plus the Latin-1, CJK Extension A and a few other units the firmware also accepts (see [gen_ime_codepages.py](../../../tools/gen_ime_codepages.py)). The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` derive it when it is 0. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown - [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn - [sceImeVshOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeKeyboardUpdate](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature diff --git a/tools/gen_ime_codepages.py b/tools/gen_ime_codepages.py new file mode 100644 index 0000000000..31e465acfb --- /dev/null +++ b/tools/gen_ime_codepages.py @@ -0,0 +1,77 @@ +import re +import sys + + +def load(path): + units = set() + for line in open(path, encoding="latin1"): + m = re.match(r"0x[0-9A-Fa-f]+\s+0x([0-9A-Fa-f]{4,6})", line) + if m and int(m.group(1), 16) < 0x10000: + units.add(int(m.group(1), 16)) + return units + + +def unit_range(first, last): + return set(range(first, last + 1)) + + +def build_a(cp932): + units = set(cp932) + units -= unit_range(0xE000, 0xE757) | {0x80} | unit_range(0xF8F0, 0xF8F3) + units |= unit_range(0xA0, 0xFF) | {0x2014, 0x2016, 0x203E, 0x2212, 0x301C} + return units + + +def build_b(cp936): + units = set(cp936) + units |= unit_range(0x80, 0xFF) | {0x1F9, 0x2E84, 0x2E88, 0x2E8B, 0x2E8C, 0x2E97, 0x2EA7, 0x2EAA} + units |= {0x2EB6, 0x2EB7, 0x2EBB, 0x303E} | unit_range(0x2FF0, 0x2FFB) + ext_a = unit_range(0x3400, 0x4DB5) - {0x3918, 0x39CF, 0x39DF, 0x415F, 0x4337, 0x4729, 0x477C, 0x478D, 0x4C9F, 0x4CA0, 0x4CA3} + units |= ext_a | unit_range(0xE766, 0xE864) + return units + + +def ranges(units): + out = [] + start = prev = None + for u in sorted(units): + if start is None: + start = prev = u + elif u == prev + 1: + prev = u + else: + out.append((start, prev)) + start = prev = u + out.append((start, prev)) + return out + + +def emit(a, b): + words = [] + for w in range(1024): + v = 0 + for bit in range(64): + if (w * 64 + bit) in a: + v |= 1 << bit + words.append(v) + rb = ranges(b) + out = ["#pragma once", "#include ", "", "namespace ime_filter {", ""] + out.append("inline constexpr std::uint64_t kCodepageABitmap[1024] = {") + for i in range(0, 1024, 4): + out.append(" " + ", ".join("0x%016xull" % x for x in words[i:i + 4]) + ",") + out.append("};") + out.append("") + out.append("struct CodepageRange {\n std::uint16_t first;\n std::uint16_t last;\n};") + out.append("") + out.append("inline constexpr CodepageRange kCodepageBRanges[%d] = {" % len(rb)) + for i in range(0, len(rb), 6): + out.append(" " + ", ".join("{0x%04x, 0x%04x}" % r for r in rb[i:i + 6]) + ",") + out.append("};") + out.append("") + out.append("}") + return "\n".join(out) + "\n" + + +if __name__ == "__main__": + mappings, target = sys.argv[1], sys.argv[2] + open(target, "w", newline="").write(emit(build_a(load(mappings + "/CP932.TXT")), build_b(load(mappings + "/CP936.TXT")))) From 9a6f7c1bac912dd14924de2f387df17bb2261bc9 Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Tue, 20 Oct 2026 11:25:58 -0300 Subject: [PATCH 163/167] chore(tools): drop the codepage generator, kept in the PR description --- docs/dev/debt/libSceIme.md | 2 +- tools/gen_ime_codepages.py | 77 -------------------------------------- 2 files changed, 1 insertion(+), 78 deletions(-) delete mode 100644 tools/gen_ime_codepages.py diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 0254c3ce17..743df9fe45 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -8,7 +8,7 @@ - [sceImeSetConsecutiveClauseConversionEnabled](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - name from its NID (`row9sfnYNx4`) in [sce_symbols](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); neither [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0ebea15c24253b60f514fc37a348dabc8d0bcb90/src/core/libraries/ime/ime.cpp) nor [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/e5f0ee7fc2fb4ce88c6abb2fa02296404e2fe2a6/src/libs/ime.cpp) implements it, so it throws. PPSA24191 calls it with 1 as its only argument before each `sceImeOpen` and opens the panel only when it returns 0; the argument type, the result while a panel is open and the error codes are unknown - [sceImeGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills. While a panel is open it reports the type, position and alignments passed to `sceImeOpen` and the size `sceImeGetPanelSize` gives; the console's values for a panel moved by its own position adjustment are unknown. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title -- [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 and is differential-tested against that function in an emulator, not measured on a console. [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp) holds the two codepage checks (helpers at 0x4850 and 0x48f0), rebuilt from Unicode's public [CP932](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT) and [CP936](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT) tables plus the Latin-1, CJK Extension A and a few other units the firmware also accepts (see [gen_ime_codepages.py](../../../tools/gen_ime_codepages.py)). The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` derive it when it is 0. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown +- [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 and is differential-tested against that function in an emulator, not measured on a console. [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp) holds the two codepage checks (helpers at 0x4850 and 0x48f0), rebuilt from Unicode's public [CP932](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT) and [CP936](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT) tables plus the Latin-1, CJK Extension A and a few other units the firmware also accepts (listed in the PR). The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` derive it when it is 0. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown - [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn - [sceImeVshOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature - [sceImeKeyboardUpdate](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature diff --git a/tools/gen_ime_codepages.py b/tools/gen_ime_codepages.py deleted file mode 100644 index 31e465acfb..0000000000 --- a/tools/gen_ime_codepages.py +++ /dev/null @@ -1,77 +0,0 @@ -import re -import sys - - -def load(path): - units = set() - for line in open(path, encoding="latin1"): - m = re.match(r"0x[0-9A-Fa-f]+\s+0x([0-9A-Fa-f]{4,6})", line) - if m and int(m.group(1), 16) < 0x10000: - units.add(int(m.group(1), 16)) - return units - - -def unit_range(first, last): - return set(range(first, last + 1)) - - -def build_a(cp932): - units = set(cp932) - units -= unit_range(0xE000, 0xE757) | {0x80} | unit_range(0xF8F0, 0xF8F3) - units |= unit_range(0xA0, 0xFF) | {0x2014, 0x2016, 0x203E, 0x2212, 0x301C} - return units - - -def build_b(cp936): - units = set(cp936) - units |= unit_range(0x80, 0xFF) | {0x1F9, 0x2E84, 0x2E88, 0x2E8B, 0x2E8C, 0x2E97, 0x2EA7, 0x2EAA} - units |= {0x2EB6, 0x2EB7, 0x2EBB, 0x303E} | unit_range(0x2FF0, 0x2FFB) - ext_a = unit_range(0x3400, 0x4DB5) - {0x3918, 0x39CF, 0x39DF, 0x415F, 0x4337, 0x4729, 0x477C, 0x478D, 0x4C9F, 0x4CA0, 0x4CA3} - units |= ext_a | unit_range(0xE766, 0xE864) - return units - - -def ranges(units): - out = [] - start = prev = None - for u in sorted(units): - if start is None: - start = prev = u - elif u == prev + 1: - prev = u - else: - out.append((start, prev)) - start = prev = u - out.append((start, prev)) - return out - - -def emit(a, b): - words = [] - for w in range(1024): - v = 0 - for bit in range(64): - if (w * 64 + bit) in a: - v |= 1 << bit - words.append(v) - rb = ranges(b) - out = ["#pragma once", "#include ", "", "namespace ime_filter {", ""] - out.append("inline constexpr std::uint64_t kCodepageABitmap[1024] = {") - for i in range(0, 1024, 4): - out.append(" " + ", ".join("0x%016xull" % x for x in words[i:i + 4]) + ",") - out.append("};") - out.append("") - out.append("struct CodepageRange {\n std::uint16_t first;\n std::uint16_t last;\n};") - out.append("") - out.append("inline constexpr CodepageRange kCodepageBRanges[%d] = {" % len(rb)) - for i in range(0, len(rb), 6): - out.append(" " + ", ".join("{0x%04x, 0x%04x}" % r for r in rb[i:i + 6]) + ",") - out.append("};") - out.append("") - out.append("}") - return "\n".join(out) + "\n" - - -if __name__ == "__main__": - mappings, target = sys.argv[1], sys.argv[2] - open(target, "w", newline="").write(emit(build_a(load(mappings + "/CP932.TXT")), build_b(load(mappings + "/CP936.TXT")))) From c9ff98e24950033e720d22230e69a876d44127de Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Tue, 20 Oct 2026 16:43:51 +0200 Subject: [PATCH 164/167] 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 165/167] 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 166/167] 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 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 167/167] 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")