Chapter 6 The Game Lifecycle
Chapter 5 showed Update / Draw as two overridden methods without asking what drives them. This chapter opens up Game itself — the class every CNA game inherits from — and the small constellation of supporting types (GameTime, GameComponent, GameWindow, GameServiceContainer) that make the loop work.
6.1 Game: base class, not framework-owned entry point
Game inherits System::Object and System::IDisposable, and its own Doxygen brief describes it precisely: “base class that provides the XNA-style game loop, services, window, content and components.” Four events — Activated, Deactivated, Disposed, Exiting — are all typed System::EventHandler<System::EventArgs>. This is worth pausing on for Exiting specifically: CNA does declare a dedicated ExitingEventArgs class (matching real XNA’s public API surface), but Game::Exiting itself is typed against plain EventArgs, not EventHandler<ExitingEventArgs> — and grepping the tree confirms ExitingEventArgs is otherwise unreferenced outside its own translation unit. This looks like an inconsistency until you check real XNA itself, which has exactly the same quirk: Game.Exiting is declared EventHandler<EventArgs> in the real assembly too, despite a dedicated ExitingEventArgs type existing. CNA preserves this faithfully rather than “fixing” it — a small, concrete example of Chapter 3’s “match FNA, not taste” rule in action.
6.1.1 Properties, and one that is deliberately not settable from outside
Game exposes its state through the project’s uniform getXProperty() / setXProperty() convention: Components, Content, GraphicsDevice (get-only, sourced from the graphics device service — there is no setGraphicsDeviceProperty), InactiveSleepTime, IsFixedTimeStep, IsMouseVisible, LaunchParameters, TargetElapsedTime, Services, Window. One property is more restricted than the rest: IsActive is get-only publicly, with its setter kept private — “only Game itself may change the active state,” matching FNA’s own internal set. C++ has no exact equivalent of C#’s assembly-internal visibility, so a private setter reachable only from within Game’s own OnActivated / OnDeactivated handlers is the closest faithful translation.
6.1.2 The override points
A concrete game overrides a fixed set of protected virtuals: Initialize(), LoadContent(), UnloadContent(), Update(GameTime&), Draw(const GameTime&), BeginRun() / EndRun(), and BeginDraw() / EndDraw() — the last of which matters more than it looks: BeginDraw() returning false skips both Draw and EndDraw for that frame entirely, the mechanism GraphicsDeviceManager (§6.7) uses to suppress rendering when no device is available yet. A game can also override ShowMissingRequirementMessage(const std::exception&) and the protected Dispose(bool disposing).
UnloadContent() is a live part of explicit shutdown. During initialization, Game subscribes that virtual to the registered graphics-device service’s DeviceDisposing event. GraphicsDeviceManager::Dispose(bool) raises the event even though the ordinary game-attached manager does not own the game-valued device it points at; only deletion of the device remains ownership-gated. Thus the public Game::Dispose() route reaches the game’s override exactly once through the service. The component-level contract is separate: explicit game disposal first disposes each IDisposable component, and DrawableGameComponent::Dispose(bool) invokes that component’s own virtual. Normal C++ destruction still does not execute either explicit callback path. Chapter 12 traces the manager ordering and ownership boundary.
6.1.3 Exit() requests a stop; Exiting describes the completed loop
The names of Exit() and Exiting invite a plausible but wrong mental model: that calling the former immediately raises the latter. In the desktop implementation, Exit() does only two things: it sets the public RunApplication loop flag to false and sets suppressDraw_. The current tick therefore skips its draw if an Update() called Exit(), and the enclosing while (RunApplication) loop then ends. Only after that loop has finished does RunLoop() call OnExiting(), which raises the event. It is a loop-boundary notification, not an immediate cancellation callback.
For an ordinary first desktop Run(), the source gives the following order:
-
1.
DoInitialize() runs once when necessary; it creates/configures the registered graphics service, initializes gamepad support, calls Initialize(), and categorizes the current components.
-
2.
BeginRun() executes, followed by BeforeLoop(). The latter sets IsActive true, so a newly inactive game raises Activated here.
-
3.
RunLoop() repeatedly calls Tick() while its loop flag remains true. Exit() or an SDL quit event clears that flag, and the loop then raises Exiting.
-
4.
Only after Exiting returns does Run() call EndRun() and AfterLoop().
This order matters to an override which has both an Exiting handler and an EndRun() override: the handler runs first. Calling Exit() before Run() does not bypass the surrounding lifecycle; the initialization and begin hooks still run, the loop has zero ticks, and desktop RunLoop() still raises Exiting. Nor does Run() reset RunApplication; a second call after exit reaches the same zero-tick exit boundary unless application code sets that public CNA extension true again. RunOneFrame(), by contrast, performs its one-time initialization and one Tick() only. It does not call BeginRun(), BeforeLoop(), EndRun(), AfterLoop(), or OnExiting(). Because only BeforeLoop() activates the game, IsActive remains false throughout RunOneFrame() — an easy trap for a test harness or editor host which uses that entry point. Browser builds use an Emscripten callback rather than the blocking desktop loop, but likewise raise Exiting only when that callback observes the false loop flag and cancels the browser loop.
6.1.4 Explicit disposal, destructor cleanup, and the gap between them
Game::Dispose() is not merely a spelling of the C++ destructor. Its protected Dispose(true) path first calls Dispose() on every current component that implements IDisposable. It then disposes the content manager, the cached graphics-device service when one has been resolved, and the SDL gamepad subsystem. It sets the private disposed flag only after that sequence. The public wrapper subsequently raises Game::Disposed. Consequently, explicit disposal is the route that makes a DrawableGameComponent’s virtual Dispose(bool) call its UnloadContent() hook. Later in the same sequence, disposing the resolved graphics service raises DeviceDisposing; the subscription installed by Game::Initialize() then calls Game::UnloadContent() before the manager’s own Disposed event. These are two distinct virtual calls on two distinct objects.
The destructor instead calls Dispose(false), then shuts down audio, and ordinary member destruction happens afterward. The false path deliberately skips the component loop, explicit ContentManager::Dispose(), graphics-service disposal, and gamepad-subsystem shutdown. The member destructors still run, so this is not a claim that all native storage survives a normal C++ scope exit; it is a precise warning that the explicit lifecycle callbacks and their ordering do not run there. In particular, ContentManager’s destructor is defaulted rather than a call to its public Dispose(), and the current SdlInputBridge::ShutdownGamepadSubsystem() call site is only the disposing == true branch. A typical scoped game which only calls Run() therefore relies on wider SDL shutdown for that gamepad-subsystem counterpart, whereas an explicitly disposed game executes it immediately. The same C++ destructor rule matters to a drawable component: it has no destructor which calls its override, and the later GameComponent base destructor’s virtual call resolves to GameComponent::Dispose(false), not DrawableGameComponent::Dispose(false). Destruction alone therefore does not dispatch the drawable’s UnloadContent() hook either.
Two event-safety details are visible directly in the short implementation. First, the disposed flag is written only after all the protected true-path cleanup succeeds. An exception from a component, content manager, or graphics service aborts the remaining cleanup, leaves the flag false, and prevents the outer Disposed notification. Second, the public wrapper raises Disposed even when the protected method returns early because the object was already disposed. Thus a second ordinary Dispose() raises the event again; a handler which calls Dispose() recursively re-enters that wrapper. This wrapper shape also matches the current FNA Game source, but it is still a reason for a handler to release only its own state and never dispose the notifying game again. No located CNA Game test exercises normal destruction, handler exceptions, or this re-entrancy boundary. Repeated public disposal is covered: the regression test proves that the manager’s one-shot guard prevents a second DeviceDisposing raise and hence a second game UnloadContent(), even though the public Game::Disposed notification is raised again.
6.1.5 The timing loop, ported wholesale from FNA
Game’s private implementation carries over FNA’s adaptive-sleep-precision timing loop directly: a 128-entry ring buffer of previousSleepTimes_ (masked with SLEEP_TIME_MASK rather than a modulo), a running worstCaseSleepPrecision_ estimate updated by UpdateEstimatedSleepPrecision(), and AdvanceElapsedTime() driving the actual frame pacing against TargetElapsedTime, capped by a static MaxElapsedTime. Two pairs of component-list vectors — updateableComponents_ / currentlyUpdatingComponents_ and drawableComponents_ / currentlyDrawingComponents_ — exist specifically to guard against a component mutating the component collection (adding or removing itself, or another component) during its own Update / Draw call, a re-entrancy hazard FNA’s own implementation guards against the same way.
One platform-conditional block is CNA-specific rather than ported: under #if defined(__EMSCRIPTEN__), Game gains a private EmscriptenLoopState struct and an EmscriptenMainLoopCallback(), because a blocking while loop is not a viable main-loop shape in a browser — Emscripten instead requires yielding control back to the browser’s own event loop via a registered callback. It is not merely another scheduler for the same Tick() body; it replaces that body and therefore changes observable contracts:
| Aspect | Desktop Tick() | Emscripten callback |
|---|---|---|
| Clock and pacing | High-resolution SDL_GetPerformanceCounter; adaptive delay/yield pacing | Millisecond SDL_GetTicks; browser-driven cadence, no sleep/spin ring |
| Step policy | Honors IsFixedTimeStep | Always drains fixed target-sized steps; ignores the property |
| Lag handling | 500ms clamp and five-tick IsRunningSlowly hysteresis | 250ms clamp; IsRunningSlowly forced false every step |
| Control methods | SuppressDraw() is consumed; ResetElapsedTime() affects variable-step mode | Neither field is consulted, so both calls are no-ops |
| Draw cadence | One draw attempt per tick | Draw only when at least one update ran |
| Lifetime | Loop returns, then EndRun() and AfterLoop() run | Registered infinite loop does not return; those hooks never run |
| State ownership | Per-game timing members | One static loop-state slot, hence one running Game per process |
The callback still cancels itself and raises Exiting after observing RunApplication == false. Chapter 64 covers the more dangerous object-lifetime consequence of Emscripten’s non-returning registration and WASM exception mode.
6.1.6 Two global debug keys hidden in the ordinary event pump
PollEvents() sends every SDL event through the input bridge first, then reserves two non-repeated key-downs: F9 calls the renderer’s DebugSimulateContextLoss(), and F10 calls DebugRestoreContext(). The input state still sees both keys because that bridge ran first; a game binding F9/F10 therefore gets its own key state and this framework action. FNA’s own Game installs no equivalent global hotkeys.
Most CNA products inherit empty hook bodies, so the reservation is invisible. EasyGL and D3D9 make it consequential. On desktop EasyGL either key performs a complete synchronous GL loss-and-recreate cycle; in a browser F9 requests asynchronous loss and F10 separately requests restore. D3D9 instead uses F9 for one DeviceLost transition and F10 for the real DeviceResetting/DeviceReset recovery sequence. The complete product and evidence matrix is §19.5.1.
6.1.7 Two small, explicitly-marked CNA extensions
Two CNAEXT-tagged additions live directly on Game: a public RunApplication flag (documented as “internal loop flag matching the FNA/XNA Game implementation shape”), and a pair of compatibility helpers, getTargetFPSProperty() and getTargetMsFrameTimeProperty(), plus a static fpsToMillisecondsPerFrame(intcs) — documented as existing specifically because “existing CNA examples” already depend on an FPS-based way to talk about frame timing, alongside the real XNA TargetElapsedTime TimeSpan.
6.1.8 What happens when a frame runs long
The timing loop described above is a real, working catch-up mechanism, not just a sleep/measure pair — and its concrete behavior under load is worth tracing through with real numbers rather than left as an abstract description, since it directly determines how many times a game’s own Update() override runs per displayed frame. Game::Tick(), read directly from Game.cpp, accumulates real elapsed wall-clock time into accumulatedElapsedTime_ every call, then — under IsFixedTimeStep (the default) — drains that accumulator in whole TargetElapsedTime_ steps, calling Update() once per step, before a single Draw() for the frame:
With the real default TargetElapsedTime of 166667 ticks (16.6667ms, i.e. 60Hz) and a single real frame that happens to take 50ms — a stutter from, say, a garbage-collector-free but still real hitch: a texture streaming in, a burst of physics work, an OS scheduling gap — the accumulator holds roughly three whole steps’ worth of time. Tick() therefore calls Update() three times in a row, each with gameTime.ElapsedGameTime pinned to exactly one TargetElapsedTime (16.6667ms), before Draw() runs once for that call to Tick(). Immediately before drawing, however, the loop rewrites ElapsedGameTime to TargetElapsedTime * stepCount; in this example the single Draw() observes about 50ms, while each of the three updates observed 16.6667ms. This update/draw asymmetry is intentional catch-up accounting, not an unstable clock.
This is the mechanism, not an edge case: a game’s Update() must be safe to call multiple times between draws, and must never assume “one Update() call happened since the last Draw()” — exactly the invariant Chapter 5’s own Update / Draw split already depends on without stating it this precisely.
6.1.9 IsRunningSlowly: five-tick hysteresis
GameTime::IsRunningSlowly looks, from its own name and Chapter 5’s brief mention, like it might flip true the instant a single Tick() needed more than one Update() step. Reading Game::Tick() directly shows real, deliberate hysteresis instead, tracked by a private updateFrameLag_ counter that persists across calls:
Every stepCount beyond the first adds to updateFrameLag_; the earlier three-Update() frame above contributes 2. IsRunningSlowly only turns true once updateFrameLag_ reaches 5 — several consecutive lagging ticks, not one — and, once true, only clears once updateFrameLag_ returns all the way to 0, one unit per ordinary (stepCount == 1) tick. A single 50ms stutter followed immediately by normal frame times never sets IsRunningSlowly at all; a genuinely sustained slowdown does, and takes several good frames afterward to clear rather than resetting instantly on the first frame that recovers. A game using IsRunningSlowly to decide whether to skip an optional visual effect this frame is reading a smoothed, several-tick signal, not a single-frame one — worth knowing before assuming it reacts as fast as the frame time itself does. This specific hysteresis mechanism has no dedicated regression test anywhere in the project (GameTimeTests.cpp only exercises GameTime’s own plain getter/setter, never Game::Tick()’s multi-tick accumulation), so its correctness rests entirely on this direct reading of Game.cpp matching FNA’s own algorithm, not on a passing test asserting it.
6.1.10 ResetElapsedTime() under fixed timestep
Game::ResetElapsedTime() exists, per its own XNA-inherited contract, to discard a large, misleading elapsed-time spike — the classic case being right after a blocking load screen, where the next Update() would otherwise see an artificially huge ElapsedGameTime covering the entire load. Reading its real implementation shows a one-line guard easy to miss:
Under the default IsFixedTimeStep = true, calling ResetElapsedTime() does nothing at all — no field is set, no flag is consulted anywhere in Tick()’s fixed-step branch. This is not a CNA gap; it matches real XNA’s own documented behavior (a fixed-timestep game’s ElapsedGameTime is always exactly TargetElapsedTime by construction, so there is no spike for ResetElapsedTime() to discard in the first place), but it is easy to call defensively after a load screen and assume it did something, when — for the large majority of CNA games, which leave IsFixedTimeStep at its default — it silently did not.
6.1.11 The base Update and Draw calls are framework work, not optional ceremony
The default Game::Update(GameTime&) first snapshots and updates enabled components, then ends with FrameworkDispatcher::Update(). A derived override which never calls Game::Update(gameTime) therefore disables more than automatic components: it silently stops dynamic-stream buffer dispatch, microphone checks, media-player transitions, and touch-panel updates. The safe shape is to perform game-specific work and call the base exactly once per update step. If a port intentionally replaces the component loop, it must still arrange one dispatcher call itself; calling both paths would pump those services twice.
The parallel rule for Game::Draw(gameTime) is simpler but still important: the base method draws the visible registered components in ascending DrawOrder. An override that never calls it suppresses every DrawableGameComponent. Presentation is different again: the outer tick calls EndDraw() after the override returns, and the registered manager presents there. Application Draw() code should not call Present() itself.
6.2 GameTime: three fields, one privileged writer
GameTime is deliberately small: TotalGameTime and ElapsedGameTime (both TimeSpan, publicly get-only), and IsRunningSlowly (bool, publicly get-only). All three setters are private, with friend class Game — no code outside Game itself may mutate a GameTime once constructed, which is exactly the invariant the two-line Update / Draw signatures from Chapter 5 rely on: a GameTime handed to your Update override is a value your code can read freely but never falsify.
6.3 GameComponent and DrawableGameComponent: the update/draw ordering contract
GameComponent implements IGameComponent, IUpdateable, and System::IComparable<GameComponent> in one base: a component knows its owning Game (get-only), can be individually Enabled, has an UpdateOrder, and exposes CompareTo. That method returns other.UpdateOrder - this.UpdateOrder, inverted relative to the usual compare-to sign convention; this is an FNA quirk preserved by CNA. The game itself does not depend on that sign: its insertion comparator explicitly sorts updateables by ascending UpdateOrder, with equal-order components retaining their existing relative order. DrawableGameComponent layers IDrawable on top, adding GraphicsDevice (get-only), DrawOrder, and Visible, and — notably — overrides Initialize() and calls LoadContent() there. In the ordinary game loop that occurs after Game::DoInitialize() has configured the already-existing device, so it is a suitable first content-load point. The private OnDeviceCreated method exists but is not subscribed to IGraphicsDeviceService; the component does not automatically reload content on a later manager DeviceCreated event. Component authors must therefore treat this as one initial-load convenience, not a complete device-lifecycle subscription.
IGameComponent itself is minimal — one pure-virtual Initialize() — and IUpdateable / IDrawable are the interface pair that GameComponent / DrawableGameComponent implement concretely: get/set enabled-or-visible state, an ordering value, a changed-event pair, and the actual Update / Draw method.
6.3.1 A self-contained DrawableGameComponent
The ordering contract described above is easiest to see in a small, real DrawableGameComponent — an FPS counter, a natural first component to reach for since it needs its own Update (accumulate elapsed time, recompute the average) and its own Draw (render the current value), independent of the main game’s own overrides:
Two details worth being deliberate about, both real rather than incidental to this example. First, GameComponentCollection::Add(IGameComponent*) takes a raw, non-owning pointer. A derived class’s members are actually destroyed before its Game base and the base’s Components member, so merely making fpsCounter_ a plain member does not make it outlive the collection. The destructor above is the safe RAII pairing: while the derived destructor body still has a live base and component, Remove() raises ComponentRemoved; Game then removes the component from its update/draw lists and removes the order-change event tokens it installed. Use the same remove-before-destroy rule for a separately owned or heap-allocated component.
This is more than collection tidiness. Game::Dispose() disposes components but does not remove them or their order-change tokens, and the collection’s implicit C++ destructor does not call Clear() (so it raises no removal events). The component setters have no disposed guard. A component which survives an explicitly disposed game can still emit an order event into that game; a component which survives the game’s destruction retains a raw callback to the now-dead game and can cause a native use-after-free on its next order change. Removing it while both objects are alive closes both routes. Second, setDrawOrderProperty(1000) is what guarantees the FPS counter renders after whatever the game’s own Draw override draws first — DrawableGameComponent does not know or care what order its owner’s own Draw override runs relative to Game’s own component-draw loop, so an on-top HUD element needs an explicit, high DrawOrder value to guarantee it, not just registration order.
6.4 GameComponentCollection: an event-raising collection, plus a real C++ iterator surface
GameComponentCollection raises ComponentAdded / ComponentRemoved events (typed EventHandler<GameComponentCollectionEventArgs>) around the usual collection operations (Add, Insert, Remove, RemoveAt, Clear, IndexOf, operator[]). Because C# ’s IEnumerable<IGameComponent> has no direct C++ equivalent, the collection adds an explicitly CNAEXT-tagged set of size_type / iterator / const_iterator aliases and begin() / end() overloads — a small, representative example of how CNA extends an XNA collection type just enough to be usable with range-based for and the standard library, without touching its XNA-facing surface.
6.5 GameServiceContainer: type-keyed lookup without C# reflection
C#’s GameServiceContainer is a dictionary keyed by System.Type. C++ has no runtime reflection to build an equivalent dictionary directly, so CNA’s AddService<TService> / GetService<TService> / RemoveService<TService> template methods key a std::unordered_map<std::type_index, void*> by typeid, delegating to non-template overloads underneath. Copy construction and copy assignment are explicitly deleted (“services are registered by pointer identity”), while move construction and move assignment are defaulted — a small but precise statement about ownership: a GameServiceContainer can be relocated, but never silently duplicated into a second set of service pointers.
6.5.1 Registering and consuming a service
GameServiceContainer’s own test suite (GameServiceContainerTests.cpp) is a good template for the pattern a real game uses to share a subsystem — say, a save-game manager — across otherwise-unrelated components without threading a pointer through every constructor:
Two behaviors worth knowing before relying on this, both read directly from the container’s own implementation rather than assumed: AddService<T>(nullptr) throws std::invalid_argument rather than silently registering an empty slot, and calling AddService<T> a second time for a type that already has a registered service also throws std::invalid_argument — there is no “last write wins” overwrite behavior. A service must be explicitly RemoveService<T>()’d before a replacement can be registered under the same type.
6.6 GameWindow: one concrete class where FNA has a hierarchy
CNA’s own class comment for GameWindow states the deviation from FNA plainly: “FNA defines GameWindow as abstract with per-platform subclasses; CNA collapses that hierarchy into one concrete SDL-backed class.” Where FNA needs a WinFormsGameWindow, an SDL2-backed window subclass, and so on, CNA needs exactly one, because SDL3 already is the cross-platform windowing abstraction — there is no per-platform subclassing left to do. GameWindow exposes the expected XNA surface (AllowUserResizing, ClientBounds, CurrentOrientation, Handle, ScreenDeviceName, Title, and the ClientSizeChanged / OrientationChanged / ScreenDeviceNameChanged events), plus a small set of explicitly-named EXT-suffixed CNA extensions: GetNativeSdlWindowEXT() (documented as “never for use in the strict XNA-facing API surface itself,” but needed so e.g. CNA::Devices::DisplayInfo can query SDL3 content-scale/safe-area state), IsBorderlessEXTProperty, and MinimizeEXT() / RestoreEXT() (“CNA extension — XNA has no minimize/restore API of its own”). The EXT suffix convention here is a sibling of CNAEXT: both mark non-XNA additions, but EXT specifically marks methods bolted onto an otherwise XNA-shaped class, kept visually distinct from the XNA members around them.
Mutating window properties is not uniformly best-effort. SDL failures in setters such as title, size, border, resizability, orientation, minimize, and restore become std::runtime_error. The deliberate exception is a read: if SDL_GetWindowSize() transiently fails during the browser backend’s asynchronous startup, ClientBounds returns its last-known rectangle and lets a later resize event correct it rather than unwinding the game loop.
GetNativeSdlWindowEXT() is a borrowed pointer, not an ownership transfer. In an ordinary windowed game it views the device-created SDL window; in a headless configuration it can be null. Never destroy it from game code or retain it across explicit graphics-device disposal: the wrapper does not own it and is not notified when a separately owned device tears it down. The complete renderer availability, attached-window, input-routing, and teardown contract is in Chapter 19, §19.1.3.
6.7 GraphicsDeviceManager’s role in the loop
GraphicsDeviceManager — already seen configuring the game-owned device in Chapter 5 — implements IGraphicsDeviceManager’s three-method contract (BeginDraw() -> bool, CreateDevice(), and EndDraw()), which is exactly what Game::BeginDraw() / EndDraw() delegate to: if GraphicsDeviceManager::BeginDraw() returns false (no device available yet), Game skips Draw / EndDraw for that frame rather than crashing on a null device. Its five events (DeviceCreated, DeviceDisposing, DeviceReset, DeviceResetting, PreparingDeviceSettings) retain the FNA-shaped service surface, but its selection virtuals are currently not a real selection pipeline: no manager path invokes FindBestDevice, RankDevices, or CanResetDevice; the defaults respectively build a new default record, do nothing, and answer only whether a device pointer exists. A subclass override of RankDevices cannot therefore choose a higher-resolution adapter in today’s CNA. The detailed event ordering, manager/device distinction, and the one-operation nature of PreparingDeviceSettings are documented in Chapter 12.
Construction deliberately registers the manager’s two services without calling ApplyChanges(): Game::DoInitialize() soon calls CreateDevice(), which is the single initial configuration pass. That pass applies graphics profile, presentation mode, a real in-place GraphicsDevice::Reset(), and viewport refresh in that order. Only after the settle-in reset does the manager subscribe to the device’s own DeviceResetting/DeviceReset events, so startup reports DeviceCreated rather than a synthetic reset pair. A later ApplyChanges() follows the same reset path; the device raises its pair and the manager forwards it exactly once instead of raising a duplicate pair itself. Forwarded reset and disposing events report the manager as sender, matching FNA.
One CNA-specific extension lives directly alongside this class rather than inside it: a CNAEXT enum class PresentationMode with five values — Letterbox, Overscan, Stretch, NativeBackBuffer, FixedHeightDynamicWidth — expressing the game’s requested policy for its virtual back buffer and actual display when the two don’t match in aspect ratio, exposed through getPreferredPresentationModeProperty() / setPreferredPresentationModeProperty(). Real XNA never had to solve this problem in the same way, since the Xbox 360 and a Windows desktop of that era rarely presented CNA’s variety of target displays (a browser canvas, an Android device in either orientation, an arbitrary desktop window); this is a genuinely new subsystem, clearly marked as such rather than folded silently into the XNA-facing API. The enum is a manager-side request, not a portable promise that every renderer has a matching scale, letterbox, or crop pass; Chapter 19 gives the current product matrix.
A game that wants a fixed, letterboxed virtual resolution — one of the more common real Getting-Started decisions, since it means every downstream draw call can assume one known coordinate space regardless of the player’s actual window or display size — sets this before the device is created, typically in the constructor right alongside graphics_(this):
On a renderer that implements Letterbox, a player resizing the window to a wider or taller aspect ratio than 1280:720 sees black bars rather than a stretched or cropped game, while draw calls remain in the 1280720 logical coordinate space. The manager delivers this request before resetting virtual resolution; the GraphicsDevice path and the renderer-specific execution matrix are documented in Chapters 12 and 19.
6.7.1 Vertical retrace is a presentation preference, not the fixed timestep
SynchronizeWithVerticalRetrace defaults to true. During device selection, the manager translates that Boolean into PresentationParameters.PresentationInterval: true means One, false means Immediate. ApplyChanges() then resets CNA’s already-existing device and forwards the integer 1 or 0 to the renderer. This is independent of Game::IsFixedTimeStep. The fixed timestep controls how many Update() calls the accumulator schedules; VSync can separately block Present() at the display boundary. Turning one off does not turn the other off.
XNA’s third public choice, PresentInterval::Two, cannot be expressed through this Boolean property. A port that genuinely needs half-refresh presentation must set the interval on explicit PresentationParameters, for example in a PreparingDeviceSettings handler, and must still check the renderer result described in §19.2.4. That matrix matters: SDL Renderer and EasyGL submit the numeric 2 but cannot guarantee driver acceptance; D3D9 has a literal native interval 2 but it is fullscreen- and capability-dependent. Several other products reduce every positive value to ordinary VSync.
The Vulkan renderer consumes its present-mode choice only when its swapchain is first constructed and inherits the runtime SetSwapInterval() no-op. CNA constructs a Game’s device before the derived game can configure its manager, so the ordinary request to disable the manager’s vertical-retrace property, followed by ApplyChanges(), leaves Vulkan on the default FIFO VSync mode. The public property still reports the requested Immediate; it is not an observation of the active swapchain mode.
6.7.2 Format and fullscreen preferences: three fields, three kinds of truth
The manager’s backbuffer format starts as Color, its depth/stencil format as Depth24, and fullscreen as false. FNA’s manager uses the same defaults. A bare presentation object differs: its depth format begins as None. During device selection the manager copies all three preferences into the reset parameters, just as it does size and interval.
They do not have one uniform meaning after ApplyChanges(). CNA first stores all three values. It then asks SDL to change fullscreen state and window size, and finally calls the renderer’s UpdatePresentationFormatEXT() hook with the requested color format, depth/stencil format, and fullscreen Boolean. Only D3D9 overrides that hook. The other thirteen products may therefore report a successful preference round trip while keeping fixed, driver-selected, or nonexistent native attachments.
Fullscreen is a separate layer from those attachments. An SDL-backed product can change its real window before its empty renderer hook is reached. A failed SDL fullscreen request, however, is cleared and treated as non-fatal. Thus a true public fullscreen property is requested state, not proof that the window manager or browser accepted the transition. D3D11 and D3D12 add another distinction: SDL may enlarge their window, but CNA never asks DXGI for exclusive mode. The complete color/depth/window matrix is §19.2.5.
6.8 Supporting cast: LaunchParameters, TitleContainer, TitleLocation, FrameworkDispatcher
Four smaller types round out the framework layer. LaunchParameters parses process argv into key/value pairs and — a CNA-specific representational choice — inherits publicly from std::unordered_map<std::string,std::string> rather than wrapping one, so a LaunchParameters instance is a standard associative container as well as an XNA-shaped one. TitleContainer is a static-only class providing OpenStream(path) -> std::unique_ptr<System::IO::Stream>, the entry point for reading title content files portably. TitleLocation resolves and caches the base path content is resolved relative to, exposing it under two accessor names — both getPathProperty() and a bare Path() return the same value, the latter kept specifically “to match XNA property name” alongside CNA’s own getXProperty convention. They are an independent direct-file facility, not the stream layer below ContentManager; their exact path, lifetime, Android, and test contract is §33.6.1. FrameworkDispatcher, also static-only, drives dynamic audio streams, microphones, media, and touch through one Update() entry point. The base Game::Update() calls it after components on every update step. Its dynamic-stream list is protected by a mutex, but callbacks execute after a snapshot has been copied and the lock released: a BufferNeeded handler may dispose its own stream without deadlocking on the same non-recursive mutex. This locking is a CNA thread-safety addition beyond FNA’s ordinary single-threaded assumption.
6.9 One catch clause for the framework boundary
The framework core does not translate failures into one XNA exception hierarchy. The pinned modules/runtime implementation has 30 explicit throw sites and all use raw standard C++ types: std::out_of_range, std::runtime_error, std::invalid_argument, std::logic_error, or std::bad_alloc. Elsewhere in CNA, some public exceptions derive from std::runtime_error, while audio, network, sensor, and project exceptions derive from System::Exception.
The hierarchy detail that matters at a porting boundary is that System::Exception : std::exception, not std::runtime_error. Consequently a catch (const std::runtime_error&) misses the System::* family, and a catch (const System::Exception&) misses raw standard failures. Only catch (const std::exception&) spans both families. Catch narrowly when recovery depends on the exact contract; use std::exception for a top-level log-and-exit boundary.
6.10 What this buys a porting engineer
None of the types in this chapter are exotic on their own. What matters, if you are porting an existing XNA or FNA game (this book’s migration chapter returns to this in practical detail), is that the entire timing/component/service/window layer a real XNA game’s Game-derived class depends on is present, member-for-member, under the same names. A Game subclass that only used the public, documented XNA surface — not FNA’s own internal/assembly-private details — should port to CNA by translating syntax, not by redesigning its structure.