diff --git a/port/fast3d/gfx_sdl.h b/port/fast3d/gfx_sdl.h index 3611dcfa..3590f42e 100644 --- a/port/fast3d/gfx_sdl.h +++ b/port/fast3d/gfx_sdl.h @@ -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). */ diff --git a/port/fast3d/gfx_sdl2.cpp b/port/fast3d/gfx_sdl2.cpp index a43552a7..e32611e5 100644 --- a/port/fast3d/gfx_sdl2.cpp +++ b/port/fast3d/gfx_sdl2.cpp @@ -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 diff --git a/port/include/video.h b/port/include/video.h index de57d5fa..00aeb865 100644 --- a/port/include/video.h +++ b/port/include/video.h @@ -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); diff --git a/port/src/video.c b/port/src/video.c index 5e6e9d2b..b49cf5cd 100644 --- a/port/src/video.c +++ b/port/src/video.c @@ -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= 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; diff --git a/tools_pc/golden_gate.sh b/tools_pc/golden_gate.sh index ea69f6a2..631d868d 100644 --- a/tools_pc/golden_gate.sh +++ b/tools_pc/golden_gate.sh @@ -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