Skip to content

perf(vk): GCN (RX 400/500): f16 lowered where the GPU has none; no VRAM mirrors or batched submits by default (from hookshot-k10) - #2343

Merged
hckrman101 merged 3 commits into
mainfrom
port/k10-gcn-f16-mirrors
Nov 11, 2026
Merged

hckrman101 merged 3 commits into
mainfrom
port/k10-gcn-f16-mirrors

Conversation

@NotRexed

Copy link
Copy Markdown
Owner

Ported from deadlynch/hookshot-k10 commit e09059c1, credited as co-author. That fork carries the fixes that got an RX 580 under RADV to play.

  • f16 only where the GPU has f16 shaders. RADV gives Polaris (RX 400/500) shaderFloat16 = false, yet the translator always emitted f16, and Kokiri Forest drew as coloured noise. f16 is now lowered to f32 there, as Pascal (GTX 10xx, also without f16) needs too. The f16 networks fall back to the linear stand-in, and these programs are kept apart in the program cache (disk_stamp), so other GPUs keep theirs.
  • GCN: VRAM mirrors and batched submits off by default. That applies on a GPU that pins no stage to 32 lanes and runs wider. An RX 580 ran Kokiri Forest at 20 fps with either on and 24.5 with both off; an RX 590 played with them off in [Tracking] GPUs without mesh shaders (GTX 9xx/10xx, RX 400/500/5000, Iris Xe/UHD): v2.0.6 fallback reports #1001. RX_VK_MIRROR / RX_VK_BATCH still choose.

GPUs with f16 shaders and 32-lane stages (RTX, RDNA) take exactly the same path as before.

Checked

  • Applied cleanly onto main; the three files compile.
  • Tested on an RX 580 by the fork's author.
  • Not yet run here (RTX 4060 Ti, where it changes nothing): a run follows before merging.

🤖 Generated with Claude Code

…16 shaders; no VRAM mirrors or batched submits by default

Ported from deadlynch's hookshot-k10 (e09059c1), tested there on an RX 580 under RADV:
- RADV gives Polaris no f16 shaders (shaderFloat16 false), yet the translator always emitted f16, and Kokiri Forest drew as coloured noise. f16 is now lowered to f32 where the GPU lacks it (as Pascal needs too), the f16 networks fall back to the linear stand-in, and such programs are kept apart in the program cache.
- On a GPU that pins no stage to 32 lanes and runs wider (GCN), VRAM mirrors and batched submits are off by default: an RX 580 ran Kokiri Forest at 20 fps with either on and 24.5 with both off (an RX 590 played with them off, #1001). RX_VK_MIRROR and RX_VK_BATCH still choose.
GPUs with f16 shaders and 32-lane stages (RTX, RDNA) are unchanged.

Co-authored-by: deadlynch <20408322+deadlynch@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@hckrman101 hckrman101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for porting this. The gating looks right to me for the cards that matter most: on RTX and RDNA, sg32_stages is non-zero (minSubgroupSize <= 32 with subgroup size control), so gcn is false and mirrors and batching stay as before. Intel and NVIDIA report 32-lane subgroups, so they're excluded too. The f16 change only applies where shaderFloat16 is false, and the nf16 stamp is only added there, so caches on f16 cards don't change. Nothing has been posted on the thread yet, so these are first notes.

The one blocker is CI. tests/test_amd_subgroup_profiles.py fails on Linux and Windows because of this PR. It copies disk_stamp() from const uint32_t lanes[] to // (the records keep Shader::Info, and the new nf16 block falls inside that slice. The fixture's dev stub has no vk12 (no member named 'vk12'), and its add stub expects every call to be 5 words. The simplest fix is to move the block below the abi[] add, outside the slice. If you'd rather keep it where it is, give the stub a vk12 and let check_stamp accept the extra word. The other red jobs (Python checks part 1/2 on Ubuntu, SPIR-V fixtures, appimage) died in apt with Temporary failure resolving 'azure.archive.ubuntu.com', so a re-run should clear them.

Things to be aware of, but not blocking:

  • The f16 lowering also turns on by default on any other card without shaderFloat16, including GTX 10xx going by the description. The existing comment at host_info() says Eden's lowering on its own drew Kokiri as noise on an RTX card. Pascal now gets that lowering, the linear stand-in for the networks, and a fresh program cache, and it hasn't been tested yet. A run on a 10xx before merging would be worth it, or at least a line in the description saying Pascal changes too.
  • The gcn test also matches Vega (the comment says so) and Vega/GCN APUs on RADV, where batching now turns off. The numbers come from an RX 580 (and an RX 590 in #1001), so for Vega and the APUs this is an assumption. That's probably fine since RX_VK_MIRROR / RX_VK_BATCH still override.

Good to merge once the subgroup-profile test is fixed and CI is green, plus the RTX 4060 Ti run you mentioned.

Comment thread gpu/vk/vk_shader.cpp
getenv("RX_VK_WARP64") ? 1u : 0u};
add(lanes, sizeof lanes);
// (k10) f16 lowered to f32 on a GPU without f16 shaders (host_info): only then, so other GPUs keep their caches
if (!dev.vk12.shaderFloat16) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sits inside the slice tests/test_amd_subgroup_profiles.py takes from disk_stamp() (const uint32_t lanes[] up to // (the records keep Shader::Info). The fixture's dev has no vk12, so it fails to compile on both platforms. Even with one, its add stub asserts bytes == 5 * sizeof(uint32_t). Moving this block after the abi[] add avoids both problems.

Comment thread gpu/vk/vk_shader.cpp Outdated
h.support_float16 = !getenv("RX_VK_NO_F16");
// (k10) and only where the GPU runs f16 shaders: RADV gives Polaris (RX 400/500) none (shaderFloat16 false), and f16 SPIR-V
// there drew Kokiri Forest as coloured noise on an RX 580. Eden's lowering to f32 is what Pascal takes as well
h.support_float16 = !getenv("RX_VK_NO_F16") && dev.vk12.shaderFloat16;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also changes Pascal (and any other card without shaderFloat16) by default, not only Polaris. The comment just above says this lowering on its own gave noise on an RTX card. It would be good to have a GTX 10xx run, or at least to say in the description that Pascal changes too.

Comment thread gpu/vk/vk_replay.cpp
const bool v = e ? atoi(e) != 0 : radv;
logf("vk: batched submits %s (RX_VK_BATCH%s)", v ? "on" : "off", !e && radv ? ", on with RADV" : "");
// (k10) not on GCN (no stage pinned to 32 lanes, 64-lane waves): an RX 580 ran Kokiri Forest at 24 fps without and 20 with
const bool gcn = dev.sg32_stages == 0 && dev.subgroup_size > 32;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: sg32_stages is also 0 when RX_VK_NO_SUBGROUP_PIN is set, so on RDNA that debug switch now also turns batching (and VRAM mirrors) off, which could confuse an A/B test. It would also catch an RDNA driver without subgroup size control. Neither is a big deal. Maybe say it in the comment, or work out GCN from minSubgroupSize instead.

NotRexed and others added 2 commits November 11, 2026 15:17
…ich host_info reads now

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stub now sets vk12.shaderFloat16 = true. test_amd_subgroup_profiles and test_shader_stamp_cache pass locally. The gate sg32_stages == 0 && subgroup_size > 32 is false on RTX, RDNA, Intel and Arc, and the nf16 stamp word is only added without shaderFloat16, so caches on f16 GPUs stay valid.

Side effect: setting RX_VK_NO_SUBGROUP_PIN on RDNA now also matches GCN and loses mirrors and batching (RX_VK_MIRROR/RX_VK_BATCH still override).

This changes vk_shader.cpp, as do #2309, #2306, #2442 and #2457. Shipping them in one release means players re-prepare once.

@hckrman101
hckrman101 merged commit 1f8c02f into main Nov 11, 2026
blessedplummer added a commit to blessedplummer/Hookshot-optimi that referenced this pull request Nov 11, 2026
A dGPU has its own fast memory and a slower bus to system RAM; stock mirrors
every guest pool into VRAM and keeps both copies coherent with captures,
shadows and write-backs. At 4K that coherence is CPU-bound: the write-back
diff compares ~3.1 GB per 600 submits and mirror syncs move ~137k dirty
pages, for 35.6 fps where 1080p runs 60 (issue NotRexed#2099).

Under RX_VK_DGPU=1 a pool's first binding makes no mirror (MIRROR_REFUSED,
the state iGPU runs use today): reads take the lock-free TLS path and GPU
reads/writes land in the imported guest memory, coherent by construction -
no mirror sync, no write-back. Every 600-submit window the heaviest
guest-memory-read pool with >=1 MB of reads and not write-dominated runs a
measured trial (120 frames direct -> 12 settle, held until the mirror is
really on -> 120 mirrored, on the recordings' own timestamps); the mirror
stays only when the win is both >10% (RX_VK_DGPU_GAIN) and >0.25 ms
(RX_VK_DGPU_FLOOR) on present-to-present frame time, so the verdict sees
the mirror's CPU cost and its PCIe benefit alike. Trials whose windows did
not measure the same workload (zero binds, a >2x bind shift, a mirror that
never came) abort with no verdict and no skip line; a pool that aborts
three times sits out the session, and iGPUs run no trials. A lost verdict
persists to hookshot_mirror_skip.txt beside the game's own state files,
keyed to the device and driver (a foreign or header-less file is ignored
and re-trialled). RX_VK_DGPU_TRIALS=0 is pure direct. Trials need serial
replay (documented); under RX_VK_PARALLEL the policy runs trial-less.
Unset = stock exactly: the added counters and logging are gated on the
switch.

Refused pools read uniform buffers as submitted (NotRexed#1795's per-pool form:
the snapshot engages for mirror-less pools at the read, trial-won pools
keep in-place mirror binds), so the artifact class the issue documents on
mirror-less GPUs is covered under the policy too.

Measured on RX 9070 XT / RADV, 4K, Uncapped frame cap, warp-normalized
adjacent pairs (Hyrule Field): 70.2->78.0, 71.0->77.0, 59.6->76.6 fps
(+9.7% median), draw parity 705 vs 709, GPU 9.2 ms of a ~14 ms frame.
Mirror-sync dirty pages 136,872 -> 0; write-back diff 3.06-3.13 GB ->
330 MB or less per 600 submits.

Review round 2 (external): RX_VK_DGPU_TRIALS=0's predicate was inverted
and now matches the docs (=0 is pure direct); the window's read counter
moved off a shared map into the pool (dgpu_read, both direct paths);
Settle holds until the mirror is actually live; zero-bind baselines and
>2x bind drift between the direct and mirrored windows abort the trial
un-blacklisted; keep_line refuses signed and trailing-garbage lines.

Review round 3 (hckrman101, Joey-Magnus): refused pools read UBs as
submitted (NotRexed#1795 per-pool, live-smoked at 1.26M copies fault-free);
verdicts decide on present-to-present frame time instead of GPU-only
time (the mirror's CPU and upload-buffer cost is what the frame pays);
the policy runs trial-less under RX_VK_PARALLEL and its fast-path
counters are parallel-safe; no trials on iGPUs and a 3-abort cap; the
skip list carries a device+driver header and ignores foreign files; the
test driver uses {cxx}. Rebased onto main's env_on switch rule (NotRexed#2042) and the split-submission
fence work (NotRexed#2419); the trials' functions sit outside the write-back
observation's compile-span check (NotRexed#2131) and the GCN mirror-less default
(NotRexed#2343) composes unchanged (on GCN the switch runs trial-less direct plus
the UB snapshot).

Co-authored-by: GLM-5.3-Flash <glm-5.3-flash@opencode.noreply.github.com>
blessedplummer added a commit to blessedplummer/Hookshot-optimi that referenced this pull request Nov 11, 2026
A dGPU has its own fast memory and a slower bus to system RAM; stock mirrors
every guest pool into VRAM and keeps both copies coherent with captures,
shadows and write-backs. At 4K that coherence is CPU-bound: the write-back
diff compares ~3.1 GB per 600 submits and mirror syncs move ~137k dirty
pages, for 35.6 fps where 1080p runs 60 (issue NotRexed#2099).

Under RX_VK_DGPU=1 a pool's first binding makes no mirror (MIRROR_REFUSED,
the state iGPU runs use today): reads take the lock-free TLS path and GPU
reads/writes land in the imported guest memory, coherent by construction -
no mirror sync, no write-back. Every 600-submit window the heaviest
guest-memory-read pool with >=1 MB of reads and not write-dominated runs a
measured trial (120 frames direct -> 12 settle, held until the mirror is
really on -> 120 mirrored, on the recordings' own timestamps); the mirror
stays only when the win is both >10% (RX_VK_DGPU_GAIN) and >0.25 ms
(RX_VK_DGPU_FLOOR) on present-to-present frame time, so the verdict sees
the mirror's CPU cost and its PCIe benefit alike. Trials whose windows did
not measure the same workload (zero binds, a >2x bind shift, a mirror that
never came) abort with no verdict and no skip line; a pool that aborts
three times sits out the session, and iGPUs run no trials. A lost verdict
persists to hookshot_mirror_skip.txt beside the game's own state files,
keyed to the device and driver (a foreign or header-less file is ignored
and re-trialled). RX_VK_DGPU_TRIALS=0 is pure direct. Trials need serial
replay (documented); under RX_VK_PARALLEL the policy runs trial-less.
Unset = stock exactly: the added counters and logging are gated on the
switch.

Refused pools read uniform buffers as submitted (NotRexed#1795's per-pool form:
the snapshot engages for mirror-less pools at the read, trial-won pools
keep in-place mirror binds), so the artifact class the issue documents on
mirror-less GPUs is covered under the policy too.

Measured on RX 9070 XT / RADV, 4K, Uncapped frame cap, warp-normalized
adjacent pairs (Hyrule Field): 70.2->78.0, 71.0->77.0, 59.6->76.6 fps
(+9.7% median), draw parity 705 vs 709, GPU 9.2 ms of a ~14 ms frame.
Mirror-sync dirty pages 136,872 -> 0; write-back diff 3.06-3.13 GB ->
330 MB or less per 600 submits.

Review round 2 (external): RX_VK_DGPU_TRIALS=0's predicate was inverted
and now matches the docs (=0 is pure direct); the window's read counter
moved off a shared map into the pool (dgpu_read, both direct paths);
Settle holds until the mirror is actually live; zero-bind baselines and
>2x bind drift between the direct and mirrored windows abort the trial
un-blacklisted; keep_line refuses signed and trailing-garbage lines.

Review round 3 (hckrman101, Joey-Magnus): refused pools read UBs as
submitted (NotRexed#1795 per-pool, live-smoked at 1.26M copies fault-free);
verdicts decide on present-to-present frame time instead of GPU-only
time (the mirror's CPU and upload-buffer cost is what the frame pays);
the policy runs trial-less under RX_VK_PARALLEL and its fast-path
counters are parallel-safe; no trials on iGPUs and a 3-abort cap; the
skip list carries a device+driver header and ignores foreign files; the
test driver uses {cxx}. Follow-up fix: the drift check counted served
volume, so a working mirror's read collapse voided 40+ trials a run and
trials never converged; binds now count once at the serving path and the
drift rule compares the scene's pool set, with a three-no-verdict cap. Rebased onto main's env_on switch rule (NotRexed#2042) and the split-submission
fence work (NotRexed#2419); the trials' functions sit outside the write-back
observation's compile-span check (NotRexed#2131) and the GCN mirror-less default
(NotRexed#2343) composes unchanged (on GCN the switch runs trial-less direct plus
the UB snapshot).

Co-authored-by: GLM-5.3-Flash <glm-5.3-flash@opencode.noreply.github.com>
blessedplummer added a commit to blessedplummer/Hookshot-optimi that referenced this pull request Nov 11, 2026
A dGPU has its own fast memory and a slower bus to system RAM; stock mirrors
every guest pool into VRAM and keeps both copies coherent with captures,
shadows and write-backs. At 4K that coherence is CPU-bound: the write-back
diff compares ~3.1 GB per 600 submits and mirror syncs move ~137k dirty
pages, for 35.6 fps where 1080p runs 60 (issue NotRexed#2099).

Under RX_VK_DGPU=1 a pool's first binding makes no mirror (MIRROR_REFUSED,
the state iGPU runs use today): reads take the lock-free TLS path and GPU
reads/writes land in the imported guest memory, coherent by construction -
no mirror sync, no write-back. Every 600-submit window the heaviest
guest-memory-read pool with >=1 MB of reads and not write-dominated runs a
measured trial (120 frames direct -> 12 settle, held until the mirror is
really on -> 120 mirrored, on the recordings' own timestamps); the mirror
stays only when the win is both >10% (RX_VK_DGPU_GAIN) and >0.25 ms
(RX_VK_DGPU_FLOOR) on present-to-present frame time, so the verdict sees
the mirror's CPU cost and its PCIe benefit alike. Trials whose windows did
not measure the same workload (zero binds, a >2x bind shift, a mirror that
never came) abort with no verdict and no skip line; a pool that aborts
three times sits out the session, and iGPUs run no trials. A lost verdict
persists to hookshot_mirror_skip.txt beside the game's own state files,
keyed to the device and driver (a foreign or header-less file is ignored
and re-trialled). RX_VK_DGPU_TRIALS=0 is pure direct. Trials need serial
replay (documented); under RX_VK_PARALLEL the policy runs trial-less.
Unset = stock exactly: the added counters and logging are gated on the
switch.

Refused pools read uniform buffers as submitted (NotRexed#1795's per-pool form:
the snapshot engages for mirror-less pools at the read, trial-won pools
keep in-place mirror binds), so the artifact class the issue documents on
mirror-less GPUs is covered under the policy too.

Measured on RX 9070 XT / RADV, 4K, Uncapped frame cap, warp-normalized
adjacent pairs (Hyrule Field): 70.2->78.0, 71.0->77.0, 59.6->76.6 fps
(+9.7% median), draw parity 705 vs 709, GPU 9.2 ms of a ~14 ms frame.
Mirror-sync dirty pages 136,872 -> 0; write-back diff 3.06-3.13 GB ->
330 MB or less per 600 submits.

Review round 2 (external): RX_VK_DGPU_TRIALS=0's predicate was inverted
and now matches the docs (=0 is pure direct); the window's read counter
moved off a shared map into the pool (dgpu_read, both direct paths);
Settle holds until the mirror is actually live; zero-bind baselines and
>2x bind drift between the direct and mirrored windows abort the trial
un-blacklisted; keep_line refuses signed and trailing-garbage lines.

Review round 3 (hckrman101, Joey-Magnus): refused pools read UBs as
submitted (NotRexed#1795 per-pool, live-smoked at 1.26M copies fault-free);
verdicts decide on present-to-present frame time instead of GPU-only
time (the mirror's CPU and upload-buffer cost is what the frame pays);
the policy runs trial-less under RX_VK_PARALLEL and its fast-path
counters are parallel-safe; no trials on iGPUs and a 3-abort cap; the
skip list carries a device+driver header and ignores foreign files; the
test driver uses {cxx}. Follow-up fix: the drift check counted served
volume, so a working mirror's read collapse voided 40+ trials a run and
trials never converged; binds now count once at the serving path and the
drift rule compares the scene's pool set, with a three-no-verdict cap. Rebased onto main's env_on switch rule (NotRexed#2042) and the split-submission
fence work (NotRexed#2419); the trials' functions sit outside the write-back
observation's compile-span check (NotRexed#2131) and the GCN mirror-less default
(NotRexed#2343) composes unchanged (on GCN the switch runs trial-less direct plus
the UB snapshot). After the presenter-thread refactor (NotRexed#2325) the
present-to-present tick lives in present_finish, which every made present
passes exactly once in either threading mode, with a headless twin so
offscreen runs stay measurable.

Co-authored-by: GLM-5.3-Flash <glm-5.3-flash@opencode.noreply.github.com>
Sellierm added a commit to Sellierm/Hookshot that referenced this pull request Nov 12, 2026
… no VRAM mirror

With no VRAM mirror (mirror_on() off) a draw's uniform buffers are bound in
the guest pools, and the GPU reads them when it runs the frame: by then the
game has written the next frame's per-object matrices over them. On Intel
Core Ultra iGPUs the Kokiri depth prepass drew the ground off screen and the
fog pass painted sky over it (the white veil, NotRexed#999). Still there on main
after NotRexed#2419.

RX_VK_UB_SNAPSHOT (copy the uniforms into the stream as submitted) is now on
by default on Intel when the pools are not mirrored. Other unmirrored GPUs
(AMD APUs, GCN since NotRexed#2343, RX_VK_MIRROR=0) keep it off until measured.
RX_VK_UB_SNAPSHOT=0/1 overrides.

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
blessedplummer added a commit to blessedplummer/Hookshot-optimi that referenced this pull request Nov 12, 2026
A dGPU has its own fast memory and a slower bus to system RAM; stock mirrors
every guest pool into VRAM and keeps both copies coherent with captures,
shadows and write-backs. At 4K that coherence is CPU-bound: the write-back
diff compares ~3.1 GB per 600 submits and mirror syncs move ~137k dirty
pages, for 35.6 fps where 1080p runs 60 (issue NotRexed#2099).

Under RX_VK_DGPU=1 a pool's first binding makes no mirror (MIRROR_REFUSED,
the state iGPU runs use today): reads take the lock-free TLS path and GPU
reads/writes land in the imported guest memory, coherent by construction -
no mirror sync, no write-back. Every 600-submit window the heaviest
guest-memory-read pool with >=1 MB of reads and not write-dominated runs a
measured trial (120 frames direct -> 12 settle, held until the mirror is
really on -> 120 mirrored, on the recordings' own timestamps); the mirror
stays only when the win is both >10% (RX_VK_DGPU_GAIN) and >0.25 ms
(RX_VK_DGPU_FLOOR) on present-to-present frame time, so the verdict sees
the mirror's CPU cost and its PCIe benefit alike. Trials whose windows did
not measure the same workload (zero binds, a >2x bind shift, a mirror that
never came) abort with no verdict and no skip line; a pool that aborts
three times sits out the session, and iGPUs run no trials. A lost verdict
persists to hookshot_mirror_skip.txt beside the game's own state files,
keyed to the device and driver (a foreign or header-less file is ignored
and re-trialled). RX_VK_DGPU_TRIALS=0 is pure direct. Trials need serial
replay (documented); under RX_VK_PARALLEL the policy runs trial-less.
Unset = stock exactly: the added counters and logging are gated on the
switch.

Refused pools read uniform buffers as submitted (NotRexed#1795's per-pool form:
the snapshot engages for mirror-less pools at the read, trial-won pools
keep in-place mirror binds), so the artifact class the issue documents on
mirror-less GPUs is covered under the policy too.

Measured on RX 9070 XT / RADV, 4K, Uncapped frame cap, warp-normalized
adjacent pairs (Hyrule Field): 70.2->78.0, 71.0->77.0, 59.6->76.6 fps
(+9.7% median), draw parity 705 vs 709, GPU 9.2 ms of a ~14 ms frame.
Mirror-sync dirty pages 136,872 -> 0; write-back diff 3.06-3.13 GB ->
330 MB or less per 600 submits.

Review round 2 (external): RX_VK_DGPU_TRIALS=0's predicate was inverted
and now matches the docs (=0 is pure direct); the window's read counter
moved off a shared map into the pool (dgpu_read, both direct paths);
Settle holds until the mirror is actually live; zero-bind baselines and
>2x bind drift between the direct and mirrored windows abort the trial
un-blacklisted; keep_line refuses signed and trailing-garbage lines.

Review round 3 (hckrman101, Joey-Magnus): refused pools read UBs as
submitted (NotRexed#1795 per-pool, live-smoked at 1.26M copies fault-free);
verdicts decide on present-to-present frame time instead of GPU-only
time (the mirror's CPU and upload-buffer cost is what the frame pays);
the policy runs trial-less under RX_VK_PARALLEL and its fast-path
counters are parallel-safe; no trials on iGPUs and a 3-abort cap; the
skip list carries a device+driver header and ignores foreign files; the
test driver uses {cxx}. Follow-up fix: the drift check counted served
volume, so a working mirror's read collapse voided 40+ trials a run and
trials never converged; binds now count once at the serving path and the
drift rule compares the scene's pool set, with a three-no-verdict cap.
The set rule still voided every trial in a streaming scene, so anchors
now match by guest range (a recycled pool is the same anchor), anchor at
4x the floor, void only when a substantial fraction of the scene moves,
and the candidate floor rose to 256 MB of reads a window (the verdict
floor is unreachable below it - a trial there is churn by construction).
Rebased onto main's env_on switch rule (NotRexed#2042) and the split-submission
fence work (NotRexed#2419); the trials' functions sit outside the write-back
observation's compile-span check (NotRexed#2131) and the GCN mirror-less default
(NotRexed#2343) composes unchanged (on GCN the switch runs trial-less direct plus
the UB snapshot). After the presenter-thread refactor (NotRexed#2325) the
present-to-present tick lives in present_finish, which every made present
passes exactly once in either threading mode, with a headless twin so
offscreen runs stay measurable.

Co-authored-by: GLM-5.3-Flash <glm-5.3-flash@opencode.noreply.github.com>
NotRexed pushed a commit that referenced this pull request Nov 12, 2026
… no VRAM mirror (#2464)

This is my first contribution to Hookshot, so I am happy to change
anything about the approach, the style or the scope. I did the
investigation and the change with the help of AI, as the commit trailers
say. I checked the results on my own machine, described below.

On Intel Core Ultra iGPUs, Kokiri Forest is covered by a white veil: the
ground is missing and the sky/fog is painted over it. The mechanism is
not Intel-specific (any GPU whose buffer pools have no VRAM mirror can
read uniform buffers stale), but the new default is limited to Intel for
now.

## Why

With no VRAM mirror (`mirror_on()` off: integrated GPUs, GCN since
#2343, `RX_VK_MIRROR=0`), a draw's uniform buffers are bound straight
into the imported guest pools. The GPU reads them when it runs the
frame. By then the game has already written the next frame's per-object
matrices over the ones that were submitted.

In Kokiri, the depth prepass (generic program `ffb53ed102dd9908`) drew
the terrain with the wrong `c4` matrices, so the terrain landed off
screen. The depth there stayed at far (1.0). The fog/atmosphere
composite (`1a7482c0ec9991ea`) then treats every pixel above 0.9999 as
sky and painted fog over the ground. The forward pass draws the ground
colour correctly, which is why only the fog layer looked wrong. On a
discrete GPU the mirror is copied at submission time, so it never sees
the rewrite.

## What changed

`RX_VK_UB_SNAPSHOT` already copies a uniform buffer into the stream as
submitted (`memory_read_as_submitted`). It is now on by default on Intel
when the pools are not mirrored (`vendorID == 0x8086 &&
!pools_mirrored()`). Other GPUs without a mirror (AMD APUs, Steam Deck,
GCN, `RX_VK_MIRROR=0`) keep it off until someone measures the cost
there. `RX_VK_UB_SNAPSHOT=0` turns it off; `=1` forces it on anywhere.

## How it was found

- `RX_VK_DUMP_EACH=ffb53ed102dd9908` + `RX_VK_DUMP_ARGS` read the depth
back after each prepass draw. 551 of 558 draws added no depth at all,
including 12k-24k-index terrain meshes drawn while the depth was still
empty.
- A CPU projection of the same draws, using the logged vertices and
`c3`/`c4`, put 454 of them on screen with depth ~0.99.
- Writing depth with compare ALWAYS, no cull and depth clamp changed
nothing. Neither did copying the vertex and index buffers into the
stream as submitted. Copying the uniform buffers did: the prepass depth
went from ~45,000 far samples (of 57,600) to ~1,000, and the veil went
away.

## Testing

- **Machine:** Lenovo laptop (type 83DJ).
  - **CPU:** Intel Core Ultra 7 155H (Meteor Lake).
- **GPU:** integrated Intel Arc Graphics (8 Xe-cores), no dedicated GPU;
video memory is shared with system RAM.
  - **RAM:** 32 GB LPDDR5x-7467.
  - **OS:** Windows 11 Pro, build 26300 (Insider).
- **Driver:** Intel 32.0.101.9175, installed by hand (the version from
#741).
- **Game settings:** output resolution setting 3 (`HOS_OUTPUT_RES=3`),
`RX_VK_FIFO=1`, `RX_VK_PREPARE_BACKGROUND=1`.
- **Build:** v2.1.0 with this change; then current `main` (3e32949, with
#2419) built from source with this branch rebased on it.
- **Current main without this change:** the veil is still there (#2419
does not cover it). Numbers and screenshots in the comment below.
- **Kokiri, from the house to the village:**
  - before: white veil over the ground in every wide view;
  - after: ground, paths, deck and ivy correct, light fog as intended;
  - frame rate unchanged, ~8.5-11 fps in Kokiri at the same settings.
- No AMD or discrete GPU tested; the default there is unchanged.

Related: #999 (Intel tracker); report and workaround:
#999 (comment)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
hckrman101 pushed a commit that referenced this pull request Nov 12, 2026
The worklist was behind the forum and the merges. Since v2.1.0, many fix
PRs have merged, several rows already shipped, and the most-reported
problems in Discord #bug-reporting had no row. This PR brings
`docs/worklist.md` back in line, following the rules in the worklist and
in `docs/workflow.md`.

## Method
- **Forum snapshot.** All 409 #bug-reporting posts as of Nov 11, ~11:10
PM ET (Nov 7-11). Each post is dated by its thread ID, so the submit
time is in ET.
- **How dates were used.**
- v2.1.0 was cut from `main` at about 8:32 PM ET on Nov 10 and published
at 9:29 PM ET. A PR merged before the cut counts as shipped; I checked
this with `git merge-base --is-ancestor <merge commit> v2.1.0`.
- A report sent before a fix shipped and not repeated after it counts as
fixed.
- A report sent after v2.1.0, or before a fix that merged later, counts
as still open or waiting for a release.
- **Score.** Severity x reach x confidence.
- Reach comes from workflow.md's 2026-11-10 reporter shares. Where the
affected subset is unknown, it is marked as an upper bound.
- Confidence is 0.7 only where the GitHub tracker has logs from several
commenters; otherwise it is 0.4.
- **Owners.** New rows have no owner. Existing owners are kept.

## Changes
- **Added 20 rows (18 Open, 2 Fixed-waiting).**
- Open: RAM/VRAM fill-up to OOM (#993, 5.6; 13 threads after v2.1.0),
shader preparation stalls (3.2), AMD Windows crashes (#996, 2.9),
CPU-bound fps in Castle Town and Hyrule Field (#995), invisible objects
and doors (#1002), build.py stopped on v2.1.0, cutscene audio desync,
shadow artifacts, coloured lighting, HDR black screen and flicker, frame
generation stutter, the block grid around lights and fog, the v2.1.0
update failing, AF outlines, wrong button glyphs, Steam Deck at 6 fps,
Linux AMD slower than Windows, and the FSR 4 tick not rebuilding.
- Fixed-waiting: the pick-up counter above 60 fps (#2006) and RADV
device lost after the intro (#2362).
- **Updated 6 rows.**
- The Link-jump row is now the "above a 60 fps cap" row (#1742),
covering the Scarecrow's Song softlock, Gohma and the item hanging above
Link's head. It goes from S2 to S1 and is re-scored from 1.2 to 3.5.
- New forum links or scores on the antivirus, Intel black screen (0.11
to 0.2, #999 has logs), fairy grotto, voices (#2506) and RX 580 (#2343)
rows.
- **Moved 10 rows to Fixed, waiting for a release.** #2545 (ground
garbling), #2451 (RTX 30 crash), #2286, #2275, #2449, #2096, #2406,
#2515, #1954 and #2367.
- **Deleted 8 rows** whose fix shipped in v2.1.0: #1792, #1922, #1819,
#1863, #1447, #1451, #1769-#1810 and the HDR10 overlay.

## Not done
- PRs listed as "candidate" fixes are matched by title and description
only. None is confirmed against the reports.
- Long forum threads were judged from their summaries.
- `tests/run_all.py` was not run (docs-only change).

Co-authored-by: Joey Magnus <joey.magnus@cxalloy.com>
Brett-D added a commit to Brett-D/Hookshot that referenced this pull request Nov 12, 2026
main's NotRexed#2464 copies uniform buffers on Intel without mirrors; this keeps that and extends the default to every integrated GPU without mirrors (the Steam Deck), not to discrete GPUs without them (GCN since NotRexed#2343). The reads use env_on; a copy that could not be read is not kept for reuse. The event-wait default (e5d5054) is taken out here, to go separately with the design note.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants