From 45fcf734ab0be5ddfdc2576829b5036a3340a46a Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:31:27 +0200 Subject: [PATCH] fix(libSceAgc): emit NOP packets for the DCB workload calls sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete threw. They now write a plain two-dword NOP and keep no workload state. Co-Authored-By: Claude Sonnet 5.5 --- .../prx/libSceAgc/DcbState/src/Workload.cpp | 22 ++++++++++++++----- core/libs/prx/libSceAgc/tests/Commands.cpp | 15 +++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp index 2ce29cfe2..c2db4e988 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp @@ -6,23 +6,33 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr std::uint32_t OpcodeNop = 0x10; + +uint32_t* EmitNop(CommandBuffer* buf, const char* function) { + Agc::Command::Require(buf != nullptr, function, "null command buffer"); + auto* packet = Agc::Command::Allocate(buf, 2, function); + packet[0] = Agc::Command::Header(OpcodeNop, 2, 0); + packet[1] = 0; + return packet; +} + +} + extern "C" { uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t stream_id, uint32_t workload_id) { - (void)buf; (void)stream_id; (void)workload_id; - NotImplemented_nid_no_patch(__func__); - return nullptr; + return EmitNop(buf, __func__); } uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t stream_id, const uint32_t* workload_ids, uint32_t workload_count) { - (void)buf; (void)stream_id; (void)workload_ids; (void)workload_count; - NotImplemented_nid_no_patch(__func__); - return nullptr; + return EmitNop(buf, __func__); } } diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index d3d9c7f83..edb09cbac 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -29,6 +29,8 @@ extern "C" std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuff extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* buf, std::uint32_t operation); extern "C" std::uint32_t* APS5_VABI sceAgcDcbPushMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcDcbPopMarker(CommandBuffer* buf); +extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, std::uint32_t stream_id, std::uint32_t workload_id); +extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t stream_id, const std::uint32_t* workload_ids, std::uint32_t workload_count); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf); @@ -207,6 +209,18 @@ void testMarkers() { expectFailure([] { sceAgcAcbSetMarker(nullptr, "frame", 0); }); } +void testWorkloads() { + Storage dcb; + const std::uint32_t ids[2] = {3, 4}; + const auto* active = sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, ids, 2); + check(active == dcb.words.data() && active[0] == Agc::Command::Header(0x10, 2, 0) && active[1] == 0, "workloads active packet is not a plain NOP"); + const auto* complete = sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 3); + check(complete == dcb.words.data() + 2 && complete[0] == Agc::Command::Header(0x10, 2, 0) && complete[1] == 0, "workload complete packet is not a plain NOP"); + check(dcb.buffer.cursor_up == dcb.words.data() + 4, "incorrect workload cursor advance"); + expectFailure([] { sceAgcDcbSetWorkloadsActive(nullptr, 1, nullptr, 0); }); + expectFailure([] { sceAgcDcbSetWorkloadComplete(nullptr, 1, 3); }); +} + void testIndexBuffer() { Storage storage; alignas(4) std::uint16_t indices[2]{}; @@ -547,6 +561,7 @@ int main(int argc, char** argv) { testClearState(); testIndexedIndirectDraws(); testMarkers(); + testWorkloads(); testIndexBuffer(); testIndirectBase(); testContextState(); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fa78fec4..fd97d1e42 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -659,6 +659,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVoiceQoSEnd](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0901` (not initialized) before any initialization was measured on a PS5 (firmware 13.60, #1407); ending an initialized library was not, because a valid `sceVoiceQoSInit` never returns inside a homebrew payload. PPSA14632 calls it at shutdown and PPSA26344 after a nonzero `sceVoiceQoSInit`. After an initialization it returns 0 and clears the init flag, so the library can be initialized again. Whether the console also ends the libSceVoice instance that its `sceVoiceQoSInit` starts ([prosper](https://github.com/mattias800/prosper)'s reading of the module), and what it answers while endpoints exist, is unknown; no endpoint is implemented - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../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 - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. +- [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) - emit a plain two-dword NOP packet and keep no workload state: the stream and workload ids are not recorded, so a capture tool never sees them. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. The packet the console writes is unknown. - [sceAgcCreatePrimState](../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well - [pthread_spin_init, pthread_spin_destroy, pthread_spin_lock, pthread_spin_trylock, pthread_spin_unlock](../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) follow FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) and the owner checks of its [`thr_umtx.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_umtx.h#L80-L175), unverified on the console: `EINVAL` for a null or destroyed lock and a `pshared` other than 0 or 1, `EBUSY` from `trylock` while any thread holds it, `EPERM` from `unlock` by a thread that does not hold it. `PTHREAD_PROCESS_SHARED` allocates a private lock, as guest code runs in one process. libthr spins forever when the holder locks again; here that call throws - [pthread_cond_reltimedwait_np](../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console