From 943a15cef4f95cdc74fc36ac621821388551206a Mon Sep 17 00:00:00 2001 From: Cyd Date: Thu, 6 Aug 2026 00:36:35 -0500 Subject: [PATCH] A second race no longer takes the stack with it Reported by a tester and reproduced here: finish a race, come back to the lobby, start another, and the game dies a few seconds in. It is a stack overflow, from CockpitShellProc calling itself. The cockpit subclasses the game window to catch WM_SIZE and re-fit the canvas, and kept SetWindowLongPtr's return as the proc to chain on to. But the game window is not the cockpit's - it outlives it, and carries the console screen from one race to the next - and nothing ever unsubclassed it. So the second race subclassed an already-subclassed window, SetWindowLongPtr handed back CockpitShellProc itself as the "original", and from the next message onwards the proc chained to itself until the stack ran out. Nothing in the log, because nothing in the game had gone wrong yet. So the destructor puts the window's own proc back, and the install site will not subclass the same window twice even if it could not. While there: the destructor also left activeCockpit pointing at the object it had just freed, so GetCockpit() handed CockpitShellProc a dead cockpit to lay out. Harmless until someone resized or maximised the window at the lobby between races, which is not a hard thing to do. Now cleared. This came in with the cockpit resize work in 6b43971, so every build since has had it. Verified under cdb: before, the crash is a c00000fd stack overflow with CockpitShellProc / CallWindowProcA repeating the whole way down. After, four consecutive races - launch, race, results, CONTINUE, lobby, launch again - complete with no exception at all, and the process exits only when asked to. Co-Authored-By: Claude Opus 5 (1M context) --- MUNGA_L4/L4VB16.cpp | 53 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/MUNGA_L4/L4VB16.cpp b/MUNGA_L4/L4VB16.cpp index fad4f98..f0e60a5 100644 --- a/MUNGA_L4/L4VB16.cpp +++ b/MUNGA_L4/L4VB16.cpp @@ -3828,6 +3828,13 @@ static LRESULT CALLBACK //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ static WNDPROC gCockpitBaseProc = NULL; +// +// Which window we subclassed, so the destructor can put its own proc +// back. The shell is the GAME window: it outlives the cockpit and +// carries the console screen from one race to the next. +// +static HWND gCockpitShellWindow = NULL; + static LRESULT CALLBACK CockpitShellProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { @@ -4688,9 +4695,25 @@ SVGA16::SVGA16( GetClientRect(cockpit, &inner); LayoutCockpit(inner.right, inner.bottom); - // catch maximise / restore / drag-resize and re-fit - gCockpitBaseProc = (WNDPROC) SetWindowLongPtrA( - cockpit, GWLP_WNDPROC, (LONG_PTR) CockpitShellProc); + // + // Catch maximise / restore / drag-resize and re-fit - ONCE. + // + // The destructor puts the original proc back, so ordinarily + // this window is unsubclassed by the time a second race + // builds a new cockpit. The guard is for the case where it + // was not: subclassing an already-subclassed window makes + // SetWindowLongPtr hand back CockpitShellProc itself as the + // "original", and the proc below then chains to itself on + // every single message until the stack runs out. That is a + // stack overflow a few frames into the second race, with no + // hint of a cause in the log. + // + if (gCockpitShellWindow != cockpit) + { + gCockpitBaseProc = (WNDPROC) SetWindowLongPtrA( + cockpit, GWLP_WNDPROC, (LONG_PTR) CockpitShellProc); + gCockpitShellWindow = cockpit; + } // // Sticky placement for the shell. Position AND size: nothing @@ -4926,6 +4949,30 @@ SVGA16::~SVGA16() cockpitViewscreen = NULL; } + // + // Give the game window its own proc back, and stop answering for a + // cockpit that is about to stop existing. + // + // The window survives us - it is the one that shows the console + // screen between races - so both of these outlived their subject. + // The subclass was the worse of the two: the next race re-subclassed + // the same window and CockpitShellProc ended up chained to itself. + // activeCockpit was the quieter one, left pointing at this object + // after it was freed, ready for the next WM_SIZE to lay out a + // cockpit that had already gone. + // + if (gCockpitShellWindow != NULL) + { + SetWindowLongPtrA( + gCockpitShellWindow, GWLP_WNDPROC, (LONG_PTR) gCockpitBaseProc); + gCockpitShellWindow = NULL; + gCockpitBaseProc = NULL; + } + if (activeCockpit == this) + { + activeCockpit = NULL; + } + Check_Fpu(); }