I want to change… (change map)

CNA snapshot 009d40f5  ·  Development › Maintainer Handbook  ·  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. Paths, symbols and test names were read at 009d40f5; tests are named because they are registered, and none was executed.

Start at the owning contract, then follow the implementation and its tests. Each row below names the first read, the next hop, the tests to run first and the trap most likely to produce a change that passes locally and is still architecturally wrong. Paths, symbols and test names were read at snapshot 009d40f5; tests are named because they are registered in CMake, not because they were run for this page. A row is a starting point, not a blast-radius analysis: for that, use Blast radius and readiness and What to test after changing X.

ℹ

How to use the map. Find the row whose public observation matches your symptom or idea, not the one that names the file you suspect. Read the owner in the first column of links, write a five-line trace (caller, boundary, callee, owner, test) as the workflow page suggests, then check the trap. If a change touches two rows, treat it as the wider of the two, and read the architectural invariants before editing any lifetime, ordering or capability code.

Runtime, window and input

ChangeStart here, thenTests to run firstTrap
Game loop and timingGame.cpp: Tick, AdvanceElapsedTime, RunLoop; then the timing calls of IPlatform.hpp (GetPerformanceCounter, Delay). Trace: one frame.GameClockFirstUpdateTest.*, GamePlatformTimingTest.*, GameTimeTest.*; platform side PlatformConformance.PerformanceCounterIsMonotonic and DelayAdvancesTheCounter.The clock is XNA's measured one, not FNA's: the first update sees zero ElapsedGameTime and TotalGameTime is the time before the step. Elapsed time is clamped to 500 ms, a fixed step waits with Delay(1) and then yields, and the Emscripten loop is a separate path.
Component lists and component lifetimeGameComponentCollection.cpp, then Game::OnComponentAdded, OnComponentRemoved and the snapshot loops of Update and Draw. Case study: Game component lifetime.GameTest.AComponentRemovedMidFrameIsNotCalledLaterInThatSameFrame, ComponentsAddedFromALoadingThreadSurviveTheFrameThatIsIteratingThem, GameComponentCollectionTest.*.Removal nulls the in-flight snapshot entry instead of erasing it; the collection itself has no lock, and Initialize() of a component added from a loader thread runs on that thread.
Startup, dispose and shutdown orderGame.hpp member order, the Game constructor and destructor, then GraphicsDevice::Dispose(bool). Traces: startup, shutdown.GamePlatformOwnershipTest.*, GameTest.DisposingDeviceInvokesUnloadContent and RepeatedDisposeDoesNotReinvokeUnloadContent, GraphicsDeviceSubsystemLifecycleTest.*.Member declaration order is the guarantee and no test asserts it (invariant). Explicit Game::Dispose(true) does not dispose the Game-owned device; the ambient platform is uninstalled before any member is destroyed.
Window creationIPlatform::CreateWindow with WindowDescription.hpp, then the backend window (Sdl3Window.cpp, X11Window.cpp, WaylandWindow.cpp, Win32Window.cpp), the renderer family's GraphicsRendererDescriptor and GraphicsDevice::createOrAttachWindow.PlatformWindowConformance.* and the CnaPlatformWindowTests CTest entry, GraphicsDevicePlatformWindowTests.*, GraphicsDeviceWindowDescriptionTest.XnaOwnedWindowIsNonResizableFromCreation.The window kind must be right at creation, because the descriptor's kind sets the render intent. A caller window adopted through DeviceWindowHandle is borrowed and never recreated for a fallback renderer. Window-free renderers (HEADLESS, SOFTWARE, STUB, PORTABLEGL) open no window on a windowing platform by design.
Resize, DPI and window stateThe window branch of Game::PollEvents, GraphicsDevice::UpdateViewportFromWindow, IGraphicsRenderer::OnSurfaceChanged and OnSurfaceInvalidated; for a requested size, applyPresentationParametersToWindow. Read the units invariant.The resize and focus cases of GameEventSemanticsGoldenTest, PlatformWindowConformance.SizeChangeLandsAfterSync, Sdl3WindowTest.PixelSizeIsReportedIndependentlyOfLogicalSize, and the backend DPI cases (Win32Dpi*, WaylandProtocol.* scale cases).Logical versus drawable size, and the event's size payload is ignored: Game re-queries the window. Sync() belongs to the requested-size path, not to the resize-event path. Minimized or zero extents need their own handling.
Keyboard and mouseThe public type (Keyboard.cpp, Mouse.cpp); the snapshot services IPlatformKeyboard and IPlatformMouse as implemented per backend (Sdl3InputServices.cpp, Win32InputServices.cpp); the event side SdlInputBridge.cpp (PlatformInputBridge::ProcessEvent; the file name is historical); all driven by Game::PollEvents. Trace: Input internals.KeyboardPlatformInputTest.* and MousePlatformInputTest.* (the public snapshot path, on canned platform services), PlatformInputBridgeKeyboardTest.* and PlatformInputBridgeMouseTest.* (the event side), KeyCodeMatchesXnaKeysTests.*, GameEventSemanticsGoldenTest.Two independent paths leave one event batch and fail independently. Relative mouse motion is consume-on-read. The bridge tests declare a local Keyboard that reads the legacy accumulators, so their assertions look like public-API assertions and are not. There is no Game::TickInput or UpdateInput: the snapshots update at the end of PollEvents.
ControllersGamePad.cpp and Joysticks.cpp, then the platform gamepad and joystick services of the backend (on SDL3, EnsureControllerSubsystem).GamePadInputTest.*, GamePadMappingTest.*, CannedJoystickTest.*, CnaInputDevicesHotplugTest.*.Controllers are acquired lazily and pumped only once the subsystem is initialized. A logical player slot is not a hotplug device id. Win32 has no gamepad or joystick service at this snapshot.
Touch and gesturesTouchPanel.cpp and GestureDetector.cpp, fed by the finger map in the bridge.TouchInputTest.*, TouchEdgeCaseTest.*, GestureDetectorTest.*, PlatformInputBridgeTouchGestureTest.*.A 64-bit native finger id is mapped to a small stable id in one process-wide map; touch state and the gesture detector are process-wide statics; TouchPanel::Update runs from FrameworkDispatcher::Update, once per update step. Only SDL3, X11 and Wayland deliver touch.
A platform service or capabilityPlatformCapabilities.hpp, then the service interface under CNA/Platform/ and the backend's platform class. Recipe: Modify a platform backend.PlatformConformance.* (run for every compiled implementation), the CnaPlatformTests entry (shuffled and repeated), ContractIsSdlFreeTests.A service is null exactly when its capability is false, and an unsupported capability refuses by name. New SDL references outside the declared edges fail a strict configure gate, and platform calls inside per-event or per-pixel loops fail the hot-path lint.

Graphics

ChangeStart here, thenTests to run firstTrap
TexturesTexture2D.cpp (and Texture3D, TextureCube), then ITextureRenderer in IGraphicsRenderer.hpp and the family's texture translation. Trace: Textures and render targets.Texture2DTest.*, PartialTextureTransfer.*, ClassicTextureFormat.*, NpotTexture.*, CrossRendererContractTest.ATextureCanBeCreatedAndReadBackOnEveryRenderer (multi-renderer build).Format, mip and row-pitch translation; upload lifetime; and device disposal (a resource keeps only a weak device token). A content load that succeeds proves bytes were read, not that the GPU translation is right.
Buffers and geometryVertexBuffer.cpp, IndexBuffer.cpp, VertexDeclaration.cpp, then IVertexBufferRenderer, IIndexBufferRenderer and the draw entry points of GraphicsDevice. Trace: Indexed draw trace.BufferDataBindingContractTest.*, IndexBufferTransferContractTest.*, NonIndexedDrawRangeTest.*, IndexedDrawDeferredTest.*, BuiltInVertexLayout*, the parity_vertex_semantics and parity_multi_stream_split fixtures.Stride, offsets and declaration must agree before the native draw. A 32-bit index buffer is an explicit renderer opt-in (the default factory throws), and multi-stream input is capability-gated: the device throws NotSupportedException rather than rendering from a subset of the streams. A draw on a disposed buffer is rejected at the device.
Shaders and effectsEffect.cpp and ShaderEffect.cpp, then IEffectRenderer and the family's compiler and binding map; compiled XNA effects go through ICompiledEffectRuntime.EffectTest.*, ShaderEffectTest.*, the stock-effect tests, and the family's own shader examples; compare at least two renderers.Preserve compiler diagnostics. CustomEffects means a renderer accepts an effect, not that it runs the source (SOFTWARE and HEADLESS accept and ignore, Vulkan takes SPIR-V); ask the matching ...EXT() query as well. Compiled effects are per-identity and off by default outside FNA3D.
Sprite batch and 2D drawingSpriteBatch.cpp, then ISpriteBatchRenderer and the family's sprite path.SpriteBatchTest.* (which uses a recording renderer double), SpriteBatchRasterizationTest.*, the sprite parity fixtures.Seven identities are deliberately 2D-only and the 3D pipeline is a capability, so a sprite change must not assume 3D state exists.
Render targetsRenderTarget2D.cpp and RenderTargetCube.cpp, GraphicsDevice::SetRenderTarget and SetRenderTargets in GraphicsDevice.cpp, then IRenderTargetRenderer and the backend transition.RenderTargetSemantics.*, RenderTargetFormatAgreement.*, RenderTargetBindingTest.*, GraphicsDeviceLifecycleTest.DisposalIsReentrantAndReleasesABoundRenderTarget, GraphicsDevicePresentRegionTest.PresentingWithRenderTargetsBoundIsRefused.Present is refused while a target is bound. Vulkan cannot free a target on unbind, and a bound target can be disposed under you (boundRenderTargetDestroyed_). The default Reach profile now limits multiple targets, float targets and large cubes.
Swapchain, present and surface changesGraphicsDevice::Present, GraphicsDeviceManager::BeginDraw and EndDraw (GraphicsDeviceManager.cpp), the renderer's Present, PresentRegionEXT and OnSurfaceChanged, then the platform window and its presenter.GraphicsDevicePresentRegionTest.*, PresentationLifecycle.*, the resize cases above, the family's swapchain examples under the private display runner.Extent and in-flight work at a resize; a zero extent when minimized; EndDraw presents and releases the frame's context lease even on an exception. On Wayland the frame pacer waits during presentation.
One renderer family (Vulkan, WebGPU, DirectX, GL, ...)The identity's descriptor (RendererRegistry.cmake maps identity to family; the family's GetDescriptor()), its create factory, then resources and frame code under modules/renderers/<family>/. Index: Graphics backends.The family's example tests (labelled by family, for example Vulkan_*), the neutral graphics tests, and a second renderer; native validation on where it is wired (validation by backend).One EasyGL change serves five identities. Do not infer parity between families: no CNA workflow names WEBGPU, SDL_GPU, OPENGL4, FNA3D, PORTABLEGL, FREEDIRECT or DIRECTX9 (the Windows Direct3D 11/12, Direct2D and GDI lanes are manual), so local coverage of the family and driver you changed is the evidence.
A new renderer identity or familyRendererIdentities.cmake, check_renderer_identities.py, RendererRegistry.cmake, RendererCombinations.cmake, the family directory with its descriptor. Worked change: add a renderer identity.RendererIdentityRegistry, RendererCombinationRegistry, RuntimeRendererDiscipline, RendererTargetDiscipline, CnaRendererDefaultSelection_*.A name outside the 25 is a configure error, C ABI values are never reassigned, and per-family macros must stay private to the family target so a multi-renderer binary stays honest.
A capability answerGraphicsCapability.hpp, IGraphicsRenderer::SupportsCapability and the opt-in queries, GraphicsDevice::SupportsCapability, and the capability profile. See truthful capabilities.GraphicsDeviceCapabilityTest.*, GraphicsCapabilityFloatRenderTargetTest.*, RendererCapabilityProfileTest.*.The default answer is true for older entries. A new entry needs an explicit opt-in per renderer, and a capability check with an honest refusal at the device.
The CNAEXT engine layermodules/graphics-ext under the CNA_CNAEXT option (off by default), then the shader packages and the renderer's own support. Overview: Graphics extension internals.The cnaext preset, CnaGraphicsExtTests, CNAEXT_GuardDiscipline, CNAEXT_DoxygenGroup, the *ShaderPackageReproducibility entries.Every file in the module must be wrapped in #ifdef CNA_CNAEXT. Ask the capability and the matching query. The layer has its own naming rules (lowerCamelCase verbs, EXT suffix), and its shaders are written to a GLSL ES 3.00 floor.

Assets and services

ChangeStart here, thenTests to run firstTrap
Content loadingContentManager.cpp and the inline Load<T> in ContentManager.hpp, then the readers: XNB through ContentTypeReaderManager.cpp and modules/content/src/Xnb/, CNB through CnbLoaderRegistry, loose files through registered readers. Trace: Content runtime internals.ContentManagerXnbTest.*, ContentManagerTexture2DXnbTest.*, CnjAssetCacheTypeSafetyTest.*, ContentPathContainmentTest.*, ContentReaderTest.*.The ladder is .xnb, then .cnb, then loose files, and a real .xnb always wins. The cache key is (requested type, normalized name); Unload() clears only the cache. Malformed input must surface as ContentLoadException.
A pipeline or content formatThe importer, processor and writer under modules/content/src/Pipeline and modules/content-pipeline/src, the command-line coordinator content.cpp (executable cna-content), then the runtime loader. Recipe: Modify the Content Pipeline.CnaContentPipelineTests and the pipeline groups of CnaContentTests, the golden-vector and CNB codec tests, CnaXnbSpecificationConformance, CnaXnbModelCorpusSweep, CnaXnbDependencyBoundary.A pipeline change is verified on both sides: the build-time route needs no device, the load-back check does. Build-time dependencies (FreeType, FFmpeg, an external effect compiler) must stay out of a game's link closure.
Audio decodingThe codec readers under modules/audio/src/Internal (WavDecoder, ImaAdpcmDecoder, MsAdpcmDecoder, XactParser), then SoundEffect data ownership in SoundEffect.cpp.WavDecoderTest.*, XactParserTest.* and XactParserFuzzTest.*, SoundEffectTest.*, ContentManagerSoundEffectXnbTest.*.Malformed data must fail cleanly, and instances keep the effect's data alive (a shared keep-alive), so a callback cannot outlive it. Without a mixer (SDL2 or NULL audio) there is no XNA playback at all.
Audio playbackSoundEffectInstance.cpp, then MixerEngine (SDL3 mixer or CNA's own mixer under modules/audio/src/Backend) and the selected device in modules/audio/src/Platform/. Trace: Audio engine internals.AudioDeviceConformanceTests.*, CnaMixer.*, AudioMixerTest.*, SoundEffectInstanceTest.*, DynamicSoundEffectInstanceTest.*, the CnaAudioPlatformTests entry.The device Stop and Close barriers, the mixer generation, and no blocking, allocating or throwing work in a buffer callback. SOUND_ENABLED exists only for the SDL3 and ALSA pairings.

Optional and extension modules

ChangeStart here, thenTests to run firstTrap
StorageStorageContainer.cpp and StorageDevice.cpp, with the containment helper PathContainment.hpp. Guide: Storage internals.StorageDeviceDeleteContainerTest.*, PathContainmentTest.*, the CnaStorageTests target.Storage is a destructive authority boundary: escape, absolute and dot names must delete nothing. The tests exercise lexical escapes; confinement against a concurrently changing filesystem is not established by them. See the storage case study.
Devices and sensorsmodules/devices/src (Accelerometer, Gyroscope, Compass, Motion, vibration) and the host services of modules/devices-ext, whose surface (dialogs, clipboard, tray and the like) is behind CNA_DEVICES (off by default; the sanitizer presets turn it on). Guide: Devices and sensor lifetime.CnaDevicesTests, CnaDevicesExtTests, the devices-asan, devices-tsan and devices-ubsan presets, DevicesShutdownOrderingTest.*.Dispatch and disposal barriers are per class; an application must call DevicesShutdownCoordinator::Shutdown() before its own platform shutdown when the default vibrate controller may have been used.
Networking and gamer servicesmodules/net and modules/gamer-services, both behind CNA_ENABLE_NET (on by default; the unit preset turns it off). Guides: Network session internals, Gamer services internals.CnaNetTests, CnaGamerServicesTests, the two-process net harness.Neither module is built by the unit preset (it sets CNA_ENABLE_NET=OFF), so a green unit run says nothing about them.
Diagnostics and the Inspectormodules/diagnostics (CNA_DIAGNOSTICS = OFF, STATS or FULL) and modules/inspector (CNA_BUILD_INSPECTOR, off by default). Guides: Diagnostics, Inspector.CnaDiagnosticsTests, CnaInspectorTests (Inspector builds only).Hooks inside the engine compile to nothing in an OFF build; the agent owns one background thread and everything it reads must stay valid while the game thread mutates it.
Video and mediamodules/media for the stable types and modules/video-ffmpeg for the optional backend (CNA_ENABLE_VIDEO = AUTO, ON or OFF). Guides: Media, FFmpeg video boundary.CnaMediaTests; the FFmpeg-conditional cases run only in a build that has the backend.Video types exist everywhere and throw NotSupportedException without a backend; FFmpeg is never built on Windows, Emscripten, Android or iOS.

API and tooling

ChangeStart here, thenTests to run firstTrap
Public XNA API surface or behaviourThe XNA-shaped header under modules/<module>/include/Microsoft/Xna/Framework/ and its implementation, then every consumer: tests, examples, downstream modules, the C API mapping. Recipe: Change public XNA behavior.The type's own tests, PublicApiInputSignatureFreezeTests where a freeze test exists, StrictXnaApiSurfaceCheck_Compile_Run, the oracle scenes when behaviour is rendered (XNA oracle).Keep names and semantics XNA-shaped; where XNA and FNA disagree, CNA follows XNA. Non-XNA declarations in the XNA namespace must carry CNAEXT. A new public symbol without a C ABI mapping makes the coverage gate stale.
C, C#, Java, Python, Rust, TypeScript, Swift, Go or Ruby APIThe owning C++ module, then modules/c-api (headers under include/CNA/C/, implementation in src/), then the separate binding repository. Recipe: Update the C API. The bindings are listed on the C API page.Build with CNA_BUILD_C_API=ON: the CApi_* entries, CApiAbiHeaderBaseline, CApiCoverageMatrix, CApiHeaderCompatibility, CApiReleaseGate; then each binding's own tests at its pinned revision.The ABI is versioned separately (0.29.0 at this snapshot) and each binding repository is independently versioned and pins an ABI. No CNA workflow builds the C library, so a green CI run says nothing about it.
A build option or selection ruleThe root CMakeLists.txt, the owning cmake/*Selection.cmake or helper, then CMakePresets.json and the workflow that uses it. Recipe: Change build configuration.Configure the valid and the invalid combinations; the configure-time CTest cases (CnaAudioPlatformSelection_*, CnaSdl2OnlyRendererGate, CnaSdlOffFindsNoSdlPackage, CnaRendererDefaultSelection_*).State the default and the refusal message, and remember the unit preset turns several options off. The generated option index is a syntactic scan: conditional defaults are not evaluated.
CIThe owning test target, then .github/workflows and clone_siblings.sh.Reproduce the workflow's configure line and test filter locally first.A workflow file is configuration, not a result. Seventeen of the twenty workflow files run automatically on pushes and pull requests (several only for changes under certain paths); the Windows D3D and GDI lanes are manual, one content lane runs only on a named branch, and one workflow is not the whole matrix.
Tests and test infrastructuremodules/<module>/tests/ (mirroring the namespace path) or the cross-cutting tests (fixtures, minimal-link probes), then registration in UnitTests.cmake and TestHelpers.cmake. Recipe: Add a regression test.Confirm the registration with ctest -N before believing the test runs; for a helper change, configure two unlike trees and compare.Sources are filtered by configuration, so absence from ctest -N is information. A test that opens a window needs a private display; exit code 77 means skipped, and Vulkan and OpenGL4 tests fail on their validation-output gates.

After you have picked a row

  1. Read the internals page linked in the row for the owner's lifetimes and failure modes, and the invariants that the row touches.
  2. Write the trace and the smallest failing test before editing (Working on CNA).
  3. Choose the broader gate from What to test after changing X, and record which platform, audio and renderer axes and which other implementations were not exercised.
  4. If the symptom is a failure rather than a change, go to the debugging cookbook first.

Related indexes: the generated module map, the test target index, the public header index and the renderer family map.

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

Tests and validation
What to test after changing X