WIP: fix(vk): closing the game while its shaders are prepared no longer counts as a crash #428

Draft
mooki wants to merge 1 commit from mooki/Longshot:fix/quit-is-not-a-crash into main
Member

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.cpp and the real close from vk_main.cpp as starts
that end through, in a crash, or in three kinds of quit. It fails 33 of its 49 behaviour checks on main and passes here.
Also arm64 object compiles and the whole game-free suite on main and on this PR. Not measured: no game run on any
platform, and nothing on Windows or Linux (see Testing).

What changed

The problem, as a chain of causes (lines at main 5e072fc8):

  • Four steps before the title write a file beside the notes when they begin and remove it when they end. A start that
    finds the file assumes the last start died inside that step.
    • The precompile, longshot_pipelines.bin.running: precompile_run, gpu/vk/vk_draw.cpp:9059-9073. At 2 the notes
      are dropped (9064-9069). The file is removed by the Done destructor (9073).
    • The create-mode warm, longshot_pipelines.bin.built.warming: precompile_warm, 8870-8885. At 2 the warm is
      skipped and .built is written (8878-8884). The file is removed only when the warm ends and was not stopped
      (8950-8955).
    • The predicted pipelines, longshot_pipelines.bin.predicting: predict_run, 9334-9348. At 2 the rest are left to
      play (9341-9345). When stopped it writes its count back (9489-9490).
    • The stage libraries, longshot_prepared.bin.warming: prepare_run, 8657, 8674-8685 and 8704-8711. One file
      is 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).
  • Every close ends the process at once, after rx_close_asked(). That calls the renderer's mkwvk_close_asked
    (host/rx_backend.c:353-357).
    • The window's close: Windows host/display.c:447-453; Linux and macOS host/posix/display_sdl.c:321-324.
    • The Insert menu's Restart now and Quit: gpu/vk/vk_menu.cpp:5190-5196 posts WM_APP + 9 or WM_CLOSE. Those are
      the same two paths on Windows, and display_sdl.c:393-401 on Linux and macOS.
    • The control socket's quit: 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) calls ExitProcess(0), after starting the game again for
      a restart.
  • ExitProcess runs no destructor and joins no thread.
    • On Linux and macOS it is fflush(NULL); _exit(code) (host/posix/win32_posix.c:407-411).
    • On Windows, Win32's ExitProcess ends every other thread without unwinding it, then detaches the DLLs
      (Microsoft's page).
    • So a step's file is removed only if the step has returned before the process ends.
  • The close tries to make the steps return, but only sometimes.
    • It stops them only when the preparation runs behind the start-up screen. prep_running is set only by
      prepare_with_screen (gpu/vk/vk_main.cpp:1165-1167).
    • Then it waits at most 10 s (vk_main.cpp:1570-1574). A compile still in flight past that leaves the file behind; the
      source itself names driver compiles of 15 s and 41 s (vk_draw.cpp:8728, 8742).
    • Without the screen (RX_VK_STARTUP_SCREEN=0, or no window: vk_main.cpp:1343-1347), nothing is stopped and every
      quit leaves it.
    • The create-mode warm leaves its file even when it stops in time (8950). It is the default on GPUs with mesh
      shaders (RX_VK_LOAD, docs/development.md), on the first start after a preparation.
  • When the precompile did stop in time, its file was removed outright, which also wiped out a real crash counted before
    it. predict_run already 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 a
    close should leave (closed, or no file). end(path) removes the file when the step got through. close() puts every
    step still open back as its start found it. After close(), begin and end do nothing, so a worker still running
    cannot 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.cpp mkwvk_close_asked: calls crash_markers::close() after the preparation's stop and its 10 s
    wait (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 thresholds
    are unchanged:
    • precompile_run: begin(running, n + 1, n or no file). Done ends it only when the precompile was not stopped; a
      stopped precompile is left for the close to put back.
    • precompile_warm: the same; end where it removed the file.
    • predict_run: begin(path, began + 1, began). Its own write-back of began on a stop goes, because the close now
      does it whether or not the workers stopped in time.
    • prepare_run: begin(warming, 1, no file) where it made the empty file, which now holds 1 (every reader only
      checks 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.
  • The log line "the last start did not get through the precompile (closed, or it crashed there)" now says "(it crashed
    there, or was killed)". The doc comments of precompile_warm and precompile_run say a close does not count.
  • Tests:
    • tests/test_quit_not_crash.py (new): see Measurements.
    • tests/test_predict_resume.py, tests/test_prepare_stall.py and tests/test_prepare_cache_save.py cut
      predict_run and prepare_run out of the source. They now read the marker through crash_markers.h. Their checks
      are 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, an
out-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 @ 4dd978d6
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 against main's sources (--source) and on this PR's head. One whole-suite run on
each commit with the same wrapper (fresh HOME, short TMPDIR, run_all.py --jobs 6). Object compiles of the two changed
renderer 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 --source set to a
checkout 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: _Exit in the middle of the step's work.
  • quit: the close and ExitProcess while 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.

main @ 5e072fc8 this PR @ 4dd978d6
tests/test_quit_not_crash.py exit 1: 33 of 49 behaviour checks wrong, 10 source checks fail exit 0, all pass
precompile (.running): quit / quit-screen / quit-slow 4 of 4 / 1 of 4 / 4 of 4 wrong 0 / 0 / 0
create-mode warm (.built.warming) 4 of 4 / 4 of 4 / 4 of 4 wrong 0 / 0 / 0
predicted pipelines (.predicting) 4 of 4 / 0 of 4 / 4 of 4 wrong 0 / 0 / 0
stage libraries (prepared.bin.warming) 2 of 2 / 0 of 2 / 2 of 2 wrong 0 / 0 / 0
two crashes (one for the libraries) still let the step go; a start that got through clears a crash all 7 right all 7 right
arm64 objects, vk_main.cpp / vk_draw.cpp exit 0 / exit 1 (posix_fallocate undeclared at pipeline_cache_file.h:274, which macOS lacks) the same: exit 0 / exit 1, the same one error
the same, with a receipt-only header that declares posix_fallocate exit 0 / exit 0, arm64 and x86_64 exit 0 / exit 0, arm64 and x86_64
whole game-free suite 525 passed, 19 failed, 17 skipped 526 passed, 19 failed, 17 skipped: the same 19 failures and 17 skips, plus the new test

The main column's failing checks, from the test's output (some of its 43 lines; all four runs against main printed the
same 43):

- 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 crash, quit-screen, crash then one through: a quit cleared the crash before it (got to the step's work: [True, True, True, True], wanted [True, True, True, False])
- precompile: a quit (quit-slow) in a new folder left ['longshot_pipelines.bin.running']
- warm, starts ended by quit-screen, quit-screen then one through: two quits counted as crashes (got to the step's work: [True, True, False], wanted [True, True, True])
- predict: a quit (quit) left ['longshot_pipelines.bin.predicting'], wanted the prediction pending, with 0 starts that crashed in it
- libraries, starts ended by quit, quit then one through: two quits counted as crashes (got to the step's work: [True, False, True], wanted [True, True, True])
- mkwvk_close_asked does not put the steps' files back (crash_markers::close)

and on this PR:

PASS: a quit in a step leaves its file as the start found it; crashes still let the step go

The suite's summary lines, main first:

525 passed, 19 failed, 17 skipped in 177 s (linux, 6 at once)
526 passed, 19 failed, 17 skipped in 184 s (linux, 6 at once)

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

Testing

Tested:

  • The new test on this PR's head and against main's sources, on the Mac above. The marker code it runs is cut from
    vk_draw.cpp and vk_main.cpp as they are, so it follows any later change to them.
  • It also passes on the macOS port's integrated branch with this commit applied: one conflict, two #include lines,
    both kept. On that branch the two renderer files compile for arm64 and x86_64 without any extra header.
  • The whole suite on both commits. Its 19 failures here are the same 19 by name as on main; this PR adds none and
    removes 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.
  • The three checks this PR updates pass on this head. A first suite run, before they were updated, failed exactly those
    three, so no other check reads the changed code.

Not tested:

  • No game run: the change is in the start's preparation and its close, which only a real start with a GPU exercises.
  • Nothing ran on Windows or Linux. No Windows (llvm-mingw) or Linux compile of the two renderer files was made: they use
    only standard C++ (<cstdio>, <mutex>, <string>, <vector>).
  • The test's quit-slow stands in for a driver compile that outlasts the close's 10 s; no such driver was used.

Needs other testers:

  • Windows or Linux, a GPU with mesh shaders (create mode, the default there): after a fresh preparation, start the game
    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, and
    longshot_pipelines.bin.built.warming is not in the game folder's cache after either close (request to open).
  • Windows or Linux, any GPU, RX_VK_STARTUP_SCREEN=0: quit through the control socket (HOS_CONTROL) during the
    precompile twice. Expect no "its notes are dropped" line at the third start, and longshot_pipelines.bin kept (request
    to open).
  • A driver that crashes in the precompile, if one is known: two crashed starts must still drop the notes, as before
    (request to open).

Screenshots

Nothing on screen.

Risk and rollback

  • A crash in the moment between the close's crash_markers::close() and the end of the process is not counted. That
    window is the rest of the close: DLSS frame generation's release, a log line and ExitProcess.
  • A player who closes the game every time during a long step now meets that step again at every start, where main let
    it go after two closes.
    • For the create-mode warm, main did this on purpose: its comment said two starts that did not get through it let it
      go.
    • What a stopped step built stays in the driver's cache and in ours: prepare_with_screen saves the pipeline cache on a
      stop. 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.
  • Reverting the commit restores the old counting. There is no switch.

Reviewer notes

  • Start with gpu/vk/crash_markers.h, then mkwvk_close_asked, then the four steps' begin/end calls.
  • Why the close puts the count back rather than removing the file: predict_run's rule since 2ed98c46. A close that removed
    .running (as a timely stop did on main) erased a real crash before it. Crash, close, crash then went on to a third
    crash instead of dropping the notes.
  • Overlaps, searched across the 150 open pull requests (their files from the API, git merge-tree against this head):
    • 31 touch vk_draw.cpp or vk_main.cpp. Two conflict here and not with main:
      • #218 (hos_fopen_write for the caches and markers): every marker write moves into crash_markers.h's put, and
        tests/test_predict_resume.py reads the marker through it. If #218 lands first, put takes hos_fopen_write, and
        the call sites need nothing.
      • #206: two #include lines side by side.
    • #75 (local crash and unclean-end records) also treats these exits as clean ends, in host/. It merges cleanly.
  • How it was found: the macOS port's windowless runs quit through the control socket. A quit during the precompile left
    .running, which was found by its 2-byte size beside a run's cache.

Issues

See #420

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing. — Named under Testing; no game run yet, so this stays WIP.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). — Filled in; the three test requests are still to open.
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. — A fix, so the measurement is the test: run against main and this PR interleaved, twice each, on the same Mac, with the same result each time (it is deterministic); no frame time applies.
  • 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.

If applicable

  • 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.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. — Not built on Windows or Linux: only macOS arm64 and x86_64 objects. The new code is standard C++, and the marker paths are the ones the steps used before.
## 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.cpp` and the real close from `vk_main.cpp` as starts that end through, in a crash, or in three kinds of quit. It fails 33 of its 49 behaviour checks on `main` and passes here. Also arm64 object compiles and the whole game-free suite on `main` and on this PR. **Not measured:** no game run on any platform, and nothing on Windows or Linux (see Testing). ## What changed **The problem, as a chain of causes** (lines at `main` 5e072fc8): - Four steps before the title write a file beside the notes when they begin and remove it when they end. A start that finds the file assumes the last start died inside that step. - The precompile, `longshot_pipelines.bin.running`: `precompile_run`, `gpu/vk/vk_draw.cpp:9059-9073`. At 2 the notes are dropped (`9064-9069`). The file is removed by the `Done` destructor (`9073`). - The create-mode warm, `longshot_pipelines.bin.built.warming`: `precompile_warm`, `8870-8885`. At 2 the warm is skipped and `.built` is written (`8878-8884`). The file is removed only when the warm ends and was not stopped (`8950-8955`). - The predicted pipelines, `longshot_pipelines.bin.predicting`: `predict_run`, `9334-9348`. At 2 the rest are left to play (`9341-9345`). When stopped it writes its count back (`9489-9490`). - The stage libraries, `longshot_prepared.bin.warming`: `prepare_run`, `8657`, `8674-8685` and `8704-8711`. One file is 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`). - Every close ends the process at once, after `rx_close_asked()`. That calls the renderer's `mkwvk_close_asked` (`host/rx_backend.c:353-357`). - The window's close: Windows `host/display.c:447-453`; Linux and macOS `host/posix/display_sdl.c:321-324`. - The Insert menu's Restart now and Quit: `gpu/vk/vk_menu.cpp:5190-5196` posts `WM_APP + 9` or `WM_CLOSE`. Those are the same two paths on Windows, and `display_sdl.c:393-401` on Linux and macOS. - The control socket's quit: `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`) calls `ExitProcess(0)`, after starting the game again for a restart. - `ExitProcess` runs no destructor and joins no thread. - On Linux and macOS it is `fflush(NULL); _exit(code)` (`host/posix/win32_posix.c:407-411`). - On Windows, Win32's `ExitProcess` ends every other thread without unwinding it, then detaches the DLLs ([Microsoft's page](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-exitprocess)). - So a step's file is removed only if the step has returned before the process ends. - The close tries to make the steps return, but only sometimes. - It stops them only when the preparation runs behind the start-up screen. `prep_running` is set only by `prepare_with_screen` (`gpu/vk/vk_main.cpp:1165-1167`). - Then it waits at most 10 s (`vk_main.cpp:1570-1574`). A compile still in flight past that leaves the file behind; the source itself names driver compiles of 15 s and 41 s (`vk_draw.cpp:8728`, `8742`). - Without the screen (`RX_VK_STARTUP_SCREEN=0`, or no window: `vk_main.cpp:1343-1347`), nothing is stopped and every quit leaves it. - The create-mode warm leaves its file even when it stops in time (`8950`). It is the default on GPUs with mesh shaders (`RX_VK_LOAD`, docs/development.md), on the first start after a preparation. - When the precompile did stop in time, its file was removed outright, which also wiped out a real crash counted before it. `predict_run` already 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 a close should leave (`closed`, or no file). `end(path)` removes the file when the step got through. `close()` puts every step still open back as its start found it. After `close()`, `begin` and `end` do nothing, so a worker still running cannot 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.cpp` `mkwvk_close_asked`: calls `crash_markers::close()` after the preparation's stop and its 10 s wait (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 thresholds are unchanged: - `precompile_run`: `begin(running, n + 1, n or no file)`. `Done` ends it only when the precompile was not stopped; a stopped precompile is left for the close to put back. - `precompile_warm`: the same; `end` where it removed the file. - `predict_run`: `begin(path, began + 1, began)`. Its own write-back of `began` on a stop goes, because the close now does it whether or not the workers stopped in time. - `prepare_run`: `begin(warming, 1, no file)` where it made the empty file, which now holds `1` (every reader only checks 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. - The log line "the last start did not get through the precompile (closed, or it crashed there)" now says "(it crashed there, or was killed)". The doc comments of `precompile_warm` and `precompile_run` say a close does not count. - Tests: - `tests/test_quit_not_crash.py` (new): see Measurements. - `tests/test_predict_resume.py`, `tests/test_prepare_stall.py` and `tests/test_prepare_cache_save.py` cut `predict_run` and `prepare_run` out of the source. They now read the marker through `crash_markers.h`. Their checks are 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, an out-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 @ 4dd978d6 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` against `main`'s sources (`--source`) and on this PR's head. One whole-suite run on each commit with the same wrapper (fresh HOME, short TMPDIR, `run_all.py --jobs 6`). Object compiles of the two changed renderer 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 `--source` set to a checkout 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`: `_Exit` in the middle of the step's work. - `quit`: the close and `ExitProcess` while 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. | | main @ 5e072fc8 | this PR @ 4dd978d6 | |---|---|---| | `tests/test_quit_not_crash.py` | exit 1: 33 of 49 behaviour checks wrong, 10 source checks fail | exit 0, all pass | | precompile (`.running`): quit / quit-screen / quit-slow | 4 of 4 / 1 of 4 / 4 of 4 wrong | 0 / 0 / 0 | | create-mode warm (`.built.warming`) | 4 of 4 / 4 of 4 / 4 of 4 wrong | 0 / 0 / 0 | | predicted pipelines (`.predicting`) | 4 of 4 / 0 of 4 / 4 of 4 wrong | 0 / 0 / 0 | | stage libraries (`prepared.bin.warming`) | 2 of 2 / 0 of 2 / 2 of 2 wrong | 0 / 0 / 0 | | two crashes (one for the libraries) still let the step go; a start that got through clears a crash | all 7 right | all 7 right | | arm64 objects, `vk_main.cpp` / `vk_draw.cpp` | exit 0 / exit 1 (`posix_fallocate` undeclared at `pipeline_cache_file.h:274`, which macOS lacks) | the same: exit 0 / exit 1, the same one error | | the same, with a receipt-only header that declares `posix_fallocate` | exit 0 / exit 0, arm64 and x86_64 | exit 0 / exit 0, arm64 and x86_64 | | whole game-free suite | 525 passed, 19 failed, 17 skipped | 526 passed, 19 failed, 17 skipped: the same 19 failures and 17 skips, plus the new test | The `main` column's failing checks, from the test's output (some of its 43 lines; all four runs against `main` printed the same 43): ``` - 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 crash, quit-screen, crash then one through: a quit cleared the crash before it (got to the step's work: [True, True, True, True], wanted [True, True, True, False]) - precompile: a quit (quit-slow) in a new folder left ['longshot_pipelines.bin.running'] - warm, starts ended by quit-screen, quit-screen then one through: two quits counted as crashes (got to the step's work: [True, True, False], wanted [True, True, True]) - predict: a quit (quit) left ['longshot_pipelines.bin.predicting'], wanted the prediction pending, with 0 starts that crashed in it - libraries, starts ended by quit, quit then one through: two quits counted as crashes (got to the step's work: [True, False, True], wanted [True, True, True]) - mkwvk_close_asked does not put the steps' files back (crash_markers::close) ``` and on this PR: ``` PASS: a quit in a step leaves its file as the start found it; crashes still let the step go ``` The suite's summary lines, `main` first: ``` 525 passed, 19 failed, 17 skipped in 177 s (linux, 6 at once) 526 passed, 19 failed, 17 skipped in 184 s (linux, 6 at once) ``` The runner calls this Mac `linux`; these are macOS results. ## Testing **Tested:** - The new test on this PR's head and against `main`'s sources, on the Mac above. The marker code it runs is cut from `vk_draw.cpp` and `vk_main.cpp` as they are, so it follows any later change to them. - It also passes on the macOS port's integrated branch with this commit applied: one conflict, two `#include` lines, both kept. On that branch the two renderer files compile for arm64 and x86_64 without any extra header. - The whole suite on both commits. Its 19 failures here are the same 19 by name as on `main`; this PR adds none and removes 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`. - The three checks this PR updates pass on this head. A first suite run, before they were updated, failed exactly those three, so no other check reads the changed code. **Not tested:** - No game run: the change is in the start's preparation and its close, which only a real start with a GPU exercises. - Nothing ran on Windows or Linux. No Windows (llvm-mingw) or Linux compile of the two renderer files was made: they use only standard C++ (`<cstdio>`, `<mutex>`, `<string>`, `<vector>`). - The test's `quit-slow` stands in for a driver compile that outlasts the close's 10 s; no such driver was used. **Needs other testers:** - Windows or Linux, a GPU with mesh shaders (create mode, the default there): after a fresh preparation, start the game 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, and `longshot_pipelines.bin.built.warming` is not in the game folder's `cache` after either close (request to open). - Windows or Linux, any GPU, `RX_VK_STARTUP_SCREEN=0`: quit through the control socket (`HOS_CONTROL`) during the precompile twice. Expect no "its notes are dropped" line at the third start, and `longshot_pipelines.bin` kept (request to open). - A driver that crashes in the precompile, if one is known: two crashed starts must still drop the notes, as before (request to open). ## Screenshots Nothing on screen. ## Risk and rollback - A crash in the moment between the close's `crash_markers::close()` and the end of the process is not counted. That window is the rest of the close: DLSS frame generation's release, a log line and `ExitProcess`. - A player who closes the game every time during a long step now meets that step again at every start, where `main` let it go after two closes. - For the create-mode warm, `main` did this on purpose: its comment said two starts that did not get through it let it go. - What a stopped step built stays in the driver's cache and in ours: `prepare_with_screen` saves the pipeline cache on a stop. 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. - Reverting the commit restores the old counting. There is no switch. ## Reviewer notes - Start with `gpu/vk/crash_markers.h`, then `mkwvk_close_asked`, then the four steps' `begin`/`end` calls. - Why the close puts the count back rather than removing the file: `predict_run`'s rule since 2ed98c46. A close that removed `.running` (as a timely stop did on `main`) erased a real crash before it. Crash, close, crash then went on to a third crash instead of dropping the notes. - Overlaps, searched across the 150 open pull requests (their files from the API, `git merge-tree` against this head): - 31 touch `vk_draw.cpp` or `vk_main.cpp`. Two conflict here and not with `main`: - #218 (`hos_fopen_write` for the caches and markers): every marker write moves into `crash_markers.h`'s `put`, and `tests/test_predict_resume.py` reads the marker through it. If #218 lands first, `put` takes `hos_fopen_write`, and the call sites need nothing. - #206: two `#include` lines side by side. - #75 (local crash and unclean-end records) also treats these exits as clean ends, in `host/`. It merges cleanly. - How it was found: the macOS port's windowless runs quit through the control socket. A quit during the precompile left `.running`, which was found by its 2-byte size beside a run's cache. ## Issues See #420 ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [ ] Tested in the game, or what wasn't is named under Testing. — Named under Testing; no game run yet, so this stays WIP. - [ ] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). — Filled in; the three test requests are still to open. - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. — A fix, so the measurement is the test: run against `main` and this PR interleaved, twice each, on the same Mac, with the same result each time (it is deterministic); no frame time applies. - [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. ### If applicable - [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. - [ ] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. — Not built on Windows or Linux: only macOS arm64 and x86_64 objects. The new code is standard C++, and the marker paths are the ones the steps used before.
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]>
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
This pull request is marked as a work in progress.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'fix/quit-is-not-a-crash:mooki-fix/quit-is-not-a-crash'
git switch 'mooki-fix/quit-is-not-a-crash'
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!428
No description provided.