fix(menu): a layout preset is applied whole after a face button was cleared #266

Merged
HumanBean merged 1 commit from jarynclouatre/Longshot:fix/pad-layout-whole into main 2026-11-13 18:34:51 +00:00
Contributor

Why

On the Controls page, a Layout preset could leave a face button with no gamepad input while the row named the preset. Clear A's Input 1, then choose Xbox / PC: A stays unbound, B moves to East, and the row says Xbox / PC.

apply_pad_layout binds A, B, X and Y's Input 1 one at a time through hos_bind. The host leaves an empty Input 1 as it is when the input asked for is the only one of another button (host/nn_hid.c, so no button is left without a gamepad input). With A cleared, Xbox / PC asks for South while B still has it as its only input, so A keeps nothing. B then takes East, which frees South, but A is not asked again. set_row had already written the chosen value to opts.pad_layout.

What changed

  • menu (gpu/vk/vk_menu.cpp, apply_pad_layout): the three presets are a table, and the four binds run again while a face button's Input 1 is not the preset's, 4 passes at most. A face button that already has the preset's input is not bound again (a bind to the same input changes nothing in hos_bind), so the rules for Input 2 are the same as before. In the case above the second pass gives A South.
  • menu (set_row): after the preset, the row takes its value from detect_pad_layout(), so a preset that cannot be applied whole (South the only input of a button outside the four) shows Custom instead of the preset's name.
  • tests: tests/test_pad_layout.py (new) compiles apply_pad_layout and detect_pad_layout with host/nn_hid.c's binding table and hos_bind as the host. From each preset, each face button's Input 1 is cleared in turn and each preset chosen: the four primaries are the preset's and detect_pad_layout names it. With South the only input of ZL and A cleared, Xbox / PC leaves ZL's South alone and the row reads Custom. A's Input 2 survives a preset. It also checks that the set_row branch ends with detect_pad_layout(). tests/test_framegen_preset.py, tests/test_rumble_menu.py and tests/test_upscaler_round_trip.py extract set_row and now get a detect_pad_layout stub beside their apply_pad_layout one.

Flags and settings

None.

Measurements

main: f3fe1763 (2026-11-13)
this PR: fix/pad-layout-whole @ 29c93738
Hardware: none used (game-free checks on a Linux x86-64 PC, g++ 15.2, Python 3.14.4); the Windows back end cross-built with llvm-mingw 20261104
Scene: none, the game was not run
Method: python3 -I -B tests/run_all.py, two runs each, interleaved (main, PR, main, PR, with another branch's runs between); both runs of a side gave the same counts. This PR's tests/test_pad_layout.py also run against main's vk_menu.cpp.
How to reproduce: the commands above in a checkout of each commit

main @ f3fe1763 this PR @ 29c93738
tests/run_all.py (run 1, run 2) 451 passed, 2 failed, 79 skipped (both) 452 passed, 2 failed, 79 skipped (both)
failing checks test_bake_portability.py, test_menu_i18n.py the same two
this PR's test_pad_layout.py fails: A cleared, Xbox / PC or Swap Face leaves A at -1 and the preset is not detected passes
longshot_vk.dll for Windows (RX_VK_ONLY=1 RX_RELEASE=1 nx2/build.py --standalone) not built builds, 0 failed (built from e29e7756, the same change before the rebase onto f3fe1763)

test_bake_portability.py fails here for a missing Python module (zstandard), on main as well. test_menu_i18n.py fails on main itself: a pt_BR hint (the testing tools' row) is no longer a text of vk_menu.cpp.

Testing

Tested: the game-free suite on a Linux PC, as in Measurements. The Windows back end builds from this branch.

Not tested: the Controls page in the game. The fixture runs the menu's own preset code against the host's own binding rules, which is where the fault was.

Needs other testers: None.

Screenshots

None.

Risk and rollback

A preset now always ends with the preset's four inputs when they can be had, or with the row on Custom when they cannot; before, the row could name a preset the pad did not have. Reverting the commit puts the single pass back.

Reviewer notes

  • Found reading the Controls page's code, not from a player's report.

Issues

None.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • 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.
  • 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

  • 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.
## Why On the Controls page, a Layout preset could leave a face button with no gamepad input while the row named the preset. Clear A's Input 1, then choose Xbox / PC: A stays unbound, B moves to East, and the row says Xbox / PC. `apply_pad_layout` binds A, B, X and Y's Input 1 one at a time through `hos_bind`. The host leaves an empty Input 1 as it is when the input asked for is the only one of another button (`host/nn_hid.c`, so no button is left without a gamepad input). With A cleared, Xbox / PC asks for South while B still has it as its only input, so A keeps nothing. B then takes East, which frees South, but A is not asked again. `set_row` had already written the chosen value to `opts.pad_layout`. ## What changed - menu (`gpu/vk/vk_menu.cpp`, `apply_pad_layout`): the three presets are a table, and the four binds run again while a face button's Input 1 is not the preset's, 4 passes at most. A face button that already has the preset's input is not bound again (a bind to the same input changes nothing in `hos_bind`), so the rules for Input 2 are the same as before. In the case above the second pass gives A South. - menu (`set_row`): after the preset, the row takes its value from `detect_pad_layout()`, so a preset that cannot be applied whole (South the only input of a button outside the four) shows Custom instead of the preset's name. - tests: `tests/test_pad_layout.py` (new) compiles `apply_pad_layout` and `detect_pad_layout` with `host/nn_hid.c`'s binding table and `hos_bind` as the host. From each preset, each face button's Input 1 is cleared in turn and each preset chosen: the four primaries are the preset's and `detect_pad_layout` names it. With South the only input of ZL and A cleared, Xbox / PC leaves ZL's South alone and the row reads Custom. A's Input 2 survives a preset. It also checks that the `set_row` branch ends with `detect_pad_layout()`. `tests/test_framegen_preset.py`, `tests/test_rumble_menu.py` and `tests/test_upscaler_round_trip.py` extract `set_row` and now get a `detect_pad_layout` stub beside their `apply_pad_layout` one. ## Flags and settings None. ## Measurements main: f3fe1763 (2026-11-13) this PR: fix/pad-layout-whole @ 29c93738 Hardware: none used (game-free checks on a Linux x86-64 PC, g++ 15.2, Python 3.14.4); the Windows back end cross-built with llvm-mingw 20261104 Scene: none, the game was not run Method: `python3 -I -B tests/run_all.py`, two runs each, interleaved (main, PR, main, PR, with another branch's runs between); both runs of a side gave the same counts. This PR's `tests/test_pad_layout.py` also run against main's `vk_menu.cpp`. How to reproduce: the commands above in a checkout of each commit | | main @ f3fe1763 | this PR @ 29c93738 | |---|---|---| | `tests/run_all.py` (run 1, run 2) | 451 passed, 2 failed, 79 skipped (both) | 452 passed, 2 failed, 79 skipped (both) | | failing checks | `test_bake_portability.py`, `test_menu_i18n.py` | the same two | | this PR's `test_pad_layout.py` | fails: A cleared, Xbox / PC or Swap Face leaves A at -1 and the preset is not detected | passes | | `longshot_vk.dll` for Windows (`RX_VK_ONLY=1 RX_RELEASE=1 nx2/build.py --standalone`) | not built | builds, 0 failed (built from e29e7756, the same change before the rebase onto f3fe1763) | `test_bake_portability.py` fails here for a missing Python module (zstandard), on main as well. `test_menu_i18n.py` fails on main itself: a pt_BR hint (the testing tools' row) is no longer a text of `vk_menu.cpp`. ## Testing **Tested:** the game-free suite on a Linux PC, as in Measurements. The Windows back end builds from this branch. **Not tested:** the Controls page in the game. The fixture runs the menu's own preset code against the host's own binding rules, which is where the fault was. **Needs other testers:** None. ## Screenshots None. ## Risk and rollback A preset now always ends with the preset's four inputs when they can be had, or with the row on Custom when they cannot; before, the row could name a preset the pad did not have. Reverting the commit puts the single pass back. ## Reviewer notes - Found reading the Controls page's code, not from a player's report. ## Issues None. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [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). - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [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] 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.
fix(menu): a layout preset is applied whole after a face button was cleared
Some checks failed
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
PR description / Description follows the template (pull_request_target) Failing after 3s
29c9373814
apply_pad_layout binds A, B, X and Y's Input 1 one at a time. hos_bind leaves an
empty Input 1 as it is when the input asked for is another button's only one, so
with A cleared, Xbox / PC asked for South while B still had it: B moved to East
and A stayed unbound, and the row said Xbox / PC.

The preset is now asked again while a face button is not the preset's (4 passes
at most; the second finds South free), and the row takes its value from
detect_pad_layout, so a preset that cannot be had whole shows Custom.
tests/test_pad_layout.py compiles apply_pad_layout and detect_pad_layout with
nn_hid.c's binding table and hos_bind as the host.
HumanBean added this to the v3.0.0 milestone 2026-11-13 18:34:45 +00:00
HumanBean deleted branch fix/pad-layout-whole 2026-11-13 18:34:54 +00:00
Some checks failed
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
PR description / Description follows the template (pull_request_target) Failing after 3s
Some workflows are waiting to be reviewed.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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!266
No description provided.