Draws with CB_COLOR_CONTROL MODE ELIMINATE_FAST_CLEAR (2) or DCC_DECOMPRESS
(6) were rejected as "only normal color rendering with copy ROP". On
hardware these full-target blits write the fast-clear value into the
fast-cleared pixels (or store the pixels uncompressed); the shader's output
never lands. In this driver's DCC model (surfaces stored uncompressed,
uniform fast-clear keys make every read return the clear value) that is:
- keys that are a clear code: the target's texels become that value and
its keys read uncompressed. Reads see the same values before and after.
- uncompressed keys, or a target without DCC: nothing changes.
- mixed keys (per-block metadata), a mipmapped DCC target (its keys are not
decoded) or a register clear of a texel over 64 bits: throw.
The texels go into the target's resident image (its refresh already
cleared it to a code's value; the register code is cleared on the GPU with
FillClear) and reach memory through its write-back, or into guest memory
on the CPU when there is no resident image. A resident image made under
other DCC keys is remade under the target's keys by the storage cache
before the pass reads it. DecodeColorMetadataPass
requires the draw to be only that pass: copy ROP, no depth or stencil
work, no multisampling, a viewport and scissor covering each target.
Indirect passes throw. The CB_COLOR decode moves into DecodeColorBuffer,
which also reads CB_COLOR_CLEAR_WORD0/1.
The register clear code (DCC key 0x20) had no value before: its keys were
read as the stored texels. A target under register keys now reads as its
CB_COLOR_CLEAR_WORD texel: a resident target is cleared to it before a draw
renders into it (then its keys read uncompressed), and a non-resident
target's copy starts from it.
Every decision that materializes a clear reads the keys through
CurrentDccKeys, which first waits for the batches up to the newest recorded
writer of the key range. The title's fast-clear fill of the metadata can
still be pending on the GPU when the next draw is decoded; reading the
stale bytes there made a later draw clear the target over the results of
the earlier ones.
The remaining CB modes (resolve, decompress, FMASK decompress) keep the
rejection, which now names the mode.
A shader sampling with MIMG DIM 2D over a T# of type 2D array was
refused: "guest texture dimension disagrees with the shader's declared
image shape (shape 1, dimension 2)". The access carries no slice
coordinate, so the hardware samples slice 0 of the resource view, the
BASE_ARRAY layer, as it does for the storage access of the previous
commit.
A 2D array Texture now also makes a 2D view of that layer
(Texture::FirstLayerView), whether it holds its own snapshot or views the
surface's storage image, and the sampled binding uses it for such an
access. Every other shape/dimension mismatch still throws.
A compute shader addressing an image with MIMG DIM 2D over a T# of type 2D
array was refused ("guest storage texture dimension disagrees with the
shader's declared image shape"). Such an access carries no slice
coordinate, so the hardware reads and writes slice 0 of the resource view,
which is the BASE_ARRAY layer of the surface.
The storage element is now bound through a 2D view of that layer of the
surface's shared storage image (StorageTexture::FirstLayerView, created on
demand per mip and released with the image). Every other shape/dimension
mismatch still throws, and both mismatch messages now name the shape and
the dimension.
The recorder test device sets formatProperties, which storage images need
to pick their Vulkan format.
A guest buffer view off minStorageBufferOffsetAlignment is bound from the
aligned offset below it, and the difference goes into the shader's memory
byte offsets (3dec6ced), but only when those live in the push constants:
a shader whose shader data is a buffer failed with "a guest buffer off the
storage buffer offset alignment in a shader whose shader data is a buffer
is not implemented".
The adjustment now goes into the shader data buffer's byte at
memoryOffsetDword * 4 + element, and a template's data refresh writes the
dispatch's words with the same patches applied.
A buffer fill that starts at a surface's DCC keys and covers at least its key range (titles fill
the padding too) is a key fill. A uniform key fill leaves the image's texels and pending results
in place and records the fill's code: a sampled read through the surface's keys binds the clear
value, a color target through the keys starts from it at its next refresh, and reads that bypass
the keys still see the texels. Key scans return the fill's code until its write lands, and a
write-back of results drawn before the fill keeps the keys holding that code. The immediate clear
of the covered images is opt-in (APS5_KEYS_FILL_CLEAR=1): it fixes titles whose sampled reads
reach the pending image before a refresh, at the cost of reads that bypass the keys (Astro Bot's
title logo, reported by @oneandonlydean).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
T# word1 MIN_LOD (unsigned 4.8, in levels of the whole surface) was
rejected. Sampled views now chain VkImageViewMinLodCreateInfoEXT
(VK_EXT_image_view_min_lod, enabled when the device offers the minLod
feature). Its minLod counts levels of the whole image exactly as MIN_LOD
counts levels of the whole surface, because the image holds every mip and
the view starts at BASE_LEVEL.
- A clamp at or below BASE_LEVEL cannot change which level a sample reads
and is left off; one past the view's last level is bounded by it (the
view selects that level anyway, and Vulkan requires the bound).
- A clamp that binds on a device without the feature throws.
- The texture cache already keys by the eight descriptor words, so views
with different clamps are not shared.
- Storage views address one level: a clamp at or below that level is
accepted, one above it still throws.
GuestMemory::CollectWrites was built on the Windows arena's MEM_WRITE_WATCH
and answered 0 on Linux, so every cached copy of guest memory (textures,
resident render targets, buffers, DCC key proofs) read as changed at every
check: resident targets were stored and re-uploaded at every refresh and
unit shadows stayed off. Astro Bot's intro ran at ~3 fps.
libc gains GuestWriteWatch, the Linux counterpart of GetWriteWatch. Every
guest mapping libkernel makes (MapAligned: fixed, hinted and aligned) is
registered with one userfaultfd in asynchronous write-protect mode
(UFFD_FEATURE_WP_ASYNC: the kernel lifts a page's protection on its first
write without a handler thread, for user and kernel-mode writes alike), and
PAGEMAP_SCAN with PM_SCAN_WP_MATCHING reports the pages written since the
last scan of a range and protects them again in one call. Unmap forgets
the range. Direct memory is mapped from shared backings over its
reservation and a fixed remap replaces the mapping, so both register the
new mapping afresh. The watch is per mapping: a write through one mapping
of shared physical pages is not seen at another, so physical pages mapped
at more than one address leave the watch in every mapping of them.
A registered mapping reads as written until a collect covers it. The watch
keeps that itself instead of relying on how the kernel classifies pages
never faulted in: Linux 6.17 reports a range without page tables as not
written and leaves it unprotected, 7.2 reports it as written. The first
collect of a fresh range reports it written and write-protects it with
UFFDIO_WRITEPROTECT, which puts markers on its unpopulated pages
(UFFD_FEATURE_WP_UNPOPULATED), so from then on only real writes report.
Fallback: the userfaultfd is opened with UFFD_USER_MODE_ONLY where
vm.unprivileged_userfaultfd restricts it, and a probe at startup checks the
mechanism on scratch pages (a populated and an unpopulated page written
after protection report exactly, untouched ones do not). Without it (kernel
older than 6.7, no userfaultfd permission, headers without PAGEMAP_SCAN, or
APS5_NO_WRITE_WATCH=1) the watch reports unavailable and the tracker stays
unwatched exactly as before: CollectWrites answers 0 and callers compare
bytes. A range with a page outside the registered mappings also answers 0,
and a protection or scan that fails reports its remaining pages as written
and answers 0.
The driver's tracker keeps its contract and generations. On Linux its
block stamps live in 4 GiB leaves over the user address space, made on
first stamp, instead of the arena's flat arrays; the collect memo now works
on both platforms. GuestMemory::WriteWatched and Watched replace the arena
queries in UnitShadowEnabled and ShadowDestinationFor, so unit shadows run
on Linux over watched memory and stay off everywhere else. The Windows
arena path is unchanged apart from going through the same stamp helpers.
Host imports: a driver may lift the write protection of imported pages
itself. amdgpu validates the userptr BO behind a VK_EXT_external_memory_host
import with a writable HMM fault on every submission after the pages were
protected again, so PAGEMAP_SCAN reports them as written with no CPU store,
and a resident display image read as written at every present (found by
Pacuka on an RX 6650 XT with RADV: the presented image went black). NVIDIA
pins the pages once at the import and stays exact from then on. A probe at
device setup decides per device: it maps and watches scratch pages, collects
them clean, imports them, collects, submits a GPU copy that reads the import,
waits, and collects again. Pages written after the submission mean imports
resolve write protection: every import from then on takes its range off the
watch (GuestMemory::Unwatch) before it is made, so Watched is false for it,
CollectWrites answers 0 and callers compare bytes; GPU writes to watched
memory keep their MarkWritten stamps. The result is logged once:
"[write-watch] host imports resolve write protection: yes/no". A probe that
cannot run takes the unwatch side. APS5_WRITE_WATCH_IMPORTS=watch|unwatch
overrides the probe (default probe; anything else throws), and
APS5_NO_WRITE_WATCH=1 still turns the watch off. On the RTX 4090 the probe
reads 0 of 4 pages written after the GPU read (4 after the import itself),
so imports stay watched there.
The import itself lifts the protection once where imports stay watched:
on the RTX 4090 the pinning at vkAllocateMemory reports every imported page
as written (4 of 4 in the probe), with no CPU store. Such a report would
read as a CPU write over any GPU results pending in the range and drop
them. So every import of a watched range is made inside an import window
(GuestMemory::ImportWatched), under the tracker lock: the range is
collected right before the import, so earlier CPU writes are stamped as
usual, and again right after it, and what the window reports is stamped
"maybe written". Readers without pending results see it as a change
(UnchangedSince fails, ChangedBlocks reports it), so caches re-upload or
compare. Decisions about pending GPU results read it as untracked:
ChangedBlocks reports BlockMaybeWritten apart from BlockWritten, and the
drop rule, the write-back's keep blocks, the alias checks, the unit
shadow's stale drop and the whole-image stale checks (through the new
WrittenSince, which also answers false for a range off the watch) store
the results instead of dropping them. A real CPU store racing an import
also races the pending GPU writes to the same memory, which has no
defined order on hardware either, so the GPU results winning is
acceptable. From the second collect on the range is protected again and
watched normally. A range the watch does not cover whole, or a collect of
the window that fails, is taken off the watch instead.
An unwatched import stays off the watch for the rest of its mapping: a
remap registers the range afresh, the import kept over it takes it off
again at the next reconcile, and a re-import after an unmap follows the
decision. Unwatch invalidates the collect memo, so a collect memoized
before the import answers 0. A range that leaves the watch while a storage
image holds a generation for it no longer reads as CPU-written:
ChangedBlocks now tells whether the range is watched, and the drop rule,
the write-back's keep blocks and the alias borrow treat an unwatched range
as untracked (stored, never dropped), as a generation of 0 is.
Tests: guest_memory checks the watch through libkernel mappings (a CPU
store, a read, a kernel write through read(), another thread, a memcpy
across pages, an mprotect cycle, a partial munmap and a fixed remap) and
over a fresh 2 MiB-aligned mapping without page tables, checks direct
memory (watched, an alias takes the shared pages off the watch in both
mappings, a fixed direct remap over flexible memory is watched afresh), and
names the failing line and the reported pages. The recorder tests allocate watched
memory on both platforms, so the UnchangedSince, key proof, close race and
unit shadow tests run on Linux, and the storage refresh test gains a
watched variant: results kept on the GPU reach a CPU reader, and a plain
CPU store into one unit with newer results pending is found by the watch
alone. The recorder tests now also run the real import probe, check the
decision with the result injected (unwatch: an import leaves the watch, a
memoized collect, a partly imported range and the unimported neighbour
answer as they should, MarkWritten stamps only watched memory, a remap and
a re-import after an unmap follow the decision; watch: the import stays
watched and stamped), and check that results pending when their memory
leaves the watch are stored rather than dropped. A further test imports the
memory of a storage image with results pending in watch mode: the
results survive the import window and reach guest memory, a cache
generation taken before the import sees a change, the window reads as
maybe written and not as a CPU store, and a CPU store after it reads as
one again. Unit shadows and the
watched storage refresh report themselves as not run when imports are
compared.
The geometry path's merged ES/GS programs (VGT_SHADER_STAGES_EN.GS_EN)
were compiled as mesh-stage programs whose input metadata was never
filled in, so every such draw failed with "mesh shader translation
configuration is not supported".
Recompiler:
- BuildShaderStageInputInfo takes the draw's MeshConfiguration; the mesh
input helpers (primitive size, step and counts) are implemented.
- The prologue emulates one NGG subgroup per workgroup: s2 GS_TG_INFO,
s3 the merged wave info per wave, the GS vertex offsets (ES thread
index times VGT_ESGS_RING_ITEMSIZE, odd strip triangles swapped), the
primitive id, and the ES vertex and instance ids. An indexed draw's
vertex id is read from the index buffer through a raw V# the driver
puts in hidden user words 4..7.
- s_sendmsg GS_ALLOC_REQ becomes MeshAllocate. The epilogue zeroes the
allocation first, clamps it to the output limits, culls primitives no
thread exported and decodes the 9-bit vertex indices.
- The draw parameters live in the last six push dwords. A BDA fault in
a mesh program no longer stops the invocation (as for other programs
with barriers, the fault is recorded and the read returns zero), so
every invocation reaches the subgroup barriers and the epilogue.
- The cache keys cover the mesh configuration; requests serialize its
new esgsItemSize field (version 4).
Driver:
- DecodeShaderStages reads VGT_ESGS_RING_ITEMSIZE (now in the draw key)
and sizes the subgroup for its ES vertices, GS primitives and outputs.
- Stages compile below the mesh draw parameters, which every mesh draw
pushes; the mesh stage always takes part in the push range.
- The index-buffer V# is set per draw (per record on the CPU-resolved
indirect path); the index-buffer alias check spares read-only
elements.
- The workgroup and LocalSize checks count two guest lanes per
invocation on 32-wide subgroups; mesh draws may carry a first vertex
or instance.
agc_driver_mesh_tests (EXCLUDE_FROM_ALL, needs VK_EXT_mesh_shader)
recompiles a hand-assembled merged ES/GS program in the PS5 compiler's
layout and draws indexed and non-indexed triangle lists in one- and
two-wave subgroups and a triangle strip across odd-starting subgroups,
reading the pixels back. agc_shader_replay prints a request's draw and
mesh configuration.
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ButHENCbXXt8h4EZuWUabJ
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
A view whose offset in its GPU owner is not a multiple of
minStorageBufferOffsetAlignment could not be bound, and an address-based
build (its registered heaps imported whole) put descriptor-bound buffers at
arbitrary offsets inside them, so such dispatches failed.
GuestBufferMemory::Descriptor now binds from the aligned offset below the
view and reports the difference; ShaderResources stores it, per guest buffer
element, into the push constant block at the shader's memory byte offsets
(RecompileResult::memoryOffsetDword), which the shader already adds to every
access. Every push constant path (dispatches, recorded and synchronous draws,
draw recipes) takes the patches; the assembling overload of
Pipeline::PushConstants is gone. A shader whose shader data is a buffer fails
when it would need one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A V# without records (all zero, say, for a slot the taken path never
uses) makes every access out of bounds on hardware: loads read zero or the
format's default, stores and atomics are dropped. The recompiler now
specializes such a buffer as empty, so every access through it is out of
bounds, and the driver binds a small per-device placeholder buffer to the
element instead of rejecting the V#. A V# with records at address zero
still throws.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A V# the SRT walker cannot compute (loaded from a descriptor heap at a
GPU offset, or built by the program) has no binding to hold it. Raw
dword loads and stores of 1-4 dwords through one now decode the V# in the
shader, apply the RDNA2 bounds rules (OOB_SELECT, stride, swizzle,
ADD_TID, format 0) and go through the BDA page table, as Kyty does.
Stores need Write permission, which only ranges that are a writable
allocation's host import get, so a store into a mirror or a copy faults
rather than being lost. The shader notes each written page in an
open-addressed set behind the fault record; once the work completed the
host marks those pages GPU-written, and an overflowing set throws.
Formatted, typed, sub-dword and atomic access through such a V#, and
their use outside compute shaders, still fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EVENT_WRITE PIXEL_PIPE_STAT_DUMP (event 0x39) dumps the DB sample
counters. The recorder, once a title asks for them, keeps an occlusion
query open over each batch (precise when the device supports it) and adds
the result to a running count when the batch is reaped. A dump drains
recorded work like a label and writes the count in the PS5 layout: 16 DBs
16 bytes apart, begin and end counters interleaved, the count in the
first and bit 63 marking each ready. Kyty and SharpEmu write a fixed
"visible" result instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A depth target lives only in a GPU image (its guest surface is never
written), so a texture over its depth or stencil plane now samples that
image through a view of the plane's aspect: R32_SFLOAT for D32, R16_UNORM
for D16, R8_UINT for stencil, one level and slice at the surface's extent.
Other shapes, and storage access to a depth surface, throw. Depth images
get the sampled usage, and the barriers after a render pass and before a
dispatch include depth attachment writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Depth/stencil: draws that test depth or stencil against a bound surface
render with a GPU-only depth/stencil attachment (DepthSurface), cleared
to DB_DEPTH_CLEAR/DB_STENCIL_CLEAR when made. Z test/write/function and
per face the stencil function, operations and masks (BACKFACE_ENABLE);
replacements by another value where Vulkan's single reference can carry
them. Clear and copy draws, add/sub other than 1, array or mip views
and MSAA throw, and texture reads of a surface's memory are refused.
Scaleform menus mask with the stencil (REPLACE 0, ADD_CLAMP, EQUAL);
their masked content was skipped before.
- Mipmapped color targets render through the resident image of the whole
chain with a view of the mip, so mips in the packed tail are drawn too;
CB_COLOR_VIEW's mip level is read from bits 26-29 (GFX10).
- Color: SNORM targets, UNORM16/SNORM16 exports, one-component formats
honouring COMP_SWAP (A8), 32_R/32_GR/32_AR exports, linear rows padded
to 256 bytes, BLEND_BYPASS.
- INDIRECT_BUFFER jumps are followed when copying a submission; thin 3D
textures in display and rotated XOR swizzles; only the bytes the GPU
changed are written back; resource registration.
- VideoOut: a flip stalls the GPU queue while the flip queue is full and
holds the submitter back.
- APS5_DUMP_FRAMES_EVERY dumps every k-th presented frame.
- The graphics test target builds again (missing sources, a hand-aligned
64 KiB buffer, stale expectations updated) and gains DepthStencilTests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vertex buffer formats 50-55 (2_10_10_10 UNORM, SNORM, USCALED, SSCALED, UINT, SINT: X in the low
10 bits, the 2-bit W on top) map to Vulkan's A2B10G10R10 packings, and format 36 (10_11_11_FLOAT:
X in the low 11 bits) to B10G11R11_UFLOAT, the same mapping TextureFormat uses for textures.
11_11_10_FLOAT (43), whose 10-bit X has no Vulkan packing, stays rejected.
fix(agc): keep BGRA color targets resident between draws
BGRA color targets fail the exact guest-texture-format lookup and fall back to a synchronous framebuffer round-trip for every draw. This keeps them resident using compatible RGBA storage with the original BGRA attachment view. Presentation checks the actual display channel order, attachment views request only attachment usage, and host-write barriers include the host stage.
This follows the existing linked PRX/Vulkan path. It adds no interpreter, separate runtime process, or new shader compilation path.
fix(agc): reject primitive restart only for indexed draws
- `DecodeState` still reads `0x24b` (so it stays in the recorded draw-key registers) but no longer rejects on it.
- `DrawRejection` takes an `indexed` flag, and reports restart only for indexed draws.
- The driver's draw path rejects an indexed draw with restart enabled before the precheck, so indexed draws with restart are still refused when the precheck is off. Restart itself is still not implemented.
When the presentation device was replaced, textures created on the old
device stayed in the global sampled and storage texture caches and kept a
reference to the destroyed TextureDetiler, whose VkDevice was then read
corrupted ("missing Vulkan function: vkResetDescriptorPool" at startup).
The replacement now flushes the old device's cached storage textures and
publishes every unit shadow before waiting for the device, and the device
teardown erases the cache entries that belong to it.
Patch author: boykopovar (fix-vulkan-texture-lifetime.patch, PR #5 review).
Co-Authored-By: boykopovar <boykopovar@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177gthrWEBBfBu5xJDXgBfo
Dispatch cache with per-key value variants and data-only hits (flat-SRT leaves the recompiler
proves pure are refreshed into the resource template instead of re-captured), dispatch and draw
recipes replaying a proven prepare, register-keyed draw entries, template data refresh, unit
shadows in device-local slabs, 64 KiB block-unit storage tracking with windowed detile, DCC
clear-key proofs, per-class GPU timing, a CPU copy path with batch read tracking, present slot
ring, rate-limited reaps and lazy completion counting, plan-failure and poisoned source memos,
bindless image tables (runtime-indexed descriptor arrays from a CPU-read table) so the
per-frame compute program that failed its plan now compiles. Worker affinity opt-in, per-draw
logs behind APS5_TRACE_DRAWS, wait-timeout reports include the recorder's view of the label.
Fixes a use-after-free behind BDA-miss dispatch snapshots and the CPU-write stamp of
UnchangedSinceCollected. Intro-video stage: 154 -> ~500 presents per 10 s at 4K.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177gthrWEBBfBu5xJDXgBfo
Replaces the synchronous per-command execution model, and with it the
upstream render-target, pipeline-cache, tile-buffer, batched-draw and
memory-ownership commits (5a280ce, 7ed5a0b, 46c799d, 598caac, 40df528),
whose files are removed.
- Recorder: one open command buffer per device; dispatches, resident
draws, uploads, write-backs, labels, fills and copies record into it;
timeline-semaphore serials let drains wait without the GPU mutex;
completions run in order at reap.
- one worker thread per PM4 queue; labels and stores written by the GPU
into the host import; GPU-side DISPATCH_INDIRECT; the buffer-fill
kernel replaced by vkCmdFillBuffer; a dispatch cache validated by
write watch; filler packets tolerated; rejected submissions dumped.
- guest memory: page-state cache, 64 KiB write generations, host imports
with VK_EXT_external_memory_host, persistent mirrors of the executable
image, GPU copies of misaligned sub-ranges.
- textures: shader-written images stay resident with deferred
write-back (adjacent-generation rule for packed video planes), sampled
views of storage images, DCC key handling, addrlib swizzle equations,
bidirectional detiler.
- draws: MRT, more formats, dynamic viewport/scissor, cached pipelines,
resident 64 KiB targets, draws recorded into the recorder.
- ShaderResources: two-stage build, descriptor and sampler caches, a
content-keyed resource cache; presentation from the resident image.
- a label deferred behind a completion is no longer re-stored by the
CPU at reap time unless a write-back overwrote it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177gthrWEBBfBu5xJDXgBfo
- libc: reserve guest memory in one arena below the PS5 map limit with
MEM_WRITE_WATCH (GuestArena) and a caching guest heap; the allocation
registry gains a generation counter, an invalidator callback for the
driver's page cache and a pin waiter for leased ranges. This replaces
the fault-based GuestMemoryTracking/GuestMemoryBacking layer, whose
files are removed.
- libkernel: mmap/mprotect/munmap on the arena, flexible memory budget,
real sceKernelVirtualQuery; POSIX pthread layer, rwlocks, timed locks,
TSD, event flags; 0.5 ms scheduler tick and high-resolution sleeps;
TSC/clock/time functions; file APIs return SCE errno codes; the APR
file-streaming API.
- libc: vectored crash reporter with backtraces for fatal exceptions,
SIGABRT, UCRT invalid parameters and process exit; SSE4a EXTRQ/INSERTQ
emulation for Intel hosts; %ls/%lc/%S/%C with the guest's 16-bit
wchar_t; hand-written setjmp/longjmp for the guest jmp_buf; a path
alias table for short save-data mount points; more string, stdio and
math exports.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177gthrWEBBfBu5xJDXgBfo