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 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
- 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 four renderers (HEADLESS,SOFTWARE,STUB,PORTABLEGL) 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]; the OpenGL4 renderer installs a KHR_debug callback in debug builds or whenCNA_OPENGL4_DEBUG_OUTPUTis set (0disables it,verboseadds informational messages, and its errors carry[OpenGL4 GL Error]); DirectX 11 creates its debug device in debug builds and followsCNA_D3D11_DEBUG_LAYER=1or0; DirectX 12's debug layer and GPU-based validation are opt-in throughCNA_D3D12_DEBUG_LAYERandCNA_D3D12_GPU_VALIDATION; 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, DirectX 12 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 and OpenGL4: the engine layer writes GLSL ES 3.00 (compute GLSL ES 3.10) and
ShaderEffectowns the#versionline and the down-level transforms; the OpenGL4 renderer adapts ES 3.00 source for desktop core inAdaptGlslEs300ForDesktopCore. 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
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_extandcna_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.
- 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 the backend cases X11Live.ResizingProducesBothALogicalAndAPixelSizeEvent and WaylandProtocol.AFractionalScaleResizesTheBufferNotTheWindow. See the units invariant and Debugging a native 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. 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 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 (on Win32WM_CHARis 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
- 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).SDL2andNULLcarry 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 native
WIN32platform (user32 and gdi32 on anHWND, 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 forWIN32in 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 Windowslists what only native Windows can establish). - 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. Win32 drains the calling thread's whole message queue in
PollEvents, window procedures run synchronously insideDispatchMessageW, 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_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 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 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 |
was requested but this machine cannot build it | X11 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 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 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 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_OPENGL4_DEBUG_OUTPUT, CNA_D3D11_DEBUG_LAYER, CNA_D3D12_DEBUG_LAYER, CNA_WEBGPU_VALIDATE_SHADERS | Driver and API misuse. | CNA's test helpers fail Vulkan and OpenGL4 tests on [Vulkan Validation] and [OpenGL4 GL Error] 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