Skip to content

feat(menu): a VRAM budget row of its own (builds on #1358) - #1859

Merged
hckrman101 merged 3 commits into
NotRexed:mainfrom
Joey-Magnus:feat/vram-budget-row
Nov 11, 2026
Merged

hckrman101 merged 3 commits into
NotRexed:mainfrom
Joey-Magnus:feat/vram-budget-row

Conversation

@Joey-Magnus

@Joey-Magnus Joey-Magnus commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Players can't choose a VRAM limit: the texture and mirror budgets come only from the Quality preset. On a 12 GB card, for example, "Custom" means the automatic 6 GB and "High (12GB)" means 7.5 GB, nothing in between and nothing lower (null and Gummi in #dev-work). This adds a VRAM budget row of its own.

Built on #1358, which is now merged (DarkzGG's work, squashed into main as 26b2f74). This branch is rebased onto main and carries only the row.

The row (Graphics, next to Render resolution):

  • Choices: Auto (the renderer's own rule: 70% of what the driver allows, 45% on cards of 8 GB or less, at most 6 GB) or a texture budget of 3.6 / 4.8 / 6 / 7.5 / 9 / 11 GB. The row's own steps skip 2.2 GB: that's the Minimum (4GB) preset's budget only, and the row shows it as "2.2 GB (4GB preset)" when that preset sets it. Measured on a 12 GB card (castle-grounds walk, two runs each): 2.2 GB evicted and remade textures 2-3x as often as Auto, with 1% lows of 4.8-7.0 fps (hckrman101's question in #dev-work). The mirror budget is scaled from it (256 MB … 3 GB). The presets' own budgets are among the choices.
  • A preset sets the row. Each preset gives exactly the budgets it gave before. Choosing Custom on the preset row clears it to Auto, as before.
  • Changing another row (preset → Custom) keeps it (feat(Menu): a row that turns the preset to Custom keeps the preset's memory budgets #1358).
  • A budget chosen on the row is the player's (opts.budget = 0), so it applies on any card. A preset's own budget still applies only on the card size it is for (feat(Menu): a row that turns the preset to Custom keeps the preset's memory budgets #1358's preset_caps_memory).
  • The row is preset_exempt: changing it leaves the preset alone. It's saved as vram_budget= like any row. An options file from before the row takes its preset's (memory_budget's) budgets. memory_budget is always saved, 0 included, so a budget chosen on the row with a preset on stays the player's after a restart.
  • RX_VK_VRAM_MB / RX_VK_MIRROR_MB, the integrated-GPU cap and the 75%-of-allowed safety clamp all apply as before.

Tests

  • New tests/test_vram_budget_row.py. It builds the menu's functions and preset_caps_memory from the source and checks:
    • each preset's budgets are unchanged, Custom on the preset row is Auto, and the row's values are right;
    • High's own 7.5 GB is ignored on a 16 GB card (as before) but a size chosen on the row applies there;
    • the row-change handler, the row's preset_exempt, and the old-file fallback (source checks).
      It fails without the row and passes here.
  • test_framegen_preset.py and test_rumble_menu.py compile the row-change handler, so their stubs gain vram_budget_sync.
  • run_all.py: 278 passed. 2 fail on main too (test_present_ahead.py, test_present_semaphores.py: main's renderer doesn't link since a259978, see #dev-work). For the same reason a standalone build of this branch stops at the link with those two symbols. The row itself compiles: I built it on a tree from before a259978.
  • In game (Linux, RTX 4070 Ti 12 GB, 595.99.02), on that tree:
    • an options file from before the row with quality_preset=4: budget 7299 MB (High's 7.5 GB under the 75% clamp, as before);
    • quality_preset=0 with vram_budget=3: budget 4800 MB, kept in the file.

Not on screen beyond the new row. The High preset's 7.5 GB on 12 GB cards at DLAA is a separate question (null's numbers); this PR doesn't change it.

🤖 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.

Read through the diff and the new test. It's a tidy change and the preset-to-row mapping checks out (each preset still gives its old budgets, 2.2 GB skipped by the row's steps). One real problem, below.

On the #1358 questions: this PR does carry #1358's two commits (8379f61, d6cbbee, same SHAs) plus three merges of main, so it works without #1358 landing first. If #1358 merges first, the shared hunks (opts.budget, preset_caps_memory, memory_budget) are identical, so it should shrink cleanly. No collision I can find: the row is not given a numeric id, its ini key comes from the name (vram_budget), and nothing on main or in the open PRs I searched uses that key or opts.budget.

Bug: a budget chosen on the row is forgotten after a restart when a preset is on. The row is preset_exempt, so with High (12GB) on, choosing 4.8 GB sets opts.budget = 0 and leaves preset = 4. save() only writes memory_budget when opts.budget > 0, so nothing marks the row as the player's. At the next start if (opts.preset > 0) opts.budget = opts.preset; puts it back to 4. On a card bigger than that preset's size (e.g. High or Low on a 16 GB card, Low on 12 GB), preset_caps_memory() is false again, so the saved vram_budget=3 shows in the menu but the renderer uses the card's own budget. The in-game check in the description (quality_preset=0, vram_budget=3) doesn't reach this. Suggest writing memory_budget=0 explicitly and only deriving opts.budget from the preset when the file has no memory_budget line (same flag pattern as has_vram_budget), plus a case in the test.

CI: the failures are test_bake_mimo / test_bake_skeleton (clang++ has no C++23 on the runner) on Linux and test_bake_ds (libzstd.so) / test_log_batch (a timing check) on Windows, none of them touched by this PR; test_vram_budget_row and the two stubbed tests pass. Builds and AppImage are green.

Comment thread gpu/vk/vk_menu.cpp Outdated
opts.sharpness = 0;
}
if (opts.preset > 0) sync_preset_budgets(opts.preset);
if (opts.preset > 0) opts.budget = opts.preset; // (a file from before memory_budget: its preset's)

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.

This is where the row's choice gets lost: with a preset on, opts.budget is always the preset's after a restart, even if the player set the row (budget 0, nothing saved to say so). Only take the preset's here when the file has no memory_budget line, and write memory_budget=0 in save() when a preset is on and budget is 0.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f1ffd27 as suggested: save() always writes memory_budget (0 included), and the preset only supplies opts.budget when the file has no memory_budget line (has_memory_budget, the same pattern as has_vram_budget).

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.

Confirmed in f1ffd27: memory_budget is written every time, and the preset only fills it in when the file has no such line. The restart case in the test covers it. Thanks!

@hckrman101

Copy link
Copy Markdown
Collaborator

Heads-up: this conflicts with main now. Could you rebase onto main and resolve the conflicts? Thanks!

Joey Magnus and others added 3 commits November 11, 2026 13:36
The texture and mirror budgets came from the Quality preset alone: NotRexed#1358 keeps them when
another row makes the preset Custom, but no limit could be chosen apart from a preset
(null and Gummi, #dev-work). The VRAM budget row (Graphics) holds them: Auto, or 2.2 / 3.6
/ 4.8 / 6 / 7.5 / 9 / 11 GB of textures with mirrors scaled from it (the presets' budgets
among them). A preset sets the row; set on the row the budget is the player's (opts.budget
0) and applies on any card, while a preset's own still applies only on the card it is for
(NotRexed#1358's preset_caps_memory). The row leaves the preset alone. A file from before the row
takes its preset's (memory_budget's) budgets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2.2 GB is too few textures past a 4 GB card: on a 12 GB card a castle-grounds walk made
textures again 2-3 times as often as at Auto, 1% lows of 5-7 fps. The row steps past it;
the Minimum (4GB) preset still sets it, shown as "2.2 GB (4GB preset)".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a restart

With a preset on, choosing a budget on the row sets opts.budget to 0 (the
player's), but save() wrote memory_budget only when above 0. The next
start took the preset's budget back ("a file from before memory_budget"),
and on a card bigger than that preset's size the renderer used the card's
own budget while the menu still showed the row's.

save() now always writes memory_budget, 0 included, and menu_init takes
the preset's only when the file has no memory_budget line.
test_vram_budget_row saves and restarts with vk_menu.cpp's own lines:
High on a 16 GB card with 4.8 GB on the row keeps 4.8 GB, and a file
from before memory_budget still takes the preset's.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Joey-Magnus

Copy link
Copy Markdown
Contributor Author

Thanks, good catch. Fixed in f1ffd27: save() always writes memory_budget, 0 included, and menu_init takes the preset's budget only when the file has no memory_budget line.

test_vram_budget_row has a restart case built from vk_menu.cpp's own save and load lines:

  • High on a 16 GB card, with 4.8 GB chosen on the row, still has budget 0, vram_budget 3 and 4800 MB after the restart, and preset_caps_memory() holds.
  • A file from before memory_budget still takes the preset's budget.

It fails without the fix (on the first case) and passes with it.

Rebased onto current main. #1358 was squashed into 26b2f74, so I replayed only this PR's two commits on main and dropped #1358's original commits and the three merges. The conflicts were mechanical: main's snap_render/env_release block next to this row's handler, main's env_override lines, and the env_release stubs in two tests.

tests/run_all.py on Linux: 393 passed, 0 failed.

@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-checked at f1ffd27 after the rebase. The restart bug from my last review is fixed, exactly as suggested: save() always writes memory_budget (0 included), and menu_init only falls back to the preset's budget when the file has no memory_budget line (has_memory_budget). The new restart case in test_vram_budget_row (High on a 16 GB card, 4.8 GB chosen on the row) uses vk_menu.cpp's own save, parse and fallback lines, so it follows the source and would catch a regression. The case for a file from before memory_budget still gets the preset's budgets.

The rest checks out:

  • #1358 is merged (26b2f74), and the branch now has only this PR's three commits on top of main. The row hunks are the same as the version I reviewed, apart from the fix.
  • It works with #2376: memory_budget is in save()'s mine list, so writing it every time still leaves one line in the file, and test_options_budget_once passes.
  • Older files are fine. Files from main always had a memory_budget line whenever a preset was on, so has_memory_budget can't change what they load. The first-start preset and RX_VK_LOW_SPEC both go through apply_preset/sync_preset_budgets, so the row follows them.
  • No settings id or ini key collisions. The row has no numeric id (its key is vram_budget), and the PR touches only vk_menu.cpp/.h and tests.
  • No overlap with #2198, which works on the texture budget further down in vk_texture.cpp/vk_vma.cpp and only reads the budget this row sets. #2401 is closed.
  • test_framegen_preset and test_rumble_menu, which compile set_row from vk_menu.cpp, have their stubs updated and pass.

CI: the only failure is test_native_astc_texture_count.py on Windows (scanf deprecated under -Werror), and main fails it too at this branch's base (2ed98c4). test_vram_budget_row passes on both Linux and Windows, and the builds and the AppImage are green. The branch is mergeable.

One small request: the new row is on screen, so per CONTRIBUTING a screenshot of the Graphics tab with it would be good to add to the description (for example with a preset on and the row showing "2.2 GB (4GB preset)", or a size chosen on the row). Not a blocker on the code side.

@hckrman101
hckrman101 merged commit bb1ffa2 into NotRexed:main Nov 11, 2026
12 of 13 checks passed
@Arkex-ai

Copy link
Copy Markdown
Contributor

Windows follow-up after the merge: tests/test_vram_budget_row.py fails to compile on main 3e95ca68 with llvm-mingw Clang under its -Wall -Wextra -Werror fixture flags:

fixture.cpp:89:15: error: unused variable 'kVramMinimumOnly' [-Werror,-Wunused-const-variable]
constexpr int kVramMinimumOnly = 1;

I saw the same failure in a full main-baseline suite and in the separate Builder-runtime candidate based on it. This is fixture compilation, not a measured VRAM-row runtime regression. Are you already covering the follow-up? I can validate the Windows fixture once adjusted.

hckrman101 pushed a commit that referenced this pull request Nov 11, 2026
… and its next load (#2524)

On NVIDIA's native cooperative-matrix path, the game's own upscaler
below 100% Render resolution (Auto included) draws flickering bright
bars. Each bar is 16 px wide and 2 rows tall, at `x % 16 == 0` and `y %
8 in {0, 1}` of the output, in different places every frame (#2455,
tracked in #2511). OoT's programs store to shared memory and read it
back, other lanes' words included, with no BAR between: the Switch runs
a warp's shared memory accesses in order. Vulkan orders another lane's
store and this lane's load only through a barrier.

## Cause
The 4/3 gameplay network `fd029a317a9a4b94` ends with 8 STS and then 16
LDS (one per output pixel it writes) with no barrier between them. I
dumped its SPIR-V with `RX_VK_DUMP_SPV`. The main function's shared
memory accesses, barriers and matrix ops in program order:

```
S8 B L32 B M176 X16 S8 B L64 B B L20 B S8 L16      (S store, L load, B OpControlBarrier, M OpCooperativeMatrixMulAddKHR, X shuffle)
```

The last `S8 L16` has nothing between them, so the driver's compiler is
free to move the loads before other lanes' stores. That is the same
mechanism as LDSM's (`lower_to_maxwell.h`, nxnative's "LDSM reads rows
other lanes stored"), but with a plain LDS. It shows on the
cooperative-matrix path. On the software HMMA path (subgroup shuffles
everywhere) it did not show in my runs.

## Change
- `lower_to_maxwell.h`, STS/LDS (0x388 / 0x984): after an STS, the next
LDS gets a subgroup barrier first (MEMBAR bit 20, `SubgroupBarrier`, as
LDSM's), but only when the LDS is unpredicated, so every lane of the
warp reaches it. A BAR.SYNC clears the pending store. In this network it
adds exactly one `OpControlBarrier Subgroup` between the `S8` and the
`L16`, in the same block as the program's workgroup barriers (so in
uniform control flow).
- `RX_VK_SHARED_NO_LDS_BARRIER=1` turns it off, as before. It is under
the `RX_VK_SHARED` prefix, so it is in `translator_stamp`
(`test_program_stamp_knobs` passes).
- `tests/test_lower_lds_after_sts.py` lowers STS/LDS/BAR sequences
against the real header: a barrier between a store and its next load,
one per store, none after a BAR.SYNC, none before a predicated LDS, none
with the switch. It fails on main (`FAIL: count(one, 'B') == 1`).

## Tested
RTX 2060 SUPER (Turing), driver 610.62, Windows 11. Kokiri Forest from a
save, in a separate copy of the game folder. The game was driven through
the control bridge (headless, `option_set`, 10 shots at 2048x1152 per
setting). The bars were counted by a detector: 16x2, brighter than the
rows above and below, ending sharply on both sides, counting only places
seen in at most half of the shots. The same patched `hookshot_vk.dll`
(built with `RX_VK_ONLY=1` on b744b1e, the exe's API version) was used
for both the on and off runs, each from a fresh program cache. Every
run's log has the network's dispatch (and its pipeline compiled) before
the shots.

| Upscaler "Game's own" | main | this PR | this PR,
`RX_VK_SHARED_NO_LDS_BARRIER=1` |
|---|---|---|---|
| Render resolution 75% (`fd029a317a9a4b94`) | 40 | **1** | 51 |
| Auto (dynamic) | 49 | **0** | - |
| 67% (`6676619641320ba7`) | 4 | 0 | - |
| 100% (control) | 4 | 2 | 1 |

For reference on main: the 75% stand-in
(`RX_VK_TAAU_KEYS=fd029a317a9a4b94`) gave 0, and AMD FSR 3.1 at 75% gave
1.

GPU time a frame at 75% (the log's `GPU time`, median of the last 12
lines): 12.8 ms on main, 12.3 ms with this change, 12.3 ms with it
switched off. No measurable cost.

`tests/run_all.py` (Windows, llvm-mingw 20261104): everything passes
except `test_vram_budget_row`, which fails the same way without this
change (`-Werror,-Wunused-const-variable` on `kVramMinimumOnly`, from
#1859).

## Not tested
- AMD, Intel and Linux. The barrier is the one LDSM already uses on
every card, but I could not run it on other hardware.
- Places other than Kokiri Forest, and the movies. The rule also applies
to every other program with an STS then an LDS, which I counted nowhere
but this network.
- #2477 (checkerboard lines when moving, RTX 4070) may be the same bug;
not verified.

Fixes #2455 (part of #2511).

[Investigated with the help of an AI agent (Hermes) on my machine; I
reviewed the change and the numbers.]
pmwork-personal added a commit to pmwork-personal/Hookshot-pr that referenced this pull request Nov 11, 2026
The table follows the Graphics sections; the GPU and memory section had no
line for NotRexed#1859's VRAM budget row, and the HDR section none for HDR
contrast. VRAM budget lists the row's own steps (the arrows go past 2.2 GB,
which only the Minimum (4GB) preset sets).

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