CMake architecture

CNA snapshot 009d40f5  ·  Development › Build System  ·  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. Read from the CMake files, presets and workflows; no configure, build or test was run. Two points were read from third-party behaviour and not observed: the GoogleTest module's PRE_TEST placeholder, and whether a CI runner's cached SDL directory name equals a Wayland-capable tree's actual prebuilt root.

The root CMakeLists.txt chooses a toolchain and feature configuration, resolves three independent backend axes, prepares dependencies and only then enters modules/. Physical modules declare static targets with their own include roots; the public CNA target is an interface composition, not the place where every source file compiles. Generated renderer registration, intentional static-archive cycles, a persistent SDL prebuilt with a build manifest and a set of configure-time gates make the graph subtler than a flat list of options. This page traces it from the top of the root file to the last link, for a maintainer who changes selection, targets, dependencies or test registration.

ℹ

Everything here was checked by reading the CMake files, presets and workflows at snapshot 009d40f5; no configure, build or test was run for the page. The user-level companion is Building CNA (options, presets and troubleshooting); the renderer half of the sequence is traced further in Renderer selection internals, the platform half in Platform backends, and the command entry point is Build system.

Root configure order

CMakeLists.txt
  project, version identity          cmake/Version.cmake renders CNA/Version.hpp
  C++23, extensions off
  Emscripten pthread ABI             decided before any target exists
  CNA_SHARED_LIBRARY decision        decided before any target: archives inside libcna.so need PIC
  CNA_BUILD_C_API                    enable_language(C), C17, PIC
  → BuildPerformance.cmake           linker choice; sanitizer, debug-info, IPO and unity option surface
  ccache launcher                    CNA_USE_CCACHE, CNA_CCACHE_BASEDIR
  root options                       tests, examples, benchmarks, inspector, diagnostics, CNAEXT, devices, net, video
  → TestDisplayPolicy.cmake          which DISPLAY window-creating tests use
  → RendererIdentities.cmake         validate public names; refuse unknown names (selector, set) and retired names (every route)
  → ApplePlatform.cmake              sysroot, architecture, deployment target (before SDL sub-builds)
  → PlatformSelection.cmake          window/event/input host, X11 and Wayland detection
  → ThirdPartySDL2.cmake             only for CNA_PLATFORM=SDL2
  → AudioPlatformSelection.cmake, Sdl2OnlyConfiguration.cmake
  → SdlAvailability.cmake            is SDL needed at all?
  → ThirdPartySDL.cmake              cna_configure_vendored_sdl() when CNA_ENABLE_SDL
  → TestHelpers.cmake, WindowsGuiExecutable.cmake, ThirdPartyENet.cmake (with CNA_ENABLE_NET)
  → instrumentation and debug-info    CNA_SANITIZE, CNA_DEBUG_INFO
  → SharpRuntimeConsumption.cmake     component closure, then add_subdirectory(sharp-runtime)
  → RendererSelection.cmake           default + compiled set, per-identity gates and dependencies
  → RendererRuntime.cmake, RendererRegistry.cmake (generate the registry), RendererDescriptorGate.cmake
  → PlatformRatchet.cmake, PlatformHotPathLint.cmake     configure-time source audits
  → enable_testing()                  when CNA_BUILD_TESTS
  → add_subdirectory(modules)         physical libraries, family examples and module tests
  → Harnesses.cmake, XnaPipelineParityGates.cmake, Tool*.cmake (converters, cna-content)
  → UnitTests.cmake                   object groups, CnaTests, discovery, platform/audio/glTF routes
  → AppleSmoke.cmake, Tests/ModuleProbes.cmake
  → Apple bundle sweep, then private build policy attached to every CNA-owned target

Ordering is functional. Platform, audio and renderer selection precede the vendored SDL setup because a headless, NULL-audio, native-renderer configuration can be truly SDL-free; the identity check runs before any of them read CNA_GRAPHICS_RENDERER, so a refused name fails before a single dependency is configured. Apple resolution runs first among them because the SDL sub-builds are separate CMake invocations that inherit nothing from this cache. Renderer selection happens after sharp-runtime shape detection but before the modules exist, so the selected family targets and the generated registry are known when cna_graphics_core is declared. Harness, test and probe registration comes after module composition so those files can name the final target graph, and the private compiler and linker policy is attached last, to every target CNA owns (targets whose source directory is under third_party/ or vendor/ are skipped). The root's C++ standard is 23; enabling the C API additionally enables C17 and position-independent code before the CNA static modules are created, because the exported shared C API links those archives.

Several of the selection files also run under cmake -P in script mode (identities, audio selection, default-set resolution, the SDL2-only guard). That is deliberate: the CTest entries listed under Configuration and policy tests that need no GPU exercise the real decision code in milliseconds instead of configuring a whole project per case.

Three independent selectors, not one "backend" knob

CNA_PLATFORM selects windowing, events, input and host services; CNA_AUDIO_PLATFORM selects playback and capture; CNA_GRAPHICS_RENDERER selects the default graphics identity, and CNA_GRAPHICS_RENDERERS optionally compiles more identities into the same binary. A headless platform can still select SDL3 audio; an SDL3 windowed application can select NULL audio. Invalid combinations fail at configure time rather than silently substituting another identity, and CNA_ENABLE_SDL is a switch layered over all three. The user guides count four axes (the operating system is the fourth); the selection axes index lists every value.

Platform

cmake/PlatformSelection.cmake builds the list of available implementations: SDL3, SDL2 and HEADLESS always; WIN32 when the target is Windows (a MinGW cross-build from Linux counts, because it targets Windows); TERMINAL on every other target (it is built on termios); X11 and WAYLAND only when their development environment is detected (cmake/PlatformX11.cmake, cmake/PlatformWayland.cmake). Asking for X11 or WAYLAND where the packages are missing is a configure error that names what to install, and CNA never falls back to SDL3 or to X11 through Xwayland. SDL12 and EMSCRIPTEN are reserved names that fail rather than alias; the host-conditional pair (WIN32 off Windows, TERMINAL on Windows) is reserved on the hosts it cannot build for, so the refusal reads as a refusal and not as a typo. The chosen value becomes CNA_PLATFORM_<NAME> on the whole project; WAYLAND also enables C for the scanner-generated bindings. Headless is compiled into every build and Terminal into every POSIX build whatever CNA_PLATFORM says, so that one process can hold several implementations to one conformance suite.

Audio

cmake/AudioPlatformSelection.cmake accepts SDL3, SDL2, NULL and ALSA; OPENAL and WASAPI are reserved and refused; ALSA needs a Linux target (it loads libasound at run time and mixes with CNA's own mixer). The value becomes CNA_AUDIO_PLATFORM_<NAME>, and cna_build_config defines SOUND_ENABLED only for SDL3 and ALSA, the two values with a mixer behind the XNA facade. cmake/Sdl2OnlyConfiguration.cmake then refuses the SDL2 and SDL3 pairings that would put both libraries in one process and, for an SDL2 platform with SDL2 audio, refuses the renderers that link SDL3 directly (SDL_RENDERER, SDL_GPU, FNA3D, FREEDIRECT); it also publishes CNA_SDL2_ONLY_CONFIGURATION, which later removes SDL3-dependent tests and harnesses from that tree.

The SDL switch

cmake/SdlAvailability.cmake normalises CNA_ENABLE_SDL (AUTO, ON, OFF) and computes why SDL would be needed: an SDL platform, an SDL audio value, or an effective renderer (or member of the plural list) among SDL_RENDERER, SDL_GPU, FNA3D, FREEDIRECT. It resolves the effective renderer through cmake/RendererIdentityDefault.cmake, the same per-host default rule the renderer selection uses, so the two cannot drift. OFF with any reason present is a configure error that lists the reasons; with no reason it forces the value, sets CMAKE_DISABLE_FIND_PACKAGE_<X> for the SDL3 and SDL2 package families so an optional lookup anywhere in the tree (a sibling's example directory included) finds nothing, and skips the SDL sub-build. AUTO and ON both configure SDL exactly as before, which is why a display-free selection still builds SDL unless you say OFF.

Renderer

The renderer side has its own identity validator (cmake/RendererIdentities.cmake: the 25 public names, and by-name refusal of any other on all three routes), a default-set resolver (cmake/RendererDefaultSelection.cmake), combination rules (cmake/RendererCombinations.cmake) and a per-identity configuration macro in cmake/RendererSelection.cmake. The macro sets the family directory, the target, the private family definitions and the dependency for each compiled-in identity; only the default identity's macros are applied project-wide. The whole path, down to GraphicsDevice::resolveRenderer, is on Renderer selection internals.

Physical modules and public build facts

modules/CMakeLists.txt defines cna_add_module(target alias sources...), which creates a static cna_* library and CNA::Alias, links cna_build_config and publishes the module's own include/ as its PUBLIC include root. It then enters the modules in a human-readable dependency order, but target references are resolved at generate time, so declaration order is not an architectural dependency mechanism. video-ffmpeg, gamer-services, net and c-api are entered conditionally; the content pipeline has its own target boundary so build-time import and processing code never enters a game's link closure. The per-module table is in the repository map.

cna_build_config is a public interface target for header and ABI facts, deliberately not a bag of include directories. It carries C++23, XNA5, the default renderer's identity macro, CNA_CNAEXT and CNA_DEVICES when enabled, CNA_DRACO_AVAILABLE, CNA_VIDEO_AVAILABLE and CNA_FFMPEG_AVAILABLE when FFmpeg was found, CNA_DIAGNOSTICS_LEVEL, SOUND_ENABLED for the mixer-backed audio values, and under Emscripten the exception ABI and stack size (plus the pthread flags with CNA_ENABLE_EMSCRIPTEN_THREADS). cna_project_options carries CNA-owned private compiler policy such as MSVC /utf-8 and /bigobj and NOMINMAX; it is linked privately to every owned target after all of them exist, so it cannot become an installed consumer requirement. cna_instrumentation, cna_debug_info_options and cna_linker_options arrive the same way (cmake/BuildPerformance.cmake).

Each module exposes its own public include root, and cross-module internal contracts live in include/CNA/Internal, not in a neighbouring module's src/. There are two deliberate exceptions to that rule, both private: every renderer target adds modules/platform/src so the SDL-backed families on the platform audit's allow-list can reach one interop header, and the unit-test build adds modules/audio/src (and the shared test-support roots) so an implementation test can see private code. The source-partition gate checks every translation unit's physical module location and rejects revival of a root src/ or include/ tree, so moving a file between directories is a build-ownership change, not merely a path rename. Sources are collected with file(GLOB … CONFIGURE_DEPENDS), so adding a file to a module's src/ does not need a CMake edit; the exceptions are c-api, whose sources are listed by hand, and the renderer families, whose cna_add_renderer() globs only src/*.cpp without recursion.

The CNA umbrella and link closure

CNA is an INTERFACE target composing runtime, devices, devices-ext, graphics-ext, the cna_cnaext compatibility umbrella, storage, content, media, audio, input, graphics core, diagnostics, core, math, platform, the build configuration and the selected renderer. CNA::Design, phone, gamer-services, net and c-api are deliberately outside it; CNA::Design is opt-in for tooling and converter registration. Because CNA has no sources, cmake --build <dir> --target CNA is not a target: build everything, or name a real one. With CNA_SHARED_LIBRARY=OFF, executables receive the module archives through this umbrella.

One libcna.so

On eligible native ELF GNU or Clang builds with CMake 3.27 or newer, CNA_SHARED_LIBRARY defaults ON and the runtime is linked once. That decision is made before any target so the static ingredients are position-independent, and CMAKE_LINK_DEPENDS_NO_SHARED stops a rebuilt libcna.so from relinking hundreds of test executables. cmake/SharedRuntimeLibrary.cmake runs deferred, after every target has its final link libraries: it walks the closure of the umbrella's parts, links every CNA-owned static archive into cna_shared (alias CNA::Shared, output name cna) with WHOLE_ARCHIVE (a shared library has no main(), so nothing would otherwise be pulled in), and gives consumers libcna.so, the compile-only usage requirements of the archives, and the external libraries the archives depend on that are not inside it (Vulkan, SDL, X11, sharp-runtime and so on). In this mode every compiled-in renderer family is named explicitly, because tests call renderer internals directly and the library has to export them. Keeping the archives off consumers' link lines is what stops a one-line module edit from relinking every executable and keeps a symbol from being defined twice, which would run a static object's initialiser twice. Windows, macOS, Android and Emscripten keep the static link; asking for the shared layout there is a configure error.

Why the link closure is broad

The graphics core constructs a renderer through a descriptor factory that lives in a renderer archive, while renderer code calls back into graphics, core and math symbols. Graphics also updates input touch, mouse and text state, while input's MouseCursor uses Texture2D. Audio and media are a third pair: FrameworkDispatcher pumps MediaPlayer, which plays through the mixer. These are real static-library cycles, expressed with reverse target_link_libraries edges so CMake repeats archives on the final link line. cna_graphics_core and cna_input both raise LINK_INTERFACE_MULTIPLICITY to three, because the shared Direct3D helper adds a third archive that reaches back into the graphics core and two repetitions would leave GraphicsDevice.cpp without libcna_input.a after it. Removing a seemingly redundant reverse edge can pass a small compile and fail only at the final executable link; treat the edges as an intentional build invariant.

Worked example: add one renderer implementation

This is the checklist a new family walks. It is a code-reading recipe, not a record of having done it; every step names the file that owns it.

  1. Identity. Add the public name to CNA_RENDERER_PUBLIC_IDENTITIES in cmake/RendererIdentities.cmake and keep the C++ enumeration (modules/core/include/CNA/GraphicsRendererType.hpp), the C ABI table (modules/c-api/include/CNA/C/graphics.h) and the canonical table in scripts/check_renderer_identities.py aligned. A previously used C ABI value is never reused.
  2. Selection. Add a CNA_RENDERER_<NAME> option and an arm in cna_configure_renderer_identity in cmake/RendererSelection.cmake: the family directory, the target, the private family definitions (appended to _cna_identity_defines, never applied with add_compile_definitions from inside the arm, or the identity's macro would leak to every renderer), host and dependency gates. A family serving several identities is entered once.
  3. Combinations. Add any pairwise rule and platform partition to cmake/RendererCombinations.cmake, and keep scripts/check_renderer_combinations.py in step with the documentation it checks.
  4. Family directory. In modules/renderers/<family>/CMakeLists.txt call cna_add_renderer() from modules/renderers/CMakeLists.txt. It derives cna_renderer_<family> from the directory name (- becomes _), globs the family's src/, adds its include root, links the common contract and sharp-runtime, and declares the reverse edges to cna_graphics_core, cna_core and cna_math.
  5. Descriptor and registry. Implement a descriptor and factory in a file named <something>RendererDescriptor.cpp whose namespace CNA::Internal::Renderers::<Namespace> line stands alone, then add the identity to the map in cmake/RendererRegistry.cmake (an identity with no row is a configure error naming the three places to register). The generator writes generated/CnaRendererRegistry.generated.cpp into the build tree and modules/graphics/CMakeLists.txt compiles it into cna_graphics_core; the explicit table is what stops the linker discarding an unreferenced self-registration unit.
  6. Descriptor gate. cmake/RendererDescriptorGate.cmake finds the descriptor file by that namespace line; a registered identity whose namespace no descriptor declares is a configure error. Families whose descriptors query real SDK devices (Direct3D 9, 11 and 12, and Vulkan where no headers exist) are compiled only by their own target.
  7. Truthful capabilities, tests and CI. Implement capability reporting honestly, add focused tests and example programs (test registration for a compiled-in family gets CNA_RENDERER_PRESENT_<IDENTITY> and its include roots through cmake/UnitTests.cmake), and add host CI. Build a single-identity configuration, then a multi-renderer set with a different default; inspect the generated table and the final link line. Registration alone does not prove drawing works.

Some families carry special setup, and copying a neighbour's CMake file without knowing which relationship applies is a common mistake: EasyGL supplies five public identities from one physical target; Metal publishes host-portable policy headers through an unconditional header-interface target even when it is not the active renderer; GDI compiles eight software-module 2D units; the Direct3D 11 and 12 families share common/d3d; the compiled-effects families pull common/mojoshader and a fetched MojoShader; and the SDL-backed families reach the platform module's interop header through the private include root described above.

Third-party dependencies: submodules, siblings, fetched sources and patches

Dependencies enter through four mechanisms (the boundary table is in the repository map). The rules that live in CMake are below.

DependencyWhere configuredPin and override
SDL3, SDL3_image, SDL3_mixercmake/ThirdPartySDL.cmake; built at configure time into a persistent prebuilt rootSubmodule commits; CNA_USE_SYSTEM_SDL=ON uses system packages instead. See the SDL prebuilt.
SDL2cmake/ThirdPartySDL2.cmake; only for CNA_PLATFORM=SDL2A pinned commit of 2.30.11, or a local tree through CNA_SDL2_ROOT.
FNA3D and MojoShadercmake/ThirdPartyFNA3D.cmake: cna_configure_mojoshader() for every compiled-effects option, cna_configure_fna3d() for the rendererPinned by CNA_FNA3D_GIT_TAG; offline with -DFETCHCONTENT_SOURCE_DIR_FNA3D=<path> (its MojoShader submodule initialised). A series of 117 local patch files against one MojoShader revision is applied by cmake/patches/apply-fna3d-mojoshader-patch.cmake as a whole, idempotently, with a stamp naming the exact series.
wgpu-nativecmake/ThirdPartyWebGPU.cmake, cmake/WebGPUChecksum.cmakeCNA_WEBGPU_VERSION; downloads verify a pinned SHA-256 before extraction and fail closed; offline with CNA_WEBGPU_ROOT and CNA_WEBGPU_AUTO_DOWNLOAD=OFF. Under Emscripten the browser supplies WebGPU through the emdawnwebgpu port and nothing is downloaded.
PortableGLcmake/ThirdPartyPortableGL.cmake; a single header fetched at configure timeA pinned commit (tag 0.100.0); offline with -DFETCHCONTENT_SOURCE_DIR_PORTABLEGL=<path>.
SDL_shadercross and SPIRV-Crosscmake/ThirdPartySDLShaderCross.cmake; for the SDL_GPU renderer's portable SPIR-V stock shaders, default ON on Windows and Apple, OFF elsewherePinned revisions; only SPIRV-Cross's C API is built, as static libraries, and one small patch is carried.
ENet, Draco, googletestcmake/ThirdPartyENet.cmake, modules/CMakeLists.txt, cmake/UnitTests.cmakeIn-tree, submodule (or a system package with CNA_USE_SYSTEM_DRACO), submodule.
sharp-runtime, easy-gl, free-directCMakeLists.txt, cmake/SharpRuntimeConsumption.cmake, the renderer selectionSibling checkouts; CNA_SHARP_RUNTIME_ROOT relocates sharp-runtime. CNA requests a fixed component closure from a modular sharp-runtime and omits Xml.Serialization on Windows targets.

The persistent SDL prebuilt and its build manifest

SDL3, SDL3_image and SDL3_mixer are not built by the CNA build proper. cmake/ThirdPartySDL.cmake configures, builds and installs them with execute_process at configure time into CNA_SDL_PREBUILT_ROOT, a directory outside every build tree, so deleting a build directory or running --clean-first does not touch it. The install is reused across build trees and build types for exactly as long as its build manifest still matches. That last condition is recent: cmake/SdlPrebuiltFingerprint.cmake replaced a bare "is the library there?" test, because a vendored source that moved, or a patch that changed, while the old library sat in the shared root would leave every test linking an SDL that the tree no longer described.

Where it lives

The default root is <source>/.sdl-prebuilt-<System>-<arch> (git-ignored). The key gains suffixes when the artefact is not interchangeable: -wayland on native Linux when SDL's Wayland prerequisites are present (pkg-config knows wayland-client, wayland-egl, wayland-cursor, egl and xkbcommon, and wayland-scanner exists), -simulator for the iOS simulator, -min<version> for the Apple deployment target; Emscripten uses .sdl-prebuilt-emscripten or -emscripten-pthreads. An older cache value equal to the previous generated default is migrated to the new key; an explicit user-chosen root is never redirected. SDL3 is built shared on Linux, macOS, Windows and Android and static on Emscripten and iOS, with the optional codec back ends of SDL_image and SDL_mixer switched off, and installed under <root>/install.

The manifest and the stamp

For each of the three dependencies cna_sdl_build_manifest writes a text record of everything that decides the library's content: the schema tag cna-sdl-prebuilt-manifest-v1, the dependency name, a SHA-256 over the sorted list of (relative path, file SHA-256) of the vendored source tree (.git excluded, so an edited or half-updated working tree is caught where a revision string would not be), one line per patch in application order with its file name and content hash, and every CMake argument the sub-build is configured with. The compiler launcher is left out on purpose: ccache changes how long a build takes, never what it produces. The record is stored next to the install as <root>/<NAME>.cna-build-manifest.txt.

cna_sdl_prebuilt_is_current answers "reuse?" only when the library file exists and the stamp is byte-for-byte the manifest computed now. A missing library, a missing stamp (every install made before manifests existed is rebuilt once, because nothing records what it was built from) or a different manifest each produce a rebuild with the reason in the log: CNA: (re)building <NAME> because … (build manifest <16 hex>). Reordering, renaming, editing, adding or removing a patch changes the manifest; so does a changed vendored file. On rebuild the stamp is deleted first and written only after a successful install, so an interrupted build is retried by the next configure instead of being trusted, and the old build directory is removed so nothing compiled from other sources leaks in.

Patched staging and the two SDL patches

When a dependency has patches the sources are staged: cna_sdl_stage_patched_source copies the vendored tree (without .git) to a source directory beside the build directory (<root>/SDL/source for SDL3, the only patched dependency) and applies each patch there with git apply, under GIT_CEILING_DIRECTORIES so git does not resolve the patch against the enclosing CNA repository. Because git apply inside an unrelated repository can exit 0 having changed nothing, success is proven by the patch now applying in reverse. A patch that does not apply removes the staged tree and stops the configure, naming the patch and saying the series is pinned to one vendored revision. The vendored third_party/SDL is never modified, so a checkout stays pristine and every build tree reads the same bytes.

Two patches are carried on the SDL3 revision cbe3fbe9, in cmake/patches/, each the upstream commit verbatim (git format-patch output named after the commit) so it can be dropped the day the pin moves past it:

The list is a normal CMake variable, CNA_SDL3_PATCHES, not a cache option. SDL3_image and SDL3_mixer have manifests of their own (source, arguments) and no patches, and a manifest does not include another dependency's output.

Concurrency, drivers and forcing a rebuild

  • One lock. Every build tree of the checkout shares the root, so two configures can arrive together; file(LOCK … .cna-prebuilt.lock GUARD FUNCTION TIMEOUT 7200) makes the second wait and then find the first one's result current.
  • Compiler cache. The parent's compiler launcher (ccache) is forwarded to the SDL sub-builds, so the shared compile cache serves them too.
  • Video drivers. After the install, the generated SDL_build_config.h is read and CNA: vendored SDL3 video drivers: … is printed. A generated -wayland root whose SDL lacks the Wayland driver is a configure error; an explicit or custom root without it only gets guidance (a warning inside a Wayland session). On the default root, installing the Wayland packages later changes the root's key (a new -wayland directory is built), but for an explicit or already-existing root it changes neither the manifest nor the install, so rebuilding for a native Wayland driver there means deleting the root printed by CMake.
  • Forcing. Delete <root> (the exact path is in the CMake output) to force a full rebuild regardless of the manifest.
  • Cache keys in CI. The workflows cache .sdl-prebuilt-Linux-x86_64 keyed by the SDL submodule commit (or a hash of the three SDL sources), not by the patch files; whether a restored directory is reused is still decided by the manifest at configure. Because a Wayland-capable host's default root carries the -wayland suffix, whether a given runner's cached directory name equals its tree's actual root was not observed here.

The behaviour is held by the pure-CMake CTest CnaSdlPrebuiltFingerprint (cmake/Tests/SdlPrebuiltFingerprint.cmake), which drives the same functions on a small fixture instead of SDL: a legacy install with no stamp is rebuilt; unchanged inputs reuse the install; a recomputed manifest is identical; an edited, added, reordered or dropped patch, a changed or added vendored file, changed configure arguments and a missing library each force a rebuild; .git metadata does not count; staging patches a copy and leaves the vendored tree pristine; and a patch that does not apply is refused and leaves nothing staged. It was not executed for this page.

Gates that run at configure time

The configure step is also a validation step. Each gate names its rule in the failure; most can be switched off for diagnosis, and none should be off when you commit.

GateWhereControlWhat it enforces
Identity, platform, audio, SDL and combination refusalsselection files abovenoneNamed refusals instead of silent fallbacks.
Platform ratchetcmake/PlatformRatchet.cmake running tools/platform/sdl_ratchet.pyCNA_PLATFORM_RATCHET (ON), CNA_PLATFORM_RATCHET_STRICT (ON)Direct SDL coupling outside the platform module stays within a budget; strict mode makes an excess a hard error. Skipped without Python. On-demand target cna_platform_ratchet.
Non-production SDL auditsame file, tools/platform/nonproduction_sdl_audit.pyruns with the ratchetJustified SDL use in tests and examples is budgeted per file (nonproduction_sdl_budget.json); a new example that touches SDL needs an entry. On-demand target cna_platform_nonproduction_sdl_audit.
Hot-path lintcmake/PlatformHotPathLint.cmakeCNA_PLATFORM_HOT_PATH_LINT (ON)No platform call inside a per-pixel, per-vertex, per-fragment, per-sample or per-event loop; a hard error, with a reasoned annotation as the only exemption. Target cna_platform_hot_path_lint.
Configure audit cachecmake/ConfigureAuditCache.cmakeCNA_CONFIGURE_AUDIT_CACHE (ON)Successful audits are remembered by a content fingerprint of their inputs, stored under CMakeFiles/, so an unchanged tree does not re-run them.
Renderer descriptor gatecmake/RendererDescriptorGate.cmakeCNA_BUILD_RENDERER_DESCRIPTOR_GATE (ON)Every registered family's descriptor is compiled by something in every configuration; an OBJECT library that nothing links.
Source-partition validatormodules/CMakeLists.txtnoneA production translation unit outside every declared module tree, or a legacy root src/ or include/, stops the configure.
Test-display policycmake/TestDisplayPolicy.cmakeCNA_TEST_DISPLAY, CNA_TEST_ALLOW_LIVE_DISPLAYGPU and window tests inherit the caller's display; the live desktop is opt-in; no test falls back to the default Wayland socket. The sweep that applies it needs CMake 3.28 or newer, otherwise a warning says so.

Verified developer configurations and rebuilds

CMakePresets.json has 17 visible configure presets plus a hidden base-ninja parent (Ninja, ccache, compile_commands.json): dev, dev-fast-debug, unit, unit-pch, unit-unity, release-modules, release-ipo, web, devices-asan, devices-tsan, devices-ubsan, macos, ios, ios-simulator, tests, multi-renderer and cnaext. Its 17 build presets are separate: dev, dev-fast-debug, unit (builds CnaTests), unit-core (CnaCoreTests), unit-math, unit-core-math-unity, unit-content, unit-content-pch, unit-graphics (CnaGraphicsTests), release-modules, release-ipo, web, the three devices-* presets, tests and cnaext. Read both halves of the JSON: macos, ios, ios-simulator and multi-renderer have no build preset, and a configure preset existing does not imply a same-named build preset. The preset table with each preset's cache values is on Build system.

# From the CNA repository root, with its toolchain and dependencies available:
cmake --preset unit
cmake --build --preset unit-graphics
./cmake-build-unit/CnaGraphicsTests --gtest_filter='<suite>.*'

The preset names and binary directory match CMakePresets.json; the commands were checked against the pinned preset names and were not executed against CNA. The focused executables are not CTest registrations, so ctest --test-dir cmake-build-unit -N lists the configure-time registrations and, once CnaTests has been built, its discovered cases (discovery runs when CTest reads the test files, DISCOVERY_MODE PRE_TEST); before that, the GoogleTest module registers a not-built placeholder instead (read from the module's PRE_TEST behaviour; not observed for this page).

For a .cpp edit, CMake's dependency graph recompiles its owning module and relinks dependent targets; for a public header, expect every includer to rebuild. A CMake source or option edit requires a reconfigure. A renderer contract edit requires every selected family to compile, not merely the default; run the descriptor and conformance gates afterwards. A public build-config macro may change every consumer's ABI and force a broad rebuild. A checked-in generated header such as Vulkan's embedded SPIR-V must be regenerated from its owning script and the affected targets rebuilt. On shared ELF builds a renderer edit rebuilds libcna.so but should not relink every executable; on static builds it may relink many. That distinction comes from the root shared-library policy, not from an IDE preference.

What configure/build success does not prove

cmake/UnitTests.cmake, module-local tests/ and renderer-family examples/CMakeLists.txt create different forms of tests. The generated test target index lists 239 syntactic declarations; that is not the configured CTest set. A platform, renderer or test option can remove or add registrations, so use ctest -N on the specific build tree. The renderer descriptor gate compiles non-selected family descriptors to catch bit rot, but it cannot run their native drawing paths. The module probes check narrow link contracts (fifteen tiny consumers with forbidden-library patterns); they do not replace feature tests. For a backend selector change, test configure-time refusal, single selection and multi selection, generated registry identity, the final link and a runtime smoke on a capable host. The recently added configure-level tests (script-mode selection cases, the SDL prebuilt fingerprint, the test-display policy) and how the test targets are assembled are described on Test architecture and change recipes.

ℹ

Recently added build mechanisms at this snapshot. The SDL prebuilt build manifest with the two patched SDL_gpu barriers and the CnaSdlPrebuiltFingerprint test; CNA_OPENGL4_COMPILED_EFFECTS (OpenGL4 compiled effects through MojoShader's GLSL 1.20 dialect, shaped like the EasyGL option); MojoShader compiled on the C standard library, without its SDL_gpu adapter, when CNA_ENABLE_SDL=OFF (FNA3D itself still needs SDL and is refused by name in that configuration); the MojoShader effect probe in cmake/Harnesses.cmake linking SDL3 only when that target exists, and the SDL_gpu and OpenGL probes built only where it does; an OpenGL4 GL-error output gate beside the Vulkan validation gate in cmake/TestHelpers.cmake; the TextureArrayShaderPackageReproducibility gate; and an EasyGL test-suite filter that now lists only the five EasyGL identities, because OPENGL4 is its own family without the EasyGL include root.

Source reading order

  1. CMakeLists.txt: follow the ordered includes through add_subdirectory(modules) and the late tools and tests.
  2. cmake/PlatformSelection.cmake, cmake/AudioPlatformSelection.cmake, cmake/Sdl2OnlyConfiguration.cmake and cmake/SdlAvailability.cmake: separate the axes and the optional-dependency gate.
  3. cmake/RendererSelection.cmake and cmake/RendererRegistry.cmake: map identities to families and generated C++ registration; cmake/RendererDescriptorGate.cmake for the gate.
  4. cmake/ThirdPartySDL.cmake with cmake/SdlPrebuiltFingerprint.cmake: the persistent SDL install, its manifest and the patched staging.
  5. modules/CMakeLists.txt, modules/renderers/CMakeLists.txt and modules/graphics/CMakeLists.txt: target creation, the umbrella, the cycles and the selected-family linkage; cmake/SharedRuntimeLibrary.cmake for shared mode.
  6. CMakePresets.json: choose an actual local configuration and a named build target.
  7. cmake/UnitTests.cmake, cmake/TestHelpers.cmake, cmake/Harnesses.cmake and cmake/Tests/ModuleProbes.cmake: how tests, harnesses and probes are registered.

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