Working on CNA
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. Workflow names, triggers, scripts, presets and the repository instruction files were read at 009d40f5; a workflow file describes what CI is configured to run, not that a run passed, and nothing was executed for this page.
Each workflow below turns an unfamiliar symptom or idea into a small, reviewable change with explicit cross-platform validation. They are the manual engineering loops behind the recipes in the Maintainer Handbook, tied to what CNA's repository actually contains at snapshot 009d40f5: its instruction files (AGENTS.md and CLAUDE.md), its presets, its gates and its twenty CI workflow files. Names of tests, scripts and workflows were read in the source; none was run for this page.
Fixing a bug
- Classify the failing boundary. Runtime scheduling, platform event or service, shared graphics state, renderer translation, content, audio or ABI. The change map names the owner for each, and the debugging cookbook the probes.
- Reproduce at the narrowest level. Save the exact backend axes and the build: the configure line or preset, the platform, audio and renderer identities, and the failing test name. Prefer a focused executable (the unit presets) or a HEADLESS or
SOFTWAREconfiguration over a windowed one, and confirm the test is registered withctest -N. - Trace from public observation backward. Find the declaration, the implementation, the factory or registry that creates it and the tests, and write the five-line trace (caller, boundary, callee, owner, test) described under Debugging unfamiliar code.
- Add a failing test beside the owning module (
modules/<name>/tests, mirroring the namespace path), or in a conformance suite when several implementations share the promise:PlatformConformanceandPlatformWindowConformancerun once per compiled platform,AudioDeviceConformanceTestsonce per audio device, the cross-renderer contracts in a multi-renderer build, the shared parity fixtures across renderer families, and the XNA oracle corpus for XNA-visible behaviour. Make sure the test can actually reach its assertions: a test that aborts before its first check, or dies on a graphics-profile refusal, reads as red and proves nothing (profile_dead_tests.pynames such failures after a private-display run). CNA's instructions also require tests to land in the same task as the code: a test that existed before an implementation does not count. - Change the smallest owner. Do not fix one backend if the wrong assumption lives in shared code, and do not push a backend quirk into shared code. Where CNA and FNA disagree on behaviour, CNA follows measured XNA, and a divergence taken on those grounds is recorded with a comment naming the XNA IL that settles it and a row in
plan_bindings_upstream.md. - Validate focused, then sanitizer, then the affected matrix. The focused executable or preset; a sanitizer build of the area where lifetime or threading is involved (sanitizer recipe); then the tuples and hosts the change can reach (What to test after changing X). Say which platform, audio and renderer combinations were not exercised.
- Review lifetime and the other backends before declaring success: the invariants and the questions under Review your own change.
Adding a feature
First decide whether it is public CNA behaviour or an implementation capability. A public surface belongs in the owning module and carries API and ABI, documentation, tests, examples and binding consequences: XNA-shaped names and semantics where compatibility is intended, the CNAEXT marker (and the EXT naming) for anything that is not XNA 4.0, a row in the C API mapping, and an example in the module's examples/ where the module has them. A renderer or platform capability needs neutral semantics and truthful support reporting: a capability enumerator or service, an opt-in answer that defaults to false where the shared default would otherwise say true (truthful capabilities), and an explicit refusal where it is unsupported. Inventory every implementation before editing the interface (25 renderer identities, 7 platforms, 4 audio implementations; the generated Selection axes index lists them). Unsupported is better than a silent no-op, and a feature that only one family implements must say so through a capability query, not through its absence from a table.
Two repository rules are easy to miss. New shader-based engine-layer code asks the capability and the matching query (for example ExecutesShaderEffectSourceEXT()). And the CNA_CNAEXT layer's files must all be wrapped in #ifdef CNA_CNAEXT, which the CNAEXT_GuardDiscipline test enforces.
Modifying public API
- Keep XNA-shaped naming and semantics where compatibility is intentional. Exact XNA names in the matching
Microsoft::Xna::Frameworknamespace, C# properties asgetXProperty()andsetXProperty(), SharpRuntime aliases for primitive types; details on Conventions. - Add Doxygen on exported public declarations in the
/** @brief ... */block form; the engine layer has a test for it, the rest is a review rule. - Search uses in samples, tests and downstream modules: the include roots mirror the namespace path, so a rename touches every consumer spelling.
- Assess source compatibility, binary layout, export visibility and the experimental C API. A new public C++ symbol without a C ABI mapping makes the coverage gate stale (
CApiCoverageMatrix), a layout or constant change is measured byCApiAbiHeaderBaselineagainsttools/c-api/abi_baseline.json, headers must still compile in every declared language mode (CApiHeaderCompatibility), and the version policy isABI_VERSIONING.md. The ABI is 0.29.0 at this snapshot and experimental. - Update language bindings only after pinning their compatible native version. The bindings are separate repositories that pin an ABI generation (0.21 at this snapshot), none qualified against 0.29.0: see the bindings table.
- Add compile-level consumer tests plus behaviour tests. The input module has
PublicApiInputCompileTestsandPublicApiInputSignatureFreezeTests; the strict-XNA purity check (StrictXnaApiSurfaceCheck_Compile_Runand its must-fail-to-compile counterpart) covers the Devices surface; the minimal-link probes intests/modulesfix each module's dependency closure.
Working on a renderer
- Select the public renderer identity and find its descriptor and factory.
RendererRegistry.cmakemaps identity to family; the family'sGetDescriptor()andcreatelive undermodules/renderers/<family>/src/(Renderer selection internals). - Trace one known-good frame: create, begin, resource and state commands, present, destroy (Indexed draw trace, then the family's page from Graphics backends).
- Identify the shared graphics contract and the backend-native translation point. The contract is
IGraphicsRendererand its resource interfaces; the translation is inside the family. - Add a conformance case independent of native details: a shared parity fixture (
modules/graphics/examples/parity), a case in the cross-renderer contract suite, or a neutral test inmodules/graphics/tests, so the promise is asserted for every renderer that makes it. - Run through the private display with native validation enabled.
tools/platform/run_gpu_tests_private.sh <build-dir> -R '^Vulkan_' -j6is CNA's own example; it refuses a build tree that forces a display. Validation switches per family are in the debugging cookbook, and Vulkan and OpenGL4 tests fail on their validation-output gates. - Compare another renderer and test resize, minimize and dispose, on a host capable of that native API. CNA's own guidance is to keep WebGPU changes renderer-local unless a common-interface change is genuinely required and verified across the existing renderers.
Before pushing, run the identity, combination, discipline and descriptor checks the multi-renderer workflow runs (check_renderer_identities.py, check_renderer_combinations.py, check_runtime_renderer_discipline.py, check_renderer_target_discipline.py, check_renderer_descriptors.py). No workflow names WEBGPU, SDL_GPU, OPENGL4, FNA3D, PORTABLEGL, FREEDIRECT or DIRECTX9, so for those families the local run is the only evidence. Recipe: Fix a renderer bug.
Working on a platform
- Read
IPlatformand the relevant narrow service contract (IPlatform.hpp,PlatformCapabilities.hpp, the interfaces underCNA/Platform/andCNA/Platform/Input/). New production code uses these and never calls SDL directly outside the declared native edges. - Trace factory selection, service acquisition and refcounting, and the event pump.
PlatformFactory::Createbuilds an implementation by name;AcquireSubsystemandReleaseSubsystemare refcounted (PlatformConformance.SubsystemsAreRefcounted);PollEventsruns once per frame and fills a caller-owned batch. - Map native events to platform-neutral values before input code. A backend alone owns native key codes, pointer coordinates and device state; the input module sees only
PlatformEventvalues and snapshot services. - Keep native handles inside the capability interfaces renderers use (
IPlatformGlContext,IPlatformVulkanSurface,NativeWindowHandle, the surface presenter), and keep a service non-null exactly when its capability is true. - Run the platform source gates, the focused contract tests and native-host CI. The gates CNA's instructions list are
tools/platform/sdl_inventory.py --check,sdl_classify.py --check,renderer_sdl_audit.py --check,sdl_ratchet.py --checkandhot_path_lint.py; the platform workflow adds--strict,nonproduction_sdl_audit.pyandcheck_contract.py. The focused tests are theCnaPlatformTests,CnaPlatformWindowTestsandGameEventSemanticsGoldenTestruns; the native-host jobs are inplatform-ci.yml. The native X11 backend has its own entries (CnaX11MappingTestsand siblings) and the Wayland backend its protocol, portal and compositor entries; none of the Wayland entries runs in a workflow.
Recipe: Modify a platform backend; backend pages start at Platform backends.
Adding tests
Use module tests for isolated behaviour and the cross-module or conformance roots for promises shared by implementations. Reuse deterministic fixtures. Include a negative or destruction case where ownership is involved, and confirm that CTest registers the new test and that the responsible CI workflow runs it. Concretely at this snapshot:
- Where.
modules/<module>/tests/mirrors the namespace path and is globbed into the module's object group (UnitTests.cmake), which feeds bothCnaTestsand the focused executable. Fixture assets and the cross-module probes live under top-leveltests. - Doubles. Consumer tests use canned platform services (for example
CannedKeyboard.hppandCannedMouse.hppunder the platform tests) or the parameterised conformance suite, not native event injection; a genuinely backend-specific test belongs with that backend. - Registration and gating. A test can also be registered explicitly with a label, timeout and environment through
cna_register_renderer_testinTestHelpers.cmake. A test that exits77is skipped, not passed; window tests need a private display; and in configurations that compile Vulkan or OpenGL4 a test fails on validation or GL-error output. - Rules for XNA API tests. Every public method, constructor, operator and constant needs at least one test; out-ref overloads and static factories are tested separately; equality,
ToString()andGetHashCode()have their own cases. - Which workflow runs it. See Which workflow will see your change; a test in a suite that no configuration builds runs nowhere.
Recipe: Add a regression test; architecture of the test targets: Test architecture and change recipes.
Debugging unfamiliar code
# declaration and definition
rg -n "class GraphicsDevice|GraphicsDevice::Present" modules
# creation/registration, not just uses
rg -n "GraphicsDevice|Register|Descriptor|Factory" modules cmake
# tests and build ownership
rg -n "Present|GraphicsDevice" modules/*/tests tests
rg -n "GraphicsDevice" --glob 'CMakeLists.txt' --glob '*.cmake'
Run them at the revision you are documenting: with plain git, git grep -n "PlatformInputBridge::ProcessEvent" 009d40f5dd085c4e674d3479675fac84b12b3e0a -- modules searches the snapshot regardless of what your working tree has checked out. Write a five-line trace in your notes (caller, boundary, callee, owner, test) before editing, which prevents local code familiarity from replacing system understanding. A filled-in example for a present call:
| Line | Example: why does Present throw? |
|---|---|
| Caller | GraphicsDeviceManager::EndDraw in GraphicsDeviceManager.cpp (or game code calling GraphicsDevice::Present directly). |
| Boundary | The public GraphicsDevice::Present, which checks disposal and renderTargetBound_ before anything reaches a renderer. |
| Callee | IGraphicsRenderer::Present of the selected family (not reached when the check fails). |
| Owner | The device layer owns the rule; the family owns only the native present. |
| Test | GraphicsDevicePresentRegionTest.PresentingWithRenderTargetsBoundIsRefused. |
The method behind the trace is in How to understand code you did not write, and the map of what to read is the change map. CNA's own repository instructions are AGENTS.md and CLAUDE.md; they describe how CNA itself is developed and are stated in the next sections, but they can lag the CMake and the source, so verify a rule against the file it names.
Review your own change
- Does the change live in the owner named by the architecture (the row in the change map)?
- Did a claimed backend or capability become dishonest, or a default of
trueget inherited by a renderer that cannot do it? - Can destruction happen with partial initialization or repeated dispose, and does the platform-outlives-dependents rule still hold after any member reorder?
- Did thread affinity or callback constraints change (Thread and callback map)?
- Which focused test failed before and passes now?
- Which platform, renderer, audio and binding was not exercised, and is that explicit in the description? A workflow file is configuration, not a result.
The self-review questions extend this list for large, machine-written patches.
Commits, task records and reports
CNA has no CONTRIBUTING file, pull-request template or code-of-conduct file at this snapshot (the only directory under .github is workflows); the working conventions are in its instruction files and plan ledgers, and the Git history follows them. They are written for whoever changes the code, including automated coding assistants, and they describe how CNA itself is developed, so they are also a fair review standard for a human patch.
- One task, one commit, committed when finished. Create the commit as soon as the task is done (build verified, tests passing, the plan,
AUDIT.mdandNEXT.mdupdated); do not push unless the maintainer asks. Stage only the files that belong to the task, by explicit name: nevergit add -Aorgit add ., because the repository routinely has unrelated local changes. Do not bundle unrelated tasks. - Messages name the task. The form is
<type>(Task <ID>): <what changed and why>, for examplefix(Task STREETS-0008): SDL_GPU saturates the unlit diffuse colour at the vertex, as Direct3D 9 does; a range is writtenfix(Tasks WGF-0001..0007): .... The types in use arefix,feat,test,docs,refactor,perf,build,ci,chore,revert,planandspike, plusmerge(<Campaign>). - Campaigns and task records. Work is organised in campaigns, each with a branch cut from
nextand a plan file in theplansdirectory that acts as its evidence ledger: task IDs with a fixed prefix (for exampleGTI-0001inplan_gpu_test_isolation.md), one row per task, and each row carrying its own evidence, deviations and refusals rather than leaving them in a commit message. A finished campaign is integrated with a merge commit such asmerge(OpenGL4ModernGraphics): integrate opengl4-modern-graphics into next.nextis the integration branch;developis the alpha.1 tag commit. - Running ledgers.
NEXT.mdrecords what changed and the decisions that did not survive contact (dated entries are history, not current support);AUDIT.mdtracks the XNA API audit and an API is not marked complete until its tests are;CHECKLIST.mdis the per-file porting checklist (SPDX header, Doxygen, API surface compared against the XNA reference assemblies and not only FNA, tests). - The task report. State the changed files, added stubs, missing dependencies, intentional deviations, the build result and the remaining errors. Build the affected target (there is no buildable
CNAtarget; build the configuration or a target by its own name). - Environment rules. Never run window or GPU tests on the live desktop; use stable in-repo
cmake-build-<variant>/directories with ccache rather than scratch directories; build parallelism is uncapped and memory is the constraint. These are stated in Getting started.
Which workflow will see your change
CNA has twenty workflow files at this snapshot. Seventeen run on pushes and pull requests to next, develop and main (several only for changes under listed paths); the rest are manual. A workflow is configuration, not a result, and this table records what each is configured to run, not whether it passed.
| Workflow | Runs | What it builds and runs |
|---|---|---|
general-tests-ci | Any change outside Markdown and docs/ | The unfiltered CnaTests suite (ctest --output-on-failure) and every CTest-registered target with OPENGLES3 (the EasyGL family), Debug, tests and examples on, on a persistent Xvfb display, with a step that classifies failures against one known, tracked failure. |
input-ci | Any code change | ctest -L input on OPENGLES3, on OPENGLES3 under ASan and UBSan, on SDL_RENDERER and on VULKAN. |
platform-ci | Any code change | Six graphical and headless platform tuples; native Win32 (mingw under Wine, and MSVC); SDL-free X11 jobs; a CNA_ENABLE_SDL matrix; the platform source gates. |
multi-renderer-ci | Any code change | A HEADLESS;SOFTWARE;STUB runtime-selection build and a single-renderer build, plus the renderer identity, combination, discipline and descriptor scripts. |
gltf-renderer-stride-ci, gltf-sanitizers-ci | Any code change (the second ignores Markdown and docs/) | glTF conformance on STUB, HEADLESS, OPENGLES3, VULKAN and SOFTWARE, and a corpus job against an external viewer checkout; glTF import under ASan and UBSan with and without Draco. |
htmldom-ci, emscripten-multi-renderer-ci | Any code change outside Markdown and docs/ | Browser builds: the HTML_DOM browser suite, and one wasm bundle with four browser renderers. |
devices-tests | Changes under the devices modules, cmake/, the root CMake files | The devices-ubsan preset with the Devices filters. |
apple-ci, metal-macos-ci | Every pull request, and pushes that touch the Apple CMake layer, selection files or listed headers | macOS and iOS builds with portable suites, and the native Metal renderer's build and tests. |
c-api-abi-baseline, c-api-compat-matrix, c-api-coverage-gate, c-api-limitations, c-api-release-gate | Changes to the C headers, the public C++ headers or the C API tooling | Generators and gates over headers, mappings and documents. None builds the C library. |
32bit-arithmetic-ci | Changes to a few named files | An i386 build and run of the audio-tag parser, content exception and software allocation arithmetic checks. |
d3d-windows-ci, gdi-windows-ci | Manual only | Native MSVC builds and tests for DIRECTX11, DIRECTX12 and DIRECT2D, and for GDI. |
content-pipeline-windows-ci | Manual, and pushes to one named branch | A HEADLESS MSVC build of cna-content and the content tests, and a Unicode command-line lifecycle. |
Most Linux workflows clone the sibling repositories with clone_siblings.sh, which takes the first of the pushed, source or target branch that exists in each, then next, then develop. The Apple, Emscripten, devices and content-pipeline workflows instead check out sharp-runtime at a fixed commit, and static reading of those files suggests that some pin a revision older than the snapshot needs, so a red or green run there is not by itself evidence about next. A local run that needs a particular sibling revision must set it up by hand.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-035: devices-tests.yml's exact-name gtest filters skip seven Microsoft::Devices suites and never match the FileDialog and MessageBox suites — DEVICES_GTEST_FILTER misses 29 of 474 TEST definitions under modules/devices/tests, and CNA_DEVICES_GTEST_FILTER names FileDialogTests.* and MessageBoxTests.*, which match no suite, so the 12 FileDialog and MessageBox ca
- CNA-BUG-192: metal-macos-ci.yml's push path filter names three CMake files that no longer exist and omits the files that now register the Metal tests — The Metal workflow's push filter lists three removed CMake files, while edits to modules/renderers/metal/CMakeLists.txt or its examples/CMakeLists.txt, where the Metal_* CTests are registered, do not trigger it.
- CNA-BUG-193: general-tests-ci.yml still allowlists EasyGL_GraphicsDevice_ReferenceStencil as a known failure after REMED-GFX-236 fixed it, so a regression of that test cannot turn the job red — The only unfiltered CI job classifies failures against a one-entry KNOWN_FAILURES list whose entry is the EasyGL ReferenceStencil test, which CNA's own record says now passes; if EasyGL's ReferenceStencil override breaks
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- Contribute · Verification: CI scope · Building CNA: running the tests
- Architecture
- Architecture overview
- Internals
- Renderer selection internals · Platform backends
- Maintainer workflow
- Maintainer Handbook · Change map · Conventions · Blast radius and readiness
- Tests and validation
- What to test after changing X · Test architecture and change recipes
- Reference
- Test target index