fix(libSceAgc): write the KytyPS5 workload packets and return null for unregistered streams
sceAgcDcbSetWorkloadsActive writes an 18-dword IT_NOP packet (stream id, 64-bit workload mask) and sceAgcDcbSetWorkloadComplete a 12-dword one (stream id, workload id, 64-bit clear mask), following KytyPS5. Both return null for a null buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. sceAgcDriverRegisterWorkloadStream and sceAgcDriverUnregisterWorkloadStream now record the stream ids the DCB calls check. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
45fcf734ab
commit
2a7b38ab75
|
|
@ -68,7 +68,7 @@ endforeach()
|
|||
|
||||
add_test_executable(agc_command_tests EXCLUDE_FROM_ALL tests/Commands.cpp)
|
||||
target_include_directories(agc_command_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_command_tests PRIVATE libSceAgc libc)
|
||||
target_link_libraries(agc_command_tests PRIVATE libSceAgc libSceAgcDriver libc)
|
||||
configure_windows_unwind(agc_command_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_command_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
|
|
|
|||
|
|
@ -1,20 +1,25 @@
|
|||
#include "prx/libSceAgc/DcbState/include/Workload.hpp"
|
||||
|
||||
#include "prx/libSceAgc/Command/include/Packet.hpp"
|
||||
#include "prx/libSceAgcDriver/Resource/include/Registration.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t OpcodeNop = 0x10;
|
||||
constexpr std::uint32_t ActivePacketWords = 18;
|
||||
constexpr std::uint32_t CompletePacketWords = 12;
|
||||
constexpr std::uint32_t MaxWorkloadId = 63;
|
||||
constexpr std::uint32_t MaxActiveCount = 63;
|
||||
|
||||
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;
|
||||
uint32_t* NewPacket(CommandBuffer* buf, std::uint32_t words, const char* function) {
|
||||
auto* packet = Agc::Command::Allocate(buf, words, function);
|
||||
std::memset(packet, 0, words * sizeof(std::uint32_t));
|
||||
packet[0] = Agc::Command::Header(OpcodeNop, words, 0);
|
||||
return packet;
|
||||
}
|
||||
|
||||
|
|
@ -23,16 +28,32 @@ uint32_t* EmitNop(CommandBuffer* buf, const char* function) {
|
|||
extern "C" {
|
||||
|
||||
uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t stream_id, uint32_t workload_id) {
|
||||
(void)stream_id;
|
||||
(void)workload_id;
|
||||
return EmitNop(buf, __func__);
|
||||
if (buf == nullptr || workload_id > MaxWorkloadId || !AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr;
|
||||
const std::uint64_t clearMask = ~(std::uint64_t{1} << workload_id);
|
||||
auto* packet = NewPacket(buf, CompletePacketWords, __func__);
|
||||
packet[1] = stream_id;
|
||||
packet[2] = workload_id;
|
||||
packet[3] = static_cast<std::uint32_t>(clearMask);
|
||||
packet[4] = static_cast<std::uint32_t>(clearMask >> 32);
|
||||
return packet;
|
||||
}
|
||||
|
||||
uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t stream_id, const uint32_t* workload_ids, uint32_t workload_count) {
|
||||
(void)stream_id;
|
||||
(void)workload_ids;
|
||||
(void)workload_count;
|
||||
return EmitNop(buf, __func__);
|
||||
if (buf == nullptr || workload_ids == nullptr || workload_count == 0 || workload_count > MaxActiveCount) return nullptr;
|
||||
std::uint64_t mask = 0;
|
||||
for (std::uint32_t index = 0; index < workload_count; ++index) {
|
||||
const auto id = workload_ids[index];
|
||||
if (id > MaxWorkloadId) return nullptr;
|
||||
const std::uint64_t bit = std::uint64_t{1} << id;
|
||||
if ((mask & bit) != 0) return nullptr;
|
||||
mask |= bit;
|
||||
}
|
||||
if (!AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr;
|
||||
auto* packet = NewPacket(buf, ActivePacketWords, __func__);
|
||||
packet[1] = stream_id;
|
||||
packet[2] = static_cast<std::uint32_t>(mask);
|
||||
packet[3] = static_cast<std::uint32_t>(mask >> 32);
|
||||
return packet;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer*
|
|||
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" int APS5_VABI sceAgcDriverRegisterWorkloadStream(std::uint32_t stream_id, const void* stream);
|
||||
extern "C" int APS5_VABI sceAgcDriverUnregisterWorkloadStream(std::uint32_t stream_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);
|
||||
|
|
@ -212,13 +214,33 @@ void testMarkers() {
|
|||
void testWorkloads() {
|
||||
Storage dcb;
|
||||
const std::uint32_t ids[2] = {3, 4};
|
||||
check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, ids, 2) == nullptr, "workloads active packet for an unregistered stream");
|
||||
check(sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 3) == nullptr, "workload complete packet for an unregistered stream");
|
||||
check(dcb.buffer.cursor_up == dcb.words.data(), "unregistered stream advanced the cursor");
|
||||
const std::uint32_t record[8] = {};
|
||||
check(sceAgcDriverRegisterWorkloadStream(1, record) == 0, "workload stream registration failed");
|
||||
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); });
|
||||
check(active == dcb.words.data() && active[0] == Agc::Command::Header(0x10, 18, 0) && active[1] == 1 && active[2] == 0x18 && active[3] == 0, "workloads active packet");
|
||||
for (std::uint32_t word = 4; word < 18; ++word) check(active[word] == 0, "workloads active packet tail is not zero");
|
||||
const auto* complete = sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 33);
|
||||
check(complete == dcb.words.data() + 18 && complete[0] == Agc::Command::Header(0x10, 12, 0) && complete[1] == 1 && complete[2] == 33 && complete[3] == 0xffffffffu && complete[4] == 0xfffffffdu, "workload complete packet");
|
||||
for (std::uint32_t word = 5; word < 12; ++word) check(complete[word] == 0, "workload complete packet tail is not zero");
|
||||
check(dcb.buffer.cursor_up == dcb.words.data() + 30, "incorrect workload cursor advance");
|
||||
const std::uint32_t duplicate[2] = {5, 5};
|
||||
const std::uint32_t outOfRange[1] = {64};
|
||||
const std::uint32_t top[1] = {63};
|
||||
check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, duplicate, 2) == nullptr, "duplicate workload ids accepted");
|
||||
check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, outOfRange, 1) == nullptr, "workload id 64 accepted");
|
||||
check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, top, 1) != nullptr, "workload id 63 rejected");
|
||||
check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, ids, 0) == nullptr, "workload count 0 accepted");
|
||||
check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, nullptr, 2) == nullptr, "null workload ids accepted");
|
||||
check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 0, ids, 2) == nullptr, "stream id 0 accepted");
|
||||
check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 32, ids, 2) == nullptr, "stream id 32 accepted");
|
||||
check(sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 64) == nullptr, "workload complete id 64 accepted");
|
||||
check(sceAgcDcbSetWorkloadsActive(nullptr, 1, ids, 2) == nullptr, "null command buffer accepted");
|
||||
check(sceAgcDcbSetWorkloadComplete(nullptr, 1, 3) == nullptr, "null command buffer accepted for complete");
|
||||
check(sceAgcDriverUnregisterWorkloadStream(1) == 0, "workload stream unregistration failed");
|
||||
check(sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 3) == nullptr, "workload complete packet after unregistration");
|
||||
}
|
||||
|
||||
void testIndexBuffer() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_RESOURCE_INCLUDE_REGISTRATION_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_RESOURCE_INCLUDE_REGISTRATION_HPP
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
extern "C" bool AgcDriverWorkloadStreamRegistered_nid_postfix(std::uint32_t streamId);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -2,15 +2,27 @@
|
|||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
// Resource registration feeds GPU debugging tools, none of which are attached; the game treats this code as benign.
|
||||
static constexpr int SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE = static_cast<int>(0x8A6C9018);
|
||||
static constexpr uint32_t RESOURCE_REGISTRATION_MAX_NAME_LENGTH = 0xfc;
|
||||
static constexpr uint32_t WORKLOAD_STREAM_MIN_ID = 1;
|
||||
static constexpr uint32_t WORKLOAD_STREAM_MAX_ID = 31;
|
||||
|
||||
static std::mutex g_workloadStreamLock;
|
||||
static uint32_t g_workloadStreams = 0;
|
||||
|
||||
extern "C" {
|
||||
|
||||
bool AgcDriverWorkloadStreamRegistered_nid_postfix(uint32_t streamId) {
|
||||
if (streamId < WORKLOAD_STREAM_MIN_ID || streamId > WORKLOAD_STREAM_MAX_ID) return false;
|
||||
std::lock_guard lock(g_workloadStreamLock);
|
||||
return (g_workloadStreams & (1u << streamId)) != 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcDriverRegisterOwner(uint32_t* owner_handle, const char* name) {
|
||||
(void)owner_handle;
|
||||
(void)name;
|
||||
|
|
@ -37,8 +49,10 @@ int APS5_VABI sceAgcDriverRegisterMultipleResources(uint32_t* resourceHandle, ui
|
|||
}
|
||||
|
||||
int APS5_VABI sceAgcDriverRegisterWorkloadStream(uint32_t stream_id, const void* stream) {
|
||||
(void)stream_id;
|
||||
(void)stream;
|
||||
if (stream != nullptr && stream_id >= WORKLOAD_STREAM_MIN_ID && stream_id <= WORKLOAD_STREAM_MAX_ID) {
|
||||
std::lock_guard lock(g_workloadStreamLock);
|
||||
g_workloadStreams |= 1u << stream_id;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +83,10 @@ int APS5_VABI sceAgcDriverUnregisterAllResourcesForOwner(uint32_t owner_handle)
|
|||
}
|
||||
|
||||
int APS5_VABI sceAgcDriverUnregisterWorkloadStream(uint32_t stream_id) {
|
||||
(void)stream_id;
|
||||
if (stream_id >= WORKLOAD_STREAM_MIN_ID && stream_id <= WORKLOAD_STREAM_MAX_ID) {
|
||||
std::lock_guard lock(g_workloadStreamLock);
|
||||
g_workloadStreams &= ~(1u << stream_id);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -659,7 +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.
|
||||
- [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the driver call records only the stream id (1 to 31; it returns 0 for any argument and keeps no stream record), and the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. The error codes the console driver returns for an invalid or repeated registration are unknown, so registration never fails here. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed.
|
||||
- [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
|
||||
|
|
|
|||
Loading…
Reference in New Issue