Skip to content

canary: rendering fixes, Figma import improvements, and frame plan optimizations#597

Merged
softmarshmallow merged 19 commits intomainfrom
canary
Mar 23, 2026
Merged

canary: rendering fixes, Figma import improvements, and frame plan optimizations#597
softmarshmallow merged 19 commits intomainfrom
canary

Conversation

@softmarshmallow
Copy link
Copy Markdown
Member

@softmarshmallow softmarshmallow commented Mar 23, 2026

Summary

  • Rendering fixes: Fix RenderSurface clip path to transform from local to world space, fixing containers with effects (drop shadow, blur) clipping children at wrong positions
  • SurfaceUI: Enhance color selection logic to reflect both selected and hovered states
  • Figma import improvements: Add background color support for canvases, improve letter spacing calculation with font-size awareness, add per-side stroke width support for rectangular nodes
  • fig2grida CLI: Support multiple input formats (.fig, .json, .json.gz, .zip)
  • Frame plan optimizations: Implement deferred frame plans and zoom image caching for improved rendering performance on large scenes
  • Tests: Add text decoration roundtrip tests, format roundtrip coverage
  • Docs: Add io-figma, io-grida, io-svg skills documentation; optimization notes
  • WASM: Bump to 0.91.0-canary.9

Changed files

Rendering engine (crates/grida-canvas)

  • painter/layer.rs — fix clip path coordinate space for render surfaces
  • surface/ui/render.rs — improve selection/hover color feedback
  • runtime/scene.rs — deferred frame plans and zoom image cache
  • io/io_grida.rs, tests/fbs_roundtrip.rs — text decoration roundtrip tests
  • window/application.rs — integration with deferred plans

Figma import (packages/grida-canvas-io-figma)

  • Background color extraction, letter spacing fix, per-side stroke widths
  • CLI multi-format support (.fig, .json, .json.gz, .zip)

Benchmarks & docs

  • Enhanced bench reporting for pan/zoom metrics
  • Optimization documentation

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • SVG import support for Grida Canvas
    • Text decoration support (underline/overline/line-through)
    • Per-side stroke widths for shapes
    • Figma REST/ZIP/JSON(.gz) input support, CLI & export improvements
    • Scene background color propagation
  • Bug Fixes

    • Fixed clip-path coordinate-space application
    • Improved pan cache reuse and settle-frame redraw behavior
  • Performance

    • Zoom image cache for faster interactive zooming
    • Enhanced benchmark reporting and frame-stability tracking

- Updated the color determination logic in the SurfaceUI rendering process to account for both selected and hovered states, improving visual feedback for user interactions.
- The color now changes to ACCENT_COLOR when a node is either selected or hovered, enhancing the user experience during surface interactions.
- Enhanced the canvas extraction and conversion logic to include background color properties, allowing for more accurate representation of Figma designs.
- Integrated the `@grida/color` library to handle background color formatting during the conversion process.
- Updated relevant functions to ensure background colors are preserved and correctly passed to the resulting Grida documents.
- Added a default font size constant to ensure consistent letter spacing calculations when converting styles from Figma.
- Updated the letter spacing logic to account for font size, ensuring accurate representation in the resulting Grida documents.
- Enhanced handling of letter spacing units to improve compatibility with the Figma API specifications.
…a import

- Implemented a new function to extract individual stroke weights from Figma's REST API, allowing for per-side stroke width properties.
- Updated the document generation logic to include these stroke widths in the resulting Grida documents.
- Added tests to verify the correct mapping of individual stroke weights for both frames and rectangles.
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Mar 23, 2026 1:08pm
grida Ready Ready Preview, Comment Mar 23, 2026 1:08pm
5 Skipped Deployments
Project Deployment Actions Updated (UTC)
code Ignored Ignored Mar 23, 2026 1:08pm
legacy Ignored Ignored Mar 23, 2026 1:08pm
backgrounds Skipped Skipped Mar 23, 2026 1:08pm
blog Skipped Skipped Mar 23, 2026 1:08pm
viewer Skipped Skipped Mar 23, 2026 1:08pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 23, 2026

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Adds I/O documentation for Figma/Grida/SVG, extends Figma REST/CLI inputs and background/stroke handling, implements text-decoration tests, introduces deferred render planning and a zoom image cache fast path, expands benchmarking scenarios/timings, and updates related rendering, layout, and tooling files.

Changes

Cohort / File(s) Summary
I/O Skill Documentation
.agents/skills/io-figma/SKILL.md, .agents/skills/io-grida/SKILL.md, .agents/skills/io-svg/SKILL.md, .agents/skills/cg-perf/SKILL.md, .agents/skills/cg-reftest/SKILL.md, .agents/skills/io-figma/scripts/figma_archive.py
Added/updated skill docs describing Figma/Grida/SVG I/O workflows, schema rules, verification steps, and tooling; new script stub referencing shared figma_archive helper.
Figma Archive Tooling
.tools/figma_archive.py, fixtures/test-figma/community/README.md, .agents/skills/io-figma/scripts/figma_archive.py, .tools/README.md
Enhanced figma_archive: token handling (FIGMA_TOKEN/X_FIGMA_TOKEN), --export support, export node collection, chunked image API requests and per-node export downloads; updated docs/README and fixture README.
Figma → Grida I/O (core & CLI)
packages/grida-canvas-io-figma/fig2grida-core.ts, packages/grida-canvas-io-figma/fig2grida.ts, packages/grida-canvas-io-figma/lib.ts, packages/grida-canvas-io-figma/__tests__/iofigma.rest-api.no-geometry.test.ts
Added REST canvas backgroundColor plumbing, page background propagation, gzip/json/zip input support in CLI, normalized letter-spacing with DEFAULT_FONT_SIZE, per-side rectangular stroke mapping (individualStrokeWeights → rectangular_stroke_width_*), and related tests/fixture formatting.
FlatBuffers / Format roundtrip tests
packages/grida-canvas-io/__tests__/format-roundtrip.test.ts, crates/grida-canvas/tests/fbs_roundtrip.rs, crates/grida-canvas/src/io/io_grida.rs
Added/expanded tests for TextSpan text_decoration roundtrips (underline/overline/line-through/none) and JSON→FB/FB→Rust roundtrip coverage.
Renderer: Deferred Planning & Caches
crates/grida-canvas/src/runtime/scene.rs, docs/wg/feat-2d/optimization.md
Introduced PlanState/DeferredPlan to defer FramePlan materialization, added ZoomImageCache fast-path with blit-based unstable-zoom rendering, reworked pan/zoom cache lifecycle and invalidation semantics, and updated design doc to match implementation.
Painter / Surface / Window tweaks
crates/grida-canvas/src/painter/layer.rs, crates/grida-canvas/src/surface/ui/render.rs, crates/grida-canvas/src/window/application.rs, crates/grida-dev/src/platform/native_application.rs
Remapped container clip_path into world-space during PainterRenderSurface creation, used hover state when picking frame title accent color, derived frame quality from flushed frame stable flag, and switched settle-frame to request stable queue.
Layout engine adjustments
crates/grida-canvas/src/layout/engine.rs
Two-phase layout clarifications: treat Group/BooleanOperation as non-layout virtual nodes, skip them in Taffy subtree but recurse children, apply schema-position correction based on parent type, and add tests for group-child positioning.
Benchmarking/reporting changes
crates/grida-dev/src/bench/runner.rs, crates/grida-dev/src/bench/report.rs, .agents/skills/cg-perf/SKILL.md
Expanded benchmark runner scenarios (zigzag/circle/pan_with_settle/realtime), added per-pass timing breakdown (min/max/queue/draw/mid_flush/compositor/flush/settle), introduced ScenarioResult/ScenarioParams and fit_zoom, and updated docs describing new report shape.
Misc UI / packaging
editor/app/(embed)/embed/v1/debug/page.tsx, crates/grida-canvas-wasm/package.json, rust-toolchain.toml, tools/README.md
Added .gz upload detection and accept list, bumped canvas-wasm package version, requested rustfmt/clippy in toolchain, and minor README edits.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Client/UI
    participant Renderer
    participant Cache as ZoomImageCache
    participant GPU

    UI->>Renderer: request render_frame(view_matrix, unstable_flag)
    Renderer->>Cache: check zoom cache validity (view_matrix, zoom)
    alt cache hit (zoom/pan fast path)
        Cache-->>Renderer: cached_snapshot + transform
        Renderer->>GPU: blit cached_snapshot with residual transform
        GPU-->>Renderer: blit complete
    else cache miss
        Renderer->>Renderer: materialize PlanState -> FramePlan (R-tree, cull, sort)
        Renderer->>GPU: full frame draw (compositor, flush)
        GPU-->>Renderer: full draw complete
        Renderer->>Cache: capture snapshot if eligible (after full draw)
    end
    Renderer-->>UI: present frame (FrameQuality stable/unstable)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

canvas, performance, cg, wasm32

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly summarizes the main changes: rendering fixes, Figma import improvements, and frame plan optimizations are all reflected in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch canary

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

softmarshmallow and others added 6 commits March 23, 2026 18:15
- Introduced unit tests for JSON roundtrip functionality of text decorations, specifically testing underline and none values.
- Enhanced existing roundtrip tests to verify that text decoration properties are correctly preserved during serialization and deserialization.
- Updated format roundtrip tests to include various text decoration styles, ensuring comprehensive coverage for text node properties.
- Updated the fig2grida CLI to accept various input formats including .fig, .json, .json.gz, and .zip.
- Improved help documentation to reflect the new input options and usage examples.
- Added logic to handle gzip decompression for .json.gz files.
- Refactored output path determination to accommodate different input file extensions.
compute_clip_path() returns clip paths in the node's local coordinate
space. For regular Draw layers this is correct because draw_layer()
applies the world transform before clipping. However,
draw_render_surface() applies the clip path directly in world space
without a preceding transform, causing containers with effects (drop
shadow, blur) to clip their children at the wrong position.

Transform the clip path from local to world space before storing it
on the PainterRenderSurface so it matches the coordinate space where
draw_render_surface applies it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Introduced comprehensive documentation for the io-figma, io-grida, and io-svg skills, detailing their architecture, usage, and key files.
- Included guidelines for common tasks, input formats, and verification processes to assist developers in working with Figma and Grida file formats.
- Added references to relevant files and tests to enhance understanding and facilitate development.
…ved rendering performance

- Introduced `DeferredPlan` and `PlanState` to optimize frame rendering by deferring expensive R-tree queries on cache-hit frames.
- Added `ZoomImageCache` to store GPU snapshots for zoom operations, allowing for efficient texture scaling during active zooming.
- Updated rendering logic to utilize the new caching mechanisms, significantly reducing frame processing time for large scenes.
- Enhanced benchmark reporting to include detailed performance metrics for pan and zoom operations, ensuring better insights into rendering efficiency.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c0d7164aec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1101 to +1104
if !plan.stable
&& self.backend.is_gpu()
&& self.zoom_image_cache.is_some()
&& plan.camera_change != CameraChangeKind::PanOnly
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude non-camera invalidations from zoom-cache blits

On GPU backends this condition also matches CameraChangeKind::None, so an unstable redraw triggered by a scene mutation can reuse the previous frame snapshot instead of repainting the scene. text_edit_sync_display_text() is one concrete path: it updates the layer text, calls queue_unstable(), leaves the camera unchanged, and this branch will blit the stale zoom_image_cache, so the edited text does not appear until a later stable frame.

Useful? React with 👍 / 👎.

Comment on lines +1101 to +1104
if !plan.stable
&& self.backend.is_gpu()
&& self.zoom_image_cache.is_some()
&& plan.camera_change != CameraChangeKind::PanOnly
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid zoom-cache blits on zoom-out frames

On GPU backends this fast path now runs for ZoomOut/PanAndZoom(false) as well, but a zoom-out reveals world content that was outside the cached viewport. try_zoom_cache_blit() only scales the old snapshot and clears the newly exposed margins to the scene background, so nearby nodes disappear while the user is zooming out and only pop back on the settle frame.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
crates/grida-dev/src/bench/report.rs (1)

45-59: ScenarioParams is already too lossy for the new scenario kinds.

circle_pan has to stash radius under speed, while zigzag, pan_with_settle, and realtime drop key knobs entirely (dy, pause/segment sizes, scroll cadence, etc.). Once serialized, those runs are no longer self-describing or reproducible. Consider a per-kind enum or adding the missing fields before tooling starts depending on this shape.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/grida-dev/src/bench/report.rs` around lines 45 - 59, ScenarioParams is
too generic and loses per-scenario knobs (e.g., circle_pan stashes radius in
speed and zigzag / pan_with_settle / realtime drop dy, pause/segment sizes,
scroll cadence), so change the shape to be per-kind and serializable: replace or
augment the flat ScenarioParams with a tagged enum (e.g.,
ScenarioParams::CirclePan { radius: f32, ... }, ::ZigZag { dy: f32,
segment_pause: f32, ... }, ::PanWithSettle { settle_time: f32, ... }, ::Realtime
{ cadence_ms: u32, ... }) or add all missing optional fields with clear names
and serde tags; update serde::Serialize derive accordingly so each run remains
self-describing and reproducible, and update usages that construct
ScenarioParams to use the new enum variants or new fields (look for
ScenarioParams construction sites and consumers).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.agents/skills/io-figma/SKILL.md:
- Around line 18-30: The fenced architecture block containing the lines starting
".fig bytes / HTML clipboard" and listing iofigma.fromKiwi*(),
iofigma.fromRest*(), fig2grida-core.ts and fig2grida.ts should include a
language tag to satisfy markdownlint MD040; update the opening fence from ``` to
```text (or another appropriate language) so the block is explicitly marked,
leaving the content unchanged.

In `@crates/grida-canvas/src/runtime/scene.rs`:
- Around line 965-969: The current fast-path condition treats
CameraChangeKind::None as a “zoom-like” change and can incorrectly reuse
zoom_image_cache during non-camera invalidations; update the condition around
the zoom_image_cache check (the block using camera_change and zoom_image_cache)
to explicitly exclude CameraChangeKind::None (e.g., require camera_change !=
CameraChangeKind::None && camera_change != CameraChangeKind::PanOnly) so only
actual camera/zoom gestures reuse the cache; apply the same change to the other
analogous checks referenced (the blocks around lines marked 1101-1105 and
1409-1416).

In `@crates/grida-dev/src/bench/runner.rs`:
- Around line 729-977: run_scenarios currently leaves camera translations from
prior passes, causing later scenarios to start with a moved viewport; fix by
restoring the original fitted camera transform before each scenario iteration
(e.g. save the fitted transform after initial fit and call
renderer.camera.set_transform(saved) or call fit_camera_to_scene(renderer) at
the start of each scenario loop) so that functions like run_pan_pass_at,
run_circle_pan_pass, run_zigzag_pan_pass, run_zoom_pass_at,
run_pan_with_settle_pass and run_realtime_pan_pass always start from the same
baseline; apply this restore before any warmup/translate calls (before
queue_stable/queue_unstable and before warmup()) in each scenario block.

In `@docs/wg/feat-2d/optimization.md`:
- Around line 744-748: Locate the paragraph containing "The zoom image cache..."
that references "item 17" and update the cross-reference so it points to the
actual item that describes border strip rasterization in the settle phase
(search for the section or list item that contains "border strip rasterization"
or "settle phase" and replace "item 17" with that item’s correct identifier or
an explicit anchor link). Ensure the text still reads naturally (e.g., "see item
X" or "see the 'border strip rasterization' section") so readers can find the
follow-up.

In `@packages/grida-canvas-io-figma/fig2grida.ts`:
- Around line 5-16: In main(), detect the input type (by extension: .json,
.json.gz, .zip vs .fig) and reject combinations where REST-style inputs are
passed with CLI flags meant for Kiwi parsing (specifically options.info and
options.pages); if the input is a REST format and either options.info or
options.pages is set, print a clear error and exit (or return non-zero). Update
the validation near where options are read in main() to check options.info and
options.pages against the inferred REST path and reference the REST handling in
fig2grida-core.ts so callers know these flags are unsupported for REST inputs;
alternatively, if you prefer to support them, wire options.pages into the REST
conversion path in fig2grida-core.ts instead of silently ignoring it (but do not
leave the current behavior).

In `@packages/grida-canvas-io-figma/lib.ts`:
- Around line 1593-1597: The code is dividing an already-normalized Kiwi
letterSpacing a second time; in the object property assignment for
letter_spacing replace the expression that divides node.style.letterSpacing by
(node.style.fontSize || DEFAULT_FONT_SIZE) with just node.style.letterSpacing
(or 0 when missing) so you don't normalize pixels twice — i.e., update the
letter_spacing assignment that currently reads node.style.letterSpacing ?
node.style.letterSpacing / (node.style.fontSize || DEFAULT_FONT_SIZE) : 0 to
return the raw node.style.letterSpacing (using DEFAULT_FONT_SIZE only for
fallback presence checks) and let the single normalization occur in the existing
Kiwi normalization logic.

---

Nitpick comments:
In `@crates/grida-dev/src/bench/report.rs`:
- Around line 45-59: ScenarioParams is too generic and loses per-scenario knobs
(e.g., circle_pan stashes radius in speed and zigzag / pan_with_settle /
realtime drop dy, pause/segment sizes, scroll cadence), so change the shape to
be per-kind and serializable: replace or augment the flat ScenarioParams with a
tagged enum (e.g., ScenarioParams::CirclePan { radius: f32, ... }, ::ZigZag {
dy: f32, segment_pause: f32, ... }, ::PanWithSettle { settle_time: f32, ... },
::Realtime { cadence_ms: u32, ... }) or add all missing optional fields with
clear names and serde tags; update serde::Serialize derive accordingly so each
run remains self-describing and reproducible, and update usages that construct
ScenarioParams to use the new enum variants or new fields (look for
ScenarioParams construction sites and consumers).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7119f29a-ec20-4685-8550-d806e92d3da7

📥 Commits

Reviewing files that changed from the base of the PR and between a875939 and c0d7164.

⛔ Files ignored due to path filters (1)
  • crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm is excluded by !**/*.wasm
📒 Files selected for processing (22)
  • .agents/skills/io-figma/SKILL.md
  • .agents/skills/io-figma/scripts/figma_archive.py
  • .agents/skills/io-grida/SKILL.md
  • .agents/skills/io-svg/SKILL.md
  • crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js
  • crates/grida-canvas-wasm/package.json
  • crates/grida-canvas/src/io/io_grida.rs
  • crates/grida-canvas/src/painter/layer.rs
  • crates/grida-canvas/src/runtime/scene.rs
  • crates/grida-canvas/src/surface/ui/render.rs
  • crates/grida-canvas/src/window/application.rs
  • crates/grida-canvas/tests/fbs_roundtrip.rs
  • crates/grida-dev/src/bench/report.rs
  • crates/grida-dev/src/bench/runner.rs
  • crates/grida-dev/src/platform/native_application.rs
  • docs/wg/feat-2d/optimization.md
  • editor/app/(embed)/embed/v1/debug/page.tsx
  • packages/grida-canvas-io-figma/__tests__/iofigma.rest-api.no-geometry.test.ts
  • packages/grida-canvas-io-figma/fig2grida-core.ts
  • packages/grida-canvas-io-figma/fig2grida.ts
  • packages/grida-canvas-io-figma/lib.ts
  • packages/grida-canvas-io/__tests__/format-roundtrip.test.ts
👮 Files not reviewed due to content moderation or server errors (6)
  • .agents/skills/io-figma/scripts/figma_archive.py
  • editor/app/(embed)/embed/v1/debug/page.tsx
  • .agents/skills/io-svg/SKILL.md
  • .agents/skills/io-grida/SKILL.md
  • packages/grida-canvas-io/tests/format-roundtrip.test.ts
  • crates/grida-canvas/src/io/io_grida.rs

Comment thread .agents/skills/io-figma/SKILL.md Outdated
Comment thread crates/grida-canvas/src/runtime/scene.rs
Comment on lines +729 to 977
fn run_scenarios(
renderer: &mut cg::runtime::scene::Renderer,
frames: u32,
fit_zoom: f32,
) -> Vec<ScenarioResult> {
let (pan_scenarios, zoom_scenarios) = standard_scenarios(fit_zoom);
let mut results = Vec::new();

for ps in &pan_scenarios {
renderer.camera.set_zoom(ps.zoom);
// Warmup at this zoom
renderer.queue_stable();
let _ = renderer.flush();
for _ in 0..5 {
renderer.camera.translate(1.0, 0.0);
renderer.queue_unstable();
let _ = renderer.flush();
}

let stats = run_pan_pass_at(renderer, frames, ps.dx);
results.push(ScenarioResult {
name: ps.name.to_string(),
kind: "pan".to_string(),
params: ScenarioParams {
speed: Some(ps.dx),
zoom: Some(ps.zoom),
zoom_min: None,
zoom_max: None,
},
stats,
});
}
let zoom_wall = zoom_start.elapsed();

if zoom_times.is_empty() {
return ZoomStats { avg_us: 0, fps: 0.0, p50_us: 0, p95_us: 0, p99_us: 0 };
// Circle pan scenarios: realistic trackpad gesture.
// Small radius = tight circles (edges constantly change).
// Large radius = wide sweeping gesture (more cache misses).
struct CirclePanScenario {
name: &'static str,
radius: f32,
zoom: f32,
}

zoom_times.sort();
let n = zoom_times.len();
let avg = zoom_wall.as_micros() as u64 / n as u64;
ZoomStats {
avg_us: avg,
fps: 1_000_000.0 / avg as f64,
p50_us: zoom_times[n / 2],
p95_us: zoom_times[n * 95 / 100],
p99_us: zoom_times[n * 99 / 100],
let zoomed_in_c = (fit_zoom * 4.0).min(10.0);
let circle_scenarios = vec![
CirclePanScenario { name: "circle_small_fit", radius: 200.0, zoom: fit_zoom },
CirclePanScenario { name: "circle_large_fit", radius: 2000.0, zoom: fit_zoom },
CirclePanScenario { name: "circle_small_zoomed", radius: 200.0, zoom: zoomed_in_c },
CirclePanScenario { name: "circle_large_zoomed", radius: 2000.0, zoom: zoomed_in_c },
];

for cs in &circle_scenarios {
renderer.camera.set_zoom(cs.zoom);
renderer.queue_stable();
let _ = renderer.flush();
// Small warmup
for _ in 0..3 {
renderer.camera.translate(1.0, 0.0);
renderer.queue_unstable();
let _ = renderer.flush();
}

let stats = run_circle_pan_pass(renderer, frames, cs.radius);
results.push(ScenarioResult {
name: cs.name.to_string(),
kind: "circle_pan".to_string(),
params: ScenarioParams {
speed: Some(cs.radius),
zoom: Some(cs.zoom),
zoom_min: None,
zoom_max: None,
},
stats,
});
}

// Zigzag pan scenarios: diagonal back-and-forth like reading a document.
// "fast" = continuous motion, no pauses.
// "slow" = pause between each zig/zag segment (settle frames fire, cache goes cold).
struct ZigzagScenario {
name: &'static str,
dx: f32,
dy: f32,
segment_frames: u32,
pause_frames: u32,
zoom: f32,
}

let zoomed_in_z = (fit_zoom * 4.0).min(10.0);
let zigzag_scenarios = vec![
// Fast zigzag: continuous diagonal sweeps, no pauses
ZigzagScenario {
name: "zigzag_fast_fit", dx: 30.0, dy: 5.0,
segment_frames: 20, pause_frames: 0, zoom: fit_zoom,
},
ZigzagScenario {
name: "zigzag_fast_zoomed", dx: 30.0, dy: 5.0,
segment_frames: 20, pause_frames: 0, zoom: zoomed_in_z,
},
// Slow zigzag: zig, stop (settle fires), zag, stop (settle fires)
// pause_frames=3 simulates ~3 settle frames during the "reading" pause
ZigzagScenario {
name: "zigzag_slow_fit", dx: 10.0, dy: 3.0,
segment_frames: 15, pause_frames: 3, zoom: fit_zoom,
},
ZigzagScenario {
name: "zigzag_slow_zoomed", dx: 10.0, dy: 3.0,
segment_frames: 15, pause_frames: 3, zoom: zoomed_in_z,
},
];

for zz in &zigzag_scenarios {
renderer.camera.set_zoom(zz.zoom);
renderer.queue_stable();
let _ = renderer.flush();
for _ in 0..3 {
renderer.camera.translate(1.0, 0.0);
renderer.queue_unstable();
let _ = renderer.flush();
}

let stats = run_zigzag_pan_pass(
renderer, frames, zz.dx, zz.dy, zz.segment_frames, zz.pause_frames,
);
results.push(ScenarioResult {
name: zz.name.to_string(),
kind: "zigzag".to_string(),
params: ScenarioParams {
speed: Some(zz.dx),
zoom: Some(zz.zoom),
zoom_min: None,
zoom_max: None,
},
stats,
});
}

for zs in &zoom_scenarios {
let stats = run_zoom_pass_at(renderer, frames, zs.step, zs.z_min, zs.z_max);
results.push(ScenarioResult {
name: zs.name.to_string(),
kind: "zoom".to_string(),
params: ScenarioParams {
speed: Some(zs.step),
zoom: None,
zoom_min: Some(zs.z_min),
zoom_max: Some(zs.z_max),
},
stats,
});
}

// Settle-interleaved pan scenarios: simulate native viewer's settle countdown.
// settle_interval=12 matches the native viewer's 12-tick countdown at 240Hz (~50ms).
struct SettlePanScenario {
name: &'static str,
dx: f32,
zoom: f32,
settle_interval: u32,
}

let zoomed_in_s = (fit_zoom * 4.0).min(10.0);
let settle_scenarios = vec![
SettlePanScenario { name: "pan_settle_slow_fit", dx: 2.0, zoom: fit_zoom, settle_interval: 12 },
SettlePanScenario { name: "pan_settle_fast_fit", dx: 50.0, zoom: fit_zoom, settle_interval: 12 },
SettlePanScenario { name: "pan_settle_slow_zoomed", dx: 2.0, zoom: zoomed_in_s, settle_interval: 12 },
SettlePanScenario { name: "pan_settle_fast_zoomed", dx: 50.0, zoom: zoomed_in_s, settle_interval: 12 },
];

for ss in &settle_scenarios {
renderer.camera.set_zoom(ss.zoom);
renderer.queue_stable();
let _ = renderer.flush();
for _ in 0..5 {
renderer.camera.translate(1.0, 0.0);
renderer.queue_unstable();
let _ = renderer.flush();
}

let stats = run_pan_with_settle_pass(renderer, frames, ss.dx, ss.settle_interval);
results.push(ScenarioResult {
name: ss.name.to_string(),
kind: "pan_with_settle".to_string(),
params: ScenarioParams {
speed: Some(ss.dx),
zoom: Some(ss.zoom),
zoom_min: None,
zoom_max: None,
},
stats,
});
}

// Realtime event loop simulation scenarios.
// These use real sleep() and simulate the native viewer's 240Hz tick
// thread + settle countdown, producing timings that match actual UX.
struct RealtimeScenario {
name: &'static str,
scroll_interval_ms: f64,
dx: f32,
dy: f32,
zoom: f32,
duration_ms: f64,
}

let zoomed_in_rt = (fit_zoom * 4.0).min(10.0);
let realtime_scenarios = vec![
RealtimeScenario {
name: "rt_pan_fast_fit", scroll_interval_ms: 8.0,
dx: 2.0, dy: 0.0, zoom: fit_zoom, duration_ms: 2000.0,
},
RealtimeScenario {
name: "rt_pan_slow_fit", scroll_interval_ms: 100.0,
dx: 5.0, dy: 0.0, zoom: fit_zoom, duration_ms: 2000.0,
},
RealtimeScenario {
name: "rt_pan_fast_zoomed", scroll_interval_ms: 8.0,
dx: 2.0, dy: 0.0, zoom: zoomed_in_rt, duration_ms: 2000.0,
},
RealtimeScenario {
name: "rt_pan_slow_zoomed", scroll_interval_ms: 100.0,
dx: 5.0, dy: 0.0, zoom: zoomed_in_rt, duration_ms: 2000.0,
},
];

for rt in &realtime_scenarios {
renderer.camera.set_zoom(rt.zoom);
renderer.queue_stable();
let _ = renderer.flush();
warmup(renderer);

let stats = run_realtime_pan_pass(
renderer, rt.scroll_interval_ms,
rt.dx, rt.dy, rt.duration_ms, 12,
);
results.push(ScenarioResult {
name: rt.name.to_string(),
kind: "realtime".to_string(),
params: ScenarioParams {
speed: Some(rt.dx),
zoom: Some(rt.zoom),
zoom_min: None,
zoom_max: None,
},
stats,
});
}

results
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reset the camera before every scenario.

Right now each pass inherits the previous pass’s translation. That makes the matrix order-dependent, and some scenarios here are explicitly non-zero-sum (zigzag keeps adding dy, realtime only pans forward, even the warmups translate right), so later runs can benchmark a very different viewport than the original fit.

Save the fitted transform once and restore it before each scenario, or call fit_camera_to_scene() per scenario before applying that scenario’s zoom.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/grida-dev/src/bench/runner.rs` around lines 729 - 977, run_scenarios
currently leaves camera translations from prior passes, causing later scenarios
to start with a moved viewport; fix by restoring the original fitted camera
transform before each scenario iteration (e.g. save the fitted transform after
initial fit and call renderer.camera.set_transform(saved) or call
fit_camera_to_scene(renderer) at the start of each scenario loop) so that
functions like run_pan_pass_at, run_circle_pan_pass, run_zigzag_pan_pass,
run_zoom_pass_at, run_pan_with_settle_pass and run_realtime_pan_pass always
start from the same baseline; apply this restore before any warmup/translate
calls (before queue_stable/queue_unstable and before warmup()) in each scenario
block.

Comment thread docs/wg/feat-2d/optimization.md Outdated
Comment thread packages/grida-canvas-io-figma/fig2grida.ts
Comment thread packages/grida-canvas-io-figma/lib.ts
- Expanded the SKILL.md documentation to include detailed descriptions of new performance metrics for pan and zoom operations.
- Introduced an expanded scenario matrix covering various gesture types and their impact on rendering performance.
- Clarified the significance of `realtime` scenarios to better align benchmark results with user experience.
- Added guidelines for ensuring stable frames recapture caches to maintain high frame rates during rendering.
- Added validation to reject --info and --pages flags when the input file is not a .fig format, ensuring proper usage of the CLI.
- Updated error messages to inform users about the limitations of these flags with non-.fig inputs.
- Updated the camera change conditions to utilize a more precise check for zoom changes, enhancing the efficiency of the zoom image cache.
- Revised documentation to clarify the handling of pan-only and no-change frames in relation to zoom caching, ensuring better performance during rendering.
- Adjusted performance metrics in the documentation to reflect the impact of these changes on rendering speed.
- Simplified the `extractCanvases` function to improve readability and maintainability.
- Consolidated canvas conversion logic into a new `convertRootsToPackedScene` function, enhancing code organization.
- Introduced an `emptyPackedScene` utility to standardize the creation of empty scene documents.
- Updated context handling for merging nodes to prevent ID collisions across canvases.
- Updated `figma_archive.py` to support optional export of node renderings as PNGs, improving the archiving process for Figma files.
- Enhanced documentation to clarify usage and output structure, including the new `--export` flag for exporting images.
- Revised README to reflect changes in the tool's capabilities and usage instructions.
- Added logic to handle export settings for nodes, ensuring only nodes with export presets are processed.
- Revised module documentation for the layout engine to clarify its purpose and functionality, emphasizing the handling of layout nodes and virtual grouping nodes.
- Improved comments in the code to enhance understanding of layout computation and schema-position correction.
- Streamlined the extraction of layouts for all nodes, ensuring clarity on how Taffy and non-Taffy nodes are processed.
- Ensured that the layout engine maintains a clean separation of concerns between layout computation and geometry transformation.
…onents

- Added `rustfmt` and `clippy` to the toolchain components in `rust-toolchain.toml`, enhancing code formatting and linting capabilities.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant