LOG IN
Docs
By Opus

Engine Initialization

The path from WinMain through SDL window creation, subsystem construction, and into the main loop.

This article traces startup from WinMain to the moment the first frame ticks. The path is short on paper but strict in ordering: each subsystem depends on the ones that came before it, and the sequence in GameEngine::init is load-bearing. If you swap two lines you usually find out during a crash, not during a compile.

From WinMain to GameMain

WinMain does the Windows-specific work — registers the exception handlers, parses the command line, opens the SDL window — then hands control to GameMain. Window creation is one of the few places where platform code touches global engine state directly. initializeAppWindowsSDL in WinMain.cpp:921 creates the SDL3 window via Platform::SDLPlatform::Instance().Init(), extracts the native HWND for the D3D11 backend, and pushes the actual client size back into TheWritableGlobalData before anything else reads it:

ApplicationHWnd = (HWND)platform.GetNativeWindowHandle();

// Push the actual size into TheGlobalData *before* the renderer is created
// so the swap chain matches the real window and isn't stretched.
if (TheWritableGlobalData)
{
    Int actualW = platform.GetWidth();
    Int actualH = platform.GetHeight();
    if (actualW > 0 && actualH > 0)
    {
        TheWritableGlobalData->m_xResolution = actualW;
        TheWritableGlobalData->m_yResolution = actualH;
    }
}

Then GameMain takes over. It is deliberately tiny. GameMain.cpp:39 reads, in full:

TheFramePacer = new FramePacer();
TheFramePacer->enableFramesPerSecondLimit(TRUE);
TheGameEngine = CreateGameEngine();
TheGameEngine->init();
// ...
TheGameEngine->execute();

FramePacer is built before the engine on purpose — GameEngine::init queries it when reading frame-rate and scheduling options. CreateGameEngine in WinMain.cpp:1511 is the factory. On this build it returns an SDLGameEngine; originally it returned Win32GameEngine. The abstract factory is how the SDL port slotted in without touching gameplay code.

The subsystem init order

GameEngine::init in GameEngine.cpp:376 is one long ordered sequence of calls to initSubsystem, each of which registers the subsystem and optionally parses its default and override INI trees. The order matters in three escalating ways.

Filesystem first. TheLocalFileSystem at line 462 is created before anything else because every later INI load, every logo texture, every audio bank reaches through it. TheArchiveFileSystem at line 473 must come after — the archive system mounts .big files via the local FS. A subsystem that tries to log or read data before this point will silently find no files.

Data before consumers. TheWritableGlobalData (line 498), TheGameText (line 548), and the INI stores (TheScienceStore, TheMultiplayerSettings, TheTerrainTypes, TheTerrainRoads, TheGlobalLanguageData) all come before audio at line 571, because audio reads global volume flags and language-specific speech paths during its own init.

Factories before factory-consumers. TheModuleFactory at line 586 must exist before TheThingFactory at line 621, because every Object template instantiates the module classes the factory knows about. TheParticleSystemManager (line 592) precedes TheFXListStore (line 602) for the same reason. The standard shape of each line looks like:

initSubsystem(TheThingFactory, "TheThingFactory",
              createThingFactory(), &xferCRC,
              "Data\\INI\\Default\\Object", "Data\\INI\\Object");

The two path arguments are the default and override INI trees. The XferCRC argument accumulates a data-CRC across every INI the subsystem parses — that CRC is what multiplayer hosts compare to catch desync before a match starts.

The main loop

Once init returns, execute() runs until something sets m_quitting. GameEngine.cpp:1143 is where the game spends ~100% of its wall-clock time:

while( !m_quitting )
{
    try
    {
        update();
    }
    catch (INIException e) { /* RELEASE_CRASH */ }
    catch (...)            { /* RELEASE_CRASH */ }

    TheFramePacer->update();
}

update() walks the subsystem list and calls UPDATE() on each one in registration order — the same order init picked them up. m_quitting is a plain Bool; anything can flip it via setQuitting(TRUE) (the benchmark timer does, so does the shell menu's Exit button, so does the Windows WM_CLOSE path). There is no graceful-shutdown state — the loop just exits and GameMain deletes TheGameEngine and TheFramePacer.

Quirks

  • -xres/-yres disables the startup maximize. If either was on the command line, TheGlobalData->m_explicitDisplayResolution is TRUE and initializeAppWindowsSDL passes startMaximized=false so SDL opens the window at the exact requested size. Otherwise SDL's SDL_WINDOW_MAXIMIZED silently substitutes the desktop work area and your -xres 1280 turns into whatever fits the screen.
  • Resize is deferred. gPendingResize is set in the window proc (see WinMain.cpp:491) and acted on by the main loop, because D3D device reset from inside a window-proc callback reenters the renderer mid-frame. The SDL event pump follows the same pattern.
  • CreateGameEngine is an abstract factory. This build returns SDLGameEngine. The original returned Win32GameEngine. Nothing in GameEngine::init knows which one it is.
  • TheFramePacer is created before TheGameEngine. GameEngine::init reads pacer options, so the order in GameMain is not cosmetic.
  • Logs emitted before TheLocalFileSystem init go nowhere. DEBUG_LOG upstream of line 462 resolves to a no-op file handle. The opening "Generals version ..." banner at line 385 is the only thing that logs before the FS is up, and it has its own in-memory buffer.
  • TheNameKeyGenerator and TheCommandList are outside the subsystem list on purpose. Line 434 and line 447 construct them directly. They survive engine resets because the name-key hashing is relied on by reset() itself.