BLIT386BLIT386

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 titled BLIT386 Demo NNN - Topic show as Topic 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-frame update() vs render() CPU time (one column per present frame, not per fixed update() tick).
  • Band height defaults to 22px; override with overlayTimingChartHeight.
  • Dot height scales linearly so about 16ms fills 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, and 33.33 ms plus a frame-budget line at 1000 / targetFPS ms are drawn behind the dots (qualitative scale on a ~22px band, 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 (then barPaletteIndex); override with overlayTimingChartStyle.gridPaletteIndex.
  • The band background uses overlayStyle.barPaletteIndex; dot colors default to overlayStyle.barPaletteIndex / textPaletteIndex; override with overlayTimingChartStyle.

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 Frame wall time against 1000 / targetFPS (warning at 1.10x budget, error at 1.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, and 5 when not overridden.
  • Drop detection for the chart runs whenever isOverlayTimingChartEnabled: true (independent of isDetectingDroppedFrames, which only controls console warnings).

Event tags

  • Call BT.assignTag(label?) from update() or init() to anchor a short label on the scrolling chart at the current BT.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 in tags.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), not BT.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 (default 5).

Renderer diagnostics (optional)

  • When the chart is enabled, overlayTimingChartDiagnostics defaults to 'minimal' (set false to 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: true for 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 not false or the diagnostics bar is enabled.
  • Overflow counts apply on WebGPU; the software backend reports ov: 0 with 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 an N/M voices | steal X | drop Y text readout.
  • Band height defaults to 13px; override with overlayAudioMeterHeight.
  • Enabling this flag also lazily creates the AnalyserNodes backing the bus level readings; no metering cost is paid while the flag is false.
  • Each bar tracks its bus level (0-1, from AudioManager.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 to overlayStyle.gapPaletteIndex (then barPaletteIndex); override with overlayAudioMeterStyle.
  • A bar switches to the warning tint at 70% of full scale and the clip tint at 95%; defaults 3 and 4, override with overlayAudioMeterStyle.warningPaletteIndex / clipPaletteIndex.
  • drop sums 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 (shows xN on update() when multiple fixed updates ran this frame)
  • N is the number of fixed update() steps the accumulator drained for that render frame - normally 1; higher when render() falls behind targetFPS (a throttled background tab, a slow device, or a power-saving cap on requestAnimationFrame). Game logic stays time-correct either way; see Multiple update() steps per render frame in Game Loop.

Bottom band

  • Default 13px hint bar with a bottom-left bitmap toggle icon (over the toggle hit region).
  • Set isOverlayPaletteEnabled: true to stack a live palette swatch grid above a 1px filled 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 overlayPaletteRowsVisible is set, only that many rows are shown in a scrollable viewport with a right-side scrollbar thumb (proportional to visible vs. total rows, minimum 4px, inset 1px from the band top, right, and bottom); wheel input over the palette band scrolls rows and does not reach BT.pointerScrollDelta outside that region. The engine forces wheel capture while the pointer is over that band even when isCapturingPointerScroll is false, so the host page does not scroll under the palette.
  • The timing chart and palette grid bands use the same overlayStyle.barPaletteIndex fill as the other overlay rows (bars draw first; chart dots and swatches render on top).
  • 1px row gaps between stacked overlay bands and cluster separators (below the top metrics cluster and above the bottom footer cluster) are filled with overlayStyle.gapPaletteIndex, defaulting to overlayStyle.barPaletteIndex when omitted.
  • Palette usage tracking (sprite and bitmap-text pixel scans) runs only when the overlay is enabled, isOverlayPaletteEnabled is 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, 1px filled gaps apart, each with left text and optional right text (same 13px bar style as the built-in rows)
  • Demos may implement optional overlayRows() on IBTDemo.
  • 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 undefined or an empty array when no custom rows are needed.
  • Reuse the same array and row objects when possible; update leftText / rightText in 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 at isOverlayVisibleAtStart (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.
  • gapPaletteIndex fills inter-band row gaps, cluster separators, and the 1px vertical dividers between segments of the engine-composed rows (backend/resolution, frame metrics, timing, renderer diagnostics, and the audio meter readout); when omitted it matches barPaletteIndex.

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(), and render() in the text row are smoothed CPU wall-time samples from performance.now().
  • The optional timing chart uses raw per-frame updateMs / renderMs from the prior frame.
  • The update() / render() figures exclude overlay draw (the overlay runs after render() is timed); Frame includes the full present frame, overlay and GPU present included.
  • Draw Calls counts 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 14px above the bottom band (13px bar + 1px filled gap).
  • Top of screen: about 43px clear (three built-in text rows + filled gaps + separator below the top cluster).
    • Add overlayTimingChartHeight (or 22px) when isOverlayTimingChartEnabled: true.
    • Add 14px when isOverlayRendererDiagnosticsBarEnabled: true.
    • Add overlayAudioMeterHeight + 1px (or 14px) when isOverlayAudioMetersEnabled: true.
  • Bottom of screen: about 14px clear by default (1px separator + 13px hint bar).
  • With the palette grid (isOverlayPaletteEnabled: true): add the grid height, its 1px filled row gap, and the 13px hint bar - for example about 83px on the default 320×240 layout with a 256-slot palette (32 columns × 8 rows of 7px swatches with 1px gaps, 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 7px with 1px gaps and 3px padding above and below the grid.
  • When overlayPaletteRowsVisible is unset, visibleRows equals rows (the full palette).
  • Wheel scrolling applies only while the pointer is over the palette footer band (grid or scrollbar); demos reading BT.pointerScrollDelta elsewhere are unaffected. Palette-band hover forces wheel capture without requiring isCapturingPointerScroll.
  • Set isOverlayPaletteEnabled: true to enable the grid, or isOverlayEnabled: false for 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

SymbolSinceLast changedStatus
OverlayAudioMeterStyle1.3.0–stable
OverlayRow1.1.0–stable
OverlayStyle1.1.0–stable
OverlayTimingChartStyle1.1.0–stable

1.3.0 - Mon Jul 13 2026

  • AddedOverlayAudioMeterStyle

1.1.0 - Sat May 30 2026

  • AddedOverlayRow
  • AddedOverlayStyle
  • AddedOverlayTimingChartStyle

See also

Last updated on October 18, 2026

On this page