D344: orderly quit so exit never lands inside the GL driver; GE_QUITFRAME

Quit (quit event, window close, Alt+F4, from either SDL pump) used to
exit(0) immediately while the render thread could be mid-frame in the
driver: the likely trigger of the host's 0x119 bugchecks. Now quits are
requested; the render thread glFinish()es, unbinds the context and parks
at a frame boundary, and the host exits after (max 2 s). GE_QUITFRAME=<n>
gives the harness a clean self-quit; golden_gate.sh uses it.
This commit is contained in:
James Dansereau committed 2026-09-27 10:38:16 -04:00
1 parent c07af11fae
commit 461ea871c2
5 files changed
+123 -11

No files matched your search

+2
View File
@@ -16,6 +16,8 @@ void gfx_sdl_make_context_current(void);
/* Release the GL context from the calling (host main) thread after init, so
* the game's scheduler thread can bind it per frame (see gfx_sdl2.cpp). */
void gfx_sdl_release_context(void);
/* D344: finish queued GL work and unbind the context (render thread, quit). */
void gfx_sdl_park_for_exit(void);
/* Refresh the cached drawable size after a window resize (called by the
* host thread's event pump). */
+21 -3
View File
@@ -9,6 +9,8 @@
#include "gfx_screen_config.h"
#include "input.h"
extern "C" void videoRequestQuit(const char *why); // port/src/video.c (D344)
static SDL_Window* wnd;
static SDL_GLContext ctx;
static SDL_Renderer* renderer;
@@ -319,7 +321,9 @@ static void gfx_sdl_handle_events(void) {
} else if (event.key.keysym.sym == SDLK_F4 && (event.key.keysym.mod & KMOD_ALT)) {
// D145: Alt+F4 quits; bare ESC no longer does (it is the
// menu "back" key -- see port/src/video.c / input.c).
exit(0);
// D344: request, don't exit(): this is the render thread,
// mid-frame. It parks at the frame's end; the host exits.
videoRequestQuit("Alt+F4 (render pump)");
} else if (event.key.keysym.sym == SDLK_ESCAPE && !event.key.repeat) {
// WI-1: this render-thread pump and port/src/video.c's
// host-thread pump both drain the same SDL queue, so either
@@ -345,7 +349,7 @@ static void gfx_sdl_handle_events(void) {
event.window.windowID == SDL_GetWindowID(wnd)) {
// We listen specifically for main window close because closing main window
// on macOS does not trigger SDL_Quit.
exit(0);
videoRequestQuit("window closed (render pump)"); // D344
} else if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
inputSetMouseGrab(0); // free + show cursor on alt-tab (also handled by video.c's pump; whichever dequeues it)
} else if (event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) {
@@ -353,7 +357,7 @@ static void gfx_sdl_handle_events(void) {
}
break;
case SDL_QUIT:
exit(0);
videoRequestQuit("quit event (render pump)"); // D344
break;
}
}
@@ -372,6 +376,20 @@ extern "C" void gfx_sdl_make_context_current(void) {
}
}
/* D344: called on the render thread when a quit was requested, at a frame
* boundary: wait for the GPU to drain everything this process queued, then
* unbind the context, so process teardown never lands inside the driver. */
extern "C" void gfx_sdl_park_for_exit(void) {
if (ctx && SDL_GL_GetCurrentContext() == ctx) {
typedef void (SDLCALL *FinishFn)(void); // = APIENTRY on Windows
FinishFn finish = (FinishFn)SDL_GL_GetProcAddress("glFinish");
if (finish) {
finish();
}
SDL_GL_MakeCurrent(NULL, NULL);
}
}
/* Release the GL context from the calling (host main) thread. WGL only allows
* a context to be current on one thread at a time, and while another thread
* still holds it, wglMakeContextCurrent fails with "The requested resource is
+7
View File
@@ -37,6 +37,13 @@ void videoEndFrame(void);
* loop from main(). Exits the process on QUIT/ESC/close. */
void videoPumpEvents(void);
/* D344: orderly quit. Any thread may request it; the render thread parks at
* the next frame boundary (glFinish + context released) and the host thread
* then exits. Never call exit() directly for a normal quit: the render thread
* could be inside the GL driver (the 0x119 bugchecks). */
void videoRequestQuit(const char *why);
int videoQuitRequested(void);
/* The game's native video mode (NTSC 640x480, PAL 640x400). fast3d scales
* N64 screen coordinates into window pixels using this. */
void videoUpdateNativeResolution(s32 w, s32 h);
+87 -6
View File
@@ -596,6 +596,68 @@ int videoInit(void)
return 0;
}
/* ---- D344: orderly quit ------------------------------------------------
* Rendering runs on the scheduler thread with the GL context bound; quit
* events arrive on the host thread (or on fast3d's render-thread pump).
* exit() used to run straight from whichever pump saw the event, tearing the
* process down while the render thread could be inside the NVIDIA driver --
* the likely trigger of the 0x119 VIDEO_SCHEDULER_INTERNAL_ERROR bugchecks
* (findings D344). Now:
* 1. anyone calls videoRequestQuit();
* 2. the render thread, at its next frame boundary, glFinish()es, unbinds
* the context and parks for good (videoRenderPark);
* 3. the host thread waits until the render thread is parked or outside a
* frame (max QUIT_WAIT_MS), then exit(0) (atexit saves config).
* Frame entry sets s_inFrame BEFORE checking s_quitReq, and the host sets
* s_quitReq BEFORE reading s_inFrame (SDL atomics are sequentially
* consistent), so a frame can never start once the host has decided to exit. */
#define QUIT_WAIT_MS 2000
static SDL_atomic_t s_quitReq;
static SDL_atomic_t s_inFrame;
static SDL_atomic_t s_parked;
static int s_quitFrame = -1; /* GE_QUITFRAME: harness self-quit, -1 = unread */
void videoRequestQuit(const char *why)
{
if (SDL_AtomicCAS(&s_quitReq, 0, 1)) {
sysLogPrintf(LOG_INFO, "video: quit requested (%s)", why ? why : "?");
}
}
int videoQuitRequested(void)
{
return SDL_AtomicGet(&s_quitReq) != 0;
}
/* Render thread only. Never returns. */
static void videoRenderPark(void)
{
gfx_sdl_park_for_exit();
SDL_AtomicSet(&s_inFrame, 0);
SDL_AtomicSet(&s_parked, 1);
for (;;) {
sysSleep(100000);
}
}
/* Host thread: exit once the render thread is out of the GL driver. */
static void videoHostExitIfRequested(void)
{
if (!SDL_AtomicGet(&s_quitReq)) {
return;
}
Uint32 start = SDL_GetTicks();
while (!SDL_AtomicGet(&s_parked) && SDL_AtomicGet(&s_inFrame) &&
SDL_GetTicks() - start < QUIT_WAIT_MS) {
SDL_Delay(2);
}
sysLogPrintf(LOG_INFO, "video: exiting (render %s after %u ms)",
SDL_AtomicGet(&s_parked) ? "parked"
: (SDL_AtomicGet(&s_inFrame) ? "STILL IN FRAME (timeout)" : "idle"),
(unsigned)(SDL_GetTicks() - start));
exit(0);
}
void videoDestroy(void)
{
if (initDone) {
@@ -609,6 +671,12 @@ void videoStartFrame(void)
if (!initDone) {
return;
}
/* D344: enter the frame first, then check for a quit (see above). */
SDL_AtomicSet(&s_inFrame, 1);
if (SDL_AtomicGet(&s_quitReq)) {
videoRenderPark();
}
/* Rendering runs on the game's scheduler thread; the GL context was
* created on the host main thread. */
gfx_sdl_make_context_current();
@@ -647,13 +715,13 @@ void videoPumpEvents(void)
* scheduler thread (must run here, on the window's creating thread). */
videoDrainWindowRequests();
inputApplyMouseRequests(); /* D287: mouse mode/cursor, same rule */
videoHostExitIfRequested(); /* D344: quit posted last pump or by another thread */
SDL_Event ev;
while (SDL_PollEvent(&ev)) {
switch (ev.type) {
case SDL_QUIT:
sysLogPrintf(LOG_INFO, "video: quit requested");
exit(0);
videoRequestQuit("quit event");
break;
case SDL_KEYDOWN:
/* D145: bare ESC used to exit(0). On the front-end / debrief
@@ -663,8 +731,7 @@ void videoPumpEvents(void)
* (back / cancel) via input.c; quitting is window-close (the X) or
* Alt+F4 only. */
if ((ev.key.keysym.sym == SDLK_F4) && (ev.key.keysym.mod & KMOD_ALT)) {
sysLogPrintf(LOG_INFO, "video: Alt+F4 -> quit");
exit(0);
videoRequestQuit("Alt+F4");
} else if (ev.key.keysym.sym == SDLK_F12 && !ev.key.repeat) {
screenshotReq = 1;
} else if (ev.key.keysym.sym == SDLK_F10 && !ev.key.repeat) {
@@ -701,8 +768,7 @@ void videoPumpEvents(void)
break;
case SDL_WINDOWEVENT:
if (ev.window.event == SDL_WINDOWEVENT_CLOSE) {
sysLogPrintf(LOG_INFO, "video: window closed");
exit(0);
videoRequestQuit("window closed");
} else if (ev.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
gfx_sdl_update_cached_size();
} else if (ev.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
@@ -792,6 +858,21 @@ void videoEndFrame(void)
}
gfx_end_frame();
/* D344: the frame (and its swap) is done. Park here on a quit request, so
* the host never exits under an in-flight frame. GE_QUITFRAME=<n> is the
* harness's clean self-quit (replaces `timeout` kills). */
if (s_quitFrame == -1) {
const char *q = getenv("GE_QUITFRAME");
s_quitFrame = (q && atoi(q) > 0) ? atoi(q) : 0;
}
if (s_quitFrame > 0 && frames + 1 >= (u32)s_quitFrame) {
videoRequestQuit("GE_QUITFRAME");
}
if (SDL_AtomicGet(&s_quitReq)) {
videoRenderPark();
}
SDL_AtomicSet(&s_inFrame, 0);
++frames;
++fpsNumFrames;
+6 -2
View File
@@ -67,8 +67,12 @@ printf '[Window]\nWidth = 640\nHeight = 480\n' > "$INI"
# --- run ---------------------------------------------------------------------
rm -rf ppm
PATH="/c/msys64/mingw64/bin:$PATH" GE_PCDUMP="$DUMP" \
timeout 90 ./build-pc/ge007.x86_64.exe "-level_$LEVEL" > "$LOG" 2>&1
# D344: quit cleanly 30 frames after the last capture (GE_QUITFRAME) instead
# of being killed by `timeout`; killing the game mid-frame is the likely
# trigger of the host's 0x119 bugchecks. timeout stays as a backstop only.
DUMP_HI=${DUMP#*-}; DUMP_HI=${DUMP_HI%%:*}
PATH="/c/msys64/mingw64/bin:$PATH" GE_PCDUMP="$DUMP" GE_QUITFRAME=$((DUMP_HI + 30)) \
timeout 120 ./build-pc/ge007.x86_64.exe "-level_$LEVEL" > "$LOG" 2>&1
if [ "$GOLDEN" = 1 ]; then
python tools_pc/framediff.py ppm > "$FRAMEDIFF_OUT" 2>&1