Repository navigation
feat(menu): a Quality preset "Auto (this PC)" that picks settings from the hardware and tunes them in play - #2850
Joey-Magnus wants to merge 8 commits into
Conversation
…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
left a comment
There was a problem hiding this comment.
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.honly bumpsMKWVK_WINDOW_VERSION, as therefresh_mhzaddition did.MkwVkHostandMkwVkRendererare unchanged, soMKWVK_API_VERSIONcan stay. - SDL. The
display_sdl.cchange is the Linux side of the desktop size (Windows reads it from the window handle) and uses the existinghos_sdl_desktop_size. - Options. There are no new option ids.
auto_forandauto_tunedare 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 inmenu_auto_tune). - Overlaps. #2676 should land before this (point 2). #2834's comment names
menu_first_start_for_gpu, which this renames. #2557 also changeslow_vram_texture_mipsand 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.
| 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; |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
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.
| } | ||
| // 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) { |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
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.
| 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) { |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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.
| }(); | ||
| 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)); |
There was a problem hiding this comment.
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.
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):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=inhookshot_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:
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_VERSION3,desktop_size);MKWVK_API_VERSIONis 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.
RX_VK_AUTO_PRESET_VRAM_MB=6144No validation, device-lost or "not made" line in any of the runs.
tests/test_auto_settings.pycovers 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.pyon Linux: 428 passed;test_lift_cachefails here as on main (the system Python has no numpy).Three checks that cut functions out of
vk_menu.cppgot 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
EnumDisplaySettingsWthere), 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-treeagainst 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