Rendering Pipeline
Varve's rendering pipeline converts a scene graph into pixels on screen through a compact Intermediate Representation (IR). This approach delivers 86 fps on Canvas2D for 600 shapes by separating computation from drawing.
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 replays it to Canvas2D or WebGPU. This is the opposite of a "pixel-push" architecture where the engine sends pixel data directly.
The rationale was validated empirically on Wayland: the IR-replay approach achieves 86 fps versus 8.5 fps for a pixel-push approach. The IR is approximately 42 KB per frame for a scene with 600 shapes, making it efficient to serialize across the IPC boundary.
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 (drop shadows, inner shadows, blurs, glows — each with independent blend mode)
- 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 is where the 86fps performance comes from — the replay is deterministic, stateless, and maps directly to canvas API calls:
- Apply transform via
setTransform() - Set opacity via
globalAlpha - Draw fills (solid:
fillRect,arc,roundRect; gradient:createLinearGradient,createRadialGradient,createConicGradient; image:drawImage) - Draw strokes with full dash/cap/join/weight styling
- Render effects (shadows via
shadowColor/shadowBlur/shadowOffsetX/Y; blurs viafilter) - Render text via
fillTextwithfontandtextBaseline - 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
applyCameraTransformhelper 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 document is serialized and sent to the worker
- The worker builds the IR and replays it to an OffscreenCanvas
- The resulting bitmap is transferred back to the main thread
- A sceneHasImageFills gate keeps image-containing scenes on the main thread (workers can't decode images)
The render worker includes a camera fast path: when the worker bitmap's docVersion matches, the main thread replays the cached bitmap with a compensation transform for the camera delta, achieving smooth 60 fps pan/zoom without a full scene rebuild.
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:
- Each shadow/glow/blur effect renders independently with its own save/restore scope
- 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)
- Multiple blurs use the maximum radius rather than compositing independently
- The filter compositor handles non-CSS filters (exposure, sharpen, temperature, tint, color balance, channel mixer, photo filter, vibrance) via offscreen canvas pixel manipulation
Related Guides
See Architecture Overview for the broader system architecture, or Color & Effects for user-facing effect controls.