Skip to content

feat(menu): a Quality preset "Auto (this PC)" that picks settings from the hardware and tunes them in play - #2850

Draft
Joey-Magnus wants to merge 8 commits into
NotRexed:mainfrom
Joey-Magnus:feat/auto-preset
Draft

Joey-Magnus wants to merge 8 commits into
NotRexed:mainfrom
Joey-Magnus:feat/auto-preset

Conversation

@Joey-Magnus

Copy link
Copy Markdown
Contributor

A first start today only looks at video memory: a small card gets Minimum or Low, and every other PC starts on the same plain defaults whether it's a 3060 or a 5090. This adds a Quality preset, Auto (this PC), that new players start on and that picks settings for the PC it runs on, then checks them against how the GPU actually does in play.

Draft until it has been tried on AMD, Intel, a 6-8 GB card and a Steam Deck (test request to follow).

What it does

On launch (gpu/vk/auto_settings.h, one rule table, no list of GPU models):

Memory tier Video memory picks the preset whose budgets and scaling apply: up to 4.5 GB Minimum, 6.5 GB Low, 9 GB Balanced, 13 GB High, above that Ultra. An integrated GPU, a Steam Deck or a card on the mesh fallback starts at Minimum; under 12 GB of RAM is held at Low
Upscaler DLSS on an NVIDIA RTX card, FSR 4 on an AMD RX 9000, where the build has them; otherwise the game's own. DLSS/FSR 4 mode by tier: Performance (Low), Balanced (Balanced), Quality (High, Ultra)
Output resolution The screen's own size when the menu offers it and the tier has the memory for it (1080p/1440p from Balanced, 4K/5120x1440 from High, 5120x2880 at Ultra); else the game's 2048x1152
Left alone The frame rate cap (60), frame generation (off), the profile (Docked)

One log line says what it chose and why, e.g.
vk: Auto (this PC): first start: 12282 MB of video memory: tier 4; NVIDIA RTX: DLSS Quality; output 2560x1440 (the screen's) (NVIDIA GeForce RTX 4070 Ti, ..., screen 2560x1440 at 60 Hz, 30987 MB of RAM, 16 threads).
With another preset chosen it still logs what Auto would choose, for bug reports.

Each start it compares a fingerprint of the GPU, its memory, the screen and the build's upscalers with the one it chose for (auto_for= in hookshot_options.ini) and chooses again when they differ: a new card or monitor gets new settings. Changing any row yourself turns the preset into Custom, as with the other presets, and from then on Auto touches nothing.

In play (the first ~15-50 s of gameplay; menus and loading screens don't count), it measures over the replay's 120-frame windows and moves the Render resolution:

  • down a step only when frames are missed (under 95% of the cap) and the GPU is over 90% busy; a PC holding its cap keeps its picture
  • up a step when the cap holds and the GPU is under half busy, past its first choice up to DLSS/FSR Quality or 100% with the game's own, never DLAA or native AA
  • nothing when the frame rate is short but the GPU is idle (the CPU is the limit)

At most two changes, never up after a down, then done for that PC (auto_tuned=). Not with frame generation on.

Switches: RX_VK_AUTO_PRESET=0 (a first start keeps the defaults), RX_VK_AUTO_PRESET_VRAM_MB=<MB> (pretend the card has that much memory), RX_VK_AUTO_TUNE=0 (no tuning in play). The Linux window bridge passes the desktop size (MKWVK_WINDOW_VERSION 3, desktop_size); MKWVK_API_VERSION is unchanged.

Tested

RTX 4070 Ti 12 GB, driver 595.99.02, Ubuntu 26.04, a private KWin virtual output at 2560x1440 (headless runs, scripted input, the test save to Kokiri Forest and Hyrule Field). Game and renderer built from this branch.

Case Result
First start (no options file) Auto: tier 4 (High), DLSS Quality, output 2560x1440
Same PC again "the settings chosen for this PC before", nothing changed
RX_VK_AUTO_PRESET_VRAM_MB=6144 chose again: Low, DLSS Performance, output 2048x1152 (1440p needs Balanced)
Back to the real 12 GB chose High again
Sharpness changed through the control bridge preset Custom; the next start only logs what Auto would choose
In play, 1440p at a 60 cap 60 fps, GPU 8.4 of 16.7 ms: kept
In play, 4K at a 90 cap (output set by hand) ~80 fps, GPU 10.6 of 11.1 ms: 67% -> 58% -> 50%, then done
In play, 8 GB faked at a 60 cap 60 fps, GPU 8.3 ms: Balanced (58%) -> Quality (67%), then done (the ceiling)

No validation, device-lost or "not made" line in any of the runs. tests/test_auto_settings.py covers the rule table (RTX/RDNA 4/8 GB on 4K/6 GB/iGPU/Deck/no mesh shaders/low RAM, the fingerprint) and the tuning (gather, down only on missed frames, up only with the cap held, the CPU-bound case, the ceiling, two steps, no seesaw). python -I -B tests/run_all.py on Linux: 428 passed; test_lift_cache fails here as on main (the system Python has no numpy).

Three checks that cut functions out of vk_menu.cpp got the new names in their stubs (test_framegen_preset, test_menu_keys, test_menu_env_overrides); none was loosened.

Not tested: AMD, Intel, any Windows run (the screen size comes from EnumDisplaySettingsW there), a real 6-8 GB card, a Steam Deck, frame generation users (the tuning skips them). Known limit: it measures once, at the start of play, so a heavier area later (Hyrule Field, Castle Town) can still dip below the cap.

Overlaps

Conflicts with this branch that they don't already have with main (git merge-tree against each open PR): #2676 (preset VRAM budgets), #2465, #2371, #2274, #1872, #1590. #2557 (Texture detail Auto) touches the same idea from the texture side.

Drafted by _trash.panda's agent; reviewed by _trash.panda.

🤖 Generated with Claude Code

Joey Magnus and others added 8 commits November 12, 2026 11:31
…re, that new players start on

The first start chose Minimum or Low by video memory alone, and every larger card started on the
plain defaults whatever it could run. Auto picks from what the PC reports (gpu/vk/auto_settings.h):
the memory tier (integrated GPUs, handhelds and cards on the mesh fallback at Minimum), DLSS on
NVIDIA RTX or FSR 4 on AMD RDNA 4 where the build has them (else the game's own upscaler), and the
screen's own output size where the tier has the memory for it. Each start compares a fingerprint of
the card, its memory, the screen and the build's upscalers with the one the settings were chosen
for (auto_for= in the options file) and chooses again when it changed. Changing a row comes back to
Custom, as with the other presets. One log line says what was chosen and why.

The screen's size on Linux comes from the window bridge (MKWVK_WINDOW_VERSION 3, desktop_size).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
save() keeps the lines it doesn't write; auto_for wasn't on its list, so the old line stayed after
the new one, won at the next start, and Auto chose again every time. Found in the first windowed
run; the test now checks the key is on save()'s own list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er resolution to what the GPU holds

In play (150 draws a frame or more), over 8 of the replay's 120-present windows: the GPU over 90%
of the cap's frame time takes the Render resolution one step lighter, under 50% one step heavier but
never past what Auto chose; a step down is never followed by a step up, at most two steps, then it
is done for this PC (auto_tuned= in the options file). A frame rate short of the cap with the GPU
idle a quarter of the frame is the CPU's: nothing changes, and the log says so. Not with frame
generation on. RX_VK_AUTO_TUNE=0: off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…OS_FPS_MAX too), as the FPS counter's line

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es; fixtures that cut out save() and the display loop know its names

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, not in hos_display_init

The checks that cut hos_display_init out of the source (the display start-up and libdecor ones)
compile it with stubs; the size is read once the window exists, before the renderer starts, which
is when Auto reads it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd can raise it past its first choice

Lowering on GPU load alone cost picture on PCs that held their cap (the GPU at 95% and 60 fps
was stepped down); raising was capped at what the profile chose, so it never rose at all, and an
8 GB card that could run DLSS Quality stayed on Balanced. Now a step down needs frames missed
(under 95% of the cap) with the GPU over 90% busy; a step up needs the cap held with the GPU
under half busy, up to the best mode that still upscales (Quality; 100% with the game's own),
never native. The guard rails stay: two changes at most, no step up after a step down, nothing
when the CPU holds the frame rate. The log gives the medians the verdict was made on.

In game (RTX 4070 Ti, 2560x1440): 4K at a 90 cap, frames missed: 67% -> 58% -> 50%. A faked 8 GB
card at the 60 cap, the GPU at 8.3 of 16.7 ms: Balanced (58%) -> Quality (67%), then settled.

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

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

Thanks for this. The rule table is easy to follow, the log line will help with bug reports, and the tuning has the right shape: medians over 8 play windows, at most two steps. I reviewed it from the code. A few things need settling before it comes out of draft; the first two matter most.

1. Linux AMD cards start on Minimum. On Linux, Mesh shaders on Automatic picks Compute for every AMD card (the RADV hang workaround in vk_main.cpp, #2245). That leaves dev.mesh_shader false, so auto_profile reports mesh_native = false, and every RX 6000-9000 on RADV, which docs/workflow.md puts in Tier 1, starts on Minimum. That means a 50% Render resolution, the plain scale, the game's output, and no FSR 4 even on a 9070 XT. On main these cards start on the defaults. The same happens on any PC where the player sets Mesh shaders to Compute. mesh_native is also in the fingerprint, so changing that row makes Auto choose again. It should come from what the card supports (the feature query, before the mode is applied), not from the mode this run uses. Every Hardware in the C++ test leaves mesh_native at its default of true, so a case for this would help.

2. The presets' memory budgets (#2676). Auto moves every card over 6.5 GB off the renderer's Auto budget rule, which is what they get on main today, and onto a preset's budget: Balanced's 4.8 GB on 8 GB cards, High's 7.5 GB plus 2 GB of mirrors from 9 GB up, and Ultra's 11 GB on 16 GB. In #2676's table, Balanced's 4.8 GB paged an RTX 3060 Ti and an RTX 4050 (13-55 ms a frame), and Ultra's 11 GB is the fill-up from #105/#106. I'd land #2676 first and rebase this on it. The High step is a separate problem (inline): a 10 GB card gets the 12 GB preset.

3. Measurements (rule 3c). This changes the first start for every new player. The description needs before/after numbers on the same scene, main's first start against Auto, with fps and VRAM, on several tiers:

  • an 8 GB NVIDIA card
  • a 10-12 GB card at 4K output
  • an RX 6000/7000 on Windows and on Linux
  • a Steam Deck
  • an iGPU

The 4070 Ti runs are a good start, and you already list the rest as untested. These are the runs that take it out of draft. Another option is to land the row first with the first start unchanged, and make Auto the default once the numbers are in.

4. CI. tests/test_low_vram_texture_mips.py fails on Linux part 1/2 and Windows part 2/2, and this PR is the cause: its fixture's opts has no budget (inline).

The smaller points are inline:

  • the preset-exempt rows Auto overwrites when the fingerprint changes
  • DLSS chosen when the build has it, even where NGX may not run
  • the CPU-bound verdict checked before the step down
  • the tuning's state not kept across runs
  • the order of the snap in apply_auto

The rest looks fine:

  • API. vk_api.h only bumps MKWVK_WINDOW_VERSION, as the refresh_mhz addition did. MkwVkHost and MkwVkRenderer are unchanged, so MKWVK_API_VERSION can stay.
  • SDL. The display_sdl.c change is the Linux side of the desktop size (Windows reads it from the window handle) and uses the existing hos_sdl_desktop_size.
  • Options. There are no new option ids. auto_for and auto_tuned are new ini keys only.
  • Tests. The C++ test really exercises the rule table and tune(). It doesn't cover what feeds them (point 1) or the menu code around them (apply_auto, the step choice in menu_auto_tune).
  • Overlaps. #2676 should land before this (point 2). #2834's comment names menu_first_start_for_gpu, which this renames. #2557 also changes low_vram_texture_mips and its test, so whichever lands second updates the other's stub.

Nit: in vk_menu.h, the menu_auto_tune declaration carries the old trailing comment "(no options file: the preset for the card)", and the preset comment in Options still lists 0-5.

Comment thread gpu/vk/vk_main.cpp
for (uint32_t i = 0; i < dev.memory.memoryHeapCount; i++)
if (dev.memory.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT)
hw.vram_mb = std::max<uint32_t>(hw.vram_mb, (uint32_t)(dev.memory.memoryHeaps[i].size >> 20));
hw.mesh_native = dev.mesh_shader;

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 this run's mode, not the card's capability. On Linux, Automatic picks Compute for every AMD card (mesh_mode_used = 2 above, #2245), so every RADV card lands here as mesh_native = false and on Minimum. A player anywhere who sets Mesh shaders to Compute gets the same. I'd record whether the card has mesh shaders before the mode is applied and use that.

Comment thread gpu/vk/auto_settings.h
inline int tier_of(const Hardware& h) {
if (h.integrated || h.handheld || !h.mesh_native) return 1;
const uint32_t mb = h.vram_mb;
int t = mb <= 4608 ? 1 : mb <= 6656 ? 2 : mb <= 9216 ? 3 : mb <= 13312 ? 4 : 5;

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.

With High starting above 9 GB, a 10 GB card (RTX 3080, RX 6700) gets the High (12GB) preset: 7.5 GB of textures plus 2 GB of mirrors, 9.5 GB of its 10 before render targets and DLSS. 12 GB cards report about 12,000-12,288 MB, so a step near 11.5 GB would put 10-11 GB cards on Balanced. It's worth checking again once #2676's budgets are in.

Comment thread gpu/vk/auto_settings.h
}
// The upscaler: the one the card has hardware for, else the game's own (on every card, and what the presets were tuned
// on). At Minimum the game's own at its lowest share is the lightest
if (c.tier >= 2 && h.rtx && h.dlss_built) {

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.

dlss_built() says the build has DLSS, not that it runs on this PC. With an old driver, or a Linux install without the NGX libraries (#1748), temporal_mode() falls back to the game's own upscaler. The player then gets DLSS's share (50-67%) drawn by the game's own upscaler, lower than the tier would set for it. dlss_why() is optimistic until NGX answers, so checking it here may not help. Another option: the tuning, or the probe's answer, could snap back to the tier's own Render resolution when upscaler() isn't the upscaler Auto chose. The same applies to FSR 4.

Comment thread gpu/vk/auto_settings.h
const float g = middle(t.gpu_ms, t.n), f = middle(t.fps, t.n), budget = 1000.0f / cap_fps;
t.n = 0, t.gpu_mid = g, t.fps_mid = f;
const bool missed = f < cap_fps * 0.95f;
if (missed && g < 0.75f * (1000.0f / f)) return t.done = true, CPU_BOUND;

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 runs before the step-down check and compares against the frame actually shown. At 40 fps under a 60 cap with the GPU at 17 ms, already over the 16.7 ms budget, it decides the CPU is the limit and finishes for good, because menu_auto_tune saves auto_tuned on CPU_BOUND. I'd only call it CPU-bound when the GPU is also under the budget (say g < 0.9f * budget). The verdict is also taken in the first minute of play, while pipelines may still be compiling and textures streaming in, so maybe CPU_BOUND should end this run without writing auto_tuned.

Comment thread gpu/vk/vk_menu.cpp
logf("vk: Auto (this PC), measured in play: %.0f fps of %.0f, the GPU at %.1f ms a frame of %.1f: Render resolution %d%% -> "
"%d%%", tuning.fps_mid, cap, tuning.gpu_mid, 1000.0 / cap, ur::percent(v), ur::percent(opts.resolution));
} else {
auto_tuned = auto_for;

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.

steps and went_down live in this run's static Tuning, and auto_tuned is only written here, on KEEP or CPU_BOUND. If the game is closed after a step and before the next verdict, the next start measures again from the new resolution and can step back up. So the no-seesaw rule holds within a run but not across runs. Saving that state, or writing auto_tuned after a step down, would fix it.

Comment thread gpu/vk/vk_menu.cpp
logf("vk: Auto (this PC) would be: %s (%s)", auto_choice.why, hw_line); // (for a bug report: what Auto would choose)
return;
}
if (fp == auto_for) {

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.

The description says Auto touches nothing once you change a row, but changing a preset-exempt row doesn't make the preset Custom: Output resolution, the VRAM budget row, and Mesh shaders (which is in the fingerprint). A player who picks their own Output resolution stays on Auto. The next fingerprint change then applies the whole profile over their choice, and a different primary monitor, a laptop or Deck docked or undocked, or a build with or without DLSS all count. Two fixes: turn the preset to Custom when one of those rows changes under Auto, or keep the player's values for them here.

Comment thread gpu/vk/vk_menu.cpp
void apply_auto() {
const ::auto_settings::Choice& c = auto_choice;
if (c.tier < 1) return;
apply_preset(c.tier); // (the tier's scaling rows and memory budgets)

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.

Nit: apply_preset snaps the tier's Render resolution to the current upscaler() before opts.dlss is set below. When Auto chooses again and switches from DLSS to the game's own (say, a build without DLSS), Balanced's 100% is snapped to DLSS's 92% stop and stays there with the game's own upscaler. Setting opts.dlss before apply_preset avoids that.

Comment thread gpu/vk/vk_texture.cpp
}();
return !disabled && small && opts.preset == 1;
// (Minimum, or Auto (this PC), 6, at Minimum's tier: its budgets, opts.budget 1)
return !disabled && small && (opts.preset == 1 || (opts.preset == 6 && opts.budget == 1));

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 why tests/test_low_vram_texture_mips.py fails in CI (no member named 'budget' in its fixture's opts). Please add budget to the stub, plus cases for Auto at tier 1 and at a higher tier. Nit: kAutoPreset rather than a bare 6, if it can be shared through vk_menu.h.

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.

2 participants