WIP: fix(vk): closing the game while its shaders are prepared no longer counts as a crash #428
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
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/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!428
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:fix/quit-is-not-a-crash"
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?
Why
Closing the game while it prepares its shaders before the title (the window's close, the Insert menu's Quit or Restart now,
the control socket's quit) could count as a crash in that step. It always did in the create-mode warm (the default on GPUs
with mesh shaders) and on a start without the start-up screen. Otherwise it did whenever a driver compile outlasted the
close's 10 s wait. After two such closes the next start dropped the precompile's pipeline notes, or skipped the warm into
the pipeline cache, and the game then compiled those pipelines while it was being played. The stage libraries were left
out after a single one.
What this is for: keeping the "this step crashed twice, go on without it" protection for drivers whose compiler
crashes on a pipeline, but counting only real crashes and kills, not closes, on every platform. What is measured: a
new game-free test that runs the real marker code cut from
vk_draw.cppand the real close fromvk_main.cppas startsthat end through, in a crash, or in three kinds of quit. It fails 33 of its 49 behaviour checks on
mainand passes here.Also arm64 object compiles and the whole game-free suite on
mainand on this PR. Not measured: no game run on anyplatform, and nothing on Windows or Linux (see Testing).
What changed
The problem, as a chain of causes (lines at
main5e072fc8):finds the file assumes the last start died inside that step.
longshot_pipelines.bin.running:precompile_run,gpu/vk/vk_draw.cpp:9059-9073. At 2 the notesare dropped (
9064-9069). The file is removed by theDonedestructor (9073).longshot_pipelines.bin.built.warming:precompile_warm,8870-8885. At 2 the warm isskipped and
.builtis written (8878-8884). The file is removed only when the warm ends and was not stopped(
8950-8955).longshot_pipelines.bin.predicting:predict_run,9334-9348. At 2 the rest are left toplay (
9341-9345). When stopped it writes its count back (9489-9490).longshot_prepared.bin.warming:prepare_run,8657,8674-8685and8704-8711. One fileis enough: "the last start did not finish the driver's part of the shaders: left out", and that driver's part is
then marked done for good (
8827-8830).rx_close_asked(). That calls the renderer'smkwvk_close_asked(
host/rx_backend.c:353-357).host/display.c:447-453; Linux and macOShost/posix/display_sdl.c:321-324.gpu/vk/vk_menu.cpp:5190-5196postsWM_APP + 9orWM_CLOSE. Those arethe same two paths on Windows, and
display_sdl.c:393-401on Linux and macOS.host/control.c:2398-2403, the named pipe on Windows and the Unix socket elsewhere.hos_window_closed_exit(host/hos_relaunch.c:105-111) callsExitProcess(0), after starting the game again fora restart.
ExitProcessruns no destructor and joins no thread.fflush(NULL); _exit(code)(host/posix/win32_posix.c:407-411).ExitProcessends every other thread without unwinding it, then detaches the DLLs(Microsoft's page).
prep_runningis set only byprepare_with_screen(gpu/vk/vk_main.cpp:1165-1167).vk_main.cpp:1570-1574). A compile still in flight past that leaves the file behind; thesource itself names driver compiles of 15 s and 41 s (
vk_draw.cpp:8728,8742).RX_VK_STARTUP_SCREEN=0, or no window:vk_main.cpp:1343-1347), nothing is stopped and everyquit leaves it.
8950). It is the default on GPUs with meshshaders (
RX_VK_LOAD, docs/development.md), on the first start after a preparation.it.
predict_runalready wrote its count back unchanged on a close ("only one that crashed or hung there counts",2ed98c46). The other three steps did not.The change:
gpu/vk/crash_markers.h(new, header only):begin(path, count, closed)writes the step's count and remembers what aclose should leave (
closed, or no file).end(path)removes the file when the step got through.close()puts everystep still open back as its start found it. After
close(),beginandenddo nothing, so a worker still runningcannot change the files in the moment before the process ends. Its state is never freed: Windows' DLL detach then runs
nothing of it.
gpu/vk/vk_main.cppmkwvk_close_asked: callscrash_markers::close()after the preparation's stop and its 10 swait (and before DLSS's close). A worker that crashes the driver while it is stopping still counts.
gpu/vk/vk_draw.cpp, every marker write goes through the header. Each step's reading of the file and its thresholdsare unchanged:
precompile_run:begin(running, n + 1, n or no file).Doneends it only when the precompile was not stopped; astopped precompile is left for the close to put back.
precompile_warm: the same;endwhere it removed the file.predict_run:begin(path, began + 1, began). Its own write-back ofbeganon a stop goes, because the close nowdoes it whether or not the workers stopped in time.
prepare_run:begin(warming, 1, no file)where it made the empty file, which now holds1(every reader onlychecks that it is there). The stopped translation no longer removes the file itself. A file left by an earlier crash
therefore survives a close, and the stage libraries are still left out at the next start.
there, or was killed)". The doc comments of
precompile_warmandprecompile_runsay a close does not count.tests/test_quit_not_crash.py(new): see Measurements.tests/test_predict_resume.py,tests/test_prepare_stall.pyandtests/test_prepare_cache_save.pycutpredict_runandprepare_runout of the source. They now read the marker throughcrash_markers.h. Their checksare kept: the marker is made before the prediction decides, a stopped one is not removed, and a close writes the
count back.
Unchanged: what each step does, the thresholds (two crashes, one for the stage libraries), the files' names and places,
host/, and every exit path. A crash, a kill (Task Manager,SIGTERM/SIGINT, a closed console window, anout-of-memory kill, power loss) or a hang the player ends from outside still counts as before.
Flags and settings
None. No switch, menu row, ini key or default changes.
Measurements
main:
5e072fc8(2026-11-14)this PR: fix/quit-is-not-a-crash @
4dd978d6Hardware: 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.pyagainstmain's sources (--source) and on this PR's head. One whole-suite run oneach commit with the same wrapper (fresh HOME, short TMPDIR,
run_all.py --jobs 6). Object compiles of the two changedrenderer files with the renderer's flags. Correctness evidence, not a timing comparison: other work shared the Mac.
How to reproduce:
python -I -B tests/test_quit_not_crash.py --cxx clang++, then the same with--sourceset to acheckout of
main(run from this branch's checkout, so the test itself is this PR's);python -I -B tests/run_all.py.The test runs each step as a series of starts in a new folder, each one a process that ends as told:
through: the step returns.crash:_Exitin the middle of the step's work.quit: the close andExitProcesswhile the work goes on, with no start-up screen.quit-screen: the start-up screen's close, whose workers stop in time.quit-slow: the same, with a compile that does not stop within the close's wait.Then one start that gets through shows whether the step was still tried.
5e072fc84dd978d6tests/test_quit_not_crash.py.running): quit / quit-screen / quit-slow.built.warming).predicting)prepared.bin.warming)vk_main.cpp/vk_draw.cppposix_fallocateundeclared atpipeline_cache_file.h:274, which macOS lacks)posix_fallocateThe
maincolumn's failing checks, from the test's output (some of its 43 lines; all four runs againstmainprinted thesame 43):
and on this PR:
The suite's summary lines,
mainfirst:The runner calls this Mac
linux; these are macOS results.Testing
Tested:
main's sources, on the Mac above. The marker code it runs is cut fromvk_draw.cppandvk_main.cppas they are, so it follows any later change to them.#includelines,both kept. On that branch the two renderer files compile for arm64 and x86_64 without any extra header.
main; this PR adds none andremoves none (not triaged here):
test_appimage_package_retry,test_collect_wide,test_control,test_framegen_resources,test_game_layout,test_low_memory_notice,test_mem_gate,test_mem_gate_children,test_mirror_trim,test_module_ids,test_oot_state_tap,test_pad_layout,test_pcache_header_read,test_pipeline_cache_file,test_pipeline_cache_save,test_pipeline_play_cache,test_prof_symbols,test_program_cache_cleanup,test_program_cache_one_variant.three, so no other check reads the changed code.
Not tested:
only standard C++ (
<cstdio>,<mutex>,<string>,<vector>).quit-slowstands in for a driver compile that outlasts the close's 10 s; no such driver was used.Needs other testers:
and close the window during "noted pipelines" twice. Expect, at the third start, no "the last two starts did not get
through building the noted pipelines into the cache" line in
longshot_log.txt: the warm runs again, andlongshot_pipelines.bin.built.warmingis not in the game folder'scacheafter either close (request to open).RX_VK_STARTUP_SCREEN=0: quit through the control socket (HOS_CONTROL) during theprecompile twice. Expect no "its notes are dropped" line at the third start, and
longshot_pipelines.binkept (requestto open).
(request to open).
Screenshots
Nothing on screen.
Risk and rollback
crash_markers::close()and the end of the process is not counted. Thatwindow is the rest of the close: DLSS frame generation's release, a log line and
ExitProcess.mainletit go after two closes.
maindid this on purpose: its comment said two starts that did not get through it let itgo.
prepare_with_screensaves the pipeline cache on astop. So the next try is faster, not a repeat from zero, and the precompile's stall guard
(
RX_VK_PRECOMPILE_STALL_S) still lets a stuck one go.Reviewer notes
gpu/vk/crash_markers.h, thenmkwvk_close_asked, then the four steps'begin/endcalls.predict_run's rule since2ed98c46. A close that removed.running(as a timely stop did onmain) erased a real crash before it. Crash, close, crash then went on to a thirdcrash instead of dropping the notes.
git merge-treeagainst this head):vk_draw.cpporvk_main.cpp. Two conflict here and not withmain:hos_fopen_writefor the caches and markers): every marker write moves intocrash_markers.h'sput, andtests/test_predict_resume.pyreads the marker through it. If #218 lands first,puttakeshos_fopen_write, andthe call sites need nothing.
#includelines side by side.host/. It merges cleanly..running, which was found by its 2-byte size beside a run's cache.Issues
See #420
Checklist
For everyone
type(scope): description).main.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).mainand this PR interleaved, twice each, on the same Mac, with the same result each time (it is deterministic); no frame time applies.third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.mainand passing here.tests/that reads changed source updated in this PR, not weakened.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.