Repository navigation
feat(input): Camera speed row for the right stick and the camera keys - #1945
Conversation
9c75ff6 to
8c7d28c
Compare
hckrman101
left a comment
There was a problem hiding this comment.
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_SPEEDinvk_api.h, thefill_replay_tableanswer, thenrx_option(...)inrealtime_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), andtest_frame_rate_cap_step.pygot 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 editvk_replay.cpp. None of them adds an option id or touchesvk_api.h, so there is no id clash. Only textual conflicts are possible near the row table and thefill_replay_tablechain, 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
s0carries 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 theif (...) return;shape the test covers; anif (...) 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.
|
I was working on the same thing myself, but this pull request beat me to it. |
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>
8c7d28c to
6cb16e2
Compare
|
Rebased onto current main again: resolved the conflicts with the new movement-speed patch ( |
hckrman101
left a comment
There was a problem hiding this comment.
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.
…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>
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).    Small and Large:   _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>
The right stick turns the camera slowly, even at the fastest of the game's own camera speeds.
What changed
camera_speed, a slider that stops at its ends.nx2/oot_patches.py: each return of the right stick's turn-speed functions multipliess0byhos_camera_scale. The functions aref_80643f10(yaw),f_80643b60(pitch) and their level-2 pairf_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.test_camera_speed.pycovers 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
🤖 Generated with Claude Code