feat(viz): count rays whose origin is outside the sensor world - #79
Conversation
Real audit signal. The visualization terrain is 4000 m × 4000 m (`TERRAIN_SIZE` in `terrain.ts`) but the default sensor world is a 1024 m cube. A ray whose origin sits outside that cube AND whose direction never crosses it returns a clean miss — silently wrong if the operator expects sensor coverage to match the visible terrain. Add `LosQueryStats.raysOutsideWorld`. Counts during host-side ray packing in `runBatch`, ~6 comparisons per ray on a path that already iterates each ray. Pure observability — no behavioural change. The fix (resize the world or make it configurable) is a separate decision because it bumps GPU voxel-buffer memory ~8x; this PR just gives the audit a way to confirm whether anyone is actually hitting the gap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughA new Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a new audit metric, raysOutsideWorld, to track rays with origins outside the voxel AABB in the Line-of-Sight (LOS) query manager. This helps identify cases where sensor coverage might not match the visualization terrain. Feedback was provided regarding the timing of the statistic update; because raysOutsideWorld is updated within the asynchronous runBatch method, it may become temporarily out of sync with other counters that are updated synchronously, which could impact tools that snapshot statistics immediately after a query.
Address Gemini review on PR #79: `raysOutsideWorld` was bumped inside the chained `runBatch`, while `totalQueries` and `totalRays` are bumped in the synchronous `query()` body. An audit snapshotting immediately after `query()` returned would see inconsistent counters when there's queued work behind a busy slot. Move the AABB check into `query()` and count in lockstep with the other stats. Use world-space comparison instead of grid-space so the check avoids the divide-by-voxelScale (`runBatch` still does the divide for grid-space packing). Net: same counter values per ray, but updated immediately on submit instead of when the slot dequeues. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/ResQ.Viz.Web/client/webgpu/los.ts (1)
60-71: Clarify “strictly outside” vs half-open bounds in the metric definition.Line 61 says “strictly outside,” but Lines 261-263 classify
o[*] >= *Maxas outside, which is half-open[min, max)behavior. Please align wording (or comparisons) so audits interpret boundary rays consistently.Suggested doc-only alignment
- * Rays whose world-space origin fell strictly outside the world's - * voxel AABB. + * Rays whose world-space origin fell outside the world's voxel AABB + * using half-open bounds per axis: [min, max).Also applies to: 261-263
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ResQ.Viz.Web/client/webgpu/los.ts` around lines 60 - 71, The docstring for raysOutsideWorld claims “strictly outside” but the classification code treats o[*] >= *Max as outside (half-open [min, max)) — update one of them to be consistent: either change the prose for raysOutsideWorld to mention the half-open interval semantics (e.g., “outside the half-open [min, max) voxel AABB”) or modify the comparison in the ray-origin classification (the o[*] >= *Max checks) to use > instead of >= so the metric truly reflects “strictly outside”; reference the raysOutsideWorld field and the o[*] >= *Max classification logic when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/ResQ.Viz.Web/client/webgpu/los.ts`:
- Around line 60-71: The docstring for raysOutsideWorld claims “strictly
outside” but the classification code treats o[*] >= *Max as outside (half-open
[min, max)) — update one of them to be consistent: either change the prose for
raysOutsideWorld to mention the half-open interval semantics (e.g., “outside the
half-open [min, max) voxel AABB”) or modify the comparison in the ray-origin
classification (the o[*] >= *Max checks) to use > instead of >= so the metric
truly reflects “strictly outside”; reference the raysOutsideWorld field and the
o[*] >= *Max classification logic when making the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 31ee410d-616d-4d23-a504-604d6d3a27f4
📒 Files selected for processing (1)
src/ResQ.Viz.Web/client/webgpu/los.ts
…82) * perf(viz): lazy-load SignalR runtime via dynamic import The main bundle has been creeping up against the 800 KiB cap (98.8 % after PR #79) and the next non-trivial feature would have failed CI. Defer the `@microsoft/signalr` runtime until `start()` runs. - Replace eager `import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr'` with `import type { HubConnection }` (type-only, zero runtime cost). - Make `connection` a module-scoped `let` initialised inside `start()`, via `await import('@microsoft/signalr')`. First start triggers a separate chunk fetch; subsequent reconnects reuse the cached module AND the same `HubConnection` instance. - Wrap handler registration in `_wireConnection(c)` so it runs once after the lazy build, before the first `connection.start()` call. Bundle delta: main: 809.0 → 753.6 KB (−55 KB; 98.8 % → 92.0 % of cap) esm: — → 55.3 KB (new lazy chunk for SignalR) sensor: 20.5 → 20.5 KB (unchanged) The first paint no longer blocks on parsing 55 KB of SignalR; the ~50 ms chunk fetch overlaps with the existing async boot path (`_autoSpawnIfEmpty`, terrain loading) so user-visible startup time is unchanged in practice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: refresh stale frontend versions in CLAUDE.md CodeRabbit flagged this PR for declaring `@microsoft/signalr` ^10 because CLAUDE.md still said the project uses signalr 8. The package was bumped to 10 alongside the .NET 10 backend (older PR); CLAUDE.md just wasn't updated. Bring the line into sync with reality and note that signalr now ships as a lazy chunk per this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaces the LosQueryStats added in PRs #78/#79 in a small dev/audit panel. Lets an operator confirm the WebGPU sensor primitive is healthy without dropping into devtools — useful both for the 2026-05-12 audit routine (P2 #5) and during dev when tuning ring sizing or world extents. - New `client/sensorStatsOverlay.ts` — `SensorStatsOverlay` class builds a fixed bottom-left panel, hidden by default. Press 'i' (KeyI; ignored when typing in inputs) to toggle. - Two sections (mesh-link, lidar) showing totalQueries / totalRays / peakSlotDepth / raysOutsideWorld. Cells colour amber when peak slot depth > 1 (callers queueing) or outside-AABB > 0 (sim is outside the brick map's coverage; project memory documents the 4 km terrain vs 1 km sensor world gap). - `update()` is called once per `ReceiveFrame`; short-circuits when the panel is hidden, so the closed-overlay cost is one boolean. - Falls back gracefully when `getSensorContext()` returns null (no-WebGPU browser, init failure) — the panel just shows "offline". Bundle: 809.0 → 811.3 KB (+2.3 KB; 99.0 % of the 800 KiB cap). Lands much more comfortably on top of #82 (defer-signalr), which would take this combined to ~756 KB. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(viz): sensor-stats overlay (toggle with 'i') Surfaces the LosQueryStats added in PRs #78/#79 in a small dev/audit panel. Lets an operator confirm the WebGPU sensor primitive is healthy without dropping into devtools — useful both for the 2026-05-12 audit routine (P2 #5) and during dev when tuning ring sizing or world extents. - New `client/sensorStatsOverlay.ts` — `SensorStatsOverlay` class builds a fixed bottom-left panel, hidden by default. Press 'i' (KeyI; ignored when typing in inputs) to toggle. - Two sections (mesh-link, lidar) showing totalQueries / totalRays / peakSlotDepth / raysOutsideWorld. Cells colour amber when peak slot depth > 1 (callers queueing) or outside-AABB > 0 (sim is outside the brick map's coverage; project memory documents the 4 km terrain vs 1 km sensor world gap). - `update()` is called once per `ReceiveFrame`; short-circuits when the panel is hidden, so the closed-overlay cost is one boolean. - Falls back gracefully when `getSensorContext()` returns null (no-WebGPU browser, init failure) — the panel just shows "offline". Bundle: 809.0 → 811.3 KB (+2.3 KB; 99.0 % of the 800 KiB cap). Lands much more comfortably on top of #82 (defer-signalr), which would take this combined to ~756 KB. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(viz): tighten overlay keybind + align CSS with design tokens Address Gemini review on PR #83: - Keybind: extend the input-bail list to cover SELECT (the operator may type 'i' to jump-search options), and require no shift key (uppercase 'I' shouldn't toggle a dev panel from within prose typed elsewhere). - CSS: replace `--fg` / `--muted` with the project's canonical `--text` / `--text-muted` (defined in `:root` lines 18-36); fix the `--warning` fallback to the palette colour `#d29922`. - Add `body.investor-mode .sensor-stats-overlay { display: none }` so the panel disappears during cinematic recordings — matches the visibility rule for the event log and telemetry strip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(viz): allow URL overrides of WebGPU world bounds Lets ops/dev override the brick-map world without redeploying — the key motivation is testing the 4 km terrain vs 1 km default sensor world gap surfaced by `LosQueryStats.raysOutsideWorld` (PR #79). The defaults stay exactly where #69 set them; nothing changes for normal users. URL params (all optional): ?worldGrid=N gridSize, must be > 0 and divisible by 8 ?voxelScale=V metres per voxel, positive finite ?worldOriginX/Y/Z=K finite numbers; if omitted, the cube auto-recentres on world X/Z and starts at Y=0 so the new cube still straddles the terrain. Example: `?worldGrid=256&voxelScale=16` → 4096 m cube at 16 m/voxel covering the full 4000 m terrain (8× GPU voxel-buffer memory). Boot log emits the resolved params so the audit can correlate `raysOutsideWorld` against actual world bounds. Defaults preserved: gridSize: 128, voxelScale: 8, origin: [-512, 0, -512] Invalid values warn-and-fall-back per param; boot continues. Sensor chunk +1 KB (now 21.5 KB); main bundle unchanged at 756.4 KB. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(viz): tighten world-bounds URL parser Address Gemini review on PR #85: - Import `BRICK` from `./brickmap` and use it as the divisibility check in `_readPositiveInt` instead of the hardcoded `8`. Keeps the world parser in sync if BRICK ever changes. - Replace `parseInt`/`parseFloat` with `Number()`. The lenient parsers silently accept trailing garbage like "128abc" and truncate decimals like "128.9" — for a config override that should fall back to the default rather than partially parse. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
TERRAIN_SIZEinclient/terrain.ts) but the default sensor world is a 1024 m cube (128³ voxels at 8 m, origin[-512, 0, -512]). Drones flying outside [-512, 512] on X/Z get clean LoS misses against terrain that exists in the visualization but not in the brick map — silently wrong.LosQueryStats.raysOutsideWorld. Counts during host-side ray packing inrunBatch, ~6 comparisons per ray on a path that already iterates each ray. Pure observability — no behavioural change.Test plan
npm run buildpasses; main bundle 809 KB (under 800 KiB cap)tsc --noEmitcleanLosQueryStatsextends — existing consumers (none yet, landed in feat(viz): expose lifetime stats on LosQueryManager #78) are forward-compatiblegetSensorContext()?.los.stats.raysOutsideWorldto decide if a world resize PR is warranted🤖 Generated with Claude Code
Summary by CodeRabbit