Imported from mattias800/prosper (
prosper/tests/fixtures/AGENTS.md). Install upstream withnpx skills add mattias800/prosper --skill fixtures. Copyright stays with the author.
prosper/tests/fixtures/ — shared harnesses and hand-built inputs
Header-only material that more than one test needs: the Vulkan/compute execution harnesses, and
hand-constructed binary inputs (a synthetic PRX, an ATRAC9 vector, captured GTA V shader words) that
would otherwise be pasted into each test that wants them. Nothing here is a test itself — every file
is included by tests in the sibling directories, which is why it is all headers with inline
definitions and no translation unit of its own.
Two kinds live here and they are worth telling apart:
- Harnesses —
render_runner.h,compute_runner.h,image_compute_runner.h,test_scratch.h,mapped_staging.h(renderer-owned persistent mappings). These do things: bring up a device, record commands, read pixels back. - Fixtures proper —
at9_testvec.h,handmade_prx.h,synth_prx.h,spirv_*.h,test_data.h,gta5_*_fixture.hpp. These are data, frozen at a known-good state so an assertion has something stable to compare against.
render_runner.h is NOT test-only, and the directory name is the trap
It is the project's offscreen Vulkan backend, and the shipping frontend compiles it:
frontends/shared/live/live_renderer.cpp and present/present_blit.cpp include it. (This line used
to name live_compute.cpp as a third includer. It is not one — checked with the preprocessor, not
with grep: -M over that translation unit's own compile command lists no render_runner.h. The
compute backend is a separate Vulkan backend that shares the device, not this header.) render_draw_pass_rgba is therefore a live render path — a wrong failure path in it
silently drops real rendered content in a real game, and "it is under tests/" has misled reviewers
into treating changes here as test-only (#3210 had to say so explicitly in its own body).
Consequences for anything you change in it:
- Dropping a pass or skipping a draw is a VISIBLE regression when the condition can fire on a
healthy device. Guard on the API's own
VkResult, not on a heuristic, and say in the PR what a false trigger would cost. - It cannot depend on the app — no capture singleton, no frontend state — because it is also compiled into Vulkan tests that link none of that.
- It cannot depend on the HLE's locks either, and that one is easy to miss (#2953). The live
route reaches this backend only under
g_agc_state_mu(src/hle/graphics/hle_agc.cpp, #278), so reading the live path alone makes the persistent caches look safely single-threaded.gpu_replay,boot_traceand every Vulkan test compile this header and link no HLE, so on them that invariant simply does not exist. Process-lifetime state declared here carries its own synchronisation:render_draw_pass_rgbaholdsBackendPersistentResourceGuardfor its whole body, which covers everystaticthat function owns. It does not cover the colour-target and depth/stencil caches' other entry points (invalidate_persistent_color_target*,readback_persistent_color_target,snapshot_persistent_ds_images, and the frontend's direct iteration of both), so do not add a new one without reading #3240. - A readback has TWO halves and they are independent (#2944). Any device write the CPU then maps
and reads needs
record_host_read_barrier()— the availability operation into the host domain, which a fence wait does not perform — and, when the memory may be non-coherent,invalidate_mapped_readback(). Coherent memory still needs the first. Neither half can be checked by reading the pixels back: on every driver here the unsynchronized code returns correct bytes, so the guard ishost_read_barrier, which asserts the barrier was RECORDED. Since #3249 the availability half lives insrc/gpu/execute/host_read_barrier.hppand is only re-exported intoprosper::testhere, because the live compute backend needs the identical rule for its dispatch results and two spellings of it is how the next site gets missed. The invalidate half stays in this header, with the allocator that can actually return non-coherent memory. Its counter is process-wide and shared with the compute guard,live_compute_host_read_barrier. - Two writes to the same image are not ordered by a barrier that names some other image (#3248).
Any
vkCmdPipelineBarrierbetween them supplies the execution dependency, and an execution dependency is not enough for write-after-write: the first write also has to be made available.record_transfer_write_after_write_barrier()is that dependency. The mip-assembly path had a full-image clear followed by per-level copies with barriers only on the copy sources, which is the shape to look for whenever a path writes one destination from several commands. Unfalsifiable from output, and worse than the readback case: assembly clears to black on purpose, so a clear that lands after a copy produces exactly what a correct run produces for a level the guest never rendered. The guard ismip_assembly_barrier. - A diagnostic in here is a Vulkan API user like any other, and until #3248 no test ran one.
PROSPER_GEOM_PROBEandPROSPER_DRAW_ISOrecord real commands; because no ctest case armed them,tools/vkvalcould not see their misuse however it was configured, and both were misusing Vulkan and misreporting because of it.render_diagnostic_pathsruns both once so the layer does see them. Arm a new env-gated render path there in the same change that adds it — and note the arming has to come from the ctest ENVIRONMENT, notsetenv()inside the test, because these names are cached one-shot reads.PROSPER_DRAW_ISOis improved, not fixed: its re-render now records the same dynamic state as the pass it isolates, but still omits that pass's per-drawvkCmdClearAttachmentsdepth/stencil clear, so a submit carrying a guest depth clear is isolated against different depth contents. The layer cannot see that half.
Adding to it
Prefer extending an existing harness over a second one that does nearly the same thing; the tests
that share render_runner.h share its device, caches and statistics, and a parallel copy would
diverge. Fixtures that only one test uses belong beside that test, not here.
Retained buffer inputs
The measurement and contract notes retain accepted mechanism evidence, adverse game comparisons and their limits.
Cross-call storage uploads own whole Vulkan buffers; they never retain a guest pointer or an arena slice. Reuse compares the complete current materialized input against an owned CPU snapshot, never the potentially uncached Vulkan mapping. The snapshot also supplies the exact device upload bytes. Refresh in place is permitted only when the cache is the sole owner; recorded and in-flight passes hold shared completion leases, including the existing indeterminate-completion retention path. The separate residency cap counts Vulkan allocation and CPU snapshot bytes until the final owner releases them, including detached older versions. It adds to the existing free host-buffer pool budget.
Admission currently requires every final shader in the pass to have complete buffer-write provenance and no storage-buffer writer, including atomic writes. This whole-pass rule preserves the old within-call alias memo: a writable later draw cannot reach retained input through a previous binding. Small inputs, descriptor-table entries, GDS, zero identities and diagnostic mutation retain their existing routes. Reflection observes writes; absence of a writable flag alone is not proof.
At capacity, new identities may rekey one of at most 32 inspected entries only when its device and exact size match and no submission owns it. Otherwise the ordinary upload route handles the miss; known capacity pressure does not evict unrelated entries or allocate new storage. Preflight uses the pool capacity class plus snapshot size; actual driver allocation requirements are checked after allocation and may still decline admission. Admitted-byte counters mean new key payloads, including rekeyed allocations, and are not Vulkan allocation-byte counters. The bounded search resumes from its previous cursor, so an incompatible old prefix cannot hide later reusable entries forever. Inspection does not change LRU recency; the cursor is a key that can safely disappear through rekey, source replacement or cache clearing.
In addition to bytes, admission limits all live retained owners to the smaller of 4096 and one
sixteenth of the device's advertised memory-allocation limit. Detached submission owners remain
counted until their actual Vulkan allocation is freed; explicit byte overrides retain this bound.
This limits the cache's added allocation population, not the whole device's independent allocators.
PROSPER_BACKEND_BUFFER_RESIDENCY_OWNERS can lower the allowance (0–4096, default 4096)
without relaxing the device fraction. Zero also bypasses the additional pass write proof;
nonzero controls retain it. Fixtures pin 4096 to isolate their synthetic device-limit tests.
Timing logs report sampled entry, owner and byte occupancy. Cleanup can change the two atomic
lifetime gauges between reads, so they are not a transactional accounting snapshot. Reading these
statistics never initializes Vulkan.
Only explicit direct guest views may use a cache-entry write watch; hosted, copied, padded and
descriptor-table data retain full comparison. Two exact equal validations precede promotion; arm
before the next authoritative comparison. Unknown or failed coverage falls back, and two dirty
queries disable watching for that entry until its source changes or it is rekeyed. Entry watches
never own or extend guest mapping lifetime; recorded GPU snapshots remain independent.
PROSPER_NO_BACKEND_BUFFER_WRITE_WATCH disables only watch validation, preserving residency for
comparable exact-validation controls. Watched bytes are a subset of reused bytes; watch time is
already included in resident time.
The combined residency budget defaults to zero on every platform. Explicit
PROSPER_BACKEND_BUFFER_RANGE_RESIDENCY_MB budgets can use
PROSPER_BACKEND_BUFFER_RANGE_MIN_BYTES to bypass small connected ranges and
PROSPER_BACKEND_BUFFER_RANGE_RECLAIM to reclaim one incompatible idle owner under pressure;
PROSPER_NO_BACKEND_BUFFER_RANGE_RECLAIM overrides the latter. Admission policy never grants
content authority. PROSPER_BACKEND_BUFFER_RESIDENCY_MB independently enables per-binding exact
snapshot residency on any platform, capped at 2048 MiB. Explicit zero and the existing disable
switches bypass retention. Inputs whose watches fail still use exact comparison; this fallback is
correct but can cost more than ordinary uploads.
Runtime cache fixtures explicitly select a nonzero budget so unsupported platforms exercise the
portable ownership and comparison contract too.
Within-pass overlapping buffer uploads
Direct guest buffer views may share one arena upload for connected overlapping ranges with the
same address residue modulo the device's storage-buffer alignment. Each descriptor retains its
original exact range. Admission excludes owned/hosted/table/GDS/texture inputs and requires complete
negative buffer-write proof for all final shaders in the pass, including later draws and geometry.
The planner deduplicates exact spans before counting benefit, never bridges gaps, and caps each
union at the smaller of 64 MiB and the device range limit. Ordinary resource sharing and arenas
must be enabled; PROSPER_BUFVERIFY and PROSPER_NO_BACKEND_BUFFER_RANGE_SHARE decline this path.
Union uploads are lazy and owned only by existing per-pass arenas through submission completion.
A failed group allocation is not retried for each member; ordinary uploads handle the fallback.
There is no cross-call guest-pointer retention or implicit extension of guest mapping lifetime.
The unions actually used by resource setup are copied together before command recording. Their
guest views remain borrowed under the synchronous-call contract; only completed arena uploads enter
a deferred submission. The bounded copy cohort shares PROSPER_RENDER_COPY_THREADS, and partial
worker creation drains all work on the workers that did start plus the calling thread.
PROSPER_NO_BACKEND_BUFFER_COPY_BATCH restores immediate per-union copies as a measurement and
recovery control without disabling range sharing.
The range counters distinguish actual union copies from distinct resolved descriptor slices;
skipped draws can leave planned bytes unused, so bound bytes minus upload bytes remains signed.
Planning and shared write-proof time are nested in resource setup, outside per-binding copy time.