Skip to content

perf(canvas): sub-pixel node culling at low zoom#636

Merged
softmarshmallow merged 1 commit intomainfrom
feature/funny-shockley
Apr 7, 2026
Merged

perf(canvas): sub-pixel node culling at low zoom#636
softmarshmallow merged 1 commit intomainfrom
feature/funny-shockley

Conversation

@softmarshmallow
Copy link
Copy Markdown
Member

@softmarshmallow softmarshmallow commented Apr 7, 2026

Summary

  • Skip drawing nodes whose projected screen size is below 0.5px at low zoom levels
  • Pre-computes min_world_size = 0.5 / zoom and filters indices in Renderer::frame() using absolute_render_bounds (O(1) per node via DenseNodeMap)
  • Applies to both the full-viewport fast path and R-tree query path, cascading through ViewportCull → draw loop

Benchmark (Apple M2 Pro, 136K-node fixture)

Scenario Metric Before After Delta
baseline_nocache_fit draw_us 8,867 7,417 -16%
fl_16ms MAX 110,407 85,990 -22%
fl_500ms draw_us 9,144 5,231 -43%

No regressions on pan/zoom cached frames or effects-heavy scenes (bench.grida 72K nodes, 7110 effects).

Test plan

  • cargo test -p cg — all 900+ tests pass
  • cargo check -p cg -p grida-canvas-wasm -p grida-dev — all crates compile
  • cargo clippy -p cg --no-deps — no new warnings
  • GPU benchmark on 136K-node and 72K-node fixtures — no regressions

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor

    • Improved rendering performance by skipping very-small (sub-pixel) content, reducing unnecessary draw calls, GPU updates, and CPU work for smoother frame rates.
  • Documentation

    • Updated docs to describe the implemented sub-pixel culling optimization and its runtime impact on rendering efficiency.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 7, 2026

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

Project Deployment Actions Updated (UTC)
blog Ready Ready Preview, Comment Apr 7, 2026 1:05pm
docs Ready Ready Preview, Comment Apr 7, 2026 1:05pm
grida Ready Ready Preview, Comment Apr 7, 2026 1:05pm
viewer Ready Ready Preview, Comment Apr 7, 2026 1:05pm
3 Skipped Deployments
Project Deployment Actions Updated (UTC)
backgrounds Ignored Ignored Preview Apr 7, 2026 1:05pm
code Ignored Ignored Apr 7, 2026 1:05pm
legacy Ignored Ignored Apr 7, 2026 1:05pm

Request Review

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

Walkthrough

Adds sub-pixel LOD culling to Renderer::frame: computes min_world_size = 0.5 / zoom and filters visible layer indices by per-node absolute render bounds (width/height vs min_world_size) in both full-viewport and R-tree query paths; nodes without bounds remain included. Documentation updated to mark the feature implemented.

Changes

Cohort / File(s) Summary
Sub-pixel LOD Culling
crates/grida-canvas/src/runtime/scene.rs
Enabled previously unused zoom parameter and introduced sub-pixel size culling: compute min_world_size = 0.5 / zoom, filter layer indices via per-node absolute_render_bounds in both the full-viewport fast path and the R-tree query path; conservatively include nodes missing bounds.
Feature Documentation
docs/wg/feat-2d/optimization.md
Mark item 52 as implemented and replace design notes with concrete implementation details: min_world_size formula, O(1) lookup via DenseNodeMap, scope (both visibility paths), and measured impact table; removes prior parallel-per-layer-bounds design note.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Renderer
    participant GeometryCache
    participant FramePlan
    participant Compositor
    Client->>Renderer: request frame(zoom, ...)
    Renderer->>GeometryCache: lookup absolute_render_bounds(node)
    GeometryCache-->>Renderer: bounds (or None)
    Renderer->>FramePlan: compute min_world_size = 0.5 / zoom
    Renderer->>FramePlan: filter layer indices by bounds vs min_world_size
    FramePlan-->>Compositor: viewport & indices (culled)
    Compositor->>GPU: skip draw calls for culled indices / submit draws
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

cg, performance

🚥 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 'perf(canvas): sub-pixel node culling at low zoom' accurately and concisely describes the main change: implementing sub-pixel culling optimization in the canvas renderer.
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 feature/funny-shockley

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.

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.

🧹 Nitpick comments (2)
docs/wg/feat-2d/optimization.md (2)

1357-1361: Nit: Word variant consistency.

Static analysis flagged mixed usage of "pre-compute" variants. Consider using "Precomputes" (unhyphenated) for consistency with common technical writing style.

     Drop leaf nodes from the frame plan when both projected dimensions
-    fall below a threshold (0.5 px). Pre-computes
+    fall below a threshold (0.5 px). Precomputes
     `min_world_size = 0.5 / zoom` and filters indices during
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/wg/feat-2d/optimization.md` around lines 1357 - 1361, The phrase
"Pre-computes" in this paragraph is using a hyphenated variant; update it to
"Precomputes" for consistency with the surrounding technical style and ensure
other occurrences in this section use the same unhyphenated form (references:
min_world_size, Renderer::frame(), absolute_render_bounds, DenseNodeMap, frame
plan, projected dimensions).

1375-1385: Minor: Benchmark numbers differ from PR summary.

The benchmark results in this table differ slightly from those in the PR description:

Metric PR Summary This Doc
baseline_nocache_fit draw_us 7,417 (−16%) 7,248 (−18%)
fl_16ms MAX 85,990 (−22%) 82,731 (−25%)

This could be from different benchmark runs. Consider aligning these values if one set is more authoritative, or note that results may vary between runs.

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

In `@docs/wg/feat-2d/optimization.md` around lines 1375 - 1385, The documented
benchmark table contains numbers that conflict with the PR summary for specific
metrics (baseline_nocache_fit draw_us and fl_16ms MAX); locate the results in
the doc table and reconcile them by either updating the table to use the
authoritative numbers from the PR (or vice versa) or add a short note stating
that benchmarks vary between runs and which run is authoritative, referencing
the affected metrics (baseline_nocache_fit draw_us, fl_16ms MAX) and the
implementation location Renderer::frame() in runtime/scene.rs so readers know
which code the numbers apply to.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@docs/wg/feat-2d/optimization.md`:
- Around line 1357-1361: The phrase "Pre-computes" in this paragraph is using a
hyphenated variant; update it to "Precomputes" for consistency with the
surrounding technical style and ensure other occurrences in this section use the
same unhyphenated form (references: min_world_size, Renderer::frame(),
absolute_render_bounds, DenseNodeMap, frame plan, projected dimensions).
- Around line 1375-1385: The documented benchmark table contains numbers that
conflict with the PR summary for specific metrics (baseline_nocache_fit draw_us
and fl_16ms MAX); locate the results in the doc table and reconcile them by
either updating the table to use the authoritative numbers from the PR (or vice
versa) or add a short note stating that benchmarks vary between runs and which
run is authoritative, referencing the affected metrics (baseline_nocache_fit
draw_us, fl_16ms MAX) and the implementation location Renderer::frame() in
runtime/scene.rs so readers know which code the numbers apply to.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b712cb76-89a0-46de-b223-87b75f2f8cbb

📥 Commits

Reviewing files that changed from the base of the PR and between f4ffe54 and e663b27.

📒 Files selected for processing (2)
  • crates/grida-canvas/src/runtime/scene.rs
  • docs/wg/feat-2d/optimization.md

At low zoom levels (e.g. 0.02 fit-zoom on 136K-node scenes), most
nodes project to sub-pixel screen size. Skip these during frame plan
building to eliminate downstream draw calls, picture cache lookups,
and GPU rasterization for invisible results.

Pre-computes min_world_size = 0.5 / zoom and filters indices in
Renderer::frame() using absolute_render_bounds from the geometry
cache (O(1) per node via DenseNodeMap). Applies to both the
full-viewport fast path and the R-tree query path.

Measured on Apple M2 Pro, 136K-node fixture:
- draw_us: -16% to -43% depending on scenario
- fl_16ms MAX: -22% (110ms → 86ms)
- No regressions on pan/zoom cached frames or effects-heavy scenes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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: 1

🧹 Nitpick comments (1)
crates/grida-canvas/src/runtime/scene.rs (1)

2146-2224: Add targeted boundary tests for the new culling threshold.

Please add focused tests around 0.5px threshold behavior (<, ==, >), zoom == 1.0 gating, and missing-bounds inclusion. This logic is performance-critical and easy to regress silently.

🤖 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 2146 - 2224, Add
focused unit tests around the sub-pixel culling logic: create tests that
exercise SUBPIXEL_CULL_THRESHOLD/min_world_size behavior by setting zoom and
render-bounds so nodes are just below, exactly equal to, and just above the
0.5px projected threshold and assert passes_size_cull returns false/true as
expected; add a test where zoom == 1.0 to ensure min_world_size gating disables
size culling and all nodes are included; include a case where a node has no
render bounds (or no layer entry) to assert it is conservatively included;
finally, exercise the full-viewport fast-path by making layer_count ==
rtree_size and the scene_envelope fully contained in bounds to ensure the code
path that returns 0..n is taken and still respects size-cull when min_world_size
> 0.0 (use the functions/fields scene_cache.layers.layers,
scene_cache.layer_index.size(), scene_cache.scene_envelope(), passes_size_cull,
and self.scene_cache.intersects to set up and verify each scenario).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/wg/feat-2d/optimization.md`:
- Line 1358: The doc uses inconsistent capitalization/hyphenation for the term
"precompute"; replace the instance "Pre-computes" (found in the sentence
containing "fall below a threshold (0.5 px). Pre-computes") with the
standardized form "precompute" to match other occurrences (e.g.,
"precompute"/"precomputed") and ensure consistent wording across the document.

---

Nitpick comments:
In `@crates/grida-canvas/src/runtime/scene.rs`:
- Around line 2146-2224: Add focused unit tests around the sub-pixel culling
logic: create tests that exercise SUBPIXEL_CULL_THRESHOLD/min_world_size
behavior by setting zoom and render-bounds so nodes are just below, exactly
equal to, and just above the 0.5px projected threshold and assert
passes_size_cull returns false/true as expected; add a test where zoom == 1.0 to
ensure min_world_size gating disables size culling and all nodes are included;
include a case where a node has no render bounds (or no layer entry) to assert
it is conservatively included; finally, exercise the full-viewport fast-path by
making layer_count == rtree_size and the scene_envelope fully contained in
bounds to ensure the code path that returns 0..n is taken and still respects
size-cull when min_world_size > 0.0 (use the functions/fields
scene_cache.layers.layers, scene_cache.layer_index.size(),
scene_cache.scene_envelope(), passes_size_cull, and self.scene_cache.intersects
to set up and verify each scenario).
🪄 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: a9e926b5-36df-46ef-96bd-3c613aea176e

📥 Commits

Reviewing files that changed from the base of the PR and between e663b27 and bfd5ce1.

📒 Files selected for processing (2)
  • crates/grida-canvas/src/runtime/scene.rs
  • docs/wg/feat-2d/optimization.md

fixture, ~38% of visible leaves have both dimensions below 0.5 px
at zoom 0.02. Culling them reduces `draw_us` by 6–18% and GPU
`mid_flush_us` by up to 24%.
fall below a threshold (0.5 px). Pre-computes
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 | 🟡 Minor

Use consistent wording for “precompute”.

Line 1358 uses “Pre-computes”; elsewhere the doc uses “precompute/precomputed”. Please standardize to one form for consistency.

✏️ Suggested doc edit
-    fall below a threshold (0.5 px). Pre-computes
+    fall below a threshold (0.5 px). Precomputes
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fall below a threshold (0.5 px). Pre-computes
fall below a threshold (0.5 px). Precomputes
🧰 Tools
🪛 LanguageTool

[uncategorized] ~1358-~1358: Do not mix variants of the same word (‘pre-compute’ and ‘precompute’) within a single text.
Context: ...ns fall below a threshold (0.5 px). Pre-computes min_world_size = 0.5 / zoom and f...

(EN_WORD_COHERENCY)

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

In `@docs/wg/feat-2d/optimization.md` at line 1358, The doc uses inconsistent
capitalization/hyphenation for the term "precompute"; replace the instance
"Pre-computes" (found in the sentence containing "fall below a threshold (0.5
px). Pre-computes") with the standardized form "precompute" to match other
occurrences (e.g., "precompute"/"precomputed") and ensure consistent wording
across the document.

@softmarshmallow softmarshmallow merged commit 3e1ec6d into main Apr 7, 2026
16 checks passed
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