Runtime module internals

CNA snapshot 009d40f5  ·  Development › Runtime internals  ·  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. Test files and case names were read at this snapshot; several cases skip when the configured platform cannot create a window, and none was executed for this page. Deeper entry-point variants (mobile and browser hosts) and platform-dependent validation are outside what was checked.

The runtime module, cna_runtime, coordinates an XNA-shaped Game: its platform, its eagerly built graphics device, the window facade, components, services and clock. Its job is orchestration — native services live in the platform module, GPU translation in graphics and the renderer families, asset decoding in content. This page maps what the module contains, what Game owns and borrows in its real declaration order, how the component lists are locked, which state flags drive the loop and how it fails. The function-level traces for startup, one frame and ownership and shutdown build on it.

Boundary and build target

modules/runtime/CMakeLists.txt globs src/*.cpp into cna_add_module(cna_runtime Runtime …) and links, all PUBLIC: cna_graphics_core, cna_input, cna_content, cna_audio, cna_media, cna_core, cna_diagnostics, cna_math, the Sharp Runtime partitions Core.Base, Globalization and IO, and cna_platform (the file's own comment: the platform module owns every native service the runtime uses). The edges are public because the exported headers need them: Game.hpp includes GraphicsDevice.hpp, GraphicsAdapter.hpp, ContentManager.hpp, FrameworkDispatcher.hpp (which belongs to cna_audio) and the component, service, window and time headers. It forward-declares CNA::Platform::IPlatform instead of including that header (only the value type PlatformCapabilities.hpp is included), which is why ~Game is defined out of line.

Source in modules/runtime/srcRole
Game.cppConstruction and ambient platform installation, Run/RunLoop/Tick, the clock, PollEvents dispatch, component categorization, Dispose, the Emscripten frame body.
GraphicsDeviceManager.cppTurns preferences into a Reset of the Game-owned device, registers two borrowed services, owns the frame's renderer-context lease between BeginDraw and EndDraw.
GameWindow.cppFacade over the device's IPlatformWindow: client bounds, screen device name, orientation, file/text drop events.
GameComponentCollection.cpp, GameServiceContainer.cppRaw and shared component ownership; type-indexed borrowed service providers.
GameTime.cpp, LaunchParameters.cpp, TitleContainer.cpp, TitleLocation.cppValue types, command-line parameters and title-file access.
ProjectGraphicsProfile.cppA process-wide default GraphicsProfile (Reach unless SetProjectGraphicsProfileEXT or a ProjectGraphicsProfileEXT object changed it), read by the GraphicsDeviceManager constructor.

The module does not implement an OS message pump (that is IPlatform::PollEvents), a GPU API (that is GraphicsDevice over IGraphicsRenderer) or any decoder. FrameworkDispatcher::Update is called by the runtime but implemented in cna_audio, which owns the dynamic-stream list it walks; from there it also calls into media (MediaPlayer) and touch input. A new native event type belongs in the platform contract, PlatformEvent.hpp, before Game::PollEvents decides how it affects the game. The runtime test group also produces a focused CnaRuntimeTests executable (EXCLUDE_FROM_ALL) through cmake/UnitTests.cmake.

⚠

A declared facade that does not exist. CNA/Misc.hpp declares CNA::RuntimeOptions and a CNA::Runtime class with five methods (Initialize, Shutdown, IsGraphicsEnabled, IsAudioEnabled, IsInputEnabled). None is defined anywhere; a call would fail to link. modules/c-api/cmake/CheckUnimplementedRuntimeFacade.cmake inspects the runtime archive and fails the moment CNA::Runtime symbols appear, so the C API's not-applicable coverage rows are revisited instead of silently staying wrong. The real entry point is Game.

Objects and relationships

The private section of Game.hpp is the lifetime contract: declaration order is construction order, and its reverse is destruction order. Read it before any method.

Game            (declaration order = construction order; reverse = destruction order)
├─ public: Activated, Deactivated, Disposed, Exiting (events), RunApplication   declared first; never reach the platform
├─ unique_ptr<IPlatform> platform_              installed as the ambient platform in its own initialiser
├─ unique_ptr<PlatformInstallation>             undo guard; disarmed when the constructor completes
├─ PlatformCapabilities platformCapabilities_   GetCapabilities() cached once
├─ unique_ptr<PlatformEventBatch> eventBatch_   reused vector<PlatformEvent> for PollEvents
├─ GameComponentCollection Components_          raw entries + optional shared_ptr ownership
├─ Graphics::GraphicsDevice GraphicsDevice_     value member: renderer and window built here
├─ Content::ContentManager Content_             given the device in the constructor body
├─ GameWindow Window_                           borrows the device's IPlatformWindow
├─ LaunchParameters LaunchParameters_
├─ GameServiceContainer Services_               type_index -> borrowed void*
│     └─ GraphicsDeviceManager*                 caller-owned; registered as IGraphicsDeviceManager
│                                               and as IGraphicsDeviceService
├─ clock and flags, four ordered component vectors, componentListsMutex_ (recursive)
└─ graphicsDeviceService_, graphicsDeviceManager_, currentAdapter_   cached raw pointers
ℹ

Member order at this snapshot. Components_ is declared before GraphicsDevice_, directly after the event batch — not after the window. The collection is therefore constructed before the device and destroyed after it, so a component the collection owns through shared_ptr is destroyed after the device has gone; the shutdown trace follows the consequences. The header comment says platform_ is declared “before every other member”; precisely, it is the first private data member — the four public event handlers and RunApplication are declared ahead of it, and none of them touches the platform.

Window. GameWindow does not own the native window. Game's constructor body calls Window_.setWindowInternal(GraphicsDevice_.GetPlatformWindowInternal(), GraphicsDevice_.GetWindowHandleInternal()), which copies title, resizability and borderlessness from the platform window and refreshes cached state without raising events. Afterwards GameWindow::updateFromPlatform calls refreshCachedPlatformState(true): it re-queries client bounds (made client-local, so a desktop position never leaks into XNA's ClientBounds), the display name and the orientation, and raises ClientSizeChanged, ScreenDeviceNameChanged or OrientationChanged only when the value changed. Orientation comes from a logical-size provider that GraphicsDeviceManager installs, so it describes the surface the game draws rather than the window around it; it is Portrait when the logical height exceeds the width, LandscapeLeft otherwise and Default when either extent is not positive, and is only adopted if it is in the supported set. LandscapeRight is therefore never derived from the surface shape: CurrentOrientation can become LandscapeRight only through the fallback in SetSupportedOrientations (tried after Portrait and LandscapeLeft), so a port that compares CurrentOrientation with LandscapeRight will not see it after a device rotation. Read at this snapshot; not run on a phone. The manager also drives BeginScreenDeviceChange/EndScreenDeviceChange: the second resizes the platform window to the requested client size (skipped on Android), sets exclusive-full-screen or windowed mode from the pending flag, and raises ClientSizeChanged/ScreenDeviceNameChanged if either changed. SetSupportedOrientations is forwarded to the platform window, where it matters on iOS and Android. A drop arrives from PollEvents as a Begin … Complete sequence; OnDropEXT collects every file, then raises FileDropEXT once with all of them and TextDropEXT once per text, moving the buffers out first so a handler that pumps events cannot see a half-built drop (Position events are ignored).

Services. GameServiceContainer stores void* keyed by std::type_index, throws std::invalid_argument for a null provider or a duplicate type, returns null for a missing one, and owns nothing. It cannot prove that a provider implements the interface it is registered under (the source records that FNA's assignability check is omitted because C++ has no reflection across void*).

Manager. GraphicsDeviceManager(Game*) throws std::invalid_argument for a null game, takes &game->getGraphicsDeviceProperty(), installs the logical-size provider on the window, then registerServices(): it throws if an IGraphicsDeviceManager is already registered, adds itself as IGraphicsDeviceManager and IGraphicsDeviceService, and subscribes a ClientSizeChanged handler that only calls UpdateViewportFromWindow (never ApplyChanges, which would feed the physical size in as the virtual resolution). It deliberately does not call ApplyChanges itself. Defaults: profile from CNA::GetProjectGraphicsProfileEXT(), back buffer 800×480, Depth24, vsync on, presentation mode Letterbox (so the viewport is the back buffer whatever shape the window has).

✎

Maintainer note — borrowed in both directions. The manager's ClientSizeChanged subscription captures this and is never removed (unregisterServices removes only the two service entries), and Game caches graphicsDeviceManager_ and graphicsDeviceService_ as raw pointers that unregistering does not clear. Conversely ~GraphicsDeviceManager calls Dispose(false), whose unregisterServices() dereferences its Game. The supported shape is therefore a manager whose lifetime is nested inside its game's — typically a member of the derived game. A manager destroyed while its game keeps pumping events, or one outliving an undisposed game, leaves a dangling reference. No test covers either shape; this is a source-read constraint, not an observed crash.

Component lists and threading

GameComponentCollection keeps raw IGameComponent* entries in items_. Add(IGameComponent*) stores the pointer only; the Add(std::shared_ptr<IGameComponent>) overload also records an owning reference in owned_ before the insert (so a ComponentAdded handler that removes the component again finds the reference to release) and erases it if the insert throws. Adding the same pointer twice throws std::invalid_argument; SetItem throws std::logic_error. RemoveItem erases the entry, raises ComponentRemoved while the component is still alive, then releases the owning reference — which may destroy it. ClearItems raises ComponentRemoved for every entry and then clears both containers.

Game::CategorizeComponent takes componentListsMutex_, uses dynamic_cast to find IUpdateable and IDrawable, inserts the pointer into the ordered list (before the first entry with a strictly greater order, so equal orders keep insertion order) and stores the UpdateOrderChanged/DrawOrderChanged subscription tokens in two maps that the same lock protects.

The XNA loading-screen pattern adds components from a background thread while the game thread updates and draws (the source comment cites two sample loading screens that do exactly that). Game::Update and Game::Draw copy the ordered list under the lock, release it, and run component code without it, so game code never executes while the lock is held. The mutex is recursive because categorizing sorts, and a component's own order-changed event re-enters the same path. OnComponentRemoved also nulls matching pointers in currentlyUpdatingComponents_/currentlyDrawingComponents_ (both loops skip null): erasing would invalidate the in-flight iteration, and leaving the raw pointer would call a component its owner may already have freed — the screen-manager case where the update that is running drops the screen owning the components.

The two snapshot vectors are members of Game, not locals, and each call clears its vector on entry and again on exit. A Game::Update or Game::Draw entered again from inside a component's own Update or Draw (or from a nested loop that calls Tick()) therefore empties and refills the vector the outer loop is still iterating, which invalidates the outer loop's iterators (formally undefined behaviour). Nothing in the runtime guards against it and no runtime test enters the loop re-entrantly. Read from Game.cpp at this snapshot; not executed.

What the lock does not cover:

  • The collection's own items_ and owned_ have no lock. Two threads mutating the collection at once, or a mutation concurrent with an iteration over Components_ (Game::Initialize, Game::Dispose(true), the categorization loop in DoInitialize), is not protected.
  • OnComponentAdded calls the component's Initialize() on whichever thread called Add; for a loading thread, that is the loading thread.
  • Component internals are not made thread-safe by any of this.
  • DoInitialize subscribes to ComponentAdded/ComponentRemoved only after Initialize() returns. Game::Initialize initializes components in an index loop that re-reads the count (so a component added by another component's Initialize is initialized too) and then calls LoadContent. A component added from LoadContent is categorized by DoInitialize — it will be updated and drawn — but nothing on this path calls its Initialize().

Case study: Game component lifetime walks through a change in this area; the thread and callback map places the loading-thread rule among the other callback contracts.

Runtime state and failure model

StateMeaning at this snapshot
hasInitialized_Gates DoInitialize in Run and RunOneFrame; set only after DoInitialize returns.
RunApplication (public, CNAEXT)The loop condition. Exit() clears it and also sets suppressDraw_.
suppressDraw_Skips one draw; Tick clears it. Not consulted by the Emscripten frame body.
isSuspended_Set by WillEnterBackground, cleared by DidEnterForeground and Terminating; the loop acts on it only where the compile-time CNA::isMobilePlatform() is true.
IsActive_Changed only through setIsActiveProperty, which raises Activated/Deactivated; set by BeforeLoop, focus events and lifecycle events.
IsFixedTimeStep_, TargetElapsedTime_Default fixed step at 166,667 ticks (1/60 s).
hasUpdatedOnce_, forceElapsedTimeToZero_First-update rule of the XNA clock; ResetElapsedTime() sets the latter only in variable-step mode.
ClockpreviousPerformanceCounter_, accumulatedElapsedTime_, updateFrameLag_, a 128-entry ring of recent sleep measurements with worstCaseSleepPrecision_ (starts at 1 ms), and MaxElapsedTime = 500 ms.
IsMouseVisible_The setter forwards to IPlatformMouse::SetCursorVisible only when a platform window exists and the platform has a mouse service; otherwise it only records the value. (A queryable Terminal platform has a mouse service whose SetCursorVisible throws PlatformException; see the Terminal page.)
InactiveSleepTime_Default 20 ms, validated, returned by the getter — and read by nothing in the loop (see the note below).

Failure model: setTargetElapsedTimeProperty with a non-positive value and setInactiveSleepTimeProperty with a negative one throw std::out_of_range. AssertNotDisposed throws std::runtime_error from Run, DoInitialize, before every Update of a Tick, and from OnActivated/OnDeactivated. Run catches both std::exception and unknown exceptions, logs them through CNA::Logger::Error and rethrows. A null platform passed to the protected constructor throws std::invalid_argument; a platform name that is not compiled in throws PlatformException from PlatformFactory::Create; a renderer that cannot be created throws out of the GraphicsDevice constructor — all during Game construction, before Run exists.

✎

Maintainer note — InactiveSleepTime is stored, not applied. Nothing in Game.cpp reads InactiveSleepTime_ after the setter: the desktop Tick does not sleep while IsActive is false, so an unfocused game keeps its normal update and draw rate. Only the C API's property getter reads the value. Recorded as a gap; a change here needs a timing test in both step modes.

Safe extension points

  • Derived games override Initialize, LoadContent, UnloadContent, Update, Draw, BeginRun, EndRun, BeginDraw, EndDraw, OnExiting, OnActivated, OnDeactivated and Dispose(bool). A derived Update that does not call Game::Update also skips component updates and FrameworkDispatcher::Update — dynamic sound streams, MediaPlayer, microphone buffers and the touch panel pump all ride on it.
  • Embedding hosts and tests use the protected CNAEXT constructor Game(std::unique_ptr<IPlatform>), GetPlatformEXT() (prefer it to the ambient accessor wherever a Game is available) and GetPlatformCapabilitiesEXT(), a value cached at construction precisely so capability discovery never becomes a per-frame virtual call. RunOneFrame() drives a single tick.
  • A new game-loop behaviour needs tests for both fixed and variable timestep, for event ordering within one batch, and a look at the separate Emscripten body.
  • A new window behaviour goes through IPlatformWindow and is reflected by GameWindow::refreshCachedPlatformState; do not store a second, independent geometry value in Game.
  • A new service needs an explicit owner whose lifetime nests inside the game's; the container owns nothing and never clears a cached pointer.
  • A new Game member is placed by lifetime: after platform_ if it may reach the platform while being built or destroyed (every current member does), and before GraphicsDevice_ only if it must outlive the device.

What the tests show

All files below are in modules/runtime/tests/Microsoft/Xna/Framework. They exist at this snapshot; none was executed for this page.

FileWhat its cases target
GameTests.cpp (9 cases)Component removed mid-frame is not called later in that frame; components added from a loading thread survive the iterating frame; built-in XNB readers registered before LoadContent; lifecycle order of Run; mobile suspend/resume/terminate through a scripted-event platform; UnloadContent on device disposal, not repeated on a second Dispose, working across repeated games; LoadContent deferred to DeviceCreated.
GamePlatformOwnershipTests.cpp (13)Explicit platform owned and installed before members, failed construction leaves nothing installed, null platform rejected, install/uninstall, nested and out-of-order lifetimes, each game owning its own platform, a platform that is usable (non-zero frequency, non-decreasing counter) while the game lives.
GameClockFirstUpdateTests.cpp (3)First fixed update sees zero elapsed time; total time is the time before the step; variable-step total also lags its own update. Its header records the values measured on XNA 4.0 under Wine.
GamePlatformTimingTests.cpp (9)Monotonic counter, stable non-zero frequency, counter and millisecond ticks advancing over a real delay, a fixed and a variable frame, mouse-visibility round trips with and without a pointer service.
GraphicsDeviceManagerTests.cpp (8)Project profile default, CreateDevice after Run, ApplyChanges raising resetting/reset exactly once, renderer-detected loss forwarded, forwarded events naming the manager as sender, repeated Dispose not re-raising DeviceDisposing, default Letterbox, presentation-mode round trip.
GameEventSemanticsGoldenTests.cppOne parameterized case per platform in PlatformFactory::GetAvailable(), comparing an event-semantics transcript with golden/platform-event-semantics.txt.
GraphicsDeviceManagerPlatformTests.cpp, GameCultureTests.cpp, GameWindowDropTests.cpp, GameComponentCollectionTests.cpp, GameServiceContainerTests.cppManager defaults without a game, platform-locale culture fallback, drop-event assembly, collection and container contracts.

Window-dependent cases call CNA::Runtime::Testing::DefaultPlatformCanCreateWindow() and GTEST_SKIP when the configured platform cannot create a window; the failed-construction case skips when the build's renderer happens to accept the Headless platform; the golden test skips a platform that cannot back the build's renderer. The nested and out-of-order ownership cases are guarded by a constant that is true at this snapshot, so that guard never skips them. None of these tests proves that every platform's native event pump or every renderer's present path works.

Curated source reading order

  1. Game.hpp — read the private member declarations before the methods; their order is the platform and device lifetime guarantee.
  2. Game.cpp — the anonymous-namespace InstallPlatform/UninstallPlatform, then the constructor, Run, Tick, PollEvents, Dispose and the component callbacks, in that order.
  3. GraphicsDeviceManager.cpp — how an already existing device receives preferences through Reset, and how device events are forwarded.
  4. GameWindow.cpp — how native window state becomes XNA window events and logical orientation.
  5. GameComponentCollection.cpp — raw versus shared ownership and the removal order.
  6. GameServiceContainer.cpp — forty lines that explain why every provider is borrowed.

Continue with the startup, frame and shutdown traces; the user-facing lifecycle is on Game loop & lifecycle.

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

Tests and validation
Test architecture