Skip to content

Core renderer overview

insomni is an opinionated 2D WebGPU renderer built on TypeGPU. It is tuned for data-heavy scenes: every primitive is packed into one flat typed array, uploaded in a single batch per frame, and drawn with SDF-style pipelines. The public surface is one barrel import:

import { initGPU, createRenderer, createLayer, rgba } from "insomni";
Hello insomni A rounded card and a row of accent dots — a hello-world that traces the full path from GPU device to pixels.

A frame moves through three stages.

  1. Pack (CPU). You author shapes into a Layer. Each pushRect / pushCircle / pushSegment call appends one instance — a fixed 16-float (64-byte) record — into the layer’s UberPack. All shape kinds share one ArrayBuffer; adjacent instances of the same kind, opacity, and group merge into a single DrawCommand.
  2. Upload (GPU). Renderer2D.render(layers) concatenates every layer’s pack into one storage buffer and uploads it once. The buffer grows power-of-2 / 1.25× and is reused frame to frame.
  3. Draw (GPU). The renderer issues the merged draw commands in a single render pass: opaque geometry first (depth write on), then transparent geometry. Depth is the per-instance order field, stamped in global submission order, so painter stacking is explicit — later-submitted draws land on top.
const gpu = await initGPU();
const renderer = createRenderer(gpu, canvas);
const layer = createLayer();
layer.pushRect({ x: 16, y: 16, width: 180, height: 72, fill: rgba(0.06, 0.09, 0.13, 1) });
layer.pushCircle({ cx: 240, cy: 52, radius: 18, fill: rgba(0.2, 0.7, 1, 0.9) });
function frame() {
renderer.render([layer]);
requestAnimationFrame(frame);
}
frame();

Ordinary painter-ordered scene content lives in a Layer. It wraps one UberPack, a coordinate space, an optional default Group, and metadata such as zIndex, cache, visible, and label. Text and sprites ride on the same layer through dedicated sibling pipelines. Specialized values such as CustomDrawable are phase hooks, not layers.

This ordering applies to ordinary Layers. Specialized BufferLayer and CustomDrawable values use documented renderer phases; custom drawables choose placement: "underlay" | "overlay".

render([a, b]) draws a first, then b. Layers are flat z-bands: layer b is entirely above layer a (painter’s order), with no per-shape interleave across layers. To interleave shapes, put them in the same layer (intra- layer overlap resolves via transparency and depth). To override insertion order without reordering the array, set Layer.zIndex.

By default a layer is dynamic: its pack is re-concatenated and re-uploaded every frame. For content that rarely changes, two retention tiers skip that work:

  • Retained buffersrenderer.retainLayer(layer, key) hands the layer’s pack a stable GPU buffer. While key (and pack.version) are unchanged the layer draws with zero re-upload. See Packing and Renderer.
  • Cached RTT bakesrenderer.cacheLayer(layer) (or the cache: "always" / "auto" hint) rasterizes the layer once into a texture and composites the snapshot instead of redrawing it.

Combined with damage-tracked partial redraw, this lets a busy scene re-rasterize only what actually moved.