-
Notifications
You must be signed in to change notification settings - Fork 131
docs: document viewport culling investigation and findings #596
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| --- | ||
| title: "Investigation: Viewport Culling & Camera Caching" | ||
| status: rejected | ||
| date: 2026-03-22 | ||
| --- | ||
|
|
||
| # Investigation: Viewport Culling & Camera Caching | ||
|
|
||
| ## Hypothesis | ||
|
|
||
| During view-only camera transforms (pan/zoom), skip drawing layers whose bounds fall outside the visible viewport. Cache Camera2D derived values (view matrix, inverse, zoom, world rect) to avoid redundant per-frame math. | ||
|
|
||
| ## What Was Tried | ||
|
|
||
| 1. **Camera2D caching** — `warm_cache()` precomputes view matrix, inverse, zoom, and world rect once per mutation. Read accessors (`view_matrix()`, `rect()`, `get_zoom()`, `screen_to_canvas_point()`) return cached fields in O(1). | ||
|
|
||
| 2. **Viewport culling** — Before each `draw_layer` call, check if the layer's render bounds (from `GeometryCache`) intersect the camera's world rect. Skip layers that are entirely off-screen. | ||
|
|
||
| ## Results | ||
|
|
||
| ### Synthetic scenes (Criterion, CPU raster, 1920x1080) | ||
|
|
||
| Sparse grids where most nodes are off-screen: | ||
|
|
||
| | Metric | 100 nodes | 1K nodes | 10K nodes | | ||
| | ------ | --------- | -------- | --------- | | ||
| | Pan | ~same | **−46%** | **−85%** | | ||
| | Zoom | ~same | **−32%** | **−81%** | | ||
|
|
||
| ### Real-world SVGs (headless, CPU raster, 1920x1080) | ||
|
|
||
| Dense content where most nodes overlap the viewport: | ||
|
|
||
| | Scene | Nodes | Pan Δ | Zoom Δ | | ||
| | -------------------------------- | ----- | ---------- | ---------- | | ||
| | Koppen-Geiger climate map (96MB) | 235K | **+8.7%** | **+13.3%** | | ||
| | San Francisco Bay map (40MB) | 85K | **+11.0%** | −7.3% | | ||
| | Lorenz 3D attractor (20MB) | 300K | +3.5% | ~same | | ||
| | Lyon fortification map (30MB) | 34 | −2.0% | −3.0% | | ||
| | Propane flame contours (30MB) | 1.8K | −6.5% | −3.3% | | ||
|
|
||
| ## Why It Failed on Real Content | ||
|
|
||
| Linear viewport culling is **O(n) per frame** — every node's bounds are checked against the viewport. For dense scenes (maps, scientific visualizations), nearly all nodes pass the intersection test, so the check is pure overhead. | ||
|
|
||
| The synthetic benchmarks were misleading: a sparse grid at 10K nodes has ~90% off-screen at any given viewport, so culling skips most work. Real documents are the opposite — content is concentrated in the viewport. | ||
|
|
||
| ## Conclusion | ||
|
|
||
| - **Camera caching**: safe but negligible (~30ns/frame savings vs 200ms+ frame times) | ||
| - **Linear viewport culling**: net negative on real content. Do not adopt without a spatial index. | ||
| - **Actual bottleneck**: Skia path rasterization dominates frame time on large scenes (235K paths = 800ms). CPU-side culling cannot fix this. | ||
|
|
||
| ## What Would Actually Help | ||
|
|
||
| Per items 6, 12, and 36 in `optimization.md`: | ||
|
|
||
| - **Spatial index** (R-tree/quadtree, item 36) would make culling O(log n) instead of O(n) | ||
| - **Tile-based raster cache** (item 6) would avoid re-rasterizing static content on camera change | ||
| - **SkPicture caching** (item 5) with dirty-region invalidation would let Skia replay recorded ops instead of re-drawing paths | ||
|
|
||
| The draw stage (Skia path rasterization) is where 95%+ of frame time goes on large scenes. Optimizations must target that. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The recommendation block points readers to the wrong follow-up work: in
optimization.md, item 12 isTight Bounds for save_layer Operationsand item 36 isScene Planner & Scheduler, while the spatial-index entry is item 38. Because this section is explicitly telling implementers what to pursue next, these misnumbered references will send follow-up work to unrelated optimizations instead of the intended spatial-index and SkPicture-caching sections.Useful? React with 👍 / 👎.