Rendering · guide
Diagnostic overlays and the console
One flag puts the frame-stats panel, the console, the log tail and every subsystem's debug lines on the screen of a running game.
Edit this page on GitHubDocuments
What it is
The panels docs/plan/13 § Diagnostic overlays asks for — frame stats with a
frame-time graph, a mini flame chart off the profiler's rings, the tail of the log ring, and a
console — plus everything any subsystem has drawn into DebugDraw, all on the screen of a running
game. One switch turns the lot on:
./MyGame --vixen-overlaysor, for a development build that always wants them:
config.Graphics.Overlays = true;Press the backtick key for the console. Type overlays to list the panels and
overlay <name> to switch one on.
What it is for
Everything in the list above was written, tested and reachable from nowhere. DiagnosticOverlaySystem
was constructed by its own tests and by nothing else in the tree; no compositor node drew a frame's
DebugDraw; and the only line shader a build could reach lived under Editor/, so a game could not
construct the renderer that drains the accumulator even if it wanted to. Physics, navigation,
animation, the AI stack and water's six water.show* verbs had all been writing into a list nothing
read.
That is this engine's commonest defect — a finished consumer that nothing feeds — and what closes it is three joins, none of them a new abstraction:
| Join | Where |
|---|---|
| The instrument is where a game can reference it | Vixen.Engine, already |
| The framework emits, not the caller | SceneRenderHost.Load appends the node to every document it builds |
| The host owns the switch, and it is off by default | GraphicsOptions.Overlays, --vixen-overlays |
The same three that fixed the GPU profiler; see timing the frame.
Using it
AppGraphics builds exactly one of each and exposes them, because the way this feature fails when
two exist is an empty screen with every counter reading as though it had worked:
| Property | Is |
|---|---|
Graphics.Debug | the DebugDraw every subsystem writes into |
Graphics.Overlays | the panel registry |
Graphics.Console | the command registry |
A game draws its own geometry straight into the accumulator, from anywhere, with nothing to create and nothing to dispose:
services.Graphics?.Debug?.Arrow(muzzle, muzzle + aim * 5f, new(1f, 0.3f, 0.2f, 1f), seconds: 1f);Which panels are already there
--vixen-overlays registers seven. Only stats starts switched on; the rest are asked for by name,
and overlays lists them.
overlay <name> | Shows |
|---|---|
stats | fps, the CPU/GPU split, draws, triangles, visible objects, memory, and a frame-time graph |
framegraph | a mini flame chart of the last frame off the profiler's rings |
gpu | where the frame's GPU time went, pass by pass — needs --vixen-gpu-profile |
water | the zone fold: zones, bodies, and the three silent failures that draw a convincing wrong lake |
watermesh | the surface node: zones drawn, patches selected and dropped, vertices, draws |
streaming | what is resident against the budget, what is loading or queued, loads and evictions, meshes — and refusals |
console | the command prompt |
log is added by AppBuilder, which owns the ring it reads. audio is not registered by the
host — nothing in AppGraphics owns an AudioEngine — so a game that opens a device registers it.
physics is the same shape, for the same reason, and is one line:
PhysicsOverlay = loop.AddPhysicsOverlay(Physics, graphics.Debug!, graphics.Overlays);overlay physics | Shows |
|---|---|
| the wireframes | every body's collision volume, the contact points and normals from the last step, and each constraint's anchors and error, drawn in world space |
| the panel | bodies awake against bodies total, constraints, and contacts against steps |
⚠ The colour is the state: grey is asleep, dark green static, blue kinematic, bright green awake and dynamic, yellow a sensor, red a contact. "The crate has gone to sleep" and "the crate is static" are otherwise identical in every respect, and they are two different bugs.
⚠ The awake count and the step count are on the panel because the wireframes cannot tell you. A scene whose bodies are all asleep draws exactly the same lines in exactly the same places as a scene nothing is stepping at all.
⚠ A mesh or a convex hull is drawn as its bounding box. Wireframing a hundred-thousand-triangle
level would produce more lines than the debug renderer holds and tell nobody anything; the bounds
answer the question actually being asked, which is whether the body is where you think it is.
PhysicsDebugOverlay narrows what is drawn — Colliders, Contacts, Bounds, Axes,
Constraints — and the default is the first, second and last.
⚠ streaming's refused row is the one worth opening the panel for. It is
PageResidency.Rejections: a request dropped because nothing could be evicted to make room, which
means the pool is too small for this scene rather than that anything is broken. Its only other
symptom is a frame that samples a coarser texture than it asked for and looks very slightly soft —
so unless somebody is watching this number, the fix is a mip bias nobody knew they needed.
⚠ And a panel of dashes there is not a panel of zeroes. Texture streaming is only stood up where the target has a bindless table to put the textures in, so "no streamer" and "a streamer that has loaded nothing" are different answers and the panel says which. The mesh row is unconditional and is reported either way.
⚠ A headless run has nobody to type overlay gpu at, and that is the run a picture comes from.
--vixen-overlay gpu,water,watermesh,audio switches them on by name and implies --vixen-overlays:
./MyGame --vixen-headless --vixen-frames 240 --vixen-overlay gpu,water --vixen-gpu-profile \ --vixen-capture ./shotsA name whose panel does not exist yet is kept rather than refused, and applied when it is registered
— which is what makes audio work when the game adds it from OnInitialise, long after the command
line was read.
Standing notices, which are not panels
A notice is one line about the build itself, drawn by DiagnosticOverlays.Draw rather than by
any overlay:
overlays.Notice("content", "CONTENT LOOSE");overlays.Forget("content");⚠ The point of a notice is that a panel cannot carry it. Every overlay can be taken off the
screen — overlay <name> off at the console, Remove, DisableAll, and Enabled = false written
straight onto an overlay somebody is holding, which the registry never sees at all — so a standing
statement about the build that lives on a panel is a statement any of those erases, silently. That is
what happened to the --vixen-loose-content stamp
(#919), which is why the one notice in the engine
today is that one.
A notice is still inside Enabled, the registry's own master switch. Hiding a panel must not take a
notice with it; asking for no diagnostics at all is a different request, and there is no honest way to
draw on a surface nobody is drawing. Notices are drawn at the top of the top-left stack, above
whatever is pinned there, and NoticesDrawn says how many reached the screen.
The GPU panel
GpuOverlay draws one bar per render-graph pass, and it is built to be watched rather than
sampled. Three things follow from that and each is deliberate:
* Rows are in the frame's declaration order, not in cost order. A table whose rows swap places
cannot be read while the camera is moving. Cost decides which twelve passes get a row; the graph
decides where each sits.
* Each bar carries a peak that decays over three seconds. A GPU reading moves by more than ten
percent between two frames of a still camera, so the bar is smoothed — and the tick is the worst
that pass has been recently, which is what catches a spike you were not watching for.
* Two rows exist so the panel cannot lie by omission. unattributed is the frame span the
level-zero passes do not fill, which is GPU work happening outside any pass the graph ran.
dropped appears only when the scope pool overflowed, which makes a timeline stop partway through
a frame while every bar that is there still looks right.
⚠ Without --vixen-gpu-profile the panel says so rather than drawing zeroes, because an empty
breakdown because nothing measured looks exactly like an empty breakdown because the frame was free.
Timestamps stay off by default for a reason that is not caution: on tile-based hardware a query write
can force a tile resolve and change the timings it reports.
Adding your own
Adding a panel is one call, and the panel belongs to whoever has the numbers — which is what
IDiagnosticOverlay is for:
services.Graphics?.Overlays?.Add(new AudioOverlay(audio));⚠ Check that the numbers behind it are real before you register it. A panel wired to a counter
nobody has validated makes that counter authoritative. Samples/13 registered audio and the
registration is what found that the sample never called AudioEngine.Update — the call that returns
a finished voice to the pool and the call that assembles AudioStatistics. The panel would have
drawn load 0 %, voices 0/0 and no faults over a game whose audio had stopped working after
sixty-four sounds.
⚠ A subsystem's console verbs should arrive on their own. [ConsoleCommand] alone has never made
a verb typable — the only thing that could find an attributed method is
ConsoleCommands.RegisterFrom(Assembly), which is RequiresUnreferencedCode and had no callers, so
for a long time water's six were the only console verbs in the whole engine. The trim-safe seam is
a module initialiser beside the verbs:
[ModuleInitializer]internal static void Register() => ConsoleCommands.Contribute(commands => MyDebug.Register(commands));A contribution reaches consoles built before it as well as after, so it does not matter when the assembly is first touched.
⚠ The console reads no keyboard of its own. Type, Backspace, Submit and the history moves
are pushed in by the host, because which device produces a character — and whether an IME is
involved — is a platform's question. VixenApplication answers it: characters come from
PlatformEventKind.TextInput and never from key codes, so the console types the right letters on a
non-US layout, and every key is swallowed while the panel is open so that typing reload does not
also make the player reload.
⚠ Both Key.Grave and Key.NonUsBackslash open it, and the second is not a courtesy. A scancode
is a position on a board, not a character: the key that types a backtick is below <kbd>Esc</kbd> on an
ANSI keyboard and beside left shift on an ISO one. Checking Grave alone opens the console for
nobody in Europe — which is exactly how the first run of this on a real machine reported "the key
does nothing" with every count reading correct.
⚠ Text input is started only where the platform says it is off. ITextInput is documented as
off by default; on SDL desktop it is already running and the characters arrive with nothing asked
for. The host therefore checks IsActive first, and stops only what it started — a browser canvas
or a phone, which is what the interface was drawn for, still gets its Activate.
⚠ Ageing happens after the frame is recorded, and it is not a system. DebugDrawSystem ages the
accumulator in SystemPhase.PostRender, which is after the drain only if the drain is itself a
system. VixenApplication runs every phase of EngineLoop.Frame and records the GPU frame
afterwards, so that system in that loop would delete each frame's lines one call before anything
drew them. AppGraphics.AdvanceDebug is the host's call instead, and it is already wired.
Examples
Where the geometry is drawn. DebugOverlayRenderer declares a pass of its own over the frame's
last colour target, loading rather than clearing, so it lands after tone mapping and the screen
chain. Drawing it as a child of the final !RenderPass would put the panels through FXAA and the
grade. Nothing has to be added to a .vxcompositor for this — SceneRenderHost.Load appends the
node to whatever document it just built, including on a reload, so a project that authors its own
frame gets the overlays without declaring a node it never asked for.
Untested against depth, deliberately. By the frame's last colour target the scene's depth may have been aliased away by the graph, and an overlay that could be occluded is one that disappears exactly when something has gone wrong in front of it. World lines are therefore drawn over everything. A viewport that wants them hidden by geometry draws them from inside the scene pass, the way the editor's does.
A line shader a game can reach. LineShaders.Default(device) creates the two pre-compiled
modules embedded in Vixen.Rendering — the same bytes the golden debug-world and debug-overlay
images are rendered with. A project with its own Raven line stage builds LineShaders itself and
never calls it.
var shaders = LineShaders.Default(device);var node = new DebugOverlayRenderer(device, shaders, draw, view) { Target = "SceneColour" };renderer.Host.Debug = node;What the panel's numbers mean. Frame time is graphed and not only printed, because a mean of
16.7 ms and a mean of 16.7 ms with a 60 ms spike every second are the same number and completely
different games. The GPU figure is several frames old and says by how much; a dash means nobody
measured, which needs --vixen-gpu-profile.
See also
* Timing the frame — the GPU profiler behind the panel's gpu row, and the
same three joins that made it reachable.
* Reading the frame — what the counters on the panel are counting.
* Booting an application — where --vixen-* flags are parsed
and why a game's OnConfigure out-votes them.