The canonical mental model
Evidence basis: source-verified at the pinned commit. 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 control path, descriptor fields, event order and link cycles were read from Game.cpp, GraphicsDevice.cpp, GraphicsDeviceManager.cpp and the module CMake files at the snapshot; no program was run.
Before studying a class, separate four questions: who starts execution, who owns the object, which abstraction receives a call, and which selected physical implementation acts. CNA's public XNA-style API often hides that last step, and the build graph can hide it too. This page gives the one model of CNA that the rest of the Development area assumes; each section ends with the internals pages that prove it line by line.
Control path of a game
executable's main() owns the derived Game (no CNA-owned main)
→ Game::Game() delegates to Game(PlatformFactory::Create())
platform_ ← InstallPlatform(): push + SetCurrentPlatform (first member)
platformInstallation_ undo guard, disarmed when the constructor completes
GraphicsDevice_ value member → GraphicsDevice ctor → resolveRenderer()
→ descriptor → window + presenter/GL/Vulkan service → IGraphicsRenderer
Content_, Window_ ← device window; built-in XNB readers registered
→ Game::Run → DoInitialize
→ IGraphicsDeviceManager::CreateDevice (if registered) → Initialize → LoadContent
→ BeginRun → BeforeLoop → RunLoop: while RunApplication → Tick
Tick: advance clock / fixed-step wait
→ PollEvents: IPlatform::PollEvents → PlatformInputBridge::ProcessEvent
→ quit / window / lifecycle handling → keyboard/mouse snapshot Update
→ Update (repeated per fixed step) → BeginDraw → Draw → EndDraw (→ Present)
→ OnExiting → EndRun → AfterLoop
→ explicit Dispose() and/or ~Game: Dispose(false), UninstallPlatform,
members destroyed in reverse order (GraphicsDevice_ before platform_)
The executable owns its Game; there is no universal CNA-owned main (demo_2d's Main.cpp simply news a Game1, calls Run() and deletes it). Platform construction and renderer/window creation happen inside the base Game constructor, before the derived constructor body and long before the derived game's Initialize: member order in Game.hpp is the only thing that guarantees the platform outlives the device, window and content manager. GraphicsDeviceManager does not own the device — CNA's Game always pre-owns it, unlike FNA — it registers itself as the IGraphicsDeviceManager and IGraphicsDeviceService services and applies settings to the existing device (CreateDevice / ApplyChanges → applyToExistingRenderer → an in-place GraphicsDevice::Reset). A game without a manager, such as demo_2d, still presents: Game::EndDraw falls back to GraphicsDevice::Present().
Two places can change what a frame does: in fixed-step mode Tick may run Update several times before drawing, and SuppressDraw() or a BeginDraw() that returns false (for example while a browser WebGL context is lost) skips Draw/EndDraw for that tick. The startup, frame and shutdown traces show why constructor order, reset events and teardown cannot be reasoned about from Game::Run alone; the user-level lifecycle is on Game loop: lifecycle diagram.
Policy above, native work below
GraphicsDevice is the shared state, validation and resource-facing layer. A renderer family publishes a GraphicsRendererDescriptor (GraphicsRendererDescriptor.hpp) that advertises what can be known before native construction: its name and window kind, whether it needs the video subsystem, a surface presenter, a GL context or a Vulkan surface from the platform, a cheap isAvailable probe, and the create factory. cmake/RendererRegistry.cmake generates a build-tree C++ table of the descriptors linked into this build, because a self-registering object file inside a static archive would be discarded by the linker.
At run time GraphicsDevice::resolveRenderer() (GraphicsDevice.cpp) walks the attempt order from CNA::GraphicsRendererSelection: it records candidates that are not compiled in, refuses a candidate whose window kind conflicts with a caller-supplied window, runs the availability probe, creates or adopts the window, then calls createRenderer(), which hands the renderer only the narrow platform services its descriptor asked for and constructs it through descriptor.create(args). The selection latches only on success; an exhausted chain throws InvalidOperationException naming the first failure. Without an opted-in fallback chain there is exactly one attempt and its exception propagates unchanged.
Public draw methods validate and collect state; a physical renderer translates buffers, effects and render targets to its API and submits and presents. Vulkan records deferred draw records (Pending3DDraw) and submits them in SubmitFrame; Software rasterizes into a CPU framebuffer; Headless validates and traces without producing pixels. A public method call therefore does not imply identical timing, synchronization or failure point across implementations. Read the device contract, selection internals and the indexed draw trace together; the user-level description is how renderers work.
Host service, not renderer superclass
CNA::Platform::IPlatform (IPlatform.hpp) owns the host-facing window, event, time, input and service contract. Graphics requests narrow services from it — GetGlContext(), GetVulkanSurface(), CreateSurfacePresenter() — rather than inheriting from a platform class. Game::PollEvents clears nothing itself; the platform's PollEvents clears and fills the caller's batch, every event goes to PlatformInputBridge::ProcessEvent first, then Game handles quit, window and application-lifecycle events, and finally advances the keyboard and mouse snapshot services once per frame (gamepad and joystick only after a game has asked for them, because acquiring that subsystem is expensive). Static input APIs such as Keyboard::GetState consult the ambient platform installed by Game, or the snapshot services behind it.
SDL3, SDL2, Win32, X11, Wayland, Headless and Terminal implement different, truthful subsets of the contract and report them through PlatformCapabilities. For instance, Terminal returns no GL context and no Vulkan surface but can present a finished CPU frame through its surface presenter, whereas Headless refuses CreateSurfacePresenter with PlatformNotSupportedException rather than accepting frames and dropping them. Read the platform architecture before one concrete backend; the user view is the seven platform implementations.
Other paths that cross the main loop
| Path | Entry and transformation | Maintenance risk |
|---|---|---|
| Content | ContentManager::Load<T> tries .xnb, then .cnb, then loose files, chooses a reader, caches the result and returns the runtime object. The build-time content pipeline (cna-content, the content-pipeline module) is a separate importer → processor → writer graph that never enters a game's link closure. | The format/reader contract and cache disposal differ from ordinary graphics allocation; a writer change must be followed into the runtime reader. |
| Audio | SoundEffect / SoundEffectInstance reach the mixer and voice layer and the selected device transport; device callbacks or worker threads pull samples. Only the SDL3 and ALSA audio selections have a mixer (SOUND_ENABLED); SDL2 and NULL transport audio without one. | Callback lifetime and synchronization, not just main-thread API behaviour. |
| Interop | External C#, Java, TypeScript, Python, Rust, Swift, Go and Ruby layers call the in-tree C ABI, which converts opaque handles and error codes (HandleRegistry, CallWithExceptionBarrier) and reaches native CNA. | ABI version, ownership and foreign-callback boundaries. The bindings target ABI 0.21.x; this snapshot exports 0.29.0, so they lag it. |
| Build/test | CMake selects platform, audio and renderer families and filters test sources by configuration. CTest sees CnaTests cases (discovered at test time) and per-family example registrations; the focused per-module test executables are iteration targets, not CTest registrations. | A passing configuration does not prove excluded backends or skipped tests. |
Follow runtime content, the content pipeline, the audio engine, the C API, the build graph and the test graph for the actual implementations. User-level entry points: the load ladder, audio implementations, the C API programming model.
Dependency direction and the places it is imperfect
As a reading heuristic, think "Game/runtime policy → public graphics, input and content services → platform or renderer implementation → OS/device", with the C API and the bindings adapting into CNA. Do not treat that as an enforced acyclic graph. At this snapshot the static archives have declared, intentional cycles, each written on both targets so that CMake repeats the archives on the link line:
cna_graphics_core↔cna_input:GraphicsDeviceupdates TouchPanel metrics and the Mouse/TextInputEXT window binding, whileMouseCursorbuilds onTexture2D;cna_graphics_core↔ each selected renderer: the device constructs renderers through descriptors whose definitions live in the renderer archives, and renderer code calls back into graphics, core and math;cna_audio↔cna_media:FrameworkDispatcher::Update()pumps both audio streams andMediaPlayer, which itself plays through the mixer.
Two repetitions were not enough once the DirectX families added a third archive (cna_renderer_d3dcommon) that reaches back into the graphics core, so modules/graphics/CMakeLists.txt and modules/input/CMakeLists.txt set LINK_INTERFACE_MULTIPLICITY 3. The CNA umbrella in modules/CMakeLists.txt is an INTERFACE composition of the runtime parts plus the default renderer target; with CNA_SHARED_LIBRARY it instead defines one shared runtime containing every compiled-in renderer family. Distinguish a CMake link edge from the owner of a runtime object; neither gives C++ source permission to include a sibling module's private src/ headers (cna_add_module exposes only each module's own include/). Consult the physical module dependency map and target creation before moving files or flattening a "redundant" dependency.
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- Game loop: lifecycle · How renderers work
- Architecture
- Runtime lifecycle · Graphics architecture · Platform architecture
- Maintainer workflow
- Ownership and lifetime master map · Thread and callback map
- Tests and validation
- Test architecture
- Reference
- Physical module dependency map