Smooth look on WebGPU: WGSL port fed by GPU inputs and CPU overrides (t88 phase 2c)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 22:42:12 -04:00
co-authored by Claude Opus 5.5
parent 65054e691b
commit 452b6aed32
26 changed files with 780 additions and 80 deletions
+51 -28
View File
@@ -149,25 +149,27 @@ The frame loop applies speed and queued steps while keeping the per-frame tick l
## Drawing
- **The pixel look:**
- `drawWorld` (`src/render/world.js`) fills one pixel per grain;
- `diagonal-screws.js` derives diagonal and straight end bends from `endJoin`, sharing casing geometry between pixels, smooth shading tags, ghosts and previews;
- `computeLight` (`light.js`) adds coloured light;
- overlays (machines, molds, the bot, rain, arrows) are drawn on the 2D canvas afterwards
(`machines.js`, `overlays.js`).
- **The smooth look** (`src/render/smooth.js`) takes the buffers `drawWorld` fills (colour and
a class per cell, the colour behind, trees, fields, light, glow) and shades them with WebGL2.
- The cell classes are set in `world.js` and read in the shader: 4 is a crisp built piece,
6 a wall tile, 7/8 a belt, 9–12 a screw conveyor, 13/14 a filter.
- On software GL it falls back to pixels, unless `window.__allowSoftwareGL` is set (the test
tools set it).
`SET.renderer` selects WebGPU or Legacy for either look. `src/render/cell.js` holds the shared
CPU rules; `objects.js` adds bars, flyers and blueprint ghosts. The GPU canvas sits behind the
transparent game canvas, which draws the existing overlays (machines, molds, bot, rain, arrows).
The pixel look can use WebGPU (`src/render/gpu/`): whole-world textures receive visible
strided uploads, `colour.js` colours plain grains, and `draw.js` selects rare CPU override
blocks and submits colour, bloom and drawing. `cell.js` shares the original CPU cell rules
between Legacy/Smooth and those overrides; `objects.js` adds bars, flyers and blueprint ghosts.
The GPU canvas sits behind the transparent game canvas, which draws all existing overlays.
CPU lighting stays in `light.js`. `SET.renderer` selects WebGPU or Legacy; Smooth uses WebGL2.
- **WebGPU** (`src/render/gpu/`): `textures.js` uploads visible world rectangles, `colour.js`
colours plain grains, and `draw.js` selects rare CPU override blocks and submits the passes.
`light.js` computes lighting; the CPU `computeLight` is never called by this path.
- Pixel uses nearest upscaling and separable GPU bloom (`shaders.js`).
- Smooth adds class, back colour/sky flag, tree/geometry tags and occupancy field outputs to
colour compute. Only this look uploads the CPU back/tree/field overrides and presence mask.
- `smooth-shader.js` and `smooth-slope.js` literally port the legacy GLSL shapes to WGSL.
`smooth.js` creates the smooth pipeline, field mip passes and bindings. Field AO samples
level 2.2; glow retains the legacy nine-tap filter. WebGPU smooth always draws full resolution.
- **Legacy**: `drawWorld` fills every cell on the CPU, and `render/light.js` computes lighting.
Pixel uses Canvas2D; smooth (`render/smooth.js`, `smooth-shaders.js`, `slope-belt-shader.js`)
shades the four inputs with WebGL2 and keeps its adaptive resolution/fallback. Both smooth
renderers share `smoothSurface` for the cached parallax horizon.
- Smooth classes are set in `cell.js`: 4 crisp built piece, 6 wall tile, 7/8 belt, 9–12 screw,
13/14 filter, 19–22 diagonal screw, 23–30 slope belt/filter, 31/32 slope sidewall and cleats.
`diagonal-screws.js` and `slope-belts.js` share the simulation join geometry with pixel colours,
smooth tags, ghosts and previews.
## The browser shell
@@ -421,29 +423,48 @@ Shared CPU colour rules, used by legacy and the rare-cell WebGPU override.
Loose bars, flying grains and queued blueprint blocks in the grain buffer.
- functions: worldObjects
### src/render/gpu/colour.js (82 lines)
### src/render/gpu/colour.js (101 lines)
The plain-cell rules mirror cellPixel; built geometry stays in the CPU override.
- variables: GPU_COLOUR
- functions: gpuColourShader
- variables: GPU_COLOUR, GPU_COLOUR_SMOOTH
### src/render/gpu/shaders.js (42 lines)
Half-resolution separable Gaussian bloom, then nearest colour + linear light.
- variables: GPU_BLUR, GPU_DRAW
### src/render/gpu/device.js (43 lines)
### src/render/gpu/light.js (143 lines)
GPU sources retain the CPU roof and transmission rules; directional relaxation follows only left/up, then right/down, so it cannot introduce extra light paths.
- functions: gpuLightGrid, gpuLightBindings, gpuLight
- variables: GPU_LIGHT, GPU_LIGHT_SPREAD, GPU_LIGHT_PACK
### src/render/gpu/smooth-slope.js (107 lines)
Smooth slope geometry and cleats, ported from slope-belt-shader.js.
- variables: GPU_SMOOTH_SLOPE_HELPERS, GPU_SMOOTH_SLOPE_DRAW
### src/render/gpu/smooth-shader.js (368 lines)
Literal port of the legacy smooth shader; dpdy is reversed for WebGPU's top-left origin.
- variables: GPU_SMOOTH
### src/render/gpu/smooth.js (47 lines)
Field mip levels preserve the legacy trilinear AO sample at level 2.2.
- functions: gpuSmoothInit, gpuSmoothTextures, gpuSmoothBindings, gpuSmoothUpload, gpuSmoothMips
- variables: GPU_FIELD_MIP
### src/render/gpu/device.js (48 lines)
Async startup shares the title screen's adapter; failures leave a usable legacy renderer.
- functions: gpuInit, gpuFail, worldPixelContext
- variables: GPU
### src/render/gpu/textures.js (84 lines)
### src/render/gpu/textures.js (104 lines)
Whole-world allocation, direct strided uploads; no cell packing or dirty tracking.
- functions: gpuTexture, gpuWrite, gpuBuffer, gpuWorldTextures, gpuViewTextures, gpuBindings, gpuUpload
### src/render/gpu/draw.js (69 lines)
### src/render/gpu/draw.js (83 lines)
Only exceptional blocks and their geometric neighbours run the CPU colour rules.
- functions: gpuOverrides, gpuDraw, gpuPresent
- functions: gpuOverrideHas, gpuOverrides, gpuDraw, gpuPresent
- variables: GPU_PATTERN
### src/render/world.js (83 lines)
### src/render/world.js (85 lines)
Scale colour without changing the smooth-look class.
- functions: fogColor, drawWorld
@@ -456,9 +477,9 @@ Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, t
- functions: spawnFloater, drawRain, drawFlowArrows, drawJoinMark, drawJoinMarks, flowLoaded, drawFlowRates, pingImage, drawPing, drawHoveredVein, drawPingRecharge, drawRunOutline, drawFilterTags, drawFloaters, moldLabel, drawMolds, drawBot, spawnDebris, spawnSpark, spawnSuck, drawParticles
- variables: PING_SPEED, PING_HOLD, PING_FADE, PING_BANDS, PING_GLINTS, GLINT_MS
### src/render/smooth.js (103 lines)
### src/render/smooth.js (108 lines)
Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution.
- functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace
- functions: smCompile, smoothAvailable, smUpload, smoothSurface, smoothDraw, smoothPace
- variables: SM, smBytes
### src/render/slope-belt-shader.js (111 lines)
@@ -604,6 +625,7 @@ The frame loop and boot.
- `test/firefox_smoke.py`: An isolated Firefox title screen and three seconds of a new pixel world.
- `test/furnace_play.js`: Fires a starter furnace design repeatedly the way a player would, without cleaning it out between firings, to see whether it clogs. Usage: node test/furnace_play.js [design] [ore] [coal] [firings] [roll] [mode] mode: 'mix' (even ore/coal mix blown in), 'layers' (ore first, coal on top), 'vent' (blown in from above through the vent)
- `test/gpu-readback.js`: Read the four actual smooth textures without adding COPY_SRC usage to game resources.
- `test/harness.js`: Loads the simulation (the src/sim/ files listed in src/manifest.json, joined in order into one script, the same way build.py joins them) so scenarios can drive it headlessly in Node. Errors point at the real file and line (sim/pipes.js:42), not at the joined script.
- `test/play.py`: Play-test in a real browser: mouse, keyboard and a mocked gamepad drive the game; save and continue. Screenshots go to shots/play/ (or the folder given as the first argument). Pixel look unless the second argument is 'smooth'.
- `test/run_all.py`: Run the test suites and report.
@@ -637,7 +659,7 @@ The frame loop and boot.
- `test/t_slope_belt_art.js`: Drawing geometry against real parts, without changing transport.
- `test/t_slope_belts.js`: Sloped belts: transport, joins, sorting, persistence and the actual drag controls.
- `test/t_slopes.js`: Sloped walls: flow, sealed joints, building, refunds and persistence.
- `test/t_smooth.py`: The smooth look: the WebGL pass compiles, draws, falls back cleanly, and the thermal lens still works.
- `test/t_smooth.py`: The smooth look: the WebGPU pass compiles, draws, falls back cleanly, and the thermal lens still works.
- `test/t_stone.js`: Uses for waste rock and sand: glass bricks from a fan furnace, the Saw, the silo and builders' orders.
- `test/t_water.js`: Water conservation, settling, ledge pouring, boundaries, pipes and phase changes. Tall worlds keep geothermal heat out of the ponds.
- `test/t_weather.js`: Weather: no more springs; steam rises into clouds and rains back; showers; snow when it is freezing.
@@ -679,4 +701,5 @@ The frame loop and boot.
- `tools/scenes/t35-joins.js`: Run after clear-sky.js. Paused, empty pipes make casing, flights and mouths easy to inspect.
- `tools/scenes/t36-funnel.js`: A twelve-block funnel, one block open at the bottom, draining 400 grains of sand.
- `tools/scenes/t40-sloped-belt.js`: Self-contained: a flat feed climbs, crosses the top, and runs downhill; a filter sorts below.
- `tools/scenes/t88-light.js`: Frozen light comparisons, also used by the WebGPU browser regression suite.
- `tools/scenes/t88-renderer.js`: Paused comparison scene: ordinary grains, fog, heat, trees and exceptional geometry.
+15 -3
View File
@@ -67,7 +67,7 @@ diff -r /tmp/before /tmp/after
gamepad and world sizes. Screenshots go to
`shots/play/`. Pixel look by default; `python3 test/play.py shots/play smooth` uses the smooth
look.
- `test/t_smooth.py` checks that the WebGL2 smooth look compiles and draws, that it falls back
- `test/t_smooth.py` checks that the WebGPU smooth look compiles and draws, compares legacy at half resolution, and falls back
to pixels cleanly, and that the thermal lens works.
- Run them one at a time. Two headless browsers at once can starve each other, and
`t_smooth.py`'s half-resolution check then fails. `run_all.py` runs them one after the other.
@@ -110,8 +110,8 @@ python3 tools/shot.py shots/silo.png --setup tools/scenes/clear-sky.js \
`tools/scenes/clear-sky.js` clears a flat work area and sets `FLOOR`, `BX`, `BY` and `CAM`. Set
`CAM = [x, y]` to frame the shot.
- The HUD and the cursor previews are hidden unless you pass `--hud` or `--previews`.
- `--renderer webgpu|legacy` selects the pixel renderer (default WebGPU). WebGPU screenshots
fail clearly when no device is available; Smooth still uses WebGL2.
- `--renderer webgpu|legacy` selects the renderer for either look (default WebGPU). WebGPU screenshots
fail clearly when no device is available; Legacy smooth uses WebGL2; WebGPU smooth uses WGSL.
- `test/t_gpu.py` compares the rendered composite against Legacy for ordinary cells, built pieces,
ghosts, fog and all thermal palettes, and checks world replacement, resize, timing and device loss.
World-pixel tests use `worldPixelContext()` to read both stacked canvases. The helper
@@ -233,3 +233,15 @@ CPU. The GPU canvas draws the world; the transparent game canvas holds the exist
The comparison fixture is `tools/scenes/t88-renderer.js`. Capture with `--freeze --zoom 12`
(and 6 or 2), `--viewport 1920x1080`, and each `--renderer`. It includes terrain, water, fire,
magma, fog, a tree, flat/slope belts, screws, pipe, mold, bar and blueprint ghost.
WebGPU Phase 2c: both looks use WebGPU. `test/t_gpu.py` also reads all four smooth input
textures back and compares them against the legacy CPU buffers at zoom 12/6/2 with fog on/off:
classes, sky flags, field bits and geometry tags must match exactly; RGB can differ by one.
The smooth shader retains the legacy nine-tap glow filter and samples field AO at mip level 2.2;
three render passes generate field levels 1-3, including non-power-of-two view sizes.
Only smooth allocates/uploads the back/tree/field overrides and a separate presence mask.
`tools/perf.py` refuses a WebGPU fallback in either look and records WebGPU scale as 1.
The screenshot tool invalidates the shared parallax horizon after scene setup changes `surfY`,
so renderer startup timing cannot change the hills in a comparison. Mip pass references:
[Chrome's porting guide](https://developer.chrome.com/docs/web-platform/webgpu/from-webgl-to-webgpu#mipmap_generation)
and [WebGPU Fundamentals](https://webgpufundamentals.org/webgpu/lessons/webgpu-importing-textures.html#generating-mips-on-the-gpu).
+14
View File
@@ -4,6 +4,20 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details.
## 2026-10-27: t88 phase 2c, smooth look on WebGPU (Codex + Claude Code)
- WGSL port of the smooth look (`src/render/gpu/smooth.js`, `smooth-shader.js`, `smooth-slope.js`):
the GPU colour pass writes all four smooth inputs for plain cells; the CPU rare-cell overrides
(`cell.js`, now also mold walls) fill them for built pieces and objects (12.73/3.41/0.38% of
factory cells at zoom 12/6/2). Field AO via three GPU mip passes; nine-tap glow kept; no
WebGPU `smoothPace` (max GPU time 1.24 ms). Legacy still selectable.
- Checked by Codex: 21 screenshot pairs legacy vs WebGPU, mean RGB error 0.01-0.22/255, small
counts off by more than 8 (thin AO and edge rounding), no missing shapes or open joins; input
readbacks equal legacy. Smooth factory drawWorld zoom 2 28.2 -> 10.0 ms; real loop zoom 2 CPU
p50/p95 34.7/39.0 -> 14.5/15.7 ms. Claude did not inspect the images (token budget).
- Open for 2d: adapters with fewer than five storage textures per stage fall back to legacy.
- Tests: `python3 test/run_all.py --browser` passed.
## 2026-10-27: t88 phase 2b, lighting on the GPU (Codex + Claude Code)
- `src/render/gpu/light.js`: light sources per light cell (transmittance, sky from `roofY`, heat,