perf(vk): sparse VRAM mirrors, memory only where a game pool is (RX_VK_MIRROR_SPARSE=1) #2794

Open
github-sync wants to merge 9 commits from github/pr/2794 into main
Owner

@SadTransGirl opened this pull request on GitHub · 2026-11-12 13:37 UTC · original

On cards short of video memory the buffer pools' VRAM mirrors hold hundreds of MB that no game pool is in. This makes them sparse, opt-in. On a 12 GB card held to a 6 GB card's allowance (RX_VK_TEST_ALLOWED_MB, not a real 6 GB card), the field walk went from 47.7-51.5 fps to 55.3-58.2 fps over three pairs. Held to 8 GB, and at 12 GB, there's no fps change, but the VRAM peak is lower. Loads are no slower.

Cause. A pool's mirror covers its import's whole range, and imports are made by chunk around what was bound (import_chunk). A temporary measuring patch counted, per 64 KB block of the live mirrors, whether any guest_pools entry was in it. Of 1.1-1.6 GB of mirrors, 243-533 MB had none (DLAA 2560x1440: Hyrule Field, Kokiri, and up to 533 MB after Castle Town to the field): chunk padding, and pools the game had let go. On an 8 GB card's allowance (6,900 MB) the field walk peaked at 6,952 MB, with 321 frames over 33 ms in five minutes.

Change (RX_VK_MIRROR_SPARSE=1; off by default, and only with strict sparse residency for buffers):

  • gpu/vk/sparse_mirror.h: which blocks a game pool is in, block spans, runs, and copies clipped to blocks with memory or cut by ranges. gpu/vk/vk_vma.cpp: a sparse buffer, and its blocks' memory from VMA (counted in its budget). vmaFreeMemory joins the recorder's drain boundaries (vk_recorder.h, vk_work.h).
  • mirror_make: a sparse buffer, with memory for the blocks a game pool is in. The page table points blocks with none at guest memory, so the shaders' pointers reach the game's own memory there, as for a pool with no mirror.
  • gpu_buffer_bind: a binding that reaches a block with none is served from guest memory for that submission. It's noted as an in-place read, or, if written, as a direct write with its shadow forgotten, and the block is wanted.
  • At each submission's start:
    • Blocks wanted, and every block a game pool has come into since (guest_pools_gen), get memory in one batched vkQueueBindSparse.
    • They're filled as a new mirror is: guest memory, then the captures' pages, after a full barrier.
    • A written range served from guest memory where blocks had memory is copied in again.
    • The write-back diff's shadow forgets what was filled.
  • Giving memory back:
    • A block that no game pool has been in, and no binding has reached, for 600 submissions gives its memory back.
    • Its page-table entries go back to guest memory at once.
    • Its memory is unbound and freed once the GPU is past it.
    • Without the 600-submission wait, short-lived pools the game frees and makes again at the same addresses churned about 28,000 blocks and 900 binds in one area change.
    • A block wanted again before its memory is freed takes it back without a bind.
  • The binds are not waited for on the CPU:
    • Each such wait waited for the whole queue (about 3 ms); 170-390 of them made the load's longest frame 1.0-1.2 s.
    • Instead they signal a timeline semaphore, and every submission of recordings waits for it on the GPU: the batched flush, a submit, and a parallel segment.
    • No other submission needs to wait. Every read or write of a mirror is recorded in a recording: bindings, write-backs, the captures' copies, texture uploads, and the page table's updates. The other submissions on the queue are the present (the game's image, upscalers and overlays: images only), the texture read-backs (an image into a staging buffer of their own), and the cooperative-matrix probe at start. None of them binds a pool's mirror.
    • Memory an unbind let go is freed once the semaphore has passed it.
    • RX_VK_SPARSE_WAIT=1 waits on the CPU, as before.
  • Write-backs, and the inheritance of a retired mirror, are clipped to blocks with memory on both sides. Inheritance also leaves a retired mirror's pending refills to guest memory. A freed sparse mirror's buffer and blocks go once the GPU and its binds are past them.

Measured. RTX 4070 SUPER 12 GB (617.42), Windows 11, DLAA 2560x1440, unattended, the same build with and without the switch:

Off On
6 GB allowance on the 12 GB card (RX_VK_TEST_ALLOWED_MB=6144, RX_VK_MIRROR_MB=1536), field walk, three pairs: fps 47.7, 51.5, 49.9 57.3, 55.3, 58.2
the same: frames over 25 ms 3,042, 1,840, 2,114 625, 1,160, 408
the same: VRAM peak 7,327, 6,254, 6,444 MB 6,394, 6,200, 6,387 MB
8 GB allowance on the 12 GB card (RX_VK_TEST_ALLOWED_MB=6900), field walk: fps / VRAM peak 59.0-59.1 / 7,028-7,138 MB 59.1-59.3 / 6,676-7,018 MB
Castle Town to the field: fps 54.0, 52.7 55.4, 52.5, 54.8
the same: mirrors with memory — 1,267-1,311 of 2,128-2,144 MB
the same: the load's longest frame 816-902 ms 866-961 ms (1,197-1,232 with RX_VK_SPARSE_WAIT=1)
Kokiri Forest: VRAM mean 8,215 MB 7,462-7,507 MB

Screenshots of Castle Town, Kokiri and the field look the same on and off. No device was lost and no write-back warning appeared in any run. The rows are in docs/tested-configurations.md.

Two independent code reviews of earlier versions found ten problems in all, each fixed as described above:

  • a binding over blocks with and without memory;
  • a block given back and bound again under work still running;
  • the write-back shadow;
  • blocks only pointers reach;
  • the budget;
  • blocks that bindings, but no pool, still reach;
  • refills lost to inheritance;
  • unbinds not batched;
  • the take-back search;
  • a freed mirror's pending blocks.

Tests. tests/test_sparse_mirror.py runs the header: live blocks, an earlier long pool, partial blocks, spans, runs, clipping across both sides' block ends, and cut ranges. It also checks the wiring in vk_memory.cpp, vk_vma.cpp and vk_replay.cpp. The fixtures of test_direct_buffer_writes, test_parallel_init_order, test_draw_stream_visibility and test_fsr_image_retirement get the helpers they now reach. test_readback_part counts the three new reads of guest memory, each noted. 102 checks read these files. 95 pass; the other 7 fail the same way on plain main on this PC (C tools it lacks).

Overlap with open PRs. It merges cleanly with #2711 and #2760. With #2618 (a mirror that found no VRAM asked for again) it conflicts in the same lines of mirror_make (where a whole mirror's buffer is made): no conflict in what they do, since a sparse mirror that cannot be filled falls back to that same whole buffer, where #2618's retry would apply. Whichever lands second takes a small merge, which I'll do if it's this one. #1377, #2338 and #2609 conflict with main already.

Not tested. AMD (RADV and Windows), Intel, Linux; a real 6 or 8 GB card; a long session.

Part of #993.

🤖 Generated with Claude Code

> [@SadTransGirl](https://github.com/SadTransGirl) opened this pull request on GitHub · 2026-11-12 13:37 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2794) On cards short of video memory the buffer pools' VRAM mirrors hold hundreds of MB that no game pool is in. This makes them sparse, opt-in. On a 12 GB card held to a 6 GB card's allowance (`RX_VK_TEST_ALLOWED_MB`, not a real 6 GB card), the field walk went from 47.7-51.5 fps to 55.3-58.2 fps over three pairs. Held to 8 GB, and at 12 GB, there's no fps change, but the VRAM peak is lower. Loads are no slower. **Cause.** A pool's mirror covers its import's whole range, and imports are made by chunk around what was bound (`import_chunk`). A temporary measuring patch counted, per 64 KB block of the live mirrors, whether any `guest_pools` entry was in it. Of 1.1-1.6 GB of mirrors, 243-533 MB had none (DLAA 2560x1440: Hyrule Field, Kokiri, and up to 533 MB after Castle Town to the field): chunk padding, and pools the game had let go. On an 8 GB card's allowance (6,900 MB) the field walk peaked at 6,952 MB, with 321 frames over 33 ms in five minutes. **Change** (`RX_VK_MIRROR_SPARSE=1`; off by default, and only with strict sparse residency for buffers): - `gpu/vk/sparse_mirror.h`: which blocks a game pool is in, block spans, runs, and copies clipped to blocks with memory or cut by ranges. `gpu/vk/vk_vma.cpp`: a sparse buffer, and its blocks' memory from VMA (counted in its budget). `vmaFreeMemory` joins the recorder's drain boundaries (`vk_recorder.h`, `vk_work.h`). - `mirror_make`: a sparse buffer, with memory for the blocks a game pool is in. The page table points blocks with none at guest memory, so the shaders' pointers reach the game's own memory there, as for a pool with no mirror. - `gpu_buffer_bind`: a binding that reaches a block with none is served from guest memory for that submission. It's noted as an in-place read, or, if written, as a direct write with its shadow forgotten, and the block is wanted. - At each submission's start: - Blocks wanted, and every block a game pool has come into since (`guest_pools_gen`), get memory in one batched `vkQueueBindSparse`. - They're filled as a new mirror is: guest memory, then the captures' pages, after a full barrier. - A written range served from guest memory where blocks had memory is copied in again. - The write-back diff's shadow forgets what was filled. - Giving memory back: - A block that no game pool has been in, and no binding has reached, for 600 submissions gives its memory back. - Its page-table entries go back to guest memory at once. - Its memory is unbound and freed once the GPU is past it. - Without the 600-submission wait, short-lived pools the game frees and makes again at the same addresses churned about 28,000 blocks and 900 binds in one area change. - A block wanted again before its memory is freed takes it back without a bind. - The binds are not waited for on the CPU: - Each such wait waited for the whole queue (about 3 ms); 170-390 of them made the load's longest frame 1.0-1.2 s. - Instead they signal a timeline semaphore, and every submission of recordings waits for it on the GPU: the batched flush, a submit, and a parallel segment. - No other submission needs to wait. Every read or write of a mirror is recorded in a recording: bindings, write-backs, the captures' copies, texture uploads, and the page table's updates. The other submissions on the queue are the present (the game's image, upscalers and overlays: images only), the texture read-backs (an image into a staging buffer of their own), and the cooperative-matrix probe at start. None of them binds a pool's mirror. - Memory an unbind let go is freed once the semaphore has passed it. - `RX_VK_SPARSE_WAIT=1` waits on the CPU, as before. - Write-backs, and the inheritance of a retired mirror, are clipped to blocks with memory on both sides. Inheritance also leaves a retired mirror's pending refills to guest memory. A freed sparse mirror's buffer and blocks go once the GPU and its binds are past them. **Measured.** RTX 4070 SUPER 12 GB (617.42), Windows 11, DLAA 2560x1440, unattended, the same build with and without the switch: | | Off | On | |---|---|---| | 6 GB allowance on the 12 GB card (`RX_VK_TEST_ALLOWED_MB=6144`, `RX_VK_MIRROR_MB=1536`), field walk, three pairs: fps | 47.7, 51.5, 49.9 | 57.3, 55.3, 58.2 | | the same: frames over 25 ms | 3,042, 1,840, 2,114 | 625, 1,160, 408 | | the same: VRAM peak | 7,327, 6,254, 6,444 MB | 6,394, 6,200, 6,387 MB | | 8 GB allowance on the 12 GB card (`RX_VK_TEST_ALLOWED_MB=6900`), field walk: fps / VRAM peak | 59.0-59.1 / 7,028-7,138 MB | 59.1-59.3 / 6,676-7,018 MB | | Castle Town to the field: fps | 54.0, 52.7 | 55.4, 52.5, 54.8 | | the same: mirrors with memory | — | 1,267-1,311 of 2,128-2,144 MB | | the same: the load's longest frame | 816-902 ms | 866-961 ms (1,197-1,232 with `RX_VK_SPARSE_WAIT=1`) | | Kokiri Forest: VRAM mean | 8,215 MB | 7,462-7,507 MB | Screenshots of Castle Town, Kokiri and the field look the same on and off. No device was lost and no write-back warning appeared in any run. The rows are in `docs/tested-configurations.md`. Two independent code reviews of earlier versions found ten problems in all, each fixed as described above: - a binding over blocks with and without memory; - a block given back and bound again under work still running; - the write-back shadow; - blocks only pointers reach; - the budget; - blocks that bindings, but no pool, still reach; - refills lost to inheritance; - unbinds not batched; - the take-back search; - a freed mirror's pending blocks. **Tests.** `tests/test_sparse_mirror.py` runs the header: live blocks, an earlier long pool, partial blocks, spans, runs, clipping across both sides' block ends, and cut ranges. It also checks the wiring in vk_memory.cpp, vk_vma.cpp and vk_replay.cpp. The fixtures of `test_direct_buffer_writes`, `test_parallel_init_order`, `test_draw_stream_visibility` and `test_fsr_image_retirement` get the helpers they now reach. `test_readback_part` counts the three new reads of guest memory, each noted. 102 checks read these files. 95 pass; the other 7 fail the same way on plain main on this PC (C tools it lacks). **Overlap with open PRs.** It merges cleanly with #2711 and #2760. With #2618 (a mirror that found no VRAM asked for again) it conflicts in the same lines of `mirror_make` (where a whole mirror's buffer is made): no conflict in what they do, since a sparse mirror that cannot be filled falls back to that same whole buffer, where #2618's retry would apply. Whichever lands second takes a small merge, which I'll do if it's this one. #1377, #2338 and #2609 conflict with main already. **Not tested.** AMD (RADV and Windows), Intel, Linux; a real 6 or 8 GB card; a long session. Part of #993. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- github-sync:item:2794 -->
A pool's VRAM mirror covers its import's whole range, made by chunk around
what was bound: 243-533 MB of the 1.1-1.6 GB of mirrors (DLAA 2560x1440)
held no game pool, chunk padding and pools the game had let go.

With RX_VK_MIRROR_SPARSE=1 (off by default) a mirror is a sparse-residency
buffer whose 64 KB blocks get memory only where a game pool is
(gpu/vk/sparse_mirror.h). A block with none is reached in guest memory:
its page-table entries point there, and a binding that reaches one is
served from guest memory for that submission. At each submission's
start, blocks a binding wanted, and every block a new game pool is in,
get memory in one batched sparse bind and are filled as a new mirror is
(guest copy, captured pages over). A block that no game pool has been in
and no binding has reached for 600 submissions gives its memory back
once the GPU is past it; wanted again before that, it takes it back
without a bind. Write-backs and inheritance copies are clipped to blocks
with memory; a written range served from guest memory is copied in again,
and inheritance leaves such pending ranges to guest memory.

The binds are not waited for on the CPU (each waited for the whole queue,
~3 ms): they signal a timeline semaphore that every submission of
recordings waits for on the GPU, and memory an unbind let go is freed
once the semaphore has passed it. RX_VK_SPARSE_WAIT=1 waits on the CPU as
before. vmaFreeMemory joins the recorder's drain boundaries. Needs strict
sparse residency for buffers.

Measured on an RTX 4070 SUPER (Windows 11, 617.42), the same build with
and without the switch, unattended:
- 6 GB allowance (RX_VK_TEST_ALLOWED_MB=6144, RX_VK_MIRROR_MB=1536),
  five-minute field walk, three pairs: 57.3 / 55.3 / 58.2 fps against
  47.7 / 51.5 / 49.9; frames over 25 ms 625 / 1160 / 408 against
  3042 / 1840 / 2114.
- 8 GB allowance (6900 MB): 59.1-59.3 fps against 59.0-59.1; VRAM peak
  6676-7018 MB against 7028-7138.
- Castle Town to the field: 55.4 / 52.5 / 54.8 fps against 54.0 / 52.7;
  790-860 MB of mirrors spared; the load's longest frame 866-961 ms
  against 816-902 (1197-1232 with RX_VK_SPARSE_WAIT=1).
- Kokiri Forest: VRAM 7462-7507 MB against 8215.
Two independent code reviews; their findings are fixed. Screenshots of
Castle Town, Kokiri and the field look alike either way; no device lost.

tests/test_sparse_mirror.py runs the header and checks the wiring;
test_direct_buffer_writes' and parallel_init_order's fixtures get the
helpers they now reach; test_readback_part counts the new reads of guest
memory. docs/tested-configurations.md has the runs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Author
Owner

@Joey-Magnus requested changes on GitHub · 2026-11-12 14:35 UTC · original

I merged this into current main and ran tests/run_all.py. test_draw_stream_visibility and test_fsr_image_retirement fail here exactly as they do in CI, and they fail because of this change. Both cut submit_recording (and queue_flush_locked, in the fsr one) out of vk_replay.cpp and compile them alone. Those functions now call memory_sparse_wait, which their fixtures don't declare ("use of undeclared identifier 'memory_sparse_wait'"). They need the same bool memory_sparse_wait(VkSemaphoreSubmitInfo*) { return false; } stub you gave parallel_init_order_test.cpp. test_sparse_mirror passes.

It's off by default and gated on sparseResidencyBuffer, residencyNonResidentStrict and a sparse-binding queue, so other tiers aren't exposed. Only the three recording submits wait on the binds' timeline. A sentence on why no other submission (presents, upscalers, texture uploads) can read a newly bound block would help. The 6 and 8 GB rows come from a 12 GB card with a lowered allowance, not real 6/8 GB cards. Your Not tested section says so, but the summary reads as if they were real. Conflicts: #2618 in mirror_make, and #2795 only in docs.

> [@Joey-Magnus](https://github.com/Joey-Magnus) requested changes on GitHub · 2026-11-12 14:35 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2794#pullrequestreview-5876258465) I merged this into current main and ran tests/run_all.py. test_draw_stream_visibility and test_fsr_image_retirement fail here exactly as they do in CI, and they fail because of this change. Both cut submit_recording (and queue_flush_locked, in the fsr one) out of vk_replay.cpp and compile them alone. Those functions now call memory_sparse_wait, which their fixtures don't declare ("use of undeclared identifier 'memory_sparse_wait'"). They need the same `bool memory_sparse_wait(VkSemaphoreSubmitInfo*) { return false; }` stub you gave parallel_init_order_test.cpp. test_sparse_mirror passes. It's off by default and gated on sparseResidencyBuffer, residencyNonResidentStrict and a sparse-binding queue, so other tiers aren't exposed. Only the three recording submits wait on the binds' timeline. A sentence on why no other submission (presents, upscalers, texture uploads) can read a newly bound block would help. The 6 and 8 GB rows come from a 12 GB card with a lowered allowance, not real 6/8 GB cards. Your Not tested section says so, but the summary reads as if they were real. Conflicts: #2618 in mirror_make, and #2795 only in docs. <!-- github-sync:r:5876258465 -->
Both cut submit_recording (and queue_flush_locked) out of vk_replay.cpp, which now call memory_sparse_wait.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Author
Owner

@SadTransGirl commented on GitHub · 2026-11-12 15:43 UTC · original

Thanks. You're right on both failures. On this PC those two checks were failing on a local compiler wrapper (a _CRT_SECURE_NO_WARNINGS redefinition under -Werror), and I had filed them with the ones that fail on main here, which hid the real failure. Fixed in 65fb4b14: both fixtures get the memory_sparse_wait stub. With the wrapper fixed, both fail without the stub and pass with it, and of the 102 checks that read the changed files, the only failures left are 7 that fail the same way on plain main (C tools this PC lacks).

On the other submissions: every read or write of a mirror is recorded in a recording (bindings, write-backs, the captures' copies, texture uploads, the page table's updates), and those go out only through the three submits that wait. The rest of what reaches the queue is:

  • the present: the game's image, the upscalers and the overlays, images only;
  • the texture read-backs: an image into a staging buffer of their own;
  • the cooperative-matrix probe at start.

None of them binds a pool's mirror. I've added that to the description, and made the summary say the 6 and 8 GB rows are a 12 GB card held to those allowances.

> [@SadTransGirl](https://github.com/SadTransGirl) commented on GitHub · 2026-11-12 15:43 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2794#issuecomment-6828248772) Thanks. You're right on both failures. On this PC those two checks were failing on a local compiler wrapper (a `_CRT_SECURE_NO_WARNINGS` redefinition under `-Werror`), and I had filed them with the ones that fail on main here, which hid the real failure. Fixed in 65fb4b14: both fixtures get the `memory_sparse_wait` stub. With the wrapper fixed, both fail without the stub and pass with it, and of the 102 checks that read the changed files, the only failures left are 7 that fail the same way on plain main (C tools this PC lacks). On the other submissions: every read or write of a mirror is recorded in a recording (bindings, write-backs, the captures' copies, texture uploads, the page table's updates), and those go out only through the three submits that wait. The rest of what reaches the queue is: - the present: the game's image, the upscalers and the overlays, images only; - the texture read-backs: an image into a staging buffer of their own; - the cooperative-matrix probe at start. None of them binds a pool's mirror. I've added that to the description, and made the summary say the 6 and 8 GB rows are a 12 GB card held to those allowances. <!-- github-sync:c:6828248772 -->
The per-block idle stamps are block_dead_since now: #2825 added a per-pool dead_since for the trim.
Both docs rows kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both Pool additions kept (the sparse fields, shadow_wanted); both sets of tested-configurations rows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Author
Owner

@Joey-Magnus reviewed on GitHub · 2026-11-12 17:02 UTC · original

Both of my points are fixed. 65fb4b14 gives tests/draw_stream_visibility_test.cpp and fsr_image_retirement_test.cpp the memory_sparse_wait stub. The description now says why only the three recording submits need to wait: nothing else that reaches the queue binds a pool's mirror. It also says the 6 and 8 GB rows come from a 12 GB card held to those allowances.

It conflicts with main again, since #2825 (mirror trim) landed after your last merge. There are two conflicts:

  • docs/development.md: keep both rows, RX_VK_MIRROR_TRIM and RX_VK_MIRROR_SPARSE.
  • gpu/vk/vk_memory.cpp, memory_begin_submission: main's early return now calls trims_settle() before returning, and there's a trims_settle() after the mirror_make loop. Your sparse block has to run before that early return, and both trims_settle() calls have to stay.

Please also check one interaction. #2825 remakes a mostly-unused import's mirror smaller while VRAM is over 80% full. A sparse mirror already holds memory only where a game pool is, so trimming it gains nothing and could fight sparse_release. Skipping p->sparse pools in the trim's candidate check looks like the simplest fix, with a test case.

Once that merge is in and CI is green, I'd approve. It's off by default and gated on strict sparse residency.

> [@Joey-Magnus](https://github.com/Joey-Magnus) reviewed on GitHub · 2026-11-12 17:02 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2794#pullrequestreview-5878654657) Both of my points are fixed. 65fb4b14 gives tests/draw_stream_visibility_test.cpp and fsr_image_retirement_test.cpp the memory_sparse_wait stub. The description now says why only the three recording submits need to wait: nothing else that reaches the queue binds a pool's mirror. It also says the 6 and 8 GB rows come from a 12 GB card held to those allowances. It conflicts with main again, since #2825 (mirror trim) landed after your last merge. There are two conflicts: - docs/development.md: keep both rows, RX_VK_MIRROR_TRIM and RX_VK_MIRROR_SPARSE. - gpu/vk/vk_memory.cpp, memory_begin_submission: main's early return now calls trims_settle() before returning, and there's a trims_settle() after the mirror_make loop. Your sparse block has to run before that early return, and both trims_settle() calls have to stay. Please also check one interaction. #2825 remakes a mostly-unused import's mirror smaller while VRAM is over 80% full. A sparse mirror already holds memory only where a game pool is, so trimming it gains nothing and could fight sparse_release. Skipping p->sparse pools in the trim's candidate check looks like the simplest fix, with a test case. Once that merge is in and CI is green, I'd approve. It's off by default and gated on strict sparse residency. <!-- github-sync:r:5878654657 -->
Author
Owner

@SadTransGirl commented on GitHub · 2026-11-12 17:25 UTC · original

Merged main again (#2825, then f1d0a679's shadow remake and #2760). Both conflicts were as you described: both trims_settle() calls are kept, with the sparse block before the early return. #2825 also added a per-pool dead_since, which clashed with this PR's per-block one, so that is block_dead_since now.

On skipping sparse pools in the trim: I looked at it, and I'd rather keep the trim on for them.

  • It doesn't fight sparse_release: that only walks imported, and a trimmed pool is retired out of it before the release looks. By the time the trim considers a pool (1,800 submissions mostly outside every pool), the release (600) has already given its dead blocks back, so the two never act on the same blocks.
  • It isn't a no-op on a sparse mirror: the pool's write-back shadow (shadow_make, p.hi - p.lo) and its import both cover the whole range, and sparse leaves both as they are. Trimming shrinks them; skipping would keep a full-size shadow in VRAM on the 8 GB cards the trim is for.
  • The new parts get sparse mirrors and inherit through the clipped copy (blocks with memory on both sides), as a pool made again larger does.

If you'd still rather skip them, it's one condition in the candidate loop plus a test case, and I'll add it.

> [@SadTransGirl](https://github.com/SadTransGirl) commented on GitHub · 2026-11-12 17:25 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2794#issuecomment-6830869030) Merged main again (#2825, then f1d0a679's shadow remake and #2760). Both conflicts were as you described: both `trims_settle()` calls are kept, with the sparse block before the early return. #2825 also added a per-pool `dead_since`, which clashed with this PR's per-block one, so that is `block_dead_since` now. On skipping sparse pools in the trim: I looked at it, and I'd rather keep the trim on for them. - It doesn't fight `sparse_release`: that only walks `imported`, and a trimmed pool is retired out of it before the release looks. By the time the trim considers a pool (1,800 submissions mostly outside every pool), the release (600) has already given its dead blocks back, so the two never act on the same blocks. - It isn't a no-op on a sparse mirror: the pool's write-back shadow (`shadow_make`, `p.hi - p.lo`) and its import both cover the whole range, and sparse leaves both as they are. Trimming shrinks them; skipping would keep a full-size shadow in VRAM on the 8 GB cards the trim is for. - The new parts get sparse mirrors and inherit through the clipped copy (blocks with memory on both sides), as a pool made again larger does. If you'd still rather skip them, it's one condition in the candidate loop plus a test case, and I'll add it. <!-- github-sync:c:6830869030 -->
Commenting is not possible because the repository is archived.
No description provided.