Skip to content

feat(viz): count rays whose origin is outside the sensor world - #79

Merged
WomB0ComB0 merged 2 commits into
mainfrom
feat/webgpu-out-of-bounds-stat
Apr 29, 2026
Merged

feat(viz): count rays whose origin is outside the sensor world#79
WomB0ComB0 merged 2 commits into
mainfrom
feat/webgpu-out-of-bounds-stat

Conversation

@WomB0ComB0

@WomB0ComB0 WomB0ComB0 commented Apr 29, 2026

Copy link
Copy Markdown
Member

Summary

  • Real audit-signal finding. The visualization terrain is 4000 m × 4000 m (TERRAIN_SIZE in client/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.
  • 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 actual fix (resize the world to ~4096 m, or make it configurable) is a separate decision because it bumps GPU voxel-buffer memory ~8x (8 → 64 MB). This PR just gives the audit a way to confirm whether the gap is being hit before committing to the memory cost.

Test plan

  • npm run build passes; main bundle 809 KB (under 800 KiB cap)
  • tsc --noEmit clean
  • LosQueryStats extends — existing consumers (none yet, landed in feat(viz): expose lifetime stats on LosQueryManager #78) are forward-compatible
  • Audit (in 2 weeks) reads getSensorContext()?.los.stats.raysOutsideWorld to decide if a world resize PR is warranted

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Line-of-sight query statistics now track rays originating outside the world boundary.

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>
@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

A new raysOutsideWorld metric is added to LosQueryStats. The LosQueryManager initializes this counter and increments it during each query() call by synchronously computing the world voxel AABB and counting rays with origins outside it, before GPU batch queueing.

Changes

Cohort / File(s) Summary
Ray Origin Boundary Tracking
src/ResQ.Viz.Web/client/webgpu/los.ts
Added raysOutsideWorld: number field to LosQueryStats export. LosQueryManager.query() now synchronously computes world AABB from world.params, iterates submitted rays, counts origins strictly outside AABB, and increments the counter before GPU batch submission.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related PRs

Poem

🐰 A ray hops through the voxel space,
But some stray far beyond the place!
Now we count the wanderers wild,
Those origins that left the grid beguiled. ✨

🚥 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 and specifically describes the main change: adding a counter for rays whose origin is outside the sensor world.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/webgpu-out-of-bounds-stat

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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread src/ResQ.Viz.Web/client/webgpu/los.ts Outdated
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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 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[*] >= *Max as 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6afd38a and af3645a.

📒 Files selected for processing (1)
  • src/ResQ.Viz.Web/client/webgpu/los.ts

@WomB0ComB0
WomB0ComB0 merged commit a30f8a1 into main Apr 29, 2026
37 checks passed
@WomB0ComB0
WomB0ComB0 deleted the feat/webgpu-out-of-bounds-stat branch April 29, 2026 03:57
WomB0ComB0 added a commit that referenced this pull request Apr 29, 2026
…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>
WomB0ComB0 added a commit that referenced this pull request Apr 29, 2026
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>
WomB0ComB0 added a commit that referenced this pull request Apr 29, 2026
* 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>
WomB0ComB0 added a commit that referenced this pull request Apr 29, 2026
* 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>
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