Debugging cookbook

CNA snapshot c1c316b9  ·  Development › Debugging  ·  source links pinned to c1c316b9

✓

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 c1c316b9; unless a sentence says otherwise, nothing here was built or executed. Functions, environment variables, presets, scripts and configure messages were read at c1c316b9; 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 c1c316b9 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 three renderers (HEADLESS, SOFTWARE, STUB) 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]; DirectX 11 creates its debug device in debug builds and follows CNA_D3D11_DEBUG_LAYER=1 or 0; 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 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: the one shader package graphics-ext keeps (post_process, used by the CRT and colour-depth effects) carries GLSL ES 3.00 and desktop GLSL 3.30 text beside its SPIR-V, WGSL and HLSL variants, and ShaderPackageEXT::selectFor picks the language the active renderer accepts. For EasyGL trace the translation and state (EasyGL internals).
  • Vulkan takes SPIR-V, not GLSL text; 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, 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, under SDL’s real X11 and Wayland drivers, the window suites that CnaPlatformSdl3X11Tests and CnaPlatformSdl3WaylandTests run on private servers. See the units invariant and Debugging a 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. 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.
  • 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). NULL carries 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 SDL3 platform through SDL’s windows video driver; the DirectX renderers exist only when targeting Windows. CNA’s platform workflow has a native MSVC SDL3 job and the D3D lane is native MSVC too; both run on manual dispatch only, and a MinGW build run under Wine (the usual development loop) is not proof of native DPI, keyboard, IME or clipboard behaviour.
  • 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. The SDL3 platform pumps SDL’s queue once per frame on the game thread, so a window’s events are seen by the thread that runs the loop; create the game, and therefore its window, on the thread that will pump 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 Windows, X11 and Wayland, SDL3 platform internals and Platforms: Windows.

Binding crash or missing dependency

A binding. Validate the exact native ABI first. This snapshot exports ABI 0.46.0 (cna_get_abi_version(), encoded 0x00002E00, and the CNA_ABI_VERSION macro in abi.h), which is exactly the version the maintained C# binding admits (CNA.NET, cna-dotnet 8219a6c, as recorded by CNA’s Apple campaign); the eight archived bindings stopped at 0.21–0.23 or 0.35.0, most of them refuse a 0.46.0 library, and the two that nominally admit any later minor were qualified only against 0.35.0, so they make no compatibility statement (the bindings table has each binding's final 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, unless the game turned on cna_game_set_foreign_thread_calls_ext (0.39.0), which queues such a call to the game thread. 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 except the UnloadContent cleanup). 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
configure with -DCNA_PLATFORM=SDL3A CNA_PLATFORM value named after a window system rather than one of SDL3, HEADLESS and TERMINAL; the message says to configure SDL3, whose windows, x11 and wayland video drivers reach those window systems (SDL_VIDEODRIVER picks one explicitly), 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 14 (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_D3D11_DEBUG_LAYER, CNA_WEBGPU_VALIDATE_SHADERSDriver and API misuse.CNA's test helpers fail Vulkan tests on [Vulkan Validation] 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