Site navigation

Rendering Pipeline

Varve's rendering pipeline converts a scene graph into pixels on screen through a compact Intermediate Representation (IR). Responsiveness depends on the document, renderer, display cadence, and device; measured examples are environment-specific evidence, not a frame-rate promise.

Architecture Decision: IR-Replay

The core rendering decision (ADR-0001) is that the Rust engine computes the scene and emits a compact IR, while the webview normally replays it through Canvas2D. An optional WebGPU compositor draws a limited set of simple shapes on supported webviews. The engine does not send full-frame pixels across the native bridge.

The IR keeps the engine-to-webview seam compact and makes replay deterministic. Throughput and input-to-paint latency still depend on document complexity, effects, renderer, display cadence, and device headroom; dated performance reports carry the fixture and environment whenever a measurement is useful.

Which renderer draws a frame?

Canvas2D is the default display path. Flat scenes can replay in a worker; frames and other structural content stay on the main-thread Canvas2D path. If you opt in to WebGPU and device initialization succeeds, shapes painted with a single solid color — rectangles, circles, and ellipses — can use an offscreen GPU surface when the frame reaches that compositor. Rounded corners, stacked or gradient paints, text, images, strokes, effects, masks, and other unsupported content keep their Canvas2D behavior. The present canvas remains Canvas2D so a GPU failure can fall back without losing the document.

The setting expresses a preference, not a promise that every frame uses GPU drawing. The editor reports the active path and any initialization or device-loss fallback. If the GPU device is lost mid-session, Canvas2D keeps drawing and Varve retries WebGPU automatically (two bounded attempts) before asking for a reload. Export and print use their own full-quality paths; the WebGPU display preference does not accelerate them.

Pipeline Stages

The rendering pipeline has four main stages:

1. Scene Graph

The document is a tree of scene nodes (ShapeNode, TextNode, FrameNode, GroupNode, ImageNode, AdjustmentNode). Each node stores its local transform, fills, strokes, effects, and other properties. The editor maintains this as an immutable data structure — any change produces a new document tree.

2. IR Generation (buildRenderIr)

The engine walks the scene graph and produces a flat array of RenderItem objects. Each RenderItem contains:

  • World-space affine transform (composed from ancestor chain)
  • Primitive geometry (rect, ellipse, circle, line, arrow, polygon, star, path, text)
  • Fill stack (solid colors, gradients, image fills — each with opacity and blend mode)
  • Stroke stack (weight, color, dash, cap, join)
  • Effect list (alpha-aware drop/inner shadows, blur, solid/gradient glows, RGB/custom chromatic split, and independent blend modes)
  • Opacity and blend mode
  • Corner radius (for rect primitives)

This IR is the stable seam between the native engine and the webview (ADR-0001). It is serialized to JSON for IPC and deserialized on the webview side.

3. Frame / Group Compositing

Before individual items are replayed, the CanvasArea performs a recursive DFS replaySubtree() that handles containers:

  • Frames — Paint background fill, save canvas state, clip to the frame's world-space polygon, recurse into children, restore state
  • Groups — Transparent pass-through unless the group has a non-passThrough blend mode or opacity < 1, in which case children render to an offscreen CompositeCanvas and composite with the group's blend mode
  • Adjustment Nodes — Render children, then apply the adjustment (curves, levels, selective color) to the rendered result

4. Replay (replayIr)

The replay engine draws each RenderItem using Canvas2D calls. This deterministic, stateless fast path maps directly to canvas API calls; its cost depends on the document, effects, renderer, and device rather than implying one universal frame rate:

  • Apply transform via setTransform()
  • Set opacity via globalAlpha
  • Draw fills (solid: fillRect, arc, roundRect; gradient: createLinearGradient, createRadialGradient, createConicGradient; image: drawImage)
  • Draw vector strokes with full dash/cap/join/weight styling
  • For text, image fills, raster layers, compound vectors, and warped images, derive effects from rendered alpha so glyphs, fill-rule holes, masks, crops, transparent backgrounds, and sparse pixels remain accurate
  • Render effects (shadows via shadowColor/shadowBlur/shadowOffsetX/Y; blurs via filter)
  • Render text via fillText with font and textBaseline
  • Render path text with per-glyph transform

Viewport Culling

The canvas supports viewport culling to skip off-screen nodes during both IR build and replay. The culling system:

  • Checks isWorldRectInViewport(bounds, viewportRect) — uses intersection (not full containment) so partially visible nodes still render
  • Pre-builds a parent index map (buildParentIndexMap(doc)) for O(1) parent lookups during the world transform computation

Floating Origin

To maintain numerical precision at large coordinates or extreme zoom levels, Varve uses a floating origin camera system:

  • The camera tracks a world-space origin point
  • All canvas drawing is offset by the camera origin, keeping coordinates in a reasonable numeric range
  • The applyCameraTransform helper translates and scales the canvas context
  • Units are in pixels with configurable display units (px, pt, mm, cm, in, pc)

Worker Render Path

For non-structural scenes (no masks, no clipping frames with children, no special groups), Varve can offload replay to an OffscreenCanvas in a Web Worker:

  • The main thread prepares a compact IR and admits transferable image payloads
  • The worker receives that prepared IR and replays it to an OffscreenCanvas when capability and structural gates allow
  • The resulting bitmap is transferred back to the main thread for presentation
  • Scenes requiring structural compositing, unavailable resources, or an authoritative fallback stay on the main-thread replay path

The render worker includes a camera fast path: when the worker bitmap's docVersion matches, the main thread can reproject the cached bitmap for a camera delta without a full scene rebuild. Admission is synchronous; whenever a fresh worker frame is not guaranteed, the authoritative main-thread replay replaces the reused pixels. Pan and zoom therefore depend on scene complexity, display cadence, and device headroom.

Subtree IR Cache

To avoid rebuilding IR for unchanged subtrees, Varve maintains a SubtreeIrCache:

  • Cache keys are based on content hashes (encoding shape kind, fill, strokes, effects, filters, opacity, blend mode, rotation, corner radius, text properties)
  • Cached IR items are reused when the node's content hasn't changed
  • The cache is invalidated on structural changes (node add/remove/reparent)

Effects and Compositing

The effects engine uses a single-pass approach with per-effect save/restore isolation:

  • Backdrop, content, and appearance effects render in explicit stages; content blur variants are applied in authored within-stage order
  • Per-effect blend mode and opacity are applied during compositing
  • Inner shadows use clip + blur technique (not canvas shadow API, which doesn't support inner shadows)
  • Gaussian, Field, Iris, Tilt-Shift, Path, and Spin use the shared CPU blur contract; spatial support is included in the padded render surface
  • The filter compositor handles non-CSS filters (exposure, sharpen, temperature, tint, color balance, channel mixer, photo filter, vibrance) via offscreen canvas pixel manipulation

See Architecture Overview for the broader system architecture, or Color & Effects for user-facing effect controls.

Search Varve