Skip to content

Graphics Backend #378

Description

@softmarshmallow

Graphics backend — Rust / Skia

Status snapshot, updated 2026-05. This document is the engineering reference for crates/grida (the canvas engine) and the WASM bridge in crates/grida-canvas-wasm. The original aspirational roadmap has been replaced with the current architecture, what is implemented, and what is still outstanding.


Overview

  • Single source of truth: crates/grida (~124k lines of Rust).
  • Skia bindings via skia-safe.
  • Two surface backends: GPU (Backend::GL(*mut Surface), Ganesh / GrDirectContext) and CPU raster (surfaces::raster_n32_premul). Selected at runtime by the host (crates/grida/src/runtime/scene.rs).
  • WASM build: crates/grida-canvas-wasm exposes a C-ABI of ~96 functions; entry points are split across wasm_application.rs, wasm_text_edit.rs, wasm_svg.rs, wasm_fonts.rs, wasm_markdown.rs. Allocator in _internal.rs.

Frame pipeline

Renderer in runtime/scene.rs owns the backend, camera, scene cache, and compositor. A frame runs in three deterministic phases:

  1. FramePlan — R-tree viewport cull + spatial sort. Produces regions: Vec<(Rectangle, Vec<usize>)> (live-drawn node indices) and promoted: Vec<NodeId> (nodes whose output is cached as a raster layer).
  2. Painter dispatchpainter/painter.rs reads cached skia_safe::Picture objects from the picture cache, chains SkImageFilter for effects, and captures promoted nodes through SaveLayerRec.
  3. Compositor blitLayerImageCache stages cached SkImage textures; the GPU blit loop reassembles the frame at the current zoom and pan offset.

FrameFlushStats (runtime/scene.rs:197) bundles per-frame timing, cache-hit counts, and live-draw cost. The unified frame loop lives in runtime/frame_loop.rs and exposes a single frame(time: f64) entry point that replaces the previous tick / redraw / scheduler split.

Cache architecture

Five independent caches, each with its own keying and invalidation:

Cache Key Value Invalidation
Picture (cache/picture.rs) NodeId or (NodeId, variant: u64) skia_safe::Picture Per-node dirty or full sweep; monotonic generation counter
Geometry (cache/geometry.rs) NodeId transform, bounds, effects overflow Dirty flag per node; R-tree rebuilt on zoom / pan
Vector path (cache/vector_path.rs) NodeId Rc<Path> + hash Content-addressed by path-data hash
Paragraph (cache/paragraph.rs) content hash (ParagraphIdentifier::ByShapeKey) Skia textlayout::Paragraph + LayoutMeasurements Font-generation epoch; width-keyed
Layer image (cache/compositor/cache.rs) NodeId LayerImage { source, zoom, bounds, … } LRU with 128 MB default budget; dirty + stale flags

Paragraph caching is content-keyed, not NodeId-keyed, to avoid leaks as nodes are recreated by undo / redo.

Compositor

cache/compositor/promotion.rs decides which nodes are promoted to a cached raster layer. Triggers: drop_shadow, inner_shadow, layer_blur, noise. Excluded: backdrop_blur and liquid_glass, which depend on what is behind them and cannot be isolated. Stale entries (cached at a different zoom density) are blit with a compensating GPU scale; dirty entries are excluded from get().

Camera

Camera2D (runtime/camera.rs) maintains:

  • transform: world-space affine.
  • cached_zoom: 1 / scale_x, precomputed.
  • cached_view_matrix + cached_view_matrix_inv: invalidated together on any mutation.

CameraChangeKind classifies every frame as PanOnly, ZoomIn, ZoomOut, or PanAndZoom. The renderer uses this to pick a fast path: PanOnly blits the cached frame at an offset; ZoomIn/Out stretches a cached frame and schedules a re-rasterization; PanAndZoom forces a full redraw. Recent micro-optimizations (get_zoom() without sqrt, quantized_transform extracting sin/cos from the matrix directly) recover 3–10 % on zoom operations across scene sizes.

Text

Skia textlayout::Paragraph for shaping and line-breaking; HarfBuzz under the hood. The cache separates measurement (paint-independent) from rendering (paint-aware). LayoutMeasurements bundles total height, intrinsic widths, and baseline offsets. Taffy integration in htmlcss/layout.rs disables rounding for sub-pixel-accurate placement when paragraphs are laid out inside flex / grid containers.

Effects and filters

In painter/effects.rs:

  • Drop shadow, inner shadow → image_filters::drop_shadow() with Skia blur.
  • Layer blur → image_filters::blur().
  • Backdrop blur → SaveLayerRec with backdrop filter (context-dependent; not promotable).
  • Liquid glass → runtime SkSL shader (liquid_glass_backdrop.sksl) applied via ImageFilter::runtime_shader() over a pre-blurred backdrop. Uniforms drive index of refraction (1.0–2.0), chromatic aberration, and an SDF-based rounded-box mask.

Blend-mode plumbing exists in painter/layer.rs (BlendMode); per-mode parity against the CSS / Figma matrix is the next item.

Layout

crates/grida/src/htmlcss/ integrates Taffy. compute_layout() builds the Taffy tree from StyledElement, supplies a measure callback for text nodes, and emits a positioned LayoutBox tree. Paint walks that tree and issues Skia draw calls. Rounding is disabled for sub-pixel correctness. This is the path exercised by the HTML / CSS and SVG importers.

File format

io/io_grida_file.rs. ZIP container with manifest.json + document.grida (FlatBuffers root). Format detection at bytes 0–7: ZIP magic 50 4B vs FlatBuffers identifier GRID. Schema in format/grida.fbs: packed u32 node IDs (actor:8 | counter:24), flat node map, union node payload for forward evolution. Maximum uncompressed size: 8 GB.


Implementation status

Step 1 — Basic rendering & assets

  • Rectangle, ellipse, line, path nodes
  • Text rendering (Skia Paragraph)
  • Solid color fills and strokes
  • Image rendering and caching (ImageRepository, layer-image cache)
  • Antialiasing, transparency, compositing
  • Video frame integration

Step 2 — Layout (Taffy)

  • Definitive positioning and transform
  • Box model (margin, padding, borders, edge insets)
  • Flex (direction, wrap, gap, justify, align)
  • Grid (auto-flow, template rows / columns)

Advanced graphics

  • Gradients: linear, radial, sweep
  • Drop shadow, inner shadow
  • Layer blur, backdrop blur
  • Liquid glass (runtime SkSL shader)
  • Full per-mode blend-mode parity

Ecosystem / IO

  • PNG, JPEG export and import (raster)
  • PDF export (multi-page)
  • PDF import
  • SVG export
  • SVG import (usvg → CG)
  • HTML / CSS import (Stylo cascade)
  • Figma import (@grida/io-figma)
  • .grida file
    • document.json 2025 edition
    • ZIP container with resources embedding
    • document.json 2026 edition (tokens, components, vector-network promotion)
    • Gzip-compressed .grida variant

Technical strategy

  • Immediate-mode paint, stateless per frame
  • Layout and paint phases separated
  • Deferred asset hydration (ImageRepository, FontRepository, frame_strategy.rs)
  • Unified frame loop
  • CameraChangeKind-driven fast paths

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions