fix(vk): a game ended while it closes, after a quit during the precompile, no longer counts as a crash #498
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
regression
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!498
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:fix/double-quit-precompile"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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:
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, main9c9d69ed), and shown by the game-free test onmacOS 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 checkswrong 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 atmain9c9d69edforhost/and the worker):mkwvk_close_asked(gpu/vk/vk_main.cpp:1567-1580) runs in this order:programs_census_log,module_ids_flush;fsr4_closeandpipeline_cache_close, each of which can wait up to 10 s in play;prepare_stop()andWaitForSingleObject(prep_ended, 10000)(1573-1577);crash_markers::close()(1578).(
vk_draw.cpp:8728,8742at5e072fc8). Once stopped, the worker also saves the pipeline cache before it signals that itended (
vk_main.cpp:1191atmain). So a compile under way uses up the 10 s.WM_CLOSEin the window procedure (host/display.c:489-493). During the start-up screen, that thread is therenderer's start (
vk_main.cpp:1129-1132atmain).host/posix/display_sdl.c:348-351,426-434).seconds", and its ghost window lets the user "close the unresponsive application"
(Microsoft).
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.
mkwvk_close_asked(one call moved), thenclose()'s doc incrash_markers.h, then the test's new ends ofa start and its close.
gpu/vk/vk_main.cpp:crash_markers::close()is now the first statement ofmkwvk_close_asked, before anything thattakes 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, maycount".
close()'s doc says it is called first, and that a worker that crashes the driver after the quit is no longercounted.
tests/test_quit_not_crash.py: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.vk_main.cpp, in its order. The preparation's stop and thecrash_markerslines arekept as they are. Every other statement becomes a stand-in where the process can be ended.
crash_markers::close()after the wait. It now wants it as the close's first statement (thereason 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 from6219fbbe).Baseline: #428 @
60d5c830is the PR this one is stacked on (this branch is #428's commit plus one), so its column showswhat 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.pyfrom this branch, run on its head and against the other trees' sources(
--source). Before the rebase: main5e072fc8, #428, main, #428, this PR, then the same again (interleaved twice each, onthe 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
6c9edffeand #42860d5c830, and on this head. Object compiles of thechanged 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--sourceset to a checkoutof
mainor of #428. Run it from this branch's checkout, so that the test is this PR's. Thenpython -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.
6c9edffe60d5c8305f2640c1tests/test_quit_not_crash.pyvk_main.cppobjects, arm64 and x86_64vk_main.cppandvk_draw.cpp, with a receipt-only header declaringposix_fallocate(missing on macOS, as for #428)Before the rebase, with the same test:
5e072fc8and9c9d69edtests/test_quit_not_crash.py6c9edffeEach 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):From #428's run, the same
quit-killed-waitinglines for the precompile, the other three steps andquit-killed-early, and:and on this PR:
The suite's summary lines,
main6c9edffefirst, then this PR's: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:
main(6c9edffe; before the rebase5e072fc8and9c9d69ed), #428, and, before the rebase, the macOS port's integrated branch with and without this commit. The markercode and the close it runs are cut from
vk_draw.cppandvk_main.cppas they are.mainlacks (draw_layouts_report); the test took it as a stand-in withoutchange. Its precompile hands off to the game while giant compiles go on, and a quit before that hand-off is covered there.
test_quit_not_crash, absent onmain, passes here.
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.fault-arm64-2026-11-14-180704.ips, recorder_sync_test.exe-2026-11-14-180744.ips).
test_quit_not_crash,test_predict_resume,test_prepare_stall,test_prepare_cache_save,test_fsr4_tune,test_startup_progressandtest_vendor_fixes.vk_draw.cpp, which includes the changed header.aa95d35f, atad41e49d(5f2640c1before the rebases ontoe090cc48andaa95d35f):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.pyon wave S1's merge11a5b4e7, which carriesthis 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:
PR stays WIP until one is done.
only. The change moves one existing call and changes comments; no new code.
_Exitfrom another thread while the close waits. It stands in for Force Quit or End task: an endfrom outside after which nothing more of the process runs. No real Force Quit was used.
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:
ad41e49d) and mainaa95d35fthe same way; on each, after afresh 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.binkept), where main logs "vk: the last two starts did not get through the precompile: its notesare 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:
let go after two crashes, with at most one extra start.
begin()andend()do nothing afterclose().#428 the same happened when the step finished after its
close().main6c9edffe(2026-11-14), #428's commit first, then this one onto it, bothwithout a conflict.
vk_main.cpp,crash_markers.hor the test, this commit adds no conflict to#428's (searched before the rebase).
vk_draw.cppafter both rebases (git merge-tree), which this commit does nottouch: 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 translatedagain. Saves, the options file, the shader and pipeline caches and the vk ABI are unchanged.
Issues
See #420
Checklist
Everyone
type(scope): description) of the status line's type.main: onaa95d35f(#761, 2026-11-15), headad41e49d, on #4287670318f, without aconflict; this PR's own commits, old → new:
5f2640c1→ad41e49d; the commit names elsewhere in this descriptionare from before the rebase.
python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). On macOS: 532 passed, 17skipped, and 32 failures that also fail on main
6c9edffeon this Mac, named under Testing. After the rebase onto mainaa95d35f(headad41e49d): wave S1's merge11a5b4e7, 568/22/17 against main aa95d35f's 543/33/17, no new failure.third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.mainand passing here.tests/that reads changed source updated in this PR, not weakened.6219fbbed35f2640c1f5WIP: fix(vk): a game ended while it closes, after a quit during the precompile, no longer counts as a crashto fix(vk): a game ended while it closes, after a quit during the precompile, no longer counts as a crash5f2640c1f53ba4d24cb53ba4d24cb5ad41e49ddfView command line instructions
Checkout
From your project repository, check out a new branch and test the changes.