Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

js River erosion simulation with CPU/webGPU backends

base idea:

  • water particles move by heightmap
  • number of particles in cell = water level = pressure = ability to erode
  • water level map is temp (clear on start of frame), not hold any real mass of water or sediments. water level map affect on water pressure and sediment speed but cant cause loss of water or soil mass by design. race write to water level is ok on GPU, no atomic need read water level map + land heightmap = total height, then apply tilt angle of whole map, then calculate gravity movement particles direction.
  • dx/dt can be > then cell size
  • tilt angle of whole map adjustable by sliders realtime
  • wrap modes: not wrap, solid border: water and sediment cant go outside not wrap: respawn: water and sediment out of map respawn as rain of water, sediment added to height level to keep mass (in GPU mode ignore sediment loss to optimize). wrap XY: properly to move water an sediment
  • dynamic manual and auto scale height prevent erode to 0

Next is not for human reading :) and may be just LLM hallucination, need review:

RiverEngine

  • CPU · Lagrangian reference (sim.js) — a DOM-free, particle-based river/erosion model driven by uniform rainfall (respawn = rain) and evaporation/domain-exit (death). It is the file:// fallback and the mass-conservation sanity check. Meandering emerges from the flow↔bed↔erosion feedback: a confinement term (k3) pulls flow into existing channels, and an outer-bank (lateral) erosion term (k4) migrates channels sideways into bends.
  • WebGPU · atomic-free Eulerian (webgpu.js) — the same physics reformulated as a grid-resident model so every compute pass is a gather (exactly one writer per cell/tracer). No atomics, no scatter hazards. It reproduces the self-consistent flow↔bed feedback that makes meanders emerge, but it will not bit-match the CPU (different carrier — by design).

Run it

# CPU only works straight from disk:
open river/index.html            # file:// — CPU backend

# WebGPU needs a secure context (localhost/HTTPS):
cd river && python3 -m http.server 8000
# then visit http://localhost:8000/  and pick "WebGPU" in the backend dropdown

If navigator.gpu / window.isSecureContext is unavailable, the WebGPU option is disabled in the UI with a note.

File layout

river/
  index.html   script loading, canvas (#cv 2D + #gpuCv WebGPU), backend select
  src/         common.js · sim.js · engine.js · webgpu.js · gui.js
  test.js      node: CPU determinism + soil-mass balance + 1000-step meander gate
  tools/       validate-wgsl.js — offline WGSL structural guard (wgsl_reflect)

Offline WGSL validation

tools/validate-wgsl.js parses the three WGSL modules (webgpu.js exports WGSL / WGSL_RENDER / WGSL_BLIT) with wgsl_reflect and asserts the structural invariants a real browser (Tint/Naga) rejects at pipeline-creation time — entry points present, @group(0) bindings unique & in range, the Sim uniform exactly 128 bytes, and that every JS bind group matches the exact binding set each entry point needs (the layout: 'auto' failure mode: an extra/missing binding throws). Run it without a GPU:

npm install            # installs wgsl_reflect into tools/
npm run validate:wgsl

The WebGPU init is also hardened: each compute entry point is created under a validation error scope and reported by name; smoothHeight and tracerAdvect are optional and degrade gracefully (the sim still runs without them) instead of taking down the whole backend.

A second offline harness, tools/smoke-gpu.js, runs the full init → step → exportToCPU path against a usage-validating fake WebGPU (no real GPU). It enforces the same rules Chrome does — a copy source needs COPY_SRC, a copy dest needs COPY_DST, writeBuffer needs COPY_DST, mapAsync(READ) needs MAP_READ, and copy offsets/sizes must be 4-byte aligned & in range. It catches the "buffer usage … does not include" class of bug without a browser:

node tools/smoke-gpu.js

Sacred invariant

Soil mass is conserved exactly on the CPU (the float64/rounding ledger: bedMass + transit + exported + roundLoss == initialBed). The GPU tracks the same quantity via a staged reduction (sum hC + sum s + exported); semi-Lagrangian advection makes it approximately conserved (reported like the example reports energy drift). Water w is a driver/diagnostic, not conserved.

Curl field

Computed on the CPU (RiverCPU.updateCurl) and uploaded to the GPU every CURL_EVERY steps — the simplex noise is never ported to WGSL (the single biggest risk-reducer).

Backend toggle

Terrain is transferred on switch; the w/s (Eulerian water/sediment) state is intentionally not transferred (different models — documented in the UI). GPU→CPU re-baselines the ledger from the read-back heightmap.

Notes / limitations

  • Verified in this repo (headless node):
    • CPU determinism, mass balance (the float64/rounding ledger is exact — lateral relocation is a within-bed move and never touches bedMass/SED/exported), and the 1000-step meander gate: significant heightmap change and channel-scale change concentrated into a minority of the grid (channels, not a diffuse sheet). See npm test / node test.js.
    • WebGPU WGSL structural validation (npm run validate:wgsl) — all three modules parse, bindings are unique/in-range, the Sim uniform is 128 bytes, and each JS bind group matches its entry point's exact binding set.
  • The WebGPU backend has not been executed on a real GPU in this environment (no GPU/secure context here). The structural checks above are the offline safety net; the remaining risk is runtime semantic validation by Tint/Naga, which wgsl_reflect cannot fully replicate. When run in a browser, init surfaces the exact failing entry point, and optional passes (smooth, tracerAdvect, lateralCompute/lateralApply, tracer plotting) degrade instead of killing the backend. The GPU lateralErode mirror uses the water gradient (no Pr on GPU) as the channel indicator — a documented backend divergence, not a bug. Sinuosity itself is a browser/visual check.
  • The GPU water field (wB) is rebuilt every step from the actual tracer density (atomic splat + decay, matching the CPU's particle-count Pr), so its volume tracks the particles and pools in low areas (lakes) instead of a uniform rain sheet. water scale / water alpha (View sliders) tune the gain/opacity.
  • GPU observation is throttled (obsEvery steps) — the sim advances every step, but the stats reduction + CPU-mirror readback (combined into one double-buffered buffer, one mapAsync) run only every N steps, or on demand for export/fallback. Validate the meander shape and frame-time in a browser; tuning knobs (k1..k4, latThr, threshold, tilt) are shared so the two backends can be visually matched.
  • requestRescale on the GPU path only updates the render auto-range; a full heightmap remap pass is intentionally left out of the GPU model.

Releases

Packages

Contributors

Languages