Skip to content

cg: headless gpu#567

Merged
softmarshmallow merged 6 commits intomainfrom
canary
Mar 10, 2026
Merged

cg: headless gpu#567
softmarshmallow merged 6 commits intomainfrom
canary

Conversation

@softmarshmallow
Copy link
Copy Markdown
Member

@softmarshmallow softmarshmallow commented Mar 10, 2026

Summary by CodeRabbit

  • New Features

    • Added headless GPU rendering support for server-side or offscreen GPU rendering without a display window.
    • Implemented viewport culling to reduce rendering overhead.
  • Tests & Benchmarks

    • Added comprehensive viewport culling performance benchmarks covering multiple camera scenarios.
    • Added headless GPU rendering example demonstrating usage.
  • Chores

    • Updated WebAssembly development server environment variable naming.

Skip off-screen layers during draw by building a visible-node set from
the R-tree frame plan and passing it to new culled-draw methods in
Painter. Camera pan/zoom on a 4900-node scene is 67x faster when
zoomed in (~1% visible) and 1529x faster when the viewport is empty.

- Add draw_layer_list_culled / draw_render_commands_culled to Painter
- Build HashSet<NodeId> from FramePlan regions in draw_layers_with_scene_cache
- Add viewport_culling correctness tests (3 integration tests)
- Add bench_viewport_culling criterion benchmark (5K and 50K nodes)
Add HeadlessGpu for windowless GPU-backed Skia rendering, useful for
benchmarks, tests, CLI tools, and future SDK usage. Gated behind the
native-gl-context feature flag so cg remains platform-agnostic by default.

- Add cg::window::headless module (CGL on macOS, EGL on Linux)
- Add headless_gpu example with per-scenario timing stats
- Add glutin + raw-window-handle as optional deps behind native-gl-context
- Enable native-gl-context in grida-dev
…y culling

Use a Vec<bool> indexed by NodeId (sequential u64) instead of
HashSet<NodeId> for the per-frame visible-node set. This eliminates
hashing overhead and HashMap bucket allocation, giving O(1) array
lookups with better cache locality on every draw call.
The R-tree query returns layer indices in arbitrary order. The previous
sort ensured Z-order, but with culled drawing the Z-order comes from
the command tree traversal, not the index array. The indices are only
used to build the visibility bitset and prefill the picture cache,
neither of which is order-dependent.
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 10, 2026

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

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

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 10, 2026

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

This PR adds viewport frustum culling optimization to reduce rendering costs for off-screen content, introduces headless GPU rendering capability without a display window, provides benchmarks and tests for validation, and updates the wasm serving environment variable naming.

Changes

Cohort / File(s) Summary
Dependency & Configuration Management
crates/grida-canvas/Cargo.toml, crates/grida-dev/Cargo.toml
Adds glutin (0.32.0) and raw-window-handle (0.6.0) optional dependencies with native-gl-context feature flag. Registers viewport culling benchmark and headless GPU example. Enables native-gl-context feature in grida-dev.
Headless GPU Rendering
crates/grida-canvas/src/window/headless.rs, crates/grida-canvas/src/window/mod.rs
Introduces HeadlessGpu struct with cross-platform GL context initialization (macOS, Linux; Windows unsupported). Exposes create_renderer(), print_gl_info(), and dimension accessors. Includes platform-specific bootstrapping for GL/Skia surface setup without display window.
Viewport Culling Implementation
crates/grida-canvas/src/painter/painter.rs, crates/grida-canvas/src/runtime/scene.rs
Adds public draw_layer_list_culled method to Painter with private culled rendering helpers that skip off-screen content by ID visibility bitset. Integrates culling into frame planning by computing visible_ids from FramePlan regions and delegating to culled drawing path.
Tests & Benchmarks
crates/grida-canvas/tests/viewport_culling.rs, crates/grida-canvas/benches/bench_viewport_culling.rs, crates/grida-canvas/examples/headless_gpu.rs
Introduces viewport culling test suite validating display-list reduction under zoom/pan scenarios, comprehensive performance benchmark suite (5k and 50k node grids with multiple viewport scales), and headless GPU example demonstrating rendering without display and frame timing collection.
Editor Configuration
editor/.env.example, editor/grida-canvas/backends/wasm-locate-file.ts
Renames NEXT_PUBLIC_GRIDA_WASM_SERVE_URL to NEXT_PUBLIC_GRIDA_WASM_DEV_SERVE_URL. Adds unpkg CDN fallback for wasm asset resolution when dev/local serving is unavailable.

Sequence Diagram(s)

sequenceDiagram
    participant Scene
    participant FramePlanner
    participant Painter
    participant LayerRenderer

    Scene->>FramePlanner: Compute regions during frame planning
    FramePlanner->>FramePlanner: Build visible_ids bitset from intersecting regions
    
    rect rgba(100, 150, 200, 0.5)
    Note over Scene,LayerRenderer: Viewport Culling Path (New)
    Painter->>Painter: draw_layer_list_culled(layer_list, visible_ids)
    Painter->>Painter: Check wireframe mode
    alt Culled Raster Path
        Painter->>LayerRenderer: draw_render_commands_culled<br/>(skip non-visible by ID)
    else Culled Outline Path
        Painter->>LayerRenderer: draw_layer_list_outline_culled<br/>(skip non-visible by ID)
    end
    LayerRenderer-->>Painter: Render only visible content
    end
    
    Painter-->>Scene: Frame rendered with off-screen culling
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

cg, canvas

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'cg: perf' is vague and generic, using non-descriptive terms that don't clearly convey the specific changes or improvements made in the pull request. Use a more descriptive title that specifically mentions the performance improvements, such as 'Add viewport culling for performance optimization' or 'Implement GPU rendering and viewport culling in cg'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • 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.

Changed the environment variable from NEXT_PUBLIC_GRIDA_WASM_SERVE_URL to NEXT_PUBLIC_GRIDA_WASM_DEV_SERVE_URL in the .env.example file and updated the locateFile function to use the new variable for local WASM file serving. This change clarifies the purpose of the variable for development environments.
Reverts the draw-time culling introduced in:
- feat(cg): viewport culling at draw time
- perf(cg): replace HashSet<NodeId> with Vec<bool> bitset for visibility culling
- perf(cg): remove unnecessary sort of R-tree indices in frame plan

Removes draw_layer_list_culled / draw_render_commands_culled /
draw_layer_list_outline_culled from Painter, restores the
draw_layer_list call in draw_layers_with_scene_cache, and re-adds
the indices.sort() in the frame plan builder for Z-order correctness.

Made-with: Cursor
@vercel vercel Bot temporarily deployed to Preview – viewer March 10, 2026 13:36 Inactive
@vercel vercel Bot temporarily deployed to Preview – grida March 10, 2026 13:36 Inactive
@vercel vercel Bot temporarily deployed to Preview – blog March 10, 2026 13:36 Inactive
@vercel vercel Bot temporarily deployed to Preview – backgrounds March 10, 2026 13:36 Inactive
@softmarshmallow softmarshmallow merged commit e75ea0c into main Mar 10, 2026
12 checks passed
@softmarshmallow softmarshmallow changed the title cg: perf cg: headless gpu Mar 10, 2026
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: b11fedc1c6

ℹ️ 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".

.with_context_api(ContextApi::OpenGl(Some(Version::new(3, 3))))
.build(None);
let fallback = ContextAttributesBuilder::new()
.with_context_api(ContextApi::OpenGl(None))
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 Fall back to GLES when creating headless EGL context

The fallback context still requests desktop OpenGL (ContextApi::OpenGl(None)), so on Linux/EGL environments that expose only GLES (common in headless CI, containers, and ARM devices) both context-creation attempts fail and HeadlessGpu::new returns an error. The existing native window bootstrap already includes a GLES fallback in crates/grida-dev/src/platform/winit.rs, so this new headless path regresses compatibility in exactly the environments this feature targets.

Useful? React with 👍 / 👎.

const [path, version] = args;
if (process.env.NEXT_PUBLIC_GRIDA_WASM_SERVE_URL) {
return `${process.env.NEXT_PUBLIC_GRIDA_WASM_SERVE_URL}/${path}`;
if (process.env.NEXT_PUBLIC_GRIDA_WASM_DEV_SERVE_URL) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve legacy WASM serve env var during rename

This rename drops support for NEXT_PUBLIC_GRIDA_WASM_SERVE_URL with no fallback, so any existing environment that still sets the old key will silently stop using its custom WASM host and fall back to localhost/unpkg resolution instead. That can load the wrong binary source at runtime until every deployed environment variable is migrated.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot mentioned this pull request Apr 22, 2026
2 tasks
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