Repository navigation
fix(agc): keep heap mirror slabs within device memory - #15295
Prophet069 wants to merge 4 commits into
Conversation
A heap mirror slab refused by a full device heap stopped the title. The slab now falls back to host memory, as buffers and images already do, and a refusal lowers the heap mirror budget, evicts down to it and retries; a failed bind gives back the blocks it took. A slab is refused before the device heap is exhausted: when all live slabs would pass APS5_HEAP_MIRROR_MIB or when the device heap would keep less than 256 MiB of its reported budget, so the shrink and sweep path runs before Windows pages device memory. A budget lowered by refusals is a soft target a mirror may pass up to the configured budget when nothing can be evicted, and the headroom refusal is not repeated within two seconds on one thread. Blocks are taken from the fullest slab, so partly used slabs drain and are freed. Nonzero scans remember each 64 KiB block with its write generation and read only blocks written since. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
XairRrR
left a comment
There was a problem hiding this comment.
Reviewed — changes requested
The merged tree builds and the heap mirror and resident tests pass. However, a new pre-allocation refusal can now stop a title on a nearly full device heap when the heap mirrors hold 256 MiB or less, where main would have tried the allocation. None of the new paths has a test.
What I ran
Head 1743177ae4af40502f26cd384d2376d555074681, main 5cb015caaaaf83f8ee431414ee6a457b5b307d49, merged tree 7895dfbc86c6627273ff29b2d3bbc7eed5af335f.
git diff --check 5cb015caaaaf83f8ee431414ee6a457b5b307d49...1743177ae4af40502f26cd384d2376d555074681
python3 tools/check_conventions.py --base 5cb015caaaaf83f8ee431414ee6a457b5b307d49
ctest --test-dir build --output-on-failure -R "^agc_driver_image_sample_lod_clamp_shader_lane$|^unwrap_self_UnwrapTest_test_new_output_inherits"
# the PR's 4 production files restored from main:
ninja -C build -j32 -l32 agc_driver_image_sample_lod_clamp_tests
ctest --test-dir build --output-on-failure -R "^agc_driver_image_sample_lod_clamp_shader_lane$"
# merged files restored, git status --porcelain empty:
ninja -C build -j32 -l32
ctest --test-dir build -j8 --test-load 32 --output-on-failure -R "heap_mirror|resident|host_import|partially_resident"
Results
| Check | Result |
|---|---|
git diff --check |
clean |
check_conventions.py |
All conventions checks passed |
Configure, build, libs (script) |
ok |
| Script test run | 2178 ran; 2 not on the baseline's failure list, neither caused by the PR (see below); 6 known failures that also fail on main |
agc_driver_image_sample_lod_clamp_shader_lane |
fails with the PR, and still fails after the PR's production files are restored from main 5cb015ca. The baseline (096c4557) is older, so this failure comes from main, not this PR. |
unwrap_self_..._destination_directory_group |
fails at os.chown on a temporary directory (EINVAL). This is a tools/ Python test the PR doesn't touch, and the failure comes from the sandbox. |
| Heap mirror, resident and host-import tests (27) | 26 passed, 1 skipped (agc_host_import_sparse, an expected skip) |
| Validation layer (script, 759 tests) | layer loaded in 758, missing in none; new VUID- lines: 0 |
lod_clamp failure, same output with the PR and on main
AGC graphics: image_sample_lz through the S# that holds the LOD at 0 with T# MIN_LOD 768 over BASE_LEVEL 0 with a second S# of words 1 0 and 2 5242880, and a second T# of MIN_LOD 0 and LAST_LEVEL 0: thread 0 component 1 is 0.000000, expected 0.062500
Negative control
There is none, because the PR adds and changes no tests. I used the restore-from-main procedure only to clear the PR of the lod_clamp failure (see Results).
Findings
Blocking
-
The headroom refusal stops the title when the heap mirrors hold 256 MiB or less. In
core/libs/prx/libSceAgcDriver/Graphics/src/ResidentMirror.cpp:153,Take()callsrefuseSlab()before anyvkAllocateMemory. AtResidentMirror.cpp:188,refuseSlab()throws when the device heap would keep less than 256 MiB of its reported budget. The only recovery isshrinkHeapMirrorBudget, and it returnsfalsewhenheld <= step(256 MiB) (core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp:2823). Both callers then rethrow the error wrapped inheapMirrorError:growResidentatGuestBufferMemory.cpp:2852-2866;acquireMirroratGuestBufferMemory.cpp:3396-3407.
So when the device heap is close to its budget for reasons other than the heap mirrors (textures, render targets), the first slab of a small mirror is refused with nothing to evict, and the title stops. On
mainthe same slab would go tovkAllocateMemory. With this PR it would even fall back to host memory, but that fallback is never reached. This is most likely on GPUs with little VRAM, which this PR is meant to help.The
APS5_HEAP_MIRROR_MIBrefusal atResidentMirror.cpp:184hits the same dead end. It counts whole 16 MiB slabs, fragmentation included, whilestate.heapBytescounts only accounted blocks. A small configured budget can therefore refuse a slab whileheld <= 256 MiB.Suggested fix: treat a refusal as a request to shrink and sweep, not as a final error. When
shrinkHeapMirrorBudgetcan't lower anything, or the retries run out, go on to the real allocation and its host-memory fallback. Add a test that sets a heap budget the refusal triggers on (or a test hook forReportedHeap) with fewer than 256 MiB of mirrors, and checks that the mirror is still created. -
None of the new behaviour has a test. The PR changes several things:
- slab refusal at the configured budget and at device headroom;
- budget shrink and retry;
- the host-memory fallback for slabs;
- returning blocks when
Bindfails partway; - taking blocks from the fullest slab;
- the generation-keyed nonzero-scan cache.
It adds no test for any of them. All 27 existing heap mirror, resident and host-import tests pass with and without the change, so none of them covers these paths. Finding 1 shows a regression these paths can hide. At least the refusal/shrink/retry path and the
Bindgive-back need a test that fails onmain(SetHeapMirrorBudgetalready exists for that). A scan-cache test should write a block after a first scan and check that the next scan sees it.
Non-blocking
- The scan cache trusts
UnchangedSinceeven for content the write watch may not record. AtResidentMirror.cpp:401, a cached "zero" result is reused whileGuestMemory::UnchangedSinceholds. On Windows,possiblyNonzeroreturns the whole range as readable, so the result there depends entirely on the write watch.docs/dev/debt/libc.mddescribes direct-memory pages that are kept in a private reservation while unmapped and mapped again later, and pages mapped at a second address that become views tracked by write protection. Content can then appear at a guest address without a CPU write there. If no stamp covers that case, the mirror leaves a nonzero block unbacked, and the GPU reads zeros. Please confirm that guest maps and alias writes stamp the tracker, or useCollectCommittedWrites/UnchangedCommittedSince(which also check refusals). If neither covers it, record the gap indocs/dev/debt/libSceAgcDriver.md. - The scan cache can use a lot of host memory. At
ResidentMirror.cpp:382-383, the map grows to 2^22 entries before it is cleared. That is a few hundred MiB ofunordered_mapnodes and buckets for a process-lifetime cache, and the whole cache is then dropped at once. A per-heap bitmap of generations, or a smaller limit, would bound this better. - The lowered budget never recovers.
heapMirrorMemoryLimit(GuestBufferMemory.cpp:2714) is only ever set from the currentheapBytesminus 256 MiB, never back toward the configured budget. A single transient headroom refusal, for example during a loading spike, caps the heap mirrors for the rest of the run. This is mitigated by the "soft target", but it is still surprising. - Refusals are recognised by their error text.
shrinkHeapMirrorBudgetmatches"of a heap mirror slab"inerror.what()(GuestBufferMemory.cpp:2816). A dedicated exception type would be more robust to message edits. - New tuning constants aren't recorded as technical debt. The 256 MiB reserve, the 2 s per-thread throttle, the 8 retries, 256 MiB shrink step and the 2^22-entry cache limit are unmeasured choices.
docs/dev/debt/libSceAgcDriver.mdalready lists similar choices for the heap mirror reference storage ("unmeasured choices"); these belong there too.
Not verified
- I couldn't reproduce Finding 1 at runtime: lavapipe reports a 64 GiB budget, so the headroom refusal never triggers here. The finding comes from reading the code paths cited above.
- The host-memory fallback for slabs, and whether sparse binding of host-visible memory works on the drivers the PR targets (NVIDIA on Windows).
- The Death Stranding 2 (PPSA02015) result and the Windows/NVIDIA test run in the description. No title was run.
- Whether guest remaps and alias writes stamp the write tracker (Non-blocking 1).
To get this to a good state
- Make a slab refusal that can't shrink anything go on to the real allocation (with its host fallback) instead of throwing.
- Add tests that fail on
mainfor the refusal/shrink/retry path, theBindgive-back on failure, and the scan cache seeing a later write. - Confirm or document how the scan cache stays sound for remapped or aliased guest memory.
- Record the new constants in the debt file.
✅ NVIDIA GPU test: passedPR head
Unstable on main (3, these also failed in recent main runs on this machine; not counted against this PR)Failing on main too (2, not counted against this PR)Validation IDs already present on main (4 IDs in 517 tests)Skipped (44)ReproduceAutomated build and test on a contributor's NVIDIA machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs. |
|
Hi Prophet069, Thanks for the heap mirror slab work. Lowering the budget on a refused slab, evicting and retrying is a sensible way to keep a full device heap from stopping the title, and the NVIDIA run at this head shows no new failures. Two requests before it can go in:
|
…ost fallback A slab refused for headroom or for APS5_HEAP_MIRROR_MIB while the heap mirrors held 256 MiB or less stopped the title: the budget shrink has nothing to evict there. Refuse a slab only while more than that is held, so the allocation and its host-memory fallback run otherwise. Refusals and failed slab allocations throw HeapMirrorSlabRefused instead of being matched by their text, a budget lowered by a refusal recovers after 30 s, and the headroom check uses HeapReport::Fits. A test reports the device heap full and checks that small mirrors are still made. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the review. Addressed in 89e3292:
Re-tested on Windows 10, NVIDIA RTX 3080 Laptop (616.64) with the validation layer: the 9 |
✅ NVIDIA GPU test: passedPR head
Unstable on main (2, these also failed in recent main runs on this machine; not counted against this PR)Failing on main too (3, not counted against this PR)Validation IDs already present on main (4 IDs in 525 tests)Skipped (49)ReproduceAutomated build and test on a contributor's NVIDIA machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs. |
XairRrR
left a comment
There was a problem hiding this comment.
Re-checked — changes still requested
Commit 89e3292072 now lets a refusal fall through to the real allocation while the live slabs hold 256 MiB or less. It replaces text matching with HeapMirrorSlabRefused, lets a lowered budget recover after 30 s, and adds one full-device-heap test, whose negative control fails as it should. Three paths still have no test: the shrink/evict/retry path, the Bind give-back and the scan cache. A refusal can also still stop the title when fragmented slabs keep the slab count above the gate while the accounted heap mirror bytes are below the shrink step.
What changed since my last review
- New commit
89e3292072("let a small heap mirror reach the slab allocation and its host fallback"). The first commit1743177ae4is unchanged.refuseSlab()returns early while the live slabs hold 256 MiB or less.- Refusals and slab allocation failures throw
HeapMirrorSlabRefused. heapMirrorBudget()restores a lowered limit after 30 s.- The headroom check uses the new
Context::HeapReport::Fits. - New test hook
SetDeviceHeapFullForTestsand testFullDeviceHeapTestsintests/execution/ResidentHeapMirror.cpp.
- The author replied in the discussion with a negative control and a Windows/RTX 3080 Laptop re-run. I accept that run as author-reported.
What I ran
Head 89e3292072015ff5710305f6fd2e097545a0bb50, main d1ae628bc2f62459cce1e9872ba5632c938c71f2, merged tree e656cfa05a1d3ab6e971f1f891ce43a02603f091.
git diff --check d1ae628bc2f62459cce1e9872ba5632c938c71f2...89e3292072015ff5710305f6fd2e097545a0bb50
python3 tools/check_conventions.py --base d1ae628bc2f62459cce1e9872ba5632c938c71f2
ctest --test-dir build -j8 --test-load 32 --output-on-failure -R "heap_mirror|resident"
ctest --test-dir build -V -R "^agc_driver_resident_heap_mirror$"
# negative control: in ResidentMirror.cpp:188, `if (live <= Reserve) return;` replaced by `(void)live;`
ninja -C build -j32 -l32 agc_driver_resident_heap_mirror_tests
ctest --test-dir build --output-on-failure -R "^agc_driver_resident_heap_mirror$"
build/tests/agc_driver_resident_heap_mirror_tests
# merged file restored from e656cfa05a, git status --porcelain empty
ninja -C build -j32 -l32 agc_driver_resident_heap_mirror_tests
ctest --test-dir build -R "^agc_driver_resident_heap_mirror$"
| Check | Result |
|---|---|
git diff --check |
clean |
check_conventions.py |
All conventions checks passed |
Configure, build, libs (script) |
ok |
| Script test run | 2142 ran, 0 new failures, 0 timeouts; 7 known failures that also fail on main (baseline = main d1ae628b) |
| Validation layer (script, 767 tests) | layer loaded in 766, missing in none; new VUID- lines: 0 |
| Heap mirror and resident tests (16) | all passed; agc_driver_resident_heap_mirror 2.1 s. The sparse branch runs on lavapipe (no skip message), so FullDeviceHeapTests is exercised. |
Negative control
The new test calls SetDeviceHeapFullForTests, which main does not have, so it cannot be built against main's production files. Instead I removed only the new early return in refuseSlab (ResidentMirror.cpp:188). The test then fails with the same message the author reported:
output
AGC graphics: heap mirror of 0x7f2aa9ff0000+0x2000000: vkAllocateMemory of a heap mirror slab refused: its device heap would keep less than 256 MiB of its reported budget; heap mirrors hold 15 MiB of 24576 MiB (APS5_HEAP_MIRROR_MIB)
With the merged file restored, the test passes and the worktree matches the merged tree.
Previous findings
- Blocking 1 (headroom refusal stops the title with ≤ 256 MiB of mirrors): resolved for the case I described. A refusal is now skipped while the live slabs hold 256 MiB or less, and the negative control above shows the test covers it. A related gap remains; see new Blocking 1.
- Blocking 2 (no tests for the new paths): still open. One test was added, for the small-mirror headroom case. Still untested: the refusal →
shrinkHeapMirrorBudget→ sweep → retry path, theBindgive-back whenTake()orsubmitfails partway, the host-memory fallback for slabs, and the generation-keyed cache inGuestNonzeroBlocks. oneandonlydean's review asked for the same three cases. TheAPS5_HEAP_MIRROR_MIBrefusal can be forced deterministically withSetHeapMirrorBudget: build more than 256 MiB of slabs (or lower the gate for tests), then check the "budget lowered" path evicts and the retry succeeds. - Non-blocking 1 (scan cache trusts
UnchangedSincefor remapped or aliased content): still open. The code is unchanged (ResidentMirror.cpp:409), with no reply and no debt entry. - Non-blocking 2 (scan cache up to 2^22 map entries): still open (
ResidentMirror.cpp:419). - Non-blocking 3 (lowered budget never recovers): resolved.
heapMirrorBudget()restores the limit 30 s after the last lowering (GuestBufferMemory.cpp:2734). - Non-blocking 4 (refusal matched by error text): resolved.
shrinkHeapMirrorBudgetusesdynamic_cast<const HeapMirrorSlabRefused*>(GuestBufferMemory.cpp:2834). - Non-blocking 5 (tuning constants not in debt): still open. The PR now also adds a 30 s recovery; no file under
docs/dev/debt/changes.
New findings
Blocking
-
The refusal gate and the shrink gate count different things, so a refusal can still stop the title.
refuseSlab()skips the refusal on live slab memory (Slabs().slabs * 16 MiB,ResidentMirror.cpp:187-188).shrinkHeapMirrorBudget()gives up on accounted heap mirror bytes (state.heapBytes <= 256 MiB,GuestBufferMemory.cpp:2841).
A slab is freed only when every block in it is free (
ResidentMirror.cpp:175). After evictions, surviving mirrors keep blocks scattered across many slabs, so the live slabs can stay well above 256 MiB whileheapBytesis far below it. That is exactly the state the shrink-and-sweep path leaves behind.What happens then on a device heap near its budget:
- The headroom refusal fires.
shrinkHeapMirrorBudgetreturnsfalse.- Both callers (
GuestBufferMemory.cpp:2872and:3416) rethrow without a retry, so the title stops.
mainwould have attempted the allocation, and with this PR that allocation even has a host fallback. The 2 s throttle (ResidentMirror.cpp:193-198) does not help, because the first refusal is already final. TheAPS5_HEAP_MIRROR_MIBrefusal has the same mismatch.Suggested fix: when
shrinkHeapMirrorBudgetreturnsfalsefor aHeapMirrorSlabRefusedthat came fromrefuseSlab(not from a failed allocation), retry once with the refusal bypassed, so the real allocation and its fallback run. Alternatively, gaterefuseSlaband the shrink on the same counter. Add a test that fragments slabs past the gate (or lowers the gate through a hook) with a full device heap, and checks the mirror is still made.
Non-blocking
- The
shrankreport prints whenever the last slab is freed.count % 32 == 0is true forcount == 0(ResidentMirror.cpp:181), so each mirror whose release empties the slab list logs a line. The test log shows several[heap-mirror] shrank: slabs 0 (0 MiB) ...lines in a row. Suggested fix: usecount != 0 && count % 32 == 0. - The recovery returns the budget to its full value in one step. After 30 s, the limit goes straight back to the configured budget (
GuestBufferMemory.cpp:2734). Under sustained pressure, this can cycle every 30 s: the mirrors grow, a refusal fires, the budget shrinks and the sweep runs again. Raising the limit in steps, or only while the heap reports headroom, would avoid the cycle. This is not shown on hardware; it comes from reading the code.
Not verified
- The new Blocking 1 at runtime. Lavapipe reports a large heap budget, and the test hook only covers the small-mirror case; the finding comes from reading the cited code.
- The host-memory fallback for slabs, and sparse binding of host-visible memory on NVIDIA/Windows.
- The Windows/RTX 3080 Laptop re-run and the PPSA02015 result (author-reported; no title was run).
- Whether guest remaps and alias writes stamp the write tracker (previous Non-blocking 1).
To get this to a good state
- Make a refusal that
shrinkHeapMirrorBudgetcannot act on go on to the real allocation, whatever the slab fragmentation. - Add tests for the refusal → shrink → sweep → retry path, the
Bindgive-back, and the scan cache seeing a write made after an earlier scan cached the block as zero. - Confirm or record in
docs/dev/debt/libSceAgcDriver.mdhow the scan cache stays sound for remapped or aliased guest memory, and record the new constants (256 MiB reserve and step, 2 s throttle, 8 retries, 30 s recovery, 2^22-entry cache limit).
✅ AMD GPU test: passedPR head
Unstable on main (1, these also failed in recent main runs on this machine; not counted against this PR)Failing on main too (8, not counted against this PR)Validation IDs already present on main (4 IDs in 525 tests)Skipped (58)ReproduceAutomated build and test on a contributor's AMD machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs. |
|
This PR's failure mode, seen in a real run: Ghost of Tsushima DC (PPSA02225) on a 16 GB RAM / 8 GB VRAM Windows laptop (RTX 4060 Laptop), main b11c7bf + #12840 + #12792, aborted at 135 s with The second part of this PR, nonzero scans that read only blocks written since the last scan, also matches what we profiled there. Before each scan on Windows, Context and screenshots: #5166 (comment). I'll test this PR (together with #15296) on the same machine and report back. |
|
Results from Ghost of Tsushima DC (PPSA02225) on the 16 GB RAM / 8 GB VRAM laptop (RTX 4060 Laptop, driver 617.42). This PR was applied onto
Logs are available on request. |
…e budget cannot shrink The slab refusal gate counts live slab memory, while the budget shrink counts the bytes of listed heap mirrors. Mirrors that are unlisted but still held by recorded work keep their slab blocks live after their bytes leave the count, so a refusal can arrive with nothing left to shrink, and both callers then failed the mirror. When the shrink cannot act, retry once with the slab refusal waived, so the allocation runs as it did before the refusal existed. Add a test that lowers the refusal gate with a full device heap. Co-authored-by: Prophet069 <291514246+Prophet069@users.noreply.github.com>
|
Hi @Prophet069, we pushed |
Addressed in c75bd0a. The fragmented-slab stop as stated does not occur: Take hands out free blocks before refuseSlab runs, so fragmentation alone never reaches a refusal. The same gate mismatch was reachable another way (mirrors unlisted while recorded work still holds their slab blocks), and c75bd0a fixes it: when shrinkHeapMirrorBudget cannot act on a refusal, the mirror retries once with the refusal waived, so the allocation runs as before. FullDeviceHeapPastReserveTests covers it and fails without the change; heap_mirror and resident tests pass 16/16 locally. The other points ask for more test coverage of working paths and name no defect.
✅ NVIDIA GPU test: passedPR head
Unstable on main (2, these also failed in recent main runs on this machine; not counted against this PR)Failing on main too (3, not counted against this PR)Passed on retry (2, failed in the parallel run, passed when re-run with a 360 s timeout; not counted against this PR)Validation IDs already present on main (9 IDs in 585 tests)Skipped (51)ReproduceAutomated build and test on a contributor's NVIDIA machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs. |
❌ NVIDIA GPU test: failedPR head
Failing testsThis PR changes test files, so each failing test was also built and run on main with only the PR's test changes applied. ✅ there means the failure comes from the PR's other changes; ❌ means the test fails on main too, so this run does not show the PR caused it.
agc_submit_progress: Failed (also failed when re-run alone), assertion and diagnosticsUnstable on main (2, these also failed in recent main runs on this machine; not counted against this PR)Failing on main too (3, not counted against this PR)Passed on retry (1, failed in the parallel run, passed when re-run with a 360 s timeout; not counted against this PR)Validation IDs already present on main (9 IDs in 585 tests)Skipped (51)ReproduceAutomated build and test on a contributor's NVIDIA machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs. |
Superseabas
left a comment
There was a problem hiding this comment.
Windows run of the current head, c75bd0a393, on an RTX 5070 Ti and on the integrated Radeon of a Ryzen 7 7700X, the first AMD result on Windows here. Nothing regresses, and the new FullDeviceHeapPastReserveTests catches the loss of the retry. One non-blocking note on what it doesn't cover.
Head merged onto main ed41d86d32 (clean merge, local only). Windows 11 Home 10.0.26200, WinLibs GCC 16.2.0 (x86_64-ucrt-posix-seh, r3), Release. ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, no missing-layer line, ANYPS5_GPU selecting the device, confirmed by Selected GPU:
- NVIDIA GeForce RTX 5070 Ti (
10de:2c05), 621.01 - AMD Radeon of a Ryzen 7 7700X (
1002:164e, integrated), AMD 26.10.2 (32.0.21045.20006)
Focused set. The 17 tests matching heap_mirror|resident (agc_driver_resident_heap_mirror, the agc_heap_mirror_* and agc_sparse_heap_mirror_* entries, agc_driver_partially_resident_*, agc_resident_* and the others) pass 3 of 3 on both GPUs, on main and on this PR. No VUID- lines. Both devices take the sparse path: agc_driver_resident_heap_mirror prints no skip line, and the [heap-mirror] refused line shows the full-heap cases ran.
Whole AGC set. ctest -R '^agc' -j 8 --timeout 300, 1,388 tests: 1,386 pass on the RTX and 1,385 on the Radeon, the same as main on this machine. Every failure is tracked: agc_driver_direct_write_completion (#15564), agc_driver_quad_pixel_mask on the RTX (#14341), and on the Radeon agc_driver_scratch_pool (#15566) and agc_driver_sample_order_grid_reference (#9503).
Red checks on agc_driver_resident_heap_mirror, 3 runs per GPU, each a local edit of the merged tree:
| Edit | Result |
|---|---|
| none | passes |
both if (!waive && dynamic_cast<const HeapMirrorSlabRefused*>(&error) != nullptr) { waive = true; continue; } retries removed (growResident, acquireMirror) |
fails: vkAllocateMemory of a heap mirror slab refused: its device heap would keep less than 256 MiB of its reported budget |
only if (slabRefusalWaived) return; removed from refuseSlab |
passes |
that line removed, and the headroom throttle (now - lastHeadroomRefusal > std::chrono::seconds(2)) disabled |
fails, the same message |
| the throttle disabled, the line kept | passes |
Non-blocking. The test covers the retry, but not the waive itself. Its refusal is the headroom one, and lastHeadroomRefusal was set by the refusal a moment earlier on the same thread, so the throttle already lets the retry's allocation through, with or without slabRefusalWaived. The waive matters for the other reason, the heap mirrors' slabs would pass APS5_HEAP_MIRROR_MIB, which has no throttle. A case refused through the budget hook, with too little held for shrinkHeapMirrorBudget to act, would cover it.
check_conventions.py --base main passes.
AI-assisted: yes (Claude Code)
What
A heap mirror slab refused by a full device heap stopped the title. The slab now falls back to host memory, as buffers and images already do, and a refusal lowers the heap mirror budget, evicts down to it and retries; a failed bind gives back the blocks it took.
Slabs are refused before the device heap is exhausted: when all live slabs would pass
APS5_HEAP_MIRROR_MIBor when the device heap would keep less than 256 MiB of its reported budget, so the shrink and sweep path runs before Windows starts paging device memory. A budget lowered by refusals is a soft target that a mirror may pass up to the configured budget when nothing can be evicted, and the headroom refusal is not repeated within two seconds on the same thread.Blocks are taken from the fullest slab, so partly used slabs drain and are freed instead of every slab staying partly used after evictions.
Nonzero scans of a heap remember each 64 KiB block with its write generation and read only blocks written since, instead of reading the whole multi-GiB heap again on every rebuild.
Tested
NVIDIA GeForce RTX 3080 Laptop GPU, driver 616.64, Windows 10 Pro 22H2 (19045.6466).
Focused tests on
main(9090972) and on this branch withVK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation,ANYPS5_NO_SHADER_CACHE=1,ANYPS5_REQUIRE_VULKAN=1,ctest -V -R "heap_mirror|^agc_driver_graphics$|^agc_driver_key_miss$|^agc_driver_bda_device$|^agc_driver_recorder$|native_multisample_depth|depth_storage|^agc_host_import_retire$|^agc_host_import_sparse$":main: 17 of 20 selected tests passed (agc_host_import_sparse skips on this machine; the three agc_driver_depth_storage_* tests were not built in this run); validation layer loaded in both logs;VUID-errors: none.VUID-errors: none.Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file.mdfiles or images added to the repository (attach them to this PR)🤖 Generated with Claude Code