perf(canvas): AA toggle, GPU sync for benchmarks, AA cost findings#633
perf(canvas): AA toggle, GPU sync for benchmarks, AA cost findings#633softmarshmallow merged 5 commits intomainfrom
Conversation
- Renamed scenarios to remove "BUG" prefix and reflect their purpose as no-cache baselines. - Updated comments in the code to clarify the intent of passing `stable=true` for full draw measurements. - Adjusted the Markdown documentation to align with the new scenario naming conventions.
…dings - Add `force_no_aa` field to `RenderPolicy` with `anti_alias()` method, allowing A/B measurement of anti-aliasing cost at different zoom levels. Paint functions (`sk_solid_paint`, `sk_paint_stack`, `sk_paint_stack_without_images`) now accept an `aa: bool` parameter instead of hardcoding `true`. - Add `sync_gpu` config on `RuntimeRendererConfig`. When enabled, `gpu_flush()` calls `flush_submit_and_sync_cpu()` instead of the async `flush_and_submit()`, making per-stage timing in `FrameFlushStats` reflect actual GPU execution cost. Benchmarks (`bench` and `bench-report`) enable this by default. - Add `--no-aa` flag to the bench CLI for A/B AA cost measurement. - Add `skia_bench_subpixel` isolated GPU benchmark measuring AA cost at sub-pixel scale (0.02x zoom). Key finding: AA on sub-pixel geometry is 3.2x more expensive than AA off. - Add `docs/wg/feat-2d/aa-cost-findings.md` documenting the investigation results, including the GPU flush timing discovery. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughAdds anti-aliasing control and GPU sync to the canvas stack, updates paint helper APIs and their call sites, introduces a GPU-timed Skia microbenchmark example, updates benchmark runner/CLI and scenario naming, and adds documentation of AA cost findings. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
rect rgba(200,200,255,0.5)
participant CLI
participant BenchRunner
participant Renderer
participant GPU
end
CLI->>BenchRunner: start benchmark (args, e.g. --no-aa)
BenchRunner->>Renderer: init(policy: force_no_aa = args.no_aa)
BenchRunner->>Renderer: set_sync_gpu(true)
loop per-scenario
BenchRunner->>Renderer: render_frame(pics, zoom, aa=policy.anti_alias())
Renderer->>GPU: submit draw commands
alt sync_gpu = true
Renderer->>GPU: flush_submit_and_sync_cpu() -- blocks until GPU complete
GPU-->>Renderer: completed
else
Renderer->>GPU: flush_and_submit() -- returns immediately
end
Renderer-->>BenchRunner: report elapsed (GPU-synced if enabled)
end
BenchRunner->>CLI: print results (AA on/off, baseline_nocache_zoom)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
# Conflicts: # .agents/skills/cg-perf/SKILL.md
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (6)
crates/grida-canvas/src/painter/paint.rs (2)
27-31: Document the newaaparameter in the function's doc comment.The
# Argumentssection listspaints,size, andimages, but the newly addedaaparameter is not documented. Consider adding it for API clarity.📝 Suggested documentation update
/// # Arguments /// * `paints` - Array of paints to blend together /// * `size` - Container size for paint calculations /// * `images` - Image repository for image paint resolution +/// * `aa` - Whether to enable anti-aliasing on the resulting paint /// /// # Returns🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/grida-canvas/src/painter/paint.rs` around lines 27 - 31, The doc comment for sk_paint_stack is missing documentation for the new aa parameter; update the function's Rust doc comment (the /// block above sk_paint_stack) to add aa under the # Arguments section, describing that aa is a boolean that toggles anti-aliasing (e.g., true = enable anti-aliasing, false = disable) and any effects on rendering behavior so callers understand its purpose alongside paints, size, and images.
87-91: Document the newaaparameter for consistency.Similar to
sk_paint_stack, the# Argumentssection should include theaaparameter.📝 Suggested documentation update
/// # Arguments /// * `paints` - Array of paints to blend together (image paints will be ignored) /// * `size` - Container size for paint calculations +/// * `aa` - Whether to enable anti-aliasing on the resulting paint /// /// # Returns🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/grida-canvas/src/painter/paint.rs` around lines 87 - 91, The public function sk_paint_stack_without_images has a new boolean parameter aa that isn't documented; update its doc comment (the /// doc block above sk_paint_stack_without_images) to include an entry in the Arguments/# Arguments section describing aa (e.g., "aa: enable/disable anti-aliasing for the returned skia_safe::Paint") matching the style and wording used for sk_paint_stack so docs stay consistent.crates/grida-canvas/src/text/attributed_paragraph.rs (1)
38-48: Text rendering always uses AA regardless of render policy.This helper always passes
truefor anti-aliasing. Unlike shape rendering inpainter.rswhich usesself.policy.anti_alias(), text will always render with AA enabled—even whenforce_no_aais set for benchmarking.If this is intentional (text quality often requires AA), consider adding a comment explaining the decision. If text should also respect the policy, this function would need to accept an
aaparameter.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/grida-canvas/src/text/attributed_paragraph.rs` around lines 38 - 48, The resolve_fill_paint helper always forces anti-aliasing by passing `true` to paint_util::sk_paint_stack and paint_util::sk_paint_stack_without_images; update resolve_fill_paint to accept an `aa: bool` parameter (or a descriptive name like `anti_alias`) and forward that boolean to those two calls so text respects rendering policy, then update all callers (text layout/render paths) to pass the renderer's policy value (e.g., self.policy.anti_alias() or force_no_aa inversion) instead of hardcoding true; alternatively, if AA-for-text is intentional, add a clear comment inside resolve_fill_paint explaining why AA is always enabled.docs/wg/feat-2d/aa-cost-findings.md (2)
112-116: Consider using a relative link for internal docs reference.The reference to
docs/wg/feat-2d/optimization.mdcould be a relative link for better navigation within the documentation site:📝 Proposed change
## Related - `crates/grida-canvas/examples/skia_bench/skia_bench_subpixel.rs` — isolated benchmark -- `docs/wg/feat-2d/optimization.md` — master optimization catalog +- [optimization.md](./optimization.md) — master optimization catalog - Chromium pinch-zoom: reduced rasterization quality during interaction🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/wg/feat-2d/aa-cost-findings.md` around lines 112 - 116, Update the internal docs reference to use a relative link instead of an absolute-looking path: replace the `docs/wg/feat-2d/optimization.md` entry in the "Related" list with a relative link such as `./optimization.md` or `../feat-2d/optimization.md` (choose the correct relative path from the current file's location) so the link resolves correctly within the docs site; ensure the link text remains `optimization.md` (or keep the existing descriptive text) and verify it navigates properly after the change.
1-5: Addformat: mdfrontmatter for MDX compatibility.Per coding guidelines, documentation files in
docs/{wg,reference}/**that don't use JSX/MDX features should includeformat: mdfrontmatter to opt out of MDX parsing. This prevents potential build issues with angle brackets or other special characters.📝 Proposed fix
+--- +format: md +--- + # Anti-Aliasing Cost at Sub-Pixel Scale **Date:** 2026-04-07As per coding guidelines: "For MDX compatibility, add
format: mdfrontmatter to documentation files that don't use JSX/MDX features to opt out of MDX parsing and prevent build issues with angle brackets."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/wg/feat-2d/aa-cost-findings.md` around lines 1 - 5, This markdown document "Anti-Aliasing Cost at Sub-Pixel Scale" is missing MDX opt-out frontmatter; add a top-matter block containing format: md (i.e., a frontmatter section with format: md) to the very top of the file so the MDX parser treats it as plain Markdown and avoids parsing angle-bracket content, ensuring the new frontmatter appears before the existing title/header.crates/grida-canvas/examples/skia_bench/skia_bench_subpixel.rs (1)
135-144: Nitpick: Float equality comparison.Direct float comparison
zoom != 1.0works here because the values are hardcoded literals (1.0 and 0.02), but it's a minor code smell. A threshold comparison would be more defensive:💡 Alternative approach
- if zoom != 1.0 { + if (zoom - 1.0).abs() > f32::EPSILON { canvas.save(); canvas.scale((zoom, zoom)); } for pic in pics { canvas.draw_picture(pic, None, None); } - if zoom != 1.0 { + if (zoom - 1.0).abs() > f32::EPSILON { canvas.restore(); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/grida-canvas/examples/skia_bench/skia_bench_subpixel.rs` around lines 135 - 144, The code uses direct float equality checks `zoom != 1.0` around canvas.save()/scale() and canvas.restore(); change these to an epsilon-based comparison (e.g., `(zoom - 1.0).abs() > 1e-6` or use `f32::EPSILON`/`f64::EPSILON` depending on zoom's type) to defensively detect when zoom meaningfully differs from 1.0 before calling canvas.scale((zoom, zoom)), ensuring both the pre-scale conditional and the matching restore check use the same epsilon comparison.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/grida-canvas/src/painter/painter.rs`:
- Line 350: Helpers are still hardcoding anti-aliasing; update
shadow::draw_drop_shadow, shadow::draw_inner_shadow,
text_stroke::draw_text_stroke, text_stroke::draw_text_stroke_outside_fast_pre to
take an anti_alias: bool (or read PainterPolicy) and call
set_anti_alias(anti_alias) instead of set_anti_alias(true), then propagate that
flag from the caller sites in painter.rs and from VNPainter (add an anti_alias
field or read self.policy.anti_alias()) so all render paths (stroked text,
shadows, vector-network) honor self.policy.anti_alias(); ensure public
signatures and call sites are updated consistently.
In `@crates/grida-canvas/src/runtime/render_policy.rs`:
- Around line 105-107: RenderPolicy's force_no_aa field is not preserved across
the flags bridge; add a bit for force_no_aa to the RenderPolicyFlags
enum/bitflags and update the conversion logic in RenderPolicy::to_flags (or
policy.to_flags()) and RenderPolicy::from_flags so the bit is set when
force_no_aa is true and read back when reconstructing the policy, removing the
hardcoded false that currently appears in the from_flags path.
In `@crates/grida-dev/src/bench/runner.rs`:
- Line 1794: Summary: The serialized "kind" value for ScenarioResult was renamed
from "zoom_forced_stable" to "baseline_nocache_zoom", which is a breaking change
for downstream consumers. Fix: restore backward compatibility by either (a)
reverting the literal back to "zoom_forced_stable" in the assignment that sets
kind, or (b) update ScenarioResult's serde serialization to accept/emit both
names (use serde rename/alias or a custom Serialize/Deserialize impl) so
existing reports still parse; reference the affected symbol "kind" on
ScenarioResult and the serialization logic in report.rs when making the change,
and if the rename is intentional ensure you also bump the package/report format
version and update any CI comparison scripts/tests that depend on the old value.
---
Nitpick comments:
In `@crates/grida-canvas/examples/skia_bench/skia_bench_subpixel.rs`:
- Around line 135-144: The code uses direct float equality checks `zoom != 1.0`
around canvas.save()/scale() and canvas.restore(); change these to an
epsilon-based comparison (e.g., `(zoom - 1.0).abs() > 1e-6` or use
`f32::EPSILON`/`f64::EPSILON` depending on zoom's type) to defensively detect
when zoom meaningfully differs from 1.0 before calling canvas.scale((zoom,
zoom)), ensuring both the pre-scale conditional and the matching restore check
use the same epsilon comparison.
In `@crates/grida-canvas/src/painter/paint.rs`:
- Around line 27-31: The doc comment for sk_paint_stack is missing documentation
for the new aa parameter; update the function's Rust doc comment (the /// block
above sk_paint_stack) to add aa under the # Arguments section, describing that
aa is a boolean that toggles anti-aliasing (e.g., true = enable anti-aliasing,
false = disable) and any effects on rendering behavior so callers understand its
purpose alongside paints, size, and images.
- Around line 87-91: The public function sk_paint_stack_without_images has a new
boolean parameter aa that isn't documented; update its doc comment (the /// doc
block above sk_paint_stack_without_images) to include an entry in the
Arguments/# Arguments section describing aa (e.g., "aa: enable/disable
anti-aliasing for the returned skia_safe::Paint") matching the style and wording
used for sk_paint_stack so docs stay consistent.
In `@crates/grida-canvas/src/text/attributed_paragraph.rs`:
- Around line 38-48: The resolve_fill_paint helper always forces anti-aliasing
by passing `true` to paint_util::sk_paint_stack and
paint_util::sk_paint_stack_without_images; update resolve_fill_paint to accept
an `aa: bool` parameter (or a descriptive name like `anti_alias`) and forward
that boolean to those two calls so text respects rendering policy, then update
all callers (text layout/render paths) to pass the renderer's policy value
(e.g., self.policy.anti_alias() or force_no_aa inversion) instead of hardcoding
true; alternatively, if AA-for-text is intentional, add a clear comment inside
resolve_fill_paint explaining why AA is always enabled.
In `@docs/wg/feat-2d/aa-cost-findings.md`:
- Around line 112-116: Update the internal docs reference to use a relative link
instead of an absolute-looking path: replace the
`docs/wg/feat-2d/optimization.md` entry in the "Related" list with a relative
link such as `./optimization.md` or `../feat-2d/optimization.md` (choose the
correct relative path from the current file's location) so the link resolves
correctly within the docs site; ensure the link text remains `optimization.md`
(or keep the existing descriptive text) and verify it navigates properly after
the change.
- Around line 1-5: This markdown document "Anti-Aliasing Cost at Sub-Pixel
Scale" is missing MDX opt-out frontmatter; add a top-matter block containing
format: md (i.e., a frontmatter section with format: md) to the very top of the
file so the MDX parser treats it as plain Markdown and avoids parsing
angle-bracket content, ensuring the new frontmatter appears before the existing
title/header.
🪄 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: a91e8ea8-f6e2-4753-b222-59fa3bbe9738
📒 Files selected for processing (16)
.agents/skills/cg-perf/SKILL.mdcrates/grida-canvas/Cargo.tomlcrates/grida-canvas/examples/golden_sk_paints.rscrates/grida-canvas/examples/skia_bench/skia_bench_subpixel.rscrates/grida-canvas/src/cache/paragraph.rscrates/grida-canvas/src/painter/paint.rscrates/grida-canvas/src/painter/painter.rscrates/grida-canvas/src/painter/text_stroke.rscrates/grida-canvas/src/runtime/config.rscrates/grida-canvas/src/runtime/render_policy.rscrates/grida-canvas/src/runtime/scene.rscrates/grida-canvas/src/text/attributed_paragraph.rscrates/grida-canvas/src/vectornetwork/vn_painter.rscrates/grida-dev/src/bench/args.rscrates/grida-dev/src/bench/runner.rsdocs/wg/feat-2d/aa-cost-findings.md
|
|
||
| let mut p = SkPaint::default(); | ||
| p.set_anti_alias(true); | ||
| p.set_anti_alias(self.policy.anti_alias()); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd -i -0 'text_stroke\.rs|shadow\.rs|vn_painter\.rs' | while IFS= read -r -d '' f; do
echo "== $f =="
rg -n -C2 'fn draw_text_stroke|fn draw_text_stroke_outside_fast_pre|fn draw_drop_shadow|fn draw_inner_shadow|struct VNPainter|impl VNPainter|set_anti_alias' "$f"
done
echo "== call sites from painter =="
rg -n -C2 'draw_text_stroke(_outside_fast_pre)?\(|draw_drop_shadow\(|draw_inner_shadow\(|VNPainter::new_with_images|draw_vector_strokes\(' --type=rustRepository: gridaco/grida
Length of output: 10121
🏁 Script executed:
# Get full signatures and first 20 lines of each helper function
echo "=== text_stroke.rs functions ==="
sed -n '12,35p' crates/grida-canvas/src/painter/text_stroke.rs
echo ""
sed -n '96,120p' crates/grida-canvas/src/painter/text_stroke.rs
echo ""
echo "=== shadow.rs draw_drop_shadow ==="
sed -n '40,50p' crates/grida-canvas/src/painter/shadow.rs
echo ""
echo "=== shadow.rs draw_inner_shadow ==="
sed -n '90,100p' crates/grida-canvas/src/painter/shadow.rs
echo ""
echo "=== vn_painter.rs struct and impl ==="
sed -n '28,80p' crates/grida-canvas/src/vectornetwork/vn_painter.rsRepository: gridaco/grida
Length of output: 4478
--no-aa flag is bypassed by helper-driven render paths.
The inline paints in painter.rs now respect self.policy.anti_alias(), but the following helpers still ignore the policy:
shadow::draw_drop_shadow()andshadow::draw_inner_shadow()hardcodeset_anti_alias(true)and accept no AA parametertext_stroke::draw_text_stroke()andtext_stroke::draw_text_stroke_outside_fast_pre()accept no AA parameterVNPainterhas no AA mechanism
This means stroked text, shape shadows, and vector-network rendering will continue using anti-aliasing even when --no-aa is enabled. Update these helpers to accept or read an anti-alias flag from the rendering policy to make the benchmark toggle effective across all render paths.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/grida-canvas/src/painter/painter.rs` at line 350, Helpers are still
hardcoding anti-aliasing; update shadow::draw_drop_shadow,
shadow::draw_inner_shadow, text_stroke::draw_text_stroke,
text_stroke::draw_text_stroke_outside_fast_pre to take an anti_alias: bool (or
read PainterPolicy) and call set_anti_alias(anti_alias) instead of
set_anti_alias(true), then propagate that flag from the caller sites in
painter.rs and from VNPainter (add an anti_alias field or read
self.policy.anti_alias()) so all render paths (stroked text, shadows,
vector-network) honor self.policy.anti_alias(); ensure public signatures and
call sites are updated consistently.
| results.push(ScenarioResult { | ||
| name: fss.name.to_string(), | ||
| kind: "zoom_forced_stable".to_string(), | ||
| kind: "baseline_nocache_zoom".to_string(), |
There was a problem hiding this comment.
Breaking change: kind field value renamed.
The kind field value changed from "zoom_forced_stable" to "baseline_nocache_zoom". Since ScenarioResult derives serde::Serialize (per report.rs:32-43), this is a breaking change for any downstream consumers—CI comparison scripts, report formatters, or dashboards—that match on this string value.
Consider whether this warrants a version bump or migration notice if existing benchmark reports are being compared across commits.
🤖 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` at line 1794, Summary: The serialized
"kind" value for ScenarioResult was renamed from "zoom_forced_stable" to
"baseline_nocache_zoom", which is a breaking change for downstream consumers.
Fix: restore backward compatibility by either (a) reverting the literal back to
"zoom_forced_stable" in the assignment that sets kind, or (b) update
ScenarioResult's serde serialization to accept/emit both names (use serde
rename/alias or a custom Serialize/Deserialize impl) so existing reports still
parse; reference the affected symbol "kind" on ScenarioResult and the
serialization logic in report.rs when making the change, and if the rename is
intentional ensure you also bump the package/report format version and update
any CI comparison scripts/tests that depend on the old value.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/grida-canvas/src/runtime/scene.rs (1)
1540-1542: Consider documentingset_sync_gpuas benchmark-oriented API.A short doc comment here would help prevent accidental enabling in interactive rendering paths.
Suggested doc tweak
pub fn set_sync_gpu(&mut self, sync: bool) { + // Benchmark/timing mode: when true, flushes block until GPU completion. + // Keep disabled for normal interactive rendering. self.config.sync_gpu = sync; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/grida-canvas/src/runtime/scene.rs` around lines 1540 - 1542, Add a short doc comment above the set_sync_gpu method explaining this API is intended for benchmarking only and should not be enabled in interactive rendering paths; mention that it toggles self.config.sync_gpu and can degrade real-time performance or block GPU work, so callers (tests/benchmarks) should use it rather than production UI code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@crates/grida-canvas/src/runtime/scene.rs`:
- Around line 1540-1542: Add a short doc comment above the set_sync_gpu method
explaining this API is intended for benchmarking only and should not be enabled
in interactive rendering paths; mention that it toggles self.config.sync_gpu and
can degrade real-time performance or block GPU work, so callers
(tests/benchmarks) should use it rather than production UI code.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c413b52c-c7f5-43c0-a8ba-cc072f0d76f2
📒 Files selected for processing (3)
.agents/skills/cg-perf/SKILL.mdcrates/grida-canvas/Cargo.tomlcrates/grida-canvas/src/runtime/scene.rs
✅ Files skipped from review due to trivial changes (1)
- crates/grida-canvas/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (1)
- .agents/skills/cg-perf/SKILL.md
Add FLAG_FORCE_NO_AA to the RenderPolicyFlags bitmap so that from_flags/to_flags round-trips preserve the AA override across the WASM/host boundary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
force_no_aafield +anti_alias()method. Paint functions acceptaa: boolinstead of hardcodingtrue. Bench CLI exposes--no-aafor A/B measurement.sync_gpuconfig makesgpu_flush()block until GPU completes, fixing per-stage timing accuracy (mid_flush_uswas measuring command submission, not GPU execution).skia_bench_subpixelmeasures AA cost at sub-pixel scale. Finding: AA on sub-pixel geometry is 3.2x more expensive than AA off.docs/wg/feat-2d/aa-cost-findings.mdwith benchmark data and optimization implications.Test plan
cargo test -p cg— 33 passedcargo check -p cg -p grida-canvas-wasm -p grida-dev— all compilecargo clippy -p cg --no-deps— no new warnings--no-aaproduces measurable difference on isolated bench (3.2x at 40K nodes, 0.02x zoom)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation