Commit Graph

41 Commits

Author SHA1 Message Date
TibixDev 2441a4b0d9 fix(agc): retry failed host imports after range replacement
Retain weak identities and the full failed span so a replacement of either owner or a shorter remaining run retries the import. Keep unchanged failures cached across unrelated mapping changes without pinning their allocations. Existing upstream import lifetime, span windows and Windows fallback remain in place. AI-assisted implementation.
2026-10-12 07:34:15 +03:00
Scramblrrrr 56d08f2547 fix(agc): exclude host arena holes from write-watch proofs 2026-10-11 10:55:48 -03:00
Cesar Rodriguez a947303731 perf(agc-driver): join host-import spans only over whole aligned windows
one span. A title that grows a heap 16 KiB at a time then re-imports the
whole chain on every growth: Ratchet & Clank: Rift Apart re-imported about
1.5 TiB while loading its first scene, and NVIDIA refused grown spans with
VK_ERROR_OUT_OF_DEVICE_MEMORY, which leaves those ranges copied per draw.

Ranges are now joined only when a chain covers a whole aligned window (8 MiB
by default, APS5_HOST_IMPORT_WINDOW_MIB on the device); until the window is
complete each range is imported on its own, and a span never crosses a
window edge. Growth therefore re-imports a range once more at most when its
window completes, instead of the chain on every growth. Joining any part of
a window was tried first and still re-imported 2 GB per second in Ratchet,
as each growth re-imported the window's partial span. A single range larger
than the window is imported whole. SetHostImportWindow(0) keeps the
unbounded spans, which is what the existing tests use.

The new importWindowTests (agc_driver_graphics) checks that a full window is
one span, that a partly filled window imports its range alone, that a view
crossing a window edge gets no import, and that completing one window leaves
the other window's import in place; with the window ignored it fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:35:28 +13:00
Chad b88ad02090 perf(agc): mirror a sparse descriptor heap instead of copying it per dispatch 2026-10-11 09:55:14 +02:00
Adrià c12a19e4de
Merge pull request #3237 from Raghav314/perf/agc-cache-fault-only-resources
perf(agc): cache resources whose only completion work is the fault check
2026-10-11 09:50:44 +02:00
Adrià fbcdf1a804
Merge pull request #2992 from CnRJay/fix/agc-sparse-heap-cut
fix(agc): copy a sparse descriptor heap only up to its last backed byte
2026-10-11 09:24:32 +02:00
raghav314 024cb1feda perf(agc): cache resources whose only completion work is the fault check
A draw or dispatch whose shaders have guarded descriptor slots (or a rect
list) but no BDA table gets a fault buffer, and with it NeedsCompletion(),
which kept its resources out of the resource cache, and NeverReusable
skipped the cache lookup for it. In PPSA36082 nearly every draw in the hub
is such a build, so the cache served none of them.

Such an object has no table to revalidate, no stores through BDA and no
copied writes, so it is now reusable. Each use of a reused object checks
the fault record when its batch completes (CheckFaults) instead of
running the once-only write-back; draw recipes over such a template
register the same check, and dispatch recipes still skip templates with
completion work. NeverReusable now refuses what noteReusable refuses: a
BDA table, or a fault buffer with APS5_LOOP_GUARD set (its fault resets
the buffer).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:15:53 +02:00
Adrià 5beb77f0f6
Merge pull request #3231 from Raghav314/perf/agc-pass-fault-only-draws
perf(agc): continue render passes across draws with only a fault buffer
2026-10-11 09:14:20 +02:00
Adrià 6e52fb8f8c
Merge pull request #3158 from XairRrR/perf/agc-host-import-spans
perf(agc-driver): import adjacent registered ranges as one host-import span
2026-10-11 09:14:15 +02:00
Chad 86d6624323 fix(agc): copy a sparse descriptor heap only up to its last backed byte
Co-authored-by: Scramblrrrr <Scramblrrrr@users.noreply.github.com>
2026-10-11 04:56:40 +02:00
raghav314 70a6dde689 perf(agc): continue render passes across draws with only a fault buffer
A recorded draw counted as reading and writing any guest memory when its
resources needed completion work, which a fault buffer alone (guarded
descriptor slots, rect lists) also needs. Such a draw reaches no guest
memory through it: the buffer is the draw's own, written only on a fault.
Its pass hazards now come from its known ranges and images like any other
draw; address-based draws, draws through the BDA table and draws with
copied writes still count as touching any guest memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 07:53:23 +05:30
raghav314 aa013f59ac fix(libc): register guest heap blocks without GPU access
AnyPS5's libc heap registered every block with GPU access, so every
address-based build imported or mirrored each malloc block the title had
made. The console's libc.prx maps its heap with protection 3 (CPU read
and write, no GPU bits), so the GPU never sees it there either.

Heap blocks are now registered without GPU access; reallocating keeps the
block's access. The AvPlayer fallback for a title that passes no texture
allocator stands in for that allocator, which hands out memory the GPU
reads, so it allocates through a new GuestHeapAlignForGpu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 07:02:30 +05:30
Scott Schuster dc48c61050 test(agc): a strided buffer's rounded-up last record stays within its own allocation
When another readable allocation starts right after the buffer's allocation,
the bound range still ends at the first allocation: the bytes past it are
outside the buffer the descriptor's records describe, and binding into the
neighbour would send the range back to a copy on every draw.

AI-assisted (Claude Code).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 01:37:00 +02:00
Scott Schuster b352406f70 fix(agc): bound a strided buffer's rounded-up last record by its allocation
RADV rounds a strided buffer descriptor's record count up, so stride * records
can run up to one stride past the end of the buffer's allocation. The guest
buffer region then no longer fits inside the allocation's host import and is
copied on the CPU, whole, for every draw that binds it.

When a strided descriptor ends less than one stride past the end of the
readable registered allocation that contains its base, the range is bound up
to the allocation's end. RetroArch's Dolphin core binds a 48 MiB stream buffer
with stride 20 (records 0x266667, 12 bytes past the allocation) on every draw.
With this change the buffer is bound in place from its import.

AI-assisted (Claude Code).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 01:37:00 +02:00
XairRrR 70d7aa58e3 perf(agc-driver): import adjacent registered ranges as one host-import span
A host import covered exactly one registered range, so a buffer crossing
two ranges had no import and was copied on the CPU at every use, under
the GPU mutex. Titles that map a GPU heap piece by piece (Uncharted maps
1 MiB direct-memory pieces) hit this for every buffer larger than a
piece.

An import now covers the run of address-adjacent registered ranges that
are readable with the same write and GPU access. Runs never overlap, so
imports do not either; an import is retired when any range of its run
changes, and a refused run is remembered by its base. Callers already
address imports relative to their base.
2026-10-11 01:04:12 +02:00
MathiasDevelopes 078b6d886f style(agc): drop the comments the conventions check rejects
Comments are reserved for technical debt (docs/dev/TechnicalDebt.md), so
the ones the previous commit added to the recorder, the snapshot keep and
the tests go.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZHkb8kUwnHAuBBbHywvJf
2026-10-11 00:33:07 +02:00
MathiasDevelopes 4940de5436 fix(agc): count memory shared by a batch's keeps once and test the three keep sites
A ShaderResources from the resource caches is the same object for every
draw or dispatch that uses it, and Recorder::ReusableDrawSnapshot hands
the next draw the same snapshot buffer, so passing their bytes with each
Keep counted one allocation once per use and submitted the batch before
it held the budget. Recorder::KeepBytes counts an owner's bytes once per
batch, and the dispatch, draw and snapshot keeps use it.

The recorded kept bytes test runs a dispatch and a draw that copy a
guest buffer and two draws that snapshot one on the device, and fails
when any of the three sites stops counting or a reused snapshot counts
twice. The BdaResources check pins the copied region it sets up.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZHkb8kUwnHAuBBbHywvJf
2026-10-11 00:33:07 +02:00
MathiasDevelopes 00fafd4379 fix(agc): count copied guest buffers and draw snapshots in the batch byte budget
The recorder bounds the bytes a batch keeps alive (KeptBytesBudget) and
waits for older batches past twice that. A recorded dispatch or draw kept
its ShaderResources, and a draw its snapshot buffers, with a size of 0, so
the host-visible copies of guest buffers they own never counted. A title
that binds a 124 MiB writable guest buffer in every draw then held about
ninety copies, 11 GB, until the GPU caught up, and aborted with
VK_ERROR_OUT_OF_HOST_MEMORY from vkAllocateMemory.

GuestBufferMemory reports the bytes of its copied regions, and the
dispatch and draw keeps and the draw snapshots pass their bytes to
Recorder::Keep, so the existing bound applies to them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 00:33:07 +02:00
Dean Galvin 2485915a03 perf(agc): skip the resource-cache key and lookup for address-based draws
A draw whose stages bind a BDA table or a fault buffer builds resources
that need completion work, so noteReusable never keeps them and the shared
resource cache never holds an entry such a draw could hit. Building the
content key and probing the cache for each of them was pure cost. Those
draws now skip both (ShaderResources::NeverReusable) and count as
uncacheable.
2026-10-10 23:02:52 +13:00
raghav314 c11a9cb59f fix(agc): address-based builds map only memory mapped for the GPU
The registry records whether a range was mapped or protected with GPU
access (0x10/0x20), and an address-based build leaves out ranges without
it, as the console's GPU page tables do. The main image is registered
without GPU access; the title's own mprotect with GPU bits adds it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:34:34 +05:30
Adrià 4fbf6ca5a3
Merge pull request #1960 from Raghav314/fix/agc-import-leaves-pages-unarmed
fix(agc): host imports write-protect only pages a collect already protects
2026-10-10 10:45:04 +02:00
seven 5b9fdd111a fix(agc): count a pixel stage's colour attachments against maxPerStageResources
Vulkan counts the framebuffer colour attachments against maxPerStageResources
for the fragment stage. The draw's attachment count now seeds the fragment
stage's resource total in ShaderResources, so a pixel shader whose buffers and
images fit the limit but whose pipeline exceeds it throws instead of creating
an invalid pipeline. The draw resource cache key carries the attachment count,
so a cached build is never reused by a draw with more attachments.
2026-10-10 12:07:56 +05:30
raghav314 c7f4690cd3 fix(agc): host imports write-protect only pages a collect already protects
Importing guest memory collected the write watch over the whole import,
which write-protected every page of it, so pages nothing had collected yet
took a write fault on their first CPU write. The import now collects only
the parts a collect already protects (GuestWriteWatchCollectArmed). The
rest stays unprotected until something collects it, which reports it as
written; a GPU write recorded into such memory (MarkWritten) first collects
the never-collected parts of its 64 KiB blocks
(GuestWriteWatchCollectFresh), so that report is older than the GPU's
results and does not read as a CPU write over them.

Those pages are stamped as written and their CPU pages are noted, but the
block's CPU stamp is left alone, so a GPU label record or a pending storage
image elsewhere in the block does not see a CPU write. StoredOver and
UnchangedSinceCollected read the noted pages whenever a block has them, so
a store to such a page is still seen by a check over that page. The extra
generation step happens only when something was collected; a block whose
write protection fails is unwatched and the write returns no generation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 11:38:28 +05:30
Dean d2612fb586
Merge pull request #1411 from Celegans12/perf/agc-heap-mirror-runs
perf(agc): skip unchanged heap mirror runs and idle mirror sweeps in address-based builds
2026-10-10 08:06:17 +13:00
Dean 05ba7f746a
Merge pull request #1410 from Celegans12/perf/agc-imported-heap-mirrors
perf(agc): release a heap mirror once a host import serves its range
2026-10-10 08:06:03 +13:00
Dean 2af96d1863
Merge pull request #1085 from Celegans12/fix/agc-host-heap-budget
fix(agc): throw instead of aborting when a heap mirror cannot be made
2026-10-10 08:05:48 +13:00
Celegans12 4709920641 fix(agc): throw instead of aborting when a heap mirror cannot be made
An address-based build whose heap mirror the device refused, or one past
APS5_HEAP_MIRROR_MIB, stopped the process inside acquireMirror. PPSA26344
hit it at 478 s with 6.8 GiB of heap mirrors beside the host imports.

- a mirror that would go past the budget first sweeps the mirrors whose
  registered range is gone, before its allocation; if it still does not
  fit, the build throws "past the budget"
- the mirror is allocated once: a refusal throws with the range, the
  Vulkan result and the mirror totals, so the draw or dispatch fails like
  any other allocation; nothing is registered for the refused range
- the BDA resource tests run with a 4 MiB mirror budget and a mock memory
  limit: a mirror past the budget is made in the swept mirror's memory,
  and a refused one fails after a single allocation attempt
2026-10-09 14:58:20 +02:00
Celegans12 3f00dc7841 perf(agc): release a heap mirror once a host import serves its range
An address-based build maps every readable registered range: through a
host import when one can be made, else through a heap mirror, a
host-visible copy of the range. A range mirrored while the import budget
was full kept its mirror after a later pass imported it, until the guest
freed the range. The pass then read the range through the import, and the
mirror only held host-visible memory and a second copy of the bytes.

The pass now releases the heap mirror of every range it serves through
an import. Recorded work that bound the mirror keeps it until it
completes. If the import goes away while the range stays registered, the
next pass makes a new mirror from the guest bytes. The [buffers] image
mirrors profile line counts the released mirrors.
2026-10-09 14:58:17 +02:00
Celegans12 e804ce85f7 perf(agc): skip unchanged heap mirror runs and idle mirror sweeps in address-based builds
A build served by the cached address space walked every region of the
space for its mirrors, sorted the heap mirrors, compared each heap
mirror's blocks with the write tracker under its own lock and swept the
whole mirror registry. In an offline replay with 37,000 heap mirrors
that took about 10 ms per build, every build.

The space now keeps its writable mirrors, its sorted heap mirrors and
their adjacent runs from when it is published. Each run is still
flushed and collected; a run with no block stamped since the generation
its last complete check collected skips the per-mirror compare, and that
generation is stored only once the build's blocks were copied. The
registry sweep is skipped while every live mirror's range is held by the
cached address space's lease and no mirror was added, replaced or
dropped since.
2026-10-09 14:58:16 +02:00
Celegans12 52eb4272de fix(agc): copy an imported range once a merge extends it past its import
An address-based build maps each imported registered range through its
host import. UploadPrepare merges a descriptor range or snapshot
overlapping such a range into the range's region, and the region kept
the import even when the merge took it past the import's end, as a V#
crossing into the next registered range does. Descriptor then bound the
import's buffer beyond its size, and the BDA table mapped the whole
region, the next range included, through the import's device address.

A merge that extends a region past its end now drops the import. The
region then takes the import lookup and, with no import covering it,
the copy path, as a range crossing an image mirror's end already does.
Like every copied range, it is read-only to pointer stores.
2026-10-09 12:11:06 +02:00
boykopovar 1b3da3a740 test(agc): update graphics binding and bda range checks 2026-10-08 17:37:40 +03:00
Celegans12 38e89df1a5 fix(agc): bind guest buffer views that start off a DWORD boundary
A V# whose base is not a multiple of 4 bytes from the start of its GPU
owner threw in GuestBufferMemory::Descriptor, because the shader added
the offset below the view only in whole dwords.

Descriptor now binds from the offset alignment below the view and
passes the whole remainder. Such a view's range covers the dwords
around it, and its descriptor region starts and ends on dword
boundaries, so a lone view copied to its own buffer binds the same way.

The buffer specialization records the V# base's low two bits. Raw
vector loads through such a V# join two dwords, glc dwordx2 loads
included, stores merge their bytes into both with compare-exchange
loops, and bounds checks include the second dword. s_buffer_load
ignores the low two bits, as SrtEvaluator does. Formatted accesses
whose element alignment (min(element bytes, 4)) the base breaks throw,
as they fault the wave on RDNA2, and so do atomics.
2026-10-08 13:01:27 +02:00
Dean Galvin 4ad7103e56 test(agc): run agc_driver_graphics_tests under ctest and build it outside Windows
Nothing built the graphics test target, so it is now registered with the other AGC tests. It set
APS5_PIN_WAIT_MS through _putenv_s only, which does not exist outside Windows; it now uses setenv
there. The heap mirror tests need a write-watched guest arena and are skipped with a notice
without one, as the recorder tests already do.
2026-09-30 23:37:40 +13:00
boykopovar abb9c85266 fix(kernel): share direct memory mappings and track physical offsets 2026-09-30 09:59:51 +03:00
STL1te 6b36c050fe style: drop the branch's code comments per CONVENTIONS.md
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>
2026-09-29 21:25:29 +03:00
STL1te 119560206d feat(agc-driver): heap ranges the host imports did not take are mirrored
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>
2026-09-29 21:22:23 +03:00
STL1te 3dec6ceda7 feat(agc-driver): bind guest buffers below their view at the storage offset alignment
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>
2026-09-29 21:22:19 +03:00
devilismyfriend 29b4601564 perf(agc): dispatch and draw recipes, data-only cache hits, unit shadows, bindless image tables
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
2026-09-26 19:00:40 -07:00
boykopovar 2539e9787f perf(agc): avoid guest memory uploads for rect-list fault buffers 2026-09-24 14:57:39 +03:00
boykopovar 069ee78158 feat(shader): generate tessellation shaders for rect-list primitives 2026-09-23 16:13:33 +03:00
boykopovar 3028c87491 feat(agc): add bda memory mapping and resource management (wip) 2026-09-21 18:54:06 +03:00