Skip to content

feat(viz): WebGPU sensor primitive on production route (foundation) - #69

Merged
WomB0ComB0 merged 2 commits into
mainfrom
feat/webgpu-sensor-foundation
Apr 28, 2026
Merged

feat(viz): WebGPU sensor primitive on production route (foundation)#69
WomB0ComB0 merged 2 commits into
mainfrom
feat/webgpu-sensor-foundation

Conversation

@WomB0ComB0

@WomB0ComB0 WomB0ComB0 commented Apr 28, 2026

Copy link
Copy Markdown
Member

Summary

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 in production.

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 page load.
  • Falls back gracefully (silent warn + null context) on browsers
    without WebGPU.

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.
  • No per-frame queries. The LosQueryManager is 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.wgsl modules being pulled into the production bundle
for the first time.

Files added

  • client/webgpu/world.ts (~100 LOC): voxelizes terrainHeight() into
    a 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): 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 — query() calls serialize. PR chore: add Apache-2.0 license headers and install canonical git hooks #5 will add a
    readback ring.
  • client/webgpu/sensors.ts (~100 LOC): bootSensors() /
    getSensorContext() singleton façade. Idempotent. Returns null on
    any failure; consumers must null-check.

Files modified

  • client/app.ts: one new import line, one new void 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:_updateMeshLinks once this foundation
lands. By splitting that out, both PRs stay reviewable and
independently revertible.

Test plan

  • npm run typecheck passes
  • npm run build passes (built in 1.21 s)
  • dotnet build -c Release passes (pre-push hook)
  • Visit / via npm 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 browser
    without WebGPU. Production renderer should be unaffected in
    either case.
  • Toggle to /spike.html route — the existing dev spike still
    renders identically (uses its own world, doesn't share with the
    production sensor stack).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added WebGPU-based sensor system that initializes at application startup
    • Enabled line-of-sight and ray-casting capabilities for terrain queries
    • Improved mesh visualization with terrain-based occlusion detection

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

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@WomB0ComB0 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 47 minutes and 37 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 99d3b657-b5bb-4b19-8631-ba22277c751a

📥 Commits

Reviewing files that changed from the base of the PR and between 734a7c0 and 4a8ae40.

📒 Files selected for processing (3)
  • src/ResQ.Viz.Web/client/app.ts
  • src/ResQ.Viz.Web/client/webgpu/los.ts
  • src/ResQ.Viz.Web/client/webgpu/world.ts
📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
App Startup Integration
src/ResQ.Viz.Web/client/app.ts
Invokes bootSensors() asynchronously after SignalR startup, non-blocking and error-tolerant to provide sensor context to effects.
Sensor Initialization & Context
src/ResQ.Viz.Web/client/webgpu/sensors.ts
Exports bootSensors(), getSensorContext(), and SensorContext type. Coordinates idempotent device initialization, world creation, and LOS manager setup with probe validation and failure isolation.
Line-of-Sight Query Engine
src/ResQ.Viz.Web/client/webgpu/los.ts
Defines LosRay and LosQueryManager for GPU-accelerated raycasting. Handles ray packing, voxel-space transforms, GPU buffer lifecycle, compute pipeline dispatch, result readback, and concurrent call serialization.
World Voxelization
src/ResQ.Viz.Web/client/webgpu/world.ts
Exports createWorld() function and World, WorldParams types. CPU-voxelizes heightfield by occupancy marking, uploads to GPU storage buffer, and builds brick map with uniform grid parameters.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #66: Introduces initDevice() WebGPU device initialization, directly used by bootSensors() to acquire the GPU device at sensor stack startup.
  • PR #68: Extends the WebGPU ray-batch sensor stack with march_batch WGSL entry point, ray/hit packing formats, and host-side batched ray query logic that LosQueryManager wraps.
  • PR #67: Implements brick-map creation and voxel pipeline that createWorld() consumes to build the spatial structure for line-of-sight queries.

Poem

🐇 Hops through GPU voxels deep,
Sensors wake while servers sleep,
Rays bounce off terrain with grace,
Finding occlusion in cyberspace!
No blockage here—just async cheer!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing a WebGPU sensor primitive to the production route as a foundational feature, which aligns with the changeset's core objective of moving the WebGPU raymarcher from spike to production.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/webgpu-sensor-foundation

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 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 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.

Comment thread src/ResQ.Viz.Web/client/webgpu/los.ts Outdated
Comment thread src/ResQ.Viz.Web/client/webgpu/los.ts Outdated

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between a752f05 and 734a7c0.

📒 Files selected for processing (4)
  • src/ResQ.Viz.Web/client/app.ts
  • src/ResQ.Viz.Web/client/webgpu/los.ts
  • src/ResQ.Viz.Web/client/webgpu/sensors.ts
  • src/ResQ.Viz.Web/client/webgpu/world.ts

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

Copy link
Copy Markdown
Member Author

Addressed all 4 review threads + the failing CI bundle-budget check in 4a8ae40:

Critical (CodeRabbit)

  • los.ts query serialization: the previous while (this.inFlight) await this.inFlight wait-loop 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 real promise 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. Only the failing caller sees the rejection.
  • world.ts createWorld validates voxelScale > 0 + finite, gridSize > 0 integer, and origin components finite before voxelizing. Avoids the infinite-loop / NaN-index footgun on bad inputs.

Medium (Gemini)

  • inFlight is non-nullable now (initialized Promise.resolve()), and the serialization uses chaining as suggested. Both Gemini comments converge with the CodeRabbit critical fix.

CI

  • client-budget was failing (bundle 822 KB > 800 KiB cap). Switched app.ts to a dynamic import for ./webgpu/sensors — Vite emits it as a 20 KB chunk that loads in parallel. Main bundle now ~803 KB, back under budget. No behaviour change.

Build green: typecheck, Vite (main 803 KB), dotnet Release.

@WomB0ComB0
WomB0ComB0 merged commit 35b3d87 into main Apr 28, 2026
37 checks passed
@WomB0ComB0
WomB0ComB0 deleted the feat/webgpu-sensor-foundation branch April 28, 2026 21:19
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