Merge pull request #14295 from Skeletor-Pirate/fix/agc-dcb-workload-inactive-packet

fix(libSceAgc): write the inactive workload stream packet from the DCB call
This commit is contained in:
Dean authored and GitHub committed 2026-11-06 17:54:13 +13:00
commit 7dd4fca86c
3 files changed
+9 -5

No files matched your search

@@ -6,13 +6,13 @@
#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 InactivePacketWords = 9;
constexpr std::uint32_t MaxWorkloadId = 63;
constexpr std::uint32_t MaxActiveCount = 63;
@@ -57,8 +57,9 @@ uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t str
uint32_t* APS5_VABI sceAgcDcbSetWorkloadStreamInactive(CommandBuffer* buf, uint32_t streamId) {
if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr;
NotImplemented_nid_no_patch(__func__);
return nullptr;
auto* packet = NewPacket(buf, InactivePacketWords, __func__);
packet[1] = streamId;
return packet;
}
}
+4 -1
View File
@@ -324,7 +324,10 @@ void testWorkloads() {
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 auto* inactive = sceAgcDcbSetWorkloadStreamInactive(&dcb.buffer, 1);
check(inactive == dcb.words.data() + 30 && inactive[0] == Agc::Command::Header(0x10, 9, 0) && inactive[1] == 1, "workload stream inactive packet");
for (std::uint32_t word = 2; word < 9; ++word) check(inactive[word] == 0, "workload stream inactive packet tail is not zero");
check(dcb.buffer.cursor_up == dcb.words.data() + 39, "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};
+1 -1
View File
@@ -48,7 +48,7 @@
- [sceAgcSetAmmSemaphoreMemory, sceAgcSetSemaphoreMemory, sceAgcGetSemaphoreLabel](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) (libSceAgc) - the checks, error codes and clearing are the firmware's, but two firmware steps are not reproduced. After its checks, the firmware validates the memory with libkernel `qSleeVC6lwI`: an ioctl `0xc018ac15` taking `{memory, size, 1}`. The AMM call always does this; the non-AMM call only for memory from 8 TiB up that ends below 15 TiB. The firmware then sends `{7: memory, 8: size, 0xb: 0 for AMM, 1 otherwise}` through sceAgcDriverPassInfoDownward. Neither step happens here, so a registration that the kernel or driver would refuse succeeds. A null memory with a non-zero size and a range that wraps the address space throw where the firmware would fault
- [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) - 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. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed.
- [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or an unregistered stream; for a registered stream they write the same `IT_NOP` packets the DCB calls write: 18, 12 and 9 dwords carrying the stream id, the 64-bit active mask, the workload id plus its clear mask. The sizes match the firmware 4.03 `libSceAgcDriver.sprx` packet-size helpers, and the ACB emitter writes the identical packet with only the queue flag differing. The packets are inert here: `Pm4::Execute` returns on a NOP with operation 0 without reading the payload, so no workload state is tracked; the console's SET_UCONFIG_REG/WRITE_DATA/ATOMIC_MEM sequence below is not written. prosper's oracle measured only the null return for unregistered streams. No ACB declaration was found; OpenAGC says the ACB calls share the DCB driver builders
- [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) - returns null without writing to the buffer for a null command buffer or an unregistered stream, and throws for a registered stream. prosper's oracle measured only that null return, with no stream registered; the packet a registered stream gets is not measured. KytyPS5 has no body for it; its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence OpenAGC derives from the driver, so neither is taken. The `(buf, streamId)` signature is that of the DCB call in OpenAGC, which says the ACB calls use the same driver builders. `sceAgcDcbSetWorkloadsActive` checks the stream before reading the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 without faulting
- [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) - writes the placeholder 9-dword `IT_NOP` packet the ACB form and the DCB active and complete calls write: the size matches the firmware 4.03 `libSceAgcDriver.sprx` packet-size helper, but the console's packet for a registered stream is the SET_UCONFIG_REG/WRITE_DATA/ATOMIC_MEM sequence in the entry below, not this one, and `Pm4::Execute` skips it unread. Returns null without writing to the buffer for a null command buffer or an unregistered stream. KytyPS5 has no body for it, and its `IT_NOP` layout for the DCB active and complete calls differs from OpenAGC's sequence, so neither is taken. The `(buf, streamId)` signature is that of the DCB call in OpenAGC, which says the ACB calls use the same driver builders. `sceAgcDcbSetWorkloadsActive` checks the stream before reading the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 without faulting
- [sceAgcDcbSetWorkload*](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkload*](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - the console's packets are not written: the executor has no ATOMIC_MEM or mask table. In `libSceAgc.sprx` (firmware 13.60, read from disassembly) all six write a SET_UCONFIG_REG to 0x342 (tag `0xCC000000 | stream`, or `0xCD000000 + workload * 32 + stream` for complete) and a WRITE_DATA to 0xC343, and active and complete add an ATOMIC_MEM (header `0xC0071E00`) on the stream's 64-bit mask. [Measured on a PS5](https://gist.github.com/orezraey/befe7632b827421988b47a6c04e89e1d) (firmware 13.60): WRITE_DATA dwords 5 to 8 are `0x06010000`, `0xC343`, 0 and the mask's high half (0 for inactive); the header low byte is 0x04 on the DCB SET_UCONFIG_REG and WRITE_DATA and 0x00 on the ACB ones; the table was at `0xFE003A000 + 8 * stream`; the pointer passed to `sceAgcDriverRegisterWorkloadStream` is never written
- [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
- [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) (libSceAgc) - non-sample-dump event types (any event other than 0x38 and 0x39) emit a 2-dword PM4 packet 0x46 without address dwords; the address argument is ignored for these events. Void Terrarium (PPSA03061, #5055) passes a non-null address buffer (0x00000002d2a10000) with event type 0x16 (cache flush).