Skip to content

perf(agc): settle Windows imports under 1 MiB without waiting - #6678

Merged
oneandonlydean merged 11 commits into
boykopovar:mainfrom
EagleBearerYS:perf/agc-async-settle
Oct 18, 2026
Merged

oneandonlydean merged 11 commits into
boykopovar:mainfrom
EagleBearerYS:perf/agc-async-settle

Conversation

@EagleBearerYS

@EagleBearerYS EagleBearerYS commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What

This implements the non-blocking settle that the debt entry for pages under an import window lists as missing. The entry (23fccd7, merged with #6060) ends: "Settling without blocking the importing thread (submit the read, keep the import compared until its fence signals, then switch it to the write watch) is not implemented."

  • The PR follows that sequence. Batching the reads, which a later lookup submits instead of the importing call, is this PR's own choice.
  • The switch to the write watch restores tracking without replacing the backing, but after the settle, while the import is live. The clause of the entry above it, "restoring tracking on import retirement without replacing the backing remains unimplemented", still holds: an import retired before its blocks return leaves them compared, as now.

Why. Since #6060, a Windows host import of private write-watched memory stays in the write watch only after its settle, a 4-byte GPU read that the importing thread waits for. Only imports of 1 MiB or more are settled (23fccd7): SuperTuxKart (PPSA69421) maps, imports and unmaps a 64 KiB buffer around every submission, and waiting for each settle cost it about 19% of its frame rate (Dripl1x, on #6060). A smaller import is compared instead, and its range leaves the write watch for good.

Resident Evil Village (PPSA01556) maps its GPU heap in 64 KiB pieces of resident direct memory. In a profiled boot to the menu, 12,628 of the 14,881 imports were made at map time, in such pieces. So on main's settle code that whole heap is compared, and so is every texture sampled from it.

  • At the menu, APS5_PROFILE_DRAW=1, per 10 s: [textures] kept 16.5 M untracked snapshots by their hint and compared 283,785, and [guestmem] compared 33 GB in 3.4 s.
  • Over the run, 2.05 TB of the 2.2 TB compared was cachedTexture's snapshot compare.

The game's menu, unprofiled, as the median of the 60-s intervals 5–15 min after the menu stands, on our Resident Evil Village test tree (main plus the open fixes the title needs, ours and others'), Windows 11, RTX 3070 Laptop:

Settle code (same tree) Menu fps (frame p50 / p90)
main's 0.133 (7.41 s / 8.03 s)
every import settled synchronously (a local switch that sets the floor to 0) 0.508 (1.83 s / 2.20 s)
this PR (floor kept at 1 MiB) 0.733 (1.35 s / 1.46 s)
  • Only the settle differs between the rows. With this PR the menu runs 5.5 times as fast as with main's settle code. The PR's row ran b62a057, the revision before this head. The head only adds the SetHostImportDeferredSettles test hook, which is on by default, and tests, so it runs the same code.
  • It also comes up sooner: the lit menu came 7.4 min after launch, against 10.0 min with main's settle code.
  • This PR is also faster than settling every import synchronously, which makes the mapping thread wait for each read.

#5692 tracks the same comparison cost on other titles. This PR covers the part of it that comes from imports under 1 MiB. I don't know whether the titles there make such imports, so it's listed as related.

How:

  • An import under 1 MiB is compared when it is made, as now, and its 4-byte read is queued.
  • The next import lookup that holds GpuMutex while the imports match the registry records the reads queued since into one command buffer on the settle queue (up to 256 reads per batch, four batches in flight) and submits it. The thread that maps and imports neither submits nor waits.
  • Once a batch's fence has signalled, a later lookup returns its imports' whole 64 KiB blocks to the write watch, except where storage results are pending over them; those wait for a later lookup.
  • GuestMemory::Rewatch, the first commit, collects and drops the blocks' write-watch marks and stamps every block as a CPU store with a new generation. So anything derived from the range while it was compared (a snapshot without a generation, a proof by its untracked hint) reads as changed once.
  • A settled import whose range had already left the watch, such as a whole range imported over a piece that was compared, returns the same way.
  • An import retired while its read is still running is kept on the read's batch and freed once the batch's fence has signalled (at the next lookup, a drain or a device switch), so no read outlives the import's memory and retiring it doesn't wait. An unmap that needs that memory waits for the batch, counted as an unmap wait.
  • A completed settle read ends the driver's marking of the import's pages, even while main-queue work that used the import first is still running, and an earlier import's later first use of the same pages marks nothing. Two canary tests check this on the device they run on.
  • A settle batch whose submit or reset fails, which means device loss, drops its settles without retrying them, and so does a Rewatch that refuses a page, which happens only for pages an import wouldn't have pinned.
  • SetHostImportDeferredSettles(false), a test hook like SetHostImportBudget, keeps imports under 1 MiB compared as before. agc_driver_dcc_keys_first_write's hinted-import case uses it, because that case tests the untracked hint of an import that stays compared. That still happens on a device without a settle queue, and until the read completes. APS5_NO_DEFERRED_SETTLES (any value) does the same for a run (5f822a5), so a title can be compared with and without the deferral on one build.

SuperTuxKart (PPSA69421) stays whole. Nothing that maps, imports or unmaps its per-submission buffer waits for a settle:

  • An import made on the title's mapping thread, off GpuMutex, only queues its read. Reads go out only from a lookup under GpuMutex.
  • Unmapping the buffer retires its import at once, through the registry's pin waiter (WaitForLeasesAndImports), as on main. A read still queued is dropped. One already submitted has almost always finished by then, and the retire checks its fence before it would wait.
  • A lookup made while the imports don't match the registry submits nothing. So an import that a new mapping is about to replace (a joined run) doesn't get a read just before its retirement.
  • The new test checks the first two: 64 rounds of mapping, importing and unmapping a 64 KiB buffer at one address off the device lock, and 64 more under it, submit no read and wait for none.
  • I couldn't run SuperTuxKart itself.

Why not #4448's GuestMemory::Rewatch. On Windows its body is empty: it re-registers the range with Linux's write watch, for imports retired after sixty idle frames. This PR's Rewatch is the Windows re-include, by whole tracker blocks. The two take different arguments, so they would sit side by side as overloads.

The commits:

  1. feat(agc): GuestMemory::Rewatch, with a write-tracking case.
  2. perf(agc): the settle, with its tests. It also keeps agc_driver_dcc_keys_first_write's hinted-import case on a compared import.
  3. docs(agc): ARCHITECTURE.md and the debt entry.
  4. perf(agc): an import retired during its settle read is freed after the read instead of waiting for it. The [buffers] line counts settle reads submitted, back in the write watch, retired during their read, the unmaps that waited for one, and pending.
  5. test(agc): the two canaries for the driver's marking, and the docs to match.

Overlap.

Related: #6060, #5851, #4448, #6192, #6308, #6546, #5692.

Tested

  • agc_write_tracking (CheckRewatch): a rewatched block is watched and collected again, reads as changed since before it left and as CPU-stored, its untracked hint stops applying, and the blocks around it keep their stamps. Part of a block, and a block with uncommitted pages, stay out.
  • New agc_import_settle_deferred and agc_import_settle_deferred_other_family (Windows, a Vulkan device with a settle queue):
    • a 64 KiB import is compared at first, returns to the write watch after its settle, and a CPU store after that is collected;
    • 64 rounds of a 64 KiB buffer mapped again at one address, imported off the device lock: each import is compared, and nothing is submitted or waited for;
    • 64 more rounds under the device lock, with no lookup between a mapping and the next: the same;
    • a buffer mapped again while its settle may be in flight is imported and returns to the write watch;
    • a 1 MiB import over a 64 KiB piece that was compared returns to the write watch;
    • with deferred settles off, a 64 KiB import stays compared.
  • Windows 11, NVIDIA GeForce RTX 3070 Laptop GPU (driver 617.42):
    • Red: the new tests on main's GuestBufferMemory.cpp, without their counter checks, fail with "(d) a 64 KiB import did not return to the write watch after its settle".
    • Green, 5 of 5: both new tests, agc_write_tracking, agc_import_watch and agc_import_watch_other_family.
    • Green, 2 of 2: the other Windows host-import tests, agc_driver_host_import_windows, _at_map, agc_host_import_retire, agc_driver_dcc_keys_first_write, agc_driver_layered_volume_submit, agc_driver_multisample_fill_host_only, agc_driver_store_over_untracked and agc_storage_cpu_snapshot.
    • The last two commits, 5 of 5: agc_import_settle_retire ("64 while the read ran, 0 waits, 0 unmaps waited for a read"; with retirement waiting for the read again it fails 3 of 3, "0 while the read ran, 64 waits" and "replacing an import during its settle read waited for the read"), and the canaries agc_import_settle_first_use (back in the write watch 21.7 to 30.4 ms after 4,096 main-queue copies of the import were submitted, while they still ran, with no driver marks afterwards) and agc_import_settle_earlier_import (an earlier import's first use runs only after the range is back in the watch, with no marks). The canaries have no red by design: they fail only on a driver that keeps marking. The other settle and watch tests pass 2 of 2.
    • 141d605 (the first-use canary retries with more copies instead of failing when its copies finish first), on main 66e290f with this PR, perf(agc): share one image table per heap and write epoch through capture, compare, resolution and descriptors #6454 and fix(agc): keep a compute data hit's live words with the regions that span them #6911 merged (the other two don't touch this path): agc_import_settle_first_use passes 10 of 10, racing in its first round (back in the write watch 26.7 ms after 4,096 main-queue copies, which were reaped by 47.9 ms; no driver marks after its return).
    • agc_driver_store_over_untracked sometimes fails on main on this PC too ("the result was never held pending").
  • GitHub's runners, on my fork, the ^agc tests (run; test(agc): align the indirect arguments block at run time #6452's test fix applied there only, because main doesn't build in full with MinGW since fix(agc): keep the gate run going past indirect dispatches whose arguments stay on the GPU #5747):
    • Linux: 606 of 606 pass, 43 skip.
    • Windows: 600 of 602 pass, 469 skip without a Vulkan device.
    • The red mark on the run comes from those two, plus guest_kqueue and precise_wait in the libc and libkernel suites, which fail on main on those runners too.
    • The sweep ran one revision before the last test fixes. The tests those fixes touch pass again on the head, on both runners (run).
    • The last two commits (run): both runners build them and run the touched tests, which skip on Windows without a Vulkan device; the run's red mark is guest_kqueue on Windows, a kqueue test this PR doesn't touch.
  • Linux, the CI job's steps on an Ubuntu 24.04 machine (GCC 13.3, lavapipe). This ran on an earlier revision. On Linux the head only adds HostImportSettles, which returns zeros there:
    • 1,277 tests: 1,211 pass, 61 skip, and 5 fail. The failures are four guest_datagram_* tests and guest_tcp: that machine has no IPv6, and they fail the same way on main.
    • agc_write_tracking and agc_import_watch pass 20 of 20.
    • Preprocessed for Linux, the changed sources differ from main only by GuestMemory::Rewatch, which is empty outside Windows, and by settleImport's device setup, which ran the same steps from adoptSettleDevice; since the merge of main 98c251f that setup is settleFor, with the same steps. Everything else this PR adds is inside #ifdef _WIN32.
    • At the last two commits' code: build and test registration pass, ^agc passes 606 of 606 (43 skip), and the preprocessed Linux sources change only by two counter fields.
  • Resident Evil Village (PPSA01556): the menu runs as in the table above, with no FATAL.
  • In the first scene, on the same tree, profiled runs show no gap: this PR ran 0.090 fps over 455 s at control, against 0.091 fps over 1,284 s with every import settled synchronously. Unprofiled, this PR held about 0.083 fps for 22 min, while synchronous settling rose from 0.089 to 0.15 fps over 12 min; main's settle code ran 0.029 fps there (one earlier run at control). All four runs fill the 6400 MiB import budget in the scene. With this PR the budget fills during the load, about 100 s before control, because imports under 1 MiB no longer wait; with synchronous settling it fills about 100 s after control, once the scene has imported its ranges. While the budget is full, every import an address-based build maps is stamped as recently used, so none of them can be evicted, and a range first needed after that, here one build import at 0x2372400000 (512 KiB to 1 MiB), is copied on the CPU at each use. That comes from the import budget's eviction, not from this PR's settle.
  • Planet of Lana (PPSA19450), as a check that a CPU-bound title doesn't slow down: our test tree for it (not main; it carries the open fixes the title needs) with fix(agc): stamp the watched blocks of a store into partly watched memory #6455 and this PR, Windows 11, RTX 3070 Laptop, warm shader cache, no APS5_PROFILE_* (the trace summary on). The meadow save is loaded from the title menu, then the player stands still; each value is the median of the five 60-s intervals from 180 to 480 s after the first present. One build, run OFF (APS5_NO_DEFERRED_SETTLES=1, the only difference between the launches), ON, then OFF again:
    • 5.06, 5.00 and 6.08 fps, at mean graphics clocks of 736, 723 and 769 MHz (clocks weren't held). The two OFF runs differ by 20 %, so this shows no frame-rate drop but can't rule out a small one.
    • GPU busy per present (the trace summary's gpu_busy_ms_per_present): 50.3, 54.4 and 44.0 ms. ON is at the high end, but inside the 6.3 ms between the OFF runs, so it can't be told apart from run-to-run spread. parl0v's RTX 3060 rows below go the same direction.
  • SILENT HILL: Townfall (PPSA33286), run by keravyn-ops on Windows 11 with an AMD Radeon 780M (driver 25.10.20.01), from boot with APS5_PROFILE_DRAW=1 and APS5_ADDRESS_COPY_MAX_MIB=8192 for 90 s (comment):
  • Hello Neighbor (PPSA35154), run by HQasper on Windows 11 Pro with an NVIDIA GeForce RTX 5090 (driver 617.42, Resizable BAR on), WinLibs GCC 15.2.0, on main 47a266c with fix(agc): stamp the watched blocks of a store into partly watched memory #6455 and this PR merged on top (comment). In game at the start of Act 1, 60 s of frames standing still from 120 s after Continue, no profiler, one run per build (noise about ±5 %):
    • main: 3.63 fps. With fix(agc): stamp the watched blocks of a store into partly watched memory #6455 and this PR: 8.12 fps. main with the settle floor lowered from 1 MiB to 64 KiB (synchronous settles, a local control): 8.45 fps.
    • 9,977 of the title's 10,400 imports are between 64 KiB and 1 MiB, all made while loading, so here synchronous settling does as well as this PR. Resident Evil Village, which imports at map time, is where the deferral makes the difference (the table above).
    • On main the render targets' DCC key ranges sit in compared imports: none of about 400,000 clear-key proofs per 10 s is reused (CollectWrites returns 0 for each), [dcc] reports about 570,000 key scans (78 GiB) per 10 s, and 15 % of the draw committer is in AllKeysEqual. With this PR, 33,207 proofs per 10 s are reused.
    • No failure or abort.
  • Minecraft Dungeons II (PPSA16064), run by parl0v on Windows 10 with an NVIDIA GeForce RTX 3060 12 GB (driver 617.42), MinGW GCC Release, title screen with no input, 200-s rows with APS5_PROFILE_DRAW=1 APS5_PROFILE_GPU=1, main 93e54ce against main with this PR at 141d605, two rounds (comment):
    • 6.61 and 6.61 fps on main, 6.50 and 6.53 with this PR (−1.4 %).
    • The idle gaps per frame shrink from 46 to 36 ms, but GPU busy grows from 104-105 to 117 ms, all of it in the title's compute dispatches; the three heaviest take 1.6 to 2.4 times their GPU time on main.
    • The cause isn't known yet. The comments ask for rows with APS5_NO_UNIT_SHADOW=1, with APS5_NO_DEFERRED_SETTLES=1, and with the settle-batching commits.
  • Onimusha: Way of the Sword (PPSA27836), run by simonbronk on Windows 11 with an NVIDIA GeForce RTX 5070 (driver 617.42) at 3840x2160: boot to the language menu with no input, warm shader cache, the trace summary over 30 s from 60 s after the first present (APS5_TRACE_SECONDS=60:30). The base is main 84b0d2f with fix(agc): import native multisample depth whose HTILE is not ITERATE_256 #6557, fix(agc): retire an unwritten native multisample depth surface on guest access #6727 and fix(agc): replace with a stencil operation value the pixel shader exports #6762, 6 runs (3 before and 3 after this PR's); this PR is merged on top of it, 3 runs (comment):
    • Frame p50: 77.9 to 84.5 ms on the base (mean 82.0), 77.0 to 78.6 ms with this PR (mean 78.0, about 5 % lower). Each run with this PR is faster than at least five of the six base runs; simonbronk counts the gain as near the noise edge, but present in every run. p90: 84.3 to 93.6 ms against 82.5 to 86.9 ms.
    • Driver.Packet.memory_wait per frame, in run 1 of each: 107.0 ms on the base, 78.5 ms with this PR. GPU busy per present is unchanged at 27.4 to 27.6 ms (GPU clocks weren't reported).
  • Merged with main 1b1f848 (c7e07ca, with 5f822a5), on GitHub's runners (run): the settle, import and write-tracking tests and fix(agc): settle dma-buf imports on Linux on the settle queue #5905's agc_driver_sparse_dma_buf_import pass 3 of 3 on Linux (45 tests) and Windows (53, of which 50 skip without Vulkan). The run shows as failed because the Windows libkernel suite's guest_kqueue fails on that runner with and without this PR (no main comparison was run).
  • Merged with main 98c251f (8f03535, the per-device settle batches after fix(agc): retain import settle resources per device #7269):
    • On an NVIDIA GeForce RTX 3070 Laptop GPU (driver 617.42, Windows 11, MinGW Release): agc_import_settle_deferred, agc_import_settle_deferred_other_family, agc_import_settle_retire, agc_import_settle_first_use, agc_write_tracking, agc_import_watch, agc_import_watch_other_family, agc_driver_host_import_windows, agc_host_import_retire and agc_storage_cpu_snapshot pass 5 runs in a row each, with none skipped.
    • On GitHub's runners (run), this PR's tests and the ^agc tests pass 3 runs in a row on Linux. On Windows, where the settle tests skip without a Vulkan device, the only failures are agc_driver_pm4_replay_tool and agc_shader_replay_tool, which fail the same way on that runner for an unrelated branch (run).
  • python3 tools/check_conventions.py --base origin/main: passed.

Checklist

GuestMemory::Rewatch takes ranges that left the write watch back into
it. Only whole tracker blocks whose pages can be collected return:
their write-watch marks are collected and dropped, and every block is
stamped as a CPU store with a new generation, so a snapshot or a proof
made before the block left the watch reads as changed, and the blocks'
untracked hints stop applying. It does nothing outside Windows.
Since boykopovar#6060 a Windows host import of private write-watched memory stays
in the write watch only after its settle, a 4-byte GPU read that the
importing thread waits for, and only imports of 1 MiB or more are
settled: SuperTuxKart imports a 64 KiB buffer around every submission,
and the wait cost it about 19% of its frame rate. A smaller import is
compared instead, and its range leaves the write watch for good.
Resident Evil Village maps its GPU heap in 64 KiB pieces, so every
texture over that heap is compared with guest memory.

An import under 1 MiB is still compared when it is made, but its read is
now queued. The next import lookup that holds the device lock records
the reads queued since into one batch on the settle queue and submits
it, so the thread that maps and imports neither submits nor waits.
Once the batch's fence has signalled, a later lookup returns the
imports' blocks to the write watch, except where storage results are
pending over them, which waits for a later lookup. An import retired
while its read is in flight waits for that batch first.

A settled import whose range had already left the write watch, such as
a whole range imported over a piece that was compared, returns to it
the same way.
ARCHITECTURE.md and the debt entry describe how imports under 1 MiB are
settled without waiting and return to the write watch, which the debt
entry listed as not implemented.
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 6472 core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp L73-74
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp L2601
PR 6868 docs/dev/debt/libSceAgcDriver.md L82-84
PR 7292 core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp L73-74

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 1191 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 1324 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 1341 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 1368 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 1395 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 2434 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 4448 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 4449 core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 4650 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 4891 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5021 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 5058 docs/dev/debt/libSceAgcDriver.md
PR 5371 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5372 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 5426 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5431 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5499 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 5925 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/ARCHITECTURE.md
docs/dev/debt/libSceAgcDriver.md
PR 5958 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5998 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5999 docs/dev/debt/libSceAgcDriver.md
PR 6044 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ARCHITECTURE.md
PR 6054 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6082 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6176 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6177 core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 6182 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6188 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6192 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6194 docs/dev/debt/libSceAgcDriver.md
PR 6197 core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 6222 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6223 docs/dev/debt/libSceAgcDriver.md
PR 6259 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6273 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6292 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 6300 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6320 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 6373 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6412 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ARCHITECTURE.md
docs/dev/debt/libSceAgcDriver.md
PR 6447 core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 6481 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6492 docs/dev/debt/libSceAgcDriver.md
PR 6498 docs/dev/debt/libSceAgcDriver.md
PR 6499 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6503 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ARCHITECTURE.md
PR 6535 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6548 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6563 docs/dev/debt/libSceAgcDriver.md
PR 6615 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 6727 docs/dev/debt/libSceAgcDriver.md
PR 6741 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6747 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6763 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/ARCHITECTURE.md
PR 6783 docs/dev/debt/libSceAgcDriver.md
PR 6786 core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6788 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6804 docs/dev/debt/libSceAgcDriver.md
PR 6808 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6867 docs/dev/debt/libSceAgcDriver.md
PR 6931 docs/dev/ARCHITECTURE.md
PR 6941 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6946 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/ARCHITECTURE.md
docs/dev/debt/libSceAgcDriver.md
PR 6947 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/ARCHITECTURE.md
docs/dev/debt/libSceAgcDriver.md
PR 6971 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6973 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6977 docs/dev/debt/libSceAgcDriver.md
PR 6987 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6989 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6995 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7002 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7154 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7201 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp
PR 7223 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7258 docs/dev/debt/libSceAgcDriver.md
PR 7261 core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 7332 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp
docs/dev/debt/libSceAgcDriver.md

Retiring a Windows import whose settle read was still running waited for
the read's batch under the imports mutex, so lookups on every thread
waited with it. The retired import is now kept with that batch and freed
once the batch has completed. Only a guest unmap of its memory still
waits for the batch, in the pin waiter, which waits for the main queue
there anyway.

The [buffers] profile line counts these retirements and the unmaps that
waited, apart from the imports of 1 MiB or more settled inside their
window, which the old wait counter mixed with the retirement waits.

agc_import_settle_retire replaces a 64 KiB window import with the import
of its whole range, 64 times, right after its settle read was submitted.
No replacement may wait, at least one must find the read running, and
the last import must return to the write watch.
…mport

A Windows import under 1 MiB returns to the write watch once its settle
read has completed, though main-queue work submitted before the read may
have used the import first, and an earlier import of the same pages may
not have run its first use yet. On the NVIDIA driver (617.42) neither
marks the pages once the read has completed, so the rewatch needs no wait
for the main queue. The docs said the marking lasts until the import's
first GPU use completes; they now say that a completed GPU use ends it,
the settle read included.

agc_import_settle_first_use submits 4096 copies of a 64 KiB import on
the main queue before its settle read is queued. The import must return
to the write watch while the copies still run, and the driver must mark
none of its pages afterwards.

agc_import_settle_earlier_import records 4096 copies of a 64 KiB window
import, replaces it with the import of its whole 128 KiB range, and
submits the copies only once the range is back in the write watch. The
driver must mark none of the range's pages while they run. A driver that
keeps marking fails either test.
@ps54all

ps54all commented Oct 16, 2026

Copy link
Copy Markdown

Data point from Metal Gear Solid 4 (PPSA18194, Master Collection) on Windows 11, RTX 3080 (NVIDIA 610.74), with this PR picked onto main 5d9dcc4 plus #6188, #6192/#6187, #6273 and #6292.

  • No regression seen. MGS4 gameplay frame rate is unchanged, though: the imports that cost MGS4 the most are not settled. Its DCC key ranges sit at the start of host imports of 0x28000 bytes (one is 0x48000), directly after their 0x1fe0000-byte surfaces, e.g. keys 0x2b86e0000+0x1fe00 for the surface at 0x2b6700000. The settle requires bytes % settleBlockBytes == 0, so they stay compared. ProvedClearKeys then never gets a collect (its CollectWrites over the key range returns 0), every lookup rescans the keys, and in gameplay that is about 230k scans, 26 GB and 1.1 s per 10 s on the draw committer, with 0 proofs.
  • I tried settling such imports and returning only their whole 64 KiB blocks (the shared tail block stays compared). The key proofs then held, but the title stopped drawing two UI texts (the autosave notice and "PRESS ANY BUTTON"), which it draws with this PR alone. So it is not that simple, and I did not dig further. I'm mentioning it in case the partial-block case is on your list.
  • agc_import_settle_first_use failed 5 of 15 runs under ctest here, always with "(f) the main-queue copies had finished before the import returned to the write watch, so its read did not race them". That is, it fails when the race it checks does not occur, rather than reporting itself inconclusive.

AI-assisted (Claude Code); numbers from runs on my machine with APS5_PROFILE_DRAW=1.

…first

The first-use check needs the main-queue copies of an import to be still in flight when the import returns to the write watch. On a fast device they can finish first, and the test then failed although the driver did nothing wrong. Retry with 4 and 16 times as many copies, and report the case as not tested when the copies finish first every time.
@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

@ps54all thanks for trying it on Metal Gear Solid 4 (PPSA18194), and for the numbers.

  • The first-use canary: you're right, it failed when the main-queue copies finished before the import returned to the write watch, which says nothing about the driver. 141d605 retries with 4 and 16 times as many copies, and reports the case as not tested when the copies finish first every time. The test now fails only when the driver marks the import's pages after its settle read.
  • Imports that aren't whole 64 KiB blocks: right, the settle takes only imports whose base and size are whole 64 KiB blocks, so Metal Gear Solid 4's 0x28000-byte DCC key imports stay compared, and ProvedClearKeys never gets a collect over them. That case is outside this PR. Thanks for trying the whole-block version; the two missing UI texts are a useful lead, and I've noted the case with your numbers for a follow-up.

@parl0v

parl0v commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

A Windows NVIDIA data point, and one that goes the other way: RTX 3060 12 GB (NVIDIA 617.42), i5-12400F, Windows 10 19045, MinGW GCC Release. Minecraft Dungeons II (PPSA16064 01.100.100), title screen, no input, 200 s rows, APS5_PROFILE_DRAW=1 APS5_PROFILE_GPU=1; main 93e54ce vs main + this PR, interleaved with three other PRs in one batch, two rounds. FPS is the mean of the last 10 [present] windows, GPU busy and idle gaps per frame over the last 8.

libs FPS GPU busy idle gaps program-keyed GPU time per 10 s
main 6.61 / 6.61 104 / 105 ms 46 / 46 ms 2477 ms
+ this PR (141d605) 6.50 / 6.53 (-1.4 %) 117 / 117 ms 36 / 36 ms 3266 ms

The idle gaps shrink as intended (-10 ms per frame), but GPU busy grows by 13 ms per frame, so the frame rate drops a little. The command classes are unchanged (draw 32.3 / 31.5 ms, storage write-back 17.4 / 17.2 ms per present); the growth is all in the title's compute dispatches. By rank (the program addresses move between runs), both rounds:

dispatch, by GPU time main this PR
1st 396-414 ms per 10 s 655 ms (+60 %)
2nd 230-233 ms 483-485 ms (+108 %)
3rd 94-99 ms 231 ms (+140 %)

So the heaviest dispatches run 1.6-2.4x longer on the GPU when the sub-1 MiB settles no longer wait. My guess is that those dispatches now read buffers that are still on the slower path because their import wasn't settled yet when they were recorded; a [guestmem] / import breakdown on your side would tell. 0 fatal in all rows.

AI-assisted: yes (Claude Code)

@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

@parl0v thank you, this is the cost side we hadn't measured: the idle gaps shrink by 10 ms per frame as intended, but the heaviest compute dispatches take 1.6–2.4× longer, so the frame rate drops a little on the 3060. We'll look at where those dispatches' buffers come from with and without this PR, a settled import read from host memory or a device copy, with an import breakdown, and report back here.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 16, 2026
@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

Thanks for the numbers, @parl0v. From the code, #6678 doesn't change which buffer a read binds. An import under 1 MiB is read in place from host memory on both trees; main creates it too, unwatched.

What #6678 changes is the write tracking. Once its settle read lands, the import is back in the write watch, and watched memory takes the unit-shadow path (ShadowDestinationFor in UnitShadow.cpp). A storage image's write-back then goes to a device-local slab instead of straight into the import. Units it covers only partly are first seeded from the import, and the slab is published back when something reads the range. On main these imports stay unwatched, so none of that runs for them. If those three dispatches write small storage images, the seeds and publishes would be new GPU work per frame. They may also land in the dispatches' own GPU time.

Could you run #6678 once more with APS5_NO_UNIT_SHADOW=1 (same title screen, same profile env)? If those dispatches drop back to main's GPU time, that's the cause. The fix would then keep imports under 1 MiB off the shadow path, or skip the seed for them.

Two lines from each tree, over the same window, would also help:

If your per-program lines have dispatch counts too, the per-dispatch time would rule out a change in how often they run.

Main's boykopovar#5905 settles Linux dma-buf imports with settleImport, which is
now built on both platforms. The batched settle of Windows imports under
1 MiB, its counters and adoptSettleDevice's batch reset stay Windows-only,
and ClearHostImports drains the batches only there.
…ERRED_SETTLES

The deferred settle of small Windows imports could only be turned off
through the test hook. The variable turns it off for a run, so a title can
be compared with and without it on one build; unset, nothing changes.
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 16, 2026
@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

@parl0v #6678 now also has APS5_NO_DEFERRED_SETTLES=1 (5f822a5), which keeps every import under 1 MiB compared, as on main. If you can, run it next to the APS5_NO_UNIT_SHADOW=1 run, so the settle's own cost and the unit-shadow path can be told apart.

…a pin wait

The Windows release of imports retired during a settle read belongs after the whole
overlapsImport block in the not-holding-the-lock branch of WaitForLeasesAndImports, as in
the original change, not inside it. It was placed inside while main was merged in.
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 18, 2026
@oneandonlydean
oneandonlydean merged commit 755fecb into boykopovar:main Oct 18, 2026
9 checks passed
Scramblrrrr pushed a commit to Scramblrrrr/AnyPS5 that referenced this pull request Oct 18, 2026
…ps it

Since boykopovar#6678, a Windows host import under 1 MiB returns to the write watch
once its deferred settle read has completed, and GuestMemory::Rewatch
stamps every block of it as a CPU store. The return waited only while
storage results were pending over the import. A storage image can take
its generation before the stamp and have results pending only after it:
an image's first upload takes its generation and then imports its block,
a recorded draw marks its targets pending once it is recorded, and the
settle is polled at the import lookups between the two. The image's
next refresh then takes the stamp for a CPU store over its results,
drops them and uploads the guest bytes again, so the GPU's output is
lost. On an RTX 3070 this also splits the first two draws of
agc_driver_address_passes into two passes.

A settled import that a live storage image overlaps now stays compared,
as every import under 1 MiB was before boykopovar#6678. Imports with no image over
them still return to the write watch. Keeping the import compared, rather
than waiting for the image to go, checks each settled import against the
live images once instead of at every poll. The [buffers] line counts the
imports kept compared.

agc_import_settle_storage_image makes an image whose first upload imports
its block, waits for the settle read, clears the image on the GPU, then
refreshes and stores it. On the old code the refresh drops the clear.
adriadam10 pushed a commit that referenced this pull request Oct 24, 2026
Since #6678 a Windows host import under 1 MiB returns to the write watch
when its deferred settle completes (rewatchSettledImports). Grand Theft
Auto V (PPSA04264) imports buffers that are resident direct memory at that
point and later become shared 16 KiB views, when a second mapping of the
same direct memory demotes them. From then on the write watch arms them,
every guest store into them faults and is resumed, and the resumed faults
damage the red zone the game's zlib inflate_fast reloads its pointers
from, so the open world crashes in every run (#9136).

Before #6678 such a range stayed out of the write watch for good and was
compared. Rewatch now marks each range it returns to the watch with
GuestArenaCompareWhenShared first. While the memory stays resident it is
write-watched, as #6678 intends. Once it is shared its collects fail and
it is compared again, as before #6678, instead of being armed.

agc_rewatched_direct_shared maps direct memory resident, takes a block
out of the watch and rewatches it as a settle does, then maps the same
memory a second time. The rewatched block must collect while resident,
then collect as 0 through either mapping, stay unarmed and show the
store through both, while a block that never left the watch is still
collected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants