feat(viz): WebGPU sensor primitive on production route (foundation) - #69
Conversation
PR #4 of the WebGPU raymarcher direction. First PR that crosses from the hidden /spike.html route into the production /index.html route. Foundation only — no rendering changes, no mesh-link opacity wiring; that arrives in PR #5 once this stabilises. What ships: - A WebGPU device + brick-map world (terrain voxelized into a 1024 m cube via `terrainHeight`) + a ray-batch LoS query manager initialise asynchronously at app boot, alongside the existing Three.js scene. - A one-shot sanity probe at boot fires a single ray straight down through origin and console.logs the resulting hit. Confirms the whole stack works end-to-end on each load. What does NOT ship (yet): - No production rendering changes. Three.js still draws everything (drones, terrain, mesh links) exactly as before. - No effects.ts or drones.ts changes. Mesh-link lines render with the same opacity they always have. Bundle impact: +20 KB minified (~3% of the existing 800 KB chunk). New files: - client/webgpu/world.ts: voxelizes terrainHeight() into a cubic brick-map world (128³ at 8 m per voxel, default origin [-512, 0, -512]). - client/webgpu/los.ts: LosQueryManager wrapper around march_batch. Accepts world-space rays, transforms to grid-space, dispatches, reads back hits, converts t back to world units. Single-slot async for now; PR #5 will add the readback ring when per-frame queries actually run. - client/webgpu/sensors.ts: bootSensors() / getSensorContext() singleton façade. Idempotent; null-safe fallback when WebGPU is unavailable. Modified: - client/app.ts: imports bootSensors and fires it at module load (`void bootSensors();`) alongside the existing `void start();`. Non-blocking, swallows its own errors. Single line of behaviour; no other production-route logic changed. Validation: npm run typecheck and npm run build both 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)
📝 WalkthroughWalkthroughAdds WebGPU sensor infrastructure for terrain occlusion queries. Introduces app boot-time sensor initialization, line-of-sight ray query batching via GPU compute, world voxelization with heightfield-based occupancy, and sensor context coordination. All initialization is non-blocking to prevent startup delays. Changes
Sequence Diagram(s)sequenceDiagram
participant App as App Startup
participant Boot as bootSensors()
participant Device as WebGPU Device
participant World as createWorld()
participant LOS as LosQueryManager
participant GPU as GPU Compute
App->>+Boot: (non-awaited)
Boot->>Device: initDevice()
activate Device
Device-->>Boot: GPUDevice
deactivate Device
Boot->>World: createWorld(device, heightFn, params)
activate World
World->>GPU: Upload voxel occupancy buffer
World->>GPU: Create brick map
World-->>Boot: World{params, brickMap, voxelBuf, gridBuf}
deactivate World
Boot->>LOS: new LosQueryManager(device, world, maxRays)
activate LOS
LOS->>GPU: Allocate ray/hit buffers
LOS->>GPU: Create compute pipeline from march.wgsl
LOS-->>Boot: LosQueryManager
deactivate LOS
Boot->>LOS: query(probe ray) [validation]
activate LOS
LOS->>GPU: Pack rays → GPU buffer
LOS->>GPU: Dispatch compute workgroups
LOS->>GPU: Readback results
LOS-->>Boot: ParsedHit[] (or error logged)
deactivate LOS
Boot-->>App: SensorContext | null (cached)
deactivate Boot
sequenceDiagram
participant User as Effect/Caller
participant Context as getSensorContext()
participant LOS as LosQueryManager
participant GPU as GPU
participant Readback as Mapped Buffer
User->>Context: getSensorContext()
Context-->>User: SensorContext | null
alt Sensor Enabled
User->>LOS: query(rays[])
activate LOS
LOS->>LOS: Validate rays.length ≤ capacity
LOS->>LOS: Serialize via in-flight promise
LOS->>LOS: Pack rays (origin → voxel space, maxT → scaled)
LOS->>GPU: Write packed rays to storage buffer
LOS->>GPU: Dispatch compute (workgroups = rays.length/64)
LOS->>GPU: Copy hits to readback buffer
LOS->>Readback: mapAsync() + getMappedRange()
LOS->>LOS: Transform hit.t back to world-space metres
LOS-->>User: ParsedHit[]
deactivate LOS
else Sensor Disabled/Pending
Context-->>User: null
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 a WebGPU-based sensor primitive for Line-of-Sight (LoS) queries, featuring terrain voxelization into a brick map and a compute-shader-driven query manager. The feedback recommends refactoring the asynchronous query serialization in LosQueryManager to use promise chaining rather than a while loop and nullable property, which would enhance robustness and simplify the implementation.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/ResQ.Viz.Web/client/webgpu/los.ts`:
- Around line 105-126: The current query() serialization uses a shared promise
(this.inFlight) and a wait-loop which lets all waiters resume when that promise
settles, causing concurrent runBatch() calls to race on rayBuf/hitBuf/readBuf
and propagating rejections to all waiters; replace this with a proper FIFO queue
or promise-chain owned by the instance (e.g., a private queue array or
chainPromise field) so each query() enqueues its request and only the head runs
runBatch() exclusively, and ensure each enqueued request gets its own
resolve/reject so a failed batch rejects only that caller while later queued
calls still run normally; update references in query(), runBatch(), and any code
touching this.inFlight, rayBuf, hitBuf, readBuf to use the new serialize
mechanism.
In `@src/ResQ.Viz.Web/client/webgpu/world.ts`:
- Around line 47-65: Before voxelizing, validate the input bounds: check that
voxelScale (vs) is a finite positive number (>0) and gridSize (N) is a finite
positive integer (>0) and still divisible by BRICK; if any check fails throw a
clear Error. Update the function that declares const { gridSize: N, voxelScale:
vs, origin } (and uses heightFn, yiMax, voxels) to perform these guards (finite,
>0 for vs; integer, >0 and N % BRICK === 0 for N) so yiMax’s division by vs
cannot produce NaN/Infinity or create an infinite loop.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e5c749e3-0b8b-4905-851c-b01a236fc66b
📒 Files selected for processing (4)
src/ResQ.Viz.Web/client/app.tssrc/ResQ.Viz.Web/client/webgpu/los.tssrc/ResQ.Viz.Web/client/webgpu/sensors.tssrc/ResQ.Viz.Web/client/webgpu/world.ts
CI: - client-budget was failing because the static import pulled the WebGPU stack into the main bundle (822 KB > 800 KB cap). Switch app.ts to a dynamic import — Vite emits sensors as its own ~20 KB chunk that loads in parallel. Main bundle now ~803 KB, back under budget. No behaviour change; bootSensors() still fires at module load. los.ts (CodeRabbit critical + Gemini medium): - Replace the shared-promise wait-loop with a real promise-chain queue. The previous `while (this.inFlight) await this.inFlight` let multiple waiters race past the same settled promise into runBatch() concurrently and corrupt the shared rayBuf/hitBuf/ readBuf. Now each query() appends to a chain via .then(), and inFlight is updated to a .catch()-wrapped tail so a failed batch settles the chain (lets later callers proceed) without poisoning their results. inFlight is non-nullable now (Promise.resolve() at init). world.ts (CodeRabbit critical): - Validate voxelScale > 0 + finite, gridSize > 0 integer, origin components finite — before voxelizing. The yiMax computation divides by voxelScale and indexes an N³ array, so zero/NaN/Infinity inputs would have produced an infinite loop or out-of-bounds writes. createWorld is exported, so it needed explicit bounds checking. Validation: typecheck, vite build (main bundle 803 KB / cap 800 KiB), dotnet build -c Release all pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Addressed all 4 review threads + the failing CI bundle-budget check in 4a8ae40: Critical (CodeRabbit)
Medium (Gemini)
CI
Build green: typecheck, Vite (main 803 KB), dotnet Release. |
* 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
PR #4 of the WebGPU raymarcher direction. First PR that crosses from
the hidden
/spike.htmlroute into the production/index.htmlroute.Foundation only — no rendering changes, no mesh-link opacity wiring;
that arrives in PR #5 once this stabilises in production.
What ships
cube via
terrainHeight) + a ray-batch LoS query manager initialiseasynchronously at app boot, alongside the existing Three.js scene.
through origin and console.logs the resulting hit. Confirms the
whole stack works end-to-end on each page load.
without WebGPU.
What does NOT ship (yet)
(drones, terrain, mesh links) exactly as before.
effects.tsordrones.tschanges. Mesh-link lines renderwith the same opacity they always have.
LosQueryManageris single-slot today;PR chore: add Apache-2.0 license headers and install canonical git hooks #5 swaps in a readback ring when per-frame mesh-link queries
actually need to keep up with the render loop.
Bundle impact
Vite build output grew from 802 KB → 822 KB minified (+20 KB,
~3 % of the existing chunk). All net additions are the four files in
this PR plus the existing
brickmap.ts/rays.ts/device.ts/shaders/march.wgslmodules being pulled into the production bundlefor the first time.
Files added
client/webgpu/world.ts(~100 LOC): voxelizesterrainHeight()intoa cubic brick-map world. Defaults to 128³ voxels at 8 m per voxel
centred on origin (1024 m cube — covers typical drone-sim flight
envelopes).
client/webgpu/los.ts(~170 LOC):LosQueryManagerwrapper aroundmarch_batch. Accepts world-space rays, transforms to grid-space,dispatches, reads back hits, converts
tback to world units.Single-slot async —
query()calls serialize. PR chore: add Apache-2.0 license headers and install canonical git hooks #5 will add areadback ring.
client/webgpu/sensors.ts(~100 LOC):bootSensors()/getSensorContext()singleton façade. Idempotent. Returns null onany failure; consumers must null-check.
Files modified
client/app.ts: one new import line, one newvoid bootSensors();call at module load. Non-blocking, swallows its own errors.
Why now
Mesh-link line-of-sight against terrain is the first user-visible
feature of this whole arc — it's the one drone-sim improvement that
none of #66/#67/#68 actually delivered. PR #5 will be a ~30-line
surgical change to
effects.ts:_updateMeshLinksonce this foundationlands. By splitting that out, both PRs stay reviewable and
independently revertible.
Test plan
npm run typecheckpassesnpm run buildpasses (built in 1.21 s)dotnet build -c Releasepasses (pre-push hook)/vianpm run dev, check the browser console:-
[viz] WebGPU sensor primitive ready (probe hit): { t: ..., flags: 3, ... }on a WebGPU-capable browser (Chrome/Edge stable, Firefox 141+).
-
[viz] WebGPU sensor primitive disabled: ...on a browserwithout WebGPU. Production renderer should be unaffected in
either case.
/spike.htmlroute — the existing dev spike stillrenders identically (uses its own world, doesn't share with the
production sensor stack).
🤖 Generated with Claude Code
Summary by CodeRabbit