Overlay
Engine overlay HUD configure flags, style objects, and worked examples.
Configure-time flags, style objects, and worked examples for the engine overlay HUD. For the higher-level subsystem
walkthrough (what the overlay is, when to use it), see the Overlay Guide. The overlay flags listed
here are part of HardwareSettings.
When isOverlayEnabled is true (default), the engine draws a screen-space HUD after each demo render() call, on top
of all demo content. The overlay body (title, metrics, timing chart, palette grid, custom rows) starts hidden unless
isOverlayVisibleAtStart: true.
While the body is hidden, the engine may still draw the toggle hint icon when isOverlayToggleHintVisible is true
(default), without the footer hint bar background. Bar bands and text anchors for the full body are computed each frame
by the internal layout planner in src/overlay/layout/layoutPlan.ts from displaySize, custom row count, and optional
feature flags (timing chart default off; palette grid opt-in via isOverlayPaletteEnabled).
Init still caches stable values such as the bottom-left toggle rect and text baselines from the system font metrics.
Migration note
Upgrading demos now starts with the overlay body hidden. Teaching demos that relied on always-visible metrics should opt
back in with isOverlayVisibleAtStart: true in configure() until authors choose otherwise.
Anatomy
The overlay body stacks, top to bottom: title and backend rows, an optional timing chart, the frame-metrics rows, any custom rows, and the bottom band (palette grid plus hint bar).
The | separators shown in the engine row formats below (backend/resolution, frame metrics, timing, renderer
diagnostics, and the audio meter readout) do not render as text glyphs: each one draws as a 1px full-row-height
vertical divider filled with overlayStyle.gapPaletteIndex - the same color as the row gaps between bands - with 7px
of space between the line and the text on each side. Overlay text and chrome keep the same 7px inset from the left and
right screen edges. Custom rows are user content, so a | in their text stays a literal glyph.
Top row 1 (left)
- Short demo title derived from
document.title(registry pages titledBLIT386 Demo NNN - Topicshow asTopic Demo).
Top row 1 (right)
- Active backend and logical resolution (for example
webgpu | 320x240).
Timing chart (optional)
- When
isOverlayTimingChartEnabled: true, a scrolling band of one-pixel dots shows raw per-frameupdate()vsrender()CPU time (one column per present frame, not per fixedupdate()tick). - Band height defaults to
22px; override withoverlayTimingChartHeight. - Dot height scales linearly so about
16msfills the band; any non-zero sample draws at least one pixel (sub-millisecond work shows as a baseline dot).
Grid lines
- Faint horizontal reference rows at
5,10, and33.33 msplus a frame-budget line at1000 / targetFPSms are drawn behind the dots (qualitative scale on a~22pxband, not a precision ruler). - The band bottom row is the zero baseline (sub-ms / light samples); top- and bottom-edge rows are not drawn as grid lines.
- Grid color defaults to
overlayStyle.gapPaletteIndex(thenbarPaletteIndex); override withoverlayTimingChartStyle.gridPaletteIndex. - The band background uses
overlayStyle.barPaletteIndex; dot colors default tooverlayStyle.barPaletteIndex/textPaletteIndex; override withoverlayTimingChartStyle.
Semantic tints
- When a column is classified as a runtime risk, both update and render dots use the warning or error palette index instead of the normal bar colors.
- Classification uses the prior frame's
Framewall time against1000 / targetFPS(warning at1.10xbudget, error at1.50x) and dropped-frame events from the game loop (one dropped frame = warning, two or more = error; error wins when both apply). - Default warning/error/event palette indices are
3,4, and5when not overridden. - Drop detection for the chart runs whenever
isOverlayTimingChartEnabled: true(independent ofisDetectingDroppedFrames, which only controls console warnings).
Event tags
- Call
BT.assignTag(label?)fromupdate()orinit()to anchor a short label on the scrolling chart at the currentBT.ticks. - Empty or omitted labels become
"Untitled". - Each tag column draws a tick row (relative ticks since chart reset, capped at
100000) and stacked label rows above the chart band; spacing is fixed intags.ts(TIMING_CHART_TAG_LABEL_Y_GAP,TIMING_CHART_TAG_LABEL_STACK_SPACING). - Tick and label text share the same horizontal offset from the timing column.
- A faint vertical grid-colored line marks the column on the chart band.
- Tag columns follow timing
samples(one per rendered frame), notBT.ticks, which can advance multiple fixed-update steps per frame: they fill from the left while the ring buffer is not full, then scroll left one column per sample once full, in lockstep with the dots. - Tags are removed when they scroll one full chart width past the left edge (text keeps drawing at negative x until the display clips it); pruning runs on each timing sample while the chart is enabled.
- Chart width resets (for example on resize) clear tags and add an automatic
"Start"marker. - Tint tags with
overlayTimingChartStyle.tagPaletteIndex(default5).
Renderer diagnostics (optional)
- When the chart is enabled,
overlayTimingChartDiagnosticsdefaults to'minimal'(setfalseto disable chart markers). - Minimal mode bumps semantic severity when either pipeline overflowed that frame and draws a baseline overflow marker
(palette index from
overlayTimingChartStyle.overflowPaletteIndex, defaulting to the warning index). 'rich'adds per-column vertex-pressure dots in the lower third of the band (primitive vs. sprite submitted counts scaled to the 50k vertex cap).- Set
isOverlayRendererDiagnosticsBarEnabled: truefor a dedicated text row (Prim Nv ov: X | Spr Nv ov: X) below the frame timing line; collection runs when either the chart diagnostics mode is notfalseor the diagnostics bar is enabled. - Overflow counts apply on WebGPU; the software backend reports
ov: 0with GPU-equivalent vertex totals for primitive and sprite work.
Audio meters (optional)
OverlayAudioMeterStyle Since 1.3.0
- When
isOverlayAudioMetersEnabled: true, a band below the renderer diagnostics row (or the frame timing row when diagnostics are disabled) shows three fixed-width bus level bars (main,music,sfx) plus anN/M voices | steal X | drop Ytext readout. - Band height defaults to
13px; override withoverlayAudioMeterHeight. - Enabling this flag also lazily creates the
AnalyserNodes backing the bus level readings; no metering cost is paid while the flag isfalse. - Each bar tracks its bus level (
0-1, fromAudioManager.getBusLevels()) as a bottom-anchored fill; a track rect always draws so a quiet bus still shows as an outline. - Fill color defaults to
overlayStyle.textPaletteIndex; track color defaults tooverlayStyle.gapPaletteIndex(thenbarPaletteIndex); override withoverlayAudioMeterStyle. - A bar switches to the warning tint at
70%of full scale and the clip tint at95%; defaults3and4, override withoverlayAudioMeterStyle.warningPaletteIndex/clipPaletteIndex. dropsums pool-exhaustion drops (no free or stealable voice slot) and pre-unlock drops (SFX requested before the audio context unlocked).
Top row 2 (left)
Present: N FPS | Target: T FPS | Draw Calls: C
Top row 3 (left)
Frame: Xms | update(): Yms | render(): Zms(showsxNonupdate()when multiple fixed updates ran this frame)Nis the number of fixedupdate()steps the accumulator drained for that render frame - normally1; higher whenrender()falls behindtargetFPS(a throttled background tab, a slow device, or a power-saving cap onrequestAnimationFrame). Game logic stays time-correct either way; see Multiple update() steps per render frame in Game Loop.
Bottom band
- Default
13pxhint bar with a bottom-left bitmap toggle icon (over the toggle hit region). - Set
isOverlayPaletteEnabled: trueto stack a live palette swatch grid above a1pxfilled gap and that hint bar; slots referenced by demo draw calls this frame are filled with their color, and unused slots render as empty squares with a small centered marker. - When
overlayPaletteRowsVisibleis set, only that many rows are shown in a scrollable viewport with a right-side scrollbar thumb (proportional to visible vs. total rows, minimum4px, inset1pxfrom the band top, right, and bottom); wheel input over the palette band scrolls rows and does not reachBT.pointerScrollDeltaoutside that region. The engine forces wheel capture while the pointer is over that band even whenisCapturingPointerScrollisfalse, so the host page does not scroll under the palette. - The timing chart and palette grid bands use the same
overlayStyle.barPaletteIndexfill as the other overlay rows (bars draw first; chart dots and swatches render on top). 1pxrow gaps between stacked overlay bands and cluster separators (below the top metrics cluster and above the bottom footer cluster) are filled withoverlayStyle.gapPaletteIndex, defaulting tooverlayStyle.barPaletteIndexwhen omitted.- Palette usage tracking (sprite and bitmap-text pixel scans) runs only when the overlay is enabled,
isOverlayPaletteEnabledis true, and the overlay body is visible (not hidden with Backquote or the corner toggle). Default demos do not pay that scanning cost while the overlay is hidden.
Custom rows (optional)
OverlayRow Since 1.1.0
- Extra bars from
overlayRows()stacked above the bottom band,1pxfilled gaps apart, each with left text and optional right text (same13pxbar style as the built-in rows) - Demos may implement optional
overlayRows()onIBTDemo. - The engine calls it once per render frame after
render()when the overlay is enabled and the body is visible (not hidden with Backquote or the corner toggle). - Return
undefinedor an empty array when no custom rows are needed. - Reuse the same array and row objects when possible; update
leftText/rightTextin place to avoid per-frame allocations.
/** @implements {IBTDemo} */
class {
readonly #overlayRows = [{ : 'Position: 0, 0' }, { : 'Score: 0', : 'ready' }];
= { : 0, : 0 };
= 0;
() {
this.#overlayRows[0]. = `Position: ${this..}, ${this..}`;
this.#overlayRows[1]. = `Score: ${this.}`;
return this.#overlayRows;
}
}Toggling visibility
Toggle the overlay body at runtime with backquote/tilde (~) or a primary pointer press in the bottom-left 17×13px
corner, as long as isOverlayToggleEnabled is true (default). These flags shape that behavior:
isOverlayToggleHintVisible: false- hide the hint icon while the body stays hidden.isOverlayToggleEnabled: false- lock body visibility atisOverlayVisibleAtStart(no key or corner input).isOverlayToggleHitDebugVisible: true- draw a 1 px outline of the bottom-left toggle hit region (opt-in; useful when tuning the corner size).isOverlayEnabled: false- disable the overlay subsystem and all toggle input entirely (for example release builds).
On WebGPU, the engine draws the HUD after your render() call, composited above demo sprites via internal overlay draw
batches (not exposed on BT).
Overlay colors
OverlayStyle Since 1.1.0
Colors follow one path: use overlayStyle when set, otherwise the defaults 1 (bar and gap) and 2 (text).
- Globally in
configure():overlayStyle: { barPaletteIndex, textPaletteIndex, gapPaletteIndex }. - Per custom row on
OverlayRow:barPaletteIndex,textPaletteIndex. gapPaletteIndexfills inter-band row gaps, cluster separators, and the1pxvertical dividers between segments of the engine-composed rows (backend/resolution, frame metrics, timing, renderer diagnostics, and the audio meter readout); when omitted it matchesbarPaletteIndex.
Present FPS vs. target FPS
The overlay label Present: N FPS is not the same as BT.targetFPS:
- Present FPS reflects how often
render()runs (browser refresh rate). It is sampled only while the overlay body is visible. - Target is the fixed
update()rate (BT.targetFPS).
How the timing numbers are sourced:
Frame,update(), andrender()in the text row are smoothed CPU wall-time samples fromperformance.now().- The optional timing chart uses raw per-frame
updateMs/renderMsfrom the prior frame. - The
update()/render()figures exclude overlay draw (the overlay runs afterrender()is timed);Frameincludes the full present frame, overlay and GPU present included. Draw Callscounts demo-issued draw API calls during the rendered frame.
While the overlay body is hidden, chart history still records demo update() / render() samples, the toggle hint icon
may still draw, and palette usage tracking is off.
Do not use present FPS for simulation timing
Use BT.ticks, BT.deltaSeconds, or Timer instead - see Game Loop. Present FPS varies with the
display refresh rate and overlay visibility.
Reserving layout space
Demos should not duplicate engine overlay text; the overlay provides it. Leave room for the bands the overlay draws:
- Per custom row: about
14pxabove the bottom band (13pxbar +1pxfilled gap). - Top of screen: about
43pxclear (three built-in text rows + filled gaps + separator below the top cluster).- Add
overlayTimingChartHeight(or22px) whenisOverlayTimingChartEnabled: true. - Add
14pxwhenisOverlayRendererDiagnosticsBarEnabled: true. - Add
overlayAudioMeterHeight + 1px(or14px) whenisOverlayAudioMetersEnabled: true.
- Add
- Bottom of screen: about
14pxclear by default (1pxseparator +13pxhint bar). - With the palette grid (
isOverlayPaletteEnabled: true): add the grid height, its1pxfilled row gap, and the13pxhint bar - for example about83pxon the default320×240layout with a 256-slot palette (32 columns × 8 rows of7pxswatches with1pxgaps, plus the gap and hint bar).
Palette grid sizing
Column count is chosen by halving from palette.size until the row fits displayWidth - 2 * edgeMargin. The footer
band height matches resolveOverlayFooterHeight() in layoutPlan.ts:
cols = pickGridColumnCount(displayWidth, swatchSize, gap, palette.size, maxColumns?)
rows = ceil(palette.size / cols)
visibleRows = resolveGridVisibleRows(rows, overlayPaletteRowsVisible)
paletteGridHeight = gridRowStackHeight(visibleRows, swatchSize, gap) + 2 * PALETTE_GRID_PADDING_PX
bottomReserve = paletteGridHeight + 1 + 13- Default swatch size is
7pxwith1pxgaps and3pxpadding above and below the grid. - When
overlayPaletteRowsVisibleis unset,visibleRowsequalsrows(the full palette). - Wheel scrolling applies only while the pointer is over the palette footer band (grid or scrollbar); demos reading
BT.pointerScrollDeltaelsewhere are unaffected. Palette-band hover forces wheel capture without requiringisCapturingPointerScroll. - Set
isOverlayPaletteEnabled: trueto enable the grid, orisOverlayEnabled: falsefor full-screen layouts (for example terminal-style demos). - Implementation lives in
src/overlay/palette/PaletteView.ts(computeGrid,pickGridColumnCount,resolveGridVisibleRows).
// Overlay off (release build or custom full-screen HUD).
function (): <HardwareSettings> {
return {
: new (320, 240),
: false,
};
}OverlayTimingChartStyle Since 1.1.0
// Overlay on with palette grid and timing chart.
function (): <HardwareSettings> {
return {
: new (320, 240),
: true,
: true,
: 3,
: true,
: 32, // optional; default 22
: 'rich', // optional; default 'minimal' when chart enabled
: true, // optional; default false
: true, // optional; default false
: 13, // optional; default 13
: { : 2, : 3, : 2 },
: {
: 2, // defaults to barPaletteIndex when omitted
: 3, // defaults to textPaletteIndex when omitted
: 2, // horizontal grid lines; defaults to gapPaletteIndex
: 3, // soft over-budget / single drop; default 3
: 4, // hard over-budget / 2+ drops; default 4
: 5, // tag and tick text; default 5
: 3, // overflow baseline marker; defaults to warningPaletteIndex
},
: {
: 3, // defaults to textPaletteIndex when omitted
: 2, // defaults to gapPaletteIndex when omitted
: 3, // bus level >= 70% of full scale; default 3
: 4, // bus level >= 95% of full scale; default 4
},
};
}API history
| Symbol | Since | Last changed | Status |
|---|---|---|---|
OverlayAudioMeterStyle | 1.3.0 | – | stable |
OverlayRow | 1.1.0 | – | stable |
OverlayStyle | 1.1.0 | – | stable |
OverlayTimingChartStyle | 1.1.0 | – | stable |
1.3.0 - Mon Jul 13 2026
- Added
OverlayAudioMeterStyle
1.1.0 - Sat May 30 2026
- Added
OverlayRow - Added
OverlayStyle - Added
OverlayTimingChartStyle
See also
Overlay Guide
API: Core
Game Loop
API: Core Types
Last updated on October 18, 2026