Startup source trace

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. 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 honours GraphicsAdapter's UseNullDevice/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 the GraphicsRendererDescriptor; 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 by CNA_DEBUG_UNAVAILABLE_RENDERERS, or one whose isAvailable() probe says no.
  • It applies CNA_FORCE_HEADLESS_DEVICE_EXT to 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 calls createRenderer(). CNA_DEBUG_FAIL_RENDERER_INIT can 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 throws System::InvalidOperationException carrying 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().

  1. DoInitialize looks up IGraphicsDeviceManager in Services_. If a manager exists it calls CreateDevice() (GraphicsDeviceManager.cpp), which re-targets the game's own device, builds a GraphicsDeviceInformation (preferred back-buffer size, swapped by orientation only on iOS and Android; formats; full screen; PresentInterval::One or Immediate from vsync; multisample 0 or 8; profile), raises PreparingDeviceSettings, brackets the window with BeginScreenDeviceChange/EndScreenDeviceChange, and calls applyToExistingRenderer: SetGraphicsProfileEXT, then SetPresentationMode (before the reset, because the logical size depends on it), then GraphicsDevice::Reset, then UpdateViewportFromWindow. Only after that settle-in reset does it subscribe to the device's Disposing, DeviceResetting and DeviceReset (once, guarded), so first-time setup raises only DeviceCreated. The manager's constructor deliberately skips ApplyChanges to avoid doing this reconfiguration twice.
  2. 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.
  3. Game::Initialize initializes the existing components, looks up IGraphicsDeviceService, subscribes UnloadContent to its DeviceDisposing event, and calls LoadContent if there is no service at all or the service has a device; if the service exists without a device, LoadContent is deferred to DeviceCreated. All three duties live in the base implementation, so a derived Initialize that never calls Game::Initialize skips component initialization, the UnloadContent subscription and the base LoadContent call. A game with no GraphicsDeviceManager (such as demo_2d) has no service to subscribe to, so its UnloadContent is never reached from disposal.
  4. Back in DoInitialize the ordered lists are cleared and rebuilt by categorizing every component, then ComponentAdded/ComponentRemoved are subscribed.
  5. BeforeLoop sets IsActive, raising Activated. The first Tick can 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

StageHow it failsWhat is undone
PlatformFactory::CreatePlatformException for a name not compiled in, or the backend's own constructor errorNothing installed yet.
InstallPlatformstd::invalid_argument for nullNothing installed.
GraphicsDevice_ constructorRenderer refused, window creation failed, renderer constructor threw, or an exhausted fallback chainThe 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 bodyCulture, XNB registration or dispatcher throwsSame unwinding, except that the fully built GraphicsDevice_ is now destroyed through its ordinary destructor; the guard is still armed until the last statement.
GraphicsDeviceManager constructorstd::invalid_argument for a null game or a second managerNothing registered by that manager.
DoInitialize, Initialize, LoadContentAny exceptionNothing 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.

  1. demo_2d Main.cpp — the executable-owned call into CNA.
  2. PlatformFactory.cpp and CurrentPlatform.cpp — distinguish owned instance, ambient borrowed pointer, lazy default and subsystem pins.
  3. Game.cpp — initialiser order, InstallPlatform, the PlatformInstallation guard, Run and DoInitialize.
  4. GraphicsDevice.cpp — the constructor's try/catch, resolveRenderer, createOrAttachWindow and createRenderer.
  5. GraphicsDeviceManager.cpp — CreateDevice, INTERNAL_CreateGraphicsDeviceInformation and applyToExistingRenderer.

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

Tests and validation
Test architecture