Repository navigation
fix: switches that behave as players expect, and dead ones removed - #2042
Conversation
… cache's stamp RX_VK_NO_CBUF_LOAD_RULE, RX_VK_PTR_SPLIT_FALLBACK, RX_VK_ALL_GLOBAL_PROGS, RX_VK_NO_EXTREGS, RX_VK_NO_DRAW_PARAMS, RX_VK_MISS_LOG and RX_VK_BLOCK_PROBE change what the translator makes but were not in disk_stamp's list, so on a PC with hookshot_programs.bin they did nothing until the cache was deleted. A new check reads every RX_VK_ switch the translator reads and fails when one is missing from the list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hookshot_env.txt replaced any switch already set in the environment (setenv(..., 1)), so a one-off launch flag (HOS_FULLSCREEN=0 in a shell, a launcher's or a test script's own switch) lost to a line saved in the file. A name already set when the game starts now keeps its value, and the log says the file's line was left out; within the file a later line still replaces an earlier one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ets keep the preset RX_VK_FRAMEGEN_MULT, RX_VK_PACE, RX_VK_UPSCALE, HOS_REALTIME_STEP, HOS_DYNRES and HOS_OUTPUT_RES were copied into their rows in menu_init, and the next change of any row saved them into hookshot_options.ini for good. They now apply to the run only: save() writes the player's value until the player changes that row (env_override, saved_value). Rows no Quality preset sets (Reflex, Anti-Lag 2, Output resolution, Anisotropic filtering, Ultrawide, Ultrawide menus, Robust buffers, Text speed, Text size, Controller in background, Layout preset, Descriptor reuse, Sampler cache) turned the preset to Custom when changed. They are preset_exempt now; tests/test_preset_exempt.py derives the rule from apply_preset. The Upscaler stays as it was: changing it moves Render resolution to its own steps. The readme and the development guide say which goes first: the environment, then hookshot_env.txt, then the menu's saved rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Switches left over from mkw-recomp and TOTK, or superseded since, each doubled the paths a bug report could be on. Removed, each after checking that nothing in the code, tests, scripts, workflows, builders or docs uses it: - RX_BACKEND=eden: it loaded mkw_eden.dll, which nothing builds, then fell back to WebGPU. RX_BACKEND unset or vulkan is unchanged; the comments and gpu/vk/VULKAN.md name hookshot_vk.dll. - HOS_FIX_RES and HOS_SCENE_NATIVE, with nx2/oot_native_resolution.py, the lift patch they switched (off since NotRexed#849). - RX_VK_FOLLOW_MESH (tried for the foliage, which it did not bring back). - TOTK_MINIMAL_LAYOUT and the TOTK_ prefix in hookshot_env.txt's allowed names. - RX_VK_OOT_VISUAL, RX_VK_OOT_FOG and RX_VK_OOT_EXPOSURE with the Orchid fog and exposure workaround (off since 11-07: the factors were tuned against bugs since fixed); the readme's credit says what it was used for. The NaN guard that shared its header is in ir_opt/fragment_output_guard.h. - nx2/build.py --lib, --only, --opt and --no-polls: nothing passes them (--dll stays: nx2/test_dll.py uses it). Merged: - RX_VK_DESC_SLOTGEN, RX_VK_NO_DESC_REUSE and RX_VK_DESC_REUSE_DGEN are one switch, RX_VK_DESC_REUSE=0/1/2 (never, the global check, by slot), over the Descriptor reuse row. The DGEN switch only turned off issue 1126's fix and is gone. - RX_VK_NAN_GUARD=0 turns off both NaN guards; RX_VK_NO_NAN_GUARD is gone. nx2/bake_ds's copy of the program stamp's list matches the live one again (tests/test_program_stamp_knobs.py checks it). Kept and now documented side by side: RX_VK_FRAMELOG / RX_VK_FRAME_LOG and RX_VK_PRESENT_AHEAD / RX_VK_PRESENTS_AHEAD. HOS_FPS_MAX stays (it takes any rate, where the row has steps); its comment no longer calls it a test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ffers moved there The readme told players with AMD texture artifacts to hand-edit RX_VK_ASTC_RGBA8 into hookshot_env.txt, and Robust buffers, a workaround for driver faults that players cannot judge, sat among the Graphics tab's picture settings. - Debug, Developer: ASTC as RGBA8 (after a restart), the AMD texture workaround, with a hint saying what it is for and what it costs. RX_VK_ASTC_RGBA8 goes over it for a run (=0 now turns it off). Not turned on by itself for any GPU: issue 993's report says it took effect on a 9070 XT and did not help. - Robust buffers moves from Graphics to the Developer section. The Debug tab draws a Developer row with more than two values as a choice with arrows. The readme's menu table, its notes and the AMD section follow, and the table lists the Debug tab. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
About 230 switches were read as on whenever they were set, so RX_VK_NO_PROGRAM_CACHE=0 left the program cache out and HOS_HEADLESS=0 opened no window, while others parsed the number and read 0 as off. env_on(name), in gpu/vk/compat/hookshot_env.h for host/ (C) and gpu/vk with its translator (C++), is off when the variable is unset, empty, 0, false, off or no (any case) and on otherwise; env_int(name, default) reads a number. Every check that only asked whether a switch was set now uses it, so =1 works as before and =0 turns the switch off. A name that some other place reads for a value (RX_VK_WARP64=0, HOS_FULLSCREEN=0, the lists and sizes) keeps its reads as they were. nx2/kit.py has the same rule as env_on() for the build's RX_VK_ONLY. nx2/build.py counts the header in the runtime's object keys, as it does dlss_math.h. nx2/bake_ds reads RX_VK_SHARED_48K and RX_VK_NO_DRAW_PARAMS as the live back end does. The AppImage packs the header with the others host/ includes. The development guide says what "set" means now. tests/test_env_on.py checks the rule in C, C++ and Python, and that no on/off switch is read the old way. The checks that compile code cut out of the changed files force-include the header (and their getenv stand-ins route env_on through them); the ones that look for a switch's read by its text look for env_on; test_display_startup_failure's HOS_HEADLESS=0 case now expects a window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
I read through this one and found nothing that blocks it. Two things need fixing before it can merge, plus a few small ones. I only read the code and haven't run the tests locally. CI hasn't run them either, since GitHub skips the test workflows while the PR has conflicts. Before merge
Minor
Checked and looks right
Suggestion: part 4 (the |
hckrman101
left a comment
There was a problem hiding this comment.
I went through all six commits against the description. Most of it holds up. The removed switches really are gone: nothing in head reads RX_BACKEND=eden/mkw_eden, HOS_FIX_RES, HOS_SCENE_NATIVE, oot_native_resolution, RX_VK_FOLLOW_MESH, TOTK_*, RX_VK_OOT_*, RX_VK_DESC_SLOTGEN/NO_DESC_REUSE/DESC_REUSE_DGEN or RX_VK_NO_NAN_GUARD, and that includes scripts, workflows, builders, docs, the readme and tests. Nothing calls the removed build.py flags either. The Eden changes are only switch plumbing. With nothing set, the translator does what it did before: FOLLOW_MESH was off by default, GuardFragmentOutputs moved over byte for byte, and both NaN guards are still on by default. MkwVkHost/MkwVkRenderer are untouched (only comments in vk_api.h), so no API bump is needed. No new option ids. The edited tests aren't weakened: the needle changes follow the rename, HOS_HEADLESS=0 and RX_VK_REGISTER_AT_ONCE=0 now expect off, and every ENV_ON_H addition has Path imported first. No Windows macro clashes in hookshot_env.h, and every file that calls env_on includes it.
Blocking
- The branch conflicts with main and CI hasn't run. It is 19 commits behind (base
c09e0ac): #2020 Movement speed, #1971 Mesh shaders row, #2000, #2046 and others touchvk_menu.cpp,vk_draw.cpp,vk_shader.cpp,vk_api.h,hos_main.cand 7 of the edited tests. Only the title check ran. The head'svk_api.hstill lacksHOS_OPT_CHEAT_SPEEDbecause #2020 isn't in the base yet. After the rebase, the newtest_preset_exemptwill flag main's Mesh shaders row:apply_presetdoesn't setmesh_mode, and the row isn't marked exempt. That's the right call, it just needs thetrue. - The screenshots don't render. The description links
debug-robust-buffers.pnganddebug-astc-rgba8.pngas relative paths, so GitHub serves broken images. They need to be uploaded into the description.
Should fix (inline)
- On Windows, the environment-goes-first rule makes the menu's Restart ignore edits to
hookshot_env.txt, and the log then says every line was "left out". RX_VK_DESC_REUSE=1means the row's Off indesc_reuse_level.test_menu_env_onstill checks the env throughmenu_env_on, where 1 means on, and the real code never takes that path.- A few on/off switches still read set-means-on (
HOS_NO_HANDOFF,RX_VK_NO_F16,RX_VK_NO_DMABUF,RX_VK_PROF_ALL, half ofRX_VK_LOW_SPEC).test_env_ononly catches the!= NULLform. - Players who followed the old readme still have
RX_VK_ASTC_RGBA8=1inhookshot_env.txt. For them, the new row can't turn the workaround off.
What a player with an existing ini / hookshot_env.txt sees after updating. No saved choice flips, so no options_version bump or migration is needed:
- Any on/off switch set to
0,false,off,noor empty is now off, where it was on before. A leftoverRX_VK_NO_NAN_GUARD=1orRX_VK_DESC_SLOTGEN=1line now does nothing. - A launch's own environment (Steam launch options, a shell) now beats the same name in
hookshot_env.txt. RX_VK_PACE,HOS_DYNRESand the other menu-row switches no longer write themselves into the ini. Values saved by earlier runs stay.- Robust buffers moves to Debug, under Developer. Its ini key is the same (
robust_buffers), so the saved value carries over. ASTC as RGBA8 is a new row, Off by default. - The listed rows no longer turn the preset to Custom. Saved presets are unchanged.
- The first start re-translates the shaders, as with any change under
gpu/vk/eden,compatorvk_shader.cpp. The stamp's new prefixes change nothing when they aren't set.
Nits
saved_value: if a preset or the upscaler's snap sets an overridden row (resolution,realtime,pace) to the same value the environment forced,save()writes the player's old value instead of the preset's.from_file[64][96]: past 64 names, or for a name of 96 characters or more, a later line no longer replaces an earlier one. On Windows the file is read twice, so the second read also logs "left out" for those names.- Stale
--no-pollsmentions remain in comments atnx2/oot_patches.py:315andtests/test_text_speed.py:24.
The two maintainer questions at the end (the WebGPU fallback, the A/B switch policy) are NotRexed's call, so I've only noted them here.
| } | ||
| int ours = 0; | ||
| for (int i = 0; i < from_file_n && !ours; i++) ours = !strcmp(from_file[i], s); | ||
| if (!ours && getenv(s)) { |
There was a problem hiding this comment.
On Windows this breaks the menu's Restart. hos_relaunch.c starts the new process with CreateProcessW(..., NULL /* env */, ...), so it inherits this process's environment, and _putenv has already put every line from the file there. In the new process from_file starts out empty, so each of the file's names is "set in the environment" and gets left out. If the player edits hookshot_env.txt and then picks Restart, the edit is ignored, and the log lists every line as left out. Linux is fine, because it re-execs with /proc/self/environ, the startup environment.
One fix: save the environment block before load_hookshot_env (GetEnvironmentStringsW) and give that to CreateProcessW with CREATE_UNICODE_ENVIRONMENT, plus HOS_RESTART_OF. Another: pass the file's names to the child, e.g. a HOS_ENV_FROM_FILE list that pre-fills from_file.
| int main(int argc, char **argv) { | ||
| if (argc != 3) return 2; | ||
| const bool reuse = menu_env_on("RX_VK_DESC_SLOTGEN", "descriptor_reuse", true, 4); | ||
| const bool reuse = menu_env_on("RX_VK_DESC_REUSE", "descriptor_reuse", true, 4); |
There was a problem hiding this comment.
This now checks behaviour the real code doesn't have. desc_reuse_level() reads RX_VK_DESC_REUSE itself, as clamp(atoi, 0, 2), and only calls menu_env_on when the variable is unset. So the env cases here ("1" meaning reuse on, "0" meaning the row's Off) go through a path the game never takes. In the game, RX_VK_DESC_REUSE=1 gives level 1, which is the row's Off (no slotgen), and =0 turns reuse off completely. That's also a trap for anyone used to RX_VK_DESC_SLOTGEN=1 meaning on. Could the test cut desc_reuse_level too and check 0/1/2 plus the unset case? Making the env values match the row (0/1) with a separate value for "never" would also avoid the surprise.
| reads[name] = reads.get(name, 0) + 1 | ||
| if "gpu/render" in path.as_posix() or "third_party" in path.as_posix(): | ||
| continue | ||
| for name in re.findall(r'getenv\("([A-Z0-9_]+)"\)\s*[!=]=\s*(?:nullptr|NULL)', text): |
There was a problem hiding this comment.
This only matches getenv("X") != NULL and == NULL. The forms the rewrite skipped still read set-means-on, so =0 turns them on:
host/nn_os_sync.c:446getenv("HOS_NO_HANDOFF") ? 0 : 1gpu/vk/vk_shader.cpp:572!getenv("RX_VK_NO_F16")(also:1559andvk_draw.cpp:6747; it's a translation switch)gpu/vk/vk_memory.cpp:276getenv("RX_VK_NO_DMABUF") ? nullptr : ...gpu/vk/vk_internal.h:358getenv("RX_VK_PROF_ALL") || ...RX_VK_LOW_SPEC:vk_menu.cppreadsatoi != 0, butvk_memory.cpp:1410treats any value as set, so=0is half on.
Converting these and widening the pattern (a bare getenv("X") followed by ), ?, &&, ||, or preceded by !) would make "every check that only asked whether a switch was set now uses it" true, and keep it true.
|
|
||
| This is a workaround for some artifacts, not a general lighting fix. It can substantially increase graphics-memory | ||
| use and cause stalls when the card runs out of headroom. To undo it, remove that line and restart the game. | ||
| use and cause stalls when the card runs out of headroom. To undo it, turn the row off and restart the game. |
There was a problem hiding this comment.
Players who followed the old instructions still have RX_VK_ASTC_RGBA8=1 in hookshot_env.txt. That line goes over the row, so turning the row off does nothing for them, and the row shows Off while the workaround is on. Could this say "turn the row off (and remove any RX_VK_ASTC_RGBA8 line from hookshot_env.txt)"? Even better, the row could show as fixed while the variable is set, the way row_fixed does for other env-held rows.
Brings in main (HKSTATE2 NotRexed#2046 and the Mesh shaders row); the tree is the knobs series rebased on it, built and tested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Two more dead ones in the same area, in case you want them in this pass: |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…epare check read env_on After the merge with main: the V-Sync row's switches in vk_menu.cpp read through env_on again, and its include was put after vk_work.h, which tests/test_recorder_sync.py wants last. tests/test_prepare_cache_save.py, new on main, looked for the prepare's RX_VK_PREPARE_AGAIN read by its old text and compiled it without the header: it now looks for env_on and force-includes gpu/vk/compat/hookshot_env.h, as the other checks that cut code out of the sources do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's new switches (RX_VK_LOW_SPEC, RX_VK_AUTO_PRESET, RX_VK_SEED_CHECK) read through env_on/env_int. SDL_VIDEODRIVER is a value, not a switch: back to getenv. hookshot_env.h sets _CRT_SECURE_NO_WARNINGS for its own stdlib.h on Windows (the checks paste it ahead of files that set it themselves), and the checks that compile env_on code get the header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its stdio.h comes before hookshot_env.h, so on Windows the CRT had already marked getenv deprecated and -Werror failed the compile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
relaunch() gave CreateProcessW no environment block, so the new process inherited this one's, with every line of hookshot_env.txt already _putenv'd: each line was then taken for the environment's own and left out, edits made while the game ran were lost, and the log listed every line as left out. main keeps the environment block it started with (GetEnvironmentStringsW, before the file is read) and the restart passes that, with HOS_RESTART_OF put in at its place, as Linux's execve with /proc/self/environ already did. Also in host/hos_main.c: a switch this build no longer reads (RX_VK_NO_DESC_REUSE, RX_VK_DESC_SLOTGEN, RX_VK_NO_NAN_GUARD and the other removed ones) gets a log line naming what replaced it, and the file's names are kept up to 256 of up to 127 characters (was 64 of 95). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… takes the row's values HOS_NO_HANDOFF, RX_NVN_TEXELS_LOG, RX_VK_NO_F16, RX_VK_HANDLES_LIVE, RX_VK_NO_DMABUF, RX_VK_PROF_ALL and vk_memory.cpp's RX_VK_LOW_SPEC were still on when set to 0. test_env_on now also finds !getenv(x), getenv(x) before ?, && or ||, and a bare getenv(x) in a condition, not only getenv(x) != NULL. RX_VK_DESC_REUSE=1 was level 1, the Descriptor reuse row's Off, where RX_VK_DESC_SLOTGEN=1 used to mean On. It now has the row's values (0 Off, 1 On, through menu_env_on and the env_on rule), and never for no reuse at all (the old RX_VK_NO_DESC_REUSE). test_menu_env_on cuts desc_reuse_level and checks the levels the game takes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eset saves the rows it sets Players who followed the old readme have RX_VK_ASTC_RGBA8=1 in hookshot_env.txt, which goes over the row: the row now shows that value, fixed, with a line saying to remove it, and the readme says so too. A preset or the Upscaler's snap that sets a row the environment held to the very value it forced now saves that value, not the player's older one. RX_VK_PACE, HOS_REALTIME_STEP and HOS_DYNRES read their value with the switches' rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-no-polls build mentions Nothing reads RX_VK_DUMP_AT or RX_VK_DUMP_TEX any more (RX_VK_DUMP takes the present now), and the build no longer has --no-polls (nx2/lift.py by hand still does). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
nullifyshot, thanks for the careful read. Here is where each point ended up: Before merge
Minor
Suggestion (splitting out the env_on rule): I kept it in this PR. The merges and CI fixes have already been done on top of it. Splitting it now would mean re-resolving the same files twice. If a maintainer would still rather have it separate, I can split it. jarynclouatre: Thanks, both removed. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…elease test_capture_copy_helpers, test_texture_handle_banks and test_taau_env_lookup cut code that now reads RX_VK_LOW_SPEC, RX_VK_HANDLES_LIVE and RX_VK_NO_F16 with env_on: they get hookshot_env.h first (test_taau_env_lookup's env_on reads its own stand-in environment). test_rumble_menu and test_framegen_preset get an env_release stand-in for set_row and apply_preset, and test_program_answer_domains expects RX_VK_HANDLES_LIVE=0 off, as the other switches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dGPU has its own fast memory and a slower bus to system RAM; stock mirrors every guest pool into VRAM and keeps both copies coherent with captures, shadows and write-backs. At 4K that coherence is CPU-bound: the write-back diff compares ~3.1 GB per 600 submits and mirror syncs move ~137k dirty pages, for 35.6 fps where 1080p runs 60 (issue NotRexed#2099). Under RX_VK_DGPU=1 a pool's first binding makes no mirror (MIRROR_REFUSED, the state iGPU runs use today): reads take the lock-free TLS path and GPU reads/writes land in the imported guest memory, coherent by construction - no mirror sync, no write-back. Every 600-submit window the heaviest guest-memory-read pool with >=1 MB of reads and not write-dominated runs a measured trial (120 frames direct -> 12 settle, held until the mirror is really on -> 120 mirrored, on the recordings' own timestamps); the mirror stays only when the win is both >10% (RX_VK_DGPU_GAIN) and >0.25 ms (RX_VK_DGPU_FLOOR) on present-to-present frame time, so the verdict sees the mirror's CPU cost and its PCIe benefit alike. Trials whose windows did not measure the same workload (zero binds, a >2x bind shift, a mirror that never came) abort with no verdict and no skip line; a pool that aborts three times sits out the session, and iGPUs run no trials. A lost verdict persists to hookshot_mirror_skip.txt beside the game's own state files, keyed to the device and driver (a foreign or header-less file is ignored and re-trialled). RX_VK_DGPU_TRIALS=0 is pure direct. Trials need serial replay (documented); under RX_VK_PARALLEL the policy runs trial-less. Unset = stock exactly: the added counters and logging are gated on the switch. Refused pools read uniform buffers as submitted (NotRexed#1795's per-pool form: the snapshot engages for mirror-less pools at the read, trial-won pools keep in-place mirror binds), so the artifact class the issue documents on mirror-less GPUs is covered under the policy too. Measured on RX 9070 XT / RADV, 4K, Uncapped frame cap, warp-normalized adjacent pairs (Hyrule Field): 70.2->78.0, 71.0->77.0, 59.6->76.6 fps (+9.7% median), draw parity 705 vs 709, GPU 9.2 ms of a ~14 ms frame. Mirror-sync dirty pages 136,872 -> 0; write-back diff 3.06-3.13 GB -> 330 MB or less per 600 submits. Review round 2 (external): RX_VK_DGPU_TRIALS=0's predicate was inverted and now matches the docs (=0 is pure direct); the window's read counter moved off a shared map into the pool (dgpu_read, both direct paths); Settle holds until the mirror is actually live; zero-bind baselines and >2x bind drift between the direct and mirrored windows abort the trial un-blacklisted; keep_line refuses signed and trailing-garbage lines. Review round 3 (hckrman101, Joey-Magnus): refused pools read UBs as submitted (NotRexed#1795 per-pool, live-smoked at 1.26M copies fault-free); verdicts decide on present-to-present frame time instead of GPU-only time (the mirror's CPU and upload-buffer cost is what the frame pays); the policy runs trial-less under RX_VK_PARALLEL and its fast-path counters are parallel-safe; no trials on iGPUs and a 3-abort cap; the skip list carries a device+driver header and ignores foreign files; the test driver uses {cxx}. Rebased onto main's env_on switch rule (NotRexed#2042) and the split-submission fence work (NotRexed#2419). Co-authored-by: GLM-5.3-Flash <glm-5.3-flash@opencode.noreply.github.com>
A dGPU has its own fast memory and a slower bus to system RAM; stock mirrors every guest pool into VRAM and keeps both copies coherent with captures, shadows and write-backs. At 4K that coherence is CPU-bound: the write-back diff compares ~3.1 GB per 600 submits and mirror syncs move ~137k dirty pages, for 35.6 fps where 1080p runs 60 (issue NotRexed#2099). Under RX_VK_DGPU=1 a pool's first binding makes no mirror (MIRROR_REFUSED, the state iGPU runs use today): reads take the lock-free TLS path and GPU reads/writes land in the imported guest memory, coherent by construction - no mirror sync, no write-back. Every 600-submit window the heaviest guest-memory-read pool with >=1 MB of reads and not write-dominated runs a measured trial (120 frames direct -> 12 settle, held until the mirror is really on -> 120 mirrored, on the recordings' own timestamps); the mirror stays only when the win is both >10% (RX_VK_DGPU_GAIN) and >0.25 ms (RX_VK_DGPU_FLOOR) on present-to-present frame time, so the verdict sees the mirror's CPU cost and its PCIe benefit alike. Trials whose windows did not measure the same workload (zero binds, a >2x bind shift, a mirror that never came) abort with no verdict and no skip line; a pool that aborts three times sits out the session, and iGPUs run no trials. A lost verdict persists to hookshot_mirror_skip.txt beside the game's own state files, keyed to the device and driver (a foreign or header-less file is ignored and re-trialled). RX_VK_DGPU_TRIALS=0 is pure direct. Trials need serial replay (documented); under RX_VK_PARALLEL the policy runs trial-less. Unset = stock exactly: the added counters and logging are gated on the switch. Refused pools read uniform buffers as submitted (NotRexed#1795's per-pool form: the snapshot engages for mirror-less pools at the read, trial-won pools keep in-place mirror binds), so the artifact class the issue documents on mirror-less GPUs is covered under the policy too. Measured on RX 9070 XT / RADV, 4K, Uncapped frame cap, warp-normalized adjacent pairs (Hyrule Field): 70.2->78.0, 71.0->77.0, 59.6->76.6 fps (+9.7% median), draw parity 705 vs 709, GPU 9.2 ms of a ~14 ms frame. Mirror-sync dirty pages 136,872 -> 0; write-back diff 3.06-3.13 GB -> 330 MB or less per 600 submits. Review round 2 (external): RX_VK_DGPU_TRIALS=0's predicate was inverted and now matches the docs (=0 is pure direct); the window's read counter moved off a shared map into the pool (dgpu_read, both direct paths); Settle holds until the mirror is actually live; zero-bind baselines and >2x bind drift between the direct and mirrored windows abort the trial un-blacklisted; keep_line refuses signed and trailing-garbage lines. Review round 3 (hckrman101, Joey-Magnus): refused pools read UBs as submitted (NotRexed#1795 per-pool, live-smoked at 1.26M copies fault-free); verdicts decide on present-to-present frame time instead of GPU-only time (the mirror's CPU and upload-buffer cost is what the frame pays); the policy runs trial-less under RX_VK_PARALLEL and its fast-path counters are parallel-safe; no trials on iGPUs and a 3-abort cap; the skip list carries a device+driver header and ignores foreign files; the test driver uses {cxx}. Rebased onto main's env_on switch rule (NotRexed#2042) and the split-submission fence work (NotRexed#2419). Co-authored-by: GLM-5.3-Flash <glm-5.3-flash@opencode.noreply.github.com>
…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>
Upstream's NotRexed#2042 gave the switches one rule (env_on: unset, empty, 0, false, off or no all mean off). RX_VK_DGPU joins it; the trials' switch keeps its default-on ternary (env_on has no default-on form), and the two numeric knobs refuse an empty value like env_int does. Assisted-by: GLM-5.3-Flash (OpenCode) Co-authored-by: GLM-5.3-Flash (OpenCode) <glm-5.3-flash@opencode.noreply.github.com>
A dGPU has its own fast memory and a slower bus to system RAM; stock mirrors every guest pool into VRAM and keeps both copies coherent with captures, shadows and write-backs. At 4K that coherence is CPU-bound: the write-back diff compares ~3.1 GB per 600 submits and mirror syncs move ~137k dirty pages, for 35.6 fps where 1080p runs 60 (issue NotRexed#2099). Under RX_VK_DGPU=1 a pool's first binding makes no mirror (MIRROR_REFUSED, the state iGPU runs use today): reads take the lock-free TLS path and GPU reads/writes land in the imported guest memory, coherent by construction - no mirror sync, no write-back. Every 600-submit window the heaviest guest-memory-read pool with >=1 MB of reads and not write-dominated runs a measured trial (120 frames direct -> 12 settle, held until the mirror is really on -> 120 mirrored, on the recordings' own timestamps); the mirror stays only when the win is both >10% (RX_VK_DGPU_GAIN) and >0.25 ms (RX_VK_DGPU_FLOOR) on present-to-present frame time, so the verdict sees the mirror's CPU cost and its PCIe benefit alike. Trials whose windows did not measure the same workload (zero binds, a >2x bind shift, a mirror that never came) abort with no verdict and no skip line; a pool that aborts three times sits out the session, and iGPUs run no trials. A lost verdict persists to hookshot_mirror_skip.txt beside the game's own state files, keyed to the device and driver (a foreign or header-less file is ignored and re-trialled). RX_VK_DGPU_TRIALS=0 is pure direct. Trials need serial replay (documented); under RX_VK_PARALLEL the policy runs trial-less. Unset = stock exactly: the added counters and logging are gated on the switch. Refused pools read uniform buffers as submitted (NotRexed#1795's per-pool form: the snapshot engages for mirror-less pools at the read, trial-won pools keep in-place mirror binds), so the artifact class the issue documents on mirror-less GPUs is covered under the policy too. Measured on RX 9070 XT / RADV, 4K, Uncapped frame cap, warp-normalized adjacent pairs (Hyrule Field): 70.2->78.0, 71.0->77.0, 59.6->76.6 fps (+9.7% median), draw parity 705 vs 709, GPU 9.2 ms of a ~14 ms frame. Mirror-sync dirty pages 136,872 -> 0; write-back diff 3.06-3.13 GB -> 330 MB or less per 600 submits. Review round 2 (external): RX_VK_DGPU_TRIALS=0's predicate was inverted and now matches the docs (=0 is pure direct); the window's read counter moved off a shared map into the pool (dgpu_read, both direct paths); Settle holds until the mirror is actually live; zero-bind baselines and >2x bind drift between the direct and mirrored windows abort the trial un-blacklisted; keep_line refuses signed and trailing-garbage lines. Review round 3 (hckrman101, Joey-Magnus): refused pools read UBs as submitted (NotRexed#1795 per-pool, live-smoked at 1.26M copies fault-free); verdicts decide on present-to-present frame time instead of GPU-only time (the mirror's CPU and upload-buffer cost is what the frame pays); the policy runs trial-less under RX_VK_PARALLEL and its fast-path counters are parallel-safe; no trials on iGPUs and a 3-abort cap; the skip list carries a device+driver header and ignores foreign files; the test driver uses {cxx}. Rebased onto main's env_on switch rule (NotRexed#2042) and the split-submission fence work (NotRexed#2419); the trials' functions sit outside the write-back observation's compile-span check (NotRexed#2131) and the GCN mirror-less default (NotRexed#2343) composes unchanged (on GCN the switch runs trial-less direct plus the UB snapshot). Co-authored-by: GLM-5.3-Flash <glm-5.3-flash@opencode.noreply.github.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>
…ches' frame said Review notes (Joey-Magnus, hckrman101): - RX_VK_REFLEX_SUBMIT_IDS is read with env_value_off (hookshot_env.h, the switches' rule since NotRexed#2042) instead of atoi(...) == 0, which took =on or =true as off. - vk_reflex.cpp and the batch start in vk_replay.cpp say how a batch is credited: to the frame of its first command buffer, read when the batch starts, so one spanning two game frames counts as the first's and one begun before the frame's sleep returned goes untagged. Batching is on by default only with RADV, which has no Reflex. - docs/development.md: the switch in the Reflex row. - test_reflex_swapchain_lifetime.py runs its fixture with RX_VK_REFLEX_SUBMIT_IDS=on (tags on; the atoi reading fails it, check 21) and with =0 (untagged). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rule (env_value_off / env_on, NotRexed#2042) Review: stage_on() used atoi, so RX_VK_SHADOW_STAGE=on turned staging off. Assisted-by: Claude Opus 5.5
A dGPU has its own fast memory and a slower bus to system RAM; stock mirrors every guest pool into VRAM and keeps both copies coherent with captures, shadows and write-backs. At 4K that coherence is CPU-bound: the write-back diff compares ~3.1 GB per 600 submits and mirror syncs move ~137k dirty pages, for 35.6 fps where 1080p runs 60 (issue NotRexed#2099). Under RX_VK_DGPU=1 a pool's first binding makes no mirror (MIRROR_REFUSED, the state iGPU runs use today): reads take the lock-free TLS path and GPU reads/writes land in the imported guest memory, coherent by construction - no mirror sync, no write-back. Every 600-submit window the heaviest guest-memory-read pool with >=1 MB of reads and not write-dominated runs a measured trial (120 frames direct -> 12 settle, held until the mirror is really on -> 120 mirrored, on the recordings' own timestamps); the mirror stays only when the win is both >10% (RX_VK_DGPU_GAIN) and >0.25 ms (RX_VK_DGPU_FLOOR) on present-to-present frame time, so the verdict sees the mirror's CPU cost and its PCIe benefit alike. Trials whose windows did not measure the same workload (zero binds, a >2x bind shift, a mirror that never came) abort with no verdict and no skip line; a pool that aborts three times sits out the session, and iGPUs run no trials. A lost verdict persists to hookshot_mirror_skip.txt beside the game's own state files, keyed to the device and driver (a foreign or header-less file is ignored and re-trialled). RX_VK_DGPU_TRIALS=0 is pure direct. Trials need serial replay (documented); under RX_VK_PARALLEL the policy runs trial-less. Unset = stock exactly: the added counters and logging are gated on the switch. Refused pools read uniform buffers as submitted (NotRexed#1795's per-pool form: the snapshot engages for mirror-less pools at the read, trial-won pools keep in-place mirror binds), so the artifact class the issue documents on mirror-less GPUs is covered under the policy too. Measured on RX 9070 XT / RADV, 4K, Uncapped frame cap, warp-normalized adjacent pairs (Hyrule Field): 70.2->78.0, 71.0->77.0, 59.6->76.6 fps (+9.7% median), draw parity 705 vs 709, GPU 9.2 ms of a ~14 ms frame. Mirror-sync dirty pages 136,872 -> 0; write-back diff 3.06-3.13 GB -> 330 MB or less per 600 submits. Review round 2 (external): RX_VK_DGPU_TRIALS=0's predicate was inverted and now matches the docs (=0 is pure direct); the window's read counter moved off a shared map into the pool (dgpu_read, both direct paths); Settle holds until the mirror is actually live; zero-bind baselines and >2x bind drift between the direct and mirrored windows abort the trial un-blacklisted; keep_line refuses signed and trailing-garbage lines. Review round 3 (hckrman101, Joey-Magnus): refused pools read UBs as submitted (NotRexed#1795 per-pool, live-smoked at 1.26M copies fault-free); verdicts decide on present-to-present frame time instead of GPU-only time (the mirror's CPU and upload-buffer cost is what the frame pays); the policy runs trial-less under RX_VK_PARALLEL and its fast-path counters are parallel-safe; no trials on iGPUs and a 3-abort cap; the skip list carries a device+driver header and ignores foreign files; the test driver uses {cxx}. Follow-up fix: the drift check counted served volume, so a working mirror's read collapse voided 40+ trials a run and trials never converged; binds now count once at the serving path and the drift rule compares the scene's pool set, with a three-no-verdict cap. Rebased onto main's env_on switch rule (NotRexed#2042) and the split-submission fence work (NotRexed#2419); the trials' functions sit outside the write-back observation's compile-span check (NotRexed#2131) and the GCN mirror-less default (NotRexed#2343) composes unchanged (on GCN the switch runs trial-less direct plus the UB snapshot). Co-authored-by: GLM-5.3-Flash <glm-5.3-flash@opencode.noreply.github.com>
Upstream's NotRexed#2042 gave the switches one rule (env_on: unset, empty, 0, false, off or no all mean off). RX_VK_DGPU joins it; the trials' switch keeps its default-on ternary (env_on has no default-on form), and the two numeric knobs refuse an empty value like env_int does. Assisted-by: GLM-5.3-Flash (OpenCode) Co-authored-by: GLM-5.3-Flash (OpenCode) <glm-5.3-flash@opencode.noreply.github.com>
A dGPU has its own fast memory and a slower bus to system RAM; stock mirrors every guest pool into VRAM and keeps both copies coherent with captures, shadows and write-backs. At 4K that coherence is CPU-bound: the write-back diff compares ~3.1 GB per 600 submits and mirror syncs move ~137k dirty pages, for 35.6 fps where 1080p runs 60 (issue NotRexed#2099). Under RX_VK_DGPU=1 a pool's first binding makes no mirror (MIRROR_REFUSED, the state iGPU runs use today): reads take the lock-free TLS path and GPU reads/writes land in the imported guest memory, coherent by construction - no mirror sync, no write-back. Every 600-submit window the heaviest guest-memory-read pool with >=1 MB of reads and not write-dominated runs a measured trial (120 frames direct -> 12 settle, held until the mirror is really on -> 120 mirrored, on the recordings' own timestamps); the mirror stays only when the win is both >10% (RX_VK_DGPU_GAIN) and >0.25 ms (RX_VK_DGPU_FLOOR) on present-to-present frame time, so the verdict sees the mirror's CPU cost and its PCIe benefit alike. Trials whose windows did not measure the same workload (zero binds, a >2x bind shift, a mirror that never came) abort with no verdict and no skip line; a pool that aborts three times sits out the session, and iGPUs run no trials. A lost verdict persists to hookshot_mirror_skip.txt beside the game's own state files, keyed to the device and driver (a foreign or header-less file is ignored and re-trialled). RX_VK_DGPU_TRIALS=0 is pure direct. Trials need serial replay (documented); under RX_VK_PARALLEL the policy runs trial-less. Unset = stock exactly: the added counters and logging are gated on the switch. Refused pools read uniform buffers as submitted (NotRexed#1795's per-pool form: the snapshot engages for mirror-less pools at the read, trial-won pools keep in-place mirror binds), so the artifact class the issue documents on mirror-less GPUs is covered under the policy too. Measured on RX 9070 XT / RADV, 4K, Uncapped frame cap, warp-normalized adjacent pairs (Hyrule Field): 70.2->78.0, 71.0->77.0, 59.6->76.6 fps (+9.7% median), draw parity 705 vs 709, GPU 9.2 ms of a ~14 ms frame. Mirror-sync dirty pages 136,872 -> 0; write-back diff 3.06-3.13 GB -> 330 MB or less per 600 submits. Review round 2 (external): RX_VK_DGPU_TRIALS=0's predicate was inverted and now matches the docs (=0 is pure direct); the window's read counter moved off a shared map into the pool (dgpu_read, both direct paths); Settle holds until the mirror is actually live; zero-bind baselines and >2x bind drift between the direct and mirrored windows abort the trial un-blacklisted; keep_line refuses signed and trailing-garbage lines. Review round 3 (hckrman101, Joey-Magnus): refused pools read UBs as submitted (NotRexed#1795 per-pool, live-smoked at 1.26M copies fault-free); verdicts decide on present-to-present frame time instead of GPU-only time (the mirror's CPU and upload-buffer cost is what the frame pays); the policy runs trial-less under RX_VK_PARALLEL and its fast-path counters are parallel-safe; no trials on iGPUs and a 3-abort cap; the skip list carries a device+driver header and ignores foreign files; the test driver uses {cxx}. Follow-up fix: the drift check counted served volume, so a working mirror's read collapse voided 40+ trials a run and trials never converged; binds now count once at the serving path and the drift rule compares the scene's pool set, with a three-no-verdict cap. Rebased onto main's env_on switch rule (NotRexed#2042) and the split-submission fence work (NotRexed#2419); the trials' functions sit outside the write-back observation's compile-span check (NotRexed#2131) and the GCN mirror-less default (NotRexed#2343) composes unchanged (on GCN the switch runs trial-less direct plus the UB snapshot). After the presenter-thread refactor (NotRexed#2325) the present-to-present tick lives in present_finish, which every made present passes exactly once in either threading mode, with a headless twin so offscreen runs stay measurable. Co-authored-by: GLM-5.3-Flash <glm-5.3-flash@opencode.noreply.github.com>
…he loop It was read with atoi, which takes any value that is not a number for 0: RX_VK_NATIVE_FADD=on turned the native float add off. native_fadd_wanted reads it with env_value_off (gpu/vk/compat/hookshot_env.h, NotRexed#2042) now: 0, false, off, no or empty keep the compare-exchange loop, any other value asks for the native add, unset as before. tests/test_native_float_atomic_add.py compiles native_fadd_wanted alone against 13 values. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dGPU has its own fast memory and a slower bus to system RAM; stock mirrors every guest pool into VRAM and keeps both copies coherent with captures, shadows and write-backs. At 4K that coherence is CPU-bound: the write-back diff compares ~3.1 GB per 600 submits and mirror syncs move ~137k dirty pages, for 35.6 fps where 1080p runs 60 (issue NotRexed#2099). Under RX_VK_DGPU=1 a pool's first binding makes no mirror (MIRROR_REFUSED, the state iGPU runs use today): reads take the lock-free TLS path and GPU reads/writes land in the imported guest memory, coherent by construction - no mirror sync, no write-back. Every 600-submit window the heaviest guest-memory-read pool with >=1 MB of reads and not write-dominated runs a measured trial (120 frames direct -> 12 settle, held until the mirror is really on -> 120 mirrored, on the recordings' own timestamps); the mirror stays only when the win is both >10% (RX_VK_DGPU_GAIN) and >0.25 ms (RX_VK_DGPU_FLOOR) on present-to-present frame time, so the verdict sees the mirror's CPU cost and its PCIe benefit alike. Trials whose windows did not measure the same workload (zero binds, a >2x bind shift, a mirror that never came) abort with no verdict and no skip line; a pool that aborts three times sits out the session, and iGPUs run no trials. A lost verdict persists to hookshot_mirror_skip.txt beside the game's own state files, keyed to the device and driver (a foreign or header-less file is ignored and re-trialled). RX_VK_DGPU_TRIALS=0 is pure direct. Trials need serial replay (documented); under RX_VK_PARALLEL the policy runs trial-less. Unset = stock exactly: the added counters and logging are gated on the switch. Refused pools read uniform buffers as submitted (NotRexed#1795's per-pool form: the snapshot engages for mirror-less pools at the read, trial-won pools keep in-place mirror binds), so the artifact class the issue documents on mirror-less GPUs is covered under the policy too. Measured on RX 9070 XT / RADV, 4K, Uncapped frame cap, warp-normalized adjacent pairs (Hyrule Field): 70.2->78.0, 71.0->77.0, 59.6->76.6 fps (+9.7% median), draw parity 705 vs 709, GPU 9.2 ms of a ~14 ms frame. Mirror-sync dirty pages 136,872 -> 0; write-back diff 3.06-3.13 GB -> 330 MB or less per 600 submits. Review round 2 (external): RX_VK_DGPU_TRIALS=0's predicate was inverted and now matches the docs (=0 is pure direct); the window's read counter moved off a shared map into the pool (dgpu_read, both direct paths); Settle holds until the mirror is actually live; zero-bind baselines and >2x bind drift between the direct and mirrored windows abort the trial un-blacklisted; keep_line refuses signed and trailing-garbage lines. Review round 3 (hckrman101, Joey-Magnus): refused pools read UBs as submitted (NotRexed#1795 per-pool, live-smoked at 1.26M copies fault-free); verdicts decide on present-to-present frame time instead of GPU-only time (the mirror's CPU and upload-buffer cost is what the frame pays); the policy runs trial-less under RX_VK_PARALLEL and its fast-path counters are parallel-safe; no trials on iGPUs and a 3-abort cap; the skip list carries a device+driver header and ignores foreign files; the test driver uses {cxx}. Follow-up fix: the drift check counted served volume, so a working mirror's read collapse voided 40+ trials a run and trials never converged; binds now count once at the serving path and the drift rule compares the scene's pool set, with a three-no-verdict cap. The set rule still voided every trial in a streaming scene, so anchors now match by guest range (a recycled pool is the same anchor), anchor at 4x the floor, void only when a substantial fraction of the scene moves, and the candidate floor rose to 256 MB of reads a window (the verdict floor is unreachable below it - a trial there is churn by construction). Rebased onto main's env_on switch rule (NotRexed#2042) and the split-submission fence work (NotRexed#2419); the trials' functions sit outside the write-back observation's compile-span check (NotRexed#2131) and the GCN mirror-less default (NotRexed#2343) composes unchanged (on GCN the switch runs trial-less direct plus the UB snapshot). After the presenter-thread refactor (NotRexed#2325) the present-to-present tick lives in present_finish, which every made present passes exactly once in either threading mode, with a headless twin so offscreen runs stay measurable. Co-authored-by: GLM-5.3-Flash <glm-5.3-flash@opencode.noreply.github.com>
…ess (#2347) **What this is for:** a player who meets a problem presses one row in the Insert menu, **Gameplay → Bug report**, and gets a folder with everything the game problem form asks for: the form's answers in `report.txt` (version, the map he is in, graphics card and driver version, CPU, RAM and system, the options changed and the switches set), this run's log and the run before's (the crashed one after a restart) with his home folder and user name taken out, and a picture of the game. Maintainers get reports with the log and the hardware from the start instead of asking for them. **What is measured:** on Linux (Omarchy, an Arch Linux; RTX 4090, NVIDIA 615.78.08), on `main` `ba6eef70`: the new test passes with g++ 16 and clang 23 and fails on `main`; its cases for the review's points (NUL bytes in a log, a picture that comes late, which switch values are written, the reason a folder cannot be made); `tests/run_all.py`; headless runs of this head that pressed the row (one pressed it in Kokiri Forest, one after a `kill -9` and a restart: both reports written, read back by a script against the logs they came from (no file holds the home folder or the user name; each log copy is the original line for line apart from those two), the after-crash report's `hookshot_log.1.txt` is the killed start's whole log, and its picture came through #926's `shot_ask`); the length GitHub takes for the link, signed out. **Not measured:** Windows at run time (compiled and linked only), a windowed run, the link opened in a browser (signed out or in), AMD and Intel drivers' strings, a Steam Deck. Refs #2346 (closed by the maintainer: tracked in this pull request). ## Problem - The game problem form (`.github/ISSUE_TEMPLATE/1-game-problem.yml`) requires the version, where in the game, the graphics card **and driver version**, CPU, RAM and operating system, and `hookshot_log.txt` (and `hookshot_log.1.txt` "if you started the game again after the problem"). The player has to find each one by hand. - `hookshot_log.txt` answers only part of it: its first line has the version, but the driver is only Vulkan's packed number (`vk: device 0: NVIDIA GeForce RTX 4090, Vulkan 1.4, driver 0x99d38200, type 2`), there is no line for the system, the CPU or the options changed, and nothing logged where the player was (the host reads `Sequence_CurrentBanc` only for the debug tools). - After a crash a player starts the game again, and the crashed run's log becomes `hookshot_log.1.txt` (`host/hos_main.c`, the rotation added because players sent the new run's log and the crash was gone). It still has to be found and attached. - The logs carry the home folder and the user name in their paths (`fs: mounted content: -> <home>/...`, `vk: pipeline cache <home>/...`), which a player may not want to post. - So reports arrive without the log or the hardware, and `docs/workflow.md` scores such a report at confidence 0.4 and asks for the log first. This gathers what CONTRIBUTING.md already asks ("Include the version, your hardware, where you were and `hookshot_log.txt`"; "Report the version you actually ran: ... the first line of `hookshot_log.txt`"). It changes nothing about where reports go. ## What changed - `gpu/vk/vk_bug_report.h`, `vk_bug_report.cpp` (new, 59 + 602 lines): the packer, without Vulkan and without the menu, so the test compiles it alone. It writes `bug-reports/<YYYY-MM-DD_HH-MM-SS>/` in the game folder on a thread of its own: - `report.txt`: the form's questions under the form's own `### ` headings, so `scripts/hardware_report.py`'s `section()` reads it as an issue: **Hookshot version** (`HOOKSHOT_VERSION`, as the log's first line); **Where in the game** (the last `map:` line of this run's log, and the run before's, and a request to add what you were doing); **Graphics card and driver version** (the device and `VkPhysicalDeviceDriverProperties`' `driverName` + `driverInfo`, e.g. `NVIDIA GeForce RTX 4090, NVIDIA 615.78.08`); **CPU, RAM and operating system** (the CPU's brand string and threads, RAM, `os-release`, desktop and session type on Linux; `RtlGetVersion` and Wine on Windows); **Changes from the defaults** (each Insert-menu row not at its default as the menu shows it; a microphone chosen by its name is not named; then the switches set, below); **hookshot_log.txt** (what is attached, the run before's version and date, each log's first `FATAL`, `CRASH` or `device lost` line); **Screenshots or video**; then a link that opens the form with version, where, GPU, system and settings filled in (GitHub fills issue-form fields from query parameters named by the fields' ids). - **The switches.** CONTRIBUTING.md lists `hookshot_env.txt` among the files never to share, so the report never copies it, and of the switches set (the names `hookshot_env.txt` takes: `RX_`, `HOS_`, `HOOKSHOT_`, `EDEN_`, `NVN_`, `SDL_`; and what hooks other code in: `VK_INSTANCE_LAYERS`, `MANGOHUD`, `ENABLE_VKBASALT`, `LD_PRELOAD` ...) it writes each name, and the value only when it cannot hold anything private: a number of at most 12 characters, a size such as `1920x1080`, or on/off, true/false, yes/no, and never for a name with `TOKEN`, `KEY`, `ROOM`, `SECRET`, `PASS`, `SERVER` or `USER` in it. Every other value reads `(value left out)`: paths, addresses, scripts. Other variables are not listed at all. - **The link** stays within 2000 characters and carries no switch value: the options changed and the switches' names, or, when the names do not fit, how many switches are set. A player who is not signed in is sent to GitHub's sign-in page with the whole link inside it, and that page failed for links from about 3500 characters of escaped text (table below). - `hookshot_log.txt` (this run's log up to the press) and `hookshot_log.1.txt` (the run before's; when this run logs elsewhere, e.g. from a terminal, the game folder's `hookshot_log.txt`). Read by length (`fread`, split on newlines), so NUL bytes, which a run ended by a power cut or a hard kill can leave, are copied as they are. Each line: the home folder (a whole folder; on Windows either slash and any case) becomes `~` or `%USERPROFILE%`, the user's name as a folder of any path `<user>`. A log over 10 MB keeps its first MB and its last 9 MB (GitHub takes files up to 25 MB). - `picture.png`: 1280x720, the game's next frame, without the menu (below). If none comes within 3 s, the report goes without it and says so; a picture that comes later is deleted again. - A folder that cannot be made is reported with the system's reason (`GetLastError()` on Windows, `errno` elsewhere). - `gpu/vk/vk_menu.cpp` (+89): the row before Restart and Quit (`kBugReportRow = kRestartRow - 1`), its help (what goes in, that no saves or game files go in and nothing is sent), its value (Press to save / Saving... / Saved / Not saved), Save in both prompt lines (the full menu's and the compact bar's), the press, `changed_rows_text()` (rows against a default-constructed `Options`), `bug_report_save()`, and the picture's ask, `bug_report_picture()`. It follows Save my pipeline list's pattern (#2447): its value in `row_value_cell`, its line under the help in `draw_options` once pressed (where the folder is and how to send it: File Explorer on Windows, a file manager on Linux; on a Steam Deck, Desktop Mode), and room for that line's longest form in `measure_help`. The game opens no browser or file manager: Game Mode on a Deck has neither. - `gpu/vk/vk_replay.cpp` (+2): `shot_ask_next()`, which calls #926's `shot_ask(path, -1, done, ctx)` from outside its anonymous namespace (declared in `vk_internal.h`). The report's picture is the control bridge's kind of shot: an image and a buffer of its own (1280x720, made at the first ask), copied from `src`, the game's frame, which the menu is never drawn into (`menu_record_image` draws only into the screenshots' image), read back once the GPU is past it and written on a thread. No other line of `do_present` changes. - `gpu/vk/vk_main.cpp` (+11), `vk_internal.h` (+6): `device_report_text()`, and the declaration above. - `host/hos_main.c` (+12): `map: <Sequence_CurrentBanc> (child|adult)` when the map or the age changes, in `oot_inventory_refresh` (on Link's update, at most every 500 ms, where the age is already read for the Debug tab). It is where the report's place comes from, and it puts the place into every log. The names are the game's own map names, which the debug tools' teleport lines already print. - `readme.md`: the row in the menu table, a paragraph on what it saves and which values it leaves out, a pointer from "If the problem continues". - `tests/test_bug_report.py` (new), and `tests/test_menu_tune.py`: its check of the compact bar's prompt line now reads the line with the Bug report in it (the check is otherwise unchanged). - Unchanged: nothing is sent anywhere; no saves, no game files, not `hookshot_options.ini` or `hookshot_env.txt` themselves; no new option or switch; the logs' own rotation; the screenshot path. ## How to try it Insert menu (Insert, F1 or Esc, or the menu button) → **Gameplay** → **Bug report** → Enter or A. The row reads Saved and its help names the folder: `bug-reports/<date>_<time>` in the game folder, with `report.txt`, `hookshot_log.txt`, `hookshot_log.1.txt` and `picture.png`. ## How it was tested **2026-11-11, after the two reviews, head `a75f3ca5` on `main` `ba6eef70`.** Rebased twice today: onto #926, then onto #2042, #2378, #2447 and the rest. The conflicts, each kept on both sides: #926's `shot_ask_record` beside the old readback (which is now gone); #2042's Debug rows where the Bug report row goes before Restart; #2378's `row_value_cell()` and #2447's share row, where the row's value cell and prompt went (the row now follows the share row's pattern, above); a forward declaration in `host/hos_main.c`. #2042's rules hold for the row: it has no value, so `preset_exempt` does not apply (`tests/test_preset_exempt.py` reads only rows with one), and the branch reads no on/off switch. #2042 also took `TOTK_` out of the names `hookshot_env.txt` takes; the report follows (the test compares the two lists). Environment: Omarchy (an Arch Linux), kernel 7.2.15-1-omarchy, RTX 4090 with NVIDIA 615.78.08, i9-13900K, 94 GB, g++ 16.2.1 and clang 23.1.3. - The new test on the branch (`python -I -B tests/test_bug_report.py`, with `--cxx g++` and with `--cxx clang++`; the same line from both): ``` bug report: ok (report.txt as the issue form, both logs redacted, the picture, the long log, the terminal start, switch values, NUL bytes, the folder's reason) ``` Against `main`'s source (the test copied into an export of `ba6eef70`; with both compilers): it fails at its first step, the menu row: ``` at = menu.index('{TAB_GAMEPLAY, "Bug report", nullptr') ValueError: substring not found ``` And the review's NUL case against the old log copy (`fgets` and `line.back()`, put back on its own): ``` bug report: the copy of a log with NUL bytes differs: b'[hos host] hookshot v2.1.0-rc.3-39-gbdfe09c (source 763271ad)\n[hos Main] fs: mounted content: -> ~/Games\n[hos Main] map: KokiriForest_Child (child)\n[hos Main] half a line' ``` It compiles `vk_bug_report.cpp` alone with a driver and writes reports from a made-up game folder inside a made-up home folder (`.../home/alice/Games/Hookshot/`), with paths of the home and of `/run/media/alice/...` in the logs, map lines and a `CRASH:` line in the run before's. It checks: the folder holds exactly the report, both logs and the picture; no file names the home folder, the user as a folder or the game folder's path (`alice123` and `Malice` stay); `hardware_report.py`'s `section`, `classify` and `os_of` read the version, `NVIDIA` / `RTX 40` and Linux from `report.txt`; the place is the last map, and the run before's; the switches: values kept (`HOS_HEADLESS=1`, `60`, `off`, `3840x2160`, `-1`, `1.5`) and left out (a path, a drive path, a menu script, an IP address, a `host:port`, a 13-digit number, and numbers under ROOM, KEY, SERVER, USER and PASSWORD names), none of those values anywhere in `report.txt`, `PATH` and a token not listed, no `%3D` in the link; the run before's crash line; the link's fields and its length (with 98 switches: within 2000, the options and the count in it, every switch in `report.txt`); the menu's way (a thread, one report at a time); no frame means no picture, said so; a picture that comes after the report went without it is deleted; a start from a terminal; two reports in one second; a 22 MB log cut to its first and last part with its middle's map still found; a log with a NUL in a line and 4 KB of NULs at its end copied byte for byte, the map after the NUL found; a folder that cannot be made, with the system's reason; the redaction's Windows cases (either slash, any case); this PC's system line. And the wiring by text: the row and what its help promises, Save in both prompt lines, the press, the picture asked of `shot_ask_next` and copied from the game's frame without the menu, the host's `map:` line, that the switches it lists are the names `hookshot_env.txt` takes (`env_line_allowed`'s), the form's labels and ids. Eight runs at once at load 280 all pass (the first version of the menu's case read the report's state too late under load in the full suite; it now holds the report until it has looked). - The checks the review named and those that read the menu's rows, on this head (with g++ and clang++ where they compile C++): `test_capture_dimensions.py`, `test_capture_slot_ownership.py`, `test_menu_env_overrides.py`, `test_menu_keys.py`, `test_pipeline_share.py`, `test_menu_tune.py`, `test_stick_dead_zone.py`, `test_preset_exempt.py` (62 rows), `test_menu_env_on.py`, `test_env_on.py`, `test_fps_stats.py`, `test_gpu_choice.py`, `test_menu_restart_slots.py`, `test_mouse_settings.py`: all pass. `vk_bug_report.cpp` alone with `-Wall -Wextra -Wpedantic -Werror`: no warning from either compiler. - `python -I -B tests/run_all.py` on Linux, on this head: ``` 440 passed, 1 failed, 20 skipped in 600 s (linux, 32 at once) ``` The one failure is `test_recorder_sync.py`, which ran out of its 600 s at load 250-320 (other work ran beside it) and touches nothing of this branch. An earlier run of the whole suite on this branch failed `test_bug_report.py` once under that load: its menu case read the report's state too late (fixed in the test, above). - Merged into the newest `main` (`fce53453`, nine commits later; it merges cleanly): `test_bug_report.py` (both compilers), `test_menu_tune.py`, `test_pipeline_share.py`, `test_menu_keys.py`, `test_menu_env_overrides.py`, `test_preset_exempt.py` (63 rows), `test_menu_restart_slots.py`, both capture tests: pass. `test_env_on.py` fails there and on `fce53453` alone (`HOS_FPS_LOG (host/oot_high_fps.c:15)`, from #2470), not from this branch. - Windows: `vk_bug_report.cpp` with a small driver compiles and links for x86_64-w64-mingw32 (clang 23 with llvm-mingw 20261104's headers and libraries, `-Wall -Wextra -Werror`: a PE32+ console executable). Not run. - In the game, on a build of this head's code (`v2.1.0-rc.3-177-g9c538c91 (source c4f2afbc)`; the head differs only in two test files), headless, RTX 4090, `RX_VK_PRECOMPILE=0` (shaders compiled when first used), load 200 to 320 (other work ran beside it), the save loaded into Kokiri Forest and the menu script pressing **Gameplay → Bug report**: | Run | What | Harness result | The report | |---|---|---|---| | br-menu2 | the row pressed 55-63 s after the first frame | exit 124 (its 120 s), fatal 0, crash 0, device lost 0, kernel driver lines 0 | `bug-reports/2026-11-11_16-58-06`: report.txt and both logs, **no picture**, and it says so. Rightly: the game showed no frame from 39 s to 107 s (the save loading, shaders compiled as they came, at load 300), took the scripted keys when frames came again, and stopped once more at 109 s | | br-crash3 | a first start killed with `kill -9` after 110 s, then a second start; the row pressed 63 s after its first frame | exit 124 (its 260 s), fatal 0, crash 0, device lost 0, kernel driver lines 0 | `bug-reports/2026-11-11_17-13-48`: report.txt, both logs (`hookshot_log.1.txt` is the killed start's whole log), picture.png (present 1772) | | br-menu3 | the row pressed 130-138 s after the first frame, in play | exit 124 (its 200 s), fatal 0, crash 0, device lost 0, kernel driver lines 0 | `bug-reports/2026-11-11_17-25-32`: report.txt, both logs, picture.png (present 3063, 138 s) | The after-crash run's log lines (the picture's path shortened): ``` [hos BaseProcExecutor5] map: KokiriForest_Child (child) [hos Presentation Thread] menu: bug report asked for [hos host] vk: present 1772 written to ~/Games/.../bug-reports/2026-11-11_17-13-48/picture.png (the control bridge) [hos host] menu: bug report written to bug-reports/2026-11-11_17-13-48: report.txt, hookshot_log.txt (245 KB), hookshot_log.1.txt (117 KB), picture.png ``` Its folder read back against the game folder's logs (`check_report.py`: each copy compared line by line with its original, with the home folder and the user name replaced as the game does): ``` files: hookshot_log.1.txt (119514 bytes), hookshot_log.txt (250199 bytes), picture.png (2765798 bytes), report.txt (3212 bytes) no file holds the home folder or the user name (any case, anywhere) [the checker's line, with the two values left out here] hookshot_log.txt: 1827 of the original's 2991 lines, 12 of them with the home or the user replaced, nothing else changed hookshot_log.1.txt: 991 of the original's 991 lines, 11 of them with the home or the user replaced, nothing else changed hardware_report.py: vendor and class ('NVIDIA', 'RTX 40'), OS Linux / SteamOS ok ``` br-menu2's and br-menu3's folders pass the same check (1387 of 1571 and 3230 of 3230 lines; 2861 of 3679 and 3230 of 3230 lines; only the home folder and the user name replaced). A picture's pixel data is not read as text: br-menu3's picture.png has the bytes `4d 4f 4f 4b 49` among its pixels (row 176), which an any-case byte search reads as a name; the PNG holds only IHDR, IDAT and IEND chunks. The host name is in no text file of either. - Pictures (headless, 1280x720; the first two are the harness's shots with the menu drawn in, the third the report's own file):  1. br-menu3, the row before the press: **Bug report** selected, **Press to save**, its help (what goes in; no saves or game files; nothing is sent), the prompt bar **Save Enter**. 2. After Enter: **Saved**, and the paragraph under the help names the folder (`bug-reports/2026-11-11_17-25-32`) and how to send it. 3. That report's `picture.png`, taken through `shot_ask` at present 3063: child Link and Navi on the path in Kokiri Forest, hearts and minimap; no menu, not dimmed.  4. br-crash3, after the `kill -9` and the restart: **Saved**, the folder `bug-reports/2026-11-11_17-13-48` under the help. 5. That report's `picture.png` (present 1772): Kokiri Forest from low down, facing the bright sky, hearts and minimap; no menu. - The `report.txt` the after-crash run wrote, as a player would attach it (the switches are the test harness's: see which values are written and which are left out): <details><summary>The report.txt that run wrote</summary> ```text Hookshot bug report, made by the Insert menu's Bug report on 2026-11-11 at 17:13. To send it: open https://github.com/NotRexed/Hookshot/issues/new/choose and choose Game problem (or the link at the end of this file, which fills in the answers below), copy the answers below into the fields of the same names, say what happened, and drag the files of this folder, this one too, into the issue. ### Hookshot version v2.1.0-rc.3-177-g9c538c91 (source c4f2afbc) ### Where in the game KokiriForest_Child (child) (The map this run's log names last: add what you were doing there.) ### Graphics card and driver version NVIDIA GeForce RTX 4090, NVIDIA 615.78.08 ### CPU, RAM and operating system 13th Gen Intel(R) Core(TM) i9-13900K (32 threads), 94 GB, Omarchy (Linux 7.2.15-1-omarchy), Hyprland (wayland) ### Changes from the defaults Every Insert menu option is at its default. Switches set (the environment, hookshot_env.txt; a value only when it is a number or on/off, and never for a name with TOKEN, KEY, ROOM, SECRET, PASS, SERVER or USER in it): HOOKSHOT_NO_UPDATE_CHECK=1 HOS_AUDIO=0 HOS_DISCORD=0 HOS_ENV_FILE=(value left out) HOS_HEADLESS=1 HOS_INPUT_BACKGROUND=0 HOS_INPUT_EXCLUSIVE=1 HOS_INPUT_FRAME_CLOCK=1 HOS_INPUT_SCRIPT=(value left out) HOS_PRESENT_TRACE=(value left out) RX_CPM=(value left out) RX_VK_DEVICE=0 RX_VK_FRAME_LOG=(value left out) RX_VK_MENU_SCRIPT=(value left out) RX_VK_PRECOMPILE=0 RX_VK_SHOTS=(value left out) RX_VK_SHOT_EVERY=300 RX_VK_SHOT_MENU=1 SDL_AUDIODRIVER=(value left out) SDL_AUDIO_DRIVER=(value left out) VK_DRIVER_FILES=(value left out) ### hookshot_log.txt hookshot_log.txt: this run's log as it was when the report was made, 245 KB. hookshot_log.1.txt: the run before's log (hookshot v2.1.0-rc.3-177-g9c538c91 (source c4f2afbc), last written 2026-11-11 17:12), 117 KB. In both, the home folder is written ~ and the user's name <user>. ### Screenshots or video picture.png: the game's picture when the report was made, without the menu. ### What this folder holds report.txt (this file), hookshot_log.txt, hookshot_log.1.txt, picture.png. Nothing else from this PC: no saves, no game files, not the options file itself. The link with the answers filled in: https://github.com/NotRexed/Hookshot/issues/new?template=1-game-problem.yml&version=v2.1.0-rc.3-177-g9c538c91%20%28source%20c4f2afbc%29&where=KokiriForest_Child%20%28child%29&gpu=NVIDIA%20GeForce%20RTX%204090%2C%20NVIDIA%20615.78.08&system=13th%20Gen%20Intel%28R%29%20Core%28TM%29%20i9-13900K%20%2832%20threads%29%2C%2094%20GB%2C%20Omarchy%20%28Linux%207.2.15-1-omarchy%29%2C%20Hyprland%20%28wayland%29&settings=Every%20Insert%20menu%20option%20is%20at%20its%20default.%0ASwitches%20set%20%28their%20values%20are%20in%20the%20attached%20report.txt%29%3A%20HOOKSHOT_NO_UPDATE_CHECK%2C%20HOS_AUDIO%2C%20HOS_DISCORD%2C%20HOS_ENV_FILE%2C%20HOS_HEADLESS%2C%20HOS_INPUT_BACKGROUND%2C%20HOS_INPUT_EXCLUSIVE%2C%20HOS_INPUT_FRAME_CLOCK%2C%20HOS_INPUT_SCRIPT%2C%20HOS_PRESENT_TRACE%2C%20RX_CPM%2C%20RX_VK_DEVICE%2C%20RX_VK_FRAME_LOG%2C%20RX_VK_MENU_SCRIPT%2C%20RX_VK_PRECOMPILE%2C%20RX_VK_SHOTS%2C%20RX_VK_SHOT_EVERY%2C%20RX_VK_SHOT_MENU%2C%20SDL_AUDIODRIVER%2C%20SDL_AUDIO_DRIVER%2C%20VK_DRIVER_FILES.%0A ``` </details> - The link's length, signed out (`curl`, dummy links to the form, 2026-11-10; nothing of this PC in them): | Link length | Padding | GitHub's answer | |---|---|---| | 1238 | the report's own link (then) | sign-in page, the link kept in `return_to` | | 1999, 2500, 2998, 3199 | `%20` | sign-in page, the link kept in `return_to` | | 3500 | `%20` | 400 | | 4000, 4500 | `%20` | 500 after the redirect | | 6000 | `%20` | sign-in page, but `return_to` dropped (the answers lost) | | 6000, 6400 | `x` | sign-in page, the link kept | | 6700, 7000, 7500, 7900 | `x` | 500 | | 8192 and more | `x` | 414 | ## Not tested - **Windows, at run time.** The report's GPU line there (`VkPhysicalDeviceDriverProperties` on a Windows driver), its CPU, RAM and system line (`__cpuid`, `GlobalMemoryStatusEx`, `RtlGetVersion`, Wine), the path of the log stderr writes to (`GetFinalPathNameByHandleA`), `GetLastError`'s reason, the redaction of real `C:\Users\...` logs, File Explorer: compiled and linked only. - **A windowed run.** Only headless runs: the picture taken with a window, HDR (a native HDR frame is copied to 8-bit as the control bridge's shots are) and frame generation were not seen. - **The link in a browser.** Not opened in a browser, signed out or signed in, and no issue was opened from it; only `curl` signed out (table above). That GitHub fills the form's fields from it is its documented behaviour, not seen here. - **A real crash's last lines in the game.** The after-crash run ended its first start with `kill -9`, which leaves no `CRASH:` line, and under that load the first start had not reached a map yet; the test covers the `CRASH:` line and the run before's map with made-up logs. - AMD and Intel drivers' `driverName`/`driverInfo` text (only NVIDIA's was read; `hardware_report.py` classifies typical RADV, AMD and Intel strings correctly when written by hand), a Steam Deck, the AppImage, and the Linux Builder's Play button (since #2279 it gives the game its `hookshot_log.txt` again, so the report finds this run's log there; not run here). ## Risks - **Something private in a report.** The home folder (as a whole folder; on Windows either slash and any case) and the user's name as a folder of any path are replaced in both logs; a switch's value is written only when it is a number, a size or on/off and its name holds none of the words above; other environment variables are not listed; a microphone chosen by name is not named; no saves, no options or env file, no game files go in. Guarded by the test (made-up home and user, Windows cases, `alice123` kept, values kept and left out) and, on this machine's real logs, by the read-back above. Not guarded: something private a log line carries in another form (a game folder outside the home named after a person, a machine name in a network path), or a future switch whose secret is a short number under a name without those words. - **A report written while the game is busy.** The press does only cheap work on the menu's thread (flushing the log, reading the environment, `/etc/os-release`, the device's properties, the menu's rows); copying the logs and writing the folder run on a thread of their own, one report at a time (a second press while it saves does nothing). This run's log is copied as far as it was when the copy began. The picture is waited for at most 3 s. - **The picture.** It shares #926's queue (16 asks at most) and its one readback: a control-bridge shot being read back delays the report's by a present or two. Without a report nothing changes on the present path. The control bridge's log line for a written shot also appears for the report's picture (`vk: present N written to ... (the control bridge)`). - **The `map:` line in every log.** It is written only when the map or Link's age changes, from `oot_inventory_refresh` (at most every 500 ms, where the age is already read). A log that a player posts now says where he was; the names are the game's own map names, which the debug tools' teleport lines already print. - A crash that leaves no line (a kill, a freeze, a power cut): the run before's log is attached, but nothing in it says how it ended. A game that stops before its menu can be used cannot make a report; the readme says to attach `hookshot_log.1.txt` by hand then. ## Overlaps `pr_overlap.py --branch bug-report` (run on this head, 2026-11-11): 79 open pull requests touch one of these files; 73 merge cleanly with it. The row stores nothing (no `Options` field, no ini key, no option id), so no option id can clash. Six conflict in this branch's files, each hunk read: - #2417 (the menu in 16 languages): it wraps the menu's texts in `i18n::tr(...)`, among them the lines of `measure_help`, `row_value_cell`'s branch for rows without a value, the help line in `draw_options` and the compact bar's prompt (and the same line in `tests/test_menu_tune.py`), where this branch adds the Bug report beside the share row. Whichever lands second wraps the Bug report's texts the same way; both stay. - #1521, #1864, #2105, #2459, #2479 (the readme): a row each after Microphone in the menu table (#2459 moves the Microphone row), where this branch adds the Bug report's row; both stay. - #2317 (saves and settings in a per-user data folder, logs in `logs/`) now merges cleanly, but overlaps in meaning: this branch finds the game folder through `ini_path()` and the logs in the game folder; after #2317 the settings are in the data folder and the logs in `logs/`. Whichever lands second moves those two reads to `game_folder()` and `log_path()`. ## Questions for you 1. Is the Insert menu the place you want this, and the game folder the place for the reports (with #2317, the data folder or `logs/` could be)? 2. The report takes the strict reading of CONTRIBUTING.md on `hookshot_env.txt` (names, and only harmless values). Should CONTRIBUTING.md say so, next to the line that lists the file? 3. The link carries the form's one-line answers and, when short enough, the options and the switches' names. Do you want it at all, or only `report.txt` to copy from? Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…es on main again
host/oot_high_fps.c read its log switch as getenv("HOS_FPS_LOG") != NULL
(NotRexed#2470, kept by NotRexed#2552), the set-means-on form NotRexed#2042 retired:
HOS_FPS_LOG=0 turned the log on. tests/test_env_on.py finds such reads, so
it fails on main since NotRexed#2470: in run 43120809833 (eed4d91) in the Python
source checks' ubuntu-24.04 part 2/2 and windows-2022 part 1/2.
The switch is read with env_on now, as host/nn_vi.c reads the same name:
unset, empty, 0, false, off and no are off.
Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…es on main again (#2575) ## What `host/oot_high_fps.c` reads its log switch with `env_on("HOS_FPS_LOG")` instead of `getenv("HOS_FPS_LOG") != NULL`. One line, one include, and the comment that describes the switch. ## Why - The old form is the set-means-on read that #2042 retired: with it `HOS_FPS_LOG=0` turned this log on, while `host/nn_vi.c` reads the same name with `env_on`. - `tests/test_env_on.py` looks for such reads, so it fails on `main` since #2470 brought the line (#2552 kept it). In run 43120809833 on `main` (`eed4d912`) that is the failure of `Python source checks (ubuntu-24.04, part 2/2)` and one of the two in `Python source checks (windows-2022, part 1/2)`: ``` FAIL: on/off switches still read as set-means-on (set to 0 they are on): use env_on: HOS_FPS_LOG (host/oot_high_fps.c:15) ``` ## How it was tested - `tests/test_env_on.py` on `main` `9df055f2`: fails, naming `host/oot_high_fps.c:16`. With this change: passes. - `tests/run_all.py --strict` with `test_env_on`, `test_menu_env_on` and `test_high_fps_fixes`: 3 passed (Arch Linux). - `host/oot_high_fps.c` compiles with `gcc` and `clang` (`-fsyntax-only -Wall -Wextra`, with the generated headers of a local build). - Not run: the game with the switch set. The change is the read of one environment variable through the function the other host files use. ## Not in this change The same run has two more failures on Windows that this does not touch: `tests/test_psychov35.py` (part 1/2) and `tests/test_pcache_header_read.py` (part 2/2). No issue was opened for this: it is one line and the failing test is the report. Assisted-by: Claude Fable 5.2 Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
vk_shader.cpp: main (NotRexed#2042) removed the dead oot_visual workaround; the fragment translation this PR moves ahead of the vertex stage drops its call too.
**What this is for:** restore ten game-free checks whose extracted fixtures or expectations no longer match intentional changes on main; [main's CI log](https://github.com/NotRexed/Hookshot/actions/runs/43151254256) shows these failures. **What is measured:** those ten checks on Linux with GCC 16.2.1 and Clang 23.1.3, plus the strict full suite and deliberate regression mutations, as detailed below. **Not measured:** a game run, Windows execution, audio output or GPU behavior; no production file changes in this diff. ## Cause and changes Each row is a separate commit, naming the change that made the fixture stale; the quoted diagnostics come from [main's failed jobs](https://github.com/NotRexed/Hookshot/actions/runs/43149508038). | Introduced by | Checks / CI diagnostic | Repair | |---|---|---| | #1544, after #2042's switch helper | `audio_aux`, `audio_biquad`: `implicit declaration of function ‘env_on’` | Include the real helper before the extracted DSP. | | #2116 | `display_startup_failure`, `wayland_libdecor_hint`: `use of undeclared identifier 'window_title'` | Extract the real title helper along with its callers. | | #1906 | `texel_buffer_ownership`: `use of undeclared identifier 'dead_import_bytes'` | Model the counter and check that it stays charged until completion. | | #2458 | `readback_part`: `extraneous closing brace ('}')` | Select the complete `wait_recorded` definition, not its new forward declaration. | | #2574 | `dlss_checks`: `FAIL under a third wide at line 40`; `framegen_preset` / `rumble_menu`: undeclared `temporal_reset_failure` | Check both sides of the intended two-pixel tolerance; supply a counted reset hook and exercise the real scaling rows. | | #2435 | `mods_size_table`: `undefined reference to hos_xinput_pads` | Supply the fixture's no-controller answer; retain real merge and routing checks. | The diff retains the existing assertions: no check is deleted or skipped, and it does not revert the production behavior named above. ## How it was tested Recorded on 2026-11-11, Linux kernel `7.2.15-1-omarchy`; base `de866017`, head `eeb09c09`, GCC 16.2.1 and Clang 23.1.3 (compiler version output and test records). - Untouched main `a016b7a6`: `455 passed, 15 failed, 20 skipped`; each of these ten failures also reproduced alone with both compilers, matching the linked CI diagnostics. - Branch: all ten repaired checks pass independently with each compiler (`python -I -B tests/run_all.py --strict --jobs 1 <test>` records). - Negative controls: 14 deliberate source regressions were each rejected with both compilers, without a compile failure—28 behavioral rejections (mutation records). These remove Aux sizing, biquad filtering, startup failure handling, the libdecor hint, view ownership release, part-specific waits, preset preservation, temporal reset or mod-table routing; the DLSS controls independently make each axis's tolerance too narrow and too broad. - One full `python -I -B tests/run_all.py --strict` run: **`464 passed, 6 failed, 20 skipped in 141 s`**, 32 workers, load 7.24 → 28.38 (complete suite output). ### Remaining failures and the path to green The complete branch run names these six failures; the first five are outside this fixture-only diff: | Remaining check | Separate fix | |---|---| | `test_env_on.py` | `fix/capture-switch-values`: the three presence-only capture switch reads from #2437. | | `test_linux_docker_packages.py` | `test/docker-ppa-options`: distinguish `--no-update` from its PPA operand. | | `test_mouse_native.py` | `fix/mouse-camera-handover`: discard retained angles while another camera owns the view. | | `test_menu_i18n.py` | `fix/menu-hint-translations`: update three complete hint keys and their translations. | | `test_hdr_encode.py` | Already repaired in #2614; deliberately not duplicated here. | | `test_release_scripts.py` | An intermittent, unchanged release-script concurrency failure: `SHA256SUMS still differs from the files after 5 uploads`. It passed untouched main and current main CI; isolated reruns passed once and failed once. Neither run invokes a C/C++ compiler. | ## Not tested / risks No Windows, game, display, controller, listening or GPU run was performed; the evidence above is game-free only. The fixture extraction still follows source structure, so future helper/signature changes must update its inputs rather than remove its behavioral guards. ## Overlaps The inspected merge hunks overlap #1834's frame-generation fixture helpers and #2622's older readback extraction; neither contains these repairs. #2614 owns the separate HDR initializer check. Other same-file changes merge cleanly in the recorded overlap comparison. Assisted-by: GPT-6 Astra Co-Authored-By: GPT-6 Astra <noreply@openai.com> --------- Co-authored-by: GPT-6 Astra <noreply@openai.com>
…he loop It was read with atoi, which takes any value that is not a number for 0: RX_VK_NATIVE_FADD=on turned the native float add off. native_fadd_wanted reads it with env_value_off (gpu/vk/compat/hookshot_env.h, NotRexed#2042) now: 0, false, off, no or empty keep the compare-exchange loop, any other value asks for the native add, unset as before. tests/test_native_float_atomic_add.py compiles native_fadd_wanted alone against 13 values. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t a compare-and-swap loop (#2306) Compute programs that run every frame in Kakariko and on Death Mountain Trail add floats to storage buffers, and the translator makes each of those adds a compare-and-swap loop: on an RTX 4090 the busiest one takes 77-80 attempts per add on average, and with the GPU's own float add the frame's GPU time is 0.4-0.6 ms (about 7%) lower there. On NVIDIA's driver the add is now one instruction. On AMD and Intel it stays the loop by default, because the native add was tried in the game only on that RTX 4090. That default changes once someone posts a run on an RDNA 3/4 (or Arc) card in Kakariko and on Death Mountain Trail with `RX_VK_NATIVE_FADD=1`. Refs #2336 (closed by the maintainer: tracked in this pull request). This takes up the idea of yassine-milal's #886 (closed to keep the queue clear), written again on current `main` with three faults of that diff fixed; no code was copied from it. ## What changed - `vk_main.cpp`: on NVIDIA's driver (`VkPhysicalDeviceDriverProperties`' `driverID`, asked before the extensions are chosen; NVK does not count), the device asks for `VK_EXT_shader_atomic_float` and turns on only `shaderBufferFloat32AtomicAdd`, the one feature used. `RX_VK_NATIVE_FADD=1` asks for it on any GPU that reports the feature, and `=0` keeps the loop everywhere without enabling the extension. The switch follows #2042's rule, so `on`, `yes` or `true` mean on and `0`, `false`, `off`, `no` or empty mean off (`native_fadd_wanted`). The log names the choice and why: `vk: float atomic add on storage buffers: native (VK_EXT_shader_atomic_float), NVIDIA's driver`, or `compare-exchange loop, by default only with NVIDIA's driver (RX_VK_NATIVE_FADD=1: native where the GPU has it)`, or `compare-exchange loop, RX_VK_NATIVE_FADD=0`, or the feature that is missing. - `vk_shader.cpp`: the profile tells the translator; the program cache's stamp includes the answer, so a `hookshot_programs.bin` made with the native add is never read on a GPU without it (#886 keyed only the environment switch). - Eden's back end: with the feature, `RED/ATOM.ADD.F32` on a storage buffer is one `OpAtomicFAddEXT` on an f32 view of the buffer, and no loop is made. The global-address fallback (#598) adds natively too, through the f32 views and the page table; the f32 views are searched with their bound length, as the u32 ones are (#886 skipped that check for them). Sirit gains `OpAtomicFAddEXT`. - Without the native add nothing changes: the compare-and-swap loop with its 1,024-attempt ceiling (#548) stays. That covers every GPU that is not on NVIDIA's driver unless the switch asks for it. - `docs/development.md`: the `RX_VK_NATIVE_FADD=0` or `1` row, with the default. Which GPUs report the feature, from the drivers' sources: NVIDIA (the RTX 4090 here reports `shaderBufferFloat32AtomicAdd = true`); AMD RDNA 3 and later on Mesa RADV (`gfx_level >= GFX11`) and RDNA 3 on AMD's own driver (PAL's Gfx11); Intel Arc and later on Mesa ANV (`has_lsc`). RDNA 1 and 2 (RX 5000, RX 6000, Steam Deck) and Intel's older integrated graphics have the extension without this feature and keep the loop either way. Of these, only NVIDIA's driver gets the native add by default. The AMD and Intel ones need `RX_VK_NATIVE_FADD=1` until they have a run here (Joey-Magnus's review). ## How it was tested - `tests/test_native_float_atomic_add.py`: cuts the storage-buffer setup and the float-add emitter out of the back end into a Sirit fixture, translates a compute shader with the feature, without it and without descriptor aliasing, and checks one `OpAtomicFAddEXT` and no compare-and-swap in the first, the loop in the others, `spirv-val --target-env vulkan1.3` on all three; and that the device, the profile, the cache stamp and the fallback follow. It also compiles `native_fadd_wanted` alone against 28 cases: unset, NVIDIA's driver alone gets the native add (not RADV, AMD's, Intel's or NVK); set, the switch's value by #2042's rule on any driver. It fails on `main`. The decision fails it when `native_fadd_wanted` reads the switch with `atoi` as before (`RX_VK_NATIVE_FADD=[on]: the loop`) or wants the native add on every driver (`[(unset)], driver 1: native`). - On the head (rebased onto `main` a5b955f), through `tests/run_all.py --strict` with `CXX=g++` and with `CXX=clang++`: `test_native_float_atomic_add`, `test_amd_subgroup_profiles`, `test_program_stamp_knobs`, `test_env_on`, `test_global_atomic_add_f32` and `test_cas_loop_ceiling` pass (6 of 6 each). Against `main`'s source, `test_native_float_atomic_add` fails with both compilers. - `python -I -B tests/run_all.py` on the head (Arch Linux, 32 at once, at a load of 60 to 140 from other work on this PC): 435 passed, 3 failed, 20 skipped; `main` a5b955f the same way: 436 passed, 1 failed, 20 skipped. The four checks that failed in one of the two runs (`test_appimage_package_retry`, `test_bake_mimo`, and `test_recorder_sync` and `test_control` by timeout) were run again alone on both trees: all four pass on the branch, and `test_appimage_package_retry` fails on `main` again. So they are timing under load, not this change. - `clang++ -Wall -Wextra` on the changed files against `main`: no new warning besides four `missing field 'pNext' initializer` on new structs (two for the driver query, two for `afloat`), the form `vk_main.cpp` already has 37 of. - A stress program with the translator's exact loop: invocations each add 1.0 to one float; lost adds are the count minus the sum (median of 5 runs). | adds to one float | RTX 4090: loop, adds lost | RTX 4090: `OpAtomicFAddEXT`, adds lost | RTX 4090: loop / native time | Intel UHD 770: loop, adds lost | |---|---|---|---|---| | 1,024 | 0 | 0 | 0.200 / 0.003 ms | 0 | | 4,096 | 2,068 | 0 | 1.37 / 0.007 ms | 2,442 | | 16,384 | 13,450 | 0 | 6.61 / 0.022 ms | 10,704 | | 65,536 | 62,884 | 0 | 28.1 / 0.084 ms | 46,654 | (RTX 4090: NVIDIA 615.78.08; Intel UHD 770: Mesa 26.2.5, which has no buffer float add, so only the loop.) Up to 1,024 adds on one float the loop loses nothing; beyond that it does. ## In the game RTX 4090, NVIDIA 615.78.08, Arch Linux, no window (scripted input), the game's default output 2048x1152, Resolution 100%, Game speed per frame; this branch's build, with and without `RX_VK_NATIVE_FADD=0`, each prepared first; a save loaded and Link left standing; 150 s each, numbers from present 1800 on (the 60 fps cap holds both ways, so this is GPU time, not frame rate). These were measured on the first version of this branch, on `main` fab8cb3. On NVIDIA's driver the review's changes leave the same native path as the default, so they were not repeated. The two runs after the rebase are below the table. | per frame | Kakariko, child, day: native / loop | Death Mountain Trail, adult, night: native / loop | |---|---|---| | GPU time (the log's `vk: GPU time`, every 120 presents: median, range) | **7.2** (7.1-7.5) / **7.8** (7.5-8.0) ms | **5.4** (4.4-5.7) / **5.8** (4.6-6.0) ms | | all timed GPU work (`RX_VK_PASS_TIMING=2`, mean of 12 reports of 600 presents) | 9.05 / 9.57 ms | 6.38 / 6.78 ms | | `ab7699ee7f536755`'s dispatch, once a frame | under 0.065 ms (not among the 25 costliest spans in any report) / 0.153 ms (in all 12) | under 0.051 ms / 0.147 ms | | `435a0138b9013040`'s dispatch, once a frame | under 0.065 ms / 0.197 ms (in all 12) | under 0.051 ms / 0.186 ms (in all 12) | | game frames | 59.3 / 59.3 fps | 59.4 / 59.6 fps | The other four float-add dispatches (below) are cheaper: not among the 25 costliest spans either way, except `592079442d7129da` once with the loop on Death Mountain Trail (0.059 ms). - Which programs add floats, and how hard their loops work (a local counting build, not part of this change, run with the loop for about five minutes in each place): six compute programs, all adding to storage buffers: `499dbfd0721d2858` (#816's sky light), `ab7699ee7f536755` (#886's Kakariko pass), `435a0138b9013040`, `592079442d7129da`, `aba5e6af8d4683eb` (Kakariko only) and `6214a97bec4cc0bf`. Of 111.7 million adds in the two places, none reached the 1,024-attempt ceiling; the busiest, `ab7699ee7f536755` (about 3,000 adds a frame), took 77-80 attempts an add on average and 704 at most. So on this card no add is lost, and the gain is time. #886's fog program `7984161db3e053b5` runs every frame in both places but has no 32-bit float add; this change does not touch it. - Pictures of the same present both ways (native, loop, difference x4) show the same light, haze and sky; the differences are animation (grass, Navi, Link's idle pose), as a present number is not exactly the same game frame in two runs.   - After the review and the rebase: two headless runs of the rebased build (head 7219b3fd on `main` a5b955f, the same save in Kakariko, 360 s each, `RX_VK_PRECOMPILE=0` so programs were made as met). Both reached Kakariko and ran to the end with no fatal error, no device loss and no kernel driver message. Their pictures show the same light and sky both ways. Other work kept this PC at a load of 60 to 165, so they say nothing about speed. The log line of each:  - Rebased once more on 2026-11-11, onto `main` `9df055f2`, as `50579bab`: the only conflict was this change's row in `docs/development.md` beside a new row of main's; the three commits' code and tests are unchanged (compared hunk by hunk). Rerun on that head: `test_native_float_atomic_add`, `test_amd_subgroup_profiles`, `test_global_atomic_add_f32`, `test_cas_loop_ceiling` and the program cache's four checks pass through `tests/run_all.py --strict`, and `test_native_float_atomic_add.py` fails on `main`'s tree. The whole suite and the two game runs above are from `7219b3fd` on `a5b955fb`. ``` vk: float atomic add on storage buffers: native (VK_EXT_shader_atomic_float), NVIDIA's driver vk: float atomic add on storage buffers: compare-exchange loop, RX_VK_NATIVE_FADD=0 ``` ## Not tested - **AMD and Intel**: the native add is off there by default until someone posts an RDNA 3/4 (or Arc) run in Kakariko and on Death Mountain Trail; with `RX_VK_NATIVE_FADD=1` it has not been run on either (which of them report the feature is from the drivers' sources above). Windows with NVIDIA is now tested by @nullifyshot (RTX 5080, NVIDIA 621.14, Windows 11, headless): [child, Kakariko and Death Mountain Trail](#2306 (comment)) and [adult, Death Mountain Trail](#2306 (comment)); the default picks the native add there too, about 4 % less GPU time. The team's test request T-54W3 asks for an AMD RX 7000 or newer or an Intel Arc. Still not tested: adult at night on Windows; NVK; the Steam Deck (keeps the loop either way). Frame rates: the RTX 4090 holds the 60 fps cap both ways here, so only GPU time was compared. #886's thin fog and 20-53 ms passes (Quadro P4000, Windows, v2.0.2) did not reproduce on this card: no add was dropped and the pictures match. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Several switches did things a player would not expect: a test run's
RX_VK_PACE=1stayed inhookshot_options.inifor good, a line inhookshot_env.txtbeat a switch set for one launch, and switches that change the shader translation did nothing on a PC that already had a program cache. This PR fixes those, removes switches left over from mkw-recomp and TOTK, makes the AMD ASTC workaround a menu row, and makes=0mean off for every on/off switch. The V-Sync row is #2041.It is a series of commits, one per part, in this order.
1. Bug fixes
Environment overrides last for their run.
RX_VK_FRAMEGEN_MULT,RX_VK_PACE,RX_VK_UPSCALE,HOS_REALTIME_STEP,HOS_DYNRESandHOS_OUTPUT_RESwere copied into their rows inmenu_init, and the next change of any row saved them. They now apply to the run only:save()writes the player's own value until the player changes that row (env_override,saved_valueinvk_menu.cpp; a custom output size of the player's is kept too).tests/test_menu_env_overrides.pycompiles the realsave()and the overrides' part ofmenu_init.Translation switches are in the program cache's stamp.
RX_VK_NO_CBUF_LOAD_RULE,RX_VK_PTR_SPLIT_FALLBACK,RX_VK_ALL_GLOBAL_PROGS,RX_VK_NO_EXTREGS,RX_VK_NO_DRAW_PARAMS, and the miss log'sRX_VK_MISS_LOGandRX_VK_BLOCK_PROBE, each change what the translator makes (checked at each read) but were not indisk_stamp's list.tests/test_program_stamp_knobs.pyreads everyRX_VK_switch the translator reads and fails when one is missing (two that only log are listed as such). Nothing changes for a run that sets none of them.Rows no preset sets keep the Quality preset. Changing Reflex, Anti-Lag 2, Output resolution, Anisotropic filtering, Ultrawide, Ultrawide menus, Robust buffers, Mesh shaders, Text speed, Text size, Controller in background, Layout preset, Descriptor reuse or Sampler cache turned the preset to Custom. They are
preset_exemptnow;tests/test_preset_exempt.pyderives the rule fromapply_preset. The Upscaler stays as it was, because changing it moves Render resolution to the upscaler's own steps.The environment goes over
hookshot_env.txt. The file usedsetenv(..., 1), so a switch set for one launch (HOS_FULLSCREEN=0 ./hookshot, a launcher's or a script's own) lost to the saved line. A name already set when the game starts now keeps its value, and the log says the file's line was left out; within the file a later line still replaces an earlier one.tests/test_env_file.pycovers it. The readme and the development guide now give the order: the environment, thenhookshot_env.txt, then the menu's saved rows.2. Dead switches removed, duplicates merged
Each removal was checked against the code, tests, scripts, workflows, the builders and the docs first.
RX_BACKEND=eden: it loadedmkw_eden.dll, which nothing builds, then fell back to WebGPU.gpu/vk/VULKAN.mdandvk_api.hnow namehookshot_vk.dll.HOS_FIX_RESandHOS_SCENE_NATIVE, withnx2/oot_native_resolution.py, the lift patch they switched (off since Output resolution (2048x1152, 2560x1440, 3840x2160) and a full-size 3D at 4K #849). The lift changes, so the next build lifts again.RX_VK_FOLLOW_MESH("tried for the foliage, which it did not bring back").TOTK_MINIMAL_LAYOUT, andTOTK_in the nameshookshot_env.txtaccepts.RX_VK_OOT_VISUAL,RX_VK_OOT_FOG,RX_VK_OOT_EXPOSUREand the Orchid fog and exposure workaround (off since 11-07; its factors were tuned against bugs since fixed). The NaN guard that shared its header is now inir_opt/fragment_output_guard.h. The readme's credits say what Orchid's workaround was used for, and the license line no longer lists Orchid's code.nx2/build.py --lib,--only,--optand--no-polls: nothing passes them.--dllstays (nx2/test_dll.pyandnx2/dllmap.pyuse it).RX_VK_DESC_SLOTGEN,RX_VK_NO_DESC_REUSEandRX_VK_DESC_REUSE_DGENare now one switch,RX_VK_DESC_REUSE=0/1/never(the row's values:0is its Off, the global check;1is its On, by slot, the default;neverturns reuse off), which goes over the Descriptor reuse row. The DGEN switch only turned off issue 1126's fix and is gone.RX_VK_NAN_GUARD=0turns off both NaN guards;RX_VK_NO_NAN_GUARDis gone.nx2/bake_ds's copy of the stamp's list matches the live one again, and the stamp check now compares them.RX_VK_FRAMELOG(pictures) andRX_VK_FRAME_LOG(frame times),RX_VK_PRESENT_AHEAD(frame generation's copies) andRX_VK_PRESENTS_AHEAD(the replay's queue depth). Renaming them would break scripts that use them for little gain.Looked at and left in place:
HOS_FPS_MAXis not a duplicate of the Game frame rate cap row: it takes any rate (75, 100, 180 Hz screens) where the row has steps, andtests/test_frame_rate_cap_step.pydrives the cap through it. Its comment no longer calls it a test.RX_JOBSis no longer inmake_game.py; the Linux builder still honours it deliberately (tests/test_linux_builder_jobs.py), andthird_party/a64-kitreads it.RX_VK_SPARSE_IDLEturns on the stricter ordering of sparse binds, which was left off for MKW's sake. Whether OoT should have it on is a measurement, not a removal.3. Menu rows
The Debug tab's Developer section, with Robust buffers and then ASTC as RGBA8 selected (headless capture, 1600x900, RTX 3090, Linux):
hookshot_env.txt(Missing and inaccurate textures, blurry/pixelated text and map (AMD, log attached) #57, [Tracking] AMD rendering: white silhouettes, block squares / grid lines, no anti-aliasing (networks in software) #1006). Its help says what it is for and that it takes far more video memory.RX_VK_ASTC_RGBA8still goes over it for a run. It does not turn itself on for any GPU: [Tracking] VRAM and RAM fill up over play time: fps fades, textures degrade or turn rainbow, a restart fixes it (mostly 4-8 GB cards) #993's report says it took effect on a 9070 XT and did not help, so there is no clear set of cards to turn it on for.4. One rule for on/off switches
About 230 switches read as on whenever they were set, so
RX_VK_NO_PROGRAM_CACHE=0orHOS_HEADLESS=0turned the feature on.env_on(name)(gpu/vk/compat/hookshot_env.h, for bothhost/andgpu/vk) is off when the variable is unset, empty,0,false,offorno, and on otherwise. Every check that only asked whether a switch was set now uses it;=1works as before.host/andgpu/vk(the translator's own switches included) now use it. A switch that some other place reads for a value is left exactly as it was: 25 of them, such asRX_VK_WARP64=0,HOS_FULLSCREEN=0, the lists and the sizes, where0already means something.nx2/kit.pyhas the same rule asenv_on()for the build'sRX_VK_ONLY.nx2/build.pycounts the header in the runtime's object keys, as it doesdlss_math.h. The AppImage now packs headershost/includes from belowgpu/vk/.tests/test_env_on.pychecks the rule in C, C++ and Python, and fails if a switch is read the old way again. Checks that compile code cut out of the changed files now force-include the header (theirgetenvstand-ins passenv_onthrough them). The checks that look for a switch's read by its text now look forenv_on.test_display_startup_failure'sHOS_HEADLESS=0case andtest_program_answer_domains'RX_VK_REGISTER_AT_ONCE=0case now expect off.RX_VK_FIFOandRX_VK_IMMEDIATEkeep their old reads here because feat(menu): a V-Sync row (Auto, On, Off) #2041 (the V-Sync row) replaces them.tests/,scripts/,.github/, the builders or the docs set any of the converted switches to0or empty to turn it on (checked by grep).This part touches many files that other PRs also edit (
vk_draw.cppabove all). Most of it is a mechanical rewrite, done by a script that only rewrites presence checks whose value is not read on the same line, so on a conflict it is simplest to drop this commit and run the rewrite again on the new base.Testing
python -I -B tests/run_all.pyon the branch, rebased onmain667ba79: 342 passed, 13 skipped. The three failures are the same as onmain:nx2/test_nup_read_name.py,tests/test_builder_log_read_only.pyandtests/test_wayland_libdecor_hint.py, all failing on this machine without these changes. New checks:test_menu_env_overrides,test_program_stamp_knobs,test_preset_exempt,test_env_on; extended:test_env_file,test_menu_keys.hookshot_vk.so, its exported-symbol check clean, the shaders prepared), and after it a build ofhookshot_vk.so(exported-symbol check clean); a headless run with the Insert menu drawn into the shots: the Debug tab's Developer rows show and change. The log hasrx: renderer: Vulkanand robust buffer access on (Auto), as before.For the maintainer to decide (not changed here)
gpu/render, about 226 switches). It is reached only whenhookshot_vkfails to load or start, and the builders still shipwgpu-native. It could be dropped, or frozen with its switches cut to a handful.RX_VK_NO_PARTIAL,RX_VK_NO_STATE_LIST,RX_VK_PUSH_ALL,RX_VK_PUSH_STATE_OLD,RX_VK_UB_CUR_ONLY,RX_VK_POOL_END_IMPORTED,RX_VK_MIRROR_KEEP,RX_VK_READBACK_NO_CATCHUP,RX_VK_FORCE_LINEAR,RX_VK_UPLOAD_ALL,RX_VK_NO_TABLE_SHRINKand others). One policy that would work: an A/B switch lives for one release after its default flips, then goes.