Skip to content

insomni-node

insomni-node runs the core insomni renderer headless in Node.js — no browser, no canvas, no swap chain. It installs navigator.gpu from the webgpu (Dawn) native package, renders into an offscreen texture, drives frames manually (no requestAnimationFrame), and reads the result back as raw RGBA pixels or an encoded PNG.

It’s the foundation for server-side rendering, headless GPU snapshot tests, and AI/agent rendering harnesses — anywhere you want a real GPU frame without a DOM.

Terminal window
pnpm add insomni-node insomni

typegpu is a peer dependency (shared with insomni). The native Dawn binary ships prebuilt inside the webgpu package — there is no compile step, and on a fresh install you may need pnpm approve-builds to let its postinstall run.

A self-contained script that renders two rounded rectangles and writes a PNG:

import { createNodeRenderer } from "insomni-node";
import { createLayer, rgba } from "insomni";
import { writeFileSync } from "node:fs";
const api = await createNodeRenderer({ width: 256, height: 256 });
api.renderer.setBackground(rgba(0.05, 0.06, 0.09, 1));
// A UI-space layer draws in pixel coordinates (no camera fit needed).
const scene = createLayer({ space: "ui" });
scene.pushRect({
x: 32,
y: 32,
width: 192,
height: 192,
fill: rgba(0.36, 0.7, 1, 1),
cornerRadius: 24,
});
scene.pushRect({
x: 72,
y: 72,
width: 112,
height: 112,
fill: rgba(0.96, 0.45, 0.62, 1),
cornerRadius: 16,
});
await api.frame([scene]); // render + flush the GPU queue
writeFileSync("out.png", await api.toPNG());
api.dispose();

async — installs the GPU provider, acquires an adapter + device, builds an offscreen target, and wires a Renderer2D through it. Resolves to a NodeRenderer facade.

OptionTypeDefaultDescription
widthnumberRequired. Offscreen target width in pixels.
heightnumberRequired. Offscreen target height in pixels.
formatGPUTextureFormatgetPreferredCanvasFormat() (bgra8unorm)Color-target format the pipelines are built against.
sampleCountnumber1MSAA samples. The main pass is single-sample; leave at 1 unless you know you need it.
gpuGPUHandlea fresh initGPU()Reuse a pre-built device handle (e.g. to share one device across renderers).
persistentbooleanfalseAccepted for API symmetry; has no effect offscreen (there is no swap chain to blit into).
MemberTypeDescription
rendererRenderer2DThe core renderer. Use setCamera / setBackground / setDpr and build layers as usual.
targetOffscreenRenderTargetThe owned color texture the frame renders into.
deviceGPUDeviceThe Dawn device.
frame(layers, maxFrames?)Promise<void>Render layers and await the queue. maxFrames (default 1) re-renders to settle async resources (e.g. font atlas uploads).
readPixels()Promise<{ data: Uint8Array; width; height }>Copy the frame back to CPU as tight RGBA (channel-swapped from BGRA when needed).
toPNG(opts?)Promise<Uint8Array>readPixels() → PNG bytes. Unpremultiplies alpha by default ({ premultiplied: false } to skip).
resize(width, height)voidResize the renderer + offscreen target (recreates color/depth + the OIT A-buffer).
dispose()voidDestroy the renderer, target, and (if owned) the GPU device handle.

createNodeRenderer is the batteries-included entry point; the pieces are also exported individually for custom wiring:

ExportDescription
installNodeGPU()Idempotently install navigator.gpu + the GPU* enum globals from the Dawn package onto globalThis.
isNodeGPUInstalled()Whether navigator.gpu is already present.
OffscreenRenderTargetA core RenderTarget that owns a COPY_SRC color texture and makes present() a no-op (see below).
readPixels(device, texture, w, h, fmt)copyTextureToBuffer → mapped, row-unstrided tight RGBA. The texture must have COPY_SRC usage.
toPNG(pixels, opts?)Encode an RGBA pixel result to PNG via fast-png (pure JS, no native deps).
renderSettled(renderer, layers, opts?)The manual frame loop: render layers then await device.queue.onSubmittedWorkDone(). No rAF.

Headless rendering is possible because the core renderer presents through a small RenderTarget seam (insomni/internal). Every frame the renderer calls target.acquireColorView() for its color attachment and target.present(encoder) after encoding:

  • CanvasRenderTarget (the default) wraps the swap chain — acquireColorView() returns getCurrentTexture() (or the persistent backbuffer) and present() performs the backbuffer blit. The canvas/browser path is unchanged.
  • OffscreenRenderTarget (this package) owns a RENDER_ATTACHMENT | COPY_SRC | TEXTURE_BINDING texture, returns its view, and makes present() a no-op — readback happens out-of-band via copyTextureToBuffer.

When an offscreen target is injected, the renderer reads its depth/MSAA/sizing from the target’s context and never touches a canvas, getCurrentTexture(), or configureContext() — which is what makes it run canvas-free in Node.

  • Offscreen only. No swap chain, no on-screen window, no DOM/pointer events.
  • Alpha is premultiplied. readPixels() returns the rendered (premultiplied) bytes; toPNG() unpremultiplies by default so the PNG stores straight alpha.
  • The native addon stays external. webgpu is a runtime dependency, not bundled by the build — dist/ contains no .node binary.
  • Higher-level scene layers are out of scope. This package targets the core renderer only.