Skip to content

Cones: stop clipping to the floor every frame - #306

Merged
SunkenInTime merged 2 commits into
mainfrom
t3/cones-fast-path
Oct 15, 2026
Merged

SunkenInTime merged 2 commits into
mainfrom
t3/cones-fast-path

Conversation

@SunkenInTime

@SunkenInTime SunkenInTime commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

View cones were the most expensive thing on screen during replay playback and editor page flips. The reason turned out to be specific to Windows: Flutter draws there with Skia and no stencil buffer. So every clip to a complex shape becomes a mask that the CPU draws and uploads. Every cone was clipped to the map's whole painted floor, and editor cones were also clipped to their own visibility shape. This PR removes those clips from the per-frame path without changing what the user sees.

ci: analyze test web

What changed

  • The floor clip moves from the cones to the revealed map. RevealedFloor and ConeCoverageLayer take the painted floor (SvgHeightMapTransform.floorOnScreen). They clip the revealed map to it once, inside the map's own repaint boundary. The replay's cone painter no longer clips at all. Editor cones that join such a layer skip their own floor clip. Drag previews, screenshots and the replay's no-worker fallback still clip themselves.
  • Map and mask are grouped in a layer that doesn't cache its children. The cone mask is composited over the revealed map with dstIn, inside a clip-rect layer that saves a layer. That isolates the two, so dstIn can't reach the map underneath. A colour-filter layer also groups them, but the engine then caches "map + this frame's mask" as a new image every frame, and the map never gets its own cache.
  • Editor cones draw their lit area as one path. For a cone without height floors, "what it sees, within reach" is made once per painter with Path.combine, then drawn with no clip. If PathOps ever fails on an outline, that cone falls back to the old clip-and-circle draw, and the failure is remembered.

Numbers

Release builds on Windows, 144 Hz (6.9 ms budget per frame). Raster time from FrameTiming, three interleaved rounds. Replay = 8 s of playback of a 13.08 Summit replay. Page flips = 10 flips between two pages captured from it. Medians:

old main (before revealed floor) main this PR
Replay, raster p50 4.00 ms 4.11 ms 2.60 ms
Page flips, raster p50 5.28 ms 4.62 ms 3.12 ms
Page flips, frames over 6.9 ms 24.9% 7.8% 1.3%

In rounds 1 and 2 this PR's replay ran at 2.5 to 2.6 ms. In round 3 one run hit a load spike on the desktop (5.9 ms, with its page flips slow too). Three earlier series of this design measured 2.5 to 2.9 ms.

How we got here

This was a measurement-driven design loop with a second reviewer (Astra), four rounds:

  • Probes split the cost. The replay cone painter cost about 2.4 ms a frame both before and after the revealed-floor change. The non-antialiased floor clip alone was about 1.4 ms. With that clip gone, every way of cutting the cone's wedge (no clip, non-AA, 8-edge, PathOps intersect, sweep gradient) landed within noise.
  • Engine and Skia source explained it. fAvoidStencilBuffers makes Skia's clip stack fall back to software masks.
  • Tried and dropped: removing the editor's per-cone layers, a colour-filter grouping, and a triangle + circle replay wedge. None beat noise or the simpler alternative.

Checked

  • Screenshots from the running release build: a replay moment and the editor, against main. The replay differs on 933 px, at most 17/255. These are 1 px lines along floor/wall edges inside cones, now antialiased instead of hard-edged. The editor differs on 367 px, at most 5/255.
  • Test: a new test in cone_coverage_test.dart covers the floor contract: a layer given the floor reveals only the floor and tells cones not to clip; without it, cones clip themselves. It is mutation-checked: ignoring the floor, always clipping, and dropping the save layer each fail.
  • Suites: cone, view-cone painter, SVG cone, app integration, replay capture, screenshot, transition and drag-feedback suites pass.
  • Not run: the web build in a browser. It uses the same code, but I haven't seen it draw.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Revealed map areas now follow the visible floor boundaries, preventing parts of the map outside the floor from showing through.
    • Cone coverage now adjusts to floor clipping: when a floor boundary is available, cones no longer apply a separate floor clip; when it isn’t, cones continue to clip themselves.
    • Replay cones are no longer clipped to the painted floor, while smoke occlusion remains.

On Windows, Flutter draws through Skia without a stencil buffer, so a
clip to any shape but a rectangle, a circle or a small convex polygon
is a mask drawn on the CPU for every draw under it. Every cone was
clipped to the map's whole painted floor (the replay once per frame
around all cones; the editor per cone), and editor cones also clipped
to their visibility shape. That was most of what cones cost.

- The revealed map is clipped to the floor once, in its own repaint
  boundary (RevealedFloor/ConeCoverageLayer take the floor), and the
  cone mask is composited over it in a clip layer that saves a layer.
  A colour filter would group them too, but the engine caches a
  filter's children as one image, redrawing the map with every mask.
- Cones joining a layer that clips the map skip their own floor clip;
  drag previews, screenshots and the replay's no-worker fallback still
  clip themselves.
- An editor cone without height floors draws its lit area (visibility
  within reach) as one path made once per painter, with no clip.

The only visible change: floor edges inside cones are now antialiased
(1 px lines, at most 17/255 on screenshots of a replay).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 53 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e6b0ad6c-57da-4900-89b4-e04a52e621d3
📥 Commits

Reviewing files that changed from the base of the PR and between 3204c8b and a0a16ef.

📒 Files selected for processing: 1
  • lib/widgets/replay/replay_cones.dart

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6521711d-9352-4451-9b2a-44d8d0ef2de1
📥 Commits

Reviewing files that changed from the base of the PR and between 13710f0 and 3204c8b.

📒 Files selected for processing: 6
  • lib/interactive_map.dart
  • lib/view_cone/cone_coverage.dart
  • lib/widgets/draggable_widgets/utilities/svg_height_view_cone.dart
  • lib/widgets/replay/replay_canvas.dart
  • lib/widgets/replay/replay_cones.dart
  • test/cone_coverage_test.dart

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

The change transforms height-model floor paths to screen space and uses them to clip revealed map content. Cone painters can disable their own floor clipping when the map layer supplies a floor. Replay cones no longer clip to the floor.

Changes: Floor-aware cone coverage

Layer / File(s) Summary
Floor geometry and cone painting
lib/widgets/draggable_widgets/utilities/svg_height_view_cone.dart
SvgHeightMapTransform now provides cached screen-space floor paths. Cone painters accept a clipToFloor argument and intersect visibility with cone reach when no height floor is present.
Revealed-map clipping and coverage wiring
lib/view_cone/cone_coverage.dart, lib/interactive_map.dart, test/cone_coverage_test.dart
ConeCoverageLayer and RevealedFloor accept optional floor paths and clip map content to them. InteractiveMap supplies its current floor path. Tests check map clipping and whether cones clip themselves.
Replay floor and cone rendering
lib/widgets/replay/replay_canvas.dart, lib/widgets/replay/replay_cones.dart
Replay rendering supplies a transformed floor path to RevealedFloor. Replay cones no longer apply floor clipping. The painter uses SvgHeightMapTransform.sourceToScreen.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Refactor

Sequence Diagram(s)

sequenceDiagram
  participant InteractiveMap
  participant SvgHeightMapTransform
  participant ConeCoverageLayer
  participant MapChild
  InteractiveMap->>SvgHeightMapTransform: Get floor path for selected height model
  SvgHeightMapTransform-->>InteractiveMap: Return screen-space floor path
  InteractiveMap->>ConeCoverageLayer: Provide floor path
  ConeCoverageLayer->>MapChild: Clip map content to floor path
Loading
Merge Risk: ⚪ Minimal · up to 3204c

The floor-clipping change is mergeable after normal checks; no actionable issue remains from this review.

Pre-merge checks: Passed 5
Passed Passed checks: 5 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: moving floor clipping out of per-frame cone rendering.
Docstring Coverage Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@SunkenInTime
SunkenInTime merged commit 20e373c into main Oct 15, 2026
14 checks passed
@SunkenInTime
SunkenInTime deleted the t3/cones-fast-path branch October 15, 2026 00:27
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