Physical module dependency map
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. Every edge was read from the CMake files at this snapshot and recomputed by parsing them; no configure or build was run, so this is the declared graph, not the transitive closure of one configuration. The multi-renderer helper-directory caveat is a reading of the CMake conditions, not a tested failure.
CNA's physical modules are not a layering diagram. A maintainer has to keep four things apart: source ownership (which directory a translation unit lives in), compile and include visibility, the static link closure (which archives an executable receives) and runtime ownership (who destroys an object). modules/CMakeLists.txt declares one target per physical module, composes them into the interface umbrella CNA, and enforces a source-partition gate; a handful of private reverse links solve real static-archive cycles. The graph tells you what compiles, links and rebuilds. It does not tell you who owns a runtime object — for that use the ownership map.
There are 23 directories under modules/: 22 framework modules (the set the partition gate calls _cna_framework_modules) plus renderers/, which holds 21 implementation-family directories and the two shared helper directories common/d3d and common/mojoshader. The per-module header, source and test counts are generated on the module index; this page is about the edges between them.
A useful partial graph of the broad runtime closure
Every edge on this page was read from the target_link_libraries calls in modules/CMakeLists.txt, in each module's CMakeLists.txt and in modules/renderers/CMakeLists.txt, and recomputed by parsing those files (no configure was run). It therefore shows what the CMake files declare, not the transitive closure of one particular configuration. Arrows below are the PUBLIC edges; the private ones are in the table, which lists CNA targets and notable third-party libraries and omits the sharp-runtime components each module adds through cna_link_sharp_runtime.
tier 0 cna_core_headers (INTERFACE) cna_core cna_diagnostics cna_build_config (INTERFACE)
▲ PUBLIC
tier 1 cna_math cna_platform cna_storage [cna_inspector] cna_design → cna_math
▲ PUBLIC (graphics_core also → cna_core, cna_diagnostics)
tier 2 cna_graphics_core ↔ cna_input graphics_core → input is PRIVATE, input → graphics_core is PUBLIC
│ PRIVATE, one per compiled identity family
cna_renderer_<family> → graphics_core, core, math (PRIVATE reverse edges)
tier 3 cna_audio ↔ cna_media audio → media is PRIVATE, media → audio is PUBLIC
tier 4 cna_content → graphics_core, audio, media, math, core
cna_content_pipeline → cna_content (build-time only; linked by cna_content_compiler)
tier 5 cna_runtime → graphics_core, input, content, audio, media, core, diagnostics, math, platform
tier 6 cna_devices, cna_devices_ext, cna_phone → cna_runtime cna_graphics_ext → cna_graphics_core
CNA_GamerServices → cna_runtime, cna_storage ← CNA_Net (+ enet) [CNA_ENABLE_NET]
cna_c_api (shared library) → the modules above, all PRIVATE [CNA_BUILD_C_API]
CNA (INTERFACE umbrella) = runtime + devices + devices_ext + graphics_ext + cnaext + storage + content
+ media + audio + input + graphics_core + diagnostics + core + math
+ platform + build_config + the default renderer target
| Target (alias) | Created by | PUBLIC links | PRIVATE links | Present when |
|---|---|---|---|---|
cna_core (CNA::Core) | cna_add_module; a separate INTERFACE target cna_core_headers (CNA::CoreHeaders) carries the core include roots | none | none | always |
cna_diagnostics (CNA::Diagnostics) | cna_add_module | none | none | always; the instrumentation compiles to nothing when CNA_DIAGNOSTICS=OFF |
cna_math, cna_platform, cna_storage | cna_add_module | cna_core_headers | none | always |
cna_design (CNA::Design) | cna_add_module | cna_math | none | always built; not in the umbrella |
cna_inspector (CNA::Inspector) | cna_add_module, plus the cna-inspector executable | cna_core_headers, cna_diagnostics | none | only CNA_BUILD_INSPECTOR=ON; a configure error on Emscripten, Android and iOS |
cna_graphics_core (CNA::GraphicsCore) | cna_add_module; the generated renderer registry is compiled into it | cna_core, cna_math, cna_diagnostics, cna_platform | cna_input, every target in CNA_RENDERER_TARGETS | always |
cna_input (CNA::Input) | cna_add_module | cna_graphics_core, cna_math, cna_core, cna_platform | none | always |
cna_audio (CNA::Audio) | cna_add_module | cna_core, cna_diagnostics, cna_math | cna_platform, cna_media, cna_input, and the selected audio backend's SDL3/SDL3_mixer, SDL2 or dl libraries | always |
cna_media (CNA::Media) | cna_add_module | cna_audio, cna_graphics_core | cna_input; cna_video_ffmpeg when FFmpeg is available | always; link-complete without FFmpeg |
cna_content (CNA::Content) | cna_add_module; also compiles tools/gltf_to_cnj/gltf_to_cnj.cpp with CNA_GLTF_TO_CNJ_NO_MAIN=1 | cna_graphics_core, cna_audio, cna_media, cna_math, cna_core | cna_draco when Draco is available; zstd when CNA_CNB_ZSTD finds it | always |
cna_content_pipeline (CNA::ContentPipelineBuild) | cna_add_module | cna_content | FreeType, FFmpeg and zlib when their probes succeed | always built; linked only by cna_content_compiler |
cna_runtime (CNA::Runtime) | cna_add_module | cna_graphics_core, cna_input, cna_content, cna_audio, cna_media, cna_core, cna_diagnostics, cna_math, cna_platform | none | always |
cna_devices, cna_devices_ext (CNA::Devices, CNA::DevicesExt) | cna_add_module | cna_runtime, cna_graphics_core, cna_core, cna_math (devices_ext also cna_platform) | none | always built; the extension code inside is compiled under CNA_DEVICES |
cna_graphics_ext (CNA::GraphicsExt) | cna_add_module | cna_graphics_core | none | always built; the extension code inside is compiled under CNA_CNAEXT |
cna_phone (CNA::Phone) | cna_add_module | cna_runtime, cna_core_headers | none | always built; not in the umbrella |
CNA_GamerServices, CNA_Net | plain add_library (no cna_add_module) | GamerServices: cna_runtime, cna_storage; Net: CNA_GamerServices and the vendored enet | none | only CNA_ENABLE_NET=ON (the default) |
cna_video_ffmpeg (CNA::VideoFfmpeg) | plain add_library | none of CNA's own (FFmpeg through pkg-config) | none | only when the FFmpeg probe succeeds |
cna_c_api (CNA::CApi) | plain add_library, SHARED (STATIC under Emscripten) | its own include root | cna_core, cna_platform, cna_runtime, cna_graphics_ext, cna_storage, cna_content, cna_media, cna_devices, cna_devices_ext, CNA_Net, CNA_GamerServices | only CNA_BUILD_C_API=ON, which requires CNA_ENABLE_NET=ON |
cna_renderer_<family> | cna_add_renderer() (or add_library plus cna_renderer_common_setup() for directx9 and software) | cna_renderer_common (INTERFACE, forwards cna_build_config) | cna_graphics_core, cna_core, cna_math | only the families the configuration selected |
The usual one-paragraph summary of these edges holds against the source and can be stated more exactly. cna_graphics_core publicly links math, core, diagnostics and platform, and privately links input and every compiled renderer target. cna_input publicly links graphics, math, core and platform: the apparent reverse direction is real (see the cycle section). cna_audio publicly links core, diagnostics and math and privately platform, media and input; cna_media publicly links audio and graphics and privately input. cna_content publicly links graphics, audio, media, math and core because a reader can construct those runtime objects. cna_content_pipeline publicly links content but is not in the game umbrella: cmake/ToolContentPipeline.cmake defines cna_content_compiler (alias CNA::ContentCompiler, a static library over tools/content/content.cpp) as the only consumer, and the cna-content executable is a small main over that library. cna_runtime publicly links the nine targets in the table, which is why a change to a foundational public header or to GraphicsDevice has a much larger rebuild and review radius than a change to one content importer. The exact per-target definitions are in graphics, input, audio, media, content and runtime.
Composition is not source ownership
cna_add_module(<target> <Alias> sources...) creates a STATIC library named cna_<name>, an alias CNA::<Alias>, links cna_build_config PUBLICly and publishes the calling module's own include/ directory. Eighteen of the 22 framework modules use it (inspector only when CNA_BUILD_INSPECTOR is ON); gamer-services, net, video-ffmpeg and c-api define their targets by hand, and core adds the extra header-only target. Renderer families use the analogous cna_add_renderer() helper described in renderer selection internals.
cna_build_config is the shared public surface and is deliberately limited to ABI and header requirements: C++23, XNA5, the default renderer's identity macro, CNA_CNAEXT and CNA_DEVICES when those options are on, CNA_DRACO_AVAILABLE, CNA_VIDEO_AVAILABLE and CNA_FFMPEG_AVAILABLE, CNA_DIAGNOSTICS_LEVEL, and SOUND_ENABLED when the audio platform is SDL3 or ALSA. Compiler policy lives elsewhere: cmake/BuildPerformance.cmake defines cna_project_options and its siblings, and cna_apply_build_support_to_cna_targets() links them PRIVATE to every CNA-owned compiled target (executables and static, shared, module and object libraries) at the end of the root file, so they travel only as link-only dependencies and are not part of a consumer's compile requirements.
The CNA umbrella is an INTERFACE library, not a module. Its part list is exactly: runtime, devices, devices_ext, graphics_ext, the cna_cnaext umbrella, storage, content, media, audio, input, graphics_core, diagnostics, core, math, platform, build_config and the default renderer target. CNA::Design, CNA::Phone, CNA::Inspector, CNA::ContentPipelineBuild, CNA::GamerServices and CNA::Net are not in it and are linked explicitly. CNA::CnaExt (cna_cnaext) composes the real graphics-ext and devices-ext modules; the historical cna_noxna spelling survives only as a comment. Non-default renderer targets reach a static executable through cna_graphics_core's private edges.
Renderer targets follow one naming rule: modules/renderers/<family> builds cna_renderer_<family> with hyphens turned into underscores (canvas, direct2d, directx9, directx11, directx12, easygl, fna3d, freedirect, gdi, headless, html_dom, metal, opengl4, portablegl, sdl_gpu, sdl_renderer, software, stub, svg_dom, vulkan and webgpu). The name is derived from the directory that calls cna_add_renderer(), so no family reads a global target variable, and one family serves several identities (EasyGL serves five). The helper targets are cna_renderer_common (INTERFACE), cna_renderer_d3dcommon (the shared Direct3D 11 and 12 core), cna_renderer_mojoshader_effect (the MojoShader-to-CNA translation shared by every family that runs compiled XNA effects), cna_renderer_d3d9_effect (DirectX 9's effect archive), cna_renderer_metal_headers (always defined, because the Metal policy suites are host-portable) and cna_renderer_software_headers (defined in the GDI configuration).
The declaration order in modules/CMakeLists.txt is readable, not a topological proof: target_link_libraries resolves names at generate time, so forward references and cycles are legal. The same file ends with the physical source-partition gate: every .cpp and .mm under modules/ must sit under a declared module's src, tests, examples or benchmarks tree (renderers under renderers/<family> or renderers/common/<helper>), and a revived root src/ or include/ tree is a configure error. Cross-module internal headers are published under a module's include/CNA/Internal; another module's private src/ is not a shared include root. Three real exceptions are worth knowing:
- GDI compiles eight software-owned 2D translation units (the list
CNA_GDI_SOFTWARE_SOURCES: framebuffer and texture allocation, framebuffer, 2D texture, 2D render target, 2D state, sprite batch and the 2D renderer) into its own archive;the software CMake filepublishes the list in the GDI configuration, and physical ownership stays withsoftware. The GDI CMake file's status message hard-codes “3 GDI-owned units”, whilemodules/renderers/gdi/srcholds four.cppfiles (a stale count in a message, found by reading). - Every renderer target gets
modules/platform/srcas a PRIVATE include root (cna_renderer_common_setup). The comment ties it to the four allowlisted SDL3-backed families (sdl-renderer,sdl-gpu,fna3d,freedirect, each settingREQUIRES_PLATFORM SDL3), which resolve a window id at one SDL3 implementation edge; the platform tree is not part of any renderer's public include contract. - A few targets compile sources from
tools/:cna_contenttakes the glTF converter, andcna_content_compileris defined incmake/rather than in a module. The partition gate scansmodules/only.
Moving a .cpp between modules is therefore an ownership and build change, not a cosmetic rename. Each module also names its sharp-runtime components through cna_link_sharp_runtime(<target> PUBLIC|PRIVATE ...) (SharpRuntimeConsumption.cmake), for example Core.Base for core and IO, Runtime and Threading for storage; against a monolithic sharp-runtime checkout the same helper links the single SHARP_RUNTIME archive.
The declared static-archive cycles
Parsing all link edges and computing strongly connected components finds exactly two groups among the framework targets: {cna_graphics_core, cna_input, the compiled renderer targets and their helper archives} and {cna_audio, cna_media}. Each cycle is declared on both members so CMake repeats the archives on the link line, and each has a stated reason in the CMake comments:
| Cycle | Why it exists | Declared as |
|---|---|---|
cna_graphics_core ↔ cna_input | GraphicsDevice updates TouchPanel display metrics and the Mouse/TextInputEXT window binding, while Input::MouseCursor builds on Texture2D | graphics→input PRIVATE, input→graphics PUBLIC; LINK_INTERFACE_MULTIPLICITY 3 on both targets |
cna_graphics_core ↔ each selected cna_renderer_<family> | the device constructs renderers through descriptors whose create function lives in the family archive, while renderer code calls back into graphics (for example Effect::Apply), core (Logger) and math | graphics→${CNA_RENDERER_TARGETS} PRIVATE; each family → graphics_core, core, math PRIVATE. cna_renderer_d3dcommon and cna_renderer_mojoshader_effect reach back the same way |
cna_audio ↔ cna_media | FrameworkDispatcher::Update() (in the audio module) pumps dynamic streams, microphone buffers, MediaPlayer and the touch panel, while media plays through the mixer | audio→media PRIVATE, media→audio PUBLIC; default repetition (no multiplicity property) |
Two repetitions were enough while the graphics/input cycle was two archives deep. The DirectX families added a third archive (cna_renderer_d3dcommon, PUBLIC for directx11 and directx12) that reaches back into the graphics core, so both modules/graphics/CMakeLists.txt and modules/input/CMakeLists.txt set LINK_INTERFACE_MULTIPLICITY 3; the comment there records a real failing link of a DirectX 12 test executable. These are static archive relationships, not permission to introduce arbitrary source-level cycles, and deleting an edge can pass object compilation and still fail the final link.
Optional modules and backend artifacts
- FFmpeg video.
video-ffmpegis entered only whenCNA_FFMPEG_AVAILABLEis true:CNA_ENABLE_VIDEOis notOFF, the target is not MinGW, Windows, Emscripten, Android or iOS (ONthere is a configure error), and the libavcodec, libavformat, libavutil and libswresample pkg-config modules are found.cna_mediakeeps the stable public API and links the backend privately; without it the media module compilesVideoDecoderUnavailable.cppandAudioDurationProbeUnavailable.cppinstead. - Networking.
gamer-servicesandnetare entered only withCNA_ENABLE_NET(default ON). The opt-inc-apiadapter links across the native modules and needs that closure for its current surface:CNA_BUILD_C_API=ONwithCNA_ENABLE_NET=OFFis a configure error in the root file. - Extension code in always-built modules.
graphics-extanddevices-exttargets always exist; their sources are guarded byCNA_CNAEXTandCNA_DEVICES, which are default-OFF options published throughcna_build_config. - Other build-time probes configured in the same file: Draco (
CNA_ENABLE_DRACO, default ON except under Emscripten, pinned submodule unlessCNA_USE_SYSTEM_DRACO) and optional zstd for.cnbchunks (CNA_CNB_ZSTD,AUTO). Both reachcna_contentas compile definitions and private links. - Renderer families. Family targets are created after the configure-time identity, platform and dependency checks; the generated registry source is compiled into
cna_graphics_coreso that static descriptors are reachable. A build can contain several identities but exactly one default identity macro oncna_build_config; every other family's identity macro is private to its own target. Three helper directories are entered from conditions that test the default identity rather than list membership:common/d3d(default DIRECTX11 or DIRECTX12),softwarefor GDI (default GDI) and the Metal family target (default METAL). By reading, a multi-renderer set whose non-default member needs one of them would not have that helper target defined by those lines; none of CNA's multi-renderer CI sets contains such a pair, and no configure was run for this page. - The shared runtime. On native ELF with GNU or Clang and CMake 3.27 or newer,
CNA_SHARED_LIBRARYdefaults to ON.SharedRuntimeLibrary.cmakethen definescna_shared(aliasCNA::Shared, output namecna, i.e.libcna.so) from every CNA-owned static library in the umbrella's closure with WHOLE_ARCHIVE, adds every compiled-in renderer target (not only the default) to that closure, and re-points theCNAumbrella at the shared library plus compile-only usage requirements. The walk runs in a deferred call (cmake_language(DEFER)) at the end of the top-level file, when every target has its final link libraries.
These are conditional graph branches. Do not count directories as proof that a piece participated in a particular binary.
How to use this graph during maintenance
- For a changed file, locate its physical module and that module's
CMakeLists.txt. Decide whether the changed header is public, underCNA/Internalor privatesrc/. - Follow the direct PUBLIC and PRIVATE target links and the umbrella. PRIVATE static links can still appear on a final executable's link line. Ask whether an optional gate (
CNA_ENABLE_NET, FFmpeg,CNA_BUILD_C_API,CNA_BUILD_INSPECTOR) excludes the module. - For a linker error, inspect the final link line and the intentional reverse edges before changing C++ includes. A static-archive cycle may require repetition, and deleting an edge can pass object compilation but fail the final link.
- For a runtime bug, switch to the ownership map. A link dependency does not imply object ownership.
- For tests, inspect the source filtering in
cmake/UnitTests.cmake(dozens oflist(FILTER ...)exclusions keyed on the configuration) and the configuredctest -N; not every module's tests enter every build.
The CMake architecture page traces configure order, selectors and the generated registry. Read the root CMakeLists.txt for option declarations and the order in which the selection files run, modules/CMakeLists.txt for umbrella and partition policy, and a module's own CMake file for its exact edges. This map is a maintainer's starting model, not a generated transitive adjacency matrix; that depends on configuration, toolchain and optional targets.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- CNAEXT catalogue: extension surfaces by namespace — A catalogue of CNA's non-XNA surface at snapshot 009d40f5, namespace by namespace, with member names, reasons and boundaries, how much the CNAEXT marker covers, and what the strict check proves.
- Framework services and ecosystem quick reference — A map of CNA's input, audio, media, device, network, gamer-services, storage and sharp-runtime types with their current boundaries and owner pages, plus the bindings and showcase applications around CNA.
- From C# to C++: CNA's translation conventions — How CNA represents C# XNA concepts in C++ so that code stays diffable against the reference: names, properties, aliases, events, interfaces, disposal, visibility, layout and the porting checklist.
- Module boundaries: cycles, umbrellas and the gates that enforce them — Why CNA keeps three static-archive cycles, what the CNA, CnaExt and BuildConfig compositions carry, what each boundary gate and the fifteen module probes check, when they run and what a violation looks like.
- sharp-runtime components and how CNA consumes them — How sharp-runtime is cut into CMake components, how an include maps to a component and link closure, and how CNA 009d40f5 selects, links and instruments the components it needs.
- The physical module architecture: ownership, targets and composition — How CNA's 22 framework modules and 21 renderer families own their targets, include roots, tests and examples, which target names cannot be guessed, and how to read a source path at snapshot 009d40f5.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-189: The GDI renderer's configure message hard-codes '3 GDI-owned units' while modules/renderers/gdi/src compiles four — modules/renderers/gdi/CMakeLists.txt reports the GDI archive as the shared CPU-2D source count plus a literal 3, but cna_add_renderer globs four GDI translation units, so the reported total is one short.
- CNA-BUG-190: modules/renderers/CMakeLists.txt enters common/d3d, the GDI software sources and the Metal renderer target only when the default identity matches, so a multi-renderer set that lists DIRECTX11, DIRECTX12, GDI or METAL without the matching default passes the combination rules and then cannot build — Three helper directories are entered by testing the default identity instead of set membership, yet the combination rules accept sets such as HEADLESS;DIRECTX11 (or DIRECTX9;DIRECTX11), whose DIRECTX11 family then links
- CNA-BUG-230: cna_define_shared_runtime_deferred's comment says cna_graphics_ext and cna_devices_ext exist only with CNA_CNAEXT; modules/CMakeLists.txt always creates both — The shared-library composition keeps an if(TARGET) filter justified by a comment that the extension targets are optional, but both are defined unconditionally, so the comment misstates the module model.
- CNA-BUG-232: docs/physical-modules.md has no row for modules/platform, omits the platform and diagnostics edges of runtime, input, audio and devices-ext, and says top-level examples/ holds only examples/golden — CNA's physical-module map, which check_renderer_identities.py keeps under count control, never mentions the platform module that input, runtime, audio and devices-ext link, and misdescribes the top-level examples directo
- CNA-VGAP-013: No gate checks include reachability or compiles each public C++ header alone; the two scripts that did are stale campaign tools wired to nothing — check_include_reachability.py and check_header_self_containment.py hard-code the August 2026 module graph and are called by no CMake file, test or workflow, and no current gate compiles each framework public C++ header i
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- Architecture: module layout · Building: build layout
- Architecture
- Architecture overview · Graphics architecture
- Maintainer workflow
- CMake architecture · Source ownership · Change build configuration · Ownership and lifetime master map
- Tests and validation
- Test architecture
- Reference
- Module index · CMake option index