fix(vk): a game ended while it closes, after a quit during the precompile, no longer counts as a crash #498

Open
mooki wants to merge 3 commits from mooki/Longshot:fix/double-quit-precompile into main
Member

Type: fix · Risk: low · Tested on: macOS 26.7.1, Apple M4 Max (game-free checks, arm64 and x86_64 object compiles; no game run) · Needs: Windows or Linux, any GPU

Depends on #428. This branch is #428's commit (60d5c830) plus one commit. Review it after #428, or fold it into #428.

Why

Quitting during the start's precompile and then quitting again still counted as a crash. That is the usual reaction when
the game stops answering after the first quit. Two such starts dropped the precompile's notes.

With #428, a quit during the start's pipeline steps does not count as a crash. Those steps are the precompile, the
create-mode warm, the predicted pipelines and the stage libraries. But #428 puts each step's file back only at the end of
the close. Before that, the close waits up to 10 s for the preparation's workers to stop, on the window's thread.
Meanwhile the window does not answer:

  • Windows marks it "Not Responding" after 5 s and lets the player close it.
  • The desktops on Linux and macOS offer Force Quit.

A second quit there ends the process before the files are put back, so the start counted as one that crashed in the step.
After two such starts the notes were dropped and the warm was skipped. After one, the stage libraries were left out.

This makes a quit by the player or the control socket never count as a crash, however the process ends after it. A
crash, or a kill with no quit asked for, still counts.

How to reproduce

Seen on: found by reading the close's order (#428's head 4dd978d6, main 9c9d69ed), and shown by the game-free test on
macOS 26.7.1, Apple M4 Max, against main 6c9edffe's and #428 60d5c830's sources; not seen in a game run. The close is
the same code on every OS; only the window's thread differs (below).
Settings and switches: defaults (the start-up screen on).
Steps: after a fresh preparation, start the game and close the window during "noted pipelines". When it stops
answering ("(Not Responding)" on Windows, the desktop's Force Quit on Linux and macOS), end it. Do this at two starts in
a row, then start a third time. Game-free: from this branch's checkout,
python -I -B tests/test_quit_not_crash.py --cxx clang++ --source <a checkout of main or of #428>.
Expected: a quit never counts as a crash: the third start tries the step again.
Actual: the second quit ends the process before the files are put back, so each start counts as one that crashed in
the step, and the third start drops the notes (vk: the last two starts did not get through the precompile: its notes are dropped) or skips the warm; after one, the stage libraries are left out. The test: 78 of its 101 behaviour checks
wrong and 10 source checks failing on main, 36 wrong and 1 source check failing on #428.

Why, in the close's order (lines at #428's head 4dd978d6, and at main 9c9d69ed for host/ and the worker):

  • mkwvk_close_asked (gpu/vk/vk_main.cpp:1567-1580) runs in this order:
    • programs_census_log, module_ids_flush;
    • fsr4_close and pipeline_cache_close, each of which can wait up to 10 s in play;
    • while the shaders are prepared, prepare_stop() and WaitForSingleObject(prep_ended, 10000) (1573-1577);
    • and only then crash_markers::close() (1578).
  • The workers it waits for cannot cut a driver compile short, and the source names driver compiles of 15 s and 41 s
    (vk_draw.cpp:8728, 8742 at 5e072fc8). Once stopped, the worker also saves the pipeline cache before it signals that it
    ended (vk_main.cpp:1191 at main). So a compile under way uses up the 10 s.
  • The close runs on the thread that answers the window:
    • Windows: WM_CLOSE in the window procedure (host/display.c:489-493). During the start-up screen, that thread is the
      renderer's start (vk_main.cpp:1129-1132 at main).
    • Linux and macOS: the SDL event loop on the main thread (host/posix/display_sdl.c:348-351, 426-434).
  • "The operating system defines an application hang as a UI thread that has not processed messages for at least 5
    seconds", and its ghost window lets the user "close the unresponsive application"
    (Microsoft).
  • So the second quit ends the process before line 1578, and the step's file is left with the start's count plus one.

What changed

The close now puts the steps' files back first, before anything in it that takes time, so a quit never counts as a crash
however the process ends afterwards. A crash, or a kill that no quit asked for, still counts. One call moves; the rest is
comments and the extended test.

  • Read first: mkwvk_close_asked (one call moved), then close()'s doc in crash_markers.h, then the test's new ends of
    a start and its close.
  • Design: the files are put back first, not after the stop as in #428; the reason is under Risks.
  • gpu/vk/vk_main.cpp: crash_markers::close() is now the first statement of mkwvk_close_asked, before anything that
    takes time. The rest of the close is unchanged and in the same order. The comment above the function says so.
  • gpu/vk/crash_markers.h: comments only. The header's rule is now "only a crash, or a kill that no quit asked for, may
    count". close()'s doc says it is called first, and that a worker that crashes the driver after the quit is no longer
    counted.
  • tests/test_quit_not_crash.py:
    • New ends of a start:
      • kill: ended from outside while the step works, with no quit asked for.
      • quit-killed-early: a quit whose process is then ended in the close's first part other than the files.
      • quit-killed-waiting: the same, ended while the close waits for the workers.
    • For every quit, a new sequence: a quit then a crash is one crash.
    • The close is now taken whole from vk_main.cpp, in its order. The preparation's stop and the crash_markers lines are
      kept as they are. Every other statement becomes a stand-in where the process can be ended.
    • #428's source check wanted crash_markers::close() after the wait. It now wants it as the close's first statement (the
      reason is under Risks).

Unchanged: the four steps, their files and thresholds, host/, and every exit path.

Measurements

Verdict: fixed in the game-free test: on main 78 of its 101 behaviour checks are wrong (10 source checks fail), on
#428 36 (1 source check fails), here none: it passes. The whole game-free suite changes only by the new test. Correctness
evidence, not a timing comparison; no game run.

Commits: main 6c9edffe (2026-11-14), the base #428 and this PR are rebased on; #428: fix/quit-is-not-a-crash @ 60d5c830
(rebased from 4dd978d6); this PR: fix/double-quit-precompile @ 5f2640c1f5 (rebased from
6219fbbe).
Baseline: #428 @ 60d5c830 is the PR this one is stacked on (this branch is #428's commit plus one), so its column shows
what this PR's one commit adds; main's column shows the whole stack against main.
Hardware: Apple M4 Max (16 cores), 64 GiB, macOS 26.7.1 (25G241), Apple clang 21.0.0, Python 3.14.10. No GPU or driver is
involved: the checks are game-free.
Scene: none (game-free checks only)
Method: tests/test_quit_not_crash.py from this branch, run on its head and against the other trees' sources
(--source). Before the rebase: main 5e072fc8, #428, main, #428, this PR, then the same again (interleaved twice each, on
the same Mac, with the same result each time: it is deterministic; no frame time applies); one run each against main
9c9d69ed's sources, the macOS port's integrated branch (1c53cff1, which carries #428), and that branch with this commit
applied. After the rebase: once each against main 6c9edffe and #428 60d5c830, and on this head. Object compiles of the
changed renderer file. One whole-suite run with the wrapper #428 used (fresh HOME, short TMPDIR, run_all.py --jobs 6).
This is correctness evidence, not a timing comparison.
How to reproduce: python -I -B tests/test_quit_not_crash.py --cxx clang++, then the same with --source set to a checkout
of main or of #428. Run it from this branch's checkout, so that the test is this PR's. Then python -I -B tests/run_all.py.

Each check runs one step as a series of starts in a new folder, each start ending as told, then one start that gets
through. That last start shows whether the step was still tried. Every quit comes while the step runs, before the start
hands over to the game.

Before: main @ 6c9edffe Before: #428 @ 60d5c830 After: this PR @ 5f2640c1
tests/test_quit_not_crash.py exit 1: 78 of 101 behaviour checks wrong, 10 source checks fail exit 1: 36 of 101 wrong, 1 source check fails exit 0, all pass
quit, then ended while the close waits for the workers (18 checks over the four steps) 18 wrong 18 wrong 0
quit, then ended in the close's first other part (18 checks) 18 wrong 18 wrong 0
#428's quits: no start-up screen / workers stop in time / a compile outlasts the wait (18 checks each) 18 / 6 / 18 wrong 0 / 0 / 0 0 / 0 / 0
precompile, quit then ended while it closes: twice in a row; then a crash both wrong (the notes dropped at the third start) both wrong both right
two crashes or two kills with no quit (one each for the libraries) let the step go; a start that got through clears a crash (11 checks) all right all right all right
vk_main.cpp objects, arm64 and x86_64 - - exit 0
vk_main.cpp and vk_draw.cpp, with a receipt-only header declaring posix_fallocate (missing on macOS, as for #428) - - exit 0, arm64 and x86_64
whole game-free suite 531 passed, 32 failed, 17 skipped (its stack top is this PR) 532 passed, 32 failed, 17 skipped

Before the rebase, with the same test:

main 5e072fc8 and 9c9d69ed integrated 1c53cff1 (with #428) integrated 1c53cff1 with this commit
tests/test_quit_not_crash.py the same 88 lines as 6c9edffe exit 1, the same 37 lines as #428 exit 0, all pass

Each tree printed the same output as before the rebase, byte for byte (the test is deterministic; before the rebase both
runs on each tree matched too). From main's run (7 of its 88 lines):

- precompile, starts ended by quit, quit then one through: two quits counted as crashes (got to the step's work: [True, True, False], wanted [True, True, True])
- precompile, starts ended by quit, crash then one through: a quit then a crash: wanted one crash counted (got to the step's work: [True, True, False], wanted [True, True, True])
- precompile, starts ended by quit-killed-waiting, quit-killed-waiting then one through: two quits counted as crashes (got to the step's work: [True, True, False], wanted [True, True, True])
- precompile, starts ended by quit-killed-waiting, crash then one through: a quit then a crash: wanted one crash counted (got to the step's work: [True, True, False], wanted [True, True, True])
- precompile, starts ended by crash, quit-killed-waiting, crash then one through: a quit cleared the crash before it (got to the step's work: [True, True, False, True], wanted [True, True, True, False])
- precompile: a quit (quit-killed-waiting) in a new folder left ['longshot_pipelines.bin.running']
- mkwvk_close_asked does not put the steps' files back (crash_markers::close)

From #428's run, the same quit-killed-waiting lines for the precompile, the other three steps and quit-killed-early, and:

- mkwvk_close_asked does programs_census_log before it puts the steps' files back (crash_markers::close): a player who ends the game while that takes time is counted as a crash

and on this PR:

PASS: a quit in a step leaves its file as the start found it, also when its process is ended while it closes; crashes and kills with no quit still let the step go

The suite's summary lines, main 6c9edffe first, then this PR's:

main @ 6c9edffe:     531 passed, 32 failed, 17 skipped in 181 s (linux, 6 at once)
this PR @ 5f2640c1:  532 passed, 32 failed, 17 skipped in 226 s (linux, 6 at once)

The runner calls this Mac linux; these are macOS results.

Screenshots: none; nothing on screen changes.

Flags and settings

None. No switch, menu row, ini key or default changes.

Testing

Tested:

  • The test on this PR's head, and from this head against the sources of main (6c9edffe; before the rebase 5e072fc8 and
    9c9d69ed), #428, and, before the rebase, the macOS port's integrated branch with and without this commit. The marker
    code and the close it runs are cut from vk_draw.cpp and vk_main.cpp as they are.
  • On the integrated branch the close has a call main lacks (draw_layouts_report); the test took it as a stand-in without
    change. Its precompile hands off to the game while giant compiles go on, and a quit before that hand-off is covered there.
  • The whole suite on this head (the top of the #428 stack).
    • By test name, against main 6c9edffe's run on the same Mac: only one status changed: test_quit_not_crash, absent on
      main, passes here.
    • These 32 checks fail here and on main on this Mac, by name (not triaged here: some need the macOS port's other pull
      requests, some are checks main added or changed since 5e072fc8): test_appimage_package_retry,
      test_appimage_player_build, test_collect_wide, test_control, test_cpm_discovery, test_crash_dialog,
      test_crash_record, test_env_on, test_game_layout, test_lift_cleanup_error, test_linux_preflight,
      test_longshot_ctl, test_low_memory_notice, test_make_game_cache, test_mem_gate, test_mem_gate_children,
      test_mirror_trim, test_oot_state_tap, test_pcache_header_read, test_pipeline_cache_file,
      test_pipeline_cache_save, test_pipeline_play_cache, test_pipeline_tour, test_prof_symbols,
      test_program_cache_cleanup, test_program_cache_one_variant, test_readback_late, test_report_run,
      test_split_giants, test_vk_pgo, test_vram_budget_row, test_walk_key.
    • The same 17 checks skipped as on main.
    • Crash reports during the run: recorder_sync_test.exe-2026-11-14-184806.ips (main's run:
      fault-arm64-2026-11-14-180704.ips, recorder_sync_test.exe-2026-11-14-180744.ips).
  • The seven checks that read the close or the steps' markers pass on this head: test_quit_not_crash,
    test_predict_resume, test_prepare_stall, test_prepare_cache_save, test_fsr4_tune, test_startup_progress and
    test_vendor_fixes.
  • arm64 and x86_64 objects of the changed renderer file, and of vk_draw.cpp, which includes the changed header.
  • After the rebase onto main aa95d35f, at ad41e49d (5f2640c1 before the rebases onto e090cc48 and aa95d35f):
    Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they
    apply) gave the same results as before; python -I -B tests/run_all.py on wave S1's merge 11a5b4e7, which carries
    this head: 568/22/17 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure
    (reports/2026-11-15-rebase-sweep-3.md).

Not tested:

  • No game run: only a real start with a GPU, and a window that stops answering, goes through the close this changes. The
    PR stays WIP until one is done.
  • Nothing ran on Windows or Linux, and no Windows (llvm-mingw) or Linux compile was made: macOS arm64 and x86_64 objects
    only. The change moves one existing call and changes comments; no new code.
  • The kill in the test is _Exit from another thread while the close waits. It stands in for Force Quit or End task: an end
    from outside after which nothing more of the process runs. No real Force Quit was used.
  • Not requested separately (one request covers this PR): a driver that crashes in the precompile, if one is known: two
    crashed starts must still drop the notes (as for #428). The test's crash and kill checks (11, all right) cover it
    game-free.

Must be tested:

  • Windows or Linux, any GPU, any driver: build this PR (head ad41e49d) and main aa95d35f the same way; on each, after a
    fresh preparation, start the game and close the window during "noted pipelines", and when it stops answering end it
    (Windows: "(Not Responding)", then "Close the program"; Linux: the desktop's Force Quit), at two starts in a row, then
    start a third time, expecting no "the last two starts did not get through" line at this PR's third start (and
    longshot_pipelines.bin kept), where main logs "vk: the last two starts did not get through the precompile: its notes
    are dropped" (on a GPU with mesh shaders, the warm's "… building the noted pipelines into the cache") (by hand: test
    request T-CEGN).

Risks

What could break:

  • A worker that crashes the driver in the seconds after a quit no longer counts. #428 counted it.
    • One that does so at every start also crashes the next start, which has no quit, and that start counts. The step is still
      let go after two crashes, with at most one extra start.
    • A crash and a kill end the process the same way, so only the quit can draw the line.
  • From the quit on, nothing can change the files: begin() and end() do nothing after close().
    • A step that finishes during the close's wait therefore leaves its file as its start found it.
    • If that start had found a crash counted, the next start tries the step once more instead of finding it cleared. Under
      #428 the same happened when the step finished after its close().
  • Landing order:
    • Depends on #428: rebased with it on main 6c9edffe (2026-11-14), #428's commit first, then this one onto it, both
      without a conflict.
    • Among the open pull requests that touch vk_main.cpp, crash_markers.h or the test, this commit adds no conflict to
      #428's (searched before the rebase).
    • #475 still conflicts with #428 in vk_draw.cpp after both rebases (git merge-tree), which this commit does not
      touch: whichever lands second needs a rebase.

Turning it off: revert this commit: that restores #428's order. There is no switch.

Compatibility: nothing a player sees changes beyond the counting: the marker files, their names and contents are
#428's. None of the changed files is in the translator stamp (nx2/translator_files.py), so no program is translated
again. Saves, the options file, the shader and pipeline caches and the vk ABI are unchanged.

Issues

See #420

Checklist

Everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description) of the status line's type.
  • Why gives the reason plainly and links the issues that fit (the tracker searched).
  • Measurements has a verdict, or one line saying why there is nothing to measure.
  • Testing names what was tested, what wasn't, and what must still be tested, where and how.
  • Rebased on current main: on aa95d35f (#761, 2026-11-15), head ad41e49d, on #428 7670318f, without a
    conflict; this PR's own commits, old → new: 5f2640c1 → ad41e49d; the commit names elsewhere in this description
    are from before the rebase.
  • python -I -B tests/run_all.py passes (any failure that also fails on main is named). On macOS: 532 passed, 17
    skipped, and 32 failures that also fail on main 6c9edffe on this Mac, named under Testing. After the rebase onto main
    aa95d35f (head ad41e49d): wave S1's merge 11a5b4e7, 568/22/17 against main aa95d35f's 543/33/17, no new failure.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

When it applies

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • A game-free test added for the fix, failing on main and passing here.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
**Type:** fix · **Risk:** low · **Tested on:** macOS 26.7.1, Apple M4 Max (game-free checks, arm64 and x86_64 object compiles; no game run) · **Needs:** Windows or Linux, any GPU **Depends on #428.** This branch is #428's commit (60d5c830) plus one commit. Review it after #428, or fold it into #428. ## Why Quitting during the start's precompile and then quitting again still counted as a crash. That is the usual reaction when the game stops answering after the first quit. Two such starts dropped the precompile's notes. With #428, a quit during the start's pipeline steps does not count as a crash. Those steps are the precompile, the create-mode warm, the predicted pipelines and the stage libraries. But #428 puts each step's file back only at the end of the close. Before that, the close waits up to 10 s for the preparation's workers to stop, on the window's thread. Meanwhile the window does not answer: - Windows marks it "Not Responding" after 5 s and lets the player close it. - The desktops on Linux and macOS offer Force Quit. A second quit there ends the process before the files are put back, so the start counted as one that crashed in the step. After two such starts the notes were dropped and the warm was skipped. After one, the stage libraries were left out. This makes a quit by the player or the control socket never count as a crash, however the process ends after it. A crash, or a kill with no quit asked for, still counts. ## How to reproduce **Seen on:** found by reading the close's order (#428's head 4dd978d6, main 9c9d69ed), and shown by the game-free test on macOS 26.7.1, Apple M4 Max, against main 6c9edffe's and #428 60d5c830's sources; not seen in a game run. The close is the same code on every OS; only the window's thread differs (below). **Settings and switches:** defaults (the start-up screen on). **Steps:** after a fresh preparation, start the game and close the window during "noted pipelines". When it stops answering ("(Not Responding)" on Windows, the desktop's Force Quit on Linux and macOS), end it. Do this at two starts in a row, then start a third time. Game-free: from this branch's checkout, `python -I -B tests/test_quit_not_crash.py --cxx clang++ --source <a checkout of main or of #428>`. **Expected:** a quit never counts as a crash: the third start tries the step again. **Actual:** the second quit ends the process before the files are put back, so each start counts as one that crashed in the step, and the third start drops the notes (`vk: the last two starts did not get through the precompile: its notes are dropped`) or skips the warm; after one, the stage libraries are left out. The test: 78 of its 101 behaviour checks wrong and 10 source checks failing on main, 36 wrong and 1 source check failing on #428. Why, in the close's order (lines at #428's head 4dd978d6, and at `main` 9c9d69ed for `host/` and the worker): - `mkwvk_close_asked` (`gpu/vk/vk_main.cpp:1567-1580`) runs in this order: - `programs_census_log`, `module_ids_flush`; - `fsr4_close` and `pipeline_cache_close`, each of which can wait up to 10 s in play; - while the shaders are prepared, `prepare_stop()` and `WaitForSingleObject(prep_ended, 10000)` (`1573-1577`); - and only then `crash_markers::close()` (`1578`). - The workers it waits for cannot cut a driver compile short, and the source names driver compiles of 15 s and 41 s (`vk_draw.cpp:8728`, `8742` at 5e072fc8). Once stopped, the worker also saves the pipeline cache before it signals that it ended (`vk_main.cpp:1191` at `main`). So a compile under way uses up the 10 s. - The close runs on the thread that answers the window: - Windows: `WM_CLOSE` in the window procedure (`host/display.c:489-493`). During the start-up screen, that thread is the renderer's start (`vk_main.cpp:1129-1132` at `main`). - Linux and macOS: the SDL event loop on the main thread (`host/posix/display_sdl.c:348-351`, `426-434`). - "The operating system defines an application hang as a UI thread that has not processed messages for at least 5 seconds", and its ghost window lets the user "close the unresponsive application" ([Microsoft](https://learn.microsoft.com/en-us/windows/win32/win7appqual/preventing-hangs-in-windows-applications)). - So the second quit ends the process before line 1578, and the step's file is left with the start's count plus one. ## What changed The close now puts the steps' files back first, before anything in it that takes time, so a quit never counts as a crash however the process ends afterwards. A crash, or a kill that no quit asked for, still counts. One call moves; the rest is comments and the extended test. - Read first: `mkwvk_close_asked` (one call moved), then `close()`'s doc in `crash_markers.h`, then the test's new ends of a start and its close. - Design: the files are put back first, not after the stop as in #428; the reason is under Risks. - `gpu/vk/vk_main.cpp`: `crash_markers::close()` is now the first statement of `mkwvk_close_asked`, before anything that takes time. The rest of the close is unchanged and in the same order. The comment above the function says so. - `gpu/vk/crash_markers.h`: comments only. The header's rule is now "only a crash, or a kill that no quit asked for, may count". `close()`'s doc says it is called first, and that a worker that crashes the driver after the quit is no longer counted. - `tests/test_quit_not_crash.py`: - New ends of a start: - `kill`: ended from outside while the step works, with no quit asked for. - `quit-killed-early`: a quit whose process is then ended in the close's first part other than the files. - `quit-killed-waiting`: the same, ended while the close waits for the workers. - For every quit, a new sequence: a quit then a crash is one crash. - The close is now taken whole from `vk_main.cpp`, in its order. The preparation's stop and the `crash_markers` lines are kept as they are. Every other statement becomes a stand-in where the process can be ended. - #428's source check wanted `crash_markers::close()` after the wait. It now wants it as the close's first statement (the reason is under Risks). Unchanged: the four steps, their files and thresholds, `host/`, and every exit path. ## Measurements **Verdict:** fixed in the game-free test: on main 78 of its 101 behaviour checks are wrong (10 source checks fail), on #428 36 (1 source check fails), here none: it passes. The whole game-free suite changes only by the new test. Correctness evidence, not a timing comparison; no game run. Commits: main 6c9edffe (2026-11-14), the base #428 and this PR are rebased on; #428: fix/quit-is-not-a-crash @ 60d5c830 (rebased from 4dd978d6); this PR: fix/double-quit-precompile @ 5f2640c1f5f838f86ca0862e9cde07fc02325126 (rebased from 6219fbbe). Baseline: #428 @ 60d5c830 is the PR this one is stacked on (this branch is #428's commit plus one), so its column shows what this PR's one commit adds; main's column shows the whole stack against main. Hardware: Apple M4 Max (16 cores), 64 GiB, macOS 26.7.1 (25G241), Apple clang 21.0.0, Python 3.14.10. No GPU or driver is involved: the checks are game-free. Scene: none (game-free checks only) Method: `tests/test_quit_not_crash.py` from this branch, run on its head and against the other trees' sources (`--source`). Before the rebase: main 5e072fc8, #428, main, #428, this PR, then the same again (interleaved twice each, on the same Mac, with the same result each time: it is deterministic; no frame time applies); one run each against main 9c9d69ed's sources, the macOS port's integrated branch (1c53cff1, which carries #428), and that branch with this commit applied. After the rebase: once each against main 6c9edffe and #428 60d5c830, and on this head. Object compiles of the changed renderer file. One whole-suite run with the wrapper #428 used (fresh HOME, short TMPDIR, `run_all.py --jobs 6`). This is correctness evidence, not a timing comparison. How to reproduce: `python -I -B tests/test_quit_not_crash.py --cxx clang++`, then the same with `--source` set to a checkout of `main` or of #428. Run it from this branch's checkout, so that the test is this PR's. Then `python -I -B tests/run_all.py`. Each check runs one step as a series of starts in a new folder, each start ending as told, then one start that gets through. That last start shows whether the step was still tried. Every quit comes while the step runs, before the start hands over to the game. | | Before: main @ 6c9edffe | Before: #428 @ 60d5c830 | After: this PR @ 5f2640c1 | |---|---|---|---| | `tests/test_quit_not_crash.py` | exit 1: 78 of 101 behaviour checks wrong, 10 source checks fail | exit 1: 36 of 101 wrong, 1 source check fails | exit 0, all pass | | quit, then ended while the close waits for the workers (18 checks over the four steps) | 18 wrong | 18 wrong | 0 | | quit, then ended in the close's first other part (18 checks) | 18 wrong | 18 wrong | 0 | | #428's quits: no start-up screen / workers stop in time / a compile outlasts the wait (18 checks each) | 18 / 6 / 18 wrong | 0 / 0 / 0 | 0 / 0 / 0 | | precompile, quit then ended while it closes: twice in a row; then a crash | both wrong (the notes dropped at the third start) | both wrong | both right | | two crashes or two kills with no quit (one each for the libraries) let the step go; a start that got through clears a crash (11 checks) | all right | all right | all right | | `vk_main.cpp` objects, arm64 and x86_64 | - | - | exit 0 | | `vk_main.cpp` and `vk_draw.cpp`, with a receipt-only header declaring `posix_fallocate` (missing on macOS, as for #428) | - | - | exit 0, arm64 and x86_64 | | whole game-free suite | 531 passed, 32 failed, 17 skipped | (its stack top is this PR) | 532 passed, 32 failed, 17 skipped | Before the rebase, with the same test: | | main 5e072fc8 and 9c9d69ed | integrated 1c53cff1 (with #428) | integrated 1c53cff1 with this commit | |---|---|---|---| | `tests/test_quit_not_crash.py` | the same 88 lines as 6c9edffe | exit 1, the same 37 lines as #428 | exit 0, all pass | Each tree printed the same output as before the rebase, byte for byte (the test is deterministic; before the rebase both runs on each tree matched too). From `main`'s run (7 of its 88 lines): ``` - precompile, starts ended by quit, quit then one through: two quits counted as crashes (got to the step's work: [True, True, False], wanted [True, True, True]) - precompile, starts ended by quit, crash then one through: a quit then a crash: wanted one crash counted (got to the step's work: [True, True, False], wanted [True, True, True]) - precompile, starts ended by quit-killed-waiting, quit-killed-waiting then one through: two quits counted as crashes (got to the step's work: [True, True, False], wanted [True, True, True]) - precompile, starts ended by quit-killed-waiting, crash then one through: a quit then a crash: wanted one crash counted (got to the step's work: [True, True, False], wanted [True, True, True]) - precompile, starts ended by crash, quit-killed-waiting, crash then one through: a quit cleared the crash before it (got to the step's work: [True, True, False, True], wanted [True, True, True, False]) - precompile: a quit (quit-killed-waiting) in a new folder left ['longshot_pipelines.bin.running'] - mkwvk_close_asked does not put the steps' files back (crash_markers::close) ``` From #428's run, the same `quit-killed-waiting` lines for the precompile, the other three steps and `quit-killed-early`, and: ``` - mkwvk_close_asked does programs_census_log before it puts the steps' files back (crash_markers::close): a player who ends the game while that takes time is counted as a crash ``` and on this PR: ``` PASS: a quit in a step leaves its file as the start found it, also when its process is ended while it closes; crashes and kills with no quit still let the step go ``` The suite's summary lines, `main` 6c9edffe first, then this PR's: ``` main @ 6c9edffe: 531 passed, 32 failed, 17 skipped in 181 s (linux, 6 at once) this PR @ 5f2640c1: 532 passed, 32 failed, 17 skipped in 226 s (linux, 6 at once) ``` The runner calls this Mac `linux`; these are macOS results. Screenshots: none; nothing on screen changes. ### Flags and settings None. No switch, menu row, ini key or default changes. ## Testing **Tested:** - The test on this PR's head, and from this head against the sources of `main` (6c9edffe; before the rebase 5e072fc8 and 9c9d69ed), #428, and, before the rebase, the macOS port's integrated branch with and without this commit. The marker code and the close it runs are cut from `vk_draw.cpp` and `vk_main.cpp` as they are. - On the integrated branch the close has a call `main` lacks (`draw_layouts_report`); the test took it as a stand-in without change. Its precompile hands off to the game while giant compiles go on, and a quit before that hand-off is covered there. - The whole suite on this head (the top of the #428 stack). - By test name, against main 6c9edffe's run on the same Mac: only one status changed: `test_quit_not_crash`, absent on main, passes here. - These 32 checks fail here and on main on this Mac, by name (not triaged here: some need the macOS port's other pull requests, some are checks main added or changed since 5e072fc8): `test_appimage_package_retry`, `test_appimage_player_build`, `test_collect_wide`, `test_control`, `test_cpm_discovery`, `test_crash_dialog`, `test_crash_record`, `test_env_on`, `test_game_layout`, `test_lift_cleanup_error`, `test_linux_preflight`, `test_longshot_ctl`, `test_low_memory_notice`, `test_make_game_cache`, `test_mem_gate`, `test_mem_gate_children`, `test_mirror_trim`, `test_oot_state_tap`, `test_pcache_header_read`, `test_pipeline_cache_file`, `test_pipeline_cache_save`, `test_pipeline_play_cache`, `test_pipeline_tour`, `test_prof_symbols`, `test_program_cache_cleanup`, `test_program_cache_one_variant`, `test_readback_late`, `test_report_run`, `test_split_giants`, `test_vk_pgo`, `test_vram_budget_row`, `test_walk_key`. - The same 17 checks skipped as on main. - Crash reports during the run: recorder_sync_test.exe-2026-11-14-184806.ips (main's run: fault-arm64-2026-11-14-180704.ips, recorder_sync_test.exe-2026-11-14-180744.ips). - The seven checks that read the close or the steps' markers pass on this head: `test_quit_not_crash`, `test_predict_resume`, `test_prepare_stall`, `test_prepare_cache_save`, `test_fsr4_tune`, `test_startup_progress` and `test_vendor_fixes`. - arm64 and x86_64 objects of the changed renderer file, and of `vk_draw.cpp`, which includes the changed header. - **After the rebase onto main aa95d35f**, at `ad41e49d` (`5f2640c1` before the rebases onto e090cc48 and aa95d35f): Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they apply) gave the same results as before; `python -I -B tests/run_all.py` on wave S1's merge `11a5b4e7`, which carries this head: 568/22/17 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure (`reports/2026-11-15-rebase-sweep-3.md`). **Not tested:** - No game run: only a real start with a GPU, and a window that stops answering, goes through the close this changes. The PR stays WIP until one is done. - Nothing ran on Windows or Linux, and no Windows (llvm-mingw) or Linux compile was made: macOS arm64 and x86_64 objects only. The change moves one existing call and changes comments; no new code. - The kill in the test is `_Exit` from another thread while the close waits. It stands in for Force Quit or End task: an end from outside after which nothing more of the process runs. No real Force Quit was used. - Not requested separately (one request covers this PR): a driver that crashes in the precompile, if one is known: two crashed starts must still drop the notes (as for #428). The test's crash and kill checks (11, all right) cover it game-free. **Must be tested:** - Windows or Linux, any GPU, any driver: build this PR (head ad41e49d) and main aa95d35f the same way; on each, after a fresh preparation, start the game and close the window during "noted pipelines", and when it stops answering end it (Windows: "(Not Responding)", then "Close the program"; Linux: the desktop's Force Quit), at two starts in a row, then start a third time, expecting no "the last two starts did not get through" line at this PR's third start (and `longshot_pipelines.bin` kept), where main logs "vk: the last two starts did not get through the precompile: its notes are dropped" (on a GPU with mesh shaders, the warm's "… building the noted pipelines into the cache") (by hand: test request T-CEGN). ## Risks **What could break:** - A worker that crashes the driver in the seconds after a quit no longer counts. #428 counted it. - One that does so at every start also crashes the next start, which has no quit, and that start counts. The step is still let go after two crashes, with at most one extra start. - A crash and a kill end the process the same way, so only the quit can draw the line. - From the quit on, nothing can change the files: `begin()` and `end()` do nothing after `close()`. - A step that finishes during the close's wait therefore leaves its file as its start found it. - If that start had found a crash counted, the next start tries the step once more instead of finding it cleared. Under #428 the same happened when the step finished after its `close()`. - Landing order: - Depends on #428: rebased with it on `main` 6c9edffe (2026-11-14), #428's commit first, then this one onto it, both without a conflict. - Among the open pull requests that touch `vk_main.cpp`, `crash_markers.h` or the test, this commit adds no conflict to #428's (searched before the rebase). - #475 still conflicts with #428 in `vk_draw.cpp` after both rebases (`git merge-tree`), which this commit does not touch: whichever lands second needs a rebase. **Turning it off:** revert this commit: that restores #428's order. There is no switch. **Compatibility:** nothing a player sees changes beyond the counting: the marker files, their names and contents are #428's. None of the changed files is in the translator stamp (`nx2/translator_files.py`), so no program is translated again. Saves, the options file, the shader and pipeline caches and the vk ABI are unchanged. ## Issues See #420 ## Checklist ### Everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`) of the status line's type. - [x] Why gives the reason plainly and links the issues that fit (the tracker searched). - [x] Measurements has a verdict, or one line saying why there is nothing to measure. - [x] Testing names what was tested, what wasn't, and what must still be tested, where and how. - [x] Rebased on current `main`: on aa95d35f (#761, 2026-11-15), head `ad41e49d`, on #428 `7670318f`, without a conflict; this PR's own commits, old → new: `5f2640c1` → `ad41e49d`; the commit names elsewhere in this description are from before the rebase. - [x] `python -I -B tests/run_all.py` passes (any failure that also fails on `main` is named). On macOS: 532 passed, 17 skipped, and 32 failures that also fail on main 6c9edffe on this Mac, named under Testing. After the rebase onto main aa95d35f (head `ad41e49d`): wave S1's merge `11a5b4e7`, 568/22/17 against main aa95d35f's 543/33/17, no new failure. - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### When it applies - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened.
fix(vk): a quit during the start's pipeline steps no longer counts as a crash
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 6s
Website / Website on Cloudflare (pull_request_target) Has been skipped
4dd978d68c
Four steps before the title leave a file that says they began and did not end, so that a driver
whose compiler crashes on one pipeline does not crash every start: the stage libraries
(longshot_prepared.bin.warming, left out after one), the predicted pipelines (.predicting), the
warm into the pipeline cache (.built.warming) and the precompile (.running), each let go after
two. Closing the window, the Insert menu's Quit and Restart now and the control socket's quit end
the process with ExitProcess (on POSIX fflush and _exit), which runs no destructor and joins no
thread, so the file stayed as if the start had crashed. Two quits in the precompile dropped its
notes; with the start-up screen this happened whenever a compile outlasted the close's 10 s wait,
and in the create-mode warm on every close.

gpu/vk/crash_markers.h keeps the steps that are open. mkwvk_close_asked, which every close path
calls first, puts each one's file back as its start found it once the preparation has stopped:
a close neither counts as a crash nor clears one. A crash or a kill still counts as before.

tests/test_quit_not_crash.py runs the marker code cut from vk_draw.cpp and the close from
vk_main.cpp as starts that end through, in a crash or in three kinds of quit. The checks that cut
predict_run and prepare_run out of the source (test_predict_resume, test_prepare_stall,
test_prepare_cache_save) read the markers through crash_markers.h now.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
fix(vk): a game ended while it closes, after a quit during the precompile, no longer counts as a crash
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
PR description / Description follows the template (pull_request_target) Successful in 5s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Third-party parts / As their notes say (pull_request) Successful in 9s
Website / Website on Cloudflare (pull_request_target) Has been skipped
6219fbbed3
The close (mkwvk_close_asked) saved the caches and waited up to 10 s for the preparation's workers before it put the steps'
crash files back (crash_markers::close), with the window not answering meanwhile. A player who quits again then ends the
process (Force Quit, End task; the system at a log-out does the same), and that start was counted as one that crashed in
the precompile, the create-mode warm, the predicted pipelines or the stage libraries: two such starts dropped the
precompile's notes. The files are now put back first, before anything that takes time. A crash, or a kill with no quit
asked for, still counts; a worker that crashes the driver after the quit no longer does (one that does so at every start
crashes the next start too).

tests/test_quit_not_crash.py: starts that are killed with no quit, and quits whose process is ended in the close's first
other part or while it waits for the workers; a quit then a crash is one crash. The source check now wants the files put
back as the close's first statement. The close is taken in its order, every other part a stand-in that can take time.

Depends on #428.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mooki force-pushed fix/double-quit-precompile from 6219fbbed3
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
PR description / Description follows the template (pull_request_target) Successful in 5s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Third-party parts / As their notes say (pull_request) Successful in 9s
Website / Website on Cloudflare (pull_request_target) Has been skipped
to 5f2640c1f5
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 5s
CI / Workflow lint (actionlint) (pull_request) Successful in 9s
Third-party parts / As their notes say (pull_request) Successful in 13s
PR description / Description follows the template (pull_request_target) Successful in 4s
2026-11-15 01:51:31 +00:00
Compare
mooki changed title from WIP: fix(vk): a game ended while it closes, after a quit during the precompile, no longer counts as a crash to fix(vk): a game ended while it closes, after a quit during the precompile, no longer counts as a crash 2026-11-15 02:39:52 +00:00
mooki force-pushed fix/double-quit-precompile from 5f2640c1f5
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 5s
CI / Workflow lint (actionlint) (pull_request) Successful in 9s
Third-party parts / As their notes say (pull_request) Successful in 13s
PR description / Description follows the template (pull_request_target) Successful in 4s
to 3ba4d24cb5
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 5s
Third-party parts / As their notes say (pull_request) Successful in 10s
2026-11-15 07:16:44 +00:00
Compare
mooki force-pushed fix/double-quit-precompile from 3ba4d24cb5
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 5s
Third-party parts / As their notes say (pull_request) Successful in 10s
to ad41e49ddf
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 7s
Third-party parts / As their notes say (pull_request) Successful in 14s
PR description / Description follows the template (pull_request_target) Successful in 5s
2026-11-15 13:09:38 +00:00
Compare
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 7s
Third-party parts / As their notes say (pull_request) Successful in 14s
PR description / Description follows the template (pull_request_target) Successful in 5s
This pull request doesn't have enough approvals yet. 0 of 1 approvals granted.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'fix/double-quit-precompile:mooki-fix/double-quit-precompile'
git switch 'mooki-fix/double-quit-precompile'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!498
No description provided.