Startup source trace
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 construction order was read from the member initialisers and constructor bodies at this snapshot; the named tests exist and were not executed. Deeper entry-point variants (mobile hosts, the browser's Asyncify linkage) and platform-dependent window creation remain unverified here.
CNA has no engine-owned main(). A game executable constructs a derived Game and calls Run(); by the time Run() begins, the platform is installed, the renderer is resolved and constructed, the window exists and the XNB readers are registered. This page traces that path function by function — from the demo executable through platform installation, the eager GraphicsDevice, and DoInitialize — and records where each stage can fail and what it rolls back. It is for anyone changing construction order, the renderer attempt loop or the first-frame lifecycle.
Construction sequence
demo_2d Main.cpp is one concrete executable-owned route: new Game1(), optional --smoke/--webgpu-2d-validation switches, game->Run(), delete game. It never constructs a GraphicsDeviceManager, so it also exercises the manager-less draw path described on the frame page.
demo_2d Main.cpp: new Game1()
→ Game::Game()
→ PlatformFactory::Create() kDefaultName from CNA_PLATFORM_<NAME>
→ Game::Game(unique_ptr<IPlatform>) member initialisers, in declaration order:
→ platform_(InstallPlatform(platform)) live-game stack + SetCurrentPlatform
→ platformInstallation_ undo guard for a throwing constructor
→ platformCapabilities_(GetCapabilities()) cached once
→ eventBatch_, Components_
→ GraphicsDevice_() default adapter, Reach, 800×480 parameters
→ GraphicsAdapter::getDefaultAdapterProperty() argument of the delegating constructor:
first-use display enumeration, ambient video pin
→ platform_ = &GetCurrentPlatform() finds the game's platform
→ TouchPanel display size = virtual size
→ resolveRenderer() per attempt-order candidate:
video subsystem → createOrAttachWindow() → applyPresentationParametersToWindow()
→ createRenderer() → Latch(candidate)
→ UpdateViewportFromWindow()
→ BlendState::Opaque, DepthStencilState::Default (if supported),
RasterizerState::CullCounterClockwise
→ Content_(), Window_(), LaunchParameters_(), Services_()
→ constructor body:
InitializeDefaultCulture(*platform_)
Window_.setWindowInternal(device window, device window handle)
Content_.setGraphicsDevice(GraphicsDevice_)
RegisterAllBuiltInXnbReaders()
FrameworkDispatcher::Update()
platformInstallation_->Disarm()
→ Game1 constructor body device and window already exist
→ optional GraphicsDeviceManager(&game) registers borrowed services; no ApplyChanges
→ game->Run()
Two consequences follow directly. First, a derived constructor already runs against a live device and a created window (for any renderer whose descriptor asks for one); what the manager's preferences change happens later, in DoInitialize, through a Reset. Second, the first FrameworkDispatcher::Update() happens inside construction, before any game code.
Platform resolution and ambient installation
cmake/PlatformSelection.cmake validates CNA_PLATFORM (default SDL3), refuses the reserved SDL12 and EMSCRIPTEN (and a host-conditional backend requested on the wrong host) with a FATAL_ERROR, and emits add_compile_definitions(CNA_PLATFORM_<NAME>). PlatformFactory.cpp turns that definition into kDefaultName, and Create(name) constructs only a backend compiled into the binary. Headless is compiled into every build and Terminal into every non-Windows build, independent of the selection, because the implementation-neutral conformance suite needs more than one live implementation; GetAvailable() is also the list that suite and the event-semantics golden test are parameterized over. An unknown or uncompiled name throws PlatformException naming what is available — never a do-nothing stub.
CurrentPlatform.cpp distinguishes three things: a borrowed installed pointer (SetCurrentPlatform), an owned lazy default that GetCurrentPlatform() creates with PlatformFactory::Create() only when nothing is installed, and a list of ambient subsystem pins (Detail::PinCurrentPlatformSubsystem, used by GraphicsAdapter) that SetCurrentPlatform transfers to the replacement platform. All of these holders are deliberately immortal, for the teardown reasons on the shutdown page.
InstallPlatform rejects null with std::invalid_argument, pushes the pointer onto a process-wide live-game stack under its own mutex, and calls SetCurrentPlatform. It runs from platform_'s own member initialiser because GraphicsDevice's constructor asks the ambient accessor for its platform: installing from the constructor body would be too late, and the device would lazily create a second platform. Immediately after, platformInstallation_ is constructed; if any later member constructor throws, ~Game never runs, but the already-constructed guard's destructor calls UninstallPlatform during unwinding, before the argument-owned platform is destroyed. The constructor disarms the guard as its last statement, after which ~Game owns the uninstall.
The constructor body then calls InitializeDefaultCulture: if the process has not set a default thread culture or UI culture, it walks the platform's preferred locales (GetSystemInfo()->GetPreferredLocales()) and adopts the first one the runtime accepts, skipping locales that throw CultureNotFoundException. Explicit process defaults win.
Renderer and window before Run
GraphicsDevice_ is a value member, so GraphicsDevice.cpp's constructor runs during Game construction with the default adapter, GraphicsProfile::Reach and default PresentationParameters (800×480). The default adapter is an argument of the delegating constructor, so GraphicsAdapter::getDefaultAdapterProperty() is evaluated before the delegated body: the first call in a process enumerates the displays of the ambient platform (which is the game's own, because it was installed first) and keeps one video-subsystem reference alive as an ambient pin owned by CurrentPlatform (GraphicsAdapter.cpp, AdapterVideoPin); a host with no display server yields a single 800×480 “Default Display” adapter. The adapter list is a process-global cache, which is why the pin has to follow the ambient platform when a game is torn down (see the shutdown page). The constructor body records the caller's current GL binding so it can restore it, publishes the virtual size to TouchPanel (which needs it to scale normalized touches), and calls resolveRenderer():
RequiredRendererForNewDevice()first honoursGraphicsAdapter'sUseNullDevice/UseReferenceDevice, refusing the device if the required renderer is not compiled in or the selection is already latched elsewhere.- For each candidate of
GraphicsRendererSelectionAccessEXT::GetAttemptOrder()it finds theGraphicsRendererDescriptor; records a skip for a candidate not compiled in, one that does not match a required device type, one that needs a different window kind when the window was supplied by the caller (WindowKindConflict), one forced unavailable byCNA_DEBUG_UNAVAILABLE_RENDERERS, or one whoseisAvailable()probe says no. - It applies
CNA_FORCE_HEADLESS_DEVICE_EXTto the parameters themselves, takes or releases this device's single video-subsystem reference for the candidate, creates or attaches a window, applies the presentation parameters to it, and callscreateRenderer().CNA_DEBUG_FAIL_RENDERER_INITcan force a failure here for tests. - With a single-entry attempt order a failure propagates unchanged. With a configured fallback chain each failure is recorded as
InitializationFailed, an owned window is discarded (a caller's window is kept), and the next candidate runs; an exhausted chain throwsSystem::InvalidOperationExceptioncarrying the first failure as the primary cause. - The selection latches only after a successful construction, so a failed attempt does not freeze a renderer that never existed and a game that caught the error may try another configuration.
Pre-construction descriptor fields answer what a not-yet-existing renderer cannot: needsWindow, windowKind, wantsHighDpi, GL framebuffer bits, and whether it needs the video subsystem, a GL context, a Vulkan surface or a surface presenter. createOrAttachWindow() returns without a window for a family that needs none (a CPU renderer stays off-screen on every windowing platform) unless the platform can present only through a presenter — surfacePresentation without nativeWindowHandle, which is Terminal attached to a TTY. With HeadlessEXT set it creates no window either. A non-zero DeviceWindowHandle is attached through IPlatform::AdoptWindowHandle and stays borrowed: CNA may neither destroy it nor rebuild it for a fallback. Otherwise it builds a WindowDescription — size from the back buffer (1024×768 if that is not positive), resizable = false to match XNA's AllowUserResizing default, render intent and framebuffer request from the descriptor — and calls IPlatform::CreateWindow. The handle goes into PresentationParameters only when the platform reports a native handle; the handle and window id are published to TextInputEXT and Mouse either way.
createRenderer() passes a surface value snapshot (window id, native handle, drawable size, display scale) and only the narrow services the descriptor asked for — IPlatformGlContext, a Vulkan surface or a surface presenter — never the whole platform. The renderer receives the virtual resolution, the requested multisample count, swap interval, back-buffer and depth formats, full-screen flag, profile, the context-recovery flag and a device-event callback that maps renderer-reported loss/resetting/reset to the device's public events (and raises content loss on a real reset). The device then writes back the applied formats and multisample count and logs the active renderer's name once through CNA::Logger. Finally the constructor pushes the three initial state objects, skipping depth/stencil on a renderer without it. This is where backend-native resources first exist, before any derived Initialize. The attempt loop in depth is on Renderer selection internals and GraphicsDevice internals.
Transition into Run
Game::Run wraps everything in a try: AssertNotDisposed(); if not yet initialized, DoInitialize() and then hasInitialized_ = true; BeginRun(); BeforeLoop(); seed previousPerformanceCounter_ from the platform; RunLoop(); then EndRun() and AfterLoop().
DoInitializelooks upIGraphicsDeviceManagerinServices_. If a manager exists it callsCreateDevice()(GraphicsDeviceManager.cpp), which re-targets the game's own device, builds aGraphicsDeviceInformation(preferred back-buffer size, swapped by orientation only on iOS and Android; formats; full screen;PresentInterval::OneorImmediatefrom vsync; multisample 0 or 8; profile), raisesPreparingDeviceSettings, brackets the window withBeginScreenDeviceChange/EndScreenDeviceChange, and callsapplyToExistingRenderer:SetGraphicsProfileEXT, thenSetPresentationMode(before the reset, because the logical size depends on it), thenGraphicsDevice::Reset, thenUpdateViewportFromWindow. Only after that settle-in reset does it subscribe to the device'sDisposing,DeviceResettingandDeviceReset(once, guarded), so first-time setup raises onlyDeviceCreated. The manager's constructor deliberately skipsApplyChangesto avoid doing this reconfiguration twice.- No controller subsystem is acquired. The source records that acquiring it here cost a full udev enumeration (about 1.9 s on the project's Linux reference machine) before the first frame; the platform now acquires it on first gamepad or joystick use.
Game::Initializeinitializes the existing components, looks upIGraphicsDeviceService, subscribesUnloadContentto itsDeviceDisposingevent, and callsLoadContentif there is no service at all or the service has a device; if the service exists without a device,LoadContentis deferred toDeviceCreated. All three duties live in the base implementation, so a derivedInitializethat never callsGame::Initializeskips component initialization, theUnloadContentsubscription and the baseLoadContentcall. A game with noGraphicsDeviceManager(such as demo_2d) has no service to subscribe to, so itsUnloadContentis never reached from disposal.- Back in
DoInitializethe ordered lists are cleared and rebuilt by categorizing every component, thenComponentAdded/ComponentRemovedare subscribed. BeforeLoopsetsIsActive, raisingActivated. The firstTickcan now process events, update and draw.
RunOneFrame(), the entry point used by hosts and tests that drive single ticks, performs DoInitialize, seeds the counter and sets hasInitialized_ on its first call, then calls Tick(). It never calls BeginRun, BeforeLoop, OnExiting or EndRun, so IsActive stays false until a focus or lifecycle event says otherwise.
Failure and rollback
| Stage | How it fails | What is undone |
|---|---|---|
PlatformFactory::Create | PlatformException for a name not compiled in, or the backend's own constructor error | Nothing installed yet. |
InstallPlatform | std::invalid_argument for null | Nothing installed. |
GraphicsDevice_ constructor | Renderer refused, window creation failed, renderer constructor threw, or an exhausted fallback chain | The device's catch calls destroyNativeResources(), drops its video reference, restores the caller's GL binding and rethrows; unwinding then runs PlatformInstallation's uninstall and destroys the platform. |
| Constructor body | Culture, XNB registration or dispatcher throws | Same unwinding, except that the fully built GraphicsDevice_ is now destroyed through its ordinary destructor; the guard is still armed until the last statement. |
GraphicsDeviceManager constructor | std::invalid_argument for a null game or a second manager | Nothing registered by that manager. |
DoInitialize, Initialize, LoadContent | Any exception | Nothing automatic; Run logs it through CNA::Logger::Error and rethrows. hasInitialized_ is set only after DoInitialize returns. |
Callers still own how to report or terminate. The logging in Run exists mainly for the web, where an exception escaping Run() otherwise reaches the browser as a bare rejected promise with no message; on native builds it adds one line before std::terminate prints what(). The Emscripten frame body has its own exception handling, described on the frame page. Non-desktop entry points (mobile hosts and the browser's Asyncify linkage) are outside this trace.
Tests and source tour
These tests exist at this snapshot; none was executed for this page. GamePlatformOwnershipTests.cpp targets install-before-members (ExplicitPlatformIsOwnedAndInstalledBeforeGameMembers) and failed-constructor cleanup (AFailedConstructionLeavesNothingInstalled, which skips when the build's renderer accepts the Headless platform and so has no failed construction to observe). GameTests.cpp targets built-in readers registered before LoadContent (ConstructionRegistersBuiltInXnbReadersBeforeLoadContent), lifecycle order (RunExecutesLifecycleInDocumentedOrder) and the deferred LoadContent path. GraphicsDeviceManagerTests.cpp targets the project profile default, CreateDevice after Run and reset-event forwarding; GameCultureTests.cpp targets the locale fallback. The renderer attempt loop and its video-reference accounting have their own suites in modules/graphics/tests/CNA: MultiRendererFallbackTests.cpp (twelve cases: substitution, walking the preference order, an exhausted chain, initialization failure after the window exists, cross-window-kind recreation, and a caller-supplied window refusing a cross-kind candidate) and GraphicsDeviceSubsystemLifecycleTests.cpp (seven cases: a windowed device takes exactly one video reference, a failed construction and a fall-back both balance, repeated lifetimes do not accumulate, disposing twice releases once). They do not show that every native backend can create a window on every host; window-dependent cases skip when the configured platform cannot create one.
demo_2d Main.cpp— the executable-owned call into CNA.PlatformFactory.cppandCurrentPlatform.cpp— distinguish owned instance, ambient borrowed pointer, lazy default and subsystem pins.Game.cpp— initialiser order,InstallPlatform, thePlatformInstallationguard,RunandDoInitialize.GraphicsDevice.cpp— the constructor'stry/catch,resolveRenderer,createOrAttachWindowandcreateRenderer.GraphicsDeviceManager.cpp—CreateDevice,INTERNAL_CreateGraphicsDeviceInformationandapplyToExistingRenderer.
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.
- DIRECTX9: stock-effect bytecode, device lifecycle and oracle findings — How the XNA-fidelity renderer compiles Microsoft's stock effects, enforces GraphicsProfile from D3DCAPS9, recovers lost devices, handles targets and why its sprite projection is what it is.
- 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.
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- Architecture
- Runtime lifecycle · Platform architecture
- Internals
- Runtime module internals · Renderer selection internals · GraphicsDevice internals · One frame source trace
- Maintainer workflow
- Ownership and lifetime master map · Debug shutdown and lifetime behavior
- Tests and validation
- Test architecture
- Reference
- Selection axes: platforms