Every run recompiled every shader: the recompiler's variant cache and
the device's VkPipelineCache only lived in memory, and the first frames
of a title spent hundreds of milliseconds per large program in the
front end, the emitter and the optimizer.
Compiled variants (the SPIR-V plus the compiled shader info and binding
allocation that per-snapshot binding population needs) are now stored
on disk, one file per key, written atomically by a background thread,
and a later run loads them instead of compiling. The key holds every
input the in-memory variant cache matches on: the in-memory recompile
key (stage, code hash, stage registers, user data count, target), every
code word, the host subgroup size, the binding layout, the resource
specialization, and the values of the APS5_* switches the recompiler
sources read (found at build time; switches that only report are left
out). Entries live under a directory per source version, a build-time
hash of the recompiler and AGC driver sources and the build
configuration, and carry their key and checksums: an entry whose
format, version, key, size or checksum does not match, or whose payload
does not decode exactly, is ignored and recompiled. Directories no
build used for two weeks and stale temporary files are removed. Requests
under the debug probe bypass the disk cache.
The device's VkPipelineCache is loaded at creation when the file's
checksum and Vulkan header match the device (vendor, device and
pipelineCacheUUID; the driver version is in the file name), saved every
10 s while it grew, and saved at teardown.
Location: a shader_cache folder beside the running executable (not the
working directory), so the relinked game stays a self-contained folder;
ANYPS5_SHADER_CACHE_DIR overrides it. ANYPS5_NO_SHADER_CACHE=1 turns both caches off; the AGC
tests run with it set so they never write to the user's cache.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01ButHENCbXXt8h4EZuWUabJ
NVIDIA's compiler inlines every call of the BDA lookup functions, each with its binary-search
loop, and then spends minutes compiling shaders with a few hundred guest memory accesses. Marked
DontInline, they compile in seconds. Pipeline creation without the driver cache, NVIDIA RTX PRO
6000 Blackwell:
| Shader | SPIR-V words | Inlined | DontInline |
|--------------|--------------|----------|------------|
| 0x25b1e8600 | 280168 | 104.4 s | 4.8 s |
| 0x7ff6df189d00 | 95698 | 86.3 s | 1.5 s |
| 0x25b27ce00 | 39959 | 6.0 s | 0.45 s |
| 0x25b27e400 | 40640 | 6.6 s | 0.48 s |
| 0x7ff6df2a8000 | 69179 | 5.7 s | 1.1 s |
| 0x7ff6df253400 | 90491 | 4.1 s | 0.74 s |
| 0x7ff6df256700 | 92459 | 4.0 s | 0.69 s |
The menu renders 3311 frames in 120 s against 3325 without the change; the story mode load takes
the same time and GPU dispatch time in both builds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 7615af4d4dd31581f52aa5d88118c73435fd6ae9)
As on FreeBSD, a munmap range covering more than one registered allocation (or gaps between them)
is unmapped allocation by allocation instead of throwing "unmap crosses allocation boundaries".
GTA V unmaps such ranges in its Story Mode prologue.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Starting in app0 when the folder exists made the working directory, and with it the tests, depend
on the layout next to the executable. The title opens its files by absolute /app0 paths, so the
fallback was never used: the route to story mode runs the same without it.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
The C11 thread layer (_Mtx_*, _Cnd_*, _Thrd_*, _Xtime_get_ticks, std::_Pad) and the libc additions
_FNan, _FDtest, _FExp, __udivti3, _Throw_Cpp_error, generic_category, localeconv, wcsncpy_s and the
aligned operator new came from the --skip-sce-module route, where our libc stood in for the title's.
With the title's sce_module loaded, its libc.prx exports every one of these NIDs and the relinker
lists the guest modules ahead of the host prx, so the imports never reach our copies. Checked both
by the NID tables (the executable imports them from libc, the guest libc.prx exports them) and by
making the four std::_Pad functions throw: the title boots, reaches the menu and loads story mode
without calling them.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
It checks the Windows formatter (the guest va_list must stay untouched).
On Linux the guest variadic path hands the call to glibc, which advances
the va_list, and snprintf_nid_postfix with arguments on the stack
crashes; upstream never registered this test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
WinLibs puts its own cmake.exe (4.3, OpenSSL) first in PATH and ships no
CA certificates, so file(DOWNLOAD) of the FFmpeg package failed with
curl error 60. SSL_CERT_FILE points it at the bundle of Git for Windows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The MinGW FFmpeg package needs bcrypt, ole32 and user32; linking them
unconditionally broke libSceAjm.native.prx on Linux. The Linux package's
x86 assembly references its constants PC32, which ld refuses in a shared
object unless the archives' symbols stay local: --exclude-libs,ALL keeps
them out of the prx's dynamic symbols.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
sceSaveDataSetParam dropped the params, and sceSaveDataGetParam and
sceSaveDataDirNameSearch returned zeros, so a title that checks the params of
the save it found took its profile for a new one and rewrote it with defaults
on every boot. The params now live in a <directory>.param sidecar next to the
save directory, written by SetParam (mtime refreshed) and returned by GetParam
and by the directory search, with the directory's write time when no param
was stored yet.
The mount point is /savedata<N>, as on the console: a title read its profile
through /host//_sm/0/Profile when the mount point had another shape, and that
path does not exist.
The module logs the first three calls of each entry point, like the core
module; APS5_SAVEDATA_TRACE lifts the limit.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
S_CBRANCH_CDBGSYS, S_CBRANCH_CDBGUSER, S_CBRANCH_CDBGSYS_OR_USER and S_CBRANCH_CDBGSYS_AND_USER
(SOPP 0x17-0x1a) branch only while a debugger sets the conditional debug bits in the wave's
status, which never happens on a retail console, so they decode to one no-op opcode.
mingw's fprintf writes an unbuffered stderr a character at a time: a title skipping many draws
spent seconds per frame printing 17 KB RecompileRequest dumps (GTA V ran at 0.1 fps). Reports are
built into one string and written once; dumps stop after 8 per reason (APS5_SKIP_REQUEST_DUMPS,
-1 for all).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
A float inline constant (4.0, 2.0, -4.0...) read by an f16 operation is the half value in the low
16 bits (LLVM getInlineEncodingV216); it was the low half of the f32 bits, 0. op_sel[0..2] now
selects the high half of v_fma/min3/max3/med3_f16 sources. GTA V's CAS sharpen computed
rcp(1 + 4w) as rcp(0) and stored black.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
sceSysmoduleLoadModule and sceSysmoduleIsLoaded already accept ids the
project does not know and log them; unloading such an id threw, so a title
that loads and later unloads an unknown module aborted at the unload.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SCE_USER_SERVICE_ERROR_INVALID_ARGUMENT is 0x80960005; 0x80960002 is
NOT_INITIALIZED (the neighbouring NO_EVENT 0x80960007 and BUFFER_TOO_SHORT
0x8096000A already match the real sequence). The privacy getter returned the
raw value and now uses the constant.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Since direct memory mapped at two addresses is shared read-only instead of
refused, the guest memory test expected an exception that no longer comes.
It now maps the second view, reads the first view's bytes through it, and
writes through the first view once the second is gone, which faults unless
its protection came back.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Only the lines this branch added lose their comments; the commits before
keep them, with the reasoning behind each change, for whoever digs into
that code later.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An address-based build maps every readable registered range. A heap
range past APS5_HOST_IMPORT_MIB (or refused by the driver) was copied by
every such build: about 3200 ranges, 670 MiB and 500 ms per build on the
story-mode load, which then stalled. Such ranges are now served by
persistent host-cached mirrors, as the main image is, and the load
reaches its next failure.
- a heap mirror keeps no shadow; it keeps a write-tracking generation
per 64 KiB block and a build copies only the blocks stamped since. The
generation is stored by the copy, so a build that throws before it
leaves no block marked current with stale bytes
- the build gathers its heap mirrors, sorts them and collects each run
of adjacent ranges once (a collect costs about 4 us whatever its size;
~3000 mirrors took 16 ms per build, now about 3 ms)
- writable ranges are mirrored too: each build keeps the bytes its merged
written ranges held at upload and write-back stores only what the GPU
changed from them, so a CPU store made meanwhile stays; recorded work
writing a range completes before a refresh reads it
- the mirrors are limited by APS5_HEAP_MIRROR_MIB (24 GiB by default); a
mirror past it, or one whose buffer the driver refuses, stops the
process. Heap mirrors are not given to descriptor sub-ranges, image
mirror serials or reusable direct regions
- past APS5_ADDRESS_COPY_MAX_MIB (64 MiB by default) of committed bytes
still copied, a build stops the process, naming the largest ranges and
why each is copied (unreadable pages, an image mirror the driver
refused, a range outside the write-watched arena, mirrors disabled)
- a direct memory page mapped at several addresses is read-only in every
view while its range stays writable: copies, descriptor regions and
mirrors take every readable page, write-back stores the writable ones
and fails on a GPU change inside a read-only page instead of dropping it
- a store through the BDA table into a mirrored or copied range faults
as before (only writable ranges imported in place are writable there);
the fault now names the table range and that rule
- the BDA resource tests run with a storage range limit that holds the
fault buffer and cover a mirror's fill, reuse, per-block refill, an
interrupted refresh, a descriptor store beside a concurrent CPU store,
and the copy limit
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both suites stopped at their first stale expectation, so nothing after
it ran; the BDA device tests did not link.
- the BDA device tests define the one recorder entry Resources.cpp
reaches (no recorder) and fail loudly on the others
- Buffer::Invalidate refuses memory that is not host mapped again (lost
in 8a69fef); the device tests expect it for a device-local buffer
- APS5_PIN_WAIT_MS (60000 by default, unchanged) bounds the wait before
a mutation of a leased range is refused, and the refusal names it; the
graphics tests set 200 ms, as they expect about a dozen refusals
- the expectations follow the behaviour the code chose since: a buffer's
four-DWORD V# is refused as a T#, a sampler exceeds the mock's zero
sampler slots, a V# at unmapped memory binds its committed pages only,
a rect-list fragment input without a vertex export reads as zero,
detiled rows span the padded block width, mip ranges are checked in
address order, block-compressed and 128/129/132 formats use render
target tiling, an XOR swizzle is refused only over a base that is not
64 KiB aligned, DCC and partial mip chain views decode, a cube reads as
a 2D array, the sampler fixture's max LOD fits the 4.8 field, a BDA
program with barriers is accepted, a tessellation control stage is
refused, a corrupt request fails on its signature and a newer one on
its version
- the DCC comment in the texture decoder no longer claims metadata
compression is refused
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A title libc from sce_module calls it from __cxa_throw/__cxa_rethrow when
no handler is found, right before std::terminate, with the thrown object,
0x400 (meaning unknown), the demangled type name and what() (or null).
The console keeps it for the core dump; there are no guest core dumps
here, so it is printed to stderr. It was the only import such a libc
needed beyond the existing libs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
--spv saves <request>.spv in the working directory for spirv-val; the usage
line lists --asm and --mem too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SPIR-V wants the back-edge block to structurally post-dominate the continue
target, but the continue target is the guest latch, and a BDA access among its
instructions returned from the invocation on a fault: spirv-val rejected
such a loop ("The continue construct ... is not structurally post dominated by
the back-edge block"), which only builds without SPIRV-Tools accepted. While
the continue target is emitted, a BDA fault is recorded without stopping the
invocation, as in a program with wave LDS barriers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every ray query returned a miss (a stub). The node test is now the GFX10
(RT IP 1.1) one AMD's GPURT software fallback defines (IntersectCommon.hlsl)
with the TriangleNode1_0 and BoxNode1_0 layouts, as SharpEmu ports it: the T#
(base, size, box grow and sort, triangle return mode) and the node pointer
are run-time values, triangle nodes return t over its denominator with the
barycentric numerators or the triangle id, float16/float32 box nodes the hit
children (clipped slabs, NaN misses, optionally sorted by entry time); an
inactive lane, a null BVH, an out-of-range node or another type returns four
invalid dwords.
The decoder addresses 11 dwords (8 with A16, the directions as halves), the
translation emits ImageBvhIntersectRay with the T#, MakeImageAddress and exec,
and the program takes the BDA path: one probe covers the node and direct loads
read it (a node the table does not map as one range records an Unmapped
fault); EmitBdaDwordReads can read every dword for the per-byte fallback.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A traversal builds the V# of a bottom-level structure from a pointer it loads
from an instance record, and reads it with s_buffer_load: no binding holds
such a V#, and TakeGpuDescriptor took vector raw dword loads and stores only,
so the resource analysis failed on the SGPR chain.
A ReadConstBuffer through a V# the SRT walker cannot compute is a
MemoryInfo::gpuDescriptor access too: the dword at the V#'s base plus the
dword-aligned offset, read through the BDA page table, zero past
STRIDE * NUM_RECORDS bytes (NUM_RECORDS for stride 0).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
APS5_WATCH_MEMORY takes <hex address>:<hex bytes> ranges separated by commas;
a snapshot holds them back to back, in order, and is kept when any changed.
Each range matches its own heap base above bit 36, and watching starts once
all are mapped.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The decoder puts an instruction's DPP fields on its source, and only a
destination carrying them has writeRawU32 keep the lanes the masks leave out
(and, without bound_ctrl, the lanes with no source lane). destinationOperand
copied them over, but no write used it, so every row was written: a wave64
scan adding the other row's total into the odd rows only (row_mask:0xa) added
it into every row, the prefix sums a key pass indexes primitives by came out
wrong, and the tree built from the keys held cycles its walkers looped on.
As in the Kyty recompiler (writes through DestinationOperand):
TranslateInstruction gives the destination its source's DPP fields once, and
a mac accumulator reads the destination without them (accumulatorOperand).
agc_dpp_write_tests checks a masked DPP add writes through DppUpdateU32.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The loop guard's report names the shader (its code address when its plan
was built, now the translator's shader hash), and LoopGuardTripped() tells
whether any dispatch reported one.
- APS5_WATCH_MEMORY=<hex address>:<hex bytes> (with APS5_SYNC_DISPATCH=1)
keeps the last 64 contents of a range with the dispatch that left each, and
writes them to watch_<n>_<program>.bin when the loop guard first trips; the
address matches any heap base above bit 36.
- agc_shader_replay --mem prints the guest buffer V#s and which the shader
writes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Operand reads already extract SDWA source fields for every instruction, so
bit reversal and find-first-bit-high only need the decoder rule, as for
V_FFBL_B32. The rejection names the selector, the opcode and the pc.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A buffer load with GLC/DLC bypasses the caches another workgroup's stores
may be stale in; a radix sort polling a partition's published prefix with
one read stale flags on the host, handed overlapping ranges to two
partitions, and lost half its keys. A tree built from the duplicates held
cycles, and the shaders walking it looped until the loop guard stopped them.
As in the Kyty recompiler (GLC/DLC buffer cache policy support): the decoder
reads DLC on MUBUF/MTBUF, a buffer load or store with GLC or DLC (atomics use
GLC only to return data) is MemoryInfo::coherent, the guest storage buffers of
a program with one are decorated Coherent, and its accesses are Volatile.
Through the BDA table (GPU-selected V#s) the loads and stores are Volatile.
Co-Authored-By: Claude Opus 5.5 <[email protected]>