Debugging cookbook

CNA snapshot 009d40f5  ·  Development › Debugging  ·  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. Functions, environment variables, presets, scripts and configure messages were read at 009d40f5; none was run for this page, and the tests named are registered, not executed.

Localize a failure by proving the transitions between runtime, platform, graphics and resource ownership, and log CNA's own stable types at each boundary before diving into a native API. This cookbook gives, per symptom, the ordered probes, the source functions to break in, the switches that exist at snapshot 009d40f5 and the tests that already pin the behaviour. Every function, switch and script named here was read in the CNA source; none was run for this page, so “check X” means “X is where the answer lives”, not “X was observed to fail”.

Moves that apply to every symptom

  1. Write down the three axes and the build. The selected platform (CNA_PLATFORM), audio implementation (CNA_AUDIO_PLATFORM) and renderer identity (CNA_GRAPHICS_RENDERER, or the entry a multi-renderer build resolved), the build type, the preset and the host. The configure log states them (CNA: Using <X> platform implementation, CNA: renderer set -- ... (default: ...), and the audio equivalent), and CMakeCache.txt holds the values. Most cross-machine surprises are a different tuple, not a different operating system.
  2. Make CNA talk. CNA::Logger writes to stderr, never stdout, because a terminal-hosted game draws on stdout (Logger.hpp). The default minimum level is TRACE in a build without NDEBUG and INFO in one with it; Logger::SetMinimumLevel changes it and SetSink redirects it. The first device logs CNA: graphics renderer: <NAME> at INFO (with the number compiled in when it is a multi-renderer build), so the renderer that is actually running is in the log.
  3. Prove the transition, not the symptom. Pick the boundary the failure crosses (below), and show that the value going in is right before suspecting the value coming out.
  4. Reduce, then compare. Reproduce headless or with a clear-only scene, then run the same scene on a second renderer or on the CPU SOFTWARE renderer. A difference that follows the renderer is translation; one that follows the scene is shared code.
  5. Keep the first error. Under a sanitizer or a validation layer, the first report is the evidence and everything after it is a cascade.

Application starts, no window

  1. Confirm the selection allows a window. HEADLESS and TERMINAL platforms have none, and four renderers (HEADLESS, SOFTWARE, STUB, PORTABLEGL) declare needsWindow = false and open no window on a windowing platform by design. TERMINAL with a CPU renderer is the deliberate exception: the frame reaches the screen through a surface presenter, not a window. GraphicsDevice::createOrAttachWindow decides this from the family descriptor and the platform's capabilities (GraphicsDevice.cpp).
  2. Break at the three creation points. PlatformFactory::Create(name) (PlatformFactory.cpp) throws a PlatformException that lists the implementations compiled into the binary, so a wrong or missing platform is named. Then GraphicsDevice::resolveRenderer (descriptor resolution and the fallback loop) and the platform's CreateWindow or AdoptWindowHandle.
  3. Check early exit and error reporting. Game::Run logs a fatal exception it did not catch (CNA: fatal exception escaped Game::Run()) and rethrows it. A QuitEvent or a CloseRequested window event in the first batch ends the loop through Exit() in Game::PollEvents. Game::ShowMissingRequirementMessage returns false by default, so a missing requirement produces no dialog unless a subclass provides one.
  4. Check the display. DISPLAY, WAYLAND_DISPLAY, the SDL video driver where SDL is the platform, and whether the run is under the private runner (run_gpu_tests_private.sh), which deliberately keeps windows off your desktop.
  5. Verify the window kind matches the renderer. The descriptor's windowKind sets the render intent given to CreateWindow and must be right at creation. A fallback candidate that wants another kind of window is refused with WindowKindConflict when the window was supplied by the caller.

Tests that pin these paths: PlatformConformance.* and PlatformWindowConformance.* for every compiled platform, GraphicsDevicePlatformWindowTests.*, and GraphicsRendererFallbackTest.*. See Platform backends and Startup source trace.

Renderer initialization fails

Record the selected identity, the adapter and device discovery result, the required capabilities or extensions, the native window and surface handles, and the first native error. Then check these, in order:

  1. What was asked for and what was resolved. CNA::GraphicsRendererSelection answers GetSelected() (explicit SetPreferred, then the CNA_GRAPHICS_RENDERER environment variable, then the compiled default), GetAvailable(), IsLatched(), GetActive() and GetFallbackHistory(). An environment value that names no renderer, or one that is not compiled in, throws instead of being ignored. Fallback is opt-in (SetFallbackChain, EnableAutomaticFallback); without it there is exactly one attempt and its error is the game's error.
  2. Read the failure text. An exhausted resolution throws CNA: no graphics renderer could be created. with the first failure (the renderer that was requested) and every attempt with its reason: NotCompiledIn, ProbeUnavailable, InitializationFailed or WindowKindConflict. The selection is latched only when a resolution succeeds, so a game can catch the error and retry another configuration.
  3. Confirm the build contains what you think. A configure that selected a different family than you meant shows in the CNA: renderer set line; a single-renderer build compiles exactly one family. Neither the platform nor the audio axis chooses a renderer for you.
  4. Enable native validation before changing code. Vulkan enables VK_LAYER_KHRONOS_validation in builds without NDEBUG when the layer is installed and echoes each message with the prefix [Vulkan Validation]; the OpenGL4 renderer installs a KHR_debug callback in debug builds or when CNA_OPENGL4_DEBUG_OUTPUT is set (0 disables it, verbose adds informational messages, and its errors carry [OpenGL4 GL Error]); DirectX 11 creates its debug device in debug builds and follows CNA_D3D11_DEBUG_LAYER=1 or 0; DirectX 12's debug layer and GPU-based validation are opt-in through CNA_D3D12_DEBUG_LAYER and CNA_D3D12_GPU_VALIDATION; WebGPU pre-compiles its whole shader set at device creation when CNA_WEBGPU_VALIDATE_SHADERS is set and reports any WGSL failure there. The per-backend table is on Validation by backend.
  5. Separate a missing dependency from an invalid host. The descriptor's availability probe and the constructor report different things; the configure step reports the same distinction earlier (see the last recipe on this page).

Pinned by GraphicsRendererFallbackTest.ExhaustedChainThrowsAndNamesEveryAttempt, AFailedResolutionDoesNotLatch and RecoveringAfterAFailedResolutionActuallyWorks; GraphicsDeviceSubsystemLifecycleTest.AFailedConstructionLeavesNoOutstandingReference. Selection detail: Renderer selection internals.

Window exists, black screen

  1. Prove Game::Draw executes. A breakpoint, a log line in your Draw, or, in a build with CNA_DIAGNOSTICS=STATS or FULL, the frame counters Runtime/UpdateCount and Runtime/DrawCount. A frame that updates but never draws points at suppressDraw_ or at BeginDraw returning false (no device, CanBeginDrawEXT() false); one that draws but never updates points at a fixed-step target above the 500 ms elapsed-time clamp. A suspended mobile game parks in WaitWhileSuspended and draws nothing.
  2. Prove BeginDraw acquired the context and EndDraw reaches Present. GraphicsDeviceManager::BeginDraw takes the frame's renderer-context lease and EndDraw presents and releases it, also on an exception (GraphicsDeviceManager.cpp).
  3. Check that no render target is left bound and the extent is nonzero. GraphicsDevice::Present throws InvalidOperationException while a target is bound (invariant), so a game that swallows exceptions sees a black frame instead. Log GetPixelSize() of the window and the viewport: a minimized or zero-size surface presents nothing.
  4. Check that clear and pipeline state reach the backend. On a HEADLESS renderer CNA_HEADLESS_MODE selects fast, trace or validation (the default), and trace records each renderer call with its frame index, which shows whether the calls happened at all; the log is read through HeadlessRenderer::TraceLog(), FormatTraceLog() or DumpTraceLog, an internal API reachable from tests and harnesses. To see the pixels, read them back: Texture2D::GetData on a render target, or GetBackBufferData (GraphicsDevice.cpp); CNA_BACKBUFFER_READ_TRACE prints the region and sizes of a backbuffer read to stderr.
  5. Reduce. Clear only, then one triangle, then a texture, then the shader. Compare another renderer, and remember that SOFTWARE and HEADLESS accept custom shader source without running it, so a custom-effect scene can look right there for the wrong reason.
  6. Rule out the debug hotkeys. Game::PollEvents calls DebugSimulateContextLoss() on F9 and DebugRestoreContext() on F10 on the game's renderer (not on repeats), with no build guard. A game that binds either key also triggers the renderer's context-loss seam where a family implements it. On DirectX 11, DirectX 12 and WebGPU a lone F9 makes BeginDraw() refuse every frame (with a registered GraphicsDeviceManager) until F10 restores the device, which looks like a frozen or black window while Update keeps running; on WebGPU with a TextureCube, RenderTargetCube, Texture3D, occlusion query or custom ShaderEffect alive, F9 throws System::NotSupportedException instead (CNA-BUG-078).

Related: Localizing a missing frame, Graphics ownership and frame flow, and the profile rule that 3D features (multiple render targets, occlusion queries, 32-bit indices, float targets, large cubes) throw under the default Reach profile unless HiDef is requested.

Corrupted geometry or missing textures

Inspect the vertex declaration, stride and offsets, the index type, the primitive count and the buffer bounds before the native draw. For textures inspect the format translation, row pitch, mip count, upload lifetime, sampler state and shader binding. A content load that succeeds proves only that bytes were read, not that the GPU translation is correct.

  • Where the neutral layer refuses. GraphicsDevice rejects a draw on a vertex or index buffer disposed after binding (GraphicsDeviceLifecycleTest.DrawRejectsVertexBufferDisposedAfterBinding and IndexedDrawRejectsIndexBufferDisposedAfterBinding), refuses multi-stream input where MultiStreamVertexInput is false with a NotSupportedException that names the capability, and requires an explicit renderer opt-in for 32-bit indices. A silent wrong image is therefore usually inside a family.
  • Trace the transfer. CNA_TEXTURE_TRANSFER_TRACE makes Texture2D::GetData print, per call, the resource size, level, region, element counts, required bytes, destination range and row pitch (Texture2D.cpp); that separates “the shared layer asked for the wrong bytes” from “a renderer wrote the wrong bytes”.
  • Use the parity corpus. The shared fixtures under modules/graphics/examples/parity (for example parity_vertex_semantics, parity_multi_stream_split, parity_sampler_filters) assert their scenes programmatically in each renderer build (ctest -R Parity), and run-parity-fixture.sh diffs one fixture's frames between an EasyGL build and a WebGPU build. A fixture that fails on one family and passes on the rest localizes the bug.
  • Profile and format limits. Compressed and float formats are per-renderer capabilities; check GetRendererCapabilityProfileEXT() and the format-support queries before assuming a format is usable.

Deeper: Indexed draw trace and Textures and render targets.

Shader compilation or binding failure

Preserve the generated or transformed shader source and the compiler diagnostics. Check the profile and backend selection, the entry point, the binding map and the agreement of vertex inputs. Then go family by family:

  • SDL_GPU inspects its SPIR-V route and binding translation (SDL_gpu internals); its optional shader cross-compiler is controlled by CNA_SDL_GPU_SHADERCROSS.
  • GL family and OpenGL4: the engine layer writes GLSL ES 3.00 (compute GLSL ES 3.10) and ShaderEffect owns the #version line and the down-level transforms; the OpenGL4 renderer adapts ES 3.00 source for desktop core in AdaptGlslEs300ForDesktopCore. For EasyGL trace the translation and state (EasyGL internals).
  • Vulkan takes SPIR-V, not the GLSL text the engine layer writes; precompiled payloads are covered by SpirvPayloadValidation and the *ShaderPackageReproducibility CTest entries in ModuleProbes.cmake.
  • WebGPU, Direct3D, Vulkan pipelines: check the pipeline, bind-group, root-signature or descriptor mapping; CNA_WEBGPU_VALIDATE_SHADERS and the DirectX debug layers above surface errors early.
  • Which shader actually ran. GraphicsCapability::CustomEffects means a renderer accepts an effect. Ask GraphicsDevice::ExecutesShaderEffectSourceEXT() as well: SOFTWARE and HEADLESS report no shader errors because they never compile the source.
  • Through a binding: the C API exposes cna_shader_effect_copy_compile_error_ext and cna_compute_shader_copy_compile_error, so a foreign caller can retrieve the compiler text.

Device, surface or swapchain failure

There are two resize paths and they fail differently, so decide which one you are on before tracing.

  1. A resize event from the window system. The platform queues Resized, PixelSizeChanged or DisplayScaleChanged; Game::PollEvents refreshes GameWindow, calls GraphicsDevice::UpdateViewportFromWindow and invalidates the renderer's surface, ignoring the event's size payload and re-querying the window instead. UpdateViewportFromWindow asks the window for its id, native handle, drawable size and display scale and calls the renderer's OnSurfaceChanged; a window that refuses a query is logged as a warning and the renderer keeps its previous surface, which is a plausible source of a stale swapchain extent.
  2. A size change the game requests (PreferredBackBuffer*, ApplyChanges, Reset). GraphicsDevice::applyPresentationParametersToWindow sets the fullscreen mode, calls SetSize and then Sync() (except on Android), because window changes are asynchronous on most systems and the viewport refresh that follows needs the new size. Sync() is part of this path, not of the event path.

At each step log GetClientBounds() (logical), GetPixelSize() (drawable) and GetDisplayScale(), and treat a minimized or zero extent separately: no swapchain should be recreated for it. Then verify that old in-flight work and resources are synchronized before a replacement is created, and that the presentation mode matches what the test oracle assumes. A lost context or device is a different failure: DebugSimulateContextLoss and DebugRestoreContext on the renderer, and SetContextRecoveryEnabled on the device, exist to rehearse it.

Pinned by PlatformWindowConformance.SizeChangeLandsAfterSync, the resize cases of GameEventSemanticsGoldenTest, GraphicsDevicePlatformWindowTests.AViewportRefreshSurvivesAWindowThatRefusesItsDrawableSize, and the backend cases X11Live.ResizingProducesBothALogicalAndAPixelSizeEvent and WaylandProtocol.AFractionalScaleResizesTheBufferNotTheWindow. See the units invariant and Debugging a native backend.

Crash during resource destruction

  1. Is the resource still registered, and is disposal repeated? Resources register with their device and unregister when disposed; GraphicsResource::Dispose(bool) is idempotent and consults a weak lifetime token before touching the device. On a HEADLESS renderer, AliveResources() and AssertNoLeaks() list every resource still alive (with its creation site in trace mode). Give resources a Name and subscribe to GraphicsDevice::ResourceCreated and ResourceDestroyed (the latter carries the name and tag) to log lifetimes; with CNA_DIAGNOSTICS at STATS or higher each resource also has a diagnostics handle (Diagnostics: resources).
  2. Does the native device, context or platform service still exist? GraphicsDevice::Dispose(bool) must drain its resources before native teardown; a resource that survives its device holds only a weak token and cannot call back into it, but a renderer-side object may outlive its renderer. A render target disposed while bound is handled by a dedicated path (boundRenderTargetDestroyed_).
  3. Is it really the destructor, or process exit? A crash after main returns is usually static destruction. CNA deliberately keeps some registries immortal (the live-platform stack in Game.cpp, the audio mixer engines, the ambient platform storage) for that reason; a new function-local static that a destructor uses is a suspect.
  4. Run a sanitizer and keep the first report. -DCNA_SANITIZE=address,undefined (comma-separated; AddressSanitizer with ThreadSanitizer, and ThreadSanitizer with MemorySanitizer, are rejected; not for MSVC or Emscripten) with CNA_SANITIZE_OPTIMIZATION of DEFAULT or O0 to O3; it cannot be combined with reduced debug information or IPO. The sanitizers cover CNA, sharp-runtime and the tests, not the vendored SDL or ENet. The presets devices-asan, devices-tsan and devices-ubsan in CMakePresets.json configure Debug, OPENGLES3, tests on, CNA_DEVICES=ON and the named sanitizer (O0 for ASan, O1 for the others) and build the full CnaTests; the TSan preset's own description records one known, unrelated race in sharp-runtime as of the task it cites, but that race (in TimeSpan's debug copy counter) was fixed in sharp-runtime 9c2cb0ae (2026-07-07), so with a current sharp-runtime treat any race as worth investigating. Build only the variant you need: the binaries are large. lsan_x11_mesa.supp holds LeakSanitizer suppressions for Mesa under the native platform suites (use it through LSAN_OPTIONS=suppressions=...); its header says every entry names a leak that was reproduced without any CNA code.
  5. Preserve the first use-after-free, not the later destructor cascade, and check the teardown order against Ownership and shutdown.

Pinned by GraphicsDeviceLifecycleTest.*, GraphicsDeviceDisposalHookTest.*, GraphicsResourceTest.CrossDeviceMoveAssignmentTransfersTrackingAndDetachesOldBindings and, for a foreign owner, the CApi_TeardownLifetime_* entries. Recipe: Debug shutdown and lifetime behavior.

Platform input not received

Prove the chain in order: native event, PlatformEvent, PlatformInputBridge::ProcessEvent, the frame snapshot, the public query. There are two paths out of one batch and they fail independently: a key that reaches a text or bridge callback but never shows in Keyboard::GetState is a snapshot-service problem, and one that shows in GetState but not in a callback is a mapper or bridge problem.

  • Does the service exist? Every service accessor on IPlatform is null when its capability is false, and the public input types then degrade quietly to empty states. SDL2 has no mouse or text service; Win32 has no gamepad or joystick service; the terminal offers a keyboard and mouse only when both standard input and output are terminals; HEADLESS has none, so an injected key reaches the bridge but never Keyboard::GetState.
  • Focus and enumeration. Check IsActive (driven by FocusGained and FocusLost) and the device lists (InputDevices::GetKeyboardsEXT and siblings, or the platform's input-device service).
  • Relative mouse. Reading relative motion consumes the accumulated delta, so the first read in a frame drains it and a second read returns zero.
  • Controllers are acquired lazily on first use and pumped only afterwards.
  • Text is a mode: TextInputEXT::StartTextInput must have been called for the window (on Win32 WM_CHAR is consumed until then).
  • Order. Exit() does not stop the batch, so later events still reach input; the runtime does not filter window events by id.

Run ctest -L input (the CnaInputTests entry runs the input suites shuffled and repeated five times under a real X display or a virtual one) and EveryImplementation/GameEventSemanticsGoldenTest.* for the cross-platform event contract. Details: Input internals.

No sound, or audio that stalls

  1. Is there a mixer? XNA playback (SoundEffect, SoundEffectInstance, DynamicSoundEffectInstance) needs the SOUND_ENABLED mixer, which exists only for CNA_AUDIO_PLATFORM=SDL3 (SDL3_mixer) and ALSA (CNA's own mixer). SDL2 and NULL carry the device contract without a mixer. The audio device is chosen at compile time (CreateSelectedAudioDevice in AudioDeviceFactory.cpp); there is no CNA_AUDIO_PLATFORM environment variable.
  2. Pick a deterministic device. On ALSA, CNA_AUDIO_DEVICE=null plays to a silent device that still paces itself in real time and file:FILE=out.raw,FORMAT=raw records what was played; CNA's own CTest entries use these. With SDL, SDL_AUDIODRIVER=dummy gives a headless-safe device. The variables are listed in the audio guide.
  3. Check the negotiated format. IAudioDevice::Open returns the actual format the callback must produce, and a device must not report the request when a different format is delivered; Open starts the device paused, so nothing sounds until Start.
  4. A stall or hang at shutdown is a callback-barrier question: a callback must not block, allocate, throw or call lifecycle methods, and Stop and Close wait for an in-flight callback. Reproduce under the mixer-destroy harnesses and AudioDeviceConformanceTests.StartStopAndCloseFormCallbackBarriers, and read the callbacks invariant.

Engine-level trace: Audio engine internals.

Passes on Linux, fails on Windows

Compare the selected platform, renderer and audio identities and the compiler configuration, not just the operating-system names. Reproduce the matching workflow. Then look for:

  • The tuple. Windows uses the native WIN32 platform (user32 and gdi32 on an HWND, no SDL) or SDL; the DirectX renderers, Direct2D and GDI exist only when targeting Windows. CNA's platform workflow runs the standalone platform-contract harness for WIN32 in two jobs, a mingw-w64 cross-build executed under Wine on Linux and a native MSVC build on a Windows runner; the D3D and GDI lanes are manual. A Wine run is not proof of native DPI, keyboard, IME or clipboard behaviour (Testing the Win32 backend on native Windows lists what only native Windows can establish).
  • Paths. Case and separators: ContentManager resolves each path component case-insensitively (ResolveExistingAssetPath, with a packaged-asset route on Android) and path containment is lexical; Unicode paths have their own tests (UnicodeContentRootTests, and the Windows content lane's Unicode command-line lifecycle).
  • Lifetime timing and event order. Win32 drains the calling thread's whole message queue in PollEvents, window procedures run synchronously inside DispatchMessageW, and Windows delivers a window's messages to the thread that created it.
  • Implicit GL or Vulkan assumptions in code that ran only where a GL context was current.
  • ABI and export definitions. CNA_SHARED_LIBRARY defaults to ON only for a native ELF toolchain with CMake 3.27 or newer; Windows, macOS, Android and Emscripten keep the static link and ON there is a configure error. The CApi_Exports check is ELF-only. sharp-runtime's Xml.Serialization component is not selected on Windows, so the math tests that need it are excluded there.

See Win32 in the Native platforms guide, Win32 platform internals and Platforms: Windows.

Binding crash or missing dependency

A binding. Validate the exact native ABI first. This snapshot exports ABI 0.29.0 (cna_get_abi_version(), encoded 0x00001D00, and the CNA_ABI_VERSION macro in abi.h), while every public binding targets a 0.21 generation and none has been qualified against 0.29.0: several loaders refuse a 0.29.0 library, one binding declares a symbol 0.29.0 no longer exports, and a loader that nominally admits it is not making a compatibility statement (the bindings table has each repository's pinned revision). A version mismatch is the first hypothesis for a crash or a missing symbol. Then the handle: a CNA_Handle is a slot plus generation, checked for kind (CNA_RESULT_INVALID_HANDLE) and creation thread (CNA_RESULT_THREAD), so a finalizer or garbage-collector thread that releases a handle gets a thread error rather than a destruction. Then ownership, callback lifetime (the owner keeps a context valid until its registration is removed) and exception translation (a failing callback latches CNA_RESULT_CALLBACK, calls Exit() and skips later callbacks). Read the diagnostic on the thread that saw the failure with cna_error_get_last_info, cna_error_get_last_message_size and cna_error_copy_last_message (the query calls never overwrite it). Note that a comment in content_readers.h names cna_get_last_error_message, which is not a declared function; use the cna_error_* accessors. The layer-by-layer picture is C API internals, and the no-CI-build caveat applies: the C library's tests run only where someone builds it.

CMake. Run a fresh configure with diagnostic output (a new build directory, or cmake --fresh), find the selection file that rejected or discovered the dependency, and distinguish “dependency not found” from “identity not valid on this host”. The messages differ, and grepping the configure log for these fragments answers the question:

Message fragmentMeaningEmitted by
Missing sharp-runtime checkout, Missing sibling repository 'easy-gl'A sibling repository is absent (dependency not found).the root CMakeLists.txt, RendererSelection.cmake
was requested but this machine cannot build itX11 or Wayland requested without its development packages; the message names what to install and never falls back.PlatformSelection.cmake
is a reserved identifier that is NOT implemented, is not a known platformA reserved or unknown platform or audio value (identity not valid), including a host-conditional one used on the wrong host.PlatformSelection.cmake, AudioPlatformSelection.cmake
unknown graphics renderer, is not a member of CNA_GRAPHICS_RENDERERSA renderer name outside the 25 (a retired name gets its own message), or a default that is not in the multi-renderer list.RendererIdentities.cmake, RendererDefaultSelection.cmake
renderer only builds when targeting Windows, cannot be built into the same binaryA platform partition or combination rule (identity not valid on this host).RendererSelection.cmake, RendererCombinations.cmake
CNA_ENABLE_SDL=OFF, but this configuration genuinely requires SDLA selection that needs SDL was combined with the SDL-free switch; the message names it.SdlAvailability.cmake

The configure step also runs the platform-boundary ratchet, the hot-path lint, the renderer-descriptor gate and the source-partition validator; each failure names its rule. The option index lists every option and default, and Change build configuration is the recipe for changing them.

Instruments at a glance

InstrumentHow to turn it onWhat it tells youCaveat
LoggingLogger::SetMinimumLevel, SetSinkThe active renderer, warnings such as a window refusing a query, category-tagged messages (RENDER, INPUT, AUDIO, GPU, SYSTEM).stderr only; default level differs between debug and NDEBUG builds.
Diagnostics counters and zones-DCNA_DIAGNOSTICS=STATS or FULLFrame counters (Runtime/UpdateCount, Runtime/DrawCount, Graphics/RenderTargetChanges), profiling scopes, resource handles. See Diagnostics.Off by default; hooks compile out in an OFF build.
Inspector-DCNA_BUILD_INSPECTOR=ON; the bridge executable takes its authentication token from a token file or CNA_INSPECTOR_TOKENA browser UI over the diagnostics provider through a local bridge. See Inspector.Not built for Emscripten, Android or iOS; one agent thread.
HEADLESS traceCNA_HEADLESS_MODE=traceEvery renderer call with its frame index, without a native API; AssertNoLeaks() lists resources still alive.Proves calls, not pixels; read through an internal API from tests and harnesses.
Native validationVulkan layer (debug builds), CNA_OPENGL4_DEBUG_OUTPUT, CNA_D3D11_DEBUG_LAYER, CNA_D3D12_DEBUG_LAYER, CNA_WEBGPU_VALIDATE_SHADERSDriver and API misuse.CNA's test helpers fail Vulkan and OpenGL4 tests on [Vulkan Validation] and [OpenGL4 GL Error] output.
Transfer and read tracesCNA_TEXTURE_TRANSFER_TRACE, CNA_BACKBUFFER_READ_TRACE, per-family sampler traces (for example CNA_VULKAN_SAMPLER_TRACE)What the shared layer asked a renderer to do.Text on stderr; per-family names differ.
Sanitizers-DCNA_SANITIZE=... or the devices-* presetsUse-after-free, races, undefined behaviour.Large binaries; not vendored SDL or ENet.
Private display runnerrun_gpu_tests_private.sh (--exec for one command)Runs GPU and window tests on a private Weston and Xwayland with DRI3, so nothing touches your desktop. Exit 2 for a bad build tree or a forced display, 77 when Weston or Xwayland is missing.The Wine-based interop tests hang inside it; profile_dead_tests.py then names tests that died on a graphics-profile refusal, which are test defects, not renderer results.
Bounded test runnerrun_gtest_bounded.shShards a large GoogleTest binary and reports a signal-killed shard as KILLED instead of a partial pass.Creates no display; compose it with the private runner.

Related maps: Runtime trace, Graphics ownership and frame flow, Platform event and window contract and How to understand code you did not write.

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