Start-up screen: the window opens at once, with a splash and the shaders' progress #860

Merged
hckrman101 merged 9 commits from startup-screen into main 2026-11-09 15:15:28 +00:00
hckrman101 commented 2026-11-08 23:27:27 +00:00 (Migrated from github.com)

On the first start after an install or an update, the game prepares every shader program and the pipelines its notes name before its window shows anything: 136 s on an RTX 3090 here, several minutes on slower PCs (#711: 7.5 min on an RX 6650 XT). On Linux nothing appears at all; on Windows a console shows a count and the window does not answer. Players take it for a hang, close it at "99%" or start the game a second time (#615, #643).

Now the window opens at once and shows what is being done:

Splash Step 1: shaders Step 2: pipelines
splash shaders pipelines

What changed

  • With a window, prepare_run, predict_run and precompile_run run on a thread of their own. A second thread draws a small screen with the Insert menu's text renderer (GDI on Windows, the stb_truetype stand-in on Linux) and presents it about 30 times a second through present_image.
  • The screen opens with the Hookshot logo (assets/hookshot.png, the builder's), the HOOKSHOT wordmark and the version ("v2.0.5"; "v2.0.5 dev" between releases). After a second it shows what the start is doing:
    • the step ("STEP 1 OF 2": the shaders; "STEP 2 OF 2": the pipelines, #772's predicted ones and then the noted ones), the title and a percentage, a rounded bar, the count, and, once a sixth is done, about how long is left;
    • the time left comes from the rate over the last 15 s, smoothed over about 8 s. The rate from the start is no good here: the first 1,500 programs took half a minute and the next 10,000 took ten seconds, and the first version said "83 min left" for a 70-second job;
    • the count is in programs, as the log counts them (38,809), not programs times variants.
  • A warm start shows "Loading pipelines" (no steps, no "after an update" hint) for the precompile's few seconds, then "Starting the game".
  • "Closing" and "Starting the game" use the same layout. The panel scales with the window's height (100% at 720 lines, 150% at 1080, 200% at 1440) and never gets wider than the window.
  • What is shown is worked out in gpu/vk/vk_progress.h (startup_view, tested on its own) and drawn in gpu/vk/vk_startup_paint.h; neither needs Vulkan.
  • The logo is not a second copy in the repository: nx2/build.py decodes assets/hookshot.png (nx2/logo_rgba.py, standard library only) into a header in the build's own folder, and the screen blends it into its picture, scaled with the rest (112 px tall at 720 lines). A tree without the picture builds without the logo.
  • The renderer's start thread only waits for the work. On Windows it is the window's thread, so it handles the window's messages meanwhile.
  • A present that waits for a hidden window (FIFO on Wayland, a locked screen) holds neither the window nor the game's start. It did in a first version: the game never started while the screen was locked.
  • A resize is followed. The FPS counter stays off until the game draws. The Insert menu works during the wait.
  • Closing the window stops the workers after the item each is on, saves the pipeline cache and removes the warm-up and precompile markers. A close is then not counted as a failed start (the "two starts in a row" rule), and the next start goes on from where it stopped: what was made is already in the program cache.
    • The host asks the back end through an optional export, mkwvk_close_asked, before it ends the process. An executable and a back end of different versions still close.
    • The wait is 10 s at most, for a driver busy on one pipeline.
  • Unchanged: headless runs (including the Windows console), --prepare-shaders and the builder's @@progress. The count also goes to the log every 10 s. RX_VK_STARTUP_SCREEN=0 brings back the old behaviour.

Measured on Linux (CachyOS, Sway/Wayland) with an RTX 3090: from launch to the first frame in the window, the program cache emptied for the first starts. Other jobs shared the PC, so the totals vary from run to run; the window times do not.

Start Before: window After: window After: the game's own frames
First start, v2.0.5 (38,809 programs, then 6,028 predicted and 10,343 noted pipelines) after the whole preparation 0.6–1.0 s 100–182 s
First start, before #772 (31,985 programs; same build with RX_VK_STARTUP_SCREEN=0 for "before") 136.4 s 0.4–0.6 s 53–111 s
After closing the window 25 s in the whole preparation again 0.5 s 30.7 s (went on from where it stopped)
Warm start, v2.0.5 (10,343 noted pipelines) as long as the precompile, 26 s here 0.7 s 27 s
Warm start, before #772 9–17 s 0.5 s 9.5–12 s

Closing at 25 s: the process exited by itself 0.7 s later (exit code 0), with no hookshot_prepared.bin.warming or .running marker left.

Overlap

  • #702 (start the game while the last pipelines still build) and #700: this calls precompile_run as it is, so it works with both. They conflict as text with the two prepare_stopped() checks in precompile_run's worker and wait loops.
  • #701 (leave the warm-up out only after two failed starts): same intent for closes. It conflicts as text in prepare_run's warm-up marker code.
  • #471 (Triforce loading screen): a different moment (area loads in play). It touches vk_menu.cpp and vk_main.cpp too.
  • It follows the stall hand-over from v2.0.3 ("stuck at 99%") and #772's predicted pipelines (part of step 2 on the screen; a close stops them too).

Testing

  • tests/test_startup_progress.py compiles gpu/vk/vk_progress.h on its own. It covers the count's separators, the percentage (never 100% before the end), the time-left wording, the estimate (steady progress, a resumed run, a slow stretch, a stall, a new stage) and the screen's states (wordmark alone in the first second, steps on a first start, none on a warm one, closing, starting). It also checks the sources: windowed starts prepare behind the screen, headless ones do not, the workers stop on a close, and a stopped preparation does not write its stamp. tests/run_all.py picks it up.
  • tests/test_fps_overlay_order.py now also covers the start-up screen's copy.
  • Linux: first starts, a close part-way and the start after it, warm starts, and a resize during the preparation, all with an RTX 3090. The game started and played normally after each.

Not tested

  • Windows (no Windows PC here). These parts are written for it but not run:
    • the window's messages handled on the start thread while it waits (PeekMessage/DispatchMessage);
    • WM_CLOSE waiting up to 10 s inside the window procedure for the workers to stop (the window does not answer during that wait);
    • the size followed with GetClientRect, since WM_SIZE is left out until the renderer has started (WM_ERASEBKGND may flash black during a resize);
    • presenting from the painter thread rather than the window's thread;
    • the console no longer shown for windowed starts.
  • AMD and Intel GPUs. On AMD the slow part is the driver's warm-up of compute pipelines, which shows as "Preparing shaders".

A second, separate commit gives the Insert menu's selected row rounded corners and a rounded marker, to match. It can be dropped without touching the rest.

On the first start after an install or an update, the game prepares every shader program and the pipelines its notes name before its window shows anything: 136 s on an RTX 3090 here, several minutes on slower PCs (#711: 7.5 min on an RX 6650 XT). On Linux nothing appears at all; on Windows a console shows a count and the window does not answer. Players take it for a hang, close it at "99%" or start the game a second time (#615, #643). Now the window opens at once and shows what is being done: | Splash | Step 1: shaders | Step 2: pipelines | |---|---|---| | ![splash](https://raw.githubusercontent.com/hckrman101/Hookshot/pr-images/startup-0-splash.png) | ![shaders](https://raw.githubusercontent.com/hckrman101/Hookshot/pr-images/startup-1-shaders.png) | ![pipelines](https://raw.githubusercontent.com/hckrman101/Hookshot/pr-images/startup-2-pipelines.png) | **What changed** - With a window, `prepare_run`, `predict_run` and `precompile_run` run on a thread of their own. A second thread draws a small screen with the Insert menu's text renderer (GDI on Windows, the stb_truetype stand-in on Linux) and presents it about 30 times a second through `present_image`. - The screen opens with the **Hookshot logo** (`assets/hookshot.png`, the builder's), the HOOKSHOT wordmark and the version ("v2.0.5"; "v2.0.5 dev" between releases). After a second it shows what the start is doing: - the step ("STEP 1 OF 2": the shaders; "STEP 2 OF 2": the pipelines, #772's predicted ones and then the noted ones), the title and a percentage, a rounded bar, the count, and, once a sixth is done, about how long is left; - the time left comes from the rate over the last 15 s, smoothed over about 8 s. The rate from the start is no good here: the first 1,500 programs took half a minute and the next 10,000 took ten seconds, and the first version said "83 min left" for a 70-second job; - the count is in programs, as the log counts them (38,809), not programs times variants. - A warm start shows "Loading pipelines" (no steps, no "after an update" hint) for the precompile's few seconds, then "Starting the game". - "Closing" and "Starting the game" use the same layout. The panel scales with the window's height (100% at 720 lines, 150% at 1080, 200% at 1440) and never gets wider than the window. - What is shown is worked out in `gpu/vk/vk_progress.h` (`startup_view`, tested on its own) and drawn in `gpu/vk/vk_startup_paint.h`; neither needs Vulkan. - The logo is not a second copy in the repository: `nx2/build.py` decodes `assets/hookshot.png` (`nx2/logo_rgba.py`, standard library only) into a header in the build's own folder, and the screen blends it into its picture, scaled with the rest (112 px tall at 720 lines). A tree without the picture builds without the logo. - The renderer's start thread only waits for the work. On Windows it is the window's thread, so it handles the window's messages meanwhile. - A present that waits for a hidden window (FIFO on Wayland, a locked screen) holds neither the window nor the game's start. It did in a first version: the game never started while the screen was locked. - A resize is followed. The FPS counter stays off until the game draws. The Insert menu works during the wait. - **Closing the window** stops the workers after the item each is on, saves the pipeline cache and removes the warm-up and precompile markers. A close is then not counted as a failed start (the "two starts in a row" rule), and the next start goes on from where it stopped: what was made is already in the program cache. - The host asks the back end through an optional export, `mkwvk_close_asked`, before it ends the process. An executable and a back end of different versions still close. - The wait is 10 s at most, for a driver busy on one pipeline. - Unchanged: headless runs (including the Windows console), `--prepare-shaders` and the builder's `@@progress`. The count also goes to the log every 10 s. `RX_VK_STARTUP_SCREEN=0` brings back the old behaviour. **Measured** on Linux (CachyOS, Sway/Wayland) with an RTX 3090: from launch to the first frame in the window, the program cache emptied for the first starts. Other jobs shared the PC, so the totals vary from run to run; the window times do not. | Start | Before: window | After: window | After: the game's own frames | |---|---|---|---| | First start, v2.0.5 (38,809 programs, then 6,028 predicted and 10,343 noted pipelines) | after the whole preparation | 0.6–1.0 s | 100–182 s | | First start, before #772 (31,985 programs; same build with `RX_VK_STARTUP_SCREEN=0` for "before") | 136.4 s | 0.4–0.6 s | 53–111 s | | After closing the window 25 s in | the whole preparation again | 0.5 s | 30.7 s (went on from where it stopped) | | Warm start, v2.0.5 (10,343 noted pipelines) | as long as the precompile, 26 s here | 0.7 s | 27 s | | Warm start, before #772 | 9–17 s | 0.5 s | 9.5–12 s | Closing at 25 s: the process exited by itself 0.7 s later (exit code 0), with no `hookshot_prepared.bin.warming` or `.running` marker left. **Overlap** - #702 (start the game while the last pipelines still build) and #700: this calls `precompile_run` as it is, so it works with both. They conflict as text with the two `prepare_stopped()` checks in `precompile_run`'s worker and wait loops. - #701 (leave the warm-up out only after two failed starts): same intent for closes. It conflicts as text in `prepare_run`'s warm-up marker code. - #471 (Triforce loading screen): a different moment (area loads in play). It touches `vk_menu.cpp` and `vk_main.cpp` too. - It follows the stall hand-over from v2.0.3 ("stuck at 99%") and #772's predicted pipelines (part of step 2 on the screen; a close stops them too). **Testing** - `tests/test_startup_progress.py` compiles `gpu/vk/vk_progress.h` on its own. It covers the count's separators, the percentage (never 100% before the end), the time-left wording, the estimate (steady progress, a resumed run, a slow stretch, a stall, a new stage) and the screen's states (wordmark alone in the first second, steps on a first start, none on a warm one, closing, starting). It also checks the sources: windowed starts prepare behind the screen, headless ones do not, the workers stop on a close, and a stopped preparation does not write its stamp. `tests/run_all.py` picks it up. - `tests/test_fps_overlay_order.py` now also covers the start-up screen's copy. - Linux: first starts, a close part-way and the start after it, warm starts, and a resize during the preparation, all with an RTX 3090. The game started and played normally after each. **Not tested** - **Windows** (no Windows PC here). These parts are written for it but not run: - the window's messages handled on the start thread while it waits (`PeekMessage`/`DispatchMessage`); - `WM_CLOSE` waiting up to 10 s inside the window procedure for the workers to stop (the window does not answer during that wait); - the size followed with `GetClientRect`, since `WM_SIZE` is left out until the renderer has started (`WM_ERASEBKGND` may flash black during a resize); - presenting from the painter thread rather than the window's thread; - the console no longer shown for windowed starts. - AMD and Intel GPUs. On AMD the slow part is the driver's warm-up of compute pipelines, which shows as "Preparing shaders". A second, separate commit gives the Insert menu's selected row rounded corners and a rounded marker, to match. It can be dropped without touching the rest.
NotRexed commented 2026-11-09 05:30:41 +00:00 (Migrated from github.com)

Closing to keep the queue clear — please rebase on current main and reopen; thanks!

Closing to keep the queue clear — please rebase on current main and reopen; thanks!
Commenting is not possible because the repository is archived.
No description provided.