Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5ca1a03
feat: add stroke_dash_array support for various node types
softmarshmallow Mar 23, 2026
3b70a83
feat: add customizable ZIP compression level to pack function
softmarshmallow Mar 23, 2026
65625eb
Merge branch 'main' of https://github.com/gridaco/grida into canary
softmarshmallow Mar 23, 2026
9cfdf6a
feat: add benchmarking for fig2grida pipeline
softmarshmallow Mar 23, 2026
c36cbe8
feat: update camera fit method to use "<scene>" selector
softmarshmallow Mar 23, 2026
d43c5f8
feat: enhance getNodeAbsoluteBoundingBox to support scene target
softmarshmallow Mar 23, 2026
e04ec63
feat: add symbolic link for skills directory
softmarshmallow Mar 23, 2026
ef288cb
feat: update .gitignore to include Claude Code local settings
softmarshmallow Mar 23, 2026
7db085a
feat: implement Figma ID preservation in fig2grida and embed bridge
softmarshmallow Mar 23, 2026
5205ce8
refactor: remove custom mipmap handling and utilize Skia's built-in m…
softmarshmallow Mar 23, 2026
a2bda07
fix: update research skill documentation to include Taffy
softmarshmallow Mar 23, 2026
cc8ceec
feat: enhance scene loading and layout documentation
softmarshmallow Mar 23, 2026
32ed28a
docs: add TODO for auto-layout conversion in Figma integration
softmarshmallow Mar 23, 2026
b8d2fd0
feat: add resize benchmarking functionality
softmarshmallow Mar 23, 2026
2c3f87d
feat: add prefer_fixed_text_sizing option to Figma integration
softmarshmallow Mar 23, 2026
2bfddfc
chore: update Node.js version in .nvmrc to v22.22.1
softmarshmallow Mar 23, 2026
8df72b3
chore: update dependencies and add benchmarking for scene loading
softmarshmallow Mar 23, 2026
57c42e5
feat: introduce layout optimization and benchmarking for scene loading
softmarshmallow Mar 23, 2026
586d786
feat: add renderer configuration for refig embed canvas
softmarshmallow Mar 23, 2026
66352c0
wasm 0.91.0-canary.11
softmarshmallow Mar 23, 2026
96afff1
fmt
softmarshmallow Mar 23, 2026
0679b0c
feat: implement centralized change tracking for renderer
softmarshmallow Mar 23, 2026
8f94d13
chore
softmarshmallow Mar 23, 2026
06ae430
wasm 0.91.0-canary.12
softmarshmallow Mar 23, 2026
6ee8103
Merge remote-tracking branch 'origin/main' into canary
softmarshmallow Mar 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion .agents/skills/cg-perf/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ relying on hardcoded paths. File locations shift as the engine evolves.
| Benchmark fixture scenes | `--list-scenes` flag on `grida-dev bench`, or run `bench-report` on a directory |
| Config toggles (compositing, atlas, etc.) | `grep "set_layer_compositing\|set_compositor_atlas\|set_interaction_render_scale" --include="*.rs"` |
| Existing `.plan.md` proposals | `glob "docs/wg/feat-2d/*.plan.md"` |
| Scene loading pipeline | `grep "fn load_scene" --include="*.rs"` in `src/runtime/scene.rs` |
| Layout engine entry point | `grep "fn compute\b" --include="*.rs"` in `src/layout/engine.rs` |
| Text measurement stats | `grep "ParagraphMeasureStats" --include="*.rs"` |
| Skip-layout config | `grep "skip_layout" --include="*.rs"` in `src/runtime/` |
| Load-bench CLI tool | Read `crates/grida-dev/src/bench/load_bench.rs` |

---

Expand Down Expand Up @@ -126,6 +131,7 @@ reports `min/p50/p95/p99/MAX` plus per-stage breakdown and settle cost.
| `zoom` | slow/fast × around-fit/high | Zoom oscillation at different levels |
| `pan_with_settle` | slow/fast × fit/zoomed | Pan with settle frames interleaved every 12 frames |
| `realtime` | fast/slow × fit/zoomed | **Real-time event loop simulation** with sleep, 240Hz tick thread, and settle countdown matching the native viewer |
| `resize` | alternating viewport sizes | `--resize` flag. Measures `resize()` + `redraw()` cost per cycle (layout rebuild + cache invalidation + repaint) |

The `realtime` scenarios use actual `thread::sleep()` between frames
and simulate the native viewer's 240Hz tick thread + settle countdown.
Expand Down Expand Up @@ -168,6 +174,7 @@ of scenes, configs, and operations. The naming convention is
| Does a config toggle actually help? | Both GPU benchmarks + Criterion |
| Does it match what users see in the app? | `realtime` scenarios (sleep + settle simulation) |
| Are there frame drops during gestures? | Check `p99` and `MAX` in scenario stats |
| Is resize janky? | Single-scene GPU bench with `--resize` |

---

Expand Down Expand Up @@ -338,13 +345,62 @@ The document is organized by category:
- Pan-Only Optimization (items 15-20)
- Zoom Asymmetry (items 21-23)
- Zoom & Interaction Optimization (items 24-30)
- Image, Text, Engine-Level (items 31-39)
- Image, Text (items 31-33)
- Scene Loading & Layout (items 40-44)
- Engine-Level (items 34-39)

When working on a new optimization, check whether an item already
exists for it, and update or add to the document as part of the work.

---

## Scene Loading & Layout Performance

Scene loading (`Renderer::load_scene`) is the cold-start bottleneck.
For large documents (100K–150K+ nodes), the layout phase dominates
load time. This is separate from frame rendering — it runs once per
scene switch, not per frame.

### Key files

| File | Role |
| --------------------------------------------------------- | ------------------------------------------ |
| `crates/grida-canvas/src/runtime/scene.rs` (`load_scene`) | Orchestrates the load pipeline |
| `crates/grida-canvas/src/layout/engine.rs` | Layout engine (Taffy tree build + compute) |
| `crates/grida-canvas/src/runtime/config.rs` | `skip_layout` flag |
| `crates/grida-dev/src/bench/load_bench.rs` | `load-bench` CLI for per-stage timing |
| `crates/grida-canvas/benches/bench_load_scene.rs` | Criterion benchmarks for layout at scale |

### The load-bench tool

Primary diagnostic for scene loading. Reports per-stage timings.

```sh
cargo run -p grida-dev --release -- load-bench file.grida --iterations 5
cargo run -p grida-dev --release -- load-bench file.grida --list-scenes
cargo run -p grida-dev --release -- load-bench file.grida --skip-text # isolate tree + flexbox cost
cargo run -p grida-dev --release -- load-bench file.grida --skip-layout # schema-only fast path
```

### Cost breakdown

For 100K–150K node scenes, layout is ~95%+ of `load_scene`. The main
cost centers:

1. **Taffy tree construction** — node insertion + ID mappings
2. **Text measurement** — Skia paragraph layout calls per Taffy measure callback
3. **Flexbox computation** — `compute_layout_with_measure()` per subtree
4. **Layout extraction** — DFS walk to read computed results

### Key optimization: skip_layout

`skip_layout` bypasses Taffy entirely. `compute_schema_only()` copies
schema positions/sizes in a single walk — correct for absolute-positioned
documents. Set via `runtime_renderer_set_skip_layout(true)` before
loading a scene.

---

## Pitfalls

These are failure modes learned from experience. Each one has caused
Expand Down Expand Up @@ -404,6 +460,13 @@ frame gets a cache hit. Without recapture, every frame after settle
is also a full draw, producing 7fps instead of 100+fps. The capture
guard should be `if self.backend.is_gpu()` — NOT `if !plan.stable`.

### Layout is the cold-start bottleneck, not rendering

For large documents (100K+ nodes), `load_scene` dominates cold start
— frame rendering optimizations do not help here. Use `load-bench`
(not `bench`) to measure this path. Use `skip_layout` for
absolute-positioned documents.

### Timing overhead in budgeted loops

`Instant::now()` costs ~30ns per call. In a tight loop processing
Expand Down
11 changes: 6 additions & 5 deletions .agents/skills/research/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: research
description: >
Research upstream and peer projects to inform Grida's design and
implementation. Use when investigating how Chromium, Skia, Servo,
or peer canvas editors solve a problem before writing code. Covers
Taffy, or peer canvas editors solve a problem before writing code. Covers
source-code exploration, research document authoring, and the
study-adapt-differ pattern used in .plan.md files. Relevant dirs:
docs/wg/research/, docs/wg/feat-2d/, crates/grida-canvas/.
Expand Down Expand Up @@ -92,10 +92,11 @@ Known citations:

### Web Standards & CSS

| Repo | Lang | When to reference | Key paths |
| --------------------------------------- | ---- | ------------------------------------------------------------------------- | ---------------------------------------- |
| [servo](https://github.com/servo/servo) | Rust | CSS layout, DOM, Rust browser-engine patterns. We vendor its style system | `components/style/` `components/layout/` |
| [stylo](https://github.com/servo/stylo) | Rust | CSS parsing and style resolution | `style/` |
| Repo | Lang | When to reference | Key paths |
| -------------------------------------------- | ---- | ------------------------------------------------------------------------- | ---------------------------------------- |
| [servo](https://github.com/servo/servo) | Rust | CSS layout, DOM, Rust browser-engine patterns. We vendor its style system | `components/style/` `components/layout/` |
| [stylo](https://github.com/servo/stylo) | Rust | CSS parsing and style resolution | `style/` |
| [taffy](https://github.com/DioxusLabs/taffy) | Rust | Flexbox/Grid layout algorithms and Rust-native layout engine internals | `src/tree/` `src/compute/` `src/style/` |

### Canvas Editor Peers

Expand Down
1 change: 1 addition & 0 deletions .claude/skills
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,12 @@ __pycache__/

# AI & Tooling
*.plan.md
*.todo.md
*.todo.md


# Claude Code (local settings and session files)
.claude/settings.local.json
.claude/plans/
.claude/todos.json
.claude/worktrees
CLAUDE.local.md
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v22.15.0
v22.22.1
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ const EXPECTED_FUNCTIONS = [
{ name: "_deallocate", paramCount: 2 },

// initialization / app lifecycle
{ name: "_init", paramCount: 3 },
{ name: "_init_with_backend", paramCount: 4 },
{ name: "_init", paramCount: 4 },
{ name: "_init_with_backend", paramCount: 5 },
{ name: "_tick", paramCount: 2 },
{ name: "_destroy", paramCount: 1 },
{ name: "_resize_surface", paramCount: 3 },
Expand Down Expand Up @@ -100,6 +100,7 @@ const EXPECTED_FUNCTIONS = [
{ name: "_runtime_renderer_set_layer_compositing", paramCount: 2 },
{ name: "_runtime_renderer_set_pixel_preview_scale", paramCount: 2 },
{ name: "_runtime_renderer_set_pixel_preview_stable", paramCount: 2 },
{ name: "_runtime_renderer_set_skip_layout", paramCount: 2 },
] as const;

// Expected Emscripten runtime methods
Expand Down Expand Up @@ -130,7 +131,10 @@ describe("WASM API Validation", () => {
useEmbeddedFonts: true,
});

const doc = readFileSync(resolve(process.cwd(), "example/rectangle.grida1"), "utf8");
const doc = readFileSync(
resolve(process.cwd(), "example/rectangle.grida1"),
"utf8"
);
canvas.loadScene(doc);
canvas.dispose();

Expand Down
2 changes: 1 addition & 1 deletion crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm
Git LFS file not shown
47 changes: 40 additions & 7 deletions crates/grida-canvas-wasm/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,40 @@ export default async function init(
);
}

/**
* Renderer configuration flags.
*
* Matches the Rust `RuntimeRendererConfig` fields that are exposed via
* the C ABI `config_flags` bitfield. Pass at init time; individual
* fields remain mutable via `Scene.runtime_renderer_set_*()` setters.
*/
interface RendererConfig {
/**
* Skip the Taffy flexbox layout engine during scene loading.
* Derives layout from schema positions/sizes instead.
* @default false
*/
skip_layout?: boolean;
}

/** Encode a `RendererConfig` into the C ABI `config_flags` bitfield. */
function encodeConfigFlags(config?: RendererConfig): number {
let flags = 0;
if (config?.skip_layout) flags |= 1 << 0;
return flags;
}

interface CreateSurfaceOptions {
/**
* when true, embedded fonts will be registered and used for text rendering.
* @default true
*/
use_embedded_fonts?: boolean;
/**
* Initial renderer configuration applied at construction.
* Fields can still be changed at runtime via `Scene.runtime_renderer_set_*()`.
*/
config?: RendererConfig;
}

class ApplicationFactory {
Expand Down Expand Up @@ -191,7 +219,8 @@ class ApplicationFactory {
const ptr = this.module._init(
canvas.width,
canvas.height,
options.use_embedded_fonts
options.use_embedded_fonts,
encodeConfigFlags(options.config)
);
const _ = new Scene(this.module, ptr);
_.resize(canvas.width, canvas.height);
Expand Down Expand Up @@ -227,13 +256,17 @@ export type CreateCanvasOptions =
canvas: HTMLCanvasElement;
locateFile?: GridaCanvasModuleInitOptions["locateFile"];
useEmbeddedFonts?: boolean;
/** Initial renderer configuration. */
config?: RendererConfig;
}
| {
backend: "raster";
width: number;
height: number;
locateFile?: GridaCanvasModuleInitOptions["locateFile"];
useEmbeddedFonts?: boolean;
/** Initial renderer configuration. */
config?: RendererConfig;
};

export class Canvas {
Expand Down Expand Up @@ -291,10 +324,7 @@ export class Canvas {
* Register image bytes with an explicit logical RID (e.g. res://images/logo.png).
* Use when you need stable, document-mapped identifiers.
*/
addImageWithId(
data: Uint8Array,
rid: string
): AddImageWithIdResult | false {
addImageWithId(data: Uint8Array, rid: string): AddImageWithIdResult | false {
return this._scene.addImageWithId(data, rid);
}

Expand Down Expand Up @@ -344,13 +374,15 @@ export async function createCanvas(opts: CreateCanvasOptions): Promise<Canvas> {

const module = bindings as createGridaCanvas.GridaCanvasWasmBindings;
const useEmbeddedFonts = opts.useEmbeddedFonts ?? true;
const configFlags = encodeConfigFlags(opts.config);

if (opts.backend === "raster") {
const appptr = module._init_with_backend(
BACKEND_ID.Raster,
opts.width,
opts.height,
useEmbeddedFonts
useEmbeddedFonts,
configFlags
);
return Canvas._fromRaster(new Scene(module, appptr));
}
Expand All @@ -369,7 +401,8 @@ export async function createCanvas(opts: CreateCanvasOptions): Promise<Canvas> {
BACKEND_ID.WebGL,
opts.canvas.width,
opts.canvas.height,
useEmbeddedFonts
useEmbeddedFonts,
configFlags
);
const scene = new Scene(module, appptr);
scene.resize(opts.canvas.width, opts.canvas.height);
Expand Down
20 changes: 11 additions & 9 deletions crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ declare namespace canvas {
_init(
width: number,
height: number,
use_embedded_fonts: boolean
use_embedded_fonts: boolean,
config_flags: number
): GridaCanvasApplicationPtr;

_init_with_backend(
backend_id: number,
width: number,
height: number,
use_embedded_fonts: boolean
use_embedded_fonts: boolean,
config_flags: number
): GridaCanvasApplicationPtr;

// ====================================================================================================
Expand Down Expand Up @@ -49,9 +51,7 @@ declare namespace canvas {
ptr: number,
len: number
): void;
_drain_missing_images(
state: GridaCanvasApplicationPtr
): Ptr;
_drain_missing_images(state: GridaCanvasApplicationPtr): Ptr;
_resolve_image(
state: GridaCanvasApplicationPtr,
rid_ptr: number,
Expand Down Expand Up @@ -248,6 +248,11 @@ declare namespace canvas {
flags: number
): void;

_runtime_renderer_set_skip_layout(
state: GridaCanvasApplicationPtr,
skip: boolean
): void;

_runtime_renderer_set_outline_mode(
state: GridaCanvasApplicationPtr,
enable: boolean
Expand All @@ -261,10 +266,7 @@ declare namespace canvas {
node_id_ptr: number,
node_id_len: number
): boolean;
_text_edit_exit(
state: GridaCanvasApplicationPtr,
commit: boolean
): Ptr;
_text_edit_exit(state: GridaCanvasApplicationPtr, commit: boolean): Ptr;
_text_edit_is_active(state: GridaCanvasApplicationPtr): boolean;
_text_edit_get_text(state: GridaCanvasApplicationPtr): Ptr;
_text_edit_undo(state: GridaCanvasApplicationPtr): boolean;
Expand Down
Loading
Loading