Skip to content

feat(input): Camera speed row for the right stick and the camera keys - #1945

Merged
hckrman101 merged 1 commit into
NotRexed:mainfrom
j64esatxzdrhfg:feat/camera-speed
Nov 11, 2026
Merged

hckrman101 merged 1 commit into
NotRexed:mainfrom
j64esatxzdrhfg:feat/camera-speed

Conversation

@j64esatxzdrhfg

@j64esatxzdrhfg j64esatxzdrhfg commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

The right stick turns the camera slowly, even at the fastest of the game's own camera speeds.

What changed

  • Controls tab, under Invert camera X/Y: Camera speed, 50–400% in steps of 25, default 100%, saved as camera_speed, a slider that stops at its ends.
  • nx2/oot_patches.py: each return of the right stick's turn-speed functions multiplies s0 by hos_camera_scale. The functions are f_80643f10 (yaw), f_80643b60 (pitch) and their level-2 pair f_82ea21f0/f_82ea2220. None of them calls another, so a turn is scaled once. The functions are found by name, so only the supported build's lift is patched, with a warning if fewer than four are found.
  • host/nn_vi.c: sets the scale from the row at each present via the option table (HOS_OPT_CAMERA_SPEED, 48; free beside main's newer ids: Text size 10, Skip title 44, camera centering 45). It sits next to feat: Add an option to disable automatic camera centering #1921's Disable camera centering in the Controls tab. At 100% the lifted code runs as shipped.
  • The camera keys and Mouse camera "As the stick" follow it; native mouse look has its own sensitivity.
  • Tests: test_camera_speed.py covers the patch on a made-up lift, the option's path to the host and the scale; test_frame_rate_cap_step.py's fixture has the new scale.

Testing: Pro Controller at 100/200/300% on the v2.0.13 build. On main with this series, the row is in the Controls tab at 200% (screenshot).

Not tested: other game versions (by design unpatched), Linux.

Screenshot

Camera speed row in the Controls tab

🤖 Generated with Claude Code

@hckrman101 hckrman101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good to me. I read the patch, the host side and the tests; I did not build or run it.

  • Option id 48 is free: main uses 0-9, 10 (Text size), 16-22, 24-28, 32-35, 40-43 and 45 (camera centering), and none of the open PRs, including the rest of #1940-#1946, claims 46-59. It is wired the same way as the other rows: HOS_OPT_CAMERA_SPEED in vk_api.h, the fill_replay_table answer, then rx_option(...) in realtime_step. Out-of-range values fall back to 100%, so a missing menu or an edited ini gives the game's own speed.
  • The patch is idempotent (a second run keeps one scale), it only touches the four functions by name, and it warns if fewer than four are found. It leaves any lift that lacks the names unpatched. The row is a slider that stops at its ends, with the readme row and the description in the Controls tab matching what the code does.
  • Tests are declared (RUN_ALL_ARGS), and test_frame_rate_cap_step.py got the new scale and the option enums it now needs.
  • Overlap with the rest of the series: #1941, #1942, #1944 and #1946 also edit vk_menu.cpp/vk_menu.h, and #1940, #1941, #1942 and #1944 edit vk_replay.cpp. None of them adds an option id or touches vk_api.h, so there is no id clash. Only textual conflicts are possible near the row table and the fill_replay_table chain, and whichever merges later rebases.

Nits and things I could not check:

  • CI has not run on this head (the workflows are waiting for approval, action_required), and the branch is 56 commits behind main, so please run it and rebase before merging.
  • That s0 carries the return value of those four functions, and that scaling them is the whole of the right stick's turn, rests on the author's reading of the lift and the in-game test at 100/200/300% on v2.0.13. I can't check that without the dump. The made-up lift in the test covers the patch mechanics only.
  • The patch turns every return; into a block, which is fine for the if (...) return; shape the test covers; an if (...) return; else ... in a real lift would not compile, though the author's run on the supported build suggests there is none.

The maintainer's call on merging; nothing here blocks it.

@Ekibunnel

Copy link
Copy Markdown

I was working on the same thing myself, but this pull request beat me to it.
And it’s neater than what I would have done.
Please merge as fast as possible. (for some of us the game is not playable without this)

Players find that the right stick turns the camera slowly, even at the
fastest of the game's own camera speeds.

The Controls tab gets a Camera speed row, under Invert camera X and Y:
50 to 400% in steps of 25,
100% out of the box, saved as camera_speed. It is a slider that stops
at its ends, as the other percentage scales are.

- nx2/oot_patches.py makes each return of the right stick's turn-speed
  functions multiply s0 by hos_camera_scale: f_80643f10 (yaw),
  f_80643b60 (pitch) and their level-2 pair f_82ea21f0 / f_82ea2220.
  None of them calls another, so a turn is scaled once. They are found
  by name alone, so only the supported build's lift is patched, and a
  warning says when fewer than four were found.
- host/nn_vi.c sets hos_camera_scale from the row at each present,
  through the renderer's option table (HOS_OPT_CAMERA_SPEED, 48).
  At 100%, or without the Vulkan renderer's menu, the lifted code runs
  as shipped.

The camera keys and a mouse with Mouse camera "As the stick" feed the
right stick and follow the row; native mouse look does not. readme.md
lists the row.

tests/test_camera_speed.py checks the patch on a made-up lift, the
option's way from the menu to the host, and the scale.
test_frame_rate_cap_step.py's fixture has the camera scale that
realtime_step now sets.

Tested in game on the v2.0.13 base (RTX 5080, Pro Controller, 100, 200
and 300%). On main: the row seen in a build with this series (Controls
tab, 200%), compile-checked, and the game-free tests.

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@j64esatxzdrhfg

Copy link
Copy Markdown
Contributor Author

Rebased onto current main again: resolved the conflicts with the new movement-speed patch (SPEED_PLACES in oot_patches.py, realtime_rate in the frame-cap fixture) and #1921's camera centering (id 45). test_camera_speed.py and the menu and frame-cap checks pass. Thanks for the review!

@hckrman101 hckrman101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-read after the rebase; still looks good. I read the diff, did not build or run it.

The push is a rebase onto current main (resolving the movement-speed SPEED_PLACES patch and #1921's camera centering), and the patch is the same as the one I approved: the four turn-speed functions in oot_patches.py, hos_camera_scale in nn_vi.c, the slider row, the fill_replay_table answer and the tests.

Option id 48 is still free: main uses 0-9, 10, 16-22, 24-28, 32-35, 40-43, 45 and 4096+, and the open PRs I checked take 46/47 (#2345's Walk key) and 60 (#2261's item wheel). #1600 and #2286 are fixes in the input path that do not touch these files or ids, so no clash with them.

The earlier nits stand: the real-lift shape of return; and the s0 reading rest on the author's in-game run on v2.0.13, which I can't check. CI has not run on this head (workflows are action_required, waiting for approval), so someone needs to approve the run before merging. The branch is mergeable now. Ekibunnel's note that this is needed for them to play is a good reason to land it soon; the call stays with the maintainer.

@hckrman101
hckrman101 merged commit c689be3 into NotRexed:main Nov 11, 2026
1 check passed
hckrman101 added a commit that referenced this pull request Nov 11, 2026
…t compile sooner and stay put (#2324)

Rebuilding the game spends most of a warm build in the lift (translating
the game to C), and most of a cold build in the eight hot pieces. This
PR makes both shorter. The game's code comes out the same.

## What changes

1. **A player's update skips the lift when the tools did not change**
(`make_game.keep_translation` / `kept_before`, `build.py` `KEPT`).
- Before, the clean-up after a player's build deleted all of `sa/`,
including the 1.5 GB of lifted C. So every update lifted again (1-3
minutes), even when nothing the lift reads had changed.
- Now the clean-up deletes only the lifted `code_*.c` files and the
build outputs. It keeps:
     - the rest of the lift (~124 MB: headers, runtime tables, image);
- a list that build.py writes after the compile: each lifted file's
object key, its size, and a hash of its flags and headers
(`gen/lifted_objects.txt`). Its first line names the lift key it was
written for and a format number; a new lift deletes the list of the one
before.
   - The lift key in `.lift_key` gets a third line, `kept`.
- The next build with the same lift key, and a list naming that key,
skips the lift. make_game passes the list as `build.py --kept-lift`.
build.py then takes the lifted files' objects from the object cache by
the keys in that list. These are the same objects as before, so nothing
is compiled again.
- If an object is missing, or the flags or headers changed, build.py
stops before compiling anything (exit status 75, `KEPT_STALE`; not 3,
which is `abort()`'s on Windows). make_game then lifts again and
compiles as usual.
- If the clean-up cannot remove a lifted file (Windows: one still held
open), the build still succeeds: it lets `sa/` go as far as it can, and
the next update lifts again.
- The two builder apps' after-build clean-up (`Storage.Free(afterBuild)`
/ `core.free(after_build=True)`) also deleted `sa/`. It now leaves a
kept lift in place. "Free up space" still deletes it.
2. **The big chunks are split in one process per file**
(`chunks.split_big`, using the same worker pool as the edits). Before,
it read all 1.5 GB of chunks on one core.
3. **The runtime tables and standalone files are written beside the
edits.** These are `a64_rtgen`, `hos_image.c`, the import stubs and
`nvn2_exports.c`. They read the image, not the lifted C, so a forked
process now makes them while the C is being edited. Without fork
(Windows), or if that process fails, they are made after the edits, as
before.
4. **The shader list is made again only when its inputs changed**
(`make_game.shader_list_current`).
- Before, every build ran `shader_census.py` (about 4 s) and rewrote the
514 MB `hookshot_shaders.bin`.
- Now a note in the work folder records what the list was made from: the
script, the Python, the names, sizes and times of the files the census
reads, and the list file itself. If none of these changed, the census is
skipped.
5. **The Windows setup parts are zipped at the same time**
(`scripts/make_setup_parts.py`), one thread each, as the Linux
`make_parts.py` has done since #2079. The zips come out byte for byte
the same.
6. (Dropped: compiling the hot pieces without loop unrolling. It was in
this PR and is reverted by a later commit; see "Not in this PR".)
7. **The hot pieces of the last lift are kept** (`split_hot.py`).
- Before, split_hot dealt the hot functions out into eight pieces from
scratch at every lift. So a few bytes more in one hot function (a patch)
moved functions between most of the pieces, and every player compiled
all eight again. That is the longest third of a cold compile.
- Now the assignment is kept in `<work>/image/hot_pieces.txt`, next to
the chunk boundaries:
     - functions from the last lift stay in their piece;
- a new function goes to the piece of the function it shares the most
calls with;
- a fresh split is made when the kept split's longest piece would cost
more than 15% over a fresh split's.
- Each piece now declares only the functions and host calls it uses. A
new declaration therefore no longer changes every piece.
- The first split after this PR puts the same functions in each piece as
before.
- `hot_pieces.txt` is part of the lift key, as `chunks.txt` is: the
pieces' C follows it.
8. **On Windows, the lift's per-file passes run in several processes**
(`chunks.file_jobs`, `_each`). Before, they used forked workers only, so
without fork (Windows) they ran in one process. Their workers take a
file's path and read the file themselves, so without fork they now start
as new processes (spawn). The emit still runs in one process there.
`HOOKSHOT_LIFT_NO_FORK=1` takes this path on Linux, for testing.
9. Earlier in this PR (first pass):
   - the lift's edits run in one process per file;
   - the lift prints a timing line for each step;
   - the lift key covers only the modules the lift imports;
- a compile that failed before the link keeps the objects it already
made.

## Measured

Setup: Linux, Ryzen 9 7900X (24 threads), 30 GB RAM, other jobs running
on the PC.

- Timed runs used `oot-status --lock --timing`, which gives the run its
own core complex (6 cores / 12 threads).
- Old and new were alternated.
- The bundled clang 23 and Python were used.

**Player update, same tools** (`make_game.py --nup`, no
`HOOKSHOT_KEEP_BUILD_FILES`, objects cached, shader prepare skipped).
Times are from start to `@@done`:

| | update 1 | update 2 |
|---|---|---|
| before (lifts each time) | 100 s (lift 83 s) | 193 s (lift 175 s, PC
busier) |
| after (lift kept) | 9 s | 7 s |

Caveat: these four runs went through `make_game.py` under `oot-status
--timing`. A queue bug that night moved `make_game.py` / `build.py`
processes onto the busy build CPUs during timed runs, so the absolute
seconds of both rows are inflated by load. The gap does not come from
load, though: the "before" runs spent 83 / 175 s in a lift that the
"after" runs skip entirely. The lift-step, hot-piece and game-speed
numbers below ran lift.py, clang or the game directly, and the bug did
not affect them.

A second pair after the queue bug was fixed (`--timing --cpu-only`, one
round, the PC heavily loaded and the builds at idle priority, so both
absolute times are high again): make_game took 640 s before (lift 614 s:
discovery alone 162 s, against 26 s on a quiet PC) and 42 s after (lift
skipped; the rest is the finish stage's file copies).

- After a full build at the same commit, a kept update produced the same
`hookshot`, `hookshot_vk.so` and `image.bin` (sha1).
- Fallback test: I deleted one kept object (`code_010`) and ran
make_game again. It printed "the translation kept from the last build
does not fit this one (1 of 80 files: code_010.c): translating the game
again", lifted, compiled that one file, and produced the same `hookshot`
sha1 as the full build.
- The kept files take 124 MB in the work folder, on top of the objects.

**Lift steps** (lift.py + split_hot.py run alone, two rounds each). All
95 output files and `chunks.txt` were sha1-identical to the old lift:

| step | old (round 1 / 2) | new (round 1 / 2) |
|---|---|---|
| big chunks split | 6.0 / 5.6 s | 2.3 / 2.1 s |
| patches | 2.7 / 2.6 s | 3.5 / 3.1 s |
| output size + page-mark groups | 5.7 / 5.7 s | 6.0 / 6.2 s |
| runtime tables + standalone files | 4.3 / 4.0 s | 0.0 s (beside the
edits) |
| these four steps | 18.7 / 17.9 s | 11.8 / 11.4 s |

**Shader list:** the finish stage took 1 s when the list was kept,
against 4-7 s when it was made.

**Windows setup parts:** I could not run a real package here. On a 104
MB stand-in package (three parts), three rounds took 1.81 / 1.50 / 1.50
s before and 1.05 / 1.15 / 1.04 s after. The zips and
`hookshot-setup.txt` were identical.

**Windows path for the per-file passes:** measured on Linux with
`HOOKSHOT_LIFT_NO_FORK=1`, against the old one-process lift
(`HOOKSHOT_LIFT_JOBS=1`), two rounds. The three per-file passes (big
chunk split, patches, output size + page-mark groups) took 9.9 / 16.1 s,
against 40.6 / 35.6 s. The non-hot lifted files were byte-identical. The
hot pieces differ only because of item 7. The whole lift's time varied
more than that, because the one-process emit took 56-84 s from run to
run.

**Hot pieces kept:** this was measured without a game run, on the game's
real `code_hot.c` (849 functions). In 12 trials, each adding 1-60 lines
to one random hot function, the next split changed 5.42 of the 8 pieces
on average before (1-6 per trial) and exactly 1 after. A fresh split put
the same functions in each piece as the old code.

**Tried and dropped:**

- A faster discovery walk in the lifter (reading the image through a
32-bit view). The output was identical, but discovery ended at 21 vs 23
s in one round and 26 vs 25 s in the other: no clear gain.
- The coverage report in a side process during the emit. The output was
identical, but over four rounds the lift took 92.5 / 61.7 / 82.0 / 68.4
s against 71.6 / 72.4 / 88.4 / 69.8 s without it, a mean change of +0.6
s. The emit slowed by about what the report saved.
- Writing the lifter's small constant-count loops (NEON permutes and
lane loops) out as straight-line C in the emitter. The instruction test
gave the same results as before (Unicorn compare, 41,552 cases, 744 of
749 forms pass, and the same 5 forms fail without the change). But one
hot piece then compiled for over 16 minutes against about 3, so the idea
was dropped.

**Full build of the branch** (before merging main, still with the
dropped item 6): lift 94 s, 9 of 264 files compiled (the eight hot
pieces, 165-363 s each, and one more), then a kept update from it as
above.

**Merge with main:** main removed `oot_native_resolution.py` (#2042) and
added patches to `oot_patches.apply` (the text hooks of #2366, Camera
speed of #1945). They are in the per-file `patch_file` / `report` split
now; the ripple fix of #2409 goes through the frame-time table as
before. A full build of the merge booted from the test save into Kokiri
Forest and played 20 s of scripted running and sword swings (headless)
without an error. It lifted with no patch warnings: every patch found
once, the camera speed at its 4 functions, the three text hooks, both
step divides and both frame counters (the ripple one included). 3 files
compiled.

**Tests:** `tests/run_all.py` at the head (the merge and the review
fixes): 402 passed, 0 failed, 12 skipped. New or changed for the fixes:
a kept list of another lift or format is not used, a clean-up that
cannot remove a file is no failed build, the kept hot pieces change the
lift key, and no module of the lift imports by a name made at run time.
Before the merge: 364 passed, 1 failed (`test_wayland_libdecor_hint`, a
harness `-Werror` warning that also fails without this PR). New tests:
`test_kept_translation.py`, `test_shader_list_skip.py` and
`test_sticky_hot_pieces.py`. Updated tests that cut functions out of
build.py and make_game.py: `test_lift_cleanup_error.py` and
`test_compile_retry_jobs.py`.

## Not tested

- Windows, for all of it (CI runs the Windows test suite; two of its
tests failed on this branch at first, line endings and a missing `lz4`,
and were fixed):
- the kept lift on Windows, and the C# builder change (`Storage.cs`): no
.NET SDK here, only the Python builder's twin was run;
  - the Windows setup zips with a real package;
- the lift's per-file passes in spawned processes on Windows (item 8:
only the Linux spawn path was run), and the side process for the tables
there (it falls back to one process).
- While checking this I noticed that the play profile is out of date for
this lift: clang ignored the profile for 58 of the 134 functions in
`code_hot_1.c` (`-Wprofile-instr-out-of-date`). This happens without
this PR too.
- A kept lift across a real release-to-release update. It was tested
with two builds of the same tools, and with a deleted object for the
fallback.
- Only one round of the re-timed update (above); the first pair is in
the table.
- A full build of the last head, with the review fixes (unrolling back,
the kept list's header, the hot pieces in the lift key): the merge just
before them was built and run, the fixes only by the game-free tests.
With unrolling back, the hot pieces compile with main's flags again.

## Not in this PR (measured or written, for later)

- Compiling the hot pieces without loop unrolling (`-fno-unroll-loops`).
It cut the hot pieces' compile a lot (`code_hot_4.c` 261 → 216 s alone;
all eight 4547 → 2807 s of CPU in a loaded build), and Kokiri Forest ran
as fast (62.8/64.8 fps before, 65.1/65.2 after). But a review found that
it also stops clang from fully unrolling the lifter's ~25,000
fixed-count lane loops (`for (_i < 4)` and the like): on six lane-heavy
functions of `code_hot_5.c` the loops left in the code went from 65 to
263 and the stack accesses from 744 to 1165. The scenes measured barely
run those functions, so they could not show it. Reverted. A narrower set
(`-mllvm -unroll-runtime=false -mllvm -unroll-allow-partial=false`,
perhaps `-mllvm -unroll-max-upperbound=0`) gave code byte-identical to
today's on that sample; its compile-time gain with the play profile is
not measured yet.

- Smaller chunks of ordinary code: three 24 MB chunks compiled whole
took 145 s of CPU and 1.4-1.6 GB each; the same code cut in thirds (at
function starts, as `split_big` cuts) took 117 s (-19%) and 0.52-0.70 GB
each. More compilers would fit on a 16 GB PC, and less CPU on any PC. It
changes every ordinary chunk's object (one full compile for everyone)
and needs a game-speed check, since fewer callees share a file.
- Unpacking the romfs while the game is translated and compiled (about
as long as the unpack, ~1-2 minutes, off a first build): written and
tested game-free, not measured end to end.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
hckrman101 pushed a commit that referenced this pull request Nov 11, 2026
The Graphics tab is one list of 24 settings, with the window, HDR,
upscaling, frame generation and GPU options mixed
together, and about ten more are on their way in open PRs. This puts its
rows under section titles, so the upscaler, frame
generation or HDR can be found without scrolling the whole list. It is
the first step of a menu organization laid out below
for discussion.

**What changed**
- The Graphics rows in six sections, under heading rows like the Debug
tab's Developer (a gold title, never selected,
  nothing to click):
  - Quality preset (first: it sets rows in every section)
- **Resolution and upscaling:** Output resolution, Profile, Upscaler,
DLSS model, Render resolution, Sharpness
- **Frame generation and latency:** Frame generation, Frame pacing, Game
frame rate cap, NVIDIA Reflex, AMD Anti-Lag 2
- **Screen:** Fullscreen, V-Sync, Window scaling filter, Aspect ratio
(#2119), Ultrawide menus
- **HDR:** HDR, HDR paper white, HDR peak brightness, HDR contrast
(#2342), HDR tone mapper (#2481)
  - **Detail:** Anisotropic filtering
- **GPU and memory:** GPU device, VRAM budget (#1859), Mesh shaders
(Robust buffers is on Debug ▸ Developer since #2042)
- Moves only: every row's definition is unchanged, word for word; no row
is renamed and no value or saved key changes, so
options files carry over, and #1864's tables (keyed by the English text)
still find every row. They need the six
  headings.
- No new drawing or input code: the menu already draws heading rows on
any tab, and the keys, the pad and the mouse step
  over them.
- `readme.md`: a sentence on the sections, and the option table in the
new order. (Ultrawide menus and Mesh shaders have
no line yet; #2105 adds them.) The **Frame generation by** row from
#2118 gets its line in the table here too: main's
readme has none yet. So do #1859's **VRAM budget** (under GPU and
memory) and #2342's **HDR contrast** (under HDR).
- `tests/test_upscaler_rows.py`: its order list follows the picture's
path (output, upscaler, render size, sharpening,
then the frame rows, then the window). New
`tests/test_menu_sections.py`: the Graphics sections and which rows they
hold,
and on every tab no section that `row_shown` could leave empty (a title
over nothing on a GPU without Reflex, say).

**The plan this is step 1 of (for discussion)**

Each tab answers one question; a section groups the rows you set
together; a row that depends on another follows it; vendor
rows sit beside their counterpart and hide where they can't run (no
vendor sections, so nothing is left empty on AMD); rows
to touch only when something is wrong go last; moves only, no renames.

| Tab | Holds | Sections |
|---|---|---|
| **Game** (was Gameplay; first, where the menu opens) | how the game
behaves and reads, and Restart and Quit, as a pause menu | flat; Restart
and Quit last |
| Graphics | how the picture is made, and what it costs | Quality preset
· Resolution and upscaling · Frame generation and latency · Detail · GPU
and memory |
| **Display** (new) | how it gets to your screen, and what is drawn over
it | Screen · HDR · Overlays (FPS counter, Menu size, from Game) |
| Audio | sound out and in | + Microphone (from Game: it picks the input
device) |
| Controls | how you play | Camera · Controller · Buttons (optional) |
| Debug | tools and developer switches | Robust buffers in Developer
(#2042, merged) |

Next PRs, one topic each: (2) a Display tab, taking the Screen and HDR
sections over whole and adding Overlays; (3) the
Gameplay tab titled Game, first in the strip, where the menu opens; (4)
Microphone to Audio; (5) optional, sections on
Controls. If a sixth tab is a no, this PR stands on its own.

Open questions:
1. A sixth tab (Display)?
2. V-Sync on Display (the PC convention) or with frame generation (it
changes with it)?
3. Sharpness with the upscaling rows (where players look for it) or with
the window filter (where it runs)?
4. DLSS 5 (#1590) and #1834's Neural rows as a page opened from a
Graphics row, like Motion, rather than a seventh tab?

<details><summary>Where open PRs' rows would go</summary>

- Resolution and upscaling: Sharpening method (#1979), DLSS 5 page
(#1590), Native 3D resolution (#2017)
- Frame generation and latency: Frame generation by (#2118, merged: in
this section now), Frame generation method (#1834)
- Detail: Shadow resolution (#1892), Screen filter and Filter strength
(#1488)
- GPU and memory: Texture streaming cache (#2017), Refresh memory
(#888); VRAM budget (#1859, merged) is in this section now
- Game (was Gameplay): Game language and Menu language (#1864), Skip
title screen (#1560), Amiibo (#1521)
- Controls: Camera speed (#1945)
- #2042 (merged) moved Robust buffers to Debug ▸ Developer.
- #2105: its readme check would read the heading rows as rows; it needs
to skip `{TAB_..., "...", nullptr, 0, 0, 0, ...,
  true}` headings, or this PR adds that if it lands first.

Whichever PR lands second moves one row on rebase; happy to do it for
anyone.

</details>

**Testing**
- `tests/run_all.py` on Windows 11 (llvm-mingw clang, 8 at once) after
merging main at 2dfe901 (with #2119's Aspect ratio, #2481's HDR tone
mapper, #2118's Frame generation by,
#2342's HDR contrast and #1859's VRAM budget): 422 passed, 33 skipped, 2
failed, both outside this PR:
`test_vram_budget_row.py` (fails the same on main's own sources here)
and `test_portable_builder_startup.py` (its
`dotnet publish` needs packages this PC does not have). A build of the
branch compiles. All 81 of main's rows keep
main's definitions word for word; the only additions are the six
headings.
`test_menu_sections.py` fails on main as it stands (no sections) and
passes here; a layout with a section of Reflex and
  Anti-Lag 2 alone fails it.
- A build of this branch (Windows 11, RTX 5090), headless with scripted
menu keys at Small, Medium and Large: 12 Downs
from the Quality preset land on V-Sync and 13 Ups come back past it to
the last row, so every heading is stepped over
both ways and Up from the first row wraps to the last; the help box
always shows the selected row (at the first head,
when Robust buffers was the last row). Again at this head (25feb16,
after #2042, #1859 and #2119): Up from the Quality preset
wraps to Mesh shaders, the last row now, under VRAM budget (the bottom
screenshot).

**Not tested**: in a window with a pad, the mouse wheel and a click just
under a heading (the headless runs drive
the keys); values kept across a restart (no row's saved key changes);
AMD and Intel (their rows hide through `row_shown` as before; the new
check makes sure no section can end up
empty), Linux.

**Screenshots**

The Graphics tab at 1920x1080, Medium menu size: the top, then scrolled
with the keys to V-Sync and to the last row
(the selection steps over every heading; Up from the first row wraps to
the last row). The bottom one is retaken at this
head (25feb16, a 16:9 picture: `HOS_ASPECT=1.7778`, as on a 16:9
monitor); the others are from the first head (634ef65).

![Graphics tab, top: Quality preset, then Resolution and upscaling and
Frame generation and
latency](https://raw.githubusercontent.com/pmwork-personal/Hookshot-pr/pr-screenshots/menu-sections/graphics-sections-medium-1.webp)

![Graphics tab, middle: Frame generation and latency, then Screen,
V-Sync
selected](https://raw.githubusercontent.com/pmwork-personal/Hookshot-pr/pr-screenshots/menu-sections/graphics-sections-medium-2.webp)

![Graphics tab, bottom: Screen with Aspect ratio, HDR, Detail, then GPU
and memory with GPU device, VRAM budget and Mesh shaders, Mesh shaders
selected](https://raw.githubusercontent.com/pmwork-personal/Hookshot-pr/pr-screenshots/menu-sections/graphics-sections-medium-3c.webp)

Small and Large:

![Graphics tab at Small menu
size](https://raw.githubusercontent.com/pmwork-personal/Hookshot-pr/pr-screenshots/menu-sections/graphics-sections-small.webp)

![Graphics tab at Large menu
size](https://raw.githubusercontent.com/pmwork-personal/Hookshot-pr/pr-screenshots/menu-sections/graphics-sections-large.webp)

_Taken from headless runs of this branch (`HOS_HEADLESS=1`,
`RX_VK_SHOT_MENU=1`, the keys from `RX_VK_MENU_SCRIPT`), RTX
5090. Headless, the list never scrolls on main either:
`menu_record_image` leaves `ease_step` at 0, so the build behind
the scrolled shots had one more line there (`ease_step = 1.0f`, hence
"-dirty" in the first head's build label), not part of
this PR.
The windowed menu eases as before._

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants