Skip to content

feat: an optional item wheel for the R/L item row, Twilight Princess style - #2261

Open
tapxgamestudio-coder wants to merge 4 commits into
NotRexed:mainfrom
tapxgamestudio-coder:feat/item-wheel
Open

tapxgamestudio-coder wants to merge 4 commits into
NotRexed:mainfrom
tapxgamestudio-coder:feat/item-wheel

Conversation

@tapxgamestudio-coder

@tapxgamestudio-coder tapxgamestudio-coder commented Nov 11, 2026 •

Copy link
Copy Markdown

Picking an item from the R/L item row means scrolling through it one item at a time, which gets slow with a full inventory. This adds an optional Twilight Princess style item wheel: hold R or L, point the left stick or the mouse at any item, and let go to equip it.

It is off by default (Insert menu > Gameplay > Item wheel), so nothing changes for anyone who does not turn it on. Off, the game's own row, its sounds and the menus' mouse support all work as before.

Screenshots

The wheel (2560x1152 scene on a 3440x1440 window, cropped to 16:9): every item in a ring over the blurred, dimmed game, the equipped one in gold, its name on a banner, counts as the HUD shows them.

The item wheel

The mouse pointing at an item (ringed in white); a click equips it and closes the wheel.

The item wheel with the mouse

The setting, and the game's own row with the setting off (unchanged).

The Item wheel setting

Item wheel off: the game's own row

What changed

  • host/oot_wheel.c (new): reads the item row's screen (vtable 0x84c0d430) on each update. It is called first from host/oot_menu.c's hos_oot_menu_tick and returns 1 while the wheel has the row, so the menus' mouse leaves that screen alone. The full item list is not in memory, so items are learned from the panes the row loads; slots not loaded yet are filled by briefly turning the hidden row and back (once per inventory change). A pick turns the row there with D-pad taps and keeps R held until it arrives, so letting go early still equips the item pointed at.
  • gpu/vk/vk_menu.cpp: draw_wheel draws the wheel with Dear ImGui under the menu and the FPS counter. It uses one ring, or two staggered rings from 17 items so slots stay large. The item icons are the game's own, put into one texture from wheel/icons.bin. A short opening animation spreads the slots out.
  • gpu/vk/vk_framegen.cpp, vk_draw.cpp: the backdrop is the scene before the HUD (the gameplay upscale's output, from TAAU, the temporal upscalers or FSR 3.1), blurred and dimmed, so the row, its hints and the HUD are gone from behind the wheel. Only with the Item wheel row on: off, the upscale hook returns at once and nothing is made or recorded. Frame interpolation is unchanged (the backdrop is painted onto the game's picture before it reads it).
  • host/nn_audio.c: the row's own open, select and close sounds play as usual. Its scroll tick, which would play once per hidden tap, is silenced while the wheel steers, and the wheel plays the same tick once per new slot instead (from wheel/sounds.bin).
  • host/display.c: while the wheel is open the mouse is released and its pointer centred; hovering points, and a click picks the item and closes the wheel.
  • If the game closes the row itself (a text box, damage, B) while the wheel is still turning it, the R it keeps held and the D-pad taps stop at once: the row counts as closed once the game's other screens have updated for 60 ms without it.
  • nx2/prompt_assets.py, nx2/wheel_sounds.py: the build makes wheel/icons.bin (prompt_assets.py --wheel-icons, with the prompts' decoders, so Pillow and texture2ddecoder stay in prompt_assets.py; a run of its own at every build, about 3 s, outside the prompts' reuse from perf(build): keep unchanged button-prompt assets in source builds #2698, which covers only the prompts' inputs and outputs) and wheel/sounds.bin (standard library and zstandard) from the player's RomFS. As with the prompts, nothing made from the game leaves the PC. Without the files the wheel shows item names and its tick is silent; a failure does not stop the build.
  • Docs: the Item wheel row in the readme's options; HOS_ITEM_WHEEL, HOS_WHEEL_TRACE and RX_VK_WHEEL_BACKDROP in docs/development.md.
  • New option id 60 (HOS_OPT_ITEM_WHEEL), MKWVK_API_VERSION 20 (MkwVkHost.wheel, after upstream's 19, feat: add separate button mappings for the ocarina notes #1930; it moves up again if feat(paths): saves and settings in a per-user data folder, caches and logs in the game folder's cache/ and logs/ #2317, feat(audio): the microphone heard on audio interfaces, with a volume, a channel and a test on the Audio tab #2459, feat(mods): a Mods tab, merged resource size tables, ExeFS patches and Atmosphere cheats #2478 or feat(amiibo): read amiibo from your own figure dumps #2479 lands first), and window message WM_APP + 20 for the wheel's mouse.

Testing

Windows 11, NVIDIA GeForce RTX 3080 (driver 621.01), 3440x1440 window, frame generation x2. In game:

  • Inventories: the Deku Tree with a child save (14 items) and with a save holding few items (2 and 3 items). Adult Link (16 items) through the age switch.
  • Item changes: adding and removing items mid-session, and the Hookshot to Longshot change (same list length).
  • Picking: the stick, mouse hover and click, R or L held.
  • While doing other things: ZR held, swimming, the pause menu (R still changes its tabs, no wheel).
  • The setting: switched on and off live in the menu. Off, with frame generation x2: the game's row, and no wheel lines in the log (nothing made or recorded).
  • The game closing the row mid-pick: B pressed while the wheel was still turning the row with R kept held: the trace shows the wheel letting go of R within one tap, and the row doesn't reopen.
  • Sounds: checked in a recording of the audio output: open, one tick per pick, select and close.

Game-free checks: tests/run_all.py, with the Vulkan and SPIR-V headers found (RX_CPM), fails the same 6 tests on this PC as clean main does (tools missing here), and passes the rest, test_framegen_resources and test_present_source_crop included. New tests/test_wheel_assets.py checks that the build makes both files in the layouts their readers expect, and that every sound the host plays is one the build writes. test_fps_overlay_order.py now covers the wheel's place among the overlays.

Not tested:

  • A text box or cutscene opening during the wheel (couldn't trigger one; B closing the row is the case tested above).
  • Epona, and Linux (the wheel has no mouse support there yet; the stick works).
  • GPUs where the gameplay network does not translate (temporal_rejected_network): there the backdrop falls back to the game's picture with its row.
  • A game stall while the wheel holds R should still count as open (no screen updates in a stall), but I couldn't reproduce one to test.

Assisted-by: Claude Code (Claude Opus 5.5)

🤖 Generated with Claude Code

@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 wheel itself looks great in the screenshots, and the host side is careful: the seqlock publish/read pair is correct, the renderer only ever reads the host's snapshot (never guest memory), and a stale row closes the wheel from the renderer's side. I focused on the parts that affect players who never turn the wheel on, on the repo rules, and on the input injection. I ran the touched checks on the PR merged with current main (no textual conflicts). I didn't review nn_audio.c's sound path or the ImGui drawing in detail.

blocking gpu/vk/vk_framegen.cpp:1131-1135, 1200: the "off by default" isn't true for the renderer. wheel_state() returns host->wheel(), which is never null (hos_wheel_state always returns &wheel_state), so framegen_on() || wheel_state() is always true. On every frame, for every player, whatever the Item wheel row says, framegen_hudless now builds the four HUD pipelines once, makes two scene-sized images, and records a vkCmdCopyImage of the gameplay upscale's output with two full_barriers. That's paid on every hardware tier, including the Deck and integrated GPUs, where a full barrier mid-replay is not cheap. Please gate it on the option (and the wheel being open), for example wheel_open() or opts.item_wheel, so the default path records nothing new.

blocking gpu/vk/vk_framegen.cpp:987, 1044 (and the hud_put calls): with frame generation on, the interpolated frames are now made from fg.hudless and the HUD is pasted back from the nearest real frame, for everyone, not just wheel users. The FSR 3.1 frame interpolation input (in.color) changes too. That's a separate feature ("HUD-aware frame generation") riding in an opt-in PR. It relies on a heuristic (scene vs. picture difference after pow(1/2.2), full weight at 1/8) that has only been seen on one RTX 3080, and it's on by default with RX_VK_FRAMEGEN_HUD=0 as the way out. Please split it into its own PR with before/after captures on AMD (FSR 3.1 FI) and NVIDIA, and keep this PR to the wheel's backdrop.

blocking: tests/test_framegen_resources.py fails at this head and merged with main (FAIL line 416: live.size() == 53 && barriers == 12 && framegen_on()), and passes on main. It compiles the real framegen constructors, and ensure_size now makes hudless/masks too. Your local run probably skipped it, since it needs Vulkan headers. Please update the check's expected counts in this PR, as AGENTS.md asks, rather than loosening it. CI has only run the title check here so far.

blocking nx2/wheel_icons.py:27, nx2/make_game.py:1890: CONTRIBUTING limits Pillow, texture2ddecoder, astc-encoder-py and fontTools to nx2/prompt_assets.py and nx2/prompt_draw.py. wheel_icons.py imports PIL and decodes through prompt_assets.decode (texture2ddecoder/astc). This needs a maintainer's explicit OK and a CONTRIBUTING update, or the icon decoding moved behind prompt_assets.py (for example a --wheel-icons output of the prompts step) so the dependency stays where it is allowed.

should fix host/oot_wheel.c:454, 569: R is kept held for the game for up to 4 s after the player lets go (the comment says 1.5 s), and while R is kept, a row that has stopped updating counts as open for ROW_STALL_NS (500 ms). If the game closes the row itself during that time (a text box, a cutscene, damage), the synthetic R and the D-pad taps reach gameplay for up to half a second. Your own comment notes that LEFT opens the pause menu there. You list this case as untested. Could you either drop the synthetic input as soon as the row's screen is gone, rather than after the stall window, or test it (for example opening the wheel as a Navi prompt fires)? Please also fix the comment.

should fix (docs): the new Gameplay row "Item wheel" needs a line in the readme's option table (#2105 adds a check that will fail without it). The new switches (HOS_ITEM_WHEEL, HOS_WHEEL_BACKDROP, HOS_WHEEL_SURVEY, HOS_WHEEL_TRACE, HOS_WHEEL_PROBE, RX_VK_FRAMEGEN_HUD) should go into docs/development.md's switch tables, as CONTRIBUTING asks.

coordination: MKWVK_API_VERSION 18 is also what #2317 uses for MkwVkHost.data_dir, so whichever lands second needs 19 and its field after the other's. The "(wheel mod)" tags in comments read like a fork's labels. The repo marks its own additions "(hookshot)".

tapxgamestudio-coder added a commit to tapxgamestudio-coder/Hookshot that referenced this pull request Nov 11, 2026
… HUD-aware interpolation, icons in prompt_assets.py

- gpu/vk/vk_framegen.cpp, vk_draw.cpp: the scene copy, its images and
  passes only with the Item wheel row on (framegen_wheel_on; with it
  off the upscale hook returns at once and nothing is made or
  recorded). It still copies every frame while the row is on: the game
  stops drawing its scene while its item row is shown, so the frame
  before the row opens is the backdrop. Pictures of an earlier size go
  when the timeline passes their retirement value (as vk_fsr3.cpp),
  and drop_size is main's again.
- The HUD-aware frame interpolation is out: hudless/masks, the split
  and put passes, RX_VK_FRAMEGEN_HUD, and the FSR 3.1 FI input are
  main's again (test_framegen_resources passes with its counts as
  they were).
- nx2/wheel_icons.py is gone: the icons are prompt_assets.py's
  write_wheel_icons (--wheel-icons), so Pillow and the texture
  decoders stay there (CONTRIBUTING.md). The file is byte-identical.
  test_wheel_assets.py checks it and that wheel_sounds.py imports none
  of the prompt-only packages.
- host/oot_wheel.c: the row counts as closed once other screens have
  updated 60 ms without it (row_gone): the game closed it, so the R
  kept held and the D-pad taps stop at once, not 500 ms later. The
  comment on R's 4 s is right now. The probe (HOS_WHEEL_PROBE,
  HOS_WHEEL_SCAN) and HOS_WHEEL_SURVEY are gone.
- RX_VK_WHEEL_BACKDROP (was HOS_WHEEL_BACKDROP: the renderer reads it),
  HOS_ITEM_WHEEL and HOS_WHEEL_TRACE in docs/development.md; the Item
  wheel row in the readme. "(wheel mod)" labels are "(hookshot)".

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.

Confirmed blockers:

  • wheel_state() (gpu/vk/vk_framegen.cpp:1131-1136) returns host->wheel(), which is never null, so framegen_on() || wheel_state() at :1200 is always true. Every player pays for the HUD pipelines, two scene-sized images, a copy and two full barriers, including Steam Deck and iGPUs.
  • tests/test_framegen_resources.py fails at the head (line 416: live.size() == 53 && barriers == 12) and passes on main.
  • HUD-aware frame generation is a second feature, on by default for everyone with frame generation (RX_VK_FRAMEGEN_HUD=0 turns it off). It changes FSR 3.1's input and was only seen on one RTX 3080. Please split it into its own PR.
  • nx2/wheel_icons.py:27 imports Pillow outside the two files CONTRIBUTING allows.
  • MKWVK_API_VERSION 18 is also claimed by #2317, #2459, #2478 and #2479.
  • Comments say "(wheel mod)" where the repo uses "(hookshot)".

@hckrman101

Copy link
Copy Markdown
Collaborator

This now conflicts with main in gpu/vk/vk_api.h. Could you merge main into the branch (or rebase) when you get a chance? I'll take another look once it's up to date.

tapxgamestudio-coder added a commit to tapxgamestudio-coder/Hookshot that referenced this pull request Nov 11, 2026
… HUD-aware interpolation, icons in prompt_assets.py

- gpu/vk/vk_framegen.cpp, vk_draw.cpp: the scene copy, its images and
  passes only with the Item wheel row on (framegen_wheel_on; with it
  off the upscale hook returns at once and nothing is made or
  recorded). It still copies every frame while the row is on: the game
  stops drawing its scene while its item row is shown, so the frame
  before the row opens is the backdrop. Pictures of an earlier size go
  when the timeline passes their retirement value (as vk_fsr3.cpp),
  and drop_size is main's again.
- The HUD-aware frame interpolation is out: hudless/masks, the split
  and put passes, RX_VK_FRAMEGEN_HUD, and the FSR 3.1 FI input are
  main's again (test_framegen_resources passes with its counts as
  they were).
- nx2/wheel_icons.py is gone: the icons are prompt_assets.py's
  write_wheel_icons (--wheel-icons), so Pillow and the texture
  decoders stay there (CONTRIBUTING.md). The file is byte-identical.
  test_wheel_assets.py checks it and that wheel_sounds.py imports none
  of the prompt-only packages.
- host/oot_wheel.c: the row counts as closed once other screens have
  updated 60 ms without it (row_gone): the game closed it, so the R
  kept held and the D-pad taps stop at once, not 500 ms later. The
  comment on R's 4 s is right now. The probe (HOS_WHEEL_PROBE,
  HOS_WHEEL_SCAN) and HOS_WHEEL_SURVEY are gone.
- RX_VK_WHEEL_BACKDROP (was HOS_WHEEL_BACKDROP: the renderer reads it),
  HOS_ITEM_WHEEL and HOS_WHEEL_TRACE in docs/development.md; the Item
  wheel row in the readme. "(wheel mod)" labels are "(hookshot)".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tapxgamestudio-coder added a commit to tapxgamestudio-coder/Hookshot that referenced this pull request Nov 11, 2026
… HUD-aware interpolation, icons in prompt_assets.py

- gpu/vk/vk_framegen.cpp, vk_draw.cpp: the scene copy, its images and
  passes only with the Item wheel row on (framegen_wheel_on; with it
  off the upscale hook returns at once and nothing is made or
  recorded). It still copies every frame while the row is on: the game
  stops drawing its scene while its item row is shown, so the frame
  before the row opens is the backdrop. Pictures of an earlier size go
  when the timeline passes their retirement value (as vk_fsr3.cpp),
  and drop_size is main's again.
- The HUD-aware frame interpolation is out: hudless/masks, the split
  and put passes, RX_VK_FRAMEGEN_HUD, and the FSR 3.1 FI input are
  main's again (test_framegen_resources passes with its counts as
  they were).
- nx2/wheel_icons.py is gone: the icons are prompt_assets.py's
  write_wheel_icons (--wheel-icons), so Pillow and the texture
  decoders stay there (CONTRIBUTING.md). The file is byte-identical.
  test_wheel_assets.py checks it and that wheel_sounds.py imports none
  of the prompt-only packages.
- host/oot_wheel.c: the row counts as closed once other screens have
  updated 60 ms without it (row_gone): the game closed it, so the R
  kept held and the D-pad taps stop at once, not 500 ms later. The
  comment on R's 4 s is right now. The probe (HOS_WHEEL_PROBE,
  HOS_WHEEL_SCAN) and HOS_WHEEL_SURVEY are gone.
- RX_VK_WHEEL_BACKDROP (was HOS_WHEEL_BACKDROP: the renderer reads it),
  HOS_ITEM_WHEEL and HOS_WHEEL_TRACE in docs/development.md; the Item
  wheel row in the readme. "(wheel mod)" labels are "(hookshot)".

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

Copy link
Copy Markdown
Author

Thanks for the careful review. Every point was right. All of it is addressed in the new head.

Your second pass looks like it read the earlier head (e2dffe5): wheel_state(), the HUD-aware passes, nx2/wheel_icons.py and the "(wheel mod)" labels were already gone in bbfa193, and are still gone now. The one new point, the API version, was real: #1990 took 18 on main today, so the branch is rebased onto current main and the wheel is 19 now. Point by point:

Blocking

  • The cost with the wheel off (vk_framegen.cpp): you were right, wheel_state() was never null, so every player paid for the copy and the barriers. Now framegen_wheel_on() gates everything on the Item wheel row (or HOS_ITEM_WHEEL): off, the gameplay upscale's hook returns at once, and no pipeline, image or command is made. The copy has to run every frame while the row is on, not only while the wheel is open: the game stops drawing its scene while its item row is shown, so the frame before the row opens is the only clean scene there is. I checked this in game: gating on wheel_open() alone left the game's row and HUD behind the wheel. With the row off, a run with frame generation x2 logs no wheel lines at all.
  • HUD-aware frame generation: removed. Interpolation, the FSR 3.1 FI input and the made frames are upstream's again (no hudless/masks images, no split/put passes, no RX_VK_FRAMEGEN_HUD). The backdrop is painted onto the game's picture before frame interpolation reads it, so the wheel doesn't need it. vk_framegen.cpp's diff against main is now only the backdrop (two passes: blur, then blur and dim).
  • test_framegen_resources: it was skipped here for lack of Vulkan headers. With them (RX_CPM pointed at the CPM cache), it passes at this head, with the counts unchanged, because ensure_size no longer makes the extra images.
  • Pillow and texture2ddecoder: nx2/wheel_icons.py is gone. The icons are now write_wheel_icons in nx2/prompt_assets.py, made by the prompts step with --wheel-icons <game>/wheel/icons.bin. The output is byte-identical to before. nx2/wheel_sounds.py uses only the standard library and zstandard (through prompt_assets.read_zs). tests/test_wheel_assets.py now fails if it imports one of the four prompt-only packages, or if wheel_icons.py comes back.

Should fix

  • Synthetic input after the game closes the row (host/oot_wheel.c): a stall and a close looked the same before. Now the menu tick also records when any other screen updates (other_tick_ns). If other screens go on updating for 60 ms without the row, the game has closed it (in a stall, nothing updates), so the wheel closes and R and the taps stop at once instead of after ROW_STALL_NS. Tested in game: pick an item 8 slots away, let go of R (R kept held), then press B in the middle of the taps. The trace shows the row closes (the game went on without it; R kept held: let go now) one tap after B, and the row doesn't reopen. Early-release picks still land (Slingshot to Nayru's Love with R let go after 0.7 s), and the trace showed no false closes in these runs. A game stall should still count as open, since no screen updates then, but I couldn't reproduce the 157 ms stall to test it. The comment now says 4 s and why.
  • Docs: the Item wheel row is in the readme's option table, with a line under the options. docs/development.md has HOS_ITEM_WHEEL, HOS_WHEEL_TRACE and RX_VK_WHEEL_BACKDROP (renamed from HOS_WHEEL_BACKDROP, since the renderer reads it). I removed the reverse-engineering probe and its switches (HOS_WHEEL_PROBE, HOS_WHEEL_SCAN) and the survey's HOS_WHEEL_SURVEY, so there are fewer switches to document.

Coordination

Tested on Windows 11, RTX 3080, frame generation x2, with the row off, the row on, early-release picks and B mid-pick, then again on the wheel after rebasing onto current main (fce5345). tests/run_all.py with the Vulkan and SPIR-V headers available: the same 4 failures as clean main on this PC (tools missing here), everything else passes. AMD, Intel and Linux are still untested.

tapxgamestudio-coder added a commit to tapxgamestudio-coder/Hookshot that referenced this pull request Nov 12, 2026
… HUD-aware interpolation, icons in prompt_assets.py

- gpu/vk/vk_framegen.cpp, vk_draw.cpp: the scene copy, its images and
  passes only with the Item wheel row on (framegen_wheel_on; with it
  off the upscale hook returns at once and nothing is made or
  recorded). It still copies every frame while the row is on: the game
  stops drawing its scene while its item row is shown, so the frame
  before the row opens is the backdrop. Pictures of an earlier size go
  when the timeline passes their retirement value (as vk_fsr3.cpp),
  and drop_size is main's again.
- The HUD-aware frame interpolation is out: hudless/masks, the split
  and put passes, RX_VK_FRAMEGEN_HUD, and the FSR 3.1 FI input are
  main's again (test_framegen_resources passes with its counts as
  they were).
- nx2/wheel_icons.py is gone: the icons are prompt_assets.py's
  write_wheel_icons (--wheel-icons), so Pillow and the texture
  decoders stay there (CONTRIBUTING.md). The file is byte-identical.
  test_wheel_assets.py checks it and that wheel_sounds.py imports none
  of the prompt-only packages.
- host/oot_wheel.c: the row counts as closed once other screens have
  updated 60 ms without it (row_gone): the game closed it, so the R
  kept held and the D-pad taps stop at once, not 500 ms later. The
  comment on R's 4 s is right now. The probe (HOS_WHEEL_PROBE,
  HOS_WHEEL_SCAN) and HOS_WHEEL_SURVEY are gone.
- RX_VK_WHEEL_BACKDROP (was HOS_WHEEL_BACKDROP: the renderer reads it),
  HOS_ITEM_WHEEL and HOS_WHEEL_TRACE in docs/development.md; the Item
  wheel row in the readme. "(wheel mod)" labels are "(hookshot)".

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

Copy link
Copy Markdown
Author

Rebased onto current main. The wheel is API version 20 now (#1930 took 19), and the wheel's sounds moved out of the source slices the new audio tests compile. Ready for another look.

hckrman101 pushed a commit that referenced this pull request Nov 12, 2026
)

**What this is for:** contributors rebuilding from source should not
spend another minute drawing the same button prompts. **What is
measured:** on an Intel Core i9-13900K running Arch Linux, the finish
stage at main `c544828f` took 67.707 and 66.898 seconds; two unchanged
runs took 0.603 and 0.651 seconds, and the final head took 0.616
seconds, clearing the run-time files as well. Other work was running
throughout. **Not measured:** Windows, a packaged Builder update, a
complete compile/lift, or gameplay performance.

Refs #2636.

## Problem

`nx2/make_game.py` removes the game folder's `prompts` directory and
runs `prompt_assets.py` on every build when fontTools is available. Most
source edits do not change anything those assets depend on.

This avoids drawing unchanged assets; it does not make the drawing
itself faster. #2636's A1 plans to measure the finish stage, but its
A1–A10 items do not already propose this prompt cache. It complements
#2674 and #2675, which address unnecessary retranslation rather than
prompt generation.

## What changed

- **`nx2/make_game.py`:** source-checkout builds request incremental
prompt generation. Packaged builders still regenerate every time, using
the finish stage's existing launcher distinction. `RX_RELEASE` cannot
distinguish them: make_game sets it for source builds too. Missing
fontTools still removes the replacement prompts, keeping the existing
fallback to the game's icons.
- **`nx2/prompt_assets.py`:** fingerprint the inputs before generating,
draw into a fresh sibling directory, and record the successful outputs
only if a second fingerprint still matches. Reuse requires both matching
inputs and every recorded output's size and SHA-256. **A failed
generation still removes the prompts, as before**, so the game shows its
own icons. An interrupted builder can leave the previous complete set
without a stamp; the next build regenerates it. The drawing, encoding
and asset formats are unchanged.
- **`tests/test_prompt_skip.py`:** game-free fixtures run the actual
finish-stage prompt call and production cache/publication functions,
with small authored files and a stand-in for expensive drawing.
- **`docs/development.md`:** explain reuse, its limits, and
`HOOKSHOT_FORCE_PROMPTS=1`. The force switch follows `env_on`: unset,
empty, `0`, `false`, `off` and `no` are off, ignoring case and
surrounding spaces.

### What prevents a wrong skip

The input record covers:

- The four prompt source modules, including controller, keyboard and
binding tables.
- The game's icon font and optional bold label font, compression
dictionary, common texture/layout archives, and matching resource-size
table names and contents.
- The repository's controller and keyboard fonts, and added, changed or
removed PNG overrides.
- Python/platform; fontTools, Pillow, ASTC encoder, texture decoder,
NumPy, zstandard and archspec availability and versions; Pillow's native
library versions and the selected ASTC backend.
- Generation parameters and relevant library switches, including
`SOURCE_DATE_EPOCH`, `FREETYPE_PROPERTIES` and the encoder's custom CPU
definitions.

Languages and saved bindings are not build inputs here: the generator
uses its source tables, and the runtime applies the selected controls.

The output record includes files **and directories**: the keyboard
directory itself is a runtime marker. Legitimately absent optional
outputs stay absent; a missing, truncated or same-size-corrupted
recorded file forces regeneration. The runtime's own compressed copies
and control-specific keys are excluded before hashing, not mistaken for
generator output. **The files the game derives at run time are still
cleared by every build**, including an unchanged hit: their own keys do
not cover changes to `host/prompts.c`. Only the known runtime filenames
are removed; if one cannot be cleared, the build regenerates instead of
keeping on doubt.

An unreadable input cannot authorize reuse. The log gives the reason,
even if a forced regeneration subsequently fails. A source edited during
generation leaves no stamp, addressing the stale-input certification
problem discussed in [#2674's
review](#2674 (comment)).

## How it was tested

2026-11-12; Arch Linux, kernel `7.2.15-1-omarchy`, x86-64, Intel Core
i9-13900K, 32 logical CPUs, Python 3.12.16. Base `c544828ff`; final head
`2ec89de8`. No game or GPU program was launched for these checks.

### Finish-stage measurements

These execute the real finish-stage statements through completion,
including binary/image copies, the unchanged shader-list check,
pipeline-list work and prompt generation, in a disposable copy of an
existing built game folder. Compilation, translation and shader
preparation are not included. They are **not whole rebuild times**.

All optional prompt packages were present: fontTools 4.68.0, Pillow
13.0.0, astc-encoder-py 0.1.12, texture2ddecoder 1.0.6, archspec 0.2.7,
NumPy 2.5.4 and zstandard 0.25.0.

| Run | Source | Finish time | 1-minute load before → after | Top other
work |
|---|---|---:|---:|---|
| Before 1 | `c544828f` | 67.707 s | 7.13 → 7.40 | Existing game 316 →
315% CPU; file indexing 66 → 79%; relay check also active |
| Before 2 | `c544828f` | 66.898 s | 7.06 → 8.24 | Existing game 315 →
318%; terminal process about 41% |
| First generation with cache | `63d5c965` | 66.400 s | 8.24 → 8.19 |
Existing game 318 → 320%; terminal process about 42% |
| Unchanged 1 | `63d5c965` | 0.603 s | 8.19 → 8.19 | Existing game 320%;
terminal process about 42% |
| Unchanged 2 | `63d5c965` | 0.651 s | 8.19 → 8.19 | Existing game 320%;
terminal process about 42% |
| Changed table source | `681208dd` | 63.147 s | 6.99 → 6.95 | Existing
game 320 → 321%; terminal process about 42% |
| Regenerate, final head | `2ec89de8` | 65.007 s | 5.77 → 5.86 |
Existing game 317 → 316%; terminal process about 43% |
| Unchanged, final head, with run-time files present | `2ec89de8` |
0.616 s | 5.79 → 5.79 | Existing game 316%; terminal process about 43% |

Other work was running; none was a quiet measurement. CPU percentages
are lifetime averages from snapshots, not interval measurements. The
first generation is included to show that this is a skip, not a faster
drawing algorithm.

The unchanged runs print:

```text
prompts: unchanged, kept
```

Adding a harmless comment to the button-table source forces a fresh
generation:

```text
prompts: regenerated (table changed)
```

The comment was removed afterwards. A timestamp-only touch does not
regenerate: the key checks content. An additional run on the final head
verifies an unchanged hit with all six runtime-derived files present:
all six are removed, while the 16 generated originals and the stamp
retain their sizes and SHA-256 values. The focused fixture also compares
the retained originals byte for byte. The final timing rows above
include that cleanup.

**Asset parity:** all 16 regenerated files, 274,704,680 bytes, compare
byte for byte with a full generation by main `c544828f`. Only the new
stamp is excluded. Both sides used the same `SOURCE_DATE_EPOCH` because
fontTools otherwise writes the current time into the OpenType header.
This comparison setting is not introduced into normal builds. A
development cache hit retains the earlier font timestamp; packaged
builds retain their old full-generation behavior.

### Game-free checks

- **23 focused tests pass** on the final change. They cover unchanged
reuse, every input kind, package versions/presence, switch/parameter
changes, missing/truncated/same-size-corrupted output, empty
directories, clearing runtime derivatives while retaining byte-identical
originals, regeneration when runtime cleanup fails, interruption,
mid-generation edits, unreadable input, force-switch parsing, incomplete
output, publication failure, and the packaged/no-fontTools paths. A
non-zero generator result removes prompts for both source and packaged
builds; interruption before that result leaves the old set unstamped.
- The same unchanged-build test against main's finish stage fails
behaviorally: `2 != 1: unchanged finish must not draw the prompts
twice`.
- Removing each of **18 guards/key components** in memory makes its
corresponding behavior test fail on the final change. Three later
corrections also fail against the earlier version of this patch:
returned failure retained old prompts in both build paths, runtime files
survived a hit, and an uncleared runtime file did not force
regeneration. The unreadable-input diagnostic has a separate
before-fails/after-passes proof.
- The whole suite one commit earlier, at `f20ea58b` (before a failed
generation was made to remove the prompts again and the run-time files
to be cleared), `python -I -B tests/run_all.py --jobs 8`:

  ```text
  472 passed, 0 failed, 20 skipped in 133 s (linux, 8 at once)
  ```

Load was 7.37 → 19.48. The first full run, at 32 checks at once, was
**471 passed, 1 failed, 20 skipped**: a clang++ child compiling
`texture_pass.cpp` for `nx2/test_depth_sample_types.py` died with
SIGKILL. That fixture and its input trees are unchanged from main. No
kernel OOM line was found, so the cause is not established. The complete
run with eight workers passes, including that check. The failed run is
retained, not counted as a pass. On the final head `2ec89de8` the
focused checks, the baseline failure and all guard mutations were rerun,
and the whole suite once more with eight workers: **471 passed, 1
failed, 20 skipped in 136 s** (load 11.14 → 22.62). The one failure is
`tests/test_release_scripts.py`,
`test_nightly_two_runs_at_once_end_at_the_newer_commit`, the
intermittent race reported in #2626; this change touches none of its
files.
- No core dumps were recorded. Nothing generated from the game is in
this change.

## Not tested

- Windows filesystem behavior or a Windows build.
- The packaged Windows Builder or Linux AppImage build/update end to
end. Their always-regenerate selection is covered by a game-free test
only.
- A full compile/lift or a new gameplay run, other CPUs, or
gameplay/frame-rate benefits.

No screenshots: the regenerated asset bytes match, and no visual change
is intended.

## Risks

The input list must grow when the generator gains new file dependencies
or outputs outside the prompt directory. A regression flags new local
module imports, but it does not replace reviewing new file reads.
Generation temporarily needs room for both old and new assets.
Publication uses completed directories and rollback on a rename error;
interruption cannot create a stamped half-set, though a process killed
between the two directory renames may require regeneration on the next
build.

## Overlaps

The open-PR scan found 47 PRs touching one of these files. The actual
conflict hunks were read:

- **#2151:** adjacent item-button generation conflicts with the
finish-stage prompt call/error branch. Preserve both steps and account
for added prompt dependencies.
- **#2261:** substantive overlap. It adds `--wheel-icons`, another input
folder and output outside `prompts`. Keep that work outside this skip or
extend the input/output proof and staging; accepting both hunks
mechanically is not enough.
- #1872, #1958, #2304, #2317, #2430, #2465, #2474, #2478, #2608, #2673,
#2675 and #2686 share code files but merged cleanly in the scan. That is
not proof that any newly added generator dependency is covered.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <noreply@openai.com>

Co-authored-by: GPT-6 Astra <noreply@openai.com>
tapxgamestudio-coder and others added 3 commits November 12, 2026 03:48
…yle (Gameplay > Item wheel, off by default)

Holding R or L shows the item row; with Gameplay > Item wheel on, the
row is shown as a ring of all the items over the blurred, dimmed game
instead. The left stick or the mouse points at an item and letting go
equips it, as the row would. Off (the default), nothing changes: the
game's own row, its sounds and the game's menus' mouse.

- host/oot_wheel.c: the row's screen (vtable 0x84c0d430) read on each
  update, from host/oot_menu.c's menu tick (first; the menus' mouse
  leaves the row's screen alone while the wheel has it). Its items are
  learned from the panes the row loads; the ones not loaded yet are
  surveyed by turning the hidden row and back. The pick turns the row
  with D-pad taps and keeps R held until it gets there. Home settles
  for 200 ms after an opening (the row's first update after its items
  change still gives the old cursor), a same-length list change clears
  the cached names, and L holds the wheel as R does.
- gpu/vk/vk_menu.cpp: the wheel drawn with Dear ImGui under the menu
  and the FPS counter (draw_wheel): one ring, or two from 17 items, the
  equipped item in gold, the pointed one ringed in white, the counts,
  and the focused item's name on a banner. The icons are the game's,
  one texture made from wheel/icons.bin.
- gpu/vk/vk_framegen.cpp, vk_draw.cpp: the backdrop is the scene
  before the HUD (the gameplay upscale's output, whichever upscaler
  made it), blurred and dimmed, so the row, its hints and the HUD are
  gone from behind the wheel. With frame generation the HUD is kept
  off the interpolated frames' motion the same way.
- host/nn_audio.c: the row's own open, select and close sounds play;
  its tick (once per hidden tap) is silent while the wheel steers, and
  the wheel ticks once per new slot instead, from wheel/sounds.bin.
- host/display.c: the mouse is let go while the wheel is open, the
  pointer in its middle; a click picks and closes.
- nx2/wheel_icons.py, nx2/wheel_sounds.py: make_game.py makes
  wheel/icons.bin and wheel/sounds.bin from the player's RomFS at every
  build, beside the button prompts (nothing of the game leaves the PC;
  without them the wheel shows names and its tick is silent).
- Option 60 (HOS_OPT_ITEM_WHEEL), MKWVK_API_VERSION 16 (MkwVkHost
  wheel). Tests: test_wheel_assets.py (the files' layouts against
  their readers), the overlay order, the menu mouse and screensaver
  fixtures' stand-ins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… HUD-aware interpolation, icons in prompt_assets.py

- gpu/vk/vk_framegen.cpp, vk_draw.cpp: the scene copy, its images and
  passes only with the Item wheel row on (framegen_wheel_on; with it
  off the upscale hook returns at once and nothing is made or
  recorded). It still copies every frame while the row is on: the game
  stops drawing its scene while its item row is shown, so the frame
  before the row opens is the backdrop. Pictures of an earlier size go
  when the timeline passes their retirement value (as vk_fsr3.cpp),
  and drop_size is main's again.
- The HUD-aware frame interpolation is out: hudless/masks, the split
  and put passes, RX_VK_FRAMEGEN_HUD, and the FSR 3.1 FI input are
  main's again (test_framegen_resources passes with its counts as
  they were).
- nx2/wheel_icons.py is gone: the icons are prompt_assets.py's
  write_wheel_icons (--wheel-icons), so Pillow and the texture
  decoders stay there (CONTRIBUTING.md). The file is byte-identical.
  test_wheel_assets.py checks it and that wheel_sounds.py imports none
  of the prompt-only packages.
- host/oot_wheel.c: the row counts as closed once other screens have
  updated 60 ms without it (row_gone): the game closed it, so the R
  kept held and the D-pad taps stop at once, not 500 ms later. The
  comment on R's 4 s is right now. The probe (HOS_WHEEL_PROBE,
  HOS_WHEEL_SCAN) and HOS_WHEEL_SURVEY are gone.
- RX_VK_WHEEL_BACKDROP (was HOS_WHEEL_BACKDROP: the renderer reads it),
  HOS_ITEM_WHEEL and HOS_WHEEL_TRACE in docs/development.md; the Item
  wheel row in the readme. "(wheel mod)" labels are "(hookshot)".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts' source slices

test_audio_aux and test_audio_biquad (NotRexed#1544, NotRexed#2648) compile slices of
host/nn_audio.c on their own: the reverb section up to the mixer, and
#define FRAME up to mix_depth. The wheel's sounds sat in both, and
their hos_game_path_a and MAX_PATH aren't there. The block now follows
mix_depth, still before render_frame, which mixes its cues in. A move
only; every tests/test_audio*.py passes.

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.

Thanks, the earlier blockers are fixed. With the row off, nothing is recorded or made (framegen_wheel_on). HUD-aware frame generation is gone. The icons are made in prompt_assets.py --wheel-icons. The readme and switch-table lines are in, and the labels say "(hookshot)". I merged it into current main and ran tests/run_all.py with the Vulkan headers: 469 passed. The only failures were test_lift_cache, which fails on main here, and a test_recorder_sync load timeout, which passes alone. test_framegen_resources and #2105's readme check pass too.

One bug: gpu/vk/vk_framegen.cpp:1038-1039 reads RX_VK_WHEEL_BACKDROP and HOS_ITEM_WHEEL with env_int, while host/oot_wheel.c:473 reads HOS_ITEM_WHEEL with env_on. With HOS_ITEM_WHEEL=on, the host turns the wheel on but the renderer follows the menu row and draws no backdrop. With =off and the row on, the wheel is off but the scene is still copied every frame. RX_VK_WHEEL_BACKDROP=off doesn't turn the backdrop off either. Please use env_on/env_value_off on both sides.

API version 20 is also claimed by #2317, #2566, #2594, #2600, #2604 and #2760, so whichever lands second has to bump it.

…P by the on/off rule

gpu/vk/vk_framegen.cpp read both with env_int, host/oot_wheel.c read
HOS_ITEM_WHEEL with env_on, so they disagreed on anything but 0 and 1:
HOS_ITEM_WHEEL=on turned the wheel on but left the renderer on the
menu's row (no backdrop behind the wheel), =off with the row on turned
the wheel off but still copied the scene every frame, and
RX_VK_WHEEL_BACKDROP=off did nothing. Both are read with env_value_off
now (as vk_memory.cpp's RX_VK_SHADOW_STAGE), the same as the host.

test_wheel_assets.py checks both sides read HOS_ITEM_WHEEL the same
way and that no wheel switch is read with env_int. The docs point the
two rows at the on/off rule.

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

Copy link
Copy Markdown
Author

Thanks. Fixed in 2fb1ed9: the renderer reads HOS_ITEM_WHEEL and RX_VK_WHEEL_BACKDROP with env_value_off now, the same as the host, and test_wheel_assets.py fails if the two readings differ or a wheel switch goes back to env_int. Checked in game with HOS_ITEM_WHEEL=on (backdrop drawn) and RX_VK_WHEEL_BACKDROP=off (none), and both readings compiled against hookshot_env.h agree for unset, 0, 1, on, off, no, false, yes, true and 2. On the API version: understood, whichever lands second bumps it.

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