WIP: feat(input): hold-to-toggle for any bound button #137

Draft
simonwjackson wants to merge 7 commits from simonwjackson/Longshot:feat/input-hold-to-toggle into main
Contributor

Why

Players who cannot keep a button held need another way to shield, target or aim. Hold-to-toggle makes one press hold a bound button and the next release it.

Carried over from https://github.com/NotRexed/Hookshot/pull/2886.

What changed

  • Controls: a Toggle column for Switch buttons, Off by default, saved as toggle_<button>=0|1. Plus, Minus, directions and note-only rows cannot toggle.
  • Input: one latch after remapping, shared by both gamepad bindings, keys and mouse buttons. Mouse Native uses latched ZR/L/R for right-stick aiming; L also retains pointing in either active mouse mode.
  • Resets: Insert, focus loss, rebinding, defaults and Plus/Minus clear latches. Enabled background pads can latch again after focus loss. Linux's existing pause-focus callback clears too; other Linux game dialogs are not guaranteed resets.
  • Documentation and tests cover these rules. Measured runs are now in docs/tested-configurations.md.

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
Controls → Toggle; toggle_<button>=0/1 Press once to hold, again to release a Switch button Off (0) for every button Keep Off by default; opt-in accessibility choice Players who cannot sustain a hold Plus/Minus, stick directions, ocarina-note-only rows Windows gameplay, physical controllers, other GPUs
Existing HOS_INPUT_BACKGROUND=0/1 / Controller in background Background latch reset follows the existing pad policy Off (0); environment overrides the menu Keep opt-in, consistent with normal pad input Background-pad users Keyboard/mouse input in the background Physical controllers, Windows gameplay

Measurements

main @ f3fe1763dd was merged in commit 68b95c1 (no rebase or force-push).
this PR @ 3582a10e01; the new Linux game build uses bc0c1292d8ce377c1d6fbcc6dc44372b8f6f74ec. Later commits change only tests and recorded evidence.

Review-fix proof

Baseline: pre-fix feature @ 68b95c1 (current main merged, before the review fix). Main alone has no latch, so these game-free regression checks compare the feature before and after the fix, using its real input/Npad path.

Game-free regression pre-fix feature @ 68b95c1 review fix @ bc0c1292
L pointing remains active after physical release Fails the pointing assertion Passes
Enabled background pad can latch ZL Fails the Npad button assertion Passes

Fresh build: rev137-bc0c1292 (source f94c58fd), Git bc0c1292, on an RX 7900 XT / RADV Mesa 26.2.3, Ryzen 5 7600X, 32 GB, NixOS 26.11 / Linux 7.2.7. Private headless Sway 1.12 + Xwayland, 1920×1080, render 100%, game upscaler, FG/HDR Off, forced mesh fallback, udmabuf enabled, audio off, copied save. Hyrule Castle Town (Market_Child), indoors by the wooden door. One 10m06s functional session, clean control-socket exit, no crash/device-lost/hang marker. Another isolated CPU build ran concurrently, so this is not an FPS comparison.

Input / setting Observed result on this PR @ bc0c1292
Mouse camera Native; ZR Toggle On; 16-frame ZR press at 21466–21481 Slingshot stays drawn after release. With no button pressed, 20 relative XTest mouse events of +20 horizontal counts move the aim from the wall to the door. No shot: 14 seeds before and after. Queue empty at frame 23052.
ZL Toggle On; 16-frame press at 18803–18818 Shield remains raised after release; queue empty at 18946.
Second 16-frame ZL press at 18954–18969 Shield returns to Link's back.
New shield latch, then open/close Insert with F1 Shield returns to Link's back.
Controls page ZL/ZR Toggle On, Plus/Minus disabled, revised help visible.

Reproduce: enable ZR/ZL Toggle and Mouse camera Native; click the game to capture the mouse, tap ZR and release, then move the mouse. Tap ZR again to release/fire and B to leave the aiming mode. In normal play, tap ZL twice to check shield hold/release. Open Insert to clear a new latch. The test sent relative XTest events to the private Xwayland display; xdotool pointer warps did not produce relative motion and are not counted as proof.

press ZR, frames=16: {"frame":21482,"start":21466,"end":21482}
status after relative mouse motion: {"frame":23052,"input":{"queued":0,"exclusive":false},"map":"Market_Child"}
press ZL, frames=16: {"frame":18819,"start":18803,"end":18819}
status after release: {"frame":18946,"input":{"queued":0,"exclusive":false}}
second press ZL, frames=16: {"frame":18970,"start":18954,"end":18970}
option mouse_camera: {"value":1,"text":"Native"}

The real read_input/Npad regression fixture failed before the fix on latched L pointing and background pads. It now passes for L/R/ZL/ZR, all three mouse modes and enabled background input. Manually supplied Plus/Minus flags cannot change their ordinary holds.

Historical performance comparison (before this review revision)

Baseline: main @ 316f9319. This PR @ 0f4b3374 is the old measured feature head, not the build above. These four runs remain useful history but do not measure the revised mouse route or current-main performance.

Hardware: AMD RX 7900 XT 20 GB, RADV Mesa 26.2.3; Ryzen 5 7600X, 32 GB RAM; NixOS 26.11, Linux 7.2.7. Private headless Sway 1.12/Xwayland; udmabuf enabled; RX_VK_FORCE_MESH_FALLBACK=1; speakers disabled.
Scene: Hyrule Castle Town (Market_Child), stationary at the copied save's wooden door. 1920×1080 drawable and output, 100% render resolution, game's own upscaler, frame generation Off, game frame-rate cap Uncapped, VSync Auto, HDR Off. Effective menu values were read through the control bridge. No overlays, injectors or external shader-cache tools.
Method: One short session, main → PR → main → PR, with fresh copies of the same save. Each process warms at the stationary scene for at least 10 s, then samples for 20 s; the last five vk: N presents, X fps log lines per run are retained. The per-run median and median of the two run medians are below. These are native-present measurements at the settings below, not a claim about other scenes or 1% lows.
How to reproduce: Load a disposable copy of a Castle Town save and stay by the same wooden door, without movement/camera input. Use the settings above, with ZL Toggle On; no held button during FPS samples on this PR; baseline main has stock behavior. Alternate the two binaries twice, let each settle, and take the last five presents-log readings per run. The additional feature proof below is separate from these stationary FPS samples.

Observation main @ 316f9319 this PR @ 0f4b3374
Run medians, native FPS (first / second) 54.10 / 53.90 53.10 / 54.20
Median of run medians, native FPS 54.00 53.65
Samples retained per run 5 / 5 5 / 5

Latch proof: on this PR with ZL Toggle On, a 120-present press covered frames 22514–22633. At frame 22775 the control bridge reported input.queued=0, and the inspected capture still shows the shield raised. The next 16-present press covered frames 22778–22793; the next capture shows the shield back on Link's back. On main, the same finite 120-present press released the shield normally.

press ZL, frames=120: {"frame":22634,"start":22514,"end":22634}
status after release: {"frame":22775,"input":{"queued":0,"exclusive":false},"map":"Market_Child"}
second press ZL, frames=16: {"frame":22794,"start":22778,"end":22794}

The old session's optional menu navigation did not finish; those attempts were not counted as reset or Off-setting passes. The fresh functional session above now verifies the Insert reset. Neither the old nor new session verifies physical controllers or Windows.

Exact presents-log samples (five per run)
Run 1: main
[hos Presentation Thread] vk: 25560 presents, 54.1 fps; a frame: 1586 draws (0 skipped), 106 dispatches, 165 passes, 159 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (1000 ms), 0 programs (10 failed), 1387 textures (3175 MB)
[hos Presentation Thread] vk: 25680 presents, 55.4 fps; a frame: 1595 draws (0 skipped), 106 dispatches, 164 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (1000 ms), 0 programs (10 failed), 1386 textures (3166 MB)
[hos Presentation Thread] vk: 25800 presents, 54.1 fps; a frame: 1589 draws (0 skipped), 106 dispatches, 164 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (1000 ms), 0 programs (10 failed), 1458 textures (3745 MB)
[hos Presentation Thread] vk: 25920 presents, 53.6 fps; a frame: 1589 draws (0 skipped), 106 dispatches, 164 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (1000 ms), 0 programs (10 failed), 1437 textures (3637 MB)
[hos Presentation Thread] vk: 26040 presents, 53.7 fps; a frame: 1590 draws (0 skipped), 106 dispatches, 164 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (1000 ms), 0 programs (10 failed), 1399 textures (3286 MB)
Run 2: this PR
[hos Presentation Thread] vk: 24720 presents, 54.4 fps; a frame: 1607 draws (0 skipped), 106 dispatches, 165 passes, 159 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (982 ms), 0 programs (10 failed), 1387 textures (3175 MB)
[hos Presentation Thread] vk: 24840 presents, 53.7 fps; a frame: 1593 draws (0 skipped), 106 dispatches, 164 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (982 ms), 0 programs (10 failed), 1386 textures (3166 MB)
[hos Presentation Thread] vk: 24960 presents, 53.1 fps; a frame: 1589 draws (0 skipped), 106 dispatches, 164 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (982 ms), 0 programs (10 failed), 1407 textures (3360 MB)
[hos Presentation Thread] vk: 25080 presents, 53.0 fps; a frame: 1585 draws (0 skipped), 107 dispatches, 165 passes, 159 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (982 ms), 0 programs (10 failed), 1401 textures (3304 MB)
[hos Presentation Thread] vk: 25200 presents, 52.8 fps; a frame: 1611 draws (0 skipped), 107 dispatches, 165 passes, 159 barriers, 6 submits, 0.1 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (982 ms), 0 programs (10 failed), 1403 textures (3211 MB)
Run 3: main
[hos Presentation Thread] vk: 26520 presents, 54.7 fps; a frame: 1599 draws (0 skipped), 106 dispatches, 165 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (968 ms), 0 programs (10 failed), 1393 textures (3178 MB)
[hos Presentation Thread] vk: 26640 presents, 53.9 fps; a frame: 1592 draws (0 skipped), 106 dispatches, 164 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (968 ms), 0 programs (10 failed), 1392 textures (3169 MB)
[hos Presentation Thread] vk: 26760 presents, 53.7 fps; a frame: 1596 draws (0 skipped), 107 dispatches, 164 passes, 159 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (968 ms), 0 programs (10 failed), 1393 textures (3178 MB)
[hos Presentation Thread] vk: 26880 presents, 54.5 fps; a frame: 1587 draws (0 skipped), 106 dispatches, 165 passes, 159 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (968 ms), 0 programs (10 failed), 1409 textures (3326 MB)
[hos Presentation Thread] vk: 27000 presents, 53.9 fps; a frame: 1604 draws (0 skipped), 106 dispatches, 164 passes, 158 barriers, 6 submits, 0.1 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (968 ms), 0 programs (10 failed), 1410 textures (3224 MB)
Run 4: this PR
[hos Presentation Thread] vk: 21960 presents, 54.4 fps; a frame: 1608 draws (0 skipped), 106 dispatches, 165 passes, 159 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (907 ms), 0 programs (10 failed), 1366 textures (3145 MB)
[hos Presentation Thread] vk: 22080 presents, 54.2 fps; a frame: 1606 draws (0 skipped), 106 dispatches, 164 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (907 ms), 0 programs (10 failed), 1364 textures (3127 MB)
[hos Presentation Thread] vk: 22200 presents, 54.1 fps; a frame: 1606 draws (0 skipped), 107 dispatches, 165 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (907 ms), 0 programs (10 failed), 1397 textures (3346 MB)
[hos Presentation Thread] vk: 22320 presents, 53.8 fps; a frame: 1607 draws (0 skipped), 107 dispatches, 165 passes, 158 barriers, 6 submits, 0.2 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (907 ms), 0 programs (10 failed), 1403 textures (3343 MB)
[hos Presentation Thread] vk: 22440 presents, 54.7 fps; a frame: 1609 draws (0 skipped), 107 dispatches, 164 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (907 ms), 0 programs (10 failed), 1386 textures (3186 MB)

Testing

Tested: Full Linux suite under the shared lock: 512 passed, 5 failed, 16 skipped in 497 seconds. Three failures were extracted-fixture compilation errors caused by the new latch type/pause constants; 7720460 fixes them without weakening assertions. The final focused run (including all three) is 14 passed, 0 failed. The two remaining failures also reproduce on untouched main f3fe1763: test_linux_tools.py (four assertions, its fixed /usr/bin:/bin PATH on Nix) and test_menu_i18n.py (stale Portuguese debug-help text). test_display_pointer.py and test_texture_staging_publication.py pass on main. The fresh Linux NUP build and shader preparation both passed; tracked source hashes matched the pinned archive before and after the build. The game-free fixtures compile the real input and menu paths. git diff --check passes. The project has no standalone formatter, linter or typechecker; compiler checks run through the fixtures and Linux build.

Only isolated folders, a private compositor and copied test saves were used. No installed game, live save, owner game session or korri* unit was changed. New screenshots below were opened and inspected; attachments load without login.

Not tested: Windows native builds/gameplay or non-ASCII Windows paths, physical controllers, other GPUs, HDR, a full playthrough and every feature combination. There is no Windows machine available. The old main/PR FPS comparison was not rerun on this head; the fresh run proves the review fix.

Needs other testers:

  • Windows, llvm-mingw, keyboard and physical controller: build main and this PR with a non-ASCII game path. Check ZL/ZR latch/release, Native mouse aim, both pad slots, Off, focus/Insert/rebind/default resets and same-scene Off/On FPS (open request T-NDNY).

Screenshots

this PR @ bc0c1292: ZR Toggle On, Plus/Minus disabled and revised help

this PR @ bc0c1292: ZR latched after release, before relative mouse motion

this PR @ bc0c1292: mouse moves the latched slingshot aim to the door; still 14 seeds

this PR @ bc0c1292: ZL remains held after release

this PR @ bc0c1292: the second ZL press releases the shield

this PR @ bc0c1292: opening Insert cleared a new shield latch

Risk and rollback

A latched action can surprise the player. There is no separate held-button indicator; scene changes, cutscenes and disconnects do not universally reset it. Linux title/game-over/save dialogs are not guaranteed resets. Open Insert to release, or set the button's Toggle Off. Long binding labels can still clip. Reverting the feature is sufficient.

Reviewer notes

Read host/nn_hid.c's mouse routing and Npad latch first. No public MkwVkHost/MkwVkRenderer structure changes, so this feature needs no API-version bump. Option 54 overlaps #196 and #152; coordination notes cover renumbering and Sprint seeing toggled A as held. A HUD indicator and a full Linux port of Windows-only menu tracking are deliberately outside this input fix. The help does not recommend an unverified built-in Targeting setting.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Merged current main (f3fe1763) into the branch (68b95c1); not rebased.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. The historical comparison above meets this; it has not been repeated on the review-fix head.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Flags noted, and the switch reference updated (docs/development.md, the README).
  • Screenshots for a change that shows on screen.
  • A row in docs/tested-configurations.md for each measured run.
  • A game-free test added for the fix, failing on main and passing here. The mouse/background regression is against the pre-fix feature head merged with main; main alone has no latch.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. Linux built; Windows remains unverified (T-NDNY).
  • Docs updated.
## Why Players who cannot keep a button held need another way to shield, target or aim. Hold-to-toggle makes one press hold a bound button and the next release it. Carried over from https://github.com/NotRexed/Hookshot/pull/2886. ## What changed - Controls: a Toggle column for Switch buttons, Off by default, saved as `toggle_<button>=0|1`. Plus, Minus, directions and note-only rows cannot toggle. - Input: one latch after remapping, shared by both gamepad bindings, keys and mouse buttons. Mouse Native uses latched ZR/L/R for right-stick aiming; L also retains pointing in either active mouse mode. - Resets: Insert, focus loss, rebinding, defaults and Plus/Minus clear latches. Enabled background pads can latch again after focus loss. Linux's existing pause-focus callback clears too; other Linux game dialogs are not guaranteed resets. - Documentation and tests cover these rules. Measured runs are now in `docs/tested-configurations.md`. ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | Controls → Toggle; `toggle_<button>=0/1` | Press once to hold, again to release a Switch button | Off (0) for every button | Keep Off by default; opt-in accessibility choice | Players who cannot sustain a hold | Plus/Minus, stick directions, ocarina-note-only rows | Windows gameplay, physical controllers, other GPUs | | Existing `HOS_INPUT_BACKGROUND=0/1` / Controller in background | Background latch reset follows the existing pad policy | Off (0); environment overrides the menu | Keep opt-in, consistent with normal pad input | Background-pad users | Keyboard/mouse input in the background | Physical controllers, Windows gameplay | ## Measurements main @ f3fe1763dda267a134494f1d21e6ad10b1ec4cd8 was merged in commit `68b95c1` (no rebase or force-push). this PR @ 3582a10e01ae8d0082ab53ebfd382cbb0359b2a9; the new Linux game build uses `bc0c1292d8ce377c1d6fbcc6dc44372b8f6f74ec`. Later commits change only tests and recorded evidence. ### Review-fix proof Baseline: pre-fix feature @ 68b95c1 (current main merged, before the review fix). Main alone has no latch, so these game-free regression checks compare the feature before and after the fix, using its real input/Npad path. | Game-free regression | pre-fix feature @ 68b95c1 | review fix @ bc0c1292 | |---|---|---| | L pointing remains active after physical release | Fails the pointing assertion | Passes | | Enabled background pad can latch ZL | Fails the Npad button assertion | Passes | Fresh build: `rev137-bc0c1292 (source f94c58fd)`, Git `bc0c1292`, on an RX 7900 XT / RADV Mesa 26.2.3, Ryzen 5 7600X, 32 GB, NixOS 26.11 / Linux 7.2.7. Private headless Sway 1.12 + Xwayland, 1920×1080, render 100%, game upscaler, FG/HDR Off, forced mesh fallback, udmabuf enabled, audio off, copied save. Hyrule Castle Town (`Market_Child`), indoors by the wooden door. One 10m06s functional session, clean control-socket exit, no crash/device-lost/hang marker. Another isolated CPU build ran concurrently, so this is **not an FPS comparison**. | Input / setting | Observed result on this PR @ bc0c1292 | |---|---| | Mouse camera Native; ZR Toggle On; 16-frame ZR press at 21466–21481 | Slingshot stays drawn after release. With no button pressed, 20 relative XTest mouse events of +20 horizontal counts move the aim from the wall to the door. No shot: 14 seeds before and after. Queue empty at frame 23052. | | ZL Toggle On; 16-frame press at 18803–18818 | Shield remains raised after release; queue empty at 18946. | | Second 16-frame ZL press at 18954–18969 | Shield returns to Link's back. | | New shield latch, then open/close Insert with F1 | Shield returns to Link's back. | | Controls page | ZL/ZR Toggle On, Plus/Minus disabled, revised help visible. | Reproduce: enable ZR/ZL Toggle and Mouse camera Native; click the game to capture the mouse, tap ZR and release, then move the mouse. Tap ZR again to release/fire and B to leave the aiming mode. In normal play, tap ZL twice to check shield hold/release. Open Insert to clear a new latch. The test sent relative XTest events to the **private** Xwayland display; xdotool pointer warps did not produce relative motion and are not counted as proof. ```text press ZR, frames=16: {"frame":21482,"start":21466,"end":21482} status after relative mouse motion: {"frame":23052,"input":{"queued":0,"exclusive":false},"map":"Market_Child"} press ZL, frames=16: {"frame":18819,"start":18803,"end":18819} status after release: {"frame":18946,"input":{"queued":0,"exclusive":false}} second press ZL, frames=16: {"frame":18970,"start":18954,"end":18970} option mouse_camera: {"value":1,"text":"Native"} ``` The real `read_input`/Npad regression fixture failed before the fix on latched L pointing and background pads. It now passes for L/R/ZL/ZR, all three mouse modes and enabled background input. Manually supplied Plus/Minus flags cannot change their ordinary holds. ### Historical performance comparison (before this review revision) Baseline: main @ 316f9319. This PR @ 0f4b3374 is the old measured feature head, not the build above. These four runs remain useful history but do not measure the revised mouse route or current-main performance. Hardware: AMD RX 7900 XT 20 GB, RADV Mesa 26.2.3; Ryzen 5 7600X, 32 GB RAM; NixOS 26.11, Linux 7.2.7. Private headless Sway 1.12/Xwayland; udmabuf enabled; `RX_VK_FORCE_MESH_FALLBACK=1`; speakers disabled. Scene: Hyrule Castle Town (`Market_Child`), stationary at the copied save's wooden door. 1920×1080 drawable and output, 100% render resolution, game's own upscaler, frame generation Off, game frame-rate cap Uncapped, VSync Auto, HDR Off. Effective menu values were read through the control bridge. No overlays, injectors or external shader-cache tools. Method: One short session, main → PR → main → PR, with fresh copies of the same save. Each process warms at the stationary scene for at least 10 s, then samples for 20 s; the last five `vk: N presents, X fps` log lines per run are retained. The per-run median and median of the two run medians are below. These are native-present measurements at the settings below, not a claim about other scenes or 1% lows. How to reproduce: Load a disposable copy of a Castle Town save and stay by the same wooden door, without movement/camera input. Use the settings above, with ZL Toggle On; no held button during FPS samples on this PR; baseline main has stock behavior. Alternate the two binaries twice, let each settle, and take the last five presents-log readings per run. The additional feature proof below is separate from these stationary FPS samples. | Observation | main @ 316f9319 | this PR @ 0f4b3374 | |---|---|---| | Run medians, native FPS (first / second) | 54.10 / 53.90 | 53.10 / 54.20 | | Median of run medians, native FPS | 54.00 | 53.65 | | Samples retained per run | 5 / 5 | 5 / 5 | **Latch proof:** on this PR with ZL Toggle On, a 120-present press covered frames 22514–22633. At frame 22775 the control bridge reported `input.queued=0`, and the inspected capture still shows the shield raised. The next 16-present press covered frames 22778–22793; the next capture shows the shield back on Link's back. On main, the same finite 120-present press released the shield normally. ```text press ZL, frames=120: {"frame":22634,"start":22514,"end":22634} status after release: {"frame":22775,"input":{"queued":0,"exclusive":false},"map":"Market_Child"} second press ZL, frames=16: {"frame":22794,"start":22778,"end":22794} ``` The old session's optional menu navigation did not finish; those attempts were not counted as reset or Off-setting passes. The fresh functional session above now verifies the Insert reset. Neither the old nor new session verifies physical controllers or Windows. <details> <summary>Exact presents-log samples (five per run)</summary> ```text Run 1: main [hos Presentation Thread] vk: 25560 presents, 54.1 fps; a frame: 1586 draws (0 skipped), 106 dispatches, 165 passes, 159 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (1000 ms), 0 programs (10 failed), 1387 textures (3175 MB) [hos Presentation Thread] vk: 25680 presents, 55.4 fps; a frame: 1595 draws (0 skipped), 106 dispatches, 164 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (1000 ms), 0 programs (10 failed), 1386 textures (3166 MB) [hos Presentation Thread] vk: 25800 presents, 54.1 fps; a frame: 1589 draws (0 skipped), 106 dispatches, 164 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (1000 ms), 0 programs (10 failed), 1458 textures (3745 MB) [hos Presentation Thread] vk: 25920 presents, 53.6 fps; a frame: 1589 draws (0 skipped), 106 dispatches, 164 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (1000 ms), 0 programs (10 failed), 1437 textures (3637 MB) [hos Presentation Thread] vk: 26040 presents, 53.7 fps; a frame: 1590 draws (0 skipped), 106 dispatches, 164 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (1000 ms), 0 programs (10 failed), 1399 textures (3286 MB) Run 2: this PR [hos Presentation Thread] vk: 24720 presents, 54.4 fps; a frame: 1607 draws (0 skipped), 106 dispatches, 165 passes, 159 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (982 ms), 0 programs (10 failed), 1387 textures (3175 MB) [hos Presentation Thread] vk: 24840 presents, 53.7 fps; a frame: 1593 draws (0 skipped), 106 dispatches, 164 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (982 ms), 0 programs (10 failed), 1386 textures (3166 MB) [hos Presentation Thread] vk: 24960 presents, 53.1 fps; a frame: 1589 draws (0 skipped), 106 dispatches, 164 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (982 ms), 0 programs (10 failed), 1407 textures (3360 MB) [hos Presentation Thread] vk: 25080 presents, 53.0 fps; a frame: 1585 draws (0 skipped), 107 dispatches, 165 passes, 159 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (982 ms), 0 programs (10 failed), 1401 textures (3304 MB) [hos Presentation Thread] vk: 25200 presents, 52.8 fps; a frame: 1611 draws (0 skipped), 107 dispatches, 165 passes, 159 barriers, 6 submits, 0.1 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (982 ms), 0 programs (10 failed), 1403 textures (3211 MB) Run 3: main [hos Presentation Thread] vk: 26520 presents, 54.7 fps; a frame: 1599 draws (0 skipped), 106 dispatches, 165 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (968 ms), 0 programs (10 failed), 1393 textures (3178 MB) [hos Presentation Thread] vk: 26640 presents, 53.9 fps; a frame: 1592 draws (0 skipped), 106 dispatches, 164 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (968 ms), 0 programs (10 failed), 1392 textures (3169 MB) [hos Presentation Thread] vk: 26760 presents, 53.7 fps; a frame: 1596 draws (0 skipped), 107 dispatches, 164 passes, 159 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (968 ms), 0 programs (10 failed), 1393 textures (3178 MB) [hos Presentation Thread] vk: 26880 presents, 54.5 fps; a frame: 1587 draws (0 skipped), 106 dispatches, 165 passes, 159 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (968 ms), 0 programs (10 failed), 1409 textures (3326 MB) [hos Presentation Thread] vk: 27000 presents, 53.9 fps; a frame: 1604 draws (0 skipped), 106 dispatches, 164 passes, 158 barriers, 6 submits, 0.1 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (968 ms), 0 programs (10 failed), 1410 textures (3224 MB) Run 4: this PR [hos Presentation Thread] vk: 21960 presents, 54.4 fps; a frame: 1608 draws (0 skipped), 106 dispatches, 165 passes, 159 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (907 ms), 0 programs (10 failed), 1366 textures (3145 MB) [hos Presentation Thread] vk: 22080 presents, 54.2 fps; a frame: 1606 draws (0 skipped), 106 dispatches, 164 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (907 ms), 0 programs (10 failed), 1364 textures (3127 MB) [hos Presentation Thread] vk: 22200 presents, 54.1 fps; a frame: 1606 draws (0 skipped), 107 dispatches, 165 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (907 ms), 0 programs (10 failed), 1397 textures (3346 MB) [hos Presentation Thread] vk: 22320 presents, 53.8 fps; a frame: 1607 draws (0 skipped), 107 dispatches, 165 passes, 158 barriers, 6 submits, 0.2 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (907 ms), 0 programs (10 failed), 1403 textures (3343 MB) [hos Presentation Thread] vk: 22440 presents, 54.7 fps; a frame: 1609 draws (0 skipped), 107 dispatches, 164 passes, 158 barriers, 6 submits, 0.0 uploads (0.0 MB), 82 ub updates (57 overlays), GPU-bound wait 0.0 ms; 25369 pipelines (907 ms), 0 programs (10 failed), 1386 textures (3186 MB) ``` </details> ## Testing **Tested:** Full Linux suite under the shared lock: **512 passed, 5 failed, 16 skipped** in 497 seconds. Three failures were extracted-fixture compilation errors caused by the new latch type/pause constants; `7720460` fixes them without weakening assertions. The final focused run (including all three) is **14 passed, 0 failed**. The two remaining failures also reproduce on untouched main `f3fe1763`: `test_linux_tools.py` (four assertions, its fixed `/usr/bin:/bin` PATH on Nix) and `test_menu_i18n.py` (stale Portuguese debug-help text). `test_display_pointer.py` and `test_texture_staging_publication.py` pass on main. The fresh Linux NUP build and shader preparation both passed; tracked source hashes matched the pinned archive before and after the build. The game-free fixtures compile the real input and menu paths. `git diff --check` passes. The project has no standalone formatter, linter or typechecker; compiler checks run through the fixtures and Linux build. Only isolated folders, a private compositor and copied test saves were used. No installed game, live save, owner game session or `korri*` unit was changed. New screenshots below were opened and inspected; attachments load without login. **Not tested:** Windows native builds/gameplay or non-ASCII Windows paths, physical controllers, other GPUs, HDR, a full playthrough and every feature combination. There is no Windows machine available. The old main/PR FPS comparison was not rerun on this head; the fresh run proves the review fix. **Needs other testers:** - Windows, llvm-mingw, keyboard and physical controller: build main and this PR with a non-ASCII game path. Check ZL/ZR latch/release, Native mouse aim, both pad slots, Off, focus/Insert/rebind/default resets and same-scene Off/On FPS (open request T-NDNY). ## Screenshots ![this PR @ bc0c1292: ZR Toggle On, Plus/Minus disabled and revised help](https://git.magicbean.dev/attachments/07582ffc-cbd1-4e09-ae7a-14cb370bf0f0) ![this PR @ bc0c1292: ZR latched after release, before relative mouse motion](https://git.magicbean.dev/attachments/1fa81e01-e956-4b1c-bc83-1aa7739223ca) ![this PR @ bc0c1292: mouse moves the latched slingshot aim to the door; still 14 seeds](https://git.magicbean.dev/attachments/5e68e49b-342d-4387-93f0-2212f5b4e692) ![this PR @ bc0c1292: ZL remains held after release](https://git.magicbean.dev/attachments/e293f00c-fab8-493a-89b7-7bb6a0c42334) ![this PR @ bc0c1292: the second ZL press releases the shield](https://git.magicbean.dev/attachments/ba5baa04-382c-4c37-b058-b0d77c885ac0) ![this PR @ bc0c1292: opening Insert cleared a new shield latch](https://git.magicbean.dev/attachments/f6cb3ea9-de3f-4ef8-874c-b936ce2a9d95) ## Risk and rollback A latched action can surprise the player. There is no separate held-button indicator; scene changes, cutscenes and disconnects do not universally reset it. Linux title/game-over/save dialogs are not guaranteed resets. Open Insert to release, or set the button's Toggle Off. Long binding labels can still clip. Reverting the feature is sufficient. ## Reviewer notes Read `host/nn_hid.c`'s mouse routing and Npad latch first. No public `MkwVkHost`/`MkwVkRenderer` structure changes, so this feature needs no API-version bump. Option 54 overlaps #196 and #152; coordination notes cover renumbering and Sprint seeing toggled A as held. A HUD indicator and a full Linux port of Windows-only menu tracking are deliberately outside this input fix. The help does not recommend an unverified built-in Targeting setting. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Merged current `main` (`f3fe1763`) into the branch (`68b95c1`); not rebased. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [ ] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. The historical comparison above meets this; it has not been repeated on the review-fix head. - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Flags noted, and the switch reference updated (docs/development.md, the README). - [x] Screenshots for a change that shows on screen. - [x] A row in `docs/tested-configurations.md` for each measured run. - [x] A game-free test added for the fix, failing on `main` and passing here. The mouse/background regression is against the pre-fix feature head merged with main; main alone has no latch. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [ ] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. Linux built; Windows remains unverified (T-NDNY). - [x] Docs updated.
feat(input): hold-to-toggle for any bound button
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 3s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
0f4b337461
Carried over from https://github.com/NotRexed/Hookshot/pull/2886.
Cherry-picked source: 157985b00e5481ec8751298c571591920d5f2894.
Resolve rename/context conflicts without changing feature behaviour.
Keep measured configurations in the PR description, not the shared table.

Assisted-by: OpenAI GPT-6 Astra
Co-Authored-By: OpenAI GPT-6 Astra <[email protected]>
HumanBean left a comment

Code review only (diff and surrounding code read; no build, tests or game run). Commenting rather than approving because the PR is WIP.

The change holds together. The latch in host/hold_toggle.h is small and correct: a press edge flips it, a repeat doesn't, and a reset blocks every enabled button until it is released. It sits in the one place every input path meets (npad_states, after remapping, for No.1 only). It costs nothing while every flag is Off. The flags round-trip through save() and menu_init() without duplicate lines, and Reset to defaults and rebinding both clear the latch. I found no blocking bug.

On the pr status hint: vk_api.h only gains an option ID, carried by the existing option callback. MkwVkHost and MkwVkRenderer don't change, so MKWVK_API_VERSION doesn't need a bump and that box shouldn't be added. ID 54 is still free on current main.

Suggestions, none of them blocking:

  • README: document the new column. The Controls paragraph and the "Writing the controls in longshot_options.ini yourself" block describe pad_/key_ but not the Toggle column or toggle_<button>=0|1.
  • Plus and Minus: I'd leave out the Toggle cell for these two (see the inline comment).
  • Inline notes: two small points on platform consistency and background input.

What keeps it WIP is in the checklist already: a run of this Longshot build in the game, interleaved main/PR measurements, and the Windows llvm-mingw build and test request.

Code review only (diff and surrounding code read; no build, tests or game run). Commenting rather than approving because the PR is WIP. The change holds together. The latch in `host/hold_toggle.h` is small and correct: a press edge flips it, a repeat doesn't, and a reset blocks every enabled button until it is released. It sits in the one place every input path meets (`npad_states`, after remapping, for No.1 only). It costs nothing while every flag is Off. The flags round-trip through `save()` and `menu_init()` without duplicate lines, and Reset to defaults and rebinding both clear the latch. I found no blocking bug. On the `pr status` hint: `vk_api.h` only gains an option ID, carried by the existing `option` callback. `MkwVkHost` and `MkwVkRenderer` don't change, so `MKWVK_API_VERSION` doesn't need a bump and that box shouldn't be added. ID 54 is still free on current main. Suggestions, none of them blocking: - **README:** document the new column. The Controls paragraph and the "Writing the controls in longshot_options.ini yourself" block describe `pad_`/`key_` but not the Toggle column or `toggle_<button>=0|1`. - **Plus and Minus:** I'd leave out the Toggle cell for these two (see the inline comment). - **Inline notes:** two small points on platform consistency and background input. What keeps it WIP is in the checklist already: a run of this Longshot build in the game, interleaved main/PR measurements, and the Windows llvm-mingw build and test request.
@ -3304,2 +3307,2 @@
const float a = col_x[col] + (col == kCtlPrimary ? 0 : 3 * s), b = col_x[col + 1] - (keyboard ? 0 : 3 * s);
const std::string v = keyboard ? keys_text(ctl.button) : disabled ? "-" : pad_name(host->binding(HOS_BIND_PAD, ctl.button, col));
const bool keyboard = col == kCtlKeyboard, toggle = col == kCtlToggle;
const bool disabled = (keys && !keyboard) || (toggle && ctl.button >= HOS_SW_N);
Owner

Suggestion: also disable Toggle for HOS_SW_PLUS and HOS_SW_MINUS. npad_states treats their press edge as a reset, so turning their Toggle On never latches anything. Instead it turns a held Plus or Minus into a one-frame tap. The cell would show On without doing what the help text promises. menu_tick's columns (line 4931) would need the same condition.

Suggestion: also disable Toggle for `HOS_SW_PLUS` and `HOS_SW_MINUS`. `npad_states` treats their press edge as a reset, so turning their Toggle On never latches anything. Instead it turns a held Plus or Minus into a one-frame tap. The cell would show On without doing what the help text promises. `menu_tick`'s `columns` (line 4931) would need the same condition.
@ -11,6 +11,7 @@
while one of the game's own menus is open (host/oot_menu.c: hos_mouse_game_menu each frame it runs), its clicks and
wheel for that menu (hos_mouse_game_pointer). */
#include "hos.h"
void hos_input_clear_toggles(void);
Owner

Nit: the same prototype is declared again in host/posix/display_sdl.c. Both files include hos.h, and hos.h would keep the one definition in nn_hid.c and its callers in step.

Nit: the same prototype is declared again in `host/posix/display_sdl.c`. Both files include `hos.h`, and `hos.h` would keep the one definition in `nn_hid.c` and its callers in step.
@ -189,6 +190,7 @@ static void gm_cursor_off(HWND w) {
back, if the window still has the focus */
static void menus_mouse(HWND w, int insert_open, int game_open) {
const int was = menu_open || game_menu_open, now = insert_open || game_open, game_was = game_menu_open;
if ((insert_open && !menu_open) || (game_open && !game_was)) hos_input_clear_toggles();
Owner

Windows also clears latches when one of the game's own menus opens (WM_APP + 10, from host/oot_menu.c). display_sdl.c has no equivalent, so Linux only clears on Plus or Minus in nn_hid.c. That's probably harmless if every game menu opens through Plus or Minus, but then this extra clear isn't needed. If some menus open another way, Linux misses them. Either way, the description's claim that the two paths are kept in step isn't quite true.

Windows also clears latches when one of the game's own menus opens (`WM_APP + 10`, from `host/oot_menu.c`). `display_sdl.c` has no equivalent, so Linux only clears on Plus or Minus in `nn_hid.c`. That's probably harmless if every game menu opens through Plus or Minus, but then this extra clear isn't needed. If some menus open another way, Linux misses them. Either way, the description's claim that the two paths are kept in step isn't quite true.
host/nn_hid.c Outdated
@ -1088,1 +1093,4 @@
const int menu_open = rx_option(1, 0);
static HoldToggle toggles;
const uint64_t enabled = (uint32_t)rx_option(HOS_OPT_HOLD_TOGGLE, 0);
const int reset = InterlockedExchange(&toggle_reset, 0) || menu_open || (enabled && !game_in_front());
Owner

Question: enabled && !game_in_front() resets the latch on every poll while the window is in the background. read_input still passes pads through in that state when the player turned on pads in the background (option 4, HOS_INPUT_BACKGROUND) and for a scripted run. With Toggle On, those players get ordinary hold input, but only for the toggled buttons. If that's intended, a word in the help text would cover it. If not, the condition could follow pads_in_background. Focus loss is already handled by the display's hos_input_clear_toggles() on WM_KILLFOCUS and HOS_SDL_EV_FOCUS_LOST.

Question: `enabled && !game_in_front()` resets the latch on every poll while the window is in the background. `read_input` still passes pads through in that state when the player turned on pads in the background (option 4, `HOS_INPUT_BACKGROUND`) and for a scripted run. With Toggle On, those players get ordinary hold input, but only for the toggled buttons. If that's intended, a word in the help text would cover it. If not, the condition could follow `pads_in_background`. Focus loss is already handled by the display's `hos_input_clear_toggles()` on `WM_KILLFOCUS` and `HOS_SDL_EV_FOCUS_LOST`.
Author
Contributor

Added a fresh interleaved main/PR Linux measurement session at the named commits, the last-five-sample FPS medians, feature proof and inspected screenshots. Windows testing requests are open; the description names the remaining gaps. The branch also rebases cleanly onto main 1be180bb with an identical stable patch ID and passing focused tests; the force-with-lease push is still gated, so the measured remote head stays pinned. No new baseline FPS is claimed for the local rebase. The extra synthetic menu-navigation attempt is explicitly excluded from the successful latch proof.

Added a fresh interleaved main/PR Linux measurement session at the named commits, the last-five-sample FPS medians, feature proof and inspected screenshots. Windows testing requests are open; the description names the remaining gaps. The branch also rebases cleanly onto main 1be180bb with an identical stable patch ID and passing focused tests; the force-with-lease push is still gated, so the measured remote head stays pinned. No new baseline FPS is claimed for the local rebase. The extra synthetic menu-navigation attempt is explicitly excluded from the successful latch proof.
simonwjackson changed title from WIP: feat(input): hold-to-toggle for any bound button to feat(input): hold-to-toggle for any bound button 2026-11-13 07:49:52 +00:00
HumanBean added this to the v3.0.0 milestone 2026-11-13 17:55:39 +00:00
HumanBean left a comment

Code review only: I read the diff and the code around it. I didn't build it, run the tests or play the game.

The latch itself is right. hold_toggle_apply flips on a press edge and ignores a repeat. After a reset it blocks every enabled button until that button is let go, and turning a button's Toggle Off drops its latch at once (latched &= enabled). The latch sits after remapping in npad_states, so both gamepad slots, the keyboard, mouse buttons and the control bridge share one latch. The default is Off for every button, and toggle_ lines survive a save without duplicates. These all clear the latch: the Insert menu, focus loss (both platforms), rebinding, Reset to defaults, and Plus or Minus. Stick directions and ocarina note rows get no Toggle cell. Steam Input and the left stick's dead zone are untouched, because the latch only sees buttons after remapping. The earlier review is still open in full: the head is unchanged since then (0f4b3374).

Must change

  • Toggled ZR, R or L and the mouse. read_input decides the mouse's part from the buttons before the latch. It calls hos_mouse_native_free(!(*buttons & game_stick)) for ZR, L and R, and passes mouse_stick's pointing flag from L. So with Mouse camera Native and ZR or R toggled, the mouse turns the camera's angles while the game is aiming or shielding, instead of acting as the right stick that aims. With L toggled, the item wheel never gets the pointing mouse, in either mouse mode. Aim and shield are the uses this feature is for. Inline comment on host/nn_hid.c.
  • "Rebased on current main" is ticked but false. The head forks at 316f9319. main@origin is now fbd42024 (#198), 55 commits on. The description says the rebase onto 1be180bb was never pushed, and 1be180bb is itself about 38 commits behind main. The text merges without conflicts against fbd42024; I checked the 10 files both sides touch with a three-way merge. Main has changed read_input's mouse modes, though (Mouse camera Off, #178), and the same fixtures. Rebase, run tests/run_all.py on the result, and then tick the box.
  • Two If-applicable boxes were rewritten instead of done.
    • The PR adds a menu column and the toggle_<button>=0|1 ini key, but nothing is documented: the README's Controls section and its options-file block (README.md lines 684 to 696), and docs/development.md "The options file" (line 558). The "Flags noted … switch reference updated" and "Docs updated" boxes were deleted, but they apply. The earlier review also asked for the README.
    • "A row in docs/tested-configurations.md" became "a folded table below", and the file is unchanged. AGENTS.md asks for the rows in that file.

Should change

  • Plus and Minus Toggle cells (earlier review, still open). Turning them On never latches. It turns a held Plus or Minus into a one-frame tap, and the cell still says On. Disable them the way the stick rows are disabled.
  • Background input (earlier review, which got one detail wrong). With pads counting in the background (option 4, HOS_INPUT_BACKGROUND), enabled && !game_in_front() resets every poll. On that path hold_toggle_apply returns down & ~enabled, so a toggled button does nothing at all behind another window, not even an ordinary hold. Follow pads_in_background there, or leave the button unlatched instead of dropping it.
  • Linux doesn't clear on the game's own menus. Windows clears when one opens (WM_APP + 10), but host/oot_menu.c is Windows-only. On Linux, a menu that doesn't open through Plus or Minus (the game-over and save dialogs, the title) keeps the latch. The help text says the menu releases it.
  • Nothing on screen shows a held button. Shield, target and aim show on Link, but a latched A, B, D-pad or stick press doesn't. Nothing clears the latch on a scene load, a cutscene, death or a controller disconnect either. Keeping it through those is defensible, since the player asked for the hold, but only if the player can see it. Consider a small indicator while any latch is set, or clearing on a pad disconnect.
  • Option ID 54 is taken three times. #196 (HOS_OPT_SPRINT) and #152 (HOS_OPT_KEYBOARD) also use 54, and both bump MKWVK_API_VERSION to 22. Whichever merges second must renumber. A note in those PRs would avoid a silent clash.
    • Sprint runs sprint_step after this latch, so a toggled A looks to Sprint like A held by the player. Worth a look when the two meet.
    • #245 (cursor) doesn't overlap.
  • Help text. If the game's own options already have a hold-or-switch Targeting setting, the Toggle help could point ZL players to it. I couldn't confirm the setting from the repository.

Not checked: any run in the game, the Windows build, and whether the Controls page's 128 hit boxes are enough when every row fits on screen. On main the 96 boxes were already short of a full grid, so that limit predates this PR.

Code review only: I read the diff and the code around it. I didn't build it, run the tests or play the game. The latch itself is right. `hold_toggle_apply` flips on a press edge and ignores a repeat. After a reset it blocks every enabled button until that button is let go, and turning a button's Toggle Off drops its latch at once (`latched &= enabled`). The latch sits after remapping in `npad_states`, so both gamepad slots, the keyboard, mouse buttons and the control bridge share one latch. The default is Off for every button, and `toggle_` lines survive a save without duplicates. These all clear the latch: the Insert menu, focus loss (both platforms), rebinding, Reset to defaults, and Plus or Minus. Stick directions and ocarina note rows get no Toggle cell. Steam Input and the left stick's dead zone are untouched, because the latch only sees buttons after remapping. The earlier review is still open in full: the head is unchanged since then (0f4b3374). ## Must change - **Toggled ZR, R or L and the mouse.** `read_input` decides the mouse's part from the buttons *before* the latch. It calls `hos_mouse_native_free(!(*buttons & game_stick))` for ZR, L and R, and passes `mouse_stick`'s pointing flag from L. So with Mouse camera Native and ZR or R toggled, the mouse turns the camera's angles while the game is aiming or shielding, instead of acting as the right stick that aims. With L toggled, the item wheel never gets the pointing mouse, in either mouse mode. Aim and shield are the uses this feature is for. Inline comment on `host/nn_hid.c`. - **"Rebased on current main" is ticked but false.** The head forks at 316f9319. main@origin is now fbd42024 (https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/198), 55 commits on. The description says the rebase onto 1be180bb was never pushed, and 1be180bb is itself about 38 commits behind main. The text merges without conflicts against fbd42024; I checked the 10 files both sides touch with a three-way merge. Main has changed `read_input`'s mouse modes, though (Mouse camera Off, https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/178), and the same fixtures. Rebase, run `tests/run_all.py` on the result, and then tick the box. - **Two If-applicable boxes were rewritten instead of done.** - The PR adds a menu column and the `toggle_<button>=0|1` ini key, but nothing is documented: the README's Controls section and its options-file block (README.md lines 684 to 696), and docs/development.md "The options file" (line 558). The "Flags noted … switch reference updated" and "Docs updated" boxes were deleted, but they apply. The earlier review also asked for the README. - "A row in `docs/tested-configurations.md`" became "a folded table below", and the file is unchanged. AGENTS.md asks for the rows in that file. ## Should change - **Plus and Minus Toggle cells** (earlier review, still open). Turning them On never latches. It turns a held Plus or Minus into a one-frame tap, and the cell still says On. Disable them the way the stick rows are disabled. - **Background input** (earlier review, which got one detail wrong). With pads counting in the background (option 4, `HOS_INPUT_BACKGROUND`), `enabled && !game_in_front()` resets every poll. On that path `hold_toggle_apply` returns `down & ~enabled`, so a toggled button does nothing at all behind another window, not even an ordinary hold. Follow `pads_in_background` there, or leave the button unlatched instead of dropping it. - **Linux doesn't clear on the game's own menus.** Windows clears when one opens (`WM_APP + 10`), but `host/oot_menu.c` is Windows-only. On Linux, a menu that doesn't open through Plus or Minus (the game-over and save dialogs, the title) keeps the latch. The help text says the menu releases it. - **Nothing on screen shows a held button.** Shield, target and aim show on Link, but a latched A, B, D-pad or stick press doesn't. Nothing clears the latch on a scene load, a cutscene, death or a controller disconnect either. Keeping it through those is defensible, since the player asked for the hold, but only if the player can see it. Consider a small indicator while any latch is set, or clearing on a pad disconnect. - **Option ID 54 is taken three times.** https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/196 (`HOS_OPT_SPRINT`) and https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/152 (`HOS_OPT_KEYBOARD`) also use 54, and both bump `MKWVK_API_VERSION` to 22. Whichever merges second must renumber. A note in those PRs would avoid a silent clash. - Sprint runs `sprint_step` after this latch, so a toggled A looks to Sprint like A held by the player. Worth a look when the two meet. - https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/245 (cursor) doesn't overlap. - **Help text.** If the game's own options already have a hold-or-switch Targeting setting, the Toggle help could point ZL players to it. I couldn't confirm the setting from the repository. Not checked: any run in the game, the Windows build, and whether the Controls page's 128 hit boxes are enough when every row fits on screen. On main the 96 boxes were already short of a full grid, so that limit predates this PR.
@ -55,2 +55,4 @@
90, 120, 144, 165, 240, 6 Uncapped; a change applies in play) */
enum { HOS_OPT_FPS_CAP = 53 };
/* Hold-to-toggle's enabled Switch buttons, 1 << HOS_SW_*. Off (zero) by default; the existing option callback. */
enum { HOS_OPT_HOLD_TOGGLE = 54 };
Owner

#196 (HOS_OPT_SPRINT) and #152 (HOS_OPT_KEYBOARD) also take 54. Whichever merges second must renumber; worth a word on those PRs.

https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/196 (`HOS_OPT_SPRINT`) and https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/152 (`HOS_OPT_KEYBOARD`) also take 54. Whichever merges second must renumber; worth a word on those PRs.
@ -3304,2 +3307,2 @@
const float a = col_x[col] + (col == kCtlPrimary ? 0 : 3 * s), b = col_x[col + 1] - (keyboard ? 0 : 3 * s);
const std::string v = keyboard ? keys_text(ctl.button) : disabled ? "-" : pad_name(host->binding(HOS_BIND_PAD, ctl.button, col));
const bool keyboard = col == kCtlKeyboard, toggle = col == kCtlToggle;
const bool disabled = (keys && !keyboard) || (toggle && ctl.button >= HOS_SW_N);
Owner

Still open from the earlier review: also disable the cell for HOS_SW_PLUS and HOS_SW_MINUS (and the columns in menu_tick). Their press edge is a reset in npad_states, so On only turns a held Plus or Minus into a one-frame tap.

Still open from the earlier review: also disable the cell for `HOS_SW_PLUS` and `HOS_SW_MINUS` (and the `columns` in `menu_tick`). Their press edge is a reset in `npad_states`, so On only turns a held Plus or Minus into a one-frame tap.
host/nn_hid.c Outdated
@ -1088,1 +1093,4 @@
const int menu_open = rx_option(1, 0);
static HoldToggle toggles;
const uint64_t enabled = (uint32_t)rx_option(HOS_OPT_HOLD_TOGGLE, 0);
const int reset = InterlockedExchange(&toggle_reset, 0) || menu_open || (enabled && !game_in_front());
Owner

On the !game_in_front() reset, hold_toggle_apply returns down & ~enabled. With pads counting in the background (option 4 or HOS_INPUT_BACKGROUND), a toggled button is then dropped completely, not passed through as an ordinary hold. Follow pads_in_background here, or pass the enabled buttons through unlatched while the reset is from the background.

On the `!game_in_front()` reset, `hold_toggle_apply` returns `down & ~enabled`. With pads counting in the background (option 4 or `HOS_INPUT_BACKGROUND`), a toggled button is then dropped completely, not passed through as an ordinary hold. Follow `pads_in_background` here, or pass the enabled buttons through unlatched while the reset is from the background.
@ -1089,0 +1096,4 @@
const int reset = InterlockedExchange(&toggle_reset, 0) || menu_open || (enabled && !game_in_front());
/* Pause/map must release shield/aim before the game's menu reads input. */
const uint64_t pause = buttons & ~toggles.previous & ((1ULL << B_PLUS) | (1ULL << B_MINUS));
buttons = hold_toggle_apply(&toggles, buttons, enabled, reset || pause);
Owner

read_input has already decided the mouse's part from the unlatched buttons: hos_mouse_native_free(!(*buttons & game_stick)) for ZR, L and R, and mouse_stick(..., *buttons >> B_L & 1). With ZR or R toggled and Mouse camera Native, the mouse turns the camera's angles while the game aims or shields, instead of acting as the right stick that aims. With L toggled, the item wheel loses the pointing mouse. Make toggles file-static and use (*buttons | toggles.latched) & game_stick there, and the same for L. Last poll's latch is close enough.

`read_input` has already decided the mouse's part from the unlatched buttons: `hos_mouse_native_free(!(*buttons & game_stick))` for ZR, L and R, and `mouse_stick(..., *buttons >> B_L & 1)`. With ZR or R toggled and Mouse camera Native, the mouse turns the camera's angles while the game aims or shields, instead of acting as the right stick that aims. With L toggled, the item wheel loses the pointing mouse. Make `toggles` file-static and use `(*buttons | toggles.latched) & game_stick` there, and the same for L. Last poll's latch is close enough.
Assisted-by: OpenAI GPT-6 Astra
Co-Authored-By: OpenAI GPT-6 Astra <[email protected]>

# Conflicts:
#	gpu/vk/vk_api.h
Disable pause/map toggles, document the Controls flags, and clear Linux
pause latches through the existing focus hook. Other Linux dialogs remain
outside the Windows-only menu tracking; describe that limit explicitly.

Assisted-by: OpenAI GPT-6 Astra
Co-Authored-By: OpenAI GPT-6 Astra <[email protected]>
Assisted-by: OpenAI GPT-6 Astra
Co-Authored-By: OpenAI GPT-6 Astra <[email protected]>
Retain their original commits and measurements; these predate the review
fixes and are not evidence for the new mouse route.

Assisted-by: OpenAI GPT-6 Astra
Co-Authored-By: OpenAI GPT-6 Astra <[email protected]>
The full suite found three fixture compilation failures after moving the
latch to file scope and excluding pause flags from ini parsing. Include
the real latch header and name the missing Switch constants.

Assisted-by: OpenAI GPT-6 Astra
Co-Authored-By: OpenAI GPT-6 Astra <[email protected]>
docs(input): record native mouse aim and latch acceptance
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 4s
3582a10e01
Prove ZR aiming with real relative mouse events after release, ZL hold and
second-press release, Insert reset, and disabled pause/map Toggle cells.

Assisted-by: OpenAI GPT-6 Astra
Co-Authored-By: OpenAI GPT-6 Astra <[email protected]>
Author
Contributor

Thanks for review 1150. I merged main f3fe1763dda267a134494f1d21e6ad10b1ec4cd8 with merge commit 68b95c1, rather than rewriting the branch. The description now states that explicitly.

  • Mouse: bc0c129 makes the latch file-static. Native's ZR/L/R decision and L's pointing flag both use *buttons | toggles.latched. The regression fixture exercises the real read_input and Npad writer for L/R/ZL/ZR in all three mouse modes. L pointing and background-pad tests failed before the fix and pass after it.
  • Documentation: README Controls, its ini block and development.md's options-file reference now document Toggle, toggle_<button>=0|1, defaults, exclusions and reset limits. The original checklist boxes are restored, including the unticked Windows build box and its open test request T-NDNY.
  • Measurements: 547f5f7 adds the four previously recorded runs to docs/tested-configurations.md, below the day's existing rows, with their original commits. New proof is recorded separately, not presented as a repeat of the old FPS comparison.
  • Plus/Minus: their Toggle cells are disabled, keyboard navigation skips that column, the ini reader ignores those flags and Npad masks them out. A manually supplied flag cannot turn either hold into a tap. Tests cover persistence, navigation and held Plus serialization.
  • Background: continuous reset now follows pads_in_background, including the environment override. Losing focus still clears once; a new background pad press can latch when background pads are enabled.
  • Linux menus: the existing pause-focus hook now clears latches (bc0c129, test in 72e2b34). Title/game-over/save-dialog tracking remains Windows-only. Porting that whole mouse/menu tracker is not a cheap, safe input fix. Help and docs now name Insert and Plus/Minus rather than promising every Linux game dialog clears a latch.
  • Indicator: not added. This keeps the change to input and the existing Controls column rather than adding HUD state and a new option. The cost is that actions without a visible hold have no separate indication; the docs state this. Insert remains the explicit release path. Scene changes, cutscenes and disconnects still do not universally reset latches.
  • Option 54: coordination notes for #196 and #152 are drafted for posting by the parent, including the Sprint-after-latch interaction and their API-version 22 overlap. This PR changes neither public interface structure and needs no API-version bump.
  • Targeting help: I have not verified the game's own hold/switch setting, so I did not recommend an unconfirmed setting.

Full Linux suite under the shared lock: 512 passed, 5 failed, 16 skipped in 497 seconds. Three failures were extracted-fixture compilation errors caused by the new latch type/pause constants; 7720460 fixes them without weakening assertions. The final focused run (including all three) is 14 passed, 0 failed. The two remaining failures also reproduce on untouched main f3fe1763: test_linux_tools.py (four assertions, its fixed /usr/bin:/bin PATH on Nix) and test_menu_i18n.py (stale Portuguese debug-help text). test_display_pointer.py and test_texture_staging_publication.py pass on main. The fresh Linux NUP build and shader preparation both passed; tracked source hashes matched the pinned archive before and after the build.

Fresh game proof (bc0c1292, same production code as head 3582a10): Native mouse + latched ZR moved the slingshot aim with relative mouse events after the press ended; ZL latched/released on successive presses; opening Insert cleared a new shield latch. Inspected Controls screenshot shows Plus/Minus disabled. The new run is in docs/tested-configurations.md (3582a10). Six screenshots are PR attachments, byte-checked without authentication; the private-fork links are removed.

The current-head two-pair FPS comparison and Windows build boxes remain unticked with reasons. The older named performance runs remain explicitly historical, not measurements of this revised head. No new full-suite performance claim or Windows pass is implied.

Please re-review the updated head. docs/forge.md has no separate re-review request command, so this is the request.

Thanks for review 1150. I merged main `f3fe1763dda267a134494f1d21e6ad10b1ec4cd8` with merge commit `68b95c1`, rather than rewriting the branch. The description now states that explicitly. - Mouse: `bc0c129` makes the latch file-static. Native's ZR/L/R decision and L's pointing flag both use `*buttons | toggles.latched`. The regression fixture exercises the real `read_input` and Npad writer for L/R/ZL/ZR in all three mouse modes. L pointing and background-pad tests failed before the fix and pass after it. - Documentation: README Controls, its ini block and development.md's options-file reference now document Toggle, `toggle_<button>=0|1`, defaults, exclusions and reset limits. The original checklist boxes are restored, including the unticked Windows build box and its open test request T-NDNY. - Measurements: `547f5f7` adds the four previously recorded runs to `docs/tested-configurations.md`, below the day's existing rows, with their original commits. New proof is recorded separately, not presented as a repeat of the old FPS comparison. - Plus/Minus: their Toggle cells are disabled, keyboard navigation skips that column, the ini reader ignores those flags and Npad masks them out. A manually supplied flag cannot turn either hold into a tap. Tests cover persistence, navigation and held Plus serialization. - Background: continuous reset now follows `pads_in_background`, including the environment override. Losing focus still clears once; a new background pad press can latch when background pads are enabled. - Linux menus: the existing pause-focus hook now clears latches (`bc0c129`, test in `72e2b34`). Title/game-over/save-dialog tracking remains Windows-only. Porting that whole mouse/menu tracker is not a cheap, safe input fix. Help and docs now name Insert and Plus/Minus rather than promising every Linux game dialog clears a latch. - Indicator: not added. This keeps the change to input and the existing Controls column rather than adding HUD state and a new option. The cost is that actions without a visible hold have no separate indication; the docs state this. Insert remains the explicit release path. Scene changes, cutscenes and disconnects still do not universally reset latches. - Option 54: coordination notes for #196 and #152 are drafted for posting by the parent, including the Sprint-after-latch interaction and their API-version 22 overlap. This PR changes neither public interface structure and needs no API-version bump. - Targeting help: I have not verified the game's own hold/switch setting, so I did not recommend an unconfirmed setting. Full Linux suite under the shared lock: **512 passed, 5 failed, 16 skipped** in 497 seconds. Three failures were extracted-fixture compilation errors caused by the new latch type/pause constants; `7720460` fixes them without weakening assertions. The final focused run (including all three) is **14 passed, 0 failed**. The two remaining failures also reproduce on untouched main `f3fe1763`: `test_linux_tools.py` (four assertions, its fixed `/usr/bin:/bin` PATH on Nix) and `test_menu_i18n.py` (stale Portuguese debug-help text). `test_display_pointer.py` and `test_texture_staging_publication.py` pass on main. The fresh Linux NUP build and shader preparation both passed; tracked source hashes matched the pinned archive before and after the build. Fresh game proof (`bc0c1292`, same production code as head `3582a10`): Native mouse + latched ZR moved the slingshot aim with relative mouse events after the press ended; ZL latched/released on successive presses; opening Insert cleared a new shield latch. Inspected Controls screenshot shows Plus/Minus disabled. The new run is in `docs/tested-configurations.md` (`3582a10`). Six screenshots are PR attachments, byte-checked without authentication; the private-fork links are removed. The current-head two-pair FPS comparison and Windows build boxes remain unticked with reasons. The older named performance runs remain explicitly historical, not measurements of this revised head. No new full-suite performance claim or Windows pass is implied. Please re-review the updated head. `docs/forge.md` has no separate re-review request command, so this is the request.
simonwjackson changed title from feat(input): hold-to-toggle for any bound button to WIP: feat(input): hold-to-toggle for any bound button 2026-11-13 20:46:25 +00:00
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 4s
This pull request is marked as a work in progress.
Some workflows are waiting to be reviewed.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'feat/input-hold-to-toggle:simonwjackson-feat/input-hold-to-toggle'
git switch 'simonwjackson-feat/input-hold-to-toggle'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!137
No description provided.