verifySelectedRecordDirectOperations plans image_store, image_atomic_add,
image_load_pck and image_load_by2 through a heap T# keyed by an SRT value,
which must be bound directly, and through a T# keyed per lane, which must
stay a table with its unsupported mark. A T# that is both sampled and
stored is bound directly for both, and one that is only sampled still
selects its record.
RunSelectedStoreTests stores through a record keyed by an SRT value on the
GPU and reads the texels back. Before the fix both failed: the T# was
planned through an image table, and the store faulted as an unsupported
operation.
An image table cannot store, update atomically or read PCK and BY2/BY4
through its records (#757): those operations fault and read zeros. #6243
turns a heap T# whose words and record offset the SRT walker evaluates
into a selected one-record table, which kept that limitation although
such a T# is one known descriptor, as #5188's direct images were.
PlanTableColumns now binds such a T# directly, for every operation of the
shader on it, when any of them is a store, an atomic, a packed or a
BY2/BY4 access. A T# keyed per lane, or by a value the walker cannot
evaluate, keeps its table and its unsupported-operation fault. The disk
format moves to 37 so that plans cached by #6243's builds are not reused.
An APS5_EXPORT whose name says the NID is unknown can carry the NID of a
symbol nid_patcher already produces from core/libs/prx: a _nid_postfix
helper, an APS5_VABI export or an assembly function. The NID is then not
unknown, and the stub can take the place of that symbol. #5608 exported
YNw9Xk5iNWo from libSceVideoOut as a throwing stub; it is the NID of
MouseIsOpen_nid_postfix, which libSceVideoOut imports from libSceMouse,
so presentation failed with "YNw9Xk5iNWo not implemented" (#5738, #6245).
The unknown-nid rule reports an added Unknown export with the NID of a
symbol in core/libs/prx, and an added symbol with the NID of an Unknown
export. Named aliases, such as the libc and libSceAmpr exports that share
NIDs on purpose, are not checked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The test imports ranges just above udmabuf's size limit and keeps the first
one live. With size_limit_mb=8192 each range is 8257 MiB, so on a host with
32 GB of RAM the default budget (half the physical memory) refuses the second
import and HostImportFor returns nullptr instead of reaching the sparse path:
"a failure at UDMABUF_CREATE after the sparse path was chosen did not throw".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A kept column the runtime cannot vouch for is read and compared with its kept words. When they
differ, the fresh walk read the same runs again, so a capture that follows every change to a table
read each word twice: 1,048,576 words per capture for a 65,536-record column, 12% slower than
before the reuse with the write watch and 16% slower outside it.
The compare now leaves its words with the snapshotter, and the fresh walk takes them instead of
reading when its plan has the same runs: words the capture has already read read the same again.
Such a capture reads each word once, as before the reuse, and the 65,536-record column costs 7%
(12% outside the write watch) more than before the reuse; the rest is keeping the column. With 16
distinct T#s it costs 5 to 8% less. agc_shader_table_snapshot_reuse expects a changed table to be
read exactly once per capture.
Every read and store through GuestMemory calls the flush hook first. Recorder's FlushForAccess waits
for recorded GPU work over the range, records a queued label that writes it, publishes the unit
shadows over it and stores the storage image results kept on the GPU for it. A storage image result
is stamped only when it is stored (its write-back's MarkWritten), so ShaderMemory's stamp and check,
which never read the column, could vouch for words a read would have replaced: the column skipped
its read and the snapshot kept the T#s from before the result.
stampWrites and unchangedSince now call GuestMemory::FlushGpuWrites over the range after the pending
write query and before the tracker, as a read does, and as the dispatch cache stores pending images
before it validates an entry. The stamp then covers what the flush stored, so the capture after it
skips the read again.
agc_shader_table_snapshot_reuse installs a flush hook that stores a new T# into the table on its first
call over it. Without the flush in unchangedSince the reused snapshot keeps the old T#; without the
one in stampWrites the next capture compares the table instead of skipping its read.
agc_shader_table_snapshot_reuse (agc_shader_memory_tests --table-snapshot-reuse) captures a
4,096-record image table in write-watched guest arena memory and checks each capture's snapshot and
captured words against the table:
- an unchanged table is reused without reading a word, and its snapshot equals the first one;
- a CPU store, a driver store (GuestMemory::Write, as a CP write or a DMA copy or fill makes it), a
GPU result stored by the driver (StoreOwnBytes) and a pending GPU write with a known value make it
read again, and the new T# is in the snapshot;
- a MarkWritten stamp (a GPU write into an import), a pending write the capture syncs on and a guest
mapping change make it compared again;
- after the reuses, the map selects each key's record;
- a table outside the write watch is compared and reused, and a store into it is seen;
- the memo keeps at most 16 columns.
Without write watching, the captures of an unchanged table expect a compared reuse instead.
Every capture of a shader with an image table read every record of each column and looked each
record up among the snapshot's distinct descriptors. A 32,768-record column of distinct T#s cost
6.1 ms per capture here (2.3 ms with 16 distinct T#s), and Resident Evil Village's title captures
such columns on nearly every draw.
TableSnapshotter now keeps the last 16 column snapshots per thread (at most 4 Mi words in all,
least recently used first out), keyed by heap base and size, record shape and key count: the runs
it read, their words, each record's state and code, and the distinct T#s. A narrowed, selected or
pointer-indirect column, or one with an unmapped record, is not kept. A capture of a kept column
takes the codes and the distinct T#s from it with no per-record work. When they are the snapshot's
first words they are taken as they are, and the words index is built only if a later column needs
it, so the result is the snapshot a fresh walk would produce, codes and words order included.
The runtime gets three optional callbacks: stampWrites gives a stamp for a range before any of it is
read, unchangedSince tells whether nothing wrote the range since, and supplyWords hands words that
equal guest memory to the capture as read, so they stay in the captured regions the draw cache
compares. ShaderMemory implements them with the write tracker. The stamp is CollectWrites's
generation, refused when this ShaderMemory already fetched a page of the range or recorded GPU work
writes it. A range is unchanged while no block of it is stamped after the stamp (collected CPU
stores, the driver's Write, WriteChanged and StoreOwnBytes, MarkWritten for GPU writes into
imports), no recorded GPU work writes it and the guest allocation generation is the stamp's. When
every run is vouched for, the column is not read at all; otherwise its runs are read and the
snapshot is reused only when they are equal word for word.
The 32,768-record column now costs 0.48 ms per capture with no word read when unwritten, and about
0.9 ms outside the write watch. APS5_NO_TABLE_SNAPSHOT_REUSE=1 turns the reuse off. The [image-table]
profile line counts the reuses, and ResourceMaterializer::TableReuse returns them with the words the
snapshotter read.
A driver store and an own store across a watched block and an unwatched
one must stamp the watched block and no other. On Linux, a store across
the halves of a split block must stamp it, and a store into its unwatched
half alone must not.
MarkWritten and StoreOwnBytes return 0 for a range that is not wholly
watched, and they stamped none of its blocks. The watched blocks such a
store wrote kept their old stamps, so UnchangedSince and StoredOver over
them still read as unchanged after a GPU write that crossed into an
unwatched import. A vouched shader table column or a cached range then
kept bytes that had been replaced.
Both now stamp each block whose part of the store starts or ends in
watched memory, as the wholly watched path stamps every block. They still
return 0 and note the store as unstamped. This costs one or two coverage
lookups per block, and only on this path.
A pixel that PA_SC_AA_MASK leaves uncovered is demoted to a helper
invocation so the covered pixels keep their derivatives. When the guest
exports under an EXEC mask that clears that lane, the colour output is
never written. On the Radeon 860M (Windows driver 32.0.13058.8001) such
a demoted invocation still writes undefined colour to the target, so
agc_driver_quad_pixel_mask fails at its first implicit-LOD sample under a
partial mask (2). Killing the uncovered invocations at the return, after every
derivative is taken, discards them on every driver.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
MinGW's minwindef.h defines near and far as empty macros, so a helper
named near stops compiling in any test that comes to include windows.h.
The project keeps those names out of its code.
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.
A read-only V# across two imports into unregistered memory, a read-only and
a written V# in that memory: UploadPrepare merges the three into one copy
that begins inside the first import. The written V# maps its own bytes in
that copy, counts as a copied write, and WriteBack lands its store on its
bytes alone.
The test of two read-only V#s merged over three imports now asserts that
its second build hits the cached address space without a dissolve and that
the third leaves it only for the import change. One crossed piece is
writable, so a join of the crossed run cannot erase the overlap the test is
about.
commands[0] is submitted twice (writtenAfterSecondSubmit); the flag made the validation layer report a second submit of a one-time buffer.
Co-Authored-By: Claude <[email protected]>
The second queue that settles imports is always created on Windows, and a device without one keeps
the comparisons. The other-family settle path is tested through the recorder test device built
with its settle queue on another family (agc_import_watch_other_family) instead of
APS5_SETTLE_OTHER_FAMILY.
The settle read keeps a private import's window open for one round trip on the settle queue, so a
store from another thread can land inside it. CheckImportWindowOtherThread: a store of new bytes from
another thread within the window counts as written, and a same-value one keeps its possible CPU
touch. The debt entry notes the longer window.
Review on #6060: the Windows probe now settles a 4,096-page import with the same 4-byte read
production uses and requires no marks after it. A failed settle destroys the import instead of
leaking it. The settle's destination, command buffer and fence are created once per device. Where
the graphics family has a single queue (AMD on Windows), the settle queue comes from another family
that copies, through a buffer aliasing the import's memory. The recorder tests' device gets the
settle queue too, and agc_driver_layered_volume_submit_other_family runs that path
(APS5_SETTLE_OTHER_FAMILY=1).
On Windows a host import without a section alias left guest write tracking, and its changes were
found by comparison. Since resident direct memory became private MEM_WRITE_WATCH memory, its
imports take that path, and every storage-texture refresh over it compares its whole snapshot.
ProbeImportWriteProtection gets a Windows body on MEM_WRITE_WATCH scratch memory, and
decideImportWatch keeps direct imports of private read-write memory watched when GetWriteWatch
stays honest after an import's first GPU use. The driver marks every imported page written until
that use completes, so each such import is settled inside ImportWatched's window by a 4-byte read on
a second queue of the graphics family (the imports lock comes after GpuMutex, so the main queue
cannot be used there). APS5_NO_WATCHED_PRIVATE_IMPORTS=1 keeps the comparisons.
Two writable 64 KiB pieces and a read-only one, one import window each,
with a read-only V# across the boundary: through the kept address space, a
hit on it and a dissolve after UploadPrepare, the BDA table has no
overlapping ranges, stores up to the boundary resolve to the writable
import and reads past it find the read-only one.
A read-only V# that crosses imports of the cached address space is bound
through a copy of its bytes, and that copy can begin inside an import. The
BDA table listed the copy and the space's ranges by begin, and a shader
takes the last range beginning at or below an address, so the bytes from
the copy's begin to the end of that import resolved to the copy. The copy
is read-only, so a BDA store or atomic into a writable import faulted with
Permission, the GPU side of the rule owner() no longer applies on the CPU.
The table now gives every byte one range: the space's ranges, or after a
dissolve the direct and mirrored regions, keep the bytes they hold, and a
build's copies keep only the bytes nothing live holds.
The ordered count's amount was shuffled from the first half's value of
the first active lane's host lane, so a wave64 program whose active lanes
were all in lanes 32 to 63 added the inactive lane's value from the lower
half. Read the amount through the context shuffle, which selects the half
the first active guest lane lives in, as v_readfirstlane_b32 does.
The GDS test now masks the ordered count to the upper part of each wave,
including a wave64 whose first active lane is lane 40, prefills the
result register to check that inactive lanes stay untouched, and keeps
the M0 cases of the counter addressing.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_019hfwvntiwabeqdZ6avJssk
Once every queue has been idle for 50 ms, the queue 0 worker stores
pending storage image results on its own (#3777, #4410). That store runs
at no ordering point of the guest and took no lease of the guest
allocations, so a guest unmap could run alongside it. The compare of
memory outside the write watch (StorageTexture::compareUntracked) then
read memory the unmap had released: the driver forgets its cached page
states only when the unmap's registry mutation ends, after the memory
is gone, so the range still read as mapped.
The idle store now holds a lease (GuestAllocationsAcquire) under the GPU
mutex, the order the other lease holders in the driver use. An unmap
waits in GuestAllocationsRequireUnpinned until the store is done, and a
store that starts during an unmap waits for the mutation to end, finds
the range no longer registered and drops its results.
ProsperoEden (PPSA99008, homebrew catalog, run without app0/eboot.bin)
closes its launcher, its GL driver unmaps its direct memory at
0x200000000+0x14000000 while both 1920x1080 color buffers there still
have results pending, and the queue worker faulted in compareUntracked
under FlushAllPending("idle"):
FATAL: unhandled exception 0xc0000005 on thread 69080 'queue worker 0x0'
read of 0x0000000200000000
On main d92a8dad3 that happens in 3 of 3 cold runs. With this change 6
of 6 runs exit with code 0 and log "storage image 0x200000000+0x10e0000:
memory no longer registered; 1 pending ranges dropped" for both images.
New test agc_driver_idle_store_over_unmap unmaps an untracked arena
surface with a pending result and releases the GPU mutex while the unmap
is in progress. Without the lease the queue worker faults at the same
compare (3 of 3); with it the result stays pending until the unmap ends
and is then dropped.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
ClassifyIndirectDraw sent every indirect rect-list draw to the CPU path
(non-vertex path), which reads the records on the CPU. When the GPU writes
those records earlier in the frame, the read waits for every batch up to
the producer: in Kena (PPSA01802) each such DRAW_INDIRECT waited about
15 ms, about once per frame.
A rect list on the vertex path is drawn by its generated tessellation
stages from three-vertex patches, with no draw-dependent constants, so the
records can be consumed on the GPU like any vertex-path draw. The
classifier now keeps a rect list without mesh or tessellation stages on
the GPU path, and Graphics::Draw accepts it. APS5_NO_RECT_INDIRECT=1
restores the CPU path.
agc_driver_rect_list_indirect submits a direct rect-list draw and the
same rectangle from a DRAW_INDIRECT record and requires the same bytes,
and a record without vertices to draw nothing; it passes on either path.
agc_driver_draw_offset_indices requires an auto indirect rect-list record
whose packet names no offset registers to be classified to the GPU.
Co-Authored-By: Claude Opus 5.5 <[email protected]>