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:
- 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).
- Painter dispatch —
painter/painter.rs reads cached skia_safe::Picture objects from the picture cache, chains SkImageFilter for effects, and captures promoted nodes through SaveLayerRec.
- Compositor blit —
LayerImageCache 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
Step 2 — Layout (Taffy)
Advanced graphics
Ecosystem / IO
Technical strategy
Related
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 incrates/grida-canvas-wasm. The original aspirational roadmap has been replaced with the current architecture, what is implemented, and what is still outstanding.Overview
crates/grida(~124k lines of Rust).skia-safe.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).crates/grida-canvas-wasmexposes a C-ABI of ~96 functions; entry points are split acrosswasm_application.rs,wasm_text_edit.rs,wasm_svg.rs,wasm_fonts.rs,wasm_markdown.rs. Allocator in_internal.rs.Frame pipeline
Rendererinruntime/scene.rsowns the backend, camera, scene cache, and compositor. A frame runs in three deterministic phases:regions: Vec<(Rectangle, Vec<usize>)>(live-drawn node indices) andpromoted: Vec<NodeId>(nodes whose output is cached as a raster layer).painter/painter.rsreads cachedskia_safe::Pictureobjects from the picture cache, chainsSkImageFilterfor effects, and captures promoted nodes throughSaveLayerRec.LayerImageCachestages cachedSkImagetextures; 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 inruntime/frame_loop.rsand exposes a singleframe(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/picture.rs)NodeIdor(NodeId, variant: u64)skia_safe::Picturecache/geometry.rs)NodeIdcache/vector_path.rs)NodeIdRc<Path>+ hashcache/paragraph.rs)ParagraphIdentifier::ByShapeKey)textlayout::Paragraph+LayoutMeasurementscache/compositor/cache.rs)NodeIdLayerImage { source, zoom, bounds, … }Paragraph caching is content-keyed, not
NodeId-keyed, to avoid leaks as nodes are recreated by undo / redo.Compositor
cache/compositor/promotion.rsdecides which nodes are promoted to a cached raster layer. Triggers:drop_shadow,inner_shadow,layer_blur, noise. Excluded:backdrop_blurandliquid_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 fromget().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.CameraChangeKindclassifies every frame asPanOnly,ZoomIn,ZoomOut, orPanAndZoom. The renderer uses this to pick a fast path:PanOnlyblits the cached frame at an offset;ZoomIn/Outstretches a cached frame and schedules a re-rasterization;PanAndZoomforces a full redraw. Recent micro-optimizations (get_zoom()withoutsqrt,quantized_transformextractingsin/cosfrom the matrix directly) recover 3–10 % on zoom operations across scene sizes.Text
Skia
textlayout::Paragraphfor shaping and line-breaking; HarfBuzz under the hood. The cache separates measurement (paint-independent) from rendering (paint-aware).LayoutMeasurementsbundles total height, intrinsic widths, and baseline offsets. Taffy integration inhtmlcss/layout.rsdisables rounding for sub-pixel-accurate placement when paragraphs are laid out inside flex / grid containers.Effects and filters
In
painter/effects.rs:image_filters::drop_shadow()with Skia blur.image_filters::blur().SaveLayerRecwith backdrop filter (context-dependent; not promotable).liquid_glass_backdrop.sksl) applied viaImageFilter::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 fromStyledElement, supplies a measure callback for text nodes, and emits a positionedLayoutBoxtree. 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 withmanifest.json+document.grida(FlatBuffers root). Format detection at bytes 0–7: ZIP magic50 4Bvs FlatBuffers identifierGRID. Schema informat/grida.fbs: packedu32node 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
Paragraph)ImageRepository, layer-image cache)Step 2 — Layout (Taffy)
Advanced graphics
Ecosystem / IO
usvg→ CG)@grida/io-figma).gridafiledocument.json2025 editiondocument.json2026 edition (tokens, components, vector-network promotion).gridavariantTechnical strategy
ImageRepository,FontRepository,frame_strategy.rs)CameraChangeKind-driven fast pathsRelated