Runtime lifecycle

CNA snapshot 009d40f5  ·  Development › Architecture Maps  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. The sequence was read from Game.cpp, GraphicsDeviceManager.cpp and GraphicsDevice.cpp at this snapshot; the browser and mobile paths were read, not run. The named tests exist but were not executed for this page.

The authoritative control path of a CNA game is concentrated in Game.cpp. Start there for startup, timing, event order, Update and Draw sequencing and shutdown; the graphics half of a frame continues in GraphicsDeviceManager.cpp and GraphicsDevice.cpp. This page is the architecture-level trace: what runs in which order, what owns what, and which observable behaviors are compatibility contracts rather than implementation detail. The function-by-function traces are the source tours linked at the end.

Construction and startup

Game::Game()  delegates to  Game(PlatformFactory::Create())
 ├─ platform_               InstallPlatform(): push on the live-platform stack + SetCurrentPlatform   (first member)
 ├─ platformInstallation_    scope guard: undoes the install if a later member throws; disarmed at the end
 ├─ platformCapabilities_, eventBatch_ (caller-owned event vector, reused every frame)
 ├─ Components_
 ├─ GraphicsDevice_         value member: GraphicsDevice() → resolveRenderer() → descriptor → window → renderer
 │                          (default adapter, GraphicsProfile::Reach, default PresentationParameters)
 ├─ Content_, Window_, LaunchParameters_, Services_
 └─ constructor body: default culture from the platform's locales · Window_ bound to the device's window
                    · Content_ bound to the device · RegisterAllBuiltInXnbReaders() · FrameworkDispatcher::Update()

Game::Run                                   (RunOneFrame performs the same DoInitialize once, then one Tick)
 ├─ DoInitialize
 │    ├─ IGraphicsDeviceManager::CreateDevice   (only if a manager registered itself as a service)
 │    ├─ Initialize: components' Initialize; DeviceDisposing → UnloadContent; LoadContent
 │    └─ components sorted into the update-order and draw-order lists
 ├─ BeginRun → BeforeLoop (marks the game active, raising Activated)
 ├─ RunLoop: while RunApplication → Tick;  then OnExiting
 └─ EndRun → AfterLoop

The declaration order in Game.hpp is load-bearing: platform_ is declared before every other member, so it exists before members that use its services and outlives them during destruction. Avoid moving members or introducing backend access into earlier constructors without re-auditing this order. GamePlatformOwnershipTest.ExplicitPlatformIsOwnedAndInstalledBeforeGameMembers and GamePlatformOwnershipTest.AFailedConstructionLeavesNothingInstalled pin the ownership half, and GameTest.ConstructionRegistersBuiltInXnbReadersBeforeLoadContent pins the reader registration.

Two points are easy to get backwards. First, the renderer and window already exist when the derived game's constructor body runs: GraphicsDevice_ resolves its renderer inside the base Game constructor, with the default profile and presentation parameters. Second, GraphicsDeviceManager does not create that device. Its constructor registers it as IGraphicsDeviceManager and IGraphicsDeviceService, and Game::DoInitialize then calls CreateDevice, which records that the manager does not own the device and applies the game's preferences to the existing one through applyToExistingRenderer (profile, presentation mode, then an in-place GraphicsDevice::Reset). GraphicsDeviceManagerTest.ApplyChangesRaisesResettingAndResetExactlyOnce covers the event side. Initialize defers LoadContent until DeviceCreated if a registered device service has no device yet (GameTest.DeferredLoadContentFiresOnDeviceCreatedWhenServiceHasNoDeviceAtInitializeTime). No controller subsystem is acquired at startup: the platform acquires it the first time something asks for the gamepad or joystick service, because eager acquisition cost a full device enumeration before the first frame.

What one Tick does

  1. Read the platform's monotonic counter (IPlatform::GetPerformanceCounter and GetPerformanceFrequency) and add the elapsed time to an accumulator.
  2. In fixed-timestep mode, wait until enough time has accumulated: IPlatform::Delay(1) while the accumulator plus the estimated worst-case sleep precision is short of TargetElapsedTime (the estimate is updated from measured sleeps, capped at 4 ms), then std::this_thread::yield() until the target is reached.
  3. Poll one caller-owned batch of platform events, IPlatform::PollEvents(std::vector<PlatformEvent>&), which clears and refills the vector.
  4. Pass every event first to PlatformInputBridge::ProcessEvent (SdlInputBridge.cpp), so that Exit() handling further down never stops the rest of the batch from reaching input. Then handle the runtime's own reactions: quit and close requests call Exit(); window resize, pixel-size and display-scale events refresh GameWindow and the device viewport and notify the renderer that its surface changed; focus events change IsActive; drop events reach GameWindow; application-lifecycle events suspend or resume the loop on mobile targets.
  5. Advance the snapshot services: the platform's keyboard and mouse Update() every frame, and gamepad and joystick Update() only if the gamepad subsystem has been initialized. This is the end of Game::PollEvents; there is no separate input-update method (no Game::UpdateInput exists).
  6. Clamp the accumulator to MaxElapsedTime (500 ms). In fixed mode run Update(GameTime) once per whole TargetElapsedTime in the accumulator (several times when catching up); in variable mode run it once with the accumulated time. IsRunningSlowly turns on when the accumulated catch-up lag reaches five steps and off again when it returns to zero. Game::Update runs enabled components in update order and ends with FrameworkDispatcher::Update().
  7. If drawing is allowed (Exit() and SuppressDraw() both suppress the current frame's draw), call BeginDraw, then Draw(GameTime), then EndDraw.

The first XNA-compatible update receives zero elapsed time, and total game time advances after the update, so update n sees the total that existed before its own step. The test file's header states that this was measured against the real XNA runtime and adopted over the FNA behavior, which set the elapsed time to TargetElapsedTime for every update and advanced the total before calling Update. GameClockFirstUpdateTest.TheFirstFixedStepUpdateSeesZeroElapsedTime, .TotalGameTimeIsTheTimeBeforeTheStep and .VariableStepTotalAlsoLagsItsOwnUpdate pin it. These details are observable compatibility behavior; changing them is not a timing “cleanup”. The event-order side is pinned by the golden transcript platform-event-semantics.txt through GameEventSemanticsGoldenTest.ObservableEventSemanticsMatchTheCapturedBaseline, which is instantiated over every platform implementation compiled into the binary (a pairing the selected renderer refuses is skipped, not compared). Two unguarded details in the event handler are worth knowing: the window-event case deliberately does not filter by window id (that is pinned observable behavior), and a non-repeating F9 or F10 key press calls the renderer's DebugSimulateContextLoss() or DebugRestoreContext() in every build; renderers that do not override those hooks ignore them.

Draw and present

Game::BeginDraw delegates to the registered graphics device manager, if any. GraphicsDeviceManager::BeginDraw asks the renderer whether a frame may start (IGraphicsRenderer::CanBeginDrawEXT(), default true; the DirectX 11, DirectX 12, WebGPU and EasyGL families override it so that a lost device or browser WebGL context skips Draw and presentation while Update keeps ticking) and then acquires a renderer thread-context lease for the frame with GraphicsDevice::AcquireRendererThreadContextLeaseForFrame(). The lease is null for renderers that need none and real for the GL families (EasyGL and OpenGL4 override AcquireThreadContextLeaseEXT). EndDraw calls GraphicsDevice::Present(), which takes its own short lease that restores the previous binding around the renderer's present, and then releases the frame lease, also when presentation throws. A game with no registered manager still presents: Game::EndDraw falls back to GraphicsDevice::Present() directly, without a frame lease.

GraphicsDevice::Present rejects presentation with an InvalidOperationException (“Cannot present while render targets are bound”) while a render target remains bound. That is the contract, not an oversight: the framework reports a frame that returns from Draw with a target still bound instead of unbinding it for the application, exactly as FNA does (present_lifecycle_contract_test.cpp documents the reasoning and is registered for several renderer families; it was not executed for this page).

Shutdown and destruction

Game::Dispose(true) disposes disposable components, then the content manager (which clears its cache), then the registered graphics device service, which for a manager unregisters its services, releases the frame lease, unsubscribes from device events and raises DeviceDisposing (this is what runs UnloadContent, so by then the content manager has already been disposed). It never deletes the Game-owned device. Disposal is idempotent: GameTest.DisposingDeviceInvokesUnloadContent and GameTest.RepeatedDisposeDoesNotReinvokeUnloadContent pin one UnloadContent per game.

The normal Game-owned GraphicsDevice_ remains a value member: ~Game calls Dispose(false) (which only marks the game disposed), uninstalls the ambient platform, and then destroys members in reverse declaration order. The device's destructor disposes it: it marks itself disposed, raises Disposing, disposes every registered resource while the renderer still exists, and only afterwards resets the renderer, the surface presenter and the window and releases its video-subsystem reference. The owned platform is destroyed last. The ownership and shutdown trace separates explicit disposal, destructor-only cleanup and borrowed service pointers.

Destruction bugs are usually order bugs. When a crash appears during exit, log the resource-registry drain, renderer teardown, window and context destruction, and platform service release separately, and remember that Game::Exit() only requests loop exit: Run() returning does not destroy the game.

Loop variants

  • Browser (Emscripten). RunLoop drives a callback under Asyncify and waits for requestAnimationFrame between frames. That callback is a separate code path: it polls events first, keeps its own accumulator (capped at 250 ms), runs Update per whole step, and draws only if at least one update ran in that callback. It does not use the fixed-step sleep, ignores IsFixedTimeStep = false, gives the first update a full step and advances total game time before Update, so the XNA clock rule above applies to the native Tick path only (the user guide states the same caveat). An exception in a frame is logged and stops the loop; an exception before the loop is logged in Run() and rethrown.
  • Mobile. On targets where CNA::isMobilePlatform() is true, a suspended game parks in WaitWhileSuspended (a 16 ms delay followed by an event poll) instead of ticking. WillEnterBackground suspends and deactivates; DidEnterForeground resets the timing counters so the background period is not replayed as catch-up updates; Terminating clears the suspension and calls Exit() so OnExiting still runs. GameTest.MobileLifecycleEventsSuspendResumeAndTerminateTheLoop scripts these events through a wrapper platform.
  • Embedded drivers. RunOneFrame() performs DoInitialize once and then one Tick, for hosts and tests that own the loop.

Practical tracing points

QuestionBreak or log here first
No UpdateGame::RunLoop, Game::Tick, the exit flag and event handling in Game::PollEvents
Timing spikethe monotonic-time read in AdvanceElapsedTime, the accumulator and its 500 ms clamp, the fixed-step catch-up loop
Input looks stalethe end of Game::PollEvents (the service Update() calls), then PlatformInputBridge::ProcessEvent
Draw skippedGame::BeginDraw, GraphicsDeviceManager::BeginDraw, IGraphicsRenderer::CanBeginDrawEXT, SuppressDraw and Exit
No presentationGame::EndDraw, GraphicsDeviceManager::EndDraw, GraphicsDevice::Present, the renderer's present path, and whether a render target is still bound
Exit crashGame::Dispose, the graphics resource registry drain and native teardown in GraphicsDevice::Dispose

Continue with the source tours: runtime module, startup, one frame and ownership and shutdown.

The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.

Tests and validation
Test architecture