The Game class: contract, run modes and extension points
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 browser loop was read, not run; XNA 4.0 comparisons come from a disassembly of the XNA 4.0 Game assembly, not from an oracle run. No test asserts the relative order of Exiting, EndRun and AfterLoop, nor the Exit-before-Run and second-Run edges.
Every CNA program derives from Microsoft::Xna::Framework::Game and hands it control with Run(). This page is the precise contract of that base class for people porting XNA or FNA code and for anyone who drives the loop themselves: what the events are typed as, which properties validate or silently store, what each override point's base implementation does, how Run(), RunOneFrame() and Tick() differ at their edges, what the browser loop changes, and which exceptions can reach main(). The function-by-function source traces are the startup and one-frame internals pages; this page stays at the level of observable semantics.
A base class, not an entry point
CNA has no engine-owned main(). The game executable constructs its derived Game and calls Run(); Game is declared in Game.hpp as class Game : public System::Object, public System::IDisposable, and its own brief describes it as the base class that provides the XNA-style game loop, services, window, content and components. The shape is XNA's Program.Main on purpose, so that an XNA entry point ports almost mechanically.
What already exists when the derived constructor runs
The base constructor (Game.cpp, Game::Game) finishes before the derived constructor body starts, and by then it has installed the game's platform as the ambient one, constructed a real GraphicsDevice value member (renderer resolved, window created for renderers that need one, default GraphicsProfile::Reach and default presentation parameters), bound the window and content manager to that device, registered every built-in XNB reader and called FrameworkDispatcher::Update() once. A GraphicsDeviceManager constructed in the derived constructor therefore does not create a device; it registers itself as a service and its preferences are applied later, by CreateDevice() inside DoInitialize, as an in-place reset of the existing device. The consequence for game code: calling getGraphicsDeviceProperty() in a derived constructor is legal and returns a live device, but it is the pre-preference device, so GPU resources and content still belong in LoadContent(). The full construction order is on Startup source trace.
Four events, all typed EventArgs
Game declares four public events: Activated, Deactivated, Disposed and Exiting, all of type System::EventHandler<System::EventArgs>. Each is raised with the game itself as sender; OnExiting, OnActivated and OnDeactivated ignore the sender argument they are given and raise with this.
The Exiting type deserves a note. CNA also ships an empty ExitingEventArgs class (ExitingEventArgs.hpp), but Game::Exiting is not declared with it, nothing in the runtime constructs one, and its only tests are that it is default-constructible and derives from System::EventArgs. That declaration is the XNA-faithful one: the XNA 4.0 Microsoft.Xna.Framework.Game assembly declares Game.Exiting as EventHandler<EventArgs>, and no ExitingEventArgs type exists in the XNA 4.0 assemblies at all (checked against a disassembly of those assemblies, not a CNA file). So the dedicated type is a CNA-specific addition, which is how CNA's own AUDIT.md lists it, not an XNA type that CNA declines to use. Handlers are written as [](System::Object* sender, const System::EventArgs& e); a handler written against ExitingEventArgs does not compile (checked with g++ -fsyntax-only: no matching operator+=). The C API header runtime.h draws the same conclusion for its event callbacks: nothing is lost by exposing only the sender. Exiting is a notification; there is no cancellation argument.
Properties, their defaults and their guards
Properties follow the project-wide getXProperty() / setXProperty() convention (language conventions). The table lists what the accessors actually do at this snapshot, including the ones that store a value without acting on it.
| Property | Access | Default | Behaviour worth knowing |
|---|---|---|---|
Components | get | empty | A GameComponentCollection member; see components and services. |
Content | get / set | root Content | setContentProperty(const ContentManager&) copy-assigns into the game's own member; the game never switches to a different object. |
GraphicsDevice | get only | the game's own device | Returns the registered IGraphicsDeviceService's device when one is registered and has a device, otherwise the game's own member. The service pointer is looked up once and cached. There is no setter. |
InactiveSleepTime | get / set | 20 ms | A negative value throws std::out_of_range (the message says "must be positive", but zero is accepted). The value is stored and read by nothing in the loop: an unfocused CNA game keeps its normal rate. XNA 4.0's Tick sleeps for this span while the game is inactive. |
IsActive | get only | false | See below. |
IsFixedTimeStep | get / set | true | Read on every tick; switching it mid-game takes effect on the next tick. Clock details: GameTime and the timestep. |
IsMouseVisible | get / set | false | Setting the current value is a no-op. A change reaches the platform mouse service only when a platform window exists and the platform has a mouse service; otherwise the value is only recorded. |
LaunchParameters | get (mutable) | parsed from the process command line | Empty on platforms where CNA cannot read the command line; see supporting types. |
TargetElapsedTime | get / set | 166,667 ticks | Zero or negative throws std::out_of_range. The default is 16.6667 ms, the nearest tick count to 1/60 s. |
Services | get | empty | The GameServiceContainer; it owns nothing. |
Window | get | bound to the device's window | A facade over the platform window the device created or adopted. |
IsActive has no public setter
FNA declares IsActive with an internal set, so only the framework may change it. C++ has no assembly-internal visibility, so CNA makes setIsActiveProperty private: it returns early when the value does not change and otherwise raises Activated or Deactivated through the protected virtuals. Only Game itself calls it: BeforeLoop() (true, once per Run()), focus-gained and focus-lost window events, and on mobile targets the background and foreground lifecycle events. OnActivated and OnDeactivated call AssertNotDisposed(), so a focus change delivered to a disposed game throws std::runtime_error; OnExiting has no such check.
The CNA extensions on Game
Members marked CNAEXT are not XNA 4.0 API (with CNA_STRICT_XNA_API the marker becomes [[deprecated]], see core module internals):
RunApplication— a publicbool, documented as the internal loop flag matching the FNA/XNA implementation shape. It is the desktop loop condition,Exit()clears it, and nothing but application code ever sets it back totrue.getTargetFPSProperty(),getTargetMsFrameTimeProperty()and the staticfpsToMillisecondsPerFrame(intcs)— compatibility helpers kept for existing CNA examples that talk about frame timing in frames per second. They are derived fromTargetElapsedTime, so with the default of 166,667 ticksgetTargetFPSProperty()returns about 59.99988, not 60.fpsToMillisecondsPerFramereturns 0 for a non-positive argument instead of throwing.GetPlatformEXT()andGetPlatformCapabilitiesEXT()— the platform the game owns and the capability set cached once at construction (capability discovery may probe a terminal, so it must not become a per-frame query).- The protected constructor
Game(std::unique_ptr<CNA::Platform::IPlatform>)for embedding hosts and tests; a null platform throwsstd::invalid_argument.
Override points and what the base implementation does
A derived game overrides protected virtuals. Several base implementations do real framework work, so whether an override calls the base is part of the contract, not style.
| Virtual | Called from | Base implementation | An override that skips the base |
|---|---|---|---|
Initialize() | DoInitialize, once | Initializes current components by index (re-reading the count, so a component added by another's Initialize is initialized too), caches the IGraphicsDeviceService, subscribes UnloadContent to its DeviceDisposing, then calls LoadContent() — or defers it to DeviceCreated when the service has no device yet | Components are never initialized, LoadContent() is never called and the UnloadContent route does not exist |
LoadContent() / UnloadContent() | base Initialize() / the service's DeviceDisposing | Empty | Nothing lost |
Update(GameTime&) | Tick, once per step | Updates enabled components in UpdateOrder, then FrameworkDispatcher::Update() | No component updates, and no dynamic-sound buffers, microphone checks, media-player transitions or touch-panel updates |
Draw(const GameTime&) | Tick, after the updates, when BeginDraw() is true | Draws visible components in DrawOrder | No DrawableGameComponent is drawn |
BeginDraw() | Tick | The registered manager's BeginDraw(), or true without one | Returning false skips both Draw and EndDraw for that tick |
EndDraw() | Tick, after Draw | The manager's EndDraw() (which presents), or GraphicsDevice::Present() without a manager | Nothing is presented |
BeginRun() / EndRun() | Run() only | Empty | Nothing lost; never called under RunOneFrame() |
OnExiting, OnActivated, OnDeactivated | the loop and window events | Raise Exiting, Activated, Deactivated | The corresponding event is never raised |
ShowMissingRequirementMessage(const std::exception&) | nothing at this snapshot | Returns false | Overriding it has no effect |
Dispose(bool) | public Dispose() (true) and the destructor (false) | See Exit, Exiting, Dispose and destruction | Components, content and the device service are not disposed |
Two rows need a comment. The safe shape for Update is game work plus exactly one base call per step; a port that deliberately replaces the component loop must still call FrameworkDispatcher::Update() itself once per step, and calling both the base and the dispatcher pumps those services twice. ShowMissingRequirementMessage is inert: no CNA code calls it. In the XNA 4.0 assembly, Game.Run (through its private RunGame) catches NoSuitableGraphicsDeviceException and NoAudioHardwareException, offers them to ShowMissingRequirementMessage and rethrows when it returns false; in CNA those failures surface as ordinary exceptions from the constructor or from Run(), so a port that relied on the hook to show a friendly message must catch in main() instead.
Run(), RunOneFrame() and Tick()
For an ordinary first desktop Run() the source gives this order:
AssertNotDisposed(), thenDoInitialize()if the game has not been initialized: look up the registeredIGraphicsDeviceManagerand call itsCreateDevice(), callInitialize(), sort the current components into the update and draw lists, subscribe toComponentAdded/ComponentRemoved.BeginRun(), thenBeforeLoop(), which setsIsActiveand so raisesActivatedthe first time.- The performance counter is seeded and
RunLoop()callsTick()whileRunApplicationis true.Exit(), a quit event, a window close request or a mobileTerminatingevent clears it. - After the loop,
RunLoop()callsOnExiting(), which raisesExiting. - Only then does
Run()callEndRun()andAfterLoop()and return. Nothing is disposed.
The whole sequence sits in one try: an exception from any step is logged through CNA::Logger::Error ("fatal exception escaped Game::Run()") and rethrown unchanged.
Edges of the run sequence
The first two edges describe the desktop loop; in a browser the frame body always runs at least once before it observes the cleared flag.
Exit()beforeRun()does not bypass the lifecycle. Initialization,BeginRunandBeforeLoopstill run, the loop performs zero ticks,Exitingis raised, andEndRunandAfterLoopfollow.- A second
Run()after the first returned does not resetRunApplication. Initialization is skipped (it happened once),BeginRunandBeforeLooprun again (IsActiveis already true, so no secondActivated), the loop performs zero ticks andExitingis raised a second time. Setting the publicRunApplicationback totruefirst makes the secondRun()tick normally. AfterDispose(),Run()throwsstd::runtime_error. RunOneFrame()performs the one-time initialization and seeds the counter on its first call, then oneTick(). It never callsBeginRun,BeforeLoop,OnExiting,EndRunorAfterLoop. Because onlyBeforeLoopactivates the game,IsActivestaysfalseunderRunOneFrame()until a focus or lifecycle event arrives — an easy trap for editor hosts and tests.Exit()inside such a host only clearsRunApplicationand suppresses the current draw; the host has to read the flag itself.Tick()is public. Calling it directly withoutRun()orRunOneFrame()skips initialization entirely: noCreateDevice, noInitialize, noLoadContent, empty component lists. The firstTick()also finds an unseeded counter, so its clock read only seeds it, and in fixed-step mode that tick then waits a full target span before updating.Run()afterRunOneFrame()does not initialize again; it runs the begin hooks, reseeds the counter and enters the loop.
Evidence: GameTest.RunExecutesLifecycleInDocumentedOrder in GameTests.cpp asserts one Initialize, one LoadContent and at least one Update and Draw; despite its name it does not assert the relative order of Exiting, EndRun and AfterLoop, and no test covers Exit() before Run() or a second Run(). The order above is read from the source; none of these tests was executed for this page.
The browser run loop
Under Emscripten the public methods are the same but the loop is a different body. RunLoop() stores the game in a static s_emLoopState and repeatedly calls EmscriptenMainLoopCallback(), suspending the same Wasm stack between frames with an EM_ASYNC_JS function that awaits requestAnimationFrame (Asyncify). Run therefore blocks and returns as on the desktop, which is why a stack-allocated game is valid on the web (emscripten-mainloop-game-lifetime.md records the earlier non-returning registration and why it was replaced). The lifecycle consequences:
- The frame body raises
Exitingitself when it observesRunApplication == falseand clears the loop pointer;RunLoop()then returns andRun()callsEndRun()andAfterLoop(). Older descriptions that say the post-loop hooks never run in a browser describe the replaced implementation. - An exception thrown by a frame is caught in the frame body, logged ("fatal exception in Emscripten main loop") and ends the loop — and
Exitingis not raised.Run()then callsEndRun()andAfterLoop()and returns normally, so on the web a frame exception does not propagate tomain(). - The loop state is one static slot: one
Gameruns its loop per process. Destroying the game that owns the loop whileRun()is active clears the slot and logs an error so the loop stops before the game's resources go. - Timing inside the frame body differs from the desktop tick in every detail (clock source, step policy, first update,
IsRunningSlowly, draw cadence); One frame source trace tabulates it and GameTime and the timestep lists what a game can observe.
Game::Run()
|
AssertNotDisposed, DoInitialize (once)
CreateDevice -> Initialize -> LoadContent
|
BeginRun -> BeforeLoop (IsActive = true)
|
+--------------+-------------------------------+
| desktop | Emscripten (Asyncify)
v v
while (RunApplication) while (loop slot == this)
Tick: wait, PollEvents, frame body: PollEvents, clock,
Update x steps, Draw, EndDraw Update x steps, Draw if updated
| await requestAnimationFrame
v |
OnExiting (Exiting raised) RunApplication false -> Exiting
| frame exception -> logged, no Exiting
+--------------+-------------------------------+
v
EndRun -> AfterLoop -> Run() returns
(nothing disposed; Dispose() is explicit)
Game::Run() at this snapshot. Both targets share initialization, BeginRun and BeforeLoop, then split: the desktop loop ticks while RunApplication is true and raises Exiting after the loop; the browser loop runs a separate frame body between requestAnimationFrame suspensions and raises Exiting from inside that body, except after a frame exception. Both paths rejoin at EndRun and AfterLoop, and Run() returns without disposing anything.F9 and F10 are reserved in every game
Game::PollEvents passes every native event to the input bridge first and then reacts to it. A non-repeated press of F9 calls the renderer's DebugSimulateContextLoss() and F10 calls DebugRestoreContext(), with no build guard. Because input saw the key first, a game that binds F9 or F10 gets its own key state and the framework action. The default hooks in IGraphicsRenderer are empty; at this snapshot the EasyGL, DirectX 9, DirectX 11, DirectX 12, Direct2D and WebGPU families declare non-empty overrides (GDI overrides both with empty bodies), so on those renderers the keys rehearse a loss and a recovery, with two consequences that are easy to miss. On DirectX 11, DirectX 12 and WebGPU a lone F9 marks the device lost and, with a registered GraphicsDeviceManager, BeginDraw() then refuses every frame until F10 restores it, so the game keeps updating but stops drawing. On WebGPU, F9 also throws System::NotSupportedException while a TextureCube, RenderTargetCube, Texture3D, occlusion query or custom ShaderEffect is alive, and nothing in PollEvents, Tick or Run catches it: natively it propagates out of Run() after the logged “fatal exception” line, and in the browser loop it is logged and the loop stops without raising Exiting (CNA-BUG-078). XNA's Game reserves no keys at all. What each family does is renderer-specific; the event table is on One frame source trace and the debugging advice on Debugging.
One catch clause at the framework boundary
The runtime module does not translate failures into an XNA exception hierarchy. Its sources and headers contain 21 explicit throw sites at this snapshot, all standard types: std::out_of_range (the two Game property guards, collection indices), std::invalid_argument (null platform, duplicate component, null or duplicate service, null game or second manager), std::runtime_error (use after dispose, CreateDevice without a game-owned device, TitleContainer failures), std::logic_error (the unreachable collection SetItem) and std::bad_alloc (TitleContainer buffers). Use after disposal is therefore a plain std::runtime_error ("The Game object was used after being disposed."), not a dedicated disposed-object type.
Calls into other modules add two families. One derives from std::runtime_error: CNA::Platform::PlatformException (for example a failed window setter on the SDL3 platform), ContentLoadException, the XML and JSON parse exceptions and XnbWriteException. The other derives from Sharp Runtime's System::Exception: the graphics device exceptions (NoSuitableGraphicsDeviceException, DeviceLostException, DeviceNotResetException), CNAException, microphone, gamer-services, network, sensor, engine-layer and pipeline exceptions. System::Exception derives from std::exception, not from std::runtime_error (sharp-runtime next @ 41b918c9, System/Exception.hpp). Consequently catch (const std::runtime_error&) misses the whole System::* family and catch (const System::Exception&) misses every standard and platform failure; only catch (const std::exception&) spans both.
int main()
{
try {
MyGame game; // renderer or platform failures surface here
game.Run(); // Run() logs, then rethrows
game.Dispose();
} catch (const std::exception& e) {
// log-and-exit boundary: catch narrower types only where recovery differs
std::fprintf(stderr, "fatal: %s\n", e.what());
return 1;
}
return 0;
}
Catch narrowly where recovery depends on the exact contract (a ContentLoadException for a missing optional asset, for example) and use std::exception only at the top-level boundary. On the web, remember that a frame exception never reaches this handler (above).
What this means for a port
- A game class that uses only the public XNA surface of
Gameports by translating syntax: the same virtuals, the same event names, the same timing and component properties underget…Property()spellings. The names carry over; the behaviour is not guaranteed to be identical, and the differences listed next are on that public surface. - Public members whose behaviour differs from XNA 4.0 need a second look:
ShowMissingRequirementMessageis never called,InactiveSleepTimeis not applied,ResetElapsedTime()does nothing under the fixed step,IsRunningSlowlyfollows a different rule (GameTime and the timestep), aDrawableGameComponentloads once and does not follow the device, disposing aGameComponentdoes not remove it fromComponents(components). Also checkIsActiveunderRunOneFrame()(false) and the browser loop, which swallows frame exceptions. - Keep the base calls in
Initialize,UpdateandDraw; never callPresent()fromDraw(the manager presents inEndDraw); callDispose()afterRun()whenUnloadContent()must run.
Evidence and limits
Everything above was read from Game.cpp, Game.hpp, the renderer headers under modules/renderers and the exception headers of each module at this snapshot. The runtime tests named here exist and were not executed; most of them skip when the configured platform cannot create a window. The browser loop was read, not built or run. XNA 4.0 statements come from a disassembly of the XNA 4.0 Microsoft.Xna.Framework.Game assembly and describe XNA, not CNA; no oracle run compares these lifecycle edges. The main() example was syntax-checked with g++ -std=c++23 -fsyntax-only against the TARGET headers as part of a complete translation unit; it was not built or run.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Architecture
- Runtime lifecycle
- Maintainer workflow
- Change public XNA behavior
- Tests and validation
- Test architecture