fix(menu): run the upscalers' one-time probes during the start-up screen, not at the menu's first open (#1886) #1916

Merged
NotRexed merged 3 commits from fix/menu-probes-at-startup into main 2026-11-11 00:27:33 +00:00
NotRexed commented 2026-11-10 20:18:59 +00:00 (Migrated from github.com)

For #1886, the menu open freezing the game and the audio going out of sync.

Measured: on a v2.0.14-line build with DLSS (G:\hk-v2015), I opened and closed the menu ten times by script during play with RX_VK_FRAME_LOG.

  • The first open held one frame for 2,906 ms. The log shows NVIDIA DLSS: NGX started and fsr3: AMD FSR 3.1 can run on this GPU at that moment, then 13.6 fps over the last 7.4 s.
  • The later opens stayed under 150 ms.

Cause: at the menu's first open, vk_menu.cpp asks on the frame's thread:

  • dlss_probe() (NVIDIA's NGX start, about 3 s);
  • fsr4_probe();
  • fsr3::supported().

While NGX starts the game stands still but its sound keeps going, so a cutscene's audio ends up out of step.

Fix: startup_screen() starts the three probes once (std::call_once) on a thread of their own when the start-up screen first shows, during shader preparation, where nobody waits on them. Each is already locked and runs once (ngx_init's s.mu and ngx_tried, FSR 4's load() lock, fsr3::supported's static), so:

  • the menu's own calls at its open return at once, or wait only for what's left of a probe still running;
  • a game started with DLSS already chosen shares the same single NGX start.

A log line gives the background probe's time.

Not yet checked: a local in-game test of this change. My PC is kept free for the owner's v2.0.15 testing right now. CI compiles it on Windows (with and without DLSS) and Linux. I'll re-run the 10-open menu test on a build of this branch before merging.

🤖 Generated with Claude Code

For #1886, the menu open freezing the game and the audio going out of sync. **Measured:** on a v2.0.14-line build with DLSS (G:\hk-v2015), I opened and closed the menu ten times by script during play with `RX_VK_FRAME_LOG`. - The first open held one frame for 2,906 ms. The log shows `NVIDIA DLSS: NGX started` and `fsr3: AMD FSR 3.1 can run on this GPU` at that moment, then `13.6 fps over the last 7.4 s`. - The later opens stayed under 150 ms. **Cause:** at the menu's first open, `vk_menu.cpp` asks on the frame's thread: - `dlss_probe()` (NVIDIA's NGX start, about 3 s); - `fsr4_probe()`; - `fsr3::supported()`. While NGX starts the game stands still but its sound keeps going, so a cutscene's audio ends up out of step. **Fix:** `startup_screen()` starts the three probes once (`std::call_once`) on a thread of their own when the start-up screen first shows, during shader preparation, where nobody waits on them. Each is already locked and runs once (`ngx_init`'s `s.mu` and `ngx_tried`, FSR 4's `load()` lock, `fsr3::supported`'s static), so: - the menu's own calls at its open return at once, or wait only for what's left of a probe still running; - a game started with DLSS already chosen shares the same single NGX start. A log line gives the background probe's time. **Not yet checked:** a local in-game test of this change. My PC is kept free for the owner's v2.0.15 testing right now. CI compiles it on Windows (with and without DLSS) and Linux. I'll re-run the 10-open menu test on a build of this branch before merging. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
hckrman101 (Migrated from github.com) reviewed 2026-11-10 20:55:09 +00:00
hckrman101 (Migrated from github.com) left a comment

The approach is right and the fix reads clean. On the thread-safety question you asked me to look at, here is what I checked in the code:

  • ngx_init (s.mu + ngx_tried) and FSR 4's load() (l.mu + tried) are locked and run once, so the menu's later calls just wait or return. fsr3::supported() is a function-local static, so its first call is serialised by the magic-static guard. g.wave, which it writes, is read only after supported() returns (e.g. vk_fsr3.cpp:418 before :423), so that is ordered too. g_why is atomic.
  • The device exists before the start-up screen: startup_screen is called from the prep loop in vk_main.cpp, after vkCreateDevice. So the dev.device guard passes in practice.

What I could not verify, and what I'd test on your PC: NVSDK_NGX_VULKAN_Init_with_ProjectID now runs on a worker thread while the start-up painter presents and the shader-prep worker builds pipelines on the same VkDevice. Before, it ran on the frame's thread between frames. If NGX touches a queue or waits idle inside Init, that would now race the painter's present. I don't see vk_main.cpp taking a lock around it. The 10-open test you plan will not exercise this, so please also run a cold start with DLSS and the validation layers on, and watch for a queue sync error during the start-up screen.

Smaller points are inline. CI: the two failures are test_bake_* (and libzstd.so.1 missing on the Windows runner), nothing in this diff touches them; the Windows/Linux builds pass. No test covers the change, which is fair for a timing fix, but the log line gives a way to check it.

The approach is right and the fix reads clean. On the thread-safety question you asked me to look at, here is what I checked in the code: - `ngx_init` (`s.mu` + `ngx_tried`) and FSR 4's `load()` (`l.mu` + `tried`) are locked and run once, so the menu's later calls just wait or return. `fsr3::supported()` is a function-local static, so its first call is serialised by the magic-static guard. `g.wave`, which it writes, is read only after `supported()` returns (e.g. `vk_fsr3.cpp:418` before `:423`), so that is ordered too. `g_why` is atomic. - The device exists before the start-up screen: `startup_screen` is called from the prep loop in `vk_main.cpp`, after `vkCreateDevice`. So the `dev.device` guard passes in practice. What I could not verify, and what I'd test on your PC: `NVSDK_NGX_VULKAN_Init_with_ProjectID` now runs on a worker thread while the start-up painter presents and the shader-prep worker builds pipelines on the same `VkDevice`. Before, it ran on the frame's thread between frames. If NGX touches a queue or waits idle inside Init, that would now race the painter's present. I don't see `vk_main.cpp` taking a lock around it. The 10-open test you plan will not exercise this, so please also run a cold start with DLSS and the validation layers on, and watch for a queue sync error during the start-up screen. Smaller points are inline. CI: the two failures are `test_bake_*` (and `libzstd.so.1` missing on the Windows runner), nothing in this diff touches them; the Windows/Linux builds pass. No test covers the change, which is fair for a timing fix, but the log line gives a way to check it.
@ -2416,0 +2427,4 @@
std::thread([] {
const auto t0 = std::chrono::steady_clock::now();
dlss_probe();
fsr4_probe();
hckrman101 (Migrated from github.com) commented 2026-11-10 20:55:09 +00:00

Since the call_once is consumed here, a call before the device exists would drop the probes for the rest of the run, and you'd silently be back to the old 3 s hitch at the menu's first open. It holds today (the device is made before the start-up screen), but it would be sturdier to set the flag only after the device check, e.g. a plain atomic exchange after the !dev.device return.

Since the `call_once` is consumed here, a call before the device exists would drop the probes for the rest of the run, and you'd silently be back to the old 3 s hitch at the menu's first open. It holds today (the device is made before the start-up screen), but it would be sturdier to set the flag only after the device check, e.g. a plain atomic exchange after the `!dev.device` return.
@ -2416,1 +2435,4 @@
});
}
void startup_screen(const StartupView* view) {
hckrman101 (Migrated from github.com) commented 2026-11-10 20:55:09 +00:00

Nit: the thread is detached, and Hookshot leaves through ExitProcess/exit() with the device alive (per the comment in vk_fsr4.cpp), so a quit while NGX is still starting is the only window. Probably fine, but worth a thought if a user quits during the first seconds of the start-up screen.

Nit: the thread is detached, and Hookshot leaves through `ExitProcess`/`exit()` with the device alive (per the comment in `vk_fsr4.cpp`), so a quit while NGX is still starting is the only window. Probably fine, but worth a thought if a user quits during the first seconds of the start-up screen.
Commenting is not possible because the repository is archived.
No description provided.