Diagnostics and profiler internals
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. Checked by reading the TARGET sources and tests; no build or test was executed, and no CI lane builds STATS or FULL. Sizes quoted are computed from declared capacities, not measured. Renderer-specific diagnostics sources do not exist yet, and consumer UIs (the Inspector's browser page) remain unverified.
modules/diagnostics/ is a bounded, process-wide measurement layer: compile-time instrumentation, counters, gauges, frame samples, CPU zones, markers, resource metadata, bounded recording and a pull-only provider. It does not own Game, render a profiler UI, start a thread or open a socket. This page is for maintainers who change the instrumentation, the frame path, the ring buffers or the trace formats, and for anyone who must tell “the code ran”, “the work was counted” and “a trace captured it” apart. The user-level guide, with the macro table, the built-in metric names and the limits table, is Diagnostics; this page does not repeat those tables but explains the machinery behind them.
Three build modes, then a runtime ceiling
The root CMakeLists.txt defines the cache variable CNA_DIAGNOSTICS, default OFF. It is upper-cased, anything other than OFF, STATS or FULL is a FATAL_ERROR, and the value is mapped to CNA_DIAGNOSTICS_LEVEL 0, 1 or 2. modules/CMakeLists.txt then puts CNA_DIAGNOSTICS_LEVEL=${CNA_DIAGNOSTICS_LEVEL} on the cna_build_config interface target, which cna_add_module links PUBLIC into every module and which reaches every consumer through the CNA umbrella. The level is therefore a build-configuration fact, not a per-target switch. Diagnostics.hpp defaults a missing definition to 0 and turns a value outside 0–2 into #error.
The level must be identical in every translation unit for a reason stronger than macro consistency: it changes class layout. GraphicsResource.hpp declares the member CNA::Diagnostics::ResourceHandle diagnosticResource_ and the method UpdateDiagnosticResourceEXT only when the level is at least 1. A consumer compiled at a different level from the graphics library sees a different GraphicsResource, which is an ODR violation, not a missing feature.
What each level removes is decided in two places:
- Macros.
Instrumentation.hppdefines the counter, gauge, frame-counter and frame-scope macros at level 1 and above, and the zone and event macros only at level 2. A disabled macro expands toCNA_DIAGNOSTICS_DETAIL_CHECK, that isdo { (void) sizeof((args)); } while (false): the arguments are type-checked but never evaluated, so an argument with a side effect silently disappears in anOFFbuild. The disabledCNA_DIAGNOSTICS_FRAME_SCOPE()is an empty statement. Enabled macros declare a function-localstatic consthandle whose name is made unique with__COUNTER__(or__LINE__), so registration happens once per call site under C++ thread-safe static initialisation. - Implementation. In
Diagnostics.cppthe wholeState, every registry and the frame logic sit under#if CNA_DIAGNOSTICS_LEVEL >= 1; the name registry, thread contexts and event history sit under>= 2. At level 0 the handle constructors are empty,GetRuntimeModereturnsMode::Off, andGetProvider().CaptureSnapshot()fills onlybuildModeandruntimeMode. The library is still linked; it simply holds no state.
At run time SetRuntimeMode refuses any mode above the constexpr GetBuildMode(). When it accepts, it exchanges the atomic mode and, if the value actually changed, increments modeGeneration. Every change bumps the generation, not only a change out of Off: a Full→Stats→Full cycle also invalidates open zones (see zones). A runtime Off in a STATS or FULL build keeps all storage allocated; it makes metric updates, resource registration, frame begins and event emission return early. It cannot create FULL events in a binary compiled at a lower level.
Physically, modules/diagnostics/CMakeLists.txt globs src/*.cpp into the static cna_diagnostics (CNA::Diagnostics), which links only cna_build_config: standard library, no UI, renderer or platform dependency. It is linked PUBLIC by the four modules that publish into it or read it: cna_audio (voice metrics), cna_graphics_core (draw metrics and resource metadata), cna_runtime (the frame) and cna_inspector (the consumer), and it is listed in the CNA umbrella's runtime parts. A benchmark executable is added only when CNA_BUILD_BENCHMARKS is on. See the module index for the module's place in the tree.
One measured Game frame
Game::Tick() (native loop, RunOneFrame; Emscripten: EmscriptenMainLoopCallback)
FrameScope ctor -> TryBeginFrame [STATS+] frameMutex; nested begin -> malformedFrames++
ZoneScope "Game/Tick" (Core) [FULL]
fixed-step sleep / yield, PollEvents
{ ZoneScope "Game/Update" } Update -> Runtime/UpdateCount (once per fixed step)
{ ZoneScope "Game/Draw" } Draw + EndDraw (Present) -> Runtime/DrawCount
~ZoneScope "Game/Tick" zone event pushed to this thread's ring
~FrameScope -> FinishFrame(false)
thread_local completingFrame guard re-entry -> refused
lock frameCompletionMutex (held to the end)
frameMutex: frame still active? no -> return
CollectDiagnosticsSources copy shared_ptrs under sourceMutex, Collect() unlocked
frameMutex + metricMutex: duration = now - start; frameActive = false
every FrameCounter: exchange(0) -> lastFrameValue, first 64 into the 240-slot frame ring
[FULL] "Frame" event (correlation = frame number), CollectEvents()
consumers, any thread: GetProvider().CaptureSnapshot() / ReadEvents(after, max)
Game.cpp declares CNA_DIAGNOSTICS_FRAME_SCOPE() first and the Game/Tick zone second in both Game::Tick and Game::EmscriptenMainLoopCallback. Destruction runs in reverse, so the Game/Tick zone is already in the thread ring when frame completion drains it. RunOneFrame reaches the same scopes through Tick; a suspended mobile game in WaitWhileSuspended publishes no frames at all.
Two timing consequences follow from where the scope sits, and both matter when a number looks wrong:
- On native builds the frame covers the fixed-step pacing wait,
PollEventsand thePresentinsideEndDraw. A fixed-step game'sFrameSample::durationNstherefore sits near the target step even when the work is small; theGame/UpdateandGame/Drawzones isolate the work. - On Emscripten,
Game::RunLoopcallsCNA_WaitForAnimationFrameoutside the callback, so the frame measures the callback's work only.framesPerSecond, computed as 109/durationNsof that single frame (pinned byFrameHistoryIsBoundedOrderedAndReportsExactFps), is a work rate there, not the display rate. This is read from the loop structure; it was not observed in a browser.
TryBeginFrame returns false in runtime Off; otherwise, under frameMutex, it refuses a second begin while a frame is active (counted in malformedFrameCount), records the start timestamp and increments currentFrameNumber. Events therefore carry the number of the frame in which they were emitted, and the frame number advances at begin, not at end. The measured duration ends after the sources have been collected, so a slow source lengthens the frame it reports on.
The frame is one process-wide state, not per Game or per thread. EndFrame passes reportMalformed = true, while the FrameScope destructor passes false. Read together, this means application code that adds its own BeginFrame/EndFrame inside Update gets its begin counted as malformed, and its EndFrame completes Game's frame early; Game's own scope then finds no active frame and ends silently. No test covers this interaction; it follows from reading FinishFrame.
The engine's other publishing sites are ordinary macro or handle calls, all renderer-neutral: the draw entry points of GraphicsDevice.cpp call RecordGraphicsDraw after the renderer call returns, so a rejected or throwing draw is not counted, and the same file counts effect and render-target changes; TextureCollection.cpp counts texture-slot changes; SpriteBatch.cpp counts sprite submissions; both mixer engines, CnaMixer/MixerEngine.cpp and Sdl3Mixer/MixerEngine.cpp, maintain the voice gauge and creation counter. The names are listed in What the engine already measures. For the surrounding frame, see the one-frame source trace.
Ownership and concurrency of records
GetState() allocates one State with new on first use and never deletes it: thread-local context destructors can publish their final events after ordinary namespace statics would already have been destroyed. Its fixed arrays dominate its size; computed from the declared capacities (not measured), that is roughly 0.3 MiB in STATS, mostly the 240-frame ring with room for 64 metric values per frame, and about 2.5 MiB in FULL, where the 32,768-entry event history of 72-byte records is added. Each producer thread in FULL adds a context of roughly 74 KiB. Snapshot::profilerOwnedBytes reports exactly sizeof(State) plus one sizeof(ThreadContext) per live context, so it is a lower bound that ignores heap held by names, maps and resource records.
Lock and thread map
| Lock | Guards | Nesting observed in the source |
|---|---|---|
metricMutex | metric slots' names and signatures, the name-to-id map, the metric count | innermost; taken inside frameMutex by frame completion and by the snapshot's frame copy |
frameCompletionMutex | serialises whole frame completions | outermost; held while sources run and, in FULL, while threadMutex/historyMutex are taken to drain |
frameMutex | frameActive, start timestamp, the 240-slot frame ring | inside frameCompletionMutex; outside metricMutex |
sourceMutex | the source-id → shared_ptr map | never held while a source runs |
resourceMutex | resource records and the saturating byte total | alone; resource events are emitted after it is released |
nameMutex (FULL) | event-name table, capped at 65,536 entries and 16 MiB | alone |
threadMutex (FULL) | the list of live thread contexts | outside historyMutex (drain) |
historyMutex (FULL) | the 32,768-event process ring, sequence counter, overwrite count | innermost of the event path |
Metric updates take no lock: a handle stores a slot id, and AddMetric/SetMetric do one relaxed load of the runtime mode and one relaxed atomic on the slot. Registration takes metricMutex, keys the slot by the UTF-8-sanitised name, and returns the existing id only if kind, unit and accuracy match; a conflicting signature, an empty or oversized name, a registration beyond the 512-metric cap or an exception all yield id 0, an inert handle. Ids are slot index + 1 and are never reused. See the thread and callback map for how these locks sit beside the rest of CNA's threads.
In FULL, each producer thread lazily creates a ThreadContext on its first event or zone and registers it under threadMutex. The context is a single-producer/single-consumer ring of 1,024 events: the owning thread writes a slot only while writeIndex - readIndex < 1024 and publishes with a release store of publishedWriteIndex; any thread that holds threadMutex may drain it, copying events into the process history under historyMutex and advancing readIndex with a release store. The zone stack (64 entries), the correlation counter and the context's mode generation belong to the producer alone. Thread ids come from an atomic counter starting at 1 and are never reused; exhausting the 64-bit space would count a drop rather than wrap. A thread_local holder drains and deletes the context when its thread exits.
Draining happens only at frame completion, in CaptureSnapshot and ReadEvents, in StartRecording/StopRecording and at thread exit. A worker thread that emits more than 1,024 events between two of those points drops the newest ones (producerEventsDropped), while the process history overwrites its oldest entries (eventHistoryOverwrites). The drainer is often not the producer: the Inspector agent's worker thread calls the provider from Agent.cpp, so a FULL snapshot there drains the game thread's ring, and a long history copy under historyMutex can delay the game thread's frame completion. That is why ReadEvents starts at the first unseen sequence instead of scanning the ring.
Sources are owned by the registry: RegisterSource stores a shared_ptr<IDiagnosticsSource> under a fresh id, and the move-only SourceRegistration erases it on Reset or destruction. Completion copies the owners first, so a source may unregister itself from Collect without deadlock and stays alive while it runs. The corollary is that Reset does not wait: a source can be called once more, on the frame-ending thread, after its registration was reset elsewhere. A throwing Collect increments sourceCollectionFailures. No production code registers a source at the snapshot, so this contract is exercised only by tests.
ResourceHandle owns a metadata id, not the resource. Ids are monotonically increasing and never reused; registration returns 0 in runtime Off and on exhaustion; Update replaces the record in place; move construction and assignment transfer the one live id; the byte total saturates at UINT64_MAX and is recomputed from the records once an update or removal happens while saturated. GraphicsResource.cpp registers every GraphicsResource at construction with an empty descriptor (ResourceKind::Unknown, 0 bytes); textures, render targets and buffers then call UpdateDiagnosticResourceEXT with typed metadata built by DiagnosticResource.hpp (estimated payload for textures, exact logical capacity for buffers). State objects and effects therefore appear as Unknown zero-byte records; an assignment that shares a resource identity drops the alias's registration so one identity has one record; Dispose resets the handle. A resource created during a runtime Off window is never registered, even after the mode returns. None of this is a GPU allocation census: compare it with real GraphicsResource and backend lifetime, as described in the ownership master map.
CaptureSnapshot returns owned copies but is not one atomic transaction: in FULL it first drains all rings (a side effect), then copies metrics, frames, resources and counters under separate locks, one after another. Two sections of one snapshot can reflect different instants.
Gauges maintained by deltas drift across a runtime Off window. AddMetric and SetMetric discard updates while the runtime mode is Off, and nothing resynchronises a gauge afterwards. Audio/AllocatedVoices is kept with +1/-1 deltas, so voices created or destroyed while diagnostics were lowered to Off leave it wrong (possibly negative) until mixer-engine shutdown sets it back to 0. OffStatsAndFullTransitionsPreserveMetricSemantics covers a cumulative counter only. Derived from reading the source; not executed.
Zones, recording and export failures
BeginZone requires runtime Full and a registered name (id ≠ 0). It resets the thread's zone stack if the global mode generation has moved on, refuses a 65th nested zone (counted as malformed, inactive token), and pushes an entry whose correlation id is derived from the thread id and a per-thread counter. Correlation ids are unique within a thread; a consumer that joins parent and child should key on the pair of thread id and correlation id. The returned ZoneToken carries correlation, thread id, generation and depth. ZoneScope is the RAII route and the one the macros use. EndZone distinguishes these cases:
| Situation | Result |
|---|---|
inactive token, or runtime mode not Full | returns true; nothing recorded |
| token from an older mode generation | returns true; the zone is silently discarded and the stack reset, with no malformed count |
| other thread, empty stack, or correlation not on the stack (e.g. a double end) | returns false; malformedZoneCount++ and a Diagnostics/MalformedZone event |
| token found on the stack, but not as the top entry at its recorded depth | returns false; the stack is unwound: inner zones are emitted as Zone events ending now, the token's own zone as EventKind::Malformed, and the count is incremented |
| token is the top entry at its recorded depth | returns true; a Zone event with start timestamp and duration |
Zone events are appended when the zone ends, so in sequence order an inner zone precedes its parent. Markers from MarkEvent, the Frame event (correlation = frame number) and the Diagnostics/ResourceCreated/ResourceDestroyed events (value = resource id) go through EmitRawEvent, which adds the thread id and current frame number and counts any exception as a producer drop.
StartRecording(maximumEvents) returns an inactive session unless the runtime mode is Full and the request is non-zero. It is a cursor, not a writer: it drains, stores the last assigned sequence, clamps the capacity to 32,768 and remembers the producer-drop count. Sessions are move-only and independent; two may be open at once. StopRecording drains again, counts the sequences that the history already overwrote since the start, keeps only the newest maximumEvents of the rest (the older excess is also counted), adds the process-wide producer drops since the start, and copies every registered event name into the trace. A mode change during a recording does not end it; events are simply not produced while the mode is below Full (RuntimeModesBeforeAndDuringRecordingFailSafely).
The CNATRACE binary layout is in the user guide. The details that matter when changing it: ReadBinary rejects bad magic, a version other than Trace::FormatVersion (1), truncation, name or event counts above the live bounds, oversized names and out-of-range EventKind/Category bytes, each with std::runtime_error; it re-sanitises every name to valid UTF-8, because a trace file need not come from this process. WriteChromeTrace streams without building a document; it writes a constant "cat":"cna" and "pid":1, uses CNA's diagnostic thread id (not the OS id) as tid, exports zones and frames as "X" and everything else as thread-scoped "i", puts value and frame in args, and truncates ts and dur to whole microseconds. The Category and the correlation ids are not exported to JSON (the viewer nests by time on one tid), and a sub-microsecond zone exports with dur 0 although the binary trace keeps nanoseconds. Both writers return the stream state rather than throwing.
The failure model is deliberately quiet. Handles, BeginZone, EndZone, MarkEvent, BeginFrame/EndFrame, RegisterSource, StartRecording and GetProvider are noexcept and return an invalid id, an inactive token or an empty registration on exhaustion or allocation failure. The provider calls, StopRecording and the Trace members allocate and are not noexcept; ReadBinary throws by design. A missing metric is therefore not proof of a missing code path, and a trace with missing events is not proof that the operations never happened.
A human investigation recipe
- Record the exact CNA build and configure
-DCNA_DIAGNOSTICS=STATSfor frame and counter questions orFULLfor zone and event ordering. The level is a compile definition, so switching needs a reconfigure and a rebuild; a runtime switch cannot upgrade anOFFbinary. - Start from the existing
Game/Tick,Game/UpdateandGame/Drawzones. Remember thatGame/Tickincludes pacing waits and event polling, and that on the web the frame excludes the wait for the next animation frame. For a renderer question, place a name at the neutral-to-native boundary and compare the same build on a second renderer. Keep every instrumentation argument free of side effects. - Take
GetProvider().CaptureSnapshot()for metrics, the recent frames and resource metadata; consumeFULLevents incrementally withReadEvents(lastSequence, max). ReadproducerEventsDropped,eventHistoryOverwrites,eventsDroppedBeforeStart,malformedZoneCount,malformedFrameCountandsourceCollectionFailuresalongside the value being diagnosed. Do not add a privateBeginFrame/EndFramepair inside a runningGame. - For a suspected leak, correlate resource metadata with explicit construct and dispose cycles and with native backend evidence; an Unknown zero-byte record is usually a state object or effect, and a diagnostics id is not proof of native bytes. For timing, the source contract says a GPU source may only publish results that are already available, so any future GPU metric may describe an earlier frame;
Collectmust never stall to make it current. - To look at the same data from outside the process, use the Inspector, which reads this provider on its own thread; its transport is described in Inspector transport internals. General tactics are in the debugging cookbook.
- After adding an instrumented call site, build the
OFF,STATSandFULLvariants at least once, then run the diagnostics tests plus the changed subsystem's tests.OFFmust still compile and must not evaluate the expression;FULLmust keep zone nesting and the bounded history behaviour.
What the diagnostics corpus proves
Three test files cover the module and its producers. Most of each file is behind a level gate, so what a build compiles depends on CNA_DIAGNOSTICS:
| File | OFF | STATS | FULL | Test group (focused target) |
|---|---|---|---|---|
DiagnosticsTests.cpp | 2 | 16 | 40 | diagnostics (CnaDiagnosticsTests) |
GraphicsDiagnosticsTests.cpp | 0 | 6 | 6 | graphics (CnaGraphicsTests) |
AudioDiagnosticsTests.cpp | 0 | 1 | 1 | audio (CnaAudioTests); also needs SOUND_ENABLED |
The counts are static TEST definitions per level, counted from the preprocessor gates; none was executed for this page. What they pin, by suite:
DiagnosticsBuildModeTest(every level): the published build mode equals the compile definition, the provider interface version is 1, a mode above the build is refused, andDisabledMacrosDoNotEvaluateArguments(0 evaluations atOFF, 3 atSTATSandFULL).DiagnosticsMetricsTest,DiagnosticsFrameTest,DiagnosticsResourceTest,DiagnosticsSourceTest,DiagnosticsRuntimeModeTest(STATS+): counter versus gauge semantics and signature conflicts, frame-counter reset, malformed frame transitions, the bounded ordered frame history and its exact FPS, one stable resource id across update and move, byte-total saturation and recovery, snapshots concurrent with a registering thread, sources collected only at valid frame ends,ASourceEndingAFrameFromCollectIsRefusedNotDeadlocked, invalid UTF-8 names stored as U+FFFD.DiagnosticsZonesTest,DiagnosticsEventsTest,DiagnosticsRecordingTest,DiagnosticsTraceTest(FULL): parent/child correlation, malformed manual ends, mode-generation invalidation (including a worker waiting across five mode changes), 256 sequential and 64 concurrent threads with distinct ids, context release at thread exit, per-thread overflow and history overwrite, cursor exactness without phantom gaps, bounded recordings that drop the oldest excess, move-only sessions, binary and Chrome round trips, JSON escaping, rejected malformed input, and 3,000 deterministically mutated binary traces, each of which must be rejected or read back into valid UTF-8.- Graphics: estimated texture bytes and unregistration, the byte-estimate formula and its saturation, one record per shared state identity, no transient registrations from
SpriteBatch::Begin, exact draw, effect, texture-binding, render-target and sprite counters. Audio: the voice gauge and creation counter across two mixer tracks.
Registration: cmake/UnitTests.cmake globs every module's tests/ into CnaTests and groups sources by module. CnaDiagnosticsTests is a focused developer executable, EXCLUDE_FROM_ALL, linking only cna_diagnostics (plus the test build configuration and gtest_main); it is not a CTest registration. CTest discovers the same tests from CnaTests with gtest_discover_tests(... DISCOVERY_MODE PRE_TEST), which runs each discovered test in its own process. Running a focused executable directly runs them all in one process against the one global State, which is why the tests measure against captured baselines and restore the runtime mode with a RuntimeModeGuard; a new test must do the same rather than rely on case order. No CI workflow and no configure preset sets CNA_DIAGNOSTICS, so a CI build compiles only the two level-independent DiagnosticsTests and none of the graphics or audio diagnostics tests; see Tests and CI.
With CNA_BUILD_BENCHMARKS=ON, DiagnosticsBenchmark.cpp builds cna_diagnostics_benchmark. It drains the event history every 512 zones so that the 1,024-event ring never overflows, which is itself a reminder of the draining rule. CNA's recorded results are in docs/diagnostics-benchmark.md; they measure instrumentation primitives on one machine, not a Game frame or GPU work.
Not proven by this corpus: any renderer-specific source (none exists at the snapshot, and every renderer's future diagnostics source remains unverified), the behaviour of a consumer UI such as the Inspector's browser page, and mixed-level builds, which are unsupported rather than tested.
Curated source route
CMakeLists.txt(theCNA_DIAGNOSTICSoption),modules/CMakeLists.txt(cna_build_config) andInstrumentation.hpp: separate compile-time absence from runtime mode before interpreting a missing metric or event.Game.cpp(Tick,EmscriptenMainLoopCallback,RunLoop) andDiagnostics.cpp(TryBeginFrame,FinishFrame,CollectDiagnosticsSources): follow one frame through source collection and publication.Diagnostics.hppfor the handles and the provider, thenState,ThreadContext,DrainContextLockedandProcessProviderinDiagnostics.cpp: identify which ids and owners live for the process, a registration, a resource or one thread, and under which lock.GraphicsResource.cppandDiagnosticResource.hpp: how engine objects become resource records and why the level changes class layout.DiagnosticsTests.cpp: check the capacity and failure contracts before changing ring sizes, trace encoding or lock order, and compare each test's#if CNA_DIAGNOSTICS_LEVELgate with the build being run.docs/diagnostics.md: CNA's own design note, including the rules it sets for an Inspector-style consumer.
Related: the Maintainer Handbook for task-oriented change routes and test architecture for how groups and focused targets are assembled.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Verification tiers: evidence forms, oracle authority and CI reporting — A decoding key for CNA verification claims: claim labels, what each evidence form establishes, which authority decides which question, what CI runs at 009d40f5 and how to report it.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-207: Delta-maintained diagnostics gauges such as Audio/AllocatedVoices drift after a runtime OFF window — GaugeHandle::Add discards deltas while the runtime mode is OFF, so a gauge kept with GAUGE_ADD(+1/-1), such as Audio/AllocatedVoices, stays wrong (too high, or negative) after statistics are re-enabled, until mixer shutd
- CNA-BUG-208: On Emscripten a diagnostics frame excludes the requestAnimationFrame wait, so FrameSample.framesPerSecond is a work rate there — The web loop opens the frame scope inside EmscriptenMainLoopCallback and waits for the next animation frame after it returns, so web frame durations and FPS exclude pacing, unlike the native Tick the documentation descri
- CNA-GAP-060: No renderer or subsystem registers an IDiagnosticsSource, and nothing registers ResourceKind::AudioVoice resources — The diagnostics source seam for GPU timings and renderer statistics has no production implementation and no engine code registers audio-voice resource metadata, so those diagnostics and Inspector views stay empty at this
- CNA-VGAP-051: No CI configuration builds diagnostics at STATS or FULL or enables CNA_BUILD_INSPECTOR, so most Diagnostics tests and every Inspector test are never compiled in CI — CNA_DIAGNOSTICS and CNA_BUILD_INSPECTOR default to OFF and no workflow or preset changes either, so 38 of 40 DiagnosticsTests, the seven audio and graphics diagnostics tests and all 27 Inspector tests run only in local b
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- Architecture
- Runtime lifecycle · Architecture overview
- Internals
- Inspector transport internals (the consumer) · One frame source trace · Textures and render targets
- Maintainer workflow
- Ownership and lifetime master map · Thread and callback map · Debugging cookbook · Maintainer Handbook
- Tests and validation
- Test architecture and change recipes
- Reference
- Module index · CMake option index · Test target index