Architecture overview

CNA snapshot 009d40f5  ·  Development › Architecture Maps  ·  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 layer picture, selection rules and ownership facts were read from the CMake selection files, Game.cpp/Game.hpp, GraphicsDevice.cpp, ContentManager.hpp and the platform and input headers; the named tests exist but were not executed for this page.

CNA separates the XNA-shaped API that games call from runtime orchestration, host integration and renderer translation. Selection happens twice: CMake decides at configuration time which platform, audio and renderer implementations exist in the binary, and generated registries and factories bind them when the process starts. This is the map a maintainer needs before opening any subsystem: the layers and the direction calls may travel, the three independent backend axes, the ownership facts that matter most, and where a change has to cross a boundary. The user-level version of the same picture is the Architecture page; the source-level tours are linked at the end.

Layers and permitted direction

Game code / samples               (the executable owns its Game; there is no CNA-owned main)
  │ public CNA headers
  ▼
runtime ── content ── input ── audio ── media ── graphics                 (physical modules)
  │                              │                    │
  │ IPlatform contract           │ IAudioDevice       │ IGraphicsRenderer + descriptor + registry;
  │ (windows, events, timing,    │ contract           │ renderers receive only narrow window /
  │  input, host services)       │                    │ surface / GL-context / Vulkan-surface services
  ▼                              ▼                    ▼
platform implementation   audio implementation   renderer implementation
(CNA_PLATFORM)            (CNA_AUDIO_PLATFORM)   (CNA_GRAPHICS_RENDERER)
  │                              │                    │
  └──────── OS / SDL / native window systems / sound devices / GPU APIs ────────┘

Policy sits above and native work below. Game is the lifecycle coordinator: it owns the platform, the graphics device, the window, the content manager and the component lists, and drives them through Run and Tick. It is not a service locator for backend internals. The one deliberate window is Game::GetPlatformEXT(), a CNAEXT accessor for the platform the game owns (its comment says subsystems a Game owns should reach the platform through it, and that the ambient accessor is for the static parts of the XNA API), together with a protected Game(std::unique_ptr<IPlatform>) constructor whose comment describes it as primarily for embedding hosts and cross-implementation tests. IPlatform (IPlatform.hpp) owns host-facing services: subsystems, windows, events, timing, input, clipboard, dialogs, filesystem and the creation seams for GL contexts, Vulkan surfaces and CPU-frame presentation. GraphicsDevice owns the public graphics state and resources and delegates native work to the selected renderer. Content, input and audio keep their own contracts even where an SDL implementation exists underneath, and audio is deliberately not part of IPlatform.

“Permitted direction” is a reading heuristic, not an enforced acyclic graph. The renderer-selection policy (GraphicsRendererSelection, the identity enumeration and its maturity and category tables) lives in the core module so that every layer can see it, and three static-archive cycles are declared on purpose (graphics ↔ input, graphics ↔ renderer families, audio ↔ media). The build graph must never be used to justify bypassing an interface; see the declared cycles and the canonical mental model.

Three independent backend axes

CMake cache variableMeaningValues at this snapshot
CNA_PLATFORMWindows, event loop, timing, input and native host servicesSeven implementations: SDL3 (default), SDL2, X11, WAYLAND, WIN32, HEADLESS, TERMINAL. SDL12 and EMSCRIPTEN are reserved and refused. WIN32 is Windows-only, TERMINAL POSIX-only, and X11 and WAYLAND are offered only where their development packages exist (PlatformSelection.cmake)
CNA_GRAPHICS_RENDERER (default identity) and optional CNA_GRAPHICS_RENDERERS (a compiled set)Graphics API and command/resource translation25 public identities over 21 implementation families; per-host default WEBGL2 under Emscripten, OPENGLES3 on Linux, SDL_RENDERER elsewhere. A name outside the 25 is a configure-time error (RendererIdentities.cmake)
CNA_AUDIO_PLATFORMPlayback and capture device backendFour implementations: SDL3 (default), SDL2, NULL, ALSA. OPENAL and WASAPI are reserved and refused. SOUND_ENABLED (a mixer exists) is defined for SDL3 and ALSA only (AudioPlatformSelection.cmake)

The axes are validated separately and then together. A reserved or unknown name fails the configure with a message naming the alternatives; nothing falls back silently to another backend, because that would build something other than what was asked for. Platform selection also depends on the host and on discovered dependencies. The per-identity host gates, dependencies and identity macros live in the cna_configure_renderer_identity macro of RendererSelection.cmake; the five GL-profile identities map to the single EasyGL family, and OPENGL4 is a separate family whose arm requires find_package(OpenGL) and declares the default-OFF option CNA_OPENGL4_COMPILED_EFFECTS. The full identity-to-family map is generated on the selection axes index.

CNA_ENABLE_SDL=AUTO|ON|OFF (SdlAvailability.cmake) controls whether SDL is configured at all; it is not a fourth runtime abstraction. At this snapshot AUTO and ON configure SDL identically, and OFF rejects any selection that needs it: the SDL3 or SDL2 platform, the SDL3 or SDL2 audio platform, and the SDL_RENDERER, SDL_GPU, FNA3D and FREEDIRECT renderers. The target operating system is a separate, compile-time concern (CNA::TargetPlatform), independent of all three axes.

Only the renderer axis has a run-time choice among compiled-in implementations (CNA::GraphicsRendererSelection); the platform and audio implementations are fixed by the build, with two footnotes: HEADLESS is compiled into every binary and TERMINAL into every POSIX one so that the conformance suite can run several platforms in one process, and PlatformFactory::Create(name) can construct any compiled name.

High-value ownership facts

  • Game declares its owned platform before every other member, installs it as the ambient platform from that first member initializer, and destroys it last. If a later member throws, a scope guard undoes the installation. Pinned by GamePlatformOwnershipTest.ExplicitPlatformIsOwnedAndInstalledBeforeGameMembers and GamePlatformOwnershipTest.AFailedConstructionLeavesNothingInstalled.
  • The GraphicsDevice is value-owned by Game (GraphicsDevice_). GraphicsDeviceManager configures that device in place rather than owning a separate one: a Game-attached manager records that it does not own the device, so disposing the manager never deletes it.
  • GraphicsDevice tracks every graphics resource it creates and disposes them while the renderer still exists, before the renderer, surface presenter, window and video-subsystem reference are torn down.
  • ContentManager owns its runtime cache, keyed by requested type and normalized asset name. Unload() clears that cache and nothing else; copies a game still holds keep their own resources.
  • Input snapshots are advanced on the game-loop thread at the end of Game::PollEvents, after the platform's event batch has been processed: the keyboard and mouse services every frame, the gamepad and joystick services only once a game has asked for them. There is no separate per-frame input-update call in Game.

The complete lifetime picture, including ambient statics, audio callbacks and C handles, is the ownership and lifetime master map.

Where to cross a boundary

New host behavior belongs behind platform interfaces. New GPU behavior belongs in the graphics contract plus every affected renderer family. A public API addition begins in its owning module, but it is incomplete until implementation, tests, C API and binding implications and backend parity have been assessed. The build graph must not be used to bypass an interface merely because two modules happen to link together.

Some of these boundaries are enforced mechanically rather than by convention: the platform contract headers are compiled without SDL (ContractIsSdlFreeTests.EveryContractHeaderCompilesWithoutSdl), the graphics core's renderer descriptors carry data instead of windowing-library flags, the tools/platform audits keep new SDL references out of production code, and the source-partition gate keeps every translation unit inside a declared module. Checks like these protect architecture and source classification; they do not prove behavior on every host.

Where to go deeper

  1. Control flow and lifetimes: Runtime lifecycle, then the startup, frame and shutdown traces under runtime internals.
  2. Host integration: Platform architecture and the platform backends map.
  3. Rendering: Graphics architecture, renderer selection internals and GraphicsDevice internals.
  4. Assets and services: Content architecture and Audio and input architecture.
  5. Foreign callers: C API and bindings architecture.
  6. The build view of all of the above: the physical module dependency map.

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

Tests and validation
Test architecture