fix(prompts): the hint bars on screen take a change of the prompts at once #2756

Open
github-sync wants to merge 5 commits from github/pr/2756 into main
Owner

@polackangry opened this pull request on GitHub · 2026-11-12 11:15 UTC · original

A change of Button prompts or of the controls while the game runs (#2366, #2435) reached every text shown after it, but the hint bars already on screen kept their old icons until they were shown again: the ocarina's bar ("Check songbook", "Play a note", "Cancel") while the ocarina is out, the pause menu's ("Rotate", "Select", "Back"), the songbook's ("Check song", "Listen to the melody").

Why

Each icon in those bars is a text box of its own with one code (the ocarina's bar: e046, e044, e045, e043, e042, e040, e041). The game lays each out and draws it once, when the bar appears, and doesn't call f_8152aec0 or f_8152b670 for it again while the bar stays, so text_rewrite never ran on them.

Marking them to be laid out again isn't enough on its own. Their pictures are made by another thread (UpdateTextBoxTextureThread, the box's +380 bit 2), which the new layout doesn't ask for a new picture. With the codes rewritten and no new picture, the icons came out blank: the new codes' glyphs were never rasterized. (Found with a log of the guest call chain at ft_smooth_render: the bars' glyphs are drawn from 0x834a2ec8 → f_80df2680 → f_80e6dfe0 → f_80e6e0e0, other boxes' from f_8152b670 → f_80e6e0e0.)

What it does (host/prompts.c)

  • shown_keep (in the layout hook): keeps the boxes with button codes the layout has seen (up to 256), with their first word, text pointer and length.
  • shown_mark (when the font changes, live_font): each kept box that is still the one seen gets the status bits hos_prompts_text_draw sets (0x20000 | 0x100000). Its next layout rewrites its codes on the game's own thread, as any box's. A box no longer the one seen (freed, or another text) is let go and never written.
  • shown_picture (in the draw hook): a box marked and laid out again, whose picture another thread makes, has its picture made at that draw. It calls f_80e6e0e0(box, the draw's two words at +24), the call f_8152b670 makes for the boxes whose picture it makes itself. The game's context is mid-call, so the call goes through hos_call_guest_kept (new in host/hos_hostcall.c, beside hos_call_guest), which puts every register (x0..x30, the vector registers, NZCV) back as it was; hos_guest_x reads the draw's x2. Boxes the draw renders itself are left to it.
  • prompts.c stays free of Ctx: tests/test_mods_size_table.py builds it with a hos.h of its own, and stubs the two new calls as it does hos_regions.
  • The live font change's log line counts the texts laid out again.
  • One picture of a box at a time (from the review): the texture thread's own call of f_80e6e0e0 (in f_80e6dfe0, its job) goes through hos_text_picture_thread (nx2/oot_patches.py patch_text_picture, host/hos_hostcall.c), which holds an SRW lock; shown_picture takes the same lock for its call. The game takes no lock around that call on either side, so without it the thread could make a box's picture while shown_picture made it too. The draw's own calls are left unlocked.

The bar's spacing (the layouts' part) is the bar's as it was made: after Keyboard → Xbox with the ocarina out, Xbox's wider LB/RB sit closer than at a fresh start until the bar is shown again. The icons are right; moving panes already made is a further step.

Screenshots

Each bar while it stays on screen, with Button prompts switched live. "Before" is main with #2755's notes, "after" this branch. The "before" runs had the default keys, the "after" runs the tester's own, so the keys differ; what matters is whether a bar follows the switch.

The ocarina, songbook and pause bars before and after a live switch

Testing

In game, native Windows 11, RTX 5060 Ti, the v131072 game, a build of this branch on main (4678050), driven with the control bridge (headless), a save in the Fire Temple:

Case Before After
The ocarina out, Keyboard → Xbox the keys stay LT · LB RB X Y B · A at once
The ocarina out, Xbox → Keyboard the pad's icons stay Z · Q R O P I · G at once
The songbook open, Keyboard ↔ Xbox "Listen to the melody" keeps its key follows both ways
The pause menu open, Xbox ↔ Keyboard "Rotate / Select / Back" keep theirs follow both ways
The game after plays on, no fault (234 fps in the field)

prompts: now the xbox icons in texts, 16 buttons on these controls; 10 texts on screen laid out again

Tests: test_prompts_live, test_prompt_font_bind, test_prompt_auto_face, test_menu_hooks, test_prompt_skip pass here; test_mods_size_table's folder part runs on Linux only (CI). After moving the guest call to hos_hostcall.c the game was built again (v2.1.0-rc.3-260, main 51da046 with both PRs) and the same run repeated: every bar follows Keyboard → Xbox → Keyboard as before, 10 texts on screen laid out again, the game plays on after the pause menu.

The branch is rebased on main (51da046) after the run; the files it touches didn't change there, and the tests above pass on it again. A row for the run is in docs/tested-configurations.md.

It's independent of the keyboard notes PR (each applies to main alone); the run above had both.

🤖 Generated with Claude Code

> [@polackangry](https://github.com/polackangry) opened this pull request on GitHub · 2026-11-12 11:15 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2756) A change of Button prompts or of the controls while the game runs (#2366, #2435) reached every text shown after it, but the hint bars already on screen kept their old icons until they were shown again: the ocarina's bar ("Check songbook", "Play a note", "Cancel") while the ocarina is out, the pause menu's ("Rotate", "Select", "Back"), the songbook's ("Check song", "Listen to the melody"). ## Why Each icon in those bars is a text box of its own with one code (the ocarina's bar: `e046`, `e044`, `e045`, `e043`, `e042`, `e040`, `e041`). The game lays each out and draws it once, when the bar appears, and doesn't call `f_8152aec0` or `f_8152b670` for it again while the bar stays, so `text_rewrite` never ran on them. Marking them to be laid out again isn't enough on its own. Their pictures are made by another thread (`UpdateTextBoxTextureThread`, the box's `+380` bit 2), which the new layout doesn't ask for a new picture. With the codes rewritten and no new picture, the icons came out blank: the new codes' glyphs were never rasterized. (Found with a log of the guest call chain at `ft_smooth_render`: the bars' glyphs are drawn from `0x834a2ec8 → f_80df2680 → f_80e6dfe0 → f_80e6e0e0`, other boxes' from `f_8152b670 → f_80e6e0e0`.) ## What it does (`host/prompts.c`) - `shown_keep` (in the layout hook): keeps the boxes with button codes the layout has seen (up to 256), with their first word, text pointer and length. - `shown_mark` (when the font changes, `live_font`): each kept box that is still the one seen gets the status bits `hos_prompts_text_draw` sets (`0x20000 | 0x100000`). Its next layout rewrites its codes on the game's own thread, as any box's. A box no longer the one seen (freed, or another text) is let go and never written. - `shown_picture` (in the draw hook): a box marked and laid out again, whose picture another thread makes, has its picture made at that draw. It calls `f_80e6e0e0(box, the draw's two words at +24)`, the call `f_8152b670` makes for the boxes whose picture it makes itself. The game's context is mid-call, so the call goes through `hos_call_guest_kept` (new in `host/hos_hostcall.c`, beside `hos_call_guest`), which puts every register (x0..x30, the vector registers, NZCV) back as it was; `hos_guest_x` reads the draw's x2. Boxes the draw renders itself are left to it. - `prompts.c` stays free of `Ctx`: `tests/test_mods_size_table.py` builds it with a `hos.h` of its own, and stubs the two new calls as it does `hos_regions`. - The live font change's log line counts the texts laid out again. - One picture of a box at a time (from the review): the texture thread's own call of `f_80e6e0e0` (in `f_80e6dfe0`, its job) goes through `hos_text_picture_thread` (`nx2/oot_patches.py` `patch_text_picture`, `host/hos_hostcall.c`), which holds an SRW lock; `shown_picture` takes the same lock for its call. The game takes no lock around that call on either side, so without it the thread could make a box's picture while `shown_picture` made it too. The draw's own calls are left unlocked. The bar's spacing (the layouts' part) is the bar's as it was made: after Keyboard → Xbox with the ocarina out, Xbox's wider LB/RB sit closer than at a fresh start until the bar is shown again. The icons are right; moving panes already made is a further step. ## Screenshots Each bar while it stays on screen, with Button prompts switched live. "Before" is main with #2755's notes, "after" this branch. The "before" runs had the default keys, the "after" runs the tester's own, so the keys differ; what matters is whether a bar follows the switch. ![The ocarina, songbook and pause bars before and after a live switch](https://raw.githubusercontent.com/polackangry/Hookshot/61394bc242987ccc59eae455af297688c4636f3b/pr2756/hint-bars-live.png) ## Testing In game, native Windows 11, RTX 5060 Ti, the v131072 game, a build of this branch on main (4678050), driven with the control bridge (headless), a save in the Fire Temple: | Case | Before | After | |---|---|---| | The ocarina out, Keyboard → Xbox | the keys stay | LT · LB RB X Y B · A at once | | The ocarina out, Xbox → Keyboard | the pad's icons stay | Z · Q R O P I · G at once | | The songbook open, Keyboard ↔ Xbox | "Listen to the melody" keeps its key | follows both ways | | The pause menu open, Xbox ↔ Keyboard | "Rotate / Select / Back" keep theirs | follow both ways | | The game after | | plays on, no fault (234 fps in the field) | `prompts: now the xbox icons in texts, 16 buttons on these controls; 10 texts on screen laid out again` Tests: `test_prompts_live`, `test_prompt_font_bind`, `test_prompt_auto_face`, `test_menu_hooks`, `test_prompt_skip` pass here; `test_mods_size_table`'s folder part runs on Linux only (CI). After moving the guest call to `hos_hostcall.c` the game was built again (v2.1.0-rc.3-260, main 51da046 with both PRs) and the same run repeated: every bar follows Keyboard → Xbox → Keyboard as before, `10 texts on screen laid out again`, the game plays on after the pause menu. The branch is rebased on main (51da046) after the run; the files it touches didn't change there, and the tests above pass on it again. A row for the run is in `docs/tested-configurations.md`. It's independent of the keyboard notes PR (each applies to main alone); the run above had both. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- github-sync:item:2756 -->
The ocarina's, the pause menu's and the songbook's hint bars are boxes the
game lays out and draws once, so a live change of Button prompts never
reached them. Keep the boxes with button codes, mark them for a new layout
when the font changes, and make the picture of those whose picture another
thread makes at their next draw (f_80e6e0e0), with the guest's registers put
back.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
test_mods_size_table builds prompts.c with a hos.h of its own, which has
no Ctx. hos_hostcall.c now has hos_guest_x and hos_call_guest_kept (the
call that puts every register back), prompts.c calls those with plain
integers, and the test stubs them as it does hos_regions.

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

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

I merged this into current main, together with #2755. tests/test_mods_size_table.py (including the folder part on Linux), test_prompts_live, test_prompt_font_bind and test_menu_hooks all pass. The only conflict with #2755 is the tested-configurations row.

The marking half (shown_keep/shown_mark) is careful: a box is matched by vtable, text pointer and length before its status word is touched. The part I'd like a maintainer to weigh is shown_picture (host/prompts.c:1141). From the draw thread, it calls f_80e6e0e0 on boxes whose +380 bit 2 says UpdateTextBoxTextureThread owns their picture. Nothing stops that thread from rasterizing the same box at the same moment, and f_80e6e0e0 may rely on locks that f_8152b670 takes before its own call. Restoring registers covers neither case. A race there would show up as a rare crash in FreeType or the upload, so one clean run doesn't rule it out.

Could you check whether f_80e6e0e0 or its caller takes a lock or a per-box flag? The other option is to set whatever bit makes the texture thread redo the picture itself.

> [@Joey-Magnus](https://github.com/Joey-Magnus) reviewed on GitHub · 2026-11-12 14:35 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2756#pullrequestreview-5876247190) I merged this into current main, together with #2755. tests/test_mods_size_table.py (including the folder part on Linux), test_prompts_live, test_prompt_font_bind and test_menu_hooks all pass. The only conflict with #2755 is the tested-configurations row. The marking half (shown_keep/shown_mark) is careful: a box is matched by vtable, text pointer and length before its status word is touched. The part I'd like a maintainer to weigh is shown_picture (host/prompts.c:1141). From the draw thread, it calls f_80e6e0e0 on boxes whose +380 bit 2 says UpdateTextBoxTextureThread owns their picture. Nothing stops that thread from rasterizing the same box at the same moment, and f_80e6e0e0 may rely on locks that f_8152b670 takes before its own call. Restoring registers covers neither case. A race there would show up as a rare crash in FreeType or the upload, so one clean run doesn't rule it out. Could you check whether f_80e6e0e0 or its caller takes a lock or a per-box flag? The other option is to set whatever bit makes the texture thread redo the picture itself. <!-- github-sync:r:5876247190 -->
Review of #2756: shown_picture calls f_80e6e0e0 from the draw thread on a
box whose picture UpdateTextBoxTextureThread makes, and nothing kept that
thread off the same box meanwhile. The game takes no lock around the
call on either side (f_8152b670's own call, f_80e6dfe0's job).

- nx2/oot_patches.py patch_text_picture: the texture thread's call
  (0x80e6e0a0 in f_80e6dfe0) goes through hos_text_picture_thread.
- host/hos_hostcall.c: hos_text_picture_thread holds an SRW lock around
  it; hos_text_picture_lock / unlock for shown_picture, which takes it
  around its own call. The draw's own calls are left unlocked.
- Tests: test_menu_hooks checks the patch, test_mods_size_table stubs
  the lock.

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

@polackangry commented on GitHub · 2026-11-12 15:06 UTC · original

Thanks, both points checked in the lifted code.

A lock on the caller's side: none. f_8152b670 calls f_80e6e0e0 for the boxes whose picture it makes itself (+380 bit 2 clear) right after loading [x2+24] / [x2+32], with no lock or per-box flag taken before (the only state it touches is +384 bit 16, set after the call). So f_80e6e0e0 doesn't rely on a lock its caller holds, and the game already runs it on the draw thread while UpdateTextBoxTextureThread makes other boxes' pictures.

The same box at once: a real gap, now closed. The texture thread's job (f_80e6dfd0 → f_80e6dfe0: the message is the job, its count at +0, the boxes at +8, SignalLightEvent on +48 when done) calls f_80e6e0e0 for each box with no lock either, so nothing in the game kept it off a box shown_picture was making.

The fix in 5eae6a5: one picture of a box at a time.

  • nx2/oot_patches.py patch_text_picture sends the texture thread's call (0x80e6e0a0 in f_80e6dfe0) through hos_text_picture_thread (host/hos_hostcall.c), which holds an SRW lock around it.
  • shown_picture takes the same lock around its own call.
  • The draw's own calls are left as they are, so the usual text drawing pays nothing; the texture thread takes an uncontended lock per box.

I tried your other route first, the texture thread making the picture again itself: after the relayout, clearing +384 bit 16 (the "picture made" flag f_8152d9a0 sets for those boxes), and also setting status bit 23 (what f_8152d9e0 checks) both left the bar blank. The trigger must be in the job's producer, which I didn't find in reasonable time; the lock covers the race without it.

Tested on Windows 11, RTX 5060 Ti, a build of this branch (main 51da046), the Fire Temple save, headless through the control bridge: the ocarina bar Keyboard → Xbox → Keyboard as before, then 12 more switches 20 frames apart with the ocarina out, no fault. test_menu_hooks now checks the new patch (once, declared, a lift patched again left alone, a file without the call untouched), test_mods_size_table stubs the lock, and the 19 checks that read oot_patches.py or hos_hostcall.c pass here, with test_prompts_live, test_prompt_font_bind, test_prompt_auto_face and test_prompt_skip.

🤖 Generated with Claude Code

> [@polackangry](https://github.com/polackangry) commented on GitHub · 2026-11-12 15:06 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2756#issuecomment-6827323612) Thanks, both points checked in the lifted code. **A lock on the caller's side: none.** `f_8152b670` calls `f_80e6e0e0` for the boxes whose picture it makes itself (`+380` bit 2 clear) right after loading `[x2+24]` / `[x2+32]`, with no lock or per-box flag taken before (the only state it touches is `+384` bit 16, set after the call). So `f_80e6e0e0` doesn't rely on a lock its caller holds, and the game already runs it on the draw thread while `UpdateTextBoxTextureThread` makes other boxes' pictures. **The same box at once: a real gap, now closed.** The texture thread's job (`f_80e6dfd0` → `f_80e6dfe0`: the message is the job, its count at `+0`, the boxes at `+8`, `SignalLightEvent` on `+48` when done) calls `f_80e6e0e0` for each box with no lock either, so nothing in the game kept it off a box `shown_picture` was making. The fix in 5eae6a5: one picture of a box at a time. - `nx2/oot_patches.py` `patch_text_picture` sends the texture thread's call (`0x80e6e0a0` in `f_80e6dfe0`) through `hos_text_picture_thread` (`host/hos_hostcall.c`), which holds an SRW lock around it. - `shown_picture` takes the same lock around its own call. - The draw's own calls are left as they are, so the usual text drawing pays nothing; the texture thread takes an uncontended lock per box. I tried your other route first, the texture thread making the picture again itself: after the relayout, clearing `+384` bit 16 (the "picture made" flag `f_8152d9a0` sets for those boxes), and also setting status bit 23 (what `f_8152d9e0` checks) both left the bar blank. The trigger must be in the job's producer, which I didn't find in reasonable time; the lock covers the race without it. Tested on Windows 11, RTX 5060 Ti, a build of this branch (main 51da046), the Fire Temple save, headless through the control bridge: the ocarina bar Keyboard → Xbox → Keyboard as before, then 12 more switches 20 frames apart with the ocarina out, no fault. `test_menu_hooks` now checks the new patch (once, declared, a lift patched again left alone, a file without the call untouched), `test_mods_size_table` stubs the lock, and the 19 checks that read `oot_patches.py` or `hos_hostcall.c` pass here, with `test_prompts_live`, `test_prompt_font_bind`, `test_prompt_auto_face` and `test_prompt_skip`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- github-sync:c:6827323612 -->
Conflicts: nx2/oot_patches.py (this branch's text picture lock and main's
zstd stream hook added at the same place: both applied, one after the
other; the build logs both patches) and docs/tested-configurations.md (both
sides' rows kept).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs/tested-configurations.md: both sides' rows kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Commenting is not possible because the repository is archived.
No description provided.