Debugging cookbook
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
- 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), andCMakeCache.txtholds the values. Most cross-machine surprises are a different tuple, not a different operating system. - Make CNA talk.
CNA::Loggerwrites to stderr, never stdout, because a terminal-hosted game draws on stdout (Logger.hpp). The default minimum level isTRACEin a build withoutNDEBUGandINFOin one with it;Logger::SetMinimumLevelchanges it andSetSinkredirects it. The first device logsCNA: graphics renderer: <NAME>atINFO(with the number compiled in when it is a multi-renderer build), so the renderer that is actually running is in the log. - 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.
- Reduce, then compare. Reproduce headless or with a clear-only scene, then run the same scene on a second renderer or on the CPU
SOFTWARErenderer. A difference that follows the renderer is translation; one that follows the scene is shared code. - 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
- Confirm the selection allows a window.
HEADLESSandTERMINALplatforms have none, and three renderers (HEADLESS,SOFTWARE,STUB) declareneedsWindow = falseand open no window on a windowing platform by design.TERMINALwith a CPU renderer is the deliberate exception: the frame reaches the screen through a surface presenter, not a window.GraphicsDevice::createOrAttachWindowdecides this from the family descriptor and the platform's capabilities (GraphicsDevice.cpp). - Break at the three creation points.
PlatformFactory::Create(name)(PlatformFactory.cpp) throws aPlatformExceptionthat lists the implementations compiled into the binary, so a wrong or missing platform is named. ThenGraphicsDevice::resolveRenderer(descriptor resolution and the fallback loop) and the platform'sCreateWindoworAdoptWindowHandle. - Check early exit and error reporting.
Game::Runlogs a fatal exception it did not catch (CNA: fatal exception escaped Game::Run()) and rethrows it. AQuitEventor aCloseRequestedwindow event in the first batch ends the loop throughExit()inGame::PollEvents.Game::ShowMissingRequirementMessagereturns false by default, so a missing requirement produces no dialog unless a subclass provides one. - 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. - Verify the window kind matches the renderer. The descriptor's
windowKindsets the render intent given toCreateWindowand must be right at creation. A fallback candidate that wants another kind of window is refused withWindowKindConflictwhen 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:
- What was asked for and what was resolved.
CNA::GraphicsRendererSelectionanswersGetSelected()(explicitSetPreferred, then theCNA_GRAPHICS_RENDERERenvironment variable, then the compiled default),GetAvailable(),IsLatched(),GetActive()andGetFallbackHistory(). 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. - 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,InitializationFailedorWindowKindConflict. The selection is latched only when a resolution succeeds, so a game can catch the error and retry another configuration. - Confirm the build contains what you think. A configure that selected a different family than you meant shows in the
CNA: renderer setline; a single-renderer build compiles exactly one family. Neither the platform nor the audio axis chooses a renderer for you. - Enable native validation before changing code. Vulkan enables
VK_LAYER_KHRONOS_validationin builds withoutNDEBUGwhen the layer is installed and echoes each message with the prefix[Vulkan Validation]; DirectX 11 creates its debug device in debug builds and followsCNA_D3D11_DEBUG_LAYER=1or0; WebGPU pre-compiles its whole shader set at device creation whenCNA_WEBGPU_VALIDATE_SHADERSis set and reports any WGSL failure there. The per-backend table is on Validation by backend. - 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
- Prove
Game::Drawexecutes. A breakpoint, a log line in yourDraw, or, in a build withCNA_DIAGNOSTICS=STATSorFULL, the frame countersRuntime/UpdateCountandRuntime/DrawCount. A frame that updates but never draws points atsuppressDraw_or atBeginDrawreturning 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 inWaitWhileSuspendedand draws nothing. - Prove
BeginDrawacquired the context andEndDrawreachesPresent.GraphicsDeviceManager::BeginDrawtakes the frame's renderer-context lease andEndDrawpresents and releases it, also on an exception (GraphicsDeviceManager.cpp). - Check that no render target is left bound and the extent is nonzero.
GraphicsDevice::PresentthrowsInvalidOperationExceptionwhile a target is bound (invariant), so a game that swallows exceptions sees a black frame instead. LogGetPixelSize()of the window and the viewport: a minimized or zero-size surface presents nothing. - Check that clear and pipeline state reach the backend. On a HEADLESS renderer
CNA_HEADLESS_MODEselectsfast,traceorvalidation(the default), andtracerecords each renderer call with its frame index, which shows whether the calls happened at all; the log is read throughHeadlessRenderer::TraceLog(),FormatTraceLog()orDumpTraceLog, an internal API reachable from tests and harnesses. To see the pixels, read them back:Texture2D::GetDataon a render target, orGetBackBufferData(GraphicsDevice.cpp);CNA_BACKBUFFER_READ_TRACEprints the region and sizes of a backbuffer read to stderr. - Reduce. Clear only, then one triangle, then a texture, then the shader. Compare another renderer, and remember that
SOFTWAREandHEADLESSaccept custom shader source without running it, so a custom-effect scene can look right there for the wrong reason. - Rule out the debug hotkeys.
Game::PollEventscallsDebugSimulateContextLoss()on F9 andDebugRestoreContext()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 makesBeginDraw()refuse every frame (with a registeredGraphicsDeviceManager) until F10 restores the device, which looks like a frozen or black window whileUpdatekeeps running; on WebGPU with a TextureCube, RenderTargetCube, Texture3D, occlusion query or custom ShaderEffect alive, F9 throwsSystem::NotSupportedExceptioninstead (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.
GraphicsDevicerejects a draw on a vertex or index buffer disposed after binding (GraphicsDeviceLifecycleTest.DrawRejectsVertexBufferDisposedAfterBindingandIndexedDrawRejectsIndexBufferDisposedAfterBinding), refuses multi-stream input whereMultiStreamVertexInputis false with aNotSupportedExceptionthat 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_TRACEmakesTexture2D::GetDataprint, 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 exampleparity_vertex_semantics,parity_multi_stream_split,parity_sampler_filters) assert their scenes programmatically in each renderer build (ctest -R Parity), andrun-parity-fixture.shdiffs 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-extkeeps (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, andShaderPackageEXT::selectForpicks 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
SpirvPayloadValidationand the*ShaderPackageReproducibilityCTest entries inModuleProbes.cmake. - WebGPU, Direct3D, Vulkan pipelines: check the pipeline, bind-group, root-signature or descriptor mapping;
CNA_WEBGPU_VALIDATE_SHADERSand the DirectX debug layers above surface errors early. - Which shader actually ran.
GraphicsCapability::CustomEffectsmeans a renderer accepts an effect. AskGraphicsDevice::ExecutesShaderEffectSourceEXT()as well:SOFTWAREandHEADLESSreport 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.
- A resize event from the window system. The platform queues
Resized,PixelSizeChangedorDisplayScaleChanged;Game::PollEventsrefreshesGameWindow, callsGraphicsDevice::UpdateViewportFromWindowand invalidates the renderer's surface, ignoring the event's size payload and re-querying the window instead.UpdateViewportFromWindowasks the window for its id, native handle, drawable size and display scale and calls the renderer'sOnSurfaceChanged; 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. - A size change the game requests (
PreferredBackBuffer*,ApplyChanges,Reset).GraphicsDevice::applyPresentationParametersToWindowsets the fullscreen mode, callsSetSizeand thenSync()(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
- 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()andAssertNoLeaks()list every resource still alive (with its creation site in trace mode). Give resources aNameand subscribe toGraphicsDevice::ResourceCreatedandResourceDestroyed(the latter carries the name and tag) to log lifetimes; withCNA_DIAGNOSTICSatSTATSor higher each resource also has a diagnostics handle (Diagnostics: resources). - 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_). - Is it really the destructor, or process exit? A crash after
mainreturns is usually static destruction. CNA deliberately keeps some registries immortal (the live-platform stack inGame.cpp, the audio mixer engines, the ambient platform storage) for that reason; a new function-local static that a destructor uses is a suspect. - 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) withCNA_SANITIZE_OPTIMIZATIONofDEFAULTorO0toO3; 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 presetsdevices-asan,devices-tsananddevices-ubsaninCMakePresets.jsonconfigure Debug,OPENGLES3, tests on,CNA_DEVICES=ONand the named sanitizer (O0for ASan,O1for the others) and build the fullCnaTests; the TSan preset's own description records one known, unrelated race in sharp-runtime as of the task it cites, but that race (inTimeSpan's debug copy counter) was fixed in sharp-runtime9c2cb0ae(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.suppholds LeakSanitizer suppressions for Mesa under the native platform suites (use it throughLSAN_OPTIONS=suppressions=...); its header says every entry names a leak that was reproduced without any CNA code. - 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
IPlatformis 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 neverKeyboard::GetState. - Focus and enumeration. Check
IsActive(driven byFocusGainedandFocusLost) and the device lists (InputDevices::GetKeyboardsEXTand 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::StartTextInputmust 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
- Is there a mixer? XNA playback (
SoundEffect,SoundEffectInstance,DynamicSoundEffectInstance) needs theSOUND_ENABLEDmixer, which exists only forCNA_AUDIO_PLATFORM=SDL3(SDL3_mixer) andALSA(CNA's own mixer).NULLcarries the device contract without a mixer. The audio device is chosen at compile time (CreateSelectedAudioDeviceinAudioDeviceFactory.cpp); there is noCNA_AUDIO_PLATFORMenvironment variable. - Pick a deterministic device. On ALSA,
CNA_AUDIO_DEVICE=nullplays to a silent device that still paces itself in real time andfile:FILE=out.raw,FORMAT=rawrecords what was played; CNA's own CTest entries use these. With SDL,SDL_AUDIODRIVER=dummygives a headless-safe device. The variables are listed in the audio guide. - Check the negotiated format.
IAudioDevice::Openreturns the actual format the callback must produce, and a device must not report the request when a different format is delivered;Openstarts the device paused, so nothing sounds untilStart. - A stall or hang at shutdown is a callback-barrier question: a callback must not block, allocate, throw or call lifecycle methods, and
StopandClosewait for an in-flight callback. Reproduce under the mixer-destroy harnesses andAudioDeviceConformanceTests.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
SDL3platform through SDL’swindowsvideo 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:
ContentManagerresolves 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_LIBRARYdefaults toONonly for a native ELF toolchain with CMake 3.27 or newer; Windows, macOS, Android and Emscripten keep the static link andONthere is a configure error. TheCApi_Exportscheck is ELF-only. sharp-runtime'sXml.Serializationcomponent 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 fragment | Meaning | Emitted 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=SDL3 | A 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 platform | A 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_RENDERERS | A 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 binary | A platform partition or combination rule (identity not valid on this host). | RendererSelection.cmake, RendererCombinations.cmake |
CNA_ENABLE_SDL=OFF, but this configuration genuinely requires SDL | A 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
| Instrument | How to turn it on | What it tells you | Caveat |
|---|---|---|---|
| Logging | Logger::SetMinimumLevel, SetSink | The 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 FULL | Frame 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_TOKEN | A browser UI over the diagnostics provider through a local bridge. See Inspector. | Not built for Emscripten, Android or iOS; one agent thread. |
| HEADLESS trace | CNA_HEADLESS_MODE=trace | Every 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 validation | Vulkan layer (debug builds), CNA_D3D11_DEBUG_LAYER, CNA_WEBGPU_VALIDATE_SHADERS | Driver and API misuse. | CNA's test helpers fail Vulkan tests on [Vulkan Validation] output. |
| Transfer and read traces | CNA_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-* presets | Use-after-free, races, undefined behaviour. | Large binaries; not vendored SDL or ENet. |
| Private display runner | run_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 runner | run_gtest_bounded.sh | Shards 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.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-078: Game::PollEvents fires the renderer's test-only context-loss hooks on F9/F10 in every build — A non-repeated F9 or F10 press in any CNA game calls the renderer's DebugSimulateContextLoss() or DebugRestoreContext(), a channel CNA itself describes as a test seam, with no build, option or opt-out guard.
- CNA-BUG-218: content_readers.h documentation names cna_get_last_error_message, which does not exist — The Doxygen of cna_object_dictionary_ext_copy_value tells callers to read 'the message cna_get_last_error_message returns', a function declared nowhere; the C API's error accessors are the cna_error_* functions in core.h
- CNA-GAP-002: CNA::Logger serialises writes but not level changes, and a sink must not call back into the logger — minimumLevel_ is read and written without synchronisation and the sink runs under a non-recursive mutex, so changing the level while other threads log is a data race and a sink that logs deadlocks; the header states neit
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- Diagnostics · Inspector · Building CNA: troubleshooting
- Architecture
- Runtime lifecycle · Graphics architecture · Platform architecture
- Internals
- One frame source trace · Renderer selection internals · Graphics backends · Platform backends · Audio engine internals
- Maintainer workflow
- Debug shutdown and lifetime behavior · How to understand code you did not write · Architectural invariants
- Tests and validation
- What to test after changing X
- Reference
- CMake option index · Selection axes index