On Windows, agc_shader_helpers returned from main with the driver's
threads still running. ExitProcess ends them first, then the DLLs'
static destructors run and wait on what those threads left locked:
the async log's queue mutex in Driver::stop, DeviceUseGate's condition
variable in ~Driver, and condition variables in libkernel's statics.
The test hung after printing its result in about one ctest run of
three on a Radeon 860M (#6791).
Like the other driver tests, it now runs LibcRunShutdown_nid_postfix
before returning, which stops the driver while its threads are alive.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reads the root at its byte offsets, walks the type table as key and
bank-index pairs and checks that the MRT0 key leads to the sixteen
colour-target registers: block 78 of the table sceAgcGetRegisterDefaults
returns, block 73 of version 10, and a valid block in every version from
4. Fails on the compact entries and on the version 0 legacy table.
The legacy call takes no version and answered with the version 0 table.
On a console it returns the running system software's table: 128 keyed
entries over 79 context blocks on 10.x and 137 over 84 on 12.02 and 13.x,
as ps5-vulkan measured and relies on. Version 0 holds 150 entries and its
MRT0 block starts at 0x318 followed by 0x319, so PPSA99011 refused it
even with the types fixed. Return the version 13 table, which is the
137-entry one.
The defaults root exposed the compact 12-byte type entries, whose second
word also packs each block's register count and stride above bit 10.
Titles read the type table as 8-byte {key, index << 2 | bank} pairs, so
PS5 FSR4 Showcase (PPSA99011) found the MRT0 key at index 529482 and
refused to build its compose pass. Build the pairs once per table and
check that every index lies inside its bank.
sceAgcCbSetShRegistersDirect and the other register list writers threw on
a count of 0 although the matching size functions report 0 words for it.
PPSA05684 calls the writer with an empty list at its main menu and
terminated. An empty list now writes no packet and returns the cursor,
as its size says; a null buffer still throws.
AI-assisted: yes
The names hash to the NIDs dbOlWdppb4o and vieBRwlh1Lw that the exports
carried as sceAgcUnknownCreateInterpolantMapping and
sceAgcUnknown_vieBRwlh1Lw.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sceAgcCreateInterpolantMapping resolved the pixel shader's ABI, which takes
the process-wide shader preparation lock until the scope ends, and then
waited under that lock for the vertex shader's background registration
preparation. sceAgcCreatePrimState and sceAgcLinkShaders could wait the same
way. Every other shader registration and ABI resolution queued behind that
one compile: on PPSA07411 the lock was held 8 to 10 s of every 10 s, almost
all of it waiting for registration preparations, while its loader threads
waited 13 to 45 s per 10 s to take it.
A preparation scope now names the shaders it resolves and waits for their
registration preparations before it can take the lock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sceAgcFuseShaderHalves copied both halves' code, the fused header and its
tables into the caller's memory and registered the program at that
address. PPSA02100 fuses different halves into the same memory a few
calls apart, while draws that use the earlier result are still queued.
The registry keeps one program per code address, so those draws were
compiled from the later program: its code read a user-data word the
bound layout does not have, and the device reported "BDA access failed"
for the mesh stage.
The PS5 5.02 module writes only the back half's register list into that
memory (sceAgcGetFusedShaderSize reports eight bytes per register,
alignment 4) and leaves the code where the halves are.
The fused code, header and tables now live in memory libSceAgc owns, one
program per distinct pair of halves, registered once. The caller's
memory receives the fused register list and the size query reports that
list. A second fusion of the same halves returns the same program; halves
whose contents changed get a new one.
The driver looked a header up by comparing its fields with the
registered header, including the register list's address. A header whose
register list is a copy elsewhere with the same contents now matches
too, which is what the title passes after a fusion.
sceAgcSetAmmSemaphoreMemory(memory, sizeInBytes) registers the AMM semaphore memory once and zeroes the first 8 bytes of each 32-byte label; sceAgcGetSemaphoreLabel(index, label) writes memory + 32 * index for an index inside the registered memory.
Unblocks New Joe & Mac Caveman Ninja (PPSA02801) which imports both symbols. Matches OpenAGC and sharpemu agreements on signatures, 16 KiB alignment, 32-byte stride, and zeroed label counters.
Report the 8-byte GS primitive payload when the shader's context registers
set SPI_SHADER_IDX_FORMAT.IDX0_EXPORT_FORMAT to SPI_SHADER_2COMP, and 0
otherwise. The search runs over every context register, so an earlier
SPI_SHADER_IDX_FORMAT entry with another format does not end it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sceAgcDcbSetWorkloadStreamInactive and the three ACB workload builders
(sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete,
sceAgcAcbSetWorkloadStreamInactive) threw for every call, and
sceAgcDcbSetWorkloadsActive read the workload ids before it checked the
stream.
The prosper console oracle (mattias800/prosper df074498,
prosper/tests/data/console_oracle/agcbuild.golden.tsv lines 48-56 and
196-204) measured all six calls on a PS5 with three argument patterns
and no workload stream registered: each returned null and wrote nothing.
Pattern b passes stream 1 with the workload ids at address 2, so the
console checks the stream before it reads the ids; here that call read
address 2 and crashed.
The four builders now return null for a null command buffer or a stream
that is not registered, and still throw for a registered stream: the
packet a registered stream gets is not measured, KytyPS5 has no body for
them, and its IT_NOP layout for the DCB calls differs from the
SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence OpenAGC
(3c7e8072, src/agc_workload_packet.c) derives from the driver. The
signatures are OpenAGC's DCB ones (src/game_compat.c), which says the
ACB calls use the same driver builders. sceAgcDcbSetWorkloadsActive now
checks the stream before the ids, as OpenAGC does. Recorded in
docs/dev/debt/libSceAgc.md.
agc_commands gains testWorkloadStreams: the three measured patterns for
all six calls (null, buffer untouched), an exhausted and a null buffer,
and a registered stream that throws without touching the buffer. On
upstream/main it fails with "sceAgcDcbSetWorkloadStreamInactive not
implemented", and the DCB active pattern b alone segfaults reading
0x2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apple clang ships libc++, which differs from libstdc++ where the sources
relied on it:
- std::atomic<std::shared_ptr<T>> is not provided. AtomicSharedPtr is that
type where the library has it and a mutex-guarded pointer with the same
member functions elsewhere; the AGC driver uses it.
- std::atomic_ref<const T> is not provided; the page span reads go through
a non-const reference.
- std::filesystem::file_time_type::clock::to_sys is not provided;
std::chrono::file_clock::to_sys is.
- std::max and std::min do not deduce across the integer types of the
guest range arithmetic, which is spelled with an explicit type.
- <string> and <initializer_list> are included where the sources and tests
use them.
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>
sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete threw. They now write a plain two-dword NOP and keep no workload state.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Hollow Knight Silksong (PPSA12544) imports libSceAgc NID vieBRwlh1Lw
(#699). Its name is unknown: it is not in aerolib, and the
sceAgcUpdateInterpolantMapping label other projects give it hashes to
SbuY2jN+axQ.
OpenAGC (src/game_compat.c at 3c7e807, sceAgcUnknownVieBRwlh1Lw) reads
it from FW 11.60 as the dbOlWdppb4o transform with the same (regs, gs,
ps) arguments, without the identity tail: it writes one register per
pixel shader input, none for a pixel shader without inputs, and rejects
a null pixel shader. It now shares the dbOlWdppb4o path and throws for
the rejected arguments.
For is_f16 1 and 3 inputs whose default_value and default_value_hi
differ, OpenAGC takes DEFAULT_VAL from default_value_hi, while KytyPS5,
sharpemu and dbOlWdppb4o here take it from default_value; these inputs
throw. On every other input the dbOlWdppb4o values equal OpenAGC's.
agc_interpolant_mapping runs the dbOlWdppb4o vectors and OpenAGC's
enhanced-mapping vectors through it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
EVO Player (PPSA99039), Jelly5 (PPSA99505), Emby5 (PPSA99515) and
Nuvio PS5 (PPSA99997) stop on main 0f863c10 with
what(): sceAgcCreatePrimState: invalid user configuration register offsets
All four build their shader headers by hand (EVO-PLAYER-PS5
evo_agc_shader_header.c, ported from ps5-xash3d-halflife) and store the
ge_user_vgpr_en special at 0x25c (GE_USER_VGPR1, value 0) instead of
0x262 (GE_USER_VGPR_EN), as analysed in #3457. They run on the console.
OpenAGC's reconstruction of the FW 5.50 sceAgcCreatePrimState
(src/game_compat.c at 3c7e8072) copies the four special register pairs
verbatim without checking their offsets; KytyPS5's offset checks
(src/libs/agc.cpp at 16a7fe68) are EXIT_NOT_IMPLEMENTED guards. The
special is now copied at the offset the header stores, not moved to
0x262; the driver reads neither register. GE_CNTL, VGT_SHADER_STAGES_EN
and a used VGT_GS_OUT_PRIM_TYPE are still required at their own
offsets because the driver reads them there, and the error now names
the special and the offset found.
With the change all four titles get past sceAgcCreatePrimState and stop
in the shader recompiler (GetImageResource dword 3 is not a valid
runtime value), as #3457 reported for EVO Player.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit aeaa5080d9595305aa73e74d966f989c88b79839)
sceAgcDebugRaiseException (NID T6xuVw0KUJo) now logs the exception id
with APS5_LOG_OUT and returns 0, as libkernel's
sceKernelDebugRaiseException does.
Palworld PPSA23126 (#2598) and 007 First Light PPSA11386 (#2523) import
it; neither is shown calling it.
OpenAGC 3c7e8072 (src/game_compat.c) returns 0 and says the function is
a no-op on retail hardware; the ud2 trap it describes for development
hardware is not reproduced. KytyPS5 6f0cddd4 (src/libs/agc.cpp) takes a
uint32 exception_id, logs it and returns 0. OpenAGC declares no
arguments, but a body that only traps reads none, so the uint32 is kept
and the conflict is recorded in TechnicalDebt. The aerolib name is from
zecoxao/sce_symbols 2883963a; ps5-payload-dev/sdk f5d40f02 has only an
untyped stub.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The builder threw on a null indirect base, while the PM4 executor accepts
SET_BASE with address 0 (it only requires the alignment and address bits) and
an indirect draw that later reads the base is checked there. Titles clear the
base with 0, as with sceAgcDcbSetIndexBuffer, which already accepts 0 to
unbind. A non-null base is still checked for 8-byte alignment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>