Skip to content

Make view cones fast on the web - #251

Merged
SunkenInTime merged 4 commits into
mainfrom
t3code/web-cone-speed
Oct 6, 2026
Merged

SunkenInTime merged 4 commits into
mainfrom
t3code/web-cone-speed

Conversation

@SunkenInTime

@SunkenInTime SunkenInTime commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

On the web, view cones drag at 60 fps or below on Lotus and Breeze. The browser has no native cone library, so every cone runs the Dart query, and that query had fallen behind the native one. On top of that, Flutter's web engine rebuilt the map's clip path point by point on every frame.

What changed

  • The Dart query now runs the native algorithm (ish_query in native/height): the same vertex events, sector culling and same-edge outline simplification. Vertices are indexed once instead of hashed per query. Hashing Offsets and doubles was the main cost once compiled to JavaScript. Seam checks are remembered until the set of blocking walls changes, and each tree node's bounds are tested once per ray, not twice. Across 256 random Lotus cones, the Dart and native queries cast the same rays and draw the same outline (area difference 0).
  • Cone clip paths are built with addPolygon. Flutter's web engine drops every built path at the end of a frame, so a path made of lineTo calls is rebuilt with one call into CanvasKit per point, every frame. The playable-area clip has thousands of points, which cost about 3 ms per frame. addPolygon passes each ring in one call. The shape is unchanged.
  • The query's crossings and topology are built when the map loads, not on the first cone. This removes a 25–45 ms stall the first time you drag a cone on each map. It only happens when the native library is unavailable.

Numbers

Release-mode profile web build, headless Edge (RTX 4070, D3D11). A Jett with a 180° max-length cone is dragged over the same 8-second path on each map, using real PlacedWidgetBuilder widgets and pointer events. Frame gaps are capped by the ~144 Hz frame clock (6.9 ms).

map query p50 main → PR query p90 frame gap p90 frame gap p99 fps
Lotus 9.3 → 1.7 ms 20.8 → 6.3 ms 27.8 → 10.7 ms 42.1 → 16.2 ms 61 → 123
Bind 7.9 → 1.9 ms 12.7 → 3.1 ms 13.2 → 7.1 ms 20.2 → 8.8 ms 116 → 137
Ascent 3.1 → 0.8 ms 4.5 → 1.3 ms 10.6 → 7.1 ms 13.2 → 7.3 ms 117 → 138
Breeze 14.5 → 1.7 ms 22.9 → 4.0 ms 25.0 → 7.2 ms 68.6 → 19.0 ms 65 → 129

Screenshots of the cone on all four maps match main pixel for pixel.

What's left

  • Breeze mid and the middle of Lotus still spike to 12–20 ms while the cone faces them. Those positions cast 1,500–5,300 rays at about 100 edge tests each, and native casts the same rays. Fixing that means culling vertices hidden behind their own wall, in both the native and Dart queries. I'd do that as its own PR.
  • Map load on the web takes 0.5–2.3 s, mostly gzip and CRC decoding in pure Dart (archive). The browser's DecompressionStream could do this natively. Also its own PR.

Testing

  • New test: a vertex that is a seam at one eye height is a corner at another, so the cached seams must reset when the blocking walls change. It fails if the reset is removed.
  • test/svg_height_visibility_test.dart's touching-pieces test now compares ray counts, not outline point counts. Same-edge simplification shortens one whole wall's outline, while each piece keeps its own edge.
  • All vision and cone tests pass locally (181 passed, 4 skipped).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • View-cone outlines now handle wall corners, openings, seams, and touching walls more consistently as camera height changes.
    • Visibility results are reused when wall activity remains unchanged, helping avoid unnecessary recalculation.
  • Performance

    • View-cone rendering uses a more streamlined approach to drawing visibility polygons, supporting more efficient rendering.

RetriggerConfidence Score: 5/5

No blocking issues were established; the reviewed changes are safe to merge on the available evidence.

What we checked:

  • Ran a general contract validation to measure provider-ready timings during the before → eager transitions and confirmed the benchmark exited with code 0. T-Rex
  • Observed the timing deltas: Provider-ready 572 ms to 551 ms (−21 ms), Icebox 1712 ms to 1711 ms (−1 ms), and Bind 1225 ms to 1245 ms (+20 ms), and noted there were no page errors according to the benchmark result. T-Rex
  • Reviewed the final pre-change and final eager-preparation map-loading visuals captured during the run. T-Rex

Summary

The PR ports the cone-query event and outline algorithm to Dart, prepares fallback geometry during map loading, and batches polygon construction for web rendering. No actionable regression was established.

Reviews (1) · Last reviewed commit: "Count a ray that misses every wall as on..."

SunkenInTime and others added 3 commits October 5, 2026 18:58
On the web there is no native library, so every cone runs the Dart
query. It had fallen behind ish_query: it cast rays at seams and beside
corners a wall runs straight through, kept every collinear outline
point, and hashed Offsets and doubles, which is slow once compiled to
JavaScript. Port ish_query's events, sector culling and same-edge
outline simplification, index vertices once instead of hashing them,
remember which vertices are seams until the wall mask changes, and test
each tree node's bounds once per ray.

A dragged Lotus cone's query drops from 9.3 ms to 1.7 ms (median) in
Edge, and from 14.5 ms to 1.8 ms on Breeze. Over 256 Lotus queries the
Dart and native cones cast the same rays and draw the same outline.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Flutter's web engine drops every built path at the end of a frame, so a
path made of lineTo calls is rebuilt point by point, one call into
CanvasKit each, every frame it is drawn. The map's playable-area clip
has thousands of points, which cost about 3 ms a frame while a cone was
dragged. addPolygon hands each ring over in one call; the shape is the
same, and screenshots of all four test maps match main pixel for pixel.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Dart query found wall crossings and vertex topology on its first
cone, a 25 to 45 ms stall the first time a cone was dragged on each map.
Build them when the map's sightlines load instead, only when the native
library is unavailable.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

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: 310fe336-fee7-4a9d-ba8d-a013b3594802
📥 Commits

Reviewing files that changed from the base of the PR and between ffd7c63 and 2c1e4e8.

📒 Files selected for processing (5)
  • lib/providers/svg_height_runtime_provider.dart
  • lib/view_cone/svg_height_visibility.dart
  • lib/widgets/draggable_widgets/utilities/svg_height_view_cone.dart
  • lib/widgets/draggable_widgets/utilities/view_cone_widget.dart
  • test/svg_height_visibility_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

Walkthrough

The change prepares Dart visibility queries when native acceleration is unavailable. It updates wall-mask caching, vertex topology, ray-event processing, and traversal. Cone paths now use polygons, and tests cover seam ray counts and visibility results when reusing a model across camera heights.

Changes

SVG height visibility

Layer / File(s) Summary
Wall masks and vertex topology
lib/view_cone/svg_height_visibility.dart
The Dart query caches active-wall masks and uses indexed vertex topology for seam and pass-through checks. prepareDartQuery() initializes the crossings and topology.
Fallback ray generation and traversal
lib/view_cone/svg_height_visibility.dart
The query filters and deduplicates ray events, reuses arc hits, reduces redundant hits, and prunes traversal using node-entry distances. Edge intersections use scalar endpoint deltas.
Runtime fallback and polygon paths
lib/providers/svg_height_runtime_provider.dart, lib/view_cone/svg_height_visibility.dart, lib/widgets/draggable_widgets/utilities/svg_height_view_cone.dart, lib/widgets/draggable_widgets/utilities/view_cone_widget.dart, test/svg_height_visibility_test.dart
The provider prepares a side’s Dart query when native acceleration setup returns false. Cone paths use addPolygon. Tests check seam ray counts and compare reused-model results with fresh models at two camera heights.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Refactor

Merge Risk: ⚪ Minimal · up to 2c1e4

No concrete issue remains that would prevent merging after normal checks.

Security Architecture Review

Security architecture risk: ⚪ Minimal · up to 2c1e4

The reviewed initialization and cache changes preserve map-data validation and existing access boundaries. No material security risk was found in these changes.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The inspected change is confined to map-model preparation and local visibility outputs. Traced consumers do not establish a new cross-service, tenant, credential, or privileged-execution path.

Trust Boundaries and Controls

  • observed — The provider continues loading registered attack and defense assets and checking map identity, side, schema, ground, supports, viewBox, and artwork hash before constructing visibility models. Eager preparation follows that existing validation path rather than bypassing it.

Resilience and Maintainability Implications

  • observed — Preparation forces idempotent lazy initialization. A preparation error is rethrown before a runtime is returned. The runtime owner retains disposal responsibility for cone caches and native acceleration, containing failures within the existing ownership scope.
🚥 Pre-merge checks | ✅ 5
✅ 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 summarizes the main change: improving view-cone performance on the web.
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
  • Autopilot · 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.

Matches what the native query and the old Dart query report. The seam
test now checks the return to the lower eye against a fresh model too,
so a cache that only resets when walls stop blocking would fail it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@SunkenInTime
SunkenInTime merged commit 98320be into main Oct 6, 2026
15 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