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 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-12 18:48:56 -05:00
co-authored by Claude Fable 5
parent 9f79508257
commit 570eb3aceb
7 changed files with 88 additions and 39 deletions
+11
View File
@@ -277,6 +277,17 @@ L4TEXOP d3d_OBJECT::LoadTexture(LPDIRECT3DDEVICE9 device, const char *fileName)
return texOp;
}
void d3d_OBJECT::FlushTextureCache()
{
stdext::hash_map<std::string, L4TEXOP>::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),
+4
View File
@@ -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);
+11
View File
@@ -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
+4
View File
@@ -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
+41 -29
View File
@@ -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;
}
+14 -6
View File
@@ -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;
}
+3 -4
View File
@@ -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();