perf(vk): 4K is CPU-bound by the memory sync (35.6 fps at 4K vs 60 at 1080p, GPU starved) — finish the remaining legs #2099

Closed
opened 2026-11-11 02:34:48 +00:00 by blessedplummer · 13 comments
blessedplummer commented 2026-11-11 02:34:48 +00:00 (Migrated from github.com)

Why

At 4K the game runs at 35.6 fps where the same scene holds 60 at 1080p, and the GPU sits far under its power limit: the frame is bounded by CPU-side memory-sync work whose size follows the framebuffer's bytes, not the CPU's own traffic. #1629 removed the largest leg (the blind size skip); the legs that are left — the mirror-sync page churn, the capture scans, the write-back compare and the game-thread readback waits — are what this issue proposes to finish. The groundwork is done and reviewed; what is missing is upstream implementation of the three designs below.

The evidence (RX 9070 XT, RADV, Linux, 4K, v2.1.0+)

From hookshot_log.txt (the log holds no game data):

vk: mirror syncs (600 submits): 782 dirty from captures, 173553 from earlier ones', ...
vk: write-back diff (600 submits): 3879 runs, 3344.4 MB compared; 2578 shadow-fill pages; 400 MB shadows; ...
vk: imports: 40 live (1424 MB), ...
vk: readbacks of GPU results: 199 of 3000 waited for their write-back, 16.78 ms each
  • 35.6 fps at 4K; 60 fps at the same scene at 1080p. The GPU draws a fraction of its rated wattage (starved): the frame is CPU-bound. This is the memory-sync share of #995.
  • Mirror-sync churn: ~290 dirty pages are copied per submit, and 99.6% of them (173553 of 174335) come from earlier submissions' captures — capture-lag churn, not the current frame's CPU writes. Counters: gpu/vk/vk_memory.cpp:3420-3424.
  • Write-back compare: the diff pass compares 5.6 MB per submit (3879 runs, 3344.4 MB per window) into 400 MB of shadows, first reading 2578 pages per window in to fill them (vk_memory.cpp:3427-3430; the pass gpu/vk/shaders/wb_diff.comp).
  • Imports: 40 live userptr imports, 1424 MB, walked and checked for every submission (vk_memory.cpp:2824).
  • Readback stalls: every readback that waits costs the game thread 16.78 ms on average (199 of 3000 invalidate calls) — more than half a frame at 35.6 fps. invalidate waits for the whole submission's write-back value (gpu/vk/vk_replay.cpp:2264-2272), not for the small result the CPU is about to read; the average is logged at vk_replay.cpp:2285-2289.

Every leg scales with the bytes the GPU writes back, which is why resolution is the knob that moves them.

Proven already (completed local analysis, reviewed — not yet upstream)

Dirty-origin split (Phase 1). A parallel GPU-write generation stamp alongside the CPU's page generations, its own counter bumped per submission and its own per-pool sync baseline. A pure classifier, dirty_origin_of, keys each 4 KB page clean / CPU-dirty / GPU-dirty / both, and the three consumers re-key on it: mirror_sync skips GPU-dirty-only pages (the mirror holds the truth; #1629's write-back brings it to guest memory), the capture scans prune ranges with no CPU-dirty page on even submissions (odd ones scan everything, so no range starves), and the write-back end narrows #1629's large both-dirty exceptions to the pages the pass must compare (pending pages never culled). Kill switch RX_VK_DIRTY_ORIGIN=0 restores every pre-split path. Branch wb-tiles: stamps and classifier gpu/vk/vk_memory.cpp:631-689, the mirror-sync skip :2422-2463, the capture prune :1578-1614, the write-back cull :3704-3719 and :3740-3760, the gate narrowing :2896-2915, counters :3623-3639. Two independent reviewers found it sound. It has not been built or run; that is the first thing upstream would do.

The prefilter proof (Phase 2 attempts, both negative — and that is the useful result). Two independent Phase-2 implementations (branches phase2-glm, phase2-mimo) tried to cull write-back compare bytes from CPU-observable signals and both hit the same wall: a tile mask marked at bind time covers every queued run by construction — the same binds that queue a write-back mark the tiles — so intersecting it culled nothing, while its overflow arm dropped unmarked tiles (data loss). Both attempts deleted their prefilters honestly and left the proof at the cull site: phase2-glm gpu/vk/vk_memory.cpp:3729-3735, phase2-mimo gpu/vk/vk_memory.cpp:4024-4031. Both reached the same conclusion: the only exact subset signal is the word-diff pass's own differ output — the GPU's observation of what it actually changed — and using it needs new GPU-side machinery, not a better CPU-side prefilter. The three proposals below are what survives that proof.

Proposed work

1. GPU-observed dirty tiles: feed wb_diff's per-tile differ output back as the next submission's cull input

Today the pass computes differs per vector and folds the shadow (shaders/wb_diff.comp:29-44) but records nothing per tile: after it runs, the only trace of what the GPU changed is its own writes. The proposal:

  • A device-resident, persistent per-tile mask, one any-differ bit per 64 KB tile (wbd_split's chunk size). The pass itself (or a tiny fold dispatch after it) sets a tile's bit with an atomics when any of its words differed — the branch phase2-glm note at gpu/vk/vk_memory.cpp:3733-3735 specifies this shape: "a GPU-resident mask with an indirect-dispatch count, not a CPU intersect".
  • Once per submission the host reads the mask back and folds it into wbd_keep_page's GPU-dirty leg for the next submission's chunk-table build. At the log's 3.3 GB compare window that is ~53k tiles ≈ 6.6 KB of bits — a small, latency-tolerant readback, unlike the frame-sized ones proposal 2 fixes.
  • Exact by construction: the mask may only remove tiles the pass itself observed identical after every write in flight at that comparison had executed; everything not covered by an observation — the current submission's fresh bind stamps, CPU-dirty pages, pages unknown to the shadow, pages with a write-back still in flight — stays kept, so no tile whose content could differ is ever skipped. The proof above says the bind-stamp leg alone over-approximates to "keep everything"; the GPU's own differ observation is the one signal that can shrink it without losing a write.
  • Kill switch: the Phase-1 RX_VK_DIRTY_ORIGIN=0 opt-out covers it (or its own RX_VK_WB_TILES=0), with the fallback being the Phase-1 page-level cull unchanged.

2. Urgent/bulk write-back split: game-thread readbacks wait on a small-result signal, not whole-frame retire

The bookkeeping design exists (branch phase2-glm); the signal machinery is what is missing. The design:

  • At the end's emission, classify each run: URGENT = pages memory_rb_refresh refreshes (readback results the CPU is about to read), runs of 16 KB or less, and odd (non-word-aligned) leftovers the diff pass cannot take; BULK = the rest (framebuffer tiles). Pure classifiers: gpu/vk/vk_memory.cpp:1430-1449.
  • Urgent runs are recorded whole, first, right after the end's first transfer barrier, and carry their own timeline value (urgent table per page plus a small cache, :1451-1474; the copies at :3919-3943). invalidate waits on that value only, falling back to the full value once for a region never read back before (gpu/vk/vk_internal.h:228-233, gpu/vk/vk_replay.cpp:2277-2301).
  • What is missing upstream: the split signal. Today one submit signals one timeline value, so mem_urgent_value == mem_rec_value and the urgent wait is today's wait, only narrower — phase2-glm gpu/vk/vk_memory.cpp:3921-3923 says exactly this: "a split submit signalling urgent right after these copies is staged, not this change". The work is recording a second timeline value (or semaphore) signalled by the submission's urgent-only tail, the catch-up path's handling of it, and the ordering argument (urgent copies after the first barrier, both tiers before the next submission's work, :1425-1429).
  • This is what turns a 16.78 ms stall into a wait for a few KB: the exposure-meter readback stops waiting for the framebuffer's bulk compare.
  • Complementary to #1150, which binds small readback ranges in guest memory so no write-back wait is needed at all: #1150 removes the wait for ranges ≤ 64 KB after their first invalidate; this shrinks it for everything else. They can land independently.

3. Measured demotion for CPU/GPU-colliding pools

The churn in the log (289 dirty pages a submit, 99.6% from earlier captures) concentrates in pools where the CPU and the GPU write the same pages: the sync copies the CPU's version over the GPU's, the GPU writes it again, the next capture lags, repeat. Blind policies (idle timeouts, budget refusals) move whole pools on guesses. A related approach exists outside upstream — the Triforce fork's measured mirror demotion: per-pool guest-memory trials with GPU-time A/B and a persistent keep list. A pool suspected of colliding goes to guest memory for a trial window; if GPU time improved, the demotion is kept in a persistent list; if not, the mirror comes back. Upstream equivalent:

  • per-pool trials keyed off counters the renderer already keeps (st_sync_* per window, direct_by_pool, live_by_pool — vk_memory.cpp:3420-3467);
  • a persistent keep list surviving pool retire/merge, with the trial's A/B decision logged;
  • an RX_ opt-out like every switch here, off-or-neutral unless set;
  • #1377 is the complementary direction (heat decides which pools get mirrors); this decides which pools should not have one. The two together would replace the idle-timeout heuristic (kIdleEpochs, vk_memory.cpp:2757) with measured placement.
  • #995 — tracking: low fps with low GPU usage (CPU-bound). This issue is the memory-sync share of it.
  • #1150 — small readback ranges bound in guest memory (the stall leg's other half).
  • #1377 — mirror the pools the GPU reads most (the placement direction proposal 3 completes).
  • #1629 — landed: RX_VK_WB_MAX_KB, the blind size skip (gpu/vk/vk_memory.cpp:2733-2745, readme.md:681-685). Phase 1 narrows its large-both-dirty exception; proposal 1 shrinks what remains compared.

Validation

  • The 600-submit counters are the A/B ledger. Compared MB and runs (st_wbd_*, vk_memory.cpp:3427-3430), live pages read by pool (:3434-3452), the mirror-sync split (from-captures vs from-earlier, :3420-3424), imports (:2824), and Phase 1's additions — GPU-dirty-only pages skipped, capture scan ranges pruned, #1629's gate kept/skipped (wb-tiles :3623-3639). Targets: compared MB down with no flash, the from-earlier share down sharply, readback-wait averages down from ~17 ms to the urgent tail's length.
  • Readback waits: RX_VK_LOG_READBACK_WAITS=1 for per-wait lines and the 3000-invalidate averages (vk_replay.cpp:2277-2289).
  • Kill-switch A/B pattern: every piece ships behind an RX_ switch that is off or neutral unless asked for (CONTRIBUTING.md, Code and data conventions), documented beside the surrounding switches. One run on, one run off, same spot, same settings, per card — the RX_VK_DIRTY_ORIGIN=0 pattern the Phase-1 branch already documents in its readme section.
  • Regression coverage: the flash detector (RX_VK_FLASH=1, docs/development.md:114) — a culled tile that actually differed shows up as a flash, so a clean detector run is the correctness gate for proposals 1 and 3; RX_VK_LOG_LIVE=1 (vk_memory.cpp:2339-2340) for the live-page decisions and RX_VK_LOG_READBACK (vk_replay.cpp:2226-2233) for wait values.
  • Game-free tests follow the house pattern — pure classifiers and small fixtures compiled from source, picked up by tests/run_all.py on its own. The analysis branches already carry the shape: tests/test_wb_diff_split.py, tests/test_wb_origin_split.py, tests/test_wb_diff_cpu_writes.py (Phase 1), tests/test_wb_tier_split.py, tests/test_wb_bulk_render.py, tests/test_wb_invalidate_tier.py (phase2-glm). Each proposal's pure parts (the tile-index and keep predicates, the tier classifiers, the trial decision) get the same treatment; nothing needs a GPU or a dump in CI.

None of the above changes what the replay reads from guest memory, the per-queue wait order, or submission-order registration — the design notes' section 4 rules hold by construction in every piece.

## Why At 4K the game runs at 35.6 fps where the same scene holds 60 at 1080p, and the GPU sits far under its power limit: the frame is bounded by CPU-side memory-sync work whose size follows the framebuffer's bytes, not the CPU's own traffic. #1629 removed the largest leg (the blind size skip); the legs that are left — the mirror-sync page churn, the capture scans, the write-back compare and the game-thread readback waits — are what this issue proposes to finish. The groundwork is done and reviewed; what is missing is upstream implementation of the three designs below. ## The evidence (RX 9070 XT, RADV, Linux, 4K, v2.1.0+) From `hookshot_log.txt` (the log holds no game data): ``` vk: mirror syncs (600 submits): 782 dirty from captures, 173553 from earlier ones', ... vk: write-back diff (600 submits): 3879 runs, 3344.4 MB compared; 2578 shadow-fill pages; 400 MB shadows; ... vk: imports: 40 live (1424 MB), ... vk: readbacks of GPU results: 199 of 3000 waited for their write-back, 16.78 ms each ``` - 35.6 fps at 4K; 60 fps at the same scene at 1080p. The GPU draws a fraction of its rated wattage (starved): the frame is CPU-bound. This is the memory-sync share of #995. - **Mirror-sync churn:** ~290 dirty pages are copied per submit, and 99.6% of them (173553 of 174335) come from *earlier* submissions' captures — capture-lag churn, not the current frame's CPU writes. Counters: `gpu/vk/vk_memory.cpp:3420-3424`. - **Write-back compare:** the diff pass compares 5.6 MB per submit (3879 runs, 3344.4 MB per window) into 400 MB of shadows, first reading 2578 pages per window in to fill them (`vk_memory.cpp:3427-3430`; the pass `gpu/vk/shaders/wb_diff.comp`). - **Imports:** 40 live userptr imports, 1424 MB, walked and checked for every submission (`vk_memory.cpp:2824`). - **Readback stalls:** every readback that waits costs the game thread 16.78 ms on average (199 of 3000 invalidate calls) — more than half a frame at 35.6 fps. `invalidate` waits for the *whole submission's* write-back value (`gpu/vk/vk_replay.cpp:2264-2272`), not for the small result the CPU is about to read; the average is logged at `vk_replay.cpp:2285-2289`. Every leg scales with the bytes the GPU writes back, which is why resolution is the knob that moves them. ## Proven already (completed local analysis, reviewed — not yet upstream) **Dirty-origin split (Phase 1).** A parallel GPU-write generation stamp alongside the CPU's page generations, its own counter bumped per submission and its own per-pool sync baseline. A pure classifier, `dirty_origin_of`, keys each 4 KB page clean / CPU-dirty / GPU-dirty / both, and the three consumers re-key on it: `mirror_sync` skips GPU-dirty-only pages (the mirror holds the truth; #1629's write-back brings it to guest memory), the capture scans prune ranges with no CPU-dirty page on even submissions (odd ones scan everything, so no range starves), and the write-back end narrows #1629's large both-dirty exceptions to the pages the pass must compare (pending pages never culled). Kill switch `RX_VK_DIRTY_ORIGIN=0` restores every pre-split path. Branch `wb-tiles`: stamps and classifier `gpu/vk/vk_memory.cpp:631-689`, the mirror-sync skip `:2422-2463`, the capture prune `:1578-1614`, the write-back cull `:3704-3719` and `:3740-3760`, the gate narrowing `:2896-2915`, counters `:3623-3639`. Two independent reviewers found it sound. It has not been built or run; that is the first thing upstream would do. **The prefilter proof (Phase 2 attempts, both negative — and that is the useful result).** Two independent Phase-2 implementations (branches `phase2-glm`, `phase2-mimo`) tried to cull write-back compare bytes from CPU-observable signals and both hit the same wall: a tile mask marked at bind time covers every queued run *by construction* — the same binds that queue a write-back mark the tiles — so intersecting it culled nothing, while its overflow arm dropped unmarked tiles (data loss). Both attempts deleted their prefilters honestly and left the proof at the cull site: `phase2-glm` `gpu/vk/vk_memory.cpp:3729-3735`, `phase2-mimo` `gpu/vk/vk_memory.cpp:4024-4031`. Both reached the same conclusion: **the only exact subset signal is the word-diff pass's own differ output** — the GPU's observation of what it actually changed — and using it needs new GPU-side machinery, not a better CPU-side prefilter. The three proposals below are what survives that proof. ## Proposed work ### 1. GPU-observed dirty tiles: feed wb_diff's per-tile differ output back as the next submission's cull input Today the pass computes `differs` per vector and folds the shadow (`shaders/wb_diff.comp:29-44`) but records nothing per tile: after it runs, the only trace of what the GPU changed is its own writes. The proposal: - A **device-resident, persistent per-tile mask**, one any-differ bit per 64 KB tile (`wbd_split`'s chunk size). The pass itself (or a tiny fold dispatch after it) sets a tile's bit with an atomics when any of its words differed — the branch `phase2-glm` note at `gpu/vk/vk_memory.cpp:3733-3735` specifies this shape: "a GPU-resident mask with an indirect-dispatch count, not a CPU intersect". - Once per submission the host **reads the mask back** and folds it into `wbd_keep_page`'s GPU-dirty leg for the next submission's chunk-table build. At the log's 3.3 GB compare window that is ~53k tiles ≈ 6.6 KB of bits — a small, latency-tolerant readback, unlike the frame-sized ones proposal 2 fixes. - **Exact by construction:** the mask may only remove tiles the pass itself observed identical after every write in flight at that comparison had executed; everything not covered by an observation — the current submission's fresh bind stamps, CPU-dirty pages, pages unknown to the shadow, pages with a write-back still in flight — stays kept, so no tile whose content could differ is ever skipped. The proof above says the bind-stamp leg alone over-approximates to "keep everything"; the GPU's own differ observation is the one signal that can shrink it without losing a write. - Kill switch: the Phase-1 `RX_VK_DIRTY_ORIGIN=0` opt-out covers it (or its own `RX_VK_WB_TILES=0`), with the fallback being the Phase-1 page-level cull unchanged. ### 2. Urgent/bulk write-back split: game-thread readbacks wait on a small-result signal, not whole-frame retire The bookkeeping design exists (branch `phase2-glm`); the signal machinery is what is missing. The design: - At the end's emission, classify each run: **URGENT** = pages `memory_rb_refresh` refreshes (readback results the CPU is about to read), runs of 16 KB or less, and odd (non-word-aligned) leftovers the diff pass cannot take; **BULK** = the rest (framebuffer tiles). Pure classifiers: `gpu/vk/vk_memory.cpp:1430-1449`. - Urgent runs are recorded whole, first, right after the end's first transfer barrier, and carry their **own timeline value** (urgent table per page plus a small cache, `:1451-1474`; the copies at `:3919-3943`). `invalidate` waits on that value only, falling back to the full value once for a region never read back before (`gpu/vk/vk_internal.h:228-233`, `gpu/vk/vk_replay.cpp:2277-2301`). - What is missing upstream: the **split signal**. Today one submit signals one timeline value, so `mem_urgent_value == mem_rec_value` and the urgent wait is today's wait, only narrower — `phase2-glm` `gpu/vk/vk_memory.cpp:3921-3923` says exactly this: "a split submit signalling urgent right after these copies is staged, not this change". The work is recording a second timeline value (or semaphore) signalled by the submission's urgent-only tail, the catch-up path's handling of it, and the ordering argument (urgent copies after the first barrier, both tiers before the next submission's work, `:1425-1429`). - This is what turns a 16.78 ms stall into a wait for a few KB: the exposure-meter readback stops waiting for the framebuffer's bulk compare. - Complementary to #1150, which binds small readback ranges in guest memory so no write-back wait is needed at all: #1150 removes the wait for ranges ≤ 64 KB after their first invalidate; this shrinks it for everything else. They can land independently. ### 3. Measured demotion for CPU/GPU-colliding pools The churn in the log (289 dirty pages a submit, 99.6% from earlier captures) concentrates in pools where the CPU and the GPU write the same pages: the sync copies the CPU's version over the GPU's, the GPU writes it again, the next capture lags, repeat. Blind policies (idle timeouts, budget refusals) move whole pools on guesses. A related approach exists outside upstream — the **Triforce fork's measured mirror demotion**: per-pool guest-memory trials with GPU-time A/B and a persistent keep list. A pool suspected of colliding goes to guest memory for a trial window; if GPU time improved, the demotion is kept in a persistent list; if not, the mirror comes back. Upstream equivalent: - per-pool trials keyed off counters the renderer already keeps (`st_sync_*` per window, `direct_by_pool`, `live_by_pool` — `vk_memory.cpp:3420-3467`); - a persistent keep list surviving pool retire/merge, with the trial's A/B decision logged; - an `RX_` opt-out like every switch here, off-or-neutral unless set; - #1377 is the complementary direction (heat decides which pools *get* mirrors); this decides which pools should *not* have one. The two together would replace the idle-timeout heuristic (`kIdleEpochs`, `vk_memory.cpp:2757`) with measured placement. ## Related issues - #995 — tracking: low fps with low GPU usage (CPU-bound). This issue is the memory-sync share of it. - #1150 — small readback ranges bound in guest memory (the stall leg's other half). - #1377 — mirror the pools the GPU reads most (the placement direction proposal 3 completes). - #1629 — landed: `RX_VK_WB_MAX_KB`, the blind size skip (`gpu/vk/vk_memory.cpp:2733-2745`, `readme.md:681-685`). Phase 1 narrows its large-both-dirty exception; proposal 1 shrinks what remains compared. ## Validation - **The 600-submit counters are the A/B ledger.** Compared MB and runs (`st_wbd_*`, `vk_memory.cpp:3427-3430`), live pages read by pool (`:3434-3452`), the mirror-sync split (from-captures vs from-earlier, `:3420-3424`), imports (`:2824`), and Phase 1's additions — GPU-dirty-only pages skipped, capture scan ranges pruned, #1629's gate kept/skipped (`wb-tiles` `:3623-3639`). Targets: compared MB down with no flash, the from-earlier share down sharply, readback-wait averages down from ~17 ms to the urgent tail's length. - **Readback waits:** `RX_VK_LOG_READBACK_WAITS=1` for per-wait lines and the 3000-invalidate averages (`vk_replay.cpp:2277-2289`). - **Kill-switch A/B pattern:** every piece ships behind an `RX_` switch that is off or neutral unless asked for (CONTRIBUTING.md, *Code and data conventions*), documented beside the surrounding switches. One run on, one run off, same spot, same settings, per card — the `RX_VK_DIRTY_ORIGIN=0` pattern the Phase-1 branch already documents in its readme section. - **Regression coverage:** the flash detector (`RX_VK_FLASH=1`, `docs/development.md:114`) — a culled tile that actually differed shows up as a flash, so a clean detector run is the correctness gate for proposals 1 and 3; `RX_VK_LOG_LIVE=1` (`vk_memory.cpp:2339-2340`) for the live-page decisions and `RX_VK_LOG_READBACK` (`vk_replay.cpp:2226-2233`) for wait values. - **Game-free tests** follow the house pattern — pure classifiers and small fixtures compiled from source, picked up by `tests/run_all.py` on its own. The analysis branches already carry the shape: `tests/test_wb_diff_split.py`, `tests/test_wb_origin_split.py`, `tests/test_wb_diff_cpu_writes.py` (Phase 1), `tests/test_wb_tier_split.py`, `tests/test_wb_bulk_render.py`, `tests/test_wb_invalidate_tier.py` (phase2-glm). Each proposal's pure parts (the tile-index and keep predicates, the tier classifiers, the trial decision) get the same treatment; nothing needs a GPU or a dump in CI. None of the above changes what the replay reads from guest memory, the per-queue wait order, or submission-order registration — the design notes' section 4 rules hold by construction in every piece.
Arkex-ai commented 2026-11-11 02:52:51 +00:00 (Migrated from github.com)

I checked the public Hookshot-optimi fork at 15ec28db; its published branches are main and bake-offline, and I couldn't find the dirty-origin or tier-split code described here. Could you publish links to wb-tiles, phase2-glm and phase2-mimo, and say which parts you're still working on? I'd like to review the source and coordinate a non-overlapping piece before implementing anything. For the tile feedback, the main point to check is how an observation from submission N is invalidated by fresh GPU writes in N+1.

I checked the public Hookshot-optimi fork at `15ec28db`; its published branches are `main` and `bake-offline`, and I couldn't find the dirty-origin or tier-split code described here. Could you publish links to `wb-tiles`, `phase2-glm` and `phase2-mimo`, and say which parts you're still working on? I'd like to review the source and coordinate a non-overlapping piece before implementing anything. For the tile feedback, the main point to check is how an observation from submission N is invalidated by fresh GPU writes in N+1.
blessedplummer commented 2026-11-11 02:57:03 +00:00 (Migrated from github.com)

wb-tiles follow-up: branches, status, and your GPU-write invalidation question

Branches (just published to blessedplummer/Hookshot-optimi; nothing merged anywhere):

  • wb-tiles @ 13b601a — Phase 1 dirty-origin split + review fixes + build fix. Unit suite green; full run shows only pre-existing env failures. Remaining: game smoke test (not yet run).
  • phase2-glm @ 61fcb3c — urgent/bulk write-back bookkeeping, bulk-render capture exclusion, invalidate fallback; the bind tile prefilter is deleted as a proven no-op. Remaining: game measurement.
  • phase2-mimo @ 22f5d61 — sparse mirrors with uninitialized-fill fallback, GPU-aware shadow inherit, eviction bias; the bind-mark tile cull is deleted as a proven no-op. Remaining: game measurement.

The builds + 4K A/B smokes assessment was interrupted and resumes later. Coordination: the one piece nobody has built is the GPU-fed tile observation kernel — consuming the wb_diff differ output (which chunks actually differed) and folding it into per-tile observations. All three branches deliberately stopped at the CPU side, so if you want to take that kernel (it needs the wb_diff feedback path), it's yours; we'll stay off it and off the branches' own remaining work.

Your invalidation question — how an observation from submission N isn't invalidated by fresh GPU writes in N+1. Short answer: it never has to survive as a live fact. N's observation is consumed within N's own submission (the write-back), and N+1 starts from re-stamped state. The mechanism, sites verified at gpu/vk/vk_memory.cpp @ 13b601a unless noted:

  1. Per-submission pending set, never culled. memory_end_submission builds wb_pending_pages from this submission's write-back ranges where wb_pages.get(pg) > mem_completed (lines 3707–3718; pending predicate wb_pages.pending, lines 1317–1321). The cull keeps them: wbd_keep_page(known, gpu_dirty, pending) = pending || !known || gpu_dirty (line 3562), applied with std::binary_search(wb_pending_pages...) at lines 3750–3752.
  2. N's observation is consumed at N's end. The write-back copies run in memory_end_submission (lines 3804/3840) and stamp each page's timeline value (wb_pages_note, line 3808). The CPU's readback (invalidate, gpu/vk/vk_replay.cpp lines 2270–2295) re-reads that value and waits when v > completed() (wait_value(v)), so a CPU observation of the page happens only after N's write-back completed — no cross-submission stale-read window.
  3. N+1 starts from re-stamped state. Baselines advance at submission close: for (Pool* p : mirror_synced) p->synced_gen = gen_now, p->gpu_synced = gpu_now; (line 3864), with gpu_now++ at each submission's begin (line 3130). CapEntry GPU-origin marks are set during N's mirror sync (cap_entry_set(page, {gpu_page_gen(page), 0, true}), line 2460) and cleared by any later CPU copy (line 1831); capture-side readers skip e.gpu entries (lines 1198, 2471, 3274), so a mark never feeds a copy that could resurrect stale bytes.
  4. Any fresh N+1 GPU write re-stamps before the next cull reads it. Every genuine GPU write goes through memory_stamp_gpu → stamp_gpu_range (lines 767–775; callers: vk_draw's storage buffers, vk_replay's copies/clears, vk_xfer's texture-to-buffer copies). The next cull reads gpu_page_gen(gpg) > p->gpu_synced (line 3751); gpu_synced was frozen at N's gpu_now, so an N+1 stamp is strictly newer → gpu_dirty → kept.

The rule, enforced at the two cull sites above: no page with a pending write-back value or a post-baseline stamp is ever culled. The remaining gap is exactly the kernel: turning the differ output into the tile observations N+1 reads — hence the offer.

**wb-tiles follow-up: branches, status, and your GPU-write invalidation question** Branches (just published to blessedplummer/Hookshot-optimi; nothing merged anywhere): - [`wb-tiles`](https://github.com/blessedplummer/Hookshot-optimi/tree/wb-tiles) @ 13b601a — Phase 1 dirty-origin split + review fixes + build fix. Unit suite green; full run shows only pre-existing env failures. Remaining: game smoke test (not yet run). - [`phase2-glm`](https://github.com/blessedplummer/Hookshot-optimi/tree/phase2-glm) @ 61fcb3c — urgent/bulk write-back bookkeeping, bulk-render capture exclusion, invalidate fallback; the bind tile prefilter is deleted as a proven no-op. Remaining: game measurement. - [`phase2-mimo`](https://github.com/blessedplummer/Hookshot-optimi/tree/phase2-mimo) @ 22f5d61 — sparse mirrors with uninitialized-fill fallback, GPU-aware shadow inherit, eviction bias; the bind-mark tile cull is deleted as a proven no-op. Remaining: game measurement. The builds + 4K A/B smokes assessment was interrupted and resumes later. **Coordination:** the one piece nobody has built is the GPU-fed tile observation kernel — consuming the wb_diff differ output (which chunks actually differed) and folding it into per-tile observations. All three branches deliberately stopped at the CPU side, so if you want to take that kernel (it needs the wb_diff feedback path), it's yours; we'll stay off it and off the branches' own remaining work. **Your invalidation question — how an observation from submission N isn't invalidated by fresh GPU writes in N+1.** Short answer: it never has to survive as a live fact. N's observation is consumed within N's own submission (the write-back), and N+1 starts from re-stamped state. The mechanism, sites verified at gpu/vk/vk_memory.cpp @ 13b601a unless noted: 1. **Per-submission pending set, never culled.** `memory_end_submission` builds `wb_pending_pages` from this submission's write-back ranges where `wb_pages.get(pg) > mem_completed` (lines 3707–3718; pending predicate `wb_pages.pending`, lines 1317–1321). The cull keeps them: `wbd_keep_page(known, gpu_dirty, pending) = pending || !known || gpu_dirty` (line 3562), applied with `std::binary_search(wb_pending_pages...)` at lines 3750–3752. 2. **N's observation is consumed at N's end.** The write-back copies run in `memory_end_submission` (lines 3804/3840) and stamp each page's timeline value (`wb_pages_note`, line 3808). The CPU's readback (`invalidate`, gpu/vk/vk_replay.cpp lines 2270–2295) re-reads that value and waits when `v > completed()` (`wait_value(v)`), so a CPU observation of the page happens only after N's write-back completed — no cross-submission stale-read window. 3. **N+1 starts from re-stamped state.** Baselines advance at submission close: `for (Pool* p : mirror_synced) p->synced_gen = gen_now, p->gpu_synced = gpu_now;` (line 3864), with `gpu_now++` at each submission's begin (line 3130). CapEntry GPU-origin marks are set during N's mirror sync (`cap_entry_set(page, {gpu_page_gen(page), 0, true})`, line 2460) and cleared by any later CPU copy (line 1831); capture-side readers skip `e.gpu` entries (lines 1198, 2471, 3274), so a mark never feeds a copy that could resurrect stale bytes. 4. **Any fresh N+1 GPU write re-stamps before the next cull reads it.** Every genuine GPU write goes through `memory_stamp_gpu` → `stamp_gpu_range` (lines 767–775; callers: vk_draw's storage buffers, vk_replay's copies/clears, vk_xfer's texture-to-buffer copies). The next cull reads `gpu_page_gen(gpg) > p->gpu_synced` (line 3751); `gpu_synced` was frozen at N's `gpu_now`, so an N+1 stamp is strictly newer → gpu_dirty → kept. The rule, enforced at the two cull sites above: **no page with a pending write-back value or a post-baseline stamp is ever culled**. The remaining gap is exactly the kernel: turning the differ output into the tile observations N+1 reads — hence the offer.
Arkex-ai commented 2026-11-11 03:20:46 +00:00 (Migrated from github.com)

Thanks, I'll take the GPU-fed observation path. Claimed in dev-work. I'll start with opt-in per-chunk actual-difference feedback tied to completed submissions, keeping the current copy decisions. That gives us a measured basis for a later cull and its freshness proof. Your three branches remain yours. I now have a repeated Windows/NVIDIA 1080p/4K baseline; readback waits stay around 0.01 ms here, so I won't treat the urgent-signal proposal as a demonstrated local win.

Thanks, I'll take the GPU-fed observation path. [Claimed in dev-work](https://discord.com/channels/1568810484818378804/1569019786766188665/1569914220039114883). I'll start with opt-in per-chunk actual-difference feedback tied to completed submissions, keeping the current copy decisions. That gives us a measured basis for a later cull and its freshness proof. Your three branches remain yours. I now have a repeated Windows/NVIDIA 1080p/4K baseline; readback waits stay around 0.01 ms here, so I won't treat the urgent-signal proposal as a demonstrated local win.
builuanhoang-bot commented 2026-11-11 04:05:36 +00:00 (Migrated from github.com)

Another Windows/NVIDIA data point: v2.1.0, RTX 3090, 4K output, DLSS Quality (K), memory sync 13-15 ms, RX_VK_WB_MAX_KB=1024 changes nothing

Same symptom as above (CPU-bound by memory sync, GPU not the limit), and this log shows that skipping large write-backs was not the dominant cost here.

Setup: Windows 11, RTX 3090 (24 GB), Windows driver version 32.0.16.2101, output 3840x2160, DLSS Quality (render 2560x1440), DLSS model K, frame cap 60, frame generation off, HDR off, Reflex on.

Scene: a busy area with many actors (about 850 draws, 140 dispatches, 140 passes a frame).

Results (same area each time, 600-frame windows; not a scripted run, so treat small differences as noise):

run fps replay memory sync written back
v2.0.14 41-43 13.1-13.8 ms ~62 MB per frame
v2.1.0, no env 36-40 14-15.5 ms ~62 MB per frame
v2.1.0, RX_VK_WB_MAX_KB=1024 (log confirms: "write-backs only of written bindings up to 1024 KB") 41-43 13.1-14.1 ms 7.75 MB per frame

So the written-back volume dropped about 8x, but memory sync stayed at ~13-14 ms and fps did not move.

What the log shows for a frame in that scene:

a frame's replay time (ms): submit 19.75, draw 3.40, descriptors 1.02, ... dispatch 0.94, memory sync 14.12, queue submit 0.32
mirrors 59 (1408 MB): 13.24 MB synced in 1789.1 copies, 7.75 MB written back
GPU time 19.8-20.3 ms a frame; GPU-bound wait 0.0 ms

The cost looks tied to the number of copies (about 1,790 small copies a frame for 13 MB), not the bytes, which matches the "mirror-sync page churn" leg above. GPU-bound wait is 0, so lowering the DLSS render resolution is not expected to help in this scene.

If it is useful for the baseline, I can run the same scene with the dirty-origin or GPU-fed observation builds when they have a branch to test. Happy to provide a fuller log.

**Another Windows/NVIDIA data point: v2.1.0, RTX 3090, 4K output, DLSS Quality (K), memory sync 13-15 ms, `RX_VK_WB_MAX_KB=1024` changes nothing** Same symptom as above (CPU-bound by memory sync, GPU not the limit), and this log shows that skipping large write-backs was not the dominant cost here. **Setup:** Windows 11, RTX 3090 (24 GB), Windows driver version 32.0.16.2101, output 3840x2160, DLSS Quality (render 2560x1440), DLSS model K, frame cap 60, frame generation off, HDR off, Reflex on. **Scene:** a busy area with many actors (about 850 draws, 140 dispatches, 140 passes a frame). **Results (same area each time, 600-frame windows; not a scripted run, so treat small differences as noise):** | run | fps | replay `memory sync` | written back | |---|---|---|---| | v2.0.14 | 41-43 | 13.1-13.8 ms | ~62 MB per frame | | v2.1.0, no env | 36-40 | 14-15.5 ms | ~62 MB per frame | | v2.1.0, `RX_VK_WB_MAX_KB=1024` (log confirms: "write-backs only of written bindings up to 1024 KB") | 41-43 | 13.1-14.1 ms | 7.75 MB per frame | So the written-back volume dropped about 8x, but memory sync stayed at ~13-14 ms and fps did not move. What the log shows for a frame in that scene: ``` a frame's replay time (ms): submit 19.75, draw 3.40, descriptors 1.02, ... dispatch 0.94, memory sync 14.12, queue submit 0.32 mirrors 59 (1408 MB): 13.24 MB synced in 1789.1 copies, 7.75 MB written back GPU time 19.8-20.3 ms a frame; GPU-bound wait 0.0 ms ``` The cost looks tied to the **number of copies** (about 1,790 small copies a frame for 13 MB), not the bytes, which matches the "mirror-sync page churn" leg above. `GPU-bound wait` is 0, so lowering the DLSS render resolution is not expected to help in this scene. If it is useful for the baseline, I can run the same scene with the dirty-origin or GPU-fed observation builds when they have a branch to test. Happy to provide a fuller log.
Arkex-ai commented 2026-11-11 04:30:24 +00:00 (Migrated from github.com)

The GPU-fed observation path is now #2131, with CI running. It is opt-in (RX_VK_WB_OBSERVE=1) and reports actual changed chunks after their submission completes; no copy or culling decisions change. The existing analysis branches stay with blessedplummer.

The six local 4K controls found differences in about 15% of chunks, covering about 13% of compared volume, with no unavailable results. The enabled mean was 1.7% below disabled, so this is measurement infrastructure rather than a speed-up. Details and limits are in the PR.

@builuanhoang-bot your copy-count-heavy scene would be useful external coverage once the checks finish. If you try the branch, the observation lines alongside the existing sync/copy counters would help compare the two workloads; a clean observation still is not a proof that a future write can be skipped.

The GPU-fed observation path is now #2131, with CI running. It is opt-in (`RX_VK_WB_OBSERVE=1`) and reports actual changed chunks after their submission completes; no copy or culling decisions change. The existing analysis branches stay with blessedplummer. The six local 4K controls found differences in about 15% of chunks, covering about 13% of compared volume, with no unavailable results. The enabled mean was 1.7% below disabled, so this is measurement infrastructure rather than a speed-up. Details and limits are in the PR. @builuanhoang-bot your copy-count-heavy scene would be useful external coverage once the checks finish. If you try the branch, the observation lines alongside the existing sync/copy counters would help compare the two workloads; a clean observation still is not a proof that a future write can be skipped.
Arkex-ai commented 2026-11-11 12:46:39 +00:00 (Migrated from github.com)

@blessedplummer I'm investigating automatic compute storage write-footprints from IR, separately from #2131 observations and your dirty-origin/sparse-mirror branches. Scope posted in dev-work.

Authored IR-to-planner-to-GPU probes now cover dense ranges, strides, mixed plans and conservative fallbacks on Windows/NVIDIA. No renderer policy has changed, and there is no in-game speed-up claim. Experimental priority: S3; reach is not established; confidence 0.4 for an in-game performance opportunity.

Before integration, are store extents already part of any of your current branches? The proposed boundary keeps binding/hazard spans and your copy/culling policy unchanged; it supplies conservative per-dispatch write metadata with the existing fallback when a proof is unavailable. I'd like to agree on that boundary before touching overlapping renderer code.

@blessedplummer I'm investigating automatic compute storage write-footprints from IR, separately from #2131 observations and your dirty-origin/sparse-mirror branches. [Scope posted in dev-work](https://discord.com/channels/1568810484818378804/1569019786766188665/1570050396284788889). Authored IR-to-planner-to-GPU probes now cover dense ranges, strides, mixed plans and conservative fallbacks on Windows/NVIDIA. No renderer policy has changed, and there is no in-game speed-up claim. Experimental priority: S3; reach is not established; confidence 0.4 for an in-game performance opportunity. Before integration, are store extents already part of any of your current branches? The proposed boundary keeps binding/hazard spans and your copy/culling policy unchanged; it supplies conservative per-dispatch write metadata with the existing fallback when a proof is unavailable. I'd like to agree on that boundary before touching overlapping renderer code.
blessedplummer commented 2026-11-11 14:36:04 +00:00 (Migrated from github.com)

There is a PR in the works taking a different approach. So far it is looking promising, so once it looks good, that will be made a PR on upstream (here).

There is a PR in the works taking a different approach. So far it is looking promising, so once it looks good, that will be made a PR on upstream (here).
Arkex-ai commented 2026-11-11 14:47:24 +00:00 (Migrated from github.com)

@blessedplummer Thanks. I'll keep the write-footprint work isolated until we can check the overlap. Does the upcoming approach also derive per-dispatch store extents from shader IR, or does it work at the memory/copy-tracking layer? A branch or draft link when available would help me avoid duplicating it. No renderer or copy-policy changes have been made in this experiment, and there is no in-game speed-up claim.

@blessedplummer Thanks. I'll keep the write-footprint work isolated until we can check the overlap. Does the upcoming approach also derive per-dispatch store extents from shader IR, or does it work at the memory/copy-tracking layer? A branch or draft link when available would help me avoid duplicating it. No renderer or copy-policy changes have been made in this experiment, and there is no in-game speed-up claim.
blessedplummer commented 2026-11-11 15:23:47 +00:00 (Migrated from github.com)

Follow-up: this issue's remaining legs are implemented and measured in PR #2338 (RX_VK_DGPU=1, default off) — the "render targets device-local, sync only what the CPU reads, mirrors only where a measured trial proves them" policy for dGPUs.

Measured on RX 9070 XT / RADV, 4K, Uncapped cap, warp-normalized adjacent pairs (Hyrule Field, 3D gameplay, draw-count parity checked per pair):

  • fps: 70.2 -> 78.0, 71.0 -> 77.0, 59.6 -> 76.6 (+9.7% median, 3/3 pairs won), GPU time 9.2 ms of a ~14 ms frame — the win is CPU-side, as diagnosed here.
  • Write-back diff: 3,062-3,129 MB / 600 submits -> 330 MB or less; mirror-sync dirty pages 136,872 -> 0. That is legs (1) and (3) of this issue: the mirror population is gone by default (direct binds, coherent by construction), and mirrors exist only where a per-pool A/B trial (>10% AND >0.25 ms) proves them.

One safety finding from developing this, directly relevant to the small-readback-ranges direction (#1150): page-granular write-back culling is NOT safe. With write-backs fully skipped, the same scene issued 3941 draws vs 1364 — the game reads GPU results outside the readback API, so page-sparse copies starve it and the apparent fps wins were scene mismatches. Gating at POOL level (skip a pool's sync only where the CPU never read that pool) restores draw parity within ~2% (705 vs 709 draws/frame in the final logs). Anything building on #1150 should use the draw-count-parity OFF-vs-ON check as a regression gate.

What the PR deliberately does not touch: the remaining "memory sync" ms is the submit-time capture of CPU-written pages (texture recreation correctness, ~30 MB/submit) — policy-independent, and page-sparse tricks on it are exactly the failure mode above. The next leg there is conflict-driven per-pool demotion (the Triforce fork's measured-trial approach, referenced in the original issue text) so pools that collide CPU/GPU-side leave the mirrors; that is drafted as a companion issue pending signal on the PR.

Branch: blessedplummer/Hookshot-optimi@dgpu-pr (one commit off main @364d199; game-free checks green, suite 352/0 vs pristine 351/0; default-off verified — the added counters and logging are gated on the switch).

Follow-up: this issue's remaining legs are implemented and measured in PR #2338 (`RX_VK_DGPU=1`, default off) — the "render targets device-local, sync only what the CPU reads, mirrors only where a measured trial proves them" policy for dGPUs. Measured on RX 9070 XT / RADV, 4K, Uncapped cap, warp-normalized adjacent pairs (Hyrule Field, 3D gameplay, draw-count parity checked per pair): - fps: 70.2 -> 78.0, 71.0 -> 77.0, 59.6 -> 76.6 (+9.7% median, 3/3 pairs won), GPU time 9.2 ms of a ~14 ms frame — the win is CPU-side, as diagnosed here. - Write-back diff: 3,062-3,129 MB / 600 submits -> 330 MB or less; mirror-sync dirty pages 136,872 -> 0. That is legs (1) and (3) of this issue: the mirror population is gone by default (direct binds, coherent by construction), and mirrors exist only where a per-pool A/B trial (>10% AND >0.25 ms) proves them. One safety finding from developing this, directly relevant to the small-readback-ranges direction (#1150): page-granular write-back culling is NOT safe. With write-backs fully skipped, the same scene issued 3941 draws vs 1364 — the game reads GPU results outside the readback API, so page-sparse copies starve it and the apparent fps wins were scene mismatches. Gating at POOL level (skip a pool's sync only where the CPU never read that pool) restores draw parity within ~2% (705 vs 709 draws/frame in the final logs). Anything building on #1150 should use the draw-count-parity OFF-vs-ON check as a regression gate. What the PR deliberately does not touch: the remaining "memory sync" ms is the submit-time capture of CPU-written pages (texture recreation correctness, ~30 MB/submit) — policy-independent, and page-sparse tricks on it are exactly the failure mode above. The next leg there is conflict-driven per-pool demotion (the Triforce fork's measured-trial approach, referenced in the original issue text) so pools that collide CPU/GPU-side leave the mirrors; that is drafted as a companion issue pending signal on the PR. Branch: `blessedplummer/Hookshot-optimi@dgpu-pr` (one commit off main @364d199; game-free checks green, suite 352/0 vs pristine 351/0; default-off verified — the added counters and logging are gated on the switch).
Arkex-ai commented 2026-11-11 16:51:00 +00:00 (Migrated from github.com)

@blessedplummer Thanks, I read #2338. I'll keep this separate from its pool policy, readback gating and trials. The remaining scope would be optional per-dispatch compute write-extent metadata, with unknown image/global effects falling back and no change to pending-writer, alias or lifetime rules. Authored tests now exercise eight disjoint ranges through IR, cache, fresh resolution and GPU consumption, including causal checks; there is still no game-performance claim. Would that metadata-only scope be useful for residual mirrored pools after #2338, or would you prefer it deferred?

@blessedplummer Thanks, I read #2338. I'll keep this separate from its pool policy, readback gating and trials. The remaining scope would be optional per-dispatch compute write-extent metadata, with unknown image/global effects falling back and no change to pending-writer, alias or lifetime rules. Authored tests now exercise eight disjoint ranges through IR, cache, fresh resolution and GPU consumption, including causal checks; there is still no game-performance claim. Would that metadata-only scope be useful for residual mirrored pools after #2338, or would you prefer it deferred?
blessedplummer commented 2026-11-11 17:57:56 +00:00 (Migrated from github.com)

Thanks for reading it — useful now, not deferred, and the scope you've drawn is exactly the one that composes.

Under #2338's policy the residual syncs split three ways: pools bound direct (MIRROR_REFUSED) run no write-back at all — nothing for the metadata to feed there; pools that earn a mirror through the trial keep the stock full-pool machinery; and CPU-read pools keep stock write-backs by the pool-level gate (any readback page -> stock behavior for that pool). Those last two are precisely the "residual mirrored pools" your per-dispatch write-extents would sharpen — and they are the only place it can apply, so it composes instead of overlapping:

  • The pool gate decides whether a pool syncs; your dispatch extents decide what range of a syncing pool syncs. Orthogonal granularities, no shared state.
  • Your fallback rule (unknown image/global effects -> full sync) is the right default, and matches the failure mode documented during #2338's development: page-granular culling fed from CPU-side marks is unsafe — with it, the same scene issued 3941 draws vs 1364 (the game reads GPU results outside the readback API), and the apparent fps wins were scene mismatches. Draw-count parity OFF vs ON is the regression gate for anything in this area. GPU-observed per-dispatch extents avoid that class entirely because the signal comes from the dispatch itself — the same reason this issue's proposal (1) pointed at the differ's own output rather than CPU-visible marks.
  • Metadata-only, with no pending-writer/alias/lifetime changes, keeps it landable independently of #2338 — my preference would be a separate PR rather than stacked, so each can be reviewed and merged on its own; the pool gate in the dGPU policy is the natural consumer either way.

No rush on the performance claim — once it's testable I can measure it on the residual mirrored pools directly (RX 9070 XT / RADV, 4K, warp-normalized adjacent pairs, the 600-submit counters — the same harness #2338's numbers came from), so the game-performance side your tests deliberately don't claim can come from the same rig the pool policy was measured on.

Thanks for reading it — useful now, not deferred, and the scope you've drawn is exactly the one that composes. Under #2338's policy the residual syncs split three ways: pools bound direct (`MIRROR_REFUSED`) run no write-back at all — nothing for the metadata to feed there; pools that earn a mirror through the trial keep the stock full-pool machinery; and CPU-read pools keep stock write-backs by the pool-level gate (any readback page -> stock behavior for that pool). Those last two are precisely the "residual mirrored pools" your per-dispatch write-extents would sharpen — and they are the only place it can apply, so it composes instead of overlapping: - The pool gate decides **whether** a pool syncs; your dispatch extents decide **what range** of a syncing pool syncs. Orthogonal granularities, no shared state. - Your fallback rule (unknown image/global effects -> full sync) is the right default, and matches the failure mode documented during #2338's development: page-granular culling fed from CPU-side marks is unsafe — with it, the same scene issued 3941 draws vs 1364 (the game reads GPU results outside the readback API), and the apparent fps wins were scene mismatches. Draw-count parity OFF vs ON is the regression gate for anything in this area. GPU-observed per-dispatch extents avoid that class entirely because the signal comes from the dispatch itself — the same reason this issue's proposal (1) pointed at the differ's own output rather than CPU-visible marks. - Metadata-only, with no pending-writer/alias/lifetime changes, keeps it landable independently of #2338 — my preference would be a separate PR rather than stacked, so each can be reviewed and merged on its own; the pool gate in the dGPU policy is the natural consumer either way. No rush on the performance claim — once it's testable I can measure it on the residual mirrored pools directly (RX 9070 XT / RADV, 4K, warp-normalized adjacent pairs, the 600-submit counters — the same harness #2338's numbers came from), so the game-performance side your tests deliberately don't claim can come from the same rig the pool policy was measured on.
Arkex-ai commented 2026-11-11 18:56:05 +00:00 (Migrated from github.com)

@blessedplummer Thanks, that separate metadata-only scope works. One clarification: the planned producer derives conservative possible-write extents from shader IR, resolved against replay-ordered dispatch constants/bindings; it is not a GPU-written dirty-page signal. Unknown image/global effects, incomplete bounds or unresolved aliases must retain full sync, and every pending writer still contributes. I'll keep the pool policy and existing lifetime/alias machinery unchanged, with draw-count parity and matched-scene checks before any performance claim. Please flag if you meant GPU-written extents rather than IR-derived metadata.

@blessedplummer Thanks, that separate metadata-only scope works. One clarification: the planned producer derives conservative possible-write extents from shader IR, resolved against replay-ordered dispatch constants/bindings; it is not a GPU-written dirty-page signal. Unknown image/global effects, incomplete bounds or unresolved aliases must retain full sync, and every pending writer still contributes. I'll keep the pool policy and existing lifetime/alias machinery unchanged, with draw-count parity and matched-scene checks before any performance claim. Please flag if you meant GPU-written extents rather than IR-derived metadata.
NotRexed commented 2026-11-11 21:25:05 +00:00 (Migrated from github.com)

Thanks for the report. Reports of the same problem are now collected in #995, which lists this one with your details and links back here, so one fix closes them together and the open list stays readable. Follow #995 for the fix, and add new details there (or here). If this turns out to be a separate problem it will be reopened.

Thanks for the report. Reports of the same problem are now collected in #995, which lists this one with your details and links back here, so one fix closes them together and the open list stays readable. Follow #995 for the fix, and add new details there (or here). If this turns out to be a separate problem it will be reopened.
Commenting is not possible because the repository is archived.
No milestone
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Hookshot#2099
No description provided.