The physical module architecture: ownership, targets and composition
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. Read from modules/CMakeLists.txt, modules/renderers/CMakeLists.txt, each module's CMake file and cmake/Version.cmake; migration figures are quoted from modularization/RECONCILIATION.md, which describes its own date. Nothing was configured or built.
CNA is one repository but not one library: its unit of ownership is the physical module, a directory under modules/ that owns its CMake target, its public include root, its implementation, its tests and its examples. Include spelling, link closure, which optional features exist and which tests compile all follow from those boundaries. This page explains the rules behind the layout at snapshot 009d40f5 and the places where a directory name, a target name or an output path would mislead you. The per-module inventory is on Repository map and the exact link edges on Physical module dependency map.
Location is ownership
A framework module has the shape modules/<name>/{CMakeLists.txt, include/, src/, tests/, examples/}, plus benchmarks/ where one exists (only diagnostics and inspector have one). The repository root deliberately has no production src/ or include/. Two configure-time checks at the end of modules/CMakeLists.txt hold that shape:
- The source-partition validator recursively globs every
*.cppand Objective-C++*.mmundermodules/and accepts a translation unit only if it sits under a declared framework module's, or a declared renderer family's,src/,tests/,examples/orbenchmarks/. The declared lists,_cna_framework_modules(22 names) and_cna_renderer_modules(21 families pluscommon/d3dandcommon/mojoshader), are the authority for what a module is; anything else stops the configure with "translation unit outside every declared physical module". - The legacy-root guard fails the configure if
src/orinclude/reappears at the repository root. It testsCNA_SOURCE_DIR, the root of the CNA project itself, and notCMAKE_SOURCE_DIR: when a game adds CNA withadd_subdirectory,CMAKE_SOURCE_DIRis the game's own root, which is entitled to its ownsrc/, and an earlier version of the guard failed every such consumer's configure with a bogus "legacy tree reappeared".
The rule is stronger than tidiness. Moving an implementation file changes the target that compiles it, the dependency declarations its symbols travel under, the test group that registers it and the include roots it can see. The validator turns a physical move into a change that has to be reconciled deliberately, either by placing the file under its real owner or by extending the declared module set.
What the partition check does not see
The validator looks only at .cpp and .mm files and only at the path prefix. A stray header or a C source outside every module is not reported. And because renderer families collect their sources with a non-recursive glob (src/*.cpp in cna_add_renderer(), modules/renderers/CMakeLists.txt), a translation unit placed in a subdirectory of a family's src/ passes the partition check (the prefix matches) and is then silently not compiled. The non-recursive glob is intentional (shader headers live in src/shaders/ and a nested file must not join the target merely by existing on disk), but it means that "the configure passed" does not prove a new renderer file is built; the configure log and the target's source list do.
The module set and why it is cut this way
The declared framework set at this snapshot is 22 modules, and modules/renderers/ is the 23rd directory. An older description of "fifteen framework modules and one optional ABI module" predates the diagnostics, design, phone, inspector, content-pipeline and video-ffmpeg modules; the fifteen plus the C ABI module plus those six make the 22, and the older count has no place for the build-time pipeline at all. The useful way to read the set is by the boundary each cluster protects:
| Cluster | Modules | The boundary it protects |
|---|---|---|
| Foundation | core, math, diagnostics | Leaf vocabulary with no upward edge. core owns logging, exceptions, the renderer-identity enum, selection policy and the generated version header; math the XNA value types; diagnostics compiles to nothing when CNA_DIAGNOSTICS=OFF. |
| Host | platform | IPlatform and its implementations. The module that links the native windowing and host libraries for the platform implementations (SDL3 or SDL2, Xlib, Wayland, Win32): PRIVATE on desktop, PUBLIC on Android and iOS, where CNA/Platform/Entrypoint.hpp puts a platform header into the consumer's own translation unit. It is not the only module that touches SDL: the SDL audio implementation and mixer in audio, and the renderer families that are SDL by identity (sdl-renderer, sdl-gpu) or by upstream dependency (fna3d, freedirect), link it privately too; the SDL ratchet allowlists exactly those (see Invariants). |
| Graphics and devices of the game | graphics, input, audio, media, video-ffmpeg | The XNA-shaped runtime services. video-ffmpeg exists so that every FFmpeg symbol lives in one optional archive that media links privately. |
| Content | content, content-pipeline | What a game needs to load assets versus what must never ship: the pipeline module (FreeType, FFmpeg importers, the XNA pipeline facade) is never linked by the CNA umbrella a game uses; only the content compiler, the pipeline's own tests and two fuzz harnesses link it. |
| Orchestration and persistence | runtime, storage | Game and its loop above everything it coordinates; storage deliberately below it, with no CNA archive in its closure. |
| Extensions and compatibility shells | devices, devices-ext, graphics-ext, phone, design | XNA-faithful surfaces kept apart from project extensions: devices-ext must never depend on the XNA devices base, and the CNAEXT engine layer compiles only with CNA_CNAEXT. |
| Networking | gamer-services, net | Entered only with CNA_ENABLE_NET (default ON); the ENet transport stays inside net. |
| Opt-in tools and adapters | inspector, c-api | Never pulled into a game silently: the Inspector exists only with CNA_BUILD_INSPECTOR, the C ABI only with CNA_BUILD_C_API (which requires networking). |
| Renderers | renderers/<family> | One archive per compiled family behind the graphics-owned renderer contract. |
Targets, aliases and names you cannot guess
Most modules are created by one function in modules/CMakeLists.txt:
function(cna_add_module target alias)
add_library(${target} STATIC ${ARGN})
add_library(CNA::${alias} ALIAS ${target})
target_link_libraries(${target} PUBLIC cna_build_config)
target_include_directories(${target} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
endfunction()
The shared public surface is cna_build_config (alias CNA::BuildConfig); an older name for it, cna_build_flags, no longer exists. It carries the C++23 compile feature and the build facts every consumer must agree on (XNA5, the default renderer's identity macro, CNA_CNAEXT, CNA_DEVICES, CNA_DRACO_AVAILABLE, CNA_VIDEO_AVAILABLE/CNA_FFMPEG_AVAILABLE, CNA_DIAGNOSTICS_LEVEL, SOUND_ENABLED for the two mixer-backed audio values, and under Emscripten the exception ABI and stack size). It exports no include directory at all.
Build tooling that derives concrete target names from directory names will be wrong for several modules. Use the exported aliases, or read the module's own CMake file:
| Directory | Concrete target | Alias | Why it differs |
|---|---|---|---|
graphics | cna_graphics_core | CNA::GraphicsCore | Named for its role; the generated renderer registry compiles into it |
content-pipeline | cna_content_pipeline | CNA::ContentPipelineBuild | Alias names the build-time role |
gamer-services, net | CNA_GamerServices, CNA_Net | CNA::GamerServices, CNA::Net | Hand-built with add_library, historical CNA_ prefix |
video-ffmpeg | cna_video_ffmpeg | CNA::VideoFfmpeg | Hand-built; exists only when the FFmpeg probe succeeds |
c-api | cna_c_api (shared; static under Emscripten) | CNA::CApi | Hand-built shared library with hand-listed sources |
core | cna_core and cna_core_headers | CNA::Core, CNA::CoreHeaders | A header-only interface beside the archive (below) |
renderers/sdl-renderer | cna_renderer_sdl_renderer | none | Derived from the directory, - becoming _ |
The renderer rule is the one place where the name is derived from the directory, and it is derived on purpose: cna_renderer_target_name() computes it from the calling family's own directory, so no family reads a global variable that some other file set. That replaced an arrangement in which each family read RENDERER_TARGET, which is correct only in a single-renderer build where the default is the only renderer; a wrong read there is invisible until a multi-renderer build enters a non-default family.
One public include root per module
Every module exports exactly its own include/, and consumer spelling does not change with the physical owner, because the module root is stripped by the include path. A game writes
#include <Microsoft/Xna/Framework/Game.hpp> // modules/runtime/include
#include <Microsoft/Xna/Framework/Graphics/Texture2D.hpp> // modules/graphics/include
#include <CNA/GraphicsRendererType.hpp> // modules/core/include
and must never embed modules/runtime/include/ or any other repository-relative prefix: the physical path identifies the owner for a reviewer, it is not the public spelling. Headers that one module needs from another live under the owner's include/CNA/Internal/. That directory name is a naming and review convention, not a visibility barrier: any target that links a module can include everything under its include/.
Module-private headers under src/ are normal (the platform module alone has more than a hundred). What is exceptional is another module reaching into them, and there are exactly two such reaches, both private: every renderer target adds modules/platform/src so that the four SDL-backed families can include one interop header, and the unit-test build adds modules/audio/src so implementation tests can see a private audio device. The comment above cna_add_module, which says the only headers under src/ are renderer-local shader headers, is older than both facts; the CMake is what executes.
Headers without the archive
cna_core_headers (CNA::CoreHeaders) is an INTERFACE target that publishes the core include roots without linking cna_core. math, storage, platform, phone and inspector use it because their public headers name leaf declarations (the CNAEXT marker from CNAHelper.hpp, PlayerIndex, the path-containment helpers) without using any core symbol, which keeps their link closures exact; the storage module's closure test depends on it. The generated CNA/Version.hpp rides the same interface: it is written by cmake/Version.cmake into <CNA build dir>/generated/include, which both the header-only target and cna_core publish. The file uses CNA_BINARY_DIR, not CMAKE_BINARY_DIR, for the same subproject reason as the legacy guard.
Renderer families are physical modules too
modules/renderers holds 21 implementation families and two shared helper directories, common/d3d and common/mojoshader. Only the EasyGL family carries more than one public identity (five GL profiles), which is why the registry has 25 identities and the tree 21 families.
| Cluster | Family directories |
|---|---|
| GL-shaped | easygl, opengl4, portablegl |
| Explicit or platform GPU APIs | vulkan, webgpu, sdl-gpu, metal |
| Direct3D | directx9, directx11, directx12 |
| Abstraction library | fna3d |
| Native or portable 2D | sdl-renderer, direct2d, gdi, freedirect |
| Browser 2D | canvas, html-dom, svg-dom |
| No GPU | software, headless, stub |
modules/renderers/CMakeLists.txt enters one directory per distinct selected family, once, in a loop that re-points CNA_GRAPHICS_RENDERER and the family's identity definition for each family so that the family file's own guard matches. A family that serves several identities is entered only for the first of them. Four directories are entered by different rules:
metalis always entered, because it defines an unconditional header-interface target: its policy suites are host-portable and compile into theCnaTestscorpus on every renderer.common/d3dis entered when the default identity isDIRECTX11orDIRECTX12;DIRECTX9has its own independent implementation.common/mojoshaderis entered when a MojoShader target was configured, that is forFNA3Dor a family whose compiled-effects option is on.softwareis entered beforegdiwhen the default isGDI, because GDI compiles eight software-owned 2D translation units that the software file publishes; physical ownership stays withsoftware.
Every family target receives the same setup (cna_renderer_common_setup): the common interface (which forwards cna_build_config), its own identity macro as a private definition (only the default identity's macro is public), the sharp-runtime closure, its own include root if it has one, the private platform-source include root, and private reverse edges to cna_graphics_core, cna_core and cna_math. Those reverse edges exist for every renderer, not only the Direct3D ones, because the renderer contract's header-inlined defaults call back into core logging. Cross-family source sharing is always declared and never implicit: GDI lists its borrowed software sources, and the Vulkan examples compile a handful of EasyGL example sources verbatim so that both renderers are checked against the same golden images. None of that is permission for one family to include another family's private headers. How the compiled set becomes a generated registry and latches one family at run time is on Renderer selection internals.
Examples live with their owners
Framework examples live beside the module whose API they demonstrate, renderer examples beside the family they exercise, and CNAEXT examples under graphics-ext. The graphics module owns the shared renderer-agnostic fixtures (the 2D demo, the house demo, the common/ pixel-test support headers such as PixelTestGame.hpp); a family's example file reaches them through CNA_GRAPHICS_EXAMPLES_DIR, which modules/CMakeLists.txt sets to modules/graphics/examples.
Two facts keep the output tree from telling you who owns an executable. The module example files set CMAKE_RUNTIME_OUTPUT_DIRECTORY to the build root, so executables from every module land side by side for compatibility with scripts that expect them there. And the repository-level examples/ directory is not an example directory any more: it holds examples/golden/, 17 reference PNGs captured from the EasyGL renderer, plus one loose screenshot demo source that only two families' example files compile. The golden images are loaded through repository-root-relative paths, so every test that compares against them (the EasyGL golden tests and the Vulkan tests that reuse their sources) is registered with the repository root as its working directory. A golden image here is a captured CNA output, not an XNA reference; the pixel oracle is a different corpus (see examples as evidence).
Ownership layers at this snapshot
CONSUMERS game executable ---------> CNA (INTERFACE umbrella; one libcna.so on native ELF) tests, tools, libraries -> narrow aliases: CNA::Math, CNA::Content, CNA::Runtime ... C programs --------------> CNA::CApi (only with CNA_BUILD_C_API) OUTSIDE THE UMBRELLA, linked by name Net ----> GamerServices ----> runtime, storage (only with CNA_ENABLE_NET) Phone --> runtime Design --> math Inspector --> diagnostics cna-content --> ContentPipelineBuild --> content (build time only) INSIDE THE UMBRELLA Devices, DevicesExt --> runtime GraphicsExt --> graphics (CnaExt = both) runtime --> graphics, input, content, audio, media, platform, diagnostics content --> graphics, audio, media audio <==> media media ..> video-ffmpeg (only when FFmpeg is found) graphics <==> input graphics <==> each compiled renderer family graphics, input, audio, runtime, DevicesExt --> platform math, storage, platform --> core headers only (no core archive) core diagnostics cna_build_config (public defines and C++23) BENEATH EVERY MODULE: the sharp-runtime components that module requests --> declared link edge <==> declared static-archive cycle ..> optional edge
A simpler picture that is often drawn puts the platform implementation, the audio implementation and the renderer side by side as three peers under the runtime, and groups diagnostics with the CNAEXT engine layer. The selection axes are indeed independent (see the four build axes), but the physical graph is different: the audio implementation is compiled inside cna_audio, the platform module is a dependency of several framework modules rather than a peer of the renderer, diagnostics is a leaf module inside the umbrella that graphics, audio and runtime link, and the Inspector sits outside the umbrella and links diagnostics.
How to read a source path
A path such as modules/content/src/Xna/ContentManager.cpp carries three claims: content owns the translation unit, its implementation is private to the module, and consumers reach its declarations through the module's public include root. It does not say which umbrella links the module, which options compiled the file, or which runtime path called it. For any architectural claim, read four artifacts together:
- the file's physical module;
- that module's
CMakeLists.txt(its PUBLIC and PRIVATE edges and whether it is entered conditionally); - the root composition and partition rules in
modules/CMakeLists.txt; - a link or execution artifact of the configuration you care about: the configure log,
ctest -N, the final link line.
Comments and notes lag the CMake. Examples at this snapshot: the cna_add_module comment about src/ headers cited above; a comment in cmake/Tests/ModuleProbes.cmake that still speaks of a far larger identity count and says SDL3 is a private detail of the core module's logger, although modules/core/CMakeLists.txt states that no core file includes SDL any more; and docs/physical-modules.md, whose dependency table omits edges that the runtime module's own CMake file declares. When a document and the CMake disagree, the CMake is what executes.
How the layout came to be, and paths that no longer exist
The module layout replaced global src/ and include/ trees in a single campaign in August 2026, before the alpha.1 tag. Its no-loss audit is kept in modularization/RECONCILIATION.md: 1,357 production files before and after, 1,287 moved byte-identical and 70 with only include-path directives changed, none lost; registered CTest names compared by name, for example 6,120 names in a HEADLESS configuration before and 6,143 after, with no removals or renames. The record opens with its own banner that it predates the renderer curation and must be read as a fact about its date: its renderer names, paths and counts are historical.
Two later changes are easy to conflate with that move: module-local example registration removed the central example manifests, and renderer terminology replaced backend-named build files. A path from before them may be historically real and absent now: cmake/CnaLibrary.cmake, cmake/BackendLibraries.cmake, cmake/BackendSelection.cmake (now RendererSelection.cmake), cmake/Examples.cmake and the per-renderer cmake/Tests/*Tests.cmake manifests. Stale paths of that kind still sit in .github/workflows/metal-macos-ci.yml: its push filter lists three files that no longer exist, and it does not list the Metal family's own CMakeLists.txt or examples/CMakeLists.txt, where the Metal tests are registered now, so a push to next that changes only Metal test registration does not start that workflow (pull requests are unfiltered and still do).
Evidence and limits
The module list, target names, include roots, entry rules and helper targets were read from modules/CMakeLists.txt, modules/renderers/CMakeLists.txt, each module's CMake file and cmake/Version.cmake at 009d40f5; the migration figures are quoted from the reconciliation record, which describes its own date. Nothing was configured or built; the consequence of a nested renderer source file and of the Metal path filter are readings of the CMake and workflow logic, not observed events.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Architecture
- Physical module dependency map
- Maintainer workflow
- Source ownership: which part of CNA owns this?
- Reference
- Module index