feat(viz): mesh-link line-of-sight against terrain - #70
Conversation
PR #5 of the WebGPU raymarcher direction. **First user-visible feature** of the whole arc — mesh-link lines in the production viz now visibly fade when terrain occludes the drone-pair line-of-sight. How it works: 1. effects.ts:_updateMeshLinks builds one LosRay per drone-pair each frame: origin = A, direction = normalize(B - A), max_t = ‖B - A‖, mask = MASK_OBSTACLES. 2. Dispatches the batch to the WebGPU sensor primitive landed in PR #4 (skip-if-busy throttle: at most one query in flight at a time, so queries don't queue unboundedly when GPU+readback can't keep up with the render). 3. When the query resolves, caches per-pair opacity (occluded pairs fade to 25% of base). The next frame's line teardown/rebuild reads from this cache, so opacity persists across the per-frame churn. Bundle architecture fix: - effects.ts importing getSensorContext from sensors.ts statically defeated the dynamic-import chunk split (Vite's INEFFECTIVE_DYNAMIC_ IMPORT warning), pulling the WebGPU runtime into the main bundle and blowing the client-budget cap. - New registry.ts holds the singleton context — runtime-free aside from a let + getter/setter — so effects.ts can import the getter without dragging the WebGPU stack along. sensors.ts calls setSensorContext() once boot finishes; getSensorContext() in registry.ts is what effects.ts reads. New file: - client/webgpu/registry.ts (~25 LOC): tiny singleton registry. Type- only import of SensorContext; no runtime WebGPU dependencies. Modified: - client/effects.ts: imports getSensorContext from registry, LosRay type from los, MASK_OBSTACLES + HIT_OBSTACLE constants from rays. EffectsManager gains _linkOpacityCache + _losQueryInFlight fields. _updateMeshLinks reads cached opacity, builds rays, dispatches one throttled LoS query per call, populates cache on resolve. - client/webgpu/sensors.ts: removes _ctx local cache + getSensorContext export, calls setSensorContext(ctx) at end of bootSensors() instead. Bundle: main 805 KB (under 800 KiB cap), sensors chunk 19 KB. Validation: typecheck, vite build, dotnet Release all pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
✨ Finishing Touches🧪 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. Comment |
There was a problem hiding this comment.
Code Review
This pull request implements Line-of-Sight (LoS) modulated opacity for mesh links, allowing occluded links to appear faded. It introduces a caching mechanism for link visibility and a throttling system for LoS queries to maintain performance. Additionally, a singleton registry is added to manage the WebGPU sensor context, preventing large static imports. Review feedback recommends caching the boolean occlusion state rather than the final opacity value to handle dynamic changes in base opacity, and using unique drone IDs instead of array indices for cache keys to ensure stability across frames.
effects.ts (Gemini medium x3): - Cache the occlusion BOOLEAN, not the computed opacity. The previous Map<string, number> went stale whenever baseOpacity changed (e.g. mesh.partitioned toggling between true/false would have continued rendering at the cached opacity until the next LoS query landed). Now the cache is Map<string, boolean> and opacity is computed at line-creation time from baseOpacity * occludedFactor — base changes take effect immediately. - Key the cache by canonical drone IDs (`<idA>--<idB>` lexicographi- cally sorted) instead of array indices. Indices are not stable across reorderings of the drones array — using IDs prevents the cache from applying the wrong drone-pair's occlusion if the array happens to be sorted differently next frame. DroneState.id is the same field other EffectsManager state (trails, detections) keys on. - Field renamed _linkOpacityCache → _meshLinkOccluded to reflect the new semantics. occludedFactor (0.25) lives at the call site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Addressed all 3 review threads in b62bff7: Cache the boolean, not the opacity (Gemini medium ×2)
Key by drone IDs, not array indices (Gemini medium)
Field renamed Build green: typecheck, Vite (main 805 KB), dotnet Release. |
) Replaces the single-slot serialization in LosQueryManager with a ring of N slots. Each slot owns its own rayBuf/hitBuf/readBuf/bindGroup and serializes only its own pending queries via a .then()/.catch() chain on its inFlight tail. Round-robin slot selection ensures successive queries pick different slots so concurrent dispatches across slots don't race on shared buffers. Why now: - The mesh-link LoS path landed in PR #70 doesn't need the ring (effects.ts has its own skip-if-busy throttle so at most one query is ever in flight). But high-rate dispatches like LiDAR scans — next on the WebGPU sensor roadmap — would otherwise stall queueing behind one slot. Ring buffer is foundation work to unblock them. Why backward compatible: - Constructor adds an optional `slotCount: number = 2` parameter. Existing call site `new LosQueryManager(device, world, 256)` in sensors.ts gets a 2-slot ring with no other change. Throughput-wise, effects.ts's skip-if-busy throttle still keeps at most one query in flight, so observable behaviour is unchanged for mesh-link LoS. - query() / capacity() / LosRay public surface unchanged. Memory: each slot allocates `maxRays * (RAY_BYTES + 2 * RAY_HIT_BYTES)`. For the existing call (maxRays=256, slotCount=2) that's ~40 KB total, negligible. LiDAR with 65k rays would want slotCount=3 ≈ 16 MB. New `slotCount` getter exposes the ring depth for callers that want to tune their throttle. Validation: typecheck passes, vite build 805 KB (under 819,200 cap), dotnet Release passes. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) * feat(viz): terrain-edit invalidation for the brick-map sensor stack Closes the highest-risk loose end from PR #70 — `setActivePreset` and `setHeightmapOverride` at runtime previously left the brick map silently stale, so mesh-link LoS and LiDAR scans would lie about occlusion until the next page load. Architecture (event-driven, no terrain↔WebGPU coupling): - terrain.ts exports `onTerrainChange(listener) -> unsubscribe`. Fires after `setActivePreset` and `setHeightmapOverride` mutate the active height field. Listener errors are caught + logged via the project logger so a failing subscriber doesn't break the others. - world.ts adds `rebuildWorld(device, heightFn, world)` — re-voxelizes via a shared `_voxelize` helper into the existing `voxelBuf` and re-runs `buildBrickMap` against the existing top_grid / brick_pool. Zero new GPU buffer allocation; the transient CPU Uint32Array gets GC'd after `writeBuffer` copies it. - sensors.ts subscribes to `onTerrainChange` after publishing the context. Listener calls `rebuildWorld` with the world it just built. - effects.ts subscribes too, clears `_meshLinkOccluded` (cached occlusion booleans go stale instantly on terrain change) and resets the LiDAR `Points` draw range to 0 (next scan repopulates). Logger consolidation (per session feedback "I have a logger remember that"): converted all WebGPU sensor stack `console.warn` / `console.info` calls to the project's `getLogger` pattern. terrain.ts already used it; sensors.ts and the LiDAR + LoS catch handlers in effects.ts are now consistent. Module names: `webgpu/sensors`, `effects`. Structured context via the `{ error, ... }` second-arg style matching terrain.ts. Bundle: main 808,316 B (under 819,200 cap), +424 B for the event API + logger imports + the `rebuildWorld` helper. Validation: typecheck, vite build, dotnet Release all pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(viz): address PR #74 review feedback sensors.ts (Gemini high + medium): - bootSensors() is now sequentially idempotent. Previously, calling bootSensors() a second time after the first resolved would re-init the device, allocate fresh GPU buffers (leaking the old ones), AND register a duplicate onTerrainChange listener — every preset change would trigger N rebuildWorld() calls instead of one. Now we check getSensorContext() first; if a context already exists, return it directly without re-running boot. - Added the corresponding `getSensorContext` import alongside `setSensorContext` from registry. effects.ts (Gemini medium): - Capture the unsubscribe handle returned by onTerrainChange() into a new private `_terrainUnsub` field. Add a `dispose()` method that calls it. EffectsManager is long-lived in the production viz, but holding the handle is best practice for hot-reload, tests, or scenario teardown — and avoids the linter complaint about an ignored return value. 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
PR #5 of the WebGPU raymarcher direction. First user-visible feature
of the whole arc — mesh-link lines in the production viz now visibly
fade when terrain occludes the drone-pair line-of-sight.
The five PRs in sequence:
march_batchcompute entry)How it works
effects.ts:_updateMeshLinksbuilds oneLosRayper drone-pair eachframe:
origin = A,direction = normalize(B - A),maxT = ‖B - A‖,mask = MASK_OBSTACLES.Skip-if-busy throttle keeps at most one query in flight — at sim
rates (10 Hz) and 60 fps render this is plenty fresh, and the cache
covers the gap.
fade to 25 % of base, e.g. 0.6 → 0.15 for normal links). The
next frame's line teardown/rebuild reads from the cache, so opacity
persists across the per-frame churn.
Bundle architecture (the load-bearing fix)
PR #69 set up
app.tswith a dynamic import for the WebGPU stack sothe runtime lives in its own ~20 KB chunk and doesn't count against
the client-budget cap (800 KiB main).
If
effects.tshad importedgetSensorContextdirectly fromsensors.ts, that static import would have defeated the chunk split(Vite's
INEFFECTIVE_DYNAMIC_IMPORTwarning) and pulled the entireWebGPU runtime back into the main bundle (824 KB → over budget).
Fix: a new tiny
registry.tsmodule — runtime-free aside from asingleton
let+ getter/setter — holds the cached context.sensors.tscallssetSensorContext()once boot finishes;effects.tsreads viagetSensorContext()from the registry.Static dependency on
registry.tsis fine because it has notransitive WebGPU runtime deps (the
SensorContexttype is importedtype-only).
Files added
client/webgpu/registry.ts(~25 LOC): singleton getter/setter forthe sensor context. Pure plumbing — type-only WebGPU imports.
Files modified
client/effects.ts:getSensorContextfromregistry,LosRaytype fromlos,MASK_OBSTACLES+HIT_OBSTACLEfromraysEffectsManagergains_linkOpacityCache: Map<string, number>and
_losQueryInFlight: Promise<void> | nullfields_updateMeshLinksreads cached opacity, builds rays in the samepass as line creation, dispatches one throttled LoS query per
call, populates cache on resolve
client/webgpu/sensors.ts:getSensorContextexport + the local_ctxcachesetSensorContext(ctx)at end ofbootSensors()to publishvia the registry
Bundle impact
+1.4 KB to main (the new effects.ts cache logic + the tiny registry
module). Comfortably under cap.
Test plan
npm run typecheckpassesnpm run buildpasses (noINEFFECTIVE_DYNAMIC_IMPORTwarning)dotnet build -c Releasepasses (pre-push hook)/vianpm run devwith two or more drones spawned;observe mesh-link lines visibly fade when one drone moves behind
a hill relative to another. Lines should snap back to full
opacity once line-of-sight is restored.
bootSensors()resolves),mesh-link lines render with their original opacity — no LoS
modulation, no errors. Graceful fallback verified.
sensors-*.jsis a separate chunk loaded async, not part ofthe main
index-*.jsbundle.🤖 Generated with Claude Code