Runtime lifecycle
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
- Read the platform's monotonic counter (
IPlatform::GetPerformanceCounterandGetPerformanceFrequency) and add the elapsed time to an accumulator. - 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 ofTargetElapsedTime(the estimate is updated from measured sleeps, capped at 4 ms), thenstd::this_thread::yield()until the target is reached. - Poll one caller-owned batch of platform events,
IPlatform::PollEvents(std::vector<PlatformEvent>&), which clears and refills the vector. - Pass every event first to
PlatformInputBridge::ProcessEvent(SdlInputBridge.cpp), so thatExit()handling further down never stops the rest of the batch from reaching input. Then handle the runtime's own reactions: quit and close requests callExit(); window resize, pixel-size and display-scale events refreshGameWindowand the device viewport and notify the renderer that its surface changed; focus events changeIsActive; drop events reachGameWindow; application-lifecycle events suspend or resume the loop on mobile targets. - Advance the snapshot services: the platform's keyboard and mouse
Update()every frame, and gamepad and joystickUpdate()only if the gamepad subsystem has been initialized. This is the end ofGame::PollEvents; there is no separate input-update method (noGame::UpdateInputexists). - Clamp the accumulator to
MaxElapsedTime(500 ms). In fixed mode runUpdate(GameTime)once per wholeTargetElapsedTimein the accumulator (several times when catching up); in variable mode run it once with the accumulated time.IsRunningSlowlyturns on when the accumulated catch-up lag reaches five steps and off again when it returns to zero.Game::Updateruns enabled components in update order and ends withFrameworkDispatcher::Update(). - If drawing is allowed (
Exit()andSuppressDraw()both suppress the current frame's draw), callBeginDraw, thenDraw(GameTime), thenEndDraw.
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).
RunLoopdrives a callback under Asyncify and waits forrequestAnimationFramebetween frames. That callback is a separate code path: it polls events first, keeps its own accumulator (capped at 250 ms), runsUpdateper whole step, and draws only if at least one update ran in that callback. It does not use the fixed-step sleep, ignoresIsFixedTimeStep = false, gives the first update a full step and advances total game time beforeUpdate, so the XNA clock rule above applies to the nativeTickpath 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 inRun()and rethrown. - Mobile. On targets where
CNA::isMobilePlatform()is true, a suspended game parks inWaitWhileSuspended(a 16 ms delay followed by an event poll) instead of ticking.WillEnterBackgroundsuspends and deactivates;DidEnterForegroundresets the timing counters so the background period is not replayed as catch-up updates;Terminatingclears the suspension and callsExit()soOnExitingstill runs.GameTest.MobileLifecycleEventsSuspendResumeAndTerminateTheLoopscripts these events through a wrapper platform. - Embedded drivers.
RunOneFrame()performsDoInitializeonce and then oneTick, for hosts and tests that own the loop.
Practical tracing points
| Question | Break or log here first |
|---|---|
No Update | Game::RunLoop, Game::Tick, the exit flag and event handling in Game::PollEvents |
| Timing spike | the monotonic-time read in AdvanceElapsedTime, the accumulator and its 500 ms clamp, the fixed-step catch-up loop |
| Input looks stale | the end of Game::PollEvents (the service Update() calls), then PlatformInputBridge::ProcessEvent |
| Draw skipped | Game::BeginDraw, GraphicsDeviceManager::BeginDraw, IGraphicsRenderer::CanBeginDrawEXT, SuppressDraw and Exit |
| No presentation | Game::EndDraw, GraphicsDeviceManager::EndDraw, GraphicsDevice::Present, the renderer's present path, and whether a render target is still bound |
| Exit crash | Game::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.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- A first CNA game, read line by line — A minimal CNA game and its extension to movement, edge clamping, sound and rectangle collision, explaining each framework contract and where CNA conveniences differ from portable XNA code.
- Android and Apple targets: structure, lifecycle, assets and evidence — How CNA's Android application and NDK code are structured, how Game handles mobile lifecycle events, how assets and saves are found on a device, and what the macOS and iOS workflows check.
- Core framework and graphics API map — An orientation map of CNA's core framework and graphics API: owning headers, public shape, the boundary that trips ports, and where each behaviour is explained, for math, framework, content, resources, effects and device state.
- Exit, Exiting, Dispose and destruction — What each way of ending a CNA game runs: Exit versus Exiting, explicit Dispose versus destruction, the XNA 4.0 disposal order, repeated, re-entrant and throwing disposal, and component lifetimes at shutdown.
- Game components and the service container — Exact contracts of GameComponent, DrawableGameComponent, GameComponentCollection and GameServiceContainer in CNA: ordering, content loading, disposal without unregistration, events, iterators and type-keyed services.
- GameTime and the timestep: exact clock semantics — What CNA's game clock reports: GameTime protection, fixed-step catch-up with worked numbers, IsRunningSlowly hysteresis versus XNA 4.0, ResetElapsedTime, vsync versus timestep, and the browser clock.
- GameWindow, GraphicsDeviceManager and the supporting types — CNA's single GameWindow facade and its failure policy, GraphicsDeviceManager construction, CreateDevice, ApplyChanges and presentation preferences, and LaunchParameters, TitleContainer, TitleLocation and FrameworkDispatcher.
- The Game class: contract, run modes and extension points — Exact semantics of CNA's Game base class: event types, property guards, override points, Run versus RunOneFrame versus Tick, the browser loop, reserved debug keys and the exception boundary.
- The web target: Emscripten build contract, browser loop, storage and renderer evidence — CNA's Emscripten build contract (exception ABI, Asyncify, threads), the Asyncify browser loop, content and save storage in the virtual file system, web networking, and the evidence per browser renderer.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-078: Game::PollEvents fires the renderer's test-only context-loss hooks on F9/F10 in every build — A non-repeated F9 or F10 press in any CNA game calls the renderer's DebugSimulateContextLoss() or DebugRestoreContext(), a channel CNA itself describes as a test seam, with no build, option or opt-out guard.
- CNA-BUG-080: The Emscripten Game loop (EmscriptenMainLoopCallback) does not implement Game's loop contract — In the browser, Game ignores IsFixedTimeStep = false, SuppressDraw and ResetElapsedTime, uses FNA's clock order instead of the XNA rule the desktop loop implements, and turns a frame exception into a normal return from R
- CNA-BUG-081: Game::InactiveSleepTime is stored and validated but the loop never sleeps while the game is inactive — InactiveSleepTime is documented in Game.hpp and the C API as how long the loop sleeps while the game is inactive, but nothing in Tick, RunLoop or the browser frame reads it, so an unfocused game keeps its full frame rate
- CNA-GAP-003: CNA::Runtime and CNA::RuntimeOptions are declared in CNA/Misc.hpp but not implemented — The runtime module's public include path declares a CNA::Runtime subsystem facade with documented behaviour, but no translation unit defines it, so any call fails to link; CNA knows and guards the fact with a CMake check
- CNA-GAP-063: A fixed-step TargetElapsedTime above 500 ms never produces an Update from Game::Tick, essentially the same ceiling XNA 4.0 has — With IsFixedTimeStep true, Game::Tick clamps the accumulator to MaxElapsedTime (500 ms) before its update loop, so a TargetElapsedTime above 500 ms is never reached: Update does not run while Draw keeps running, at about
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- Game loop: lifecycle diagram · Fixed vs variable timestep · Game loop: Dispose(bool) · Game loop: GraphicsDeviceManager
- Architecture
- Architecture overview · Graphics architecture · Platform architecture
- Internals
- Runtime module internals · Startup source trace · One frame source trace · Ownership and shutdown
- Maintainer workflow
- The canonical mental model · Ownership and lifetime master map · Debug shutdown and lifetime behavior
- Tests and validation
- Test architecture
- Reference
- Test target index