From 570eb3aceb7c7d16f16299f6285266718284f15c Mon Sep 17 00:00:00 2001 From: Cyd Date: Sun, 12 Jul 2026 18:48:56 -0500 Subject: [PATCH] Single-binary race loop: menu -> race -> menu in one process WinMain now wraps the engine block in a loop: when a front-end-launched mission ends under the local console, the setup screen comes back in the same process instead of exiting (the arcade relaunch-per-mission model). Replaces the CreateProcess self-respawn - required for Steam, where the lobby and sockets must survive across races. Second-cycle re-init crash fixed: d3d_OBJECT kept a static texture cache keyed by filename, so race 2 got IDirect3DTexture9 pointers created on race 1 destroyed device and died at first draw (DrawMesh AV). The cache is now flushed in ~DPLRenderer before the device is released, and ParticleEngine::Initialize drops particles left over from the previous mission. Verified: three consecutive 30s races in one PID, each stopped on time by the console with final scores collected. Also: L4CONSOLELEN env override for test-length races, and the console exposes MissionCompleted() for the loop. Co-Authored-By: Claude Fable 5 --- MUNGA_L4/L4D3D.cpp | 11 +++++++ MUNGA_L4/L4D3D.h | 4 +++ MUNGA_L4/L4PARTICLES.cpp | 11 +++++++ MUNGA_L4/L4VIDEO.cpp | 4 +++ RP_L4/RPL4.CPP | 70 +++++++++++++++++++++++----------------- RP_L4/RPL4CONSOLE.cpp | 20 ++++++++---- RP_L4/RPL4CONSOLE.h | 7 ++-- 7 files changed, 88 insertions(+), 39 deletions(-) diff --git a/MUNGA_L4/L4D3D.cpp b/MUNGA_L4/L4D3D.cpp index 46c15b2..5ca1274 100644 --- a/MUNGA_L4/L4D3D.cpp +++ b/MUNGA_L4/L4D3D.cpp @@ -277,6 +277,17 @@ L4TEXOP d3d_OBJECT::LoadTexture(LPDIRECT3DDEVICE9 device, const char *fileName) return texOp; } +void d3d_OBJECT::FlushTextureCache() +{ + stdext::hash_map::iterator iter; + for (iter = mTextureCache.begin(); iter != mTextureCache.end(); ++iter) + { + if ((*iter).second.texture != NULL) + (*iter).second.texture->Release(); + } + mTextureCache.clear(); +} + d3d_OBJECT::d3d_OBJECT(LPDIRECT3DDEVICE9 device, int vertCount) : mDevice(device), mVertCount(vertCount), diff --git a/MUNGA_L4/L4D3D.h b/MUNGA_L4/L4D3D.h index d3ec379..30275c7 100644 --- a/MUNGA_L4/L4D3D.h +++ b/MUNGA_L4/L4D3D.h @@ -106,6 +106,10 @@ public: static void ResetState(LPDIRECT3DDEVICE9 device); static d3d_OBJECT* LoadObject(LPDIRECT3DDEVICE9 device, char *fileName); static L4TEXOP LoadTexture(LPDIRECT3DDEVICE9 device, const char *fileName); + // Cached textures are bound to the device that created them - the + // cache MUST be flushed when that device goes away (the single-binary + // race loop tears the renderer down between missions). + static void FlushTextureCache(); private: static d3d_OBJECT* LoadSpheres(LPDIRECT3DDEVICE9 device, char *fileName); diff --git a/MUNGA_L4/L4PARTICLES.cpp b/MUNGA_L4/L4PARTICLES.cpp index 599cdc4..234905d 100644 --- a/MUNGA_L4/L4PARTICLES.cpp +++ b/MUNGA_L4/L4PARTICLES.cpp @@ -260,6 +260,17 @@ void ParticleEngine::Initialize(LPDIRECT3DDEVICE9 device) mDevice = device; memset(mInstalledEffects, 0, sizeof(mInstalledEffects)); + // particles left over from the previous mission (single-binary race + // loop) reference the old device's resources - drop them + while (mParticlesHead) + { + Particle *next = mParticlesHead->mNextParticle; + delete mParticlesHead; + mParticlesHead = next; + } + mParticlesTail = NULL; + mTotalParticleCount = 0; + ParticleEngine::mMaxParticleCount = atoi(getenv("MAXPARTICLES")); // create the vertex buffer that will store the four vertices we need for the billboards diff --git a/MUNGA_L4/L4VIDEO.cpp b/MUNGA_L4/L4VIDEO.cpp index 10f5626..de2eaeb 100644 --- a/MUNGA_L4/L4VIDEO.cpp +++ b/MUNGA_L4/L4VIDEO.cpp @@ -3495,6 +3495,10 @@ DPLRenderer::~DPLRenderer() delete mAux2Index; } + // device-bound resources living in static caches would dangle into + // the next race of the single-binary loop - drop them with the device + d3d_OBJECT::FlushTextureCache(); + SAFE_RELEASE(mDevice); SAFE_RELEASE(gD3D); //STUBBED: DPL RB 1/14/07 diff --git a/RP_L4/RPL4.CPP b/RP_L4/RPL4.CPP index 4552861..dd822ed 100644 --- a/RP_L4/RPL4.CPP +++ b/RP_L4/RPL4.CPP @@ -195,25 +195,45 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine // //------------------------------------------------------------------------- - // Front end: started without an egg, a network, or mission review -> - // run the in-game race setup and build the mission egg locally. + // Front-end mode: started without an egg, a network, or mission review. + // The race loop below keeps everything in ONE process - setup screen, + // mission, teardown, and back to the setup screen - with the local + // console marshaling each race. (-egg / -net / -mr run a single pass, + // exactly as before.) //------------------------------------------------------------------------- // - if (L4Application::GetMissionReviewMode() == 0 && + Logical front_end_mode = + L4Application::GetMissionReviewMode() == 0 && L4Application::GetNetworkCommonFlatAddress() == 0 && - !L4Application::GetEggNotationFileName()) // no egg on the command line + !L4Application::GetEggNotationFileName(); // no egg on the command line + + for (;;) + { + if (front_end_mode) { + if (!IsWindow(hWnd)) + { + break; // the cockpit window is gone (closed mid-race) + } + + // drain any WM_QUIT left over from the previous mission's + // teardown so it cannot instantly close the setup screen + MSG leftover; + while (PeekMessage(&leftover, NULL, WM_QUIT, WM_QUIT, PM_REMOVE)) + { + } + static char frontend_egg[MAX_PATH]; if (!RPL4FrontEnd_Run(hInstance, hWnd, frontend_egg, sizeof(frontend_egg))) { - return 0; // player closed the menu without launching + break; // player closed the menu without launching } L4Application::SetEggNotationFileName(frontend_egg); + Exit_Code = 0; // Front-end games are marshaled by the in-process console: it - // ends the race when the selected time expires and loops back - // to the setup screen. (Hand-fed -egg runs stay unmarshaled - - // the developer shortcut.) + // ends the race when the selected time expires. (Hand-fed -egg + // runs stay unmarshaled - the developer shortcut.) RPL4LocalConsole_Install(RPL4FrontEnd_LastMissionSeconds()); } @@ -309,6 +329,19 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine delete app_manager; } + // + //------------------------------------------------------------------------- + // Single-binary race loop: if the local console ended this mission, + // go back to the setup screen for the next one; anything else (user + // quit, -egg / -net / -mr single pass) falls out. + //------------------------------------------------------------------------- + // + if (!front_end_mode || !RPL4LocalConsole_MissionCompleted()) + { + break; + } + } + #if !_DEBUG // symmetric with the fullscreen-only hide at startup if (L4Application::GetFullscreen()) @@ -319,26 +352,5 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine Stop_Registering(); - // - //------------------------------------------------------------------------- - // Launcher role: when the local console ended the mission, respawn the - // process for the next race (the arcade launcher restarted the pod after - // every mission) - the fresh instance boots back into the setup screen. - //------------------------------------------------------------------------- - // - if (RPL4LocalConsole_ShouldRelaunch()) - { - STARTUPINFOW startup_info; - PROCESS_INFORMATION process_info; - memset(&startup_info, 0, sizeof(startup_info)); - startup_info.cb = sizeof(startup_info); - if (CreateProcessW(NULL, GetCommandLineW(), NULL, NULL, FALSE, - 0, NULL, NULL, &startup_info, &process_info)) - { - CloseHandle(process_info.hThread); - CloseHandle(process_info.hProcess); - } - } - return Exit_Code; } diff --git a/RP_L4/RPL4CONSOLE.cpp b/RP_L4/RPL4CONSOLE.cpp index 31b5ba5..9d57975 100644 --- a/RP_L4/RPL4CONSOLE.cpp +++ b/RP_L4/RPL4CONSOLE.cpp @@ -147,11 +147,10 @@ namespace break; case PhaseStopped: - // The application tears itself down after a stop (in the - // arcade the launcher restarted the pod for the next - // mission). WinMain asks ShouldRelaunch() on the way out - // and respawns the process, landing back on the setup - // screen with the collected results in the log. + // The application tears itself down after a stop (arcade + // pods were relaunched per mission). WinMain's race loop + // asks MissionCompleted() and cycles back to the setup + // screen in the same process. break; } } @@ -160,6 +159,15 @@ namespace void RPL4LocalConsole_Install(int mission_seconds) { + // debug: L4CONSOLELEN overrides the mission length (test races) + const char *override_string = getenv("L4CONSOLELEN"); + if (override_string != NULL && atoi(override_string) > 0) + { + mission_seconds = atoi(override_string); + DEBUG_STREAM << "LocalConsole: L4CONSOLELEN override, " + << mission_seconds << "s\n" << std::flush; + } + gMissionSeconds = mission_seconds; InterlockedExchange(&gLengthMs, (LONG) mission_seconds * 1000); gPhase = PhaseWaiting; @@ -184,7 +192,7 @@ void } Logical - RPL4LocalConsole_ShouldRelaunch() + RPL4LocalConsole_MissionCompleted() { return gPhase == PhaseStopped; } diff --git a/RP_L4/RPL4CONSOLE.h b/RP_L4/RPL4CONSOLE.h index e284345..7181739 100644 --- a/RP_L4/RPL4CONSOLE.h +++ b/RP_L4/RPL4CONSOLE.h @@ -21,8 +21,7 @@ void RPL4LocalConsole_Install(int mission_seconds); // True when the last mission ended under the console's control (timer -// stop or pilot exit). WinMain then plays the launcher role - the -// arcade launcher restarted the pod after every mission - and respawns -// the process, landing back on the race setup screen. +// stop or pilot exit). WinMain's single-binary race loop then cycles +// back to the setup screen for the next race in the same process. Logical - RPL4LocalConsole_ShouldRelaunch(); + RPL4LocalConsole_MissionCompleted();