WebGPU pixel look: GPU colours the world, CPU only rare cells (t88 phase 2a)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+42
-3
@@ -162,6 +162,13 @@ The frame loop applies speed and queued steps while keeping the per-frame tick l
|
|||||||
- On software GL it falls back to pixels, unless `window.__allowSoftwareGL` is set (the test
|
- On software GL it falls back to pixels, unless `window.__allowSoftwareGL` is set (the test
|
||||||
tools set it).
|
tools set it).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## The browser shell
|
## The browser shell
|
||||||
|
|
||||||
- `GAME` (`src/ui/state.js`) holds the UI state: tool, selected piece, build tab, drag,
|
- `GAME` (`src/ui/state.js`) holds the UI state: tool, selected piece, build tab, drag,
|
||||||
@@ -406,7 +413,37 @@ Screw casing geometry shared by both looks, ghosts and placement previews. smoot
|
|||||||
- functions: diagonalScrewJoins, straightScrewJoins, diagonalScrewShape, diagonalScrewBlock
|
- functions: diagonalScrewJoins, straightScrewJoins, diagonalScrewShape, diagonalScrewBlock
|
||||||
- variables: SCREW_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap
|
- variables: SCREW_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap
|
||||||
|
|
||||||
### src/render/world.js (419 lines)
|
### src/render/cell.js (272 lines)
|
||||||
|
Shared CPU colour rules, used by legacy and the rare-cell WebGPU override.
|
||||||
|
- functions: cellPixel
|
||||||
|
|
||||||
|
### src/render/objects.js (80 lines)
|
||||||
|
Loose bars, flying grains and queued blueprint blocks in the grain buffer.
|
||||||
|
- functions: worldObjects
|
||||||
|
|
||||||
|
### src/render/gpu/colour.js (82 lines)
|
||||||
|
The plain-cell rules mirror cellPixel; built geometry stays in the CPU override.
|
||||||
|
- variables: GPU_COLOUR
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
Only exceptional blocks and their geometric neighbours run the CPU colour rules.
|
||||||
|
- functions: gpuOverrides, gpuDraw, gpuPresent
|
||||||
|
- variables: GPU_PATTERN
|
||||||
|
|
||||||
|
### src/render/world.js (83 lines)
|
||||||
Scale colour without changing the smooth-look class.
|
Scale colour without changing the smooth-look class.
|
||||||
- functions: fogColor, drawWorld
|
- functions: fogColor, drawWorld
|
||||||
|
|
||||||
@@ -442,7 +479,7 @@ Game shell (src/ui/): browser only. This file: settings, the GAME state object,
|
|||||||
- functions: loadSettings, saveSettings, el, esc, rgbOf, fmtT, toast, setStatus
|
- functions: loadSettings, saveSettings, el, esc, rgbOf, fmtT, toast, setStatus
|
||||||
- variables: SET_DEFAULT, SET, TOOLS, ICONS, BAG_ORDER, GAME, keys, hudDirty, $
|
- variables: SET_DEFAULT, SET, TOOLS, ICONS, BAG_ORDER, GAME, keys, hudDirty, $
|
||||||
|
|
||||||
### src/ui/title.js (74 lines)
|
### src/ui/title.js (76 lines)
|
||||||
Startup capabilities and title art.
|
Startup capabilities and title art.
|
||||||
- functions: checkCapabilities, renderCapabilityNotice, canStartGame, buildTitleCells, drawTitle
|
- functions: checkCapabilities, renderCapabilityNotice, canStartGame, buildTitleCells, drawTitle
|
||||||
- variables: REQUIRE_GPU_AND_THREADS, CAPS, CAPS_CHECK, FONT5, TITLE
|
- variables: REQUIRE_GPU_AND_THREADS, CAPS, CAPS_CHECK, FONT5, TITLE
|
||||||
@@ -461,7 +498,7 @@ Shared player state; the world save remains a complete standalone fallback.
|
|||||||
Starting, loading and quitting a game; modal dialogs; the pause menu.
|
Starting, loading and quitting a game; modal dialogs; the pause menu.
|
||||||
- functions: showLoading, enterGame, setupNewWorld, startNewWorld, startFromSave, loadSavedWorld, quitToTitle, refreshContinue, anyModal, openModal, closeModal, closeAllModals, topModal, dialog, openPause, renderWorlds
|
- functions: showLoading, enterGame, setupNewWorld, startNewWorld, startFromSave, loadSavedWorld, quitToTitle, refreshContinue, anyModal, openModal, closeModal, closeAllModals, topModal, dialog, openPause, renderWorlds
|
||||||
|
|
||||||
### src/ui/settings.js (125 lines)
|
### src/ui/settings.js (126 lines)
|
||||||
The settings screen.
|
The settings screen.
|
||||||
- functions: openSettings, renderSettings, applySettings, toggleFullscreen, confirmWipe
|
- functions: openSettings, renderSettings, applySettings, toggleFullscreen, confirmWipe
|
||||||
- variables: SET_TABS, setTab
|
- variables: SET_TABS, setTab
|
||||||
@@ -585,6 +622,7 @@ The frame loop and boot.
|
|||||||
- `test/t_flow.js`: Throughput overlay (t49): grains per second per transport block, counted only while the overlay is on, never changing what the simulation does.
|
- `test/t_flow.js`: Throughput overlay (t49): grains per second per transport block, counted only while the overlay is on, never changing what the simulation does.
|
||||||
- `test/t_fog.js`: Fog: surface, sight, range, saves and simulation independence.
|
- `test/t_fog.js`: Fog: surface, sight, range, saves and simulation independence.
|
||||||
- `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player.
|
- `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player.
|
||||||
|
- `test/t_gpu.py`: WebGPU pixel parity, rare-cell selection, world replacement, resize and device loss.
|
||||||
- `test/t_joins.js`: The oracle states the geometric rule independently of the precomputed neighbour tables.
|
- `test/t_joins.js`: The oracle states the geometric rule independently of the precomputed neighbour tables.
|
||||||
- `test/t_light.js`: Run the renderer's lighting against real simulation data, with only canvas storage stubbed.
|
- `test/t_light.js`: Run the renderer's lighting against real simulation data, with only canvas storage stubbed.
|
||||||
- `test/t_load.js`: Reload wakes nearby areas first, without losing distant water, fire or saved state.
|
- `test/t_load.js`: Reload wakes nearby areas first, without losing distant water, fire or saved state.
|
||||||
@@ -641,3 +679,4 @@ 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/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/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/t40-sloped-belt.js`: Self-contained: a flat feed climbs, crosses the top, and runs downhill; a filter sorts below.
|
||||||
|
- `tools/scenes/t88-renderer.js`: Paused comparison scene: ordinary grains, fog, heat, trees and exceptional geometry.
|
||||||
|
|||||||
+22
-1
@@ -110,6 +110,15 @@ 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
|
`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.
|
`CAM = [x, y]` to frame the shot.
|
||||||
- The HUD and the cursor previews are hidden unless you pass `--hud` or `--previews`.
|
- 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.
|
||||||
|
- `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
|
||||||
|
presents the retained GPU result again before copying, because canvas textures expire after
|
||||||
|
presentation ([WebGPU specification](https://gpuweb.github.io/gpuweb/#abstract-opdef-expire-the-current-texture),
|
||||||
|
[Mozilla implementation notes](https://bugzilla.mozilla.org/show_bug.cgi?id=1965377)). Tests that change
|
||||||
|
static `noiseB` in place set `GPU.world = null` before drawing to upload their new fixture.
|
||||||
- `--look smooth` for the smooth look. Test in the pixel look unless you are checking graphics.
|
- `--look smooth` for the smooth look. Test in the pixel look unless you are checking graphics.
|
||||||
- `--freeze` draws the last frame at a fixed time and frame count, so animations are caught at
|
- `--freeze` draws the last frame at a fixed time and frame count, so animations are caught at
|
||||||
the same moment. Two shots of the same scene then match pixel for pixel, and a before / after
|
the same moment. Two shots of the same scene then match pixel for pixel, and a before / after
|
||||||
@@ -193,7 +202,7 @@ python3 tools/shot.py shots/silo.png --setup tools/scenes/clear-sky.js \
|
|||||||
simulation work per frame and number of ticks. p50/p95/p99 use interpolated sorted samples;
|
simulation work per frame and number of ticks. p50/p95/p99 use interpolated sorted samples;
|
||||||
five warm-up frames are excluded. Zero-tick frames correctly have zero simulation time.
|
five warm-up frames are excluded. Zero-tick frames correctly have zero simulation time.
|
||||||
JSON also records the smooth renderer's adaptive resolution scale: compare it as well as time.
|
JSON also records the smooth renderer's adaptive resolution scale: compare it as well as time.
|
||||||
Comparisons require identical options, print all timing changes, and mark >10% regressions;
|
Comparisons accept subsets of the baseline scenes, looks and zooms, with identical world/viewport dimensions and run duration, print all timing changes, and mark >10% regressions;
|
||||||
tiny timings and tails can be noisy, and a zero baseline has no meaningful percent change.
|
tiny timings and tails can be noisy, and a zero baseline has no meaningful percent change.
|
||||||
|
|
||||||
Performance runs launch Chromium with
|
Performance runs launch Chromium with
|
||||||
@@ -212,3 +221,15 @@ python3 tools/shot.py shots/silo.png --setup tools/scenes/clear-sky.js \
|
|||||||
- `tools/experiments/factory_cost.js`: what running factories cost the simulation.
|
- `tools/experiments/factory_cost.js`: what running factories cost the simulation.
|
||||||
- `tools/experiments/flow.js`: belt and screw conveyor throughput in grains a second.
|
- `tools/experiments/flow.js`: belt and screw conveyor throughput in grains a second.
|
||||||
- `tools/experiments/worldgen.js`: material counts and timings of world generation.
|
- `tools/experiments/worldgen.js`: material counts and timings of world generation.
|
||||||
|
|
||||||
|
WebGPU Phase 2a: Colour measures the CPU override fill (including loose objects and ghosts).
|
||||||
|
`frame_ms` is the complete paused-frame CPU call (including overlays and UI).
|
||||||
|
Upload measures strided visible-array writes and the override/light uploads, and Blit measures
|
||||||
|
GPU command submission. `override_cells` records the selected CPU cells; GPU timing is a
|
||||||
|
completed timestamp sample for that configuration across colour, bloom and drawing when `timestamp-query` is
|
||||||
|
available. The timestamp excludes uploads and the CPU lighting pass. Lighting remains on the
|
||||||
|
CPU. The GPU canvas draws the world; the transparent game canvas holds the existing overlays.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
@@ -4,6 +4,22 @@ 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
|
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.
|
open or not verified. Keep each entry short; the git history has the details.
|
||||||
|
|
||||||
|
## 2026-10-27: t88 phase 2a, WebGPU pixel look (Claude Code + Codex)
|
||||||
|
|
||||||
|
- New `src/render/gpu/` (device, textures, colour compute, glow blur, draw) renders the pixel look
|
||||||
|
on a `#gpu` canvas under `#game`, which becomes a transparent overlay in WebGPU mode. The GPU
|
||||||
|
colours plain cells (background, materials with noise and grain, loose shade, fire, water,
|
||||||
|
ALPHA, digp shake, incandescence, lens, fog); the CPU colours only rare cells (built blocks,
|
||||||
|
screw/slope neighbours, bars, flyers, ghosts) through the extracted per-cell code
|
||||||
|
(`src/render/cell.js`, `src/render/objects.js`). World arrays upload per visible rectangle with
|
||||||
|
`queue.writeTexture`; light is still `computeLight` on the CPU, multiplied on the GPU.
|
||||||
|
`SET.renderer` = webgpu | legacy (Settings). The smooth look still uses the WebGL2 path.
|
||||||
|
- Screenshots legacy vs WebGPU: mean abs RGB error 0.112 / 0.062 / 0.034 at zoom 12/6/2, no pixel
|
||||||
|
off by more than 8. Factory scene drawWorld at zoom 2: 24.55 -> 9.98 ms (overrides 1.81, uploads
|
||||||
|
2.39, CPU light 5.59; GPU time 0.06 ms). Real loop zoom 2 CPU p50/p95: 31.7/35.3 -> 15.4/16.7 ms.
|
||||||
|
- Tests: `python3 test/run_all.py --browser` all passed (new `test/t_gpu.py`). Not checked:
|
||||||
|
Firefox WebGPU (no adapter in Playwright Firefox on Linux).
|
||||||
|
|
||||||
## 2026-10-27: t88 phase 1, serving and capability checks (Claude Code + Codex)
|
## 2026-10-27: t88 phase 1, serving and capability checks (Claude Code + Codex)
|
||||||
|
|
||||||
- The game must be served with COOP/COEP (README has nginx and Caddy lines; opening from disk is
|
- The game must be served with COOP/COEP (README has nginx and Caddy lines; opening from disk is
|
||||||
|
|||||||
@@ -48,6 +48,13 @@
|
|||||||
"render/light.js",
|
"render/light.js",
|
||||||
"render/slope-belts.js",
|
"render/slope-belts.js",
|
||||||
"render/diagonal-screws.js",
|
"render/diagonal-screws.js",
|
||||||
|
"render/cell.js",
|
||||||
|
"render/objects.js",
|
||||||
|
"render/gpu/colour.js",
|
||||||
|
"render/gpu/shaders.js",
|
||||||
|
"render/gpu/device.js",
|
||||||
|
"render/gpu/textures.js",
|
||||||
|
"render/gpu/draw.js",
|
||||||
"render/world.js",
|
"render/world.js",
|
||||||
"render/machines.js",
|
"render/machines.js",
|
||||||
"render/overlays.js",
|
"render/overlays.js",
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
// Shared CPU colour rules, used by legacy and the rare-cell WebGPU override.
|
||||||
|
function cellPixel(x, y) {
|
||||||
|
const { bw, bh, px, vx0, vy0, smoothOn: sm, rain: rk } = R;
|
||||||
|
const bx = x - vx0, by = y - vy0, row = by * bw;
|
||||||
|
if (R.gpuOn && !px[row + bx]) GPU.overrideCount++;
|
||||||
|
const bpx = R.backPx, tpx = R.treePx, fpx = R.fieldPx, mmask = R.moldMask;
|
||||||
|
const OUT = sm ? 0x04000000 : 0xff000000;
|
||||||
|
if (x < 0 || y < 0 || x >= W || y >= H) {
|
||||||
|
px[row + bx] = OUT;
|
||||||
|
if (sm) { bpx[row + bx] = 0; tpx[row + bx] = 0; fpx[row + bx] = 0xff; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const useFog = SET.fog, grain = SET.grain, incand = SET.incandescence;
|
||||||
|
const anim = R.frame, lens = R.lens, tl = THERM[SET.thermalPalette] || THERM.inferno;
|
||||||
|
const glowOn = SET.glow !== 'off' && !lens, gpx = R.glowPx, gw = R.glowC.width;
|
||||||
|
const fy = y / FOG, fj = Math.floor(fy), ft = fy - fj, fr = fj * FW, fn = Math.min(FH - 1, fj + 1) * FW;
|
||||||
|
const yw = y * W;
|
||||||
|
let skr = SKY_R[y], skg = SKY_G[y], skb = SKY_B[y];
|
||||||
|
if (rk > 0.01) { const gy = (skr + skg + skb) / 3 * 0.82; skr += (gy - skr) * rk * 0.7; skg += (gy - skg) * rk * 0.7; skb += (gy - skb) * rk * 0.65; }
|
||||||
|
let fv = 255;
|
||||||
|
if (useFog) {
|
||||||
|
const fx = x / FOG, j = Math.floor(fx), t = fx - j, j1 = Math.min(FW - 1, j + 1);
|
||||||
|
const a = fog[fr + j], b = fog[fr + j1], c = fog[fn + j], d = fog[fn + j1];
|
||||||
|
if ((a & b & c & d) !== 255) fv = (a + (b - a) * t) * (1 - ft) + (c + (d - c) * t) * ft;
|
||||||
|
if (!fv) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } return; }
|
||||||
|
// ease the fade into black (smoothstep) and break up the half-resolution steps with grain
|
||||||
|
// noise that is strongest mid-fade and nothing at either end, so the outer edge has no rim
|
||||||
|
if (fv < 255) {
|
||||||
|
let s = fv / 255;
|
||||||
|
s = Math.min(1, Math.max(0, s + (noiseB[yw + x] - 128) * (0.6 / 128) * s * (1 - s)));
|
||||||
|
fv = Math.max(1, 255 * s * s * (3 - 2 * s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const i = yw + x, m = mat[i], nz = noiseB[i] - 128;
|
||||||
|
const T = temp[i];
|
||||||
|
// Also used by the thermal lens, which always draws in pixels.
|
||||||
|
let looseShade = 1;
|
||||||
|
if (!sm && m !== EMPTY && !builtForLight(i, x, y)) {
|
||||||
|
if (PH[m] === PH_POWDER) looseShade = 1.1 + nz * 0.00065;
|
||||||
|
else if (PH[m] === PH_SOLID && (
|
||||||
|
(x > 0 && PH[mat[i - 1]] === PH_POWDER && !part[i - 1]) ||
|
||||||
|
(x + 1 < W && PH[mat[i + 1]] === PH_POWDER && !part[i + 1]) ||
|
||||||
|
(y > 0 && PH[mat[i - W]] === PH_POWDER && !part[i - W]) ||
|
||||||
|
(y + 1 < H && PH[mat[i + W]] === PH_POWDER && !part[i + W])
|
||||||
|
)) looseShade = 0.72;
|
||||||
|
}
|
||||||
|
let r, g, b;
|
||||||
|
if (lens) {
|
||||||
|
const ti = thermIdx(T);
|
||||||
|
if (m === EMPTY) { r = tl[ti] * 0.55; g = tl[ti + 1] * 0.55; b = tl[ti + 2] * 0.55; }
|
||||||
|
else { const v = (COLR[m] + COLG[m] + COLB[m]) / 3 * 0.25; r = tl[ti] * 0.78 + v; g = tl[ti + 1] * 0.78 + v; b = tl[ti + 2] * 0.78 + v; }
|
||||||
|
const p = part[i];
|
||||||
|
r *= looseShade; g *= looseShade; b *= looseShade;
|
||||||
|
if (p && ((x + y) & 1)) { r = r * 0.7 + 40; g = g * 0.7 + 40; b = b * 0.7 + 40; }
|
||||||
|
if (fv < 255) { r *= fv / 255; g *= fv / 255; b *= fv / 255; }
|
||||||
|
px[row + bx] = 0xff000000 | ((b > 255 ? 255 : b) << 16) | ((g > 255 ? 255 : g) << 8) | (r > 255 ? 255 : r);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// background
|
||||||
|
let br, bgc, bb, sky = false;
|
||||||
|
const tb = bgm[i];
|
||||||
|
if (tb && !sm) { const v = nz * CVAR[tb] / 128; br = TREE_R[tb] + v; bgc = TREE_G[tb] + v; bb = TREE_B[tb] + v; if (digp[i]) { const f = 1 + digp[i] / 700; br *= f; bgc *= f; bb *= f; } }
|
||||||
|
else if (y < surfY[x]) { br = skr; bgc = skg; bb = skb; sky = true; }
|
||||||
|
else {
|
||||||
|
const L = y < dirtY[x] ? BG.dirt : y < graniteY[x] ? BG.stone : y < basaltY[x] ? BG.granite : BG.basalt;
|
||||||
|
const v = sm ? 0 : nz * 0.06;
|
||||||
|
br = L[0] + v; bgc = L[1] + v; bb = L[2] + v;
|
||||||
|
}
|
||||||
|
if (sm) {
|
||||||
|
// the colour behind the grain (alpha marks open sky), and a tree in front of that if there is one
|
||||||
|
bpx[row + bx] = (sky ? 0xff000000 : 0) | ((bb & 255) << 16) | ((bgc & 255) << 8) | (br & 255);
|
||||||
|
if (tb) {
|
||||||
|
const v = nz * CVAR[tb] / 256, f = digp[i] ? 1 + digp[i] / 700 : 1;
|
||||||
|
const tr = Math.max(0, Math.min(255, (TREE_R[tb] + v) * f)), tg = Math.max(0, Math.min(255, (TREE_G[tb] + v) * f)), tbb = Math.max(0, Math.min(255, (TREE_B[tb] + v) * f));
|
||||||
|
tpx[row + bx] = ((tb === LEAVES ? 0xff : 0x80) << 24) | (tbb << 16) | (tg << 8) | tr;
|
||||||
|
} else tpx[row + bx] = 0;
|
||||||
|
}
|
||||||
|
if (m === EMPTY) { r = br; g = bgc; b = bb; }
|
||||||
|
else {
|
||||||
|
const v = nz * CVAR[m] * (sm ? (PH[m] === PH_SOLID ? 0 : grain * 0.35) : grain) / 128;
|
||||||
|
r = COLR[m] + v; g = COLG[m] + v; b = COLB[m] + v;
|
||||||
|
r *= looseShade; g *= looseShade; b *= looseShade;
|
||||||
|
if (m === FIRE) { const f = (life[i] & 15) * 6; r = 255; g = 120 + f; b = 30 + f * 0.5; }
|
||||||
|
else if (m === WATER && !sm) { const s = ((x * 3 + y * 5 + (anim >> 2)) & 15) === 0 ? 14 : 0; r += s; g += s; b += s; }
|
||||||
|
else if (m === BELTL || m === BELTR) {
|
||||||
|
const dir = m === BELTR ? -1 : 1, on = netSat(i) > 0, ly = y & 3, lx = x & 3;
|
||||||
|
if (ly === 0 || ly === 3) {
|
||||||
|
// top run moves with the load, bottom run returns the other way
|
||||||
|
const st = ((x + (ly === 0 ? dir : -dir) * (on ? anim >> 1 : 0)) >> 1) & 1;
|
||||||
|
const k = ly === 0 ? 1 : 0.72;
|
||||||
|
r = (st ? 92 : 56) * k; g = (st ? 96 : 58) * k; b = (st ? 104 : 66) * k;
|
||||||
|
} else if (lx === 1 || lx === 2) {
|
||||||
|
const spin = on ? ((anim >> 2) + (dir < 0 ? lx : 3 - lx) + ly) & 1 : (lx + ly) & 1;
|
||||||
|
r = spin ? 132 : 104; g = spin ? 120 : 94; b = spin ? 108 : 84; // copper rollers
|
||||||
|
} else { r = 46; g = 48; b = 54; }
|
||||||
|
} else if (m === PIPE) {
|
||||||
|
pipeCellColor(x, y, anim); r = PC[0]; g = PC[1]; b = PC[2];
|
||||||
|
} else if (m === SHAFT) {
|
||||||
|
const lx = x & 3, ly = y & 3;
|
||||||
|
if (lx === 0 || lx === 3 || ly === 0 || ly === 3) { if (((x + y) & 3) === 0) { r -= 22; g -= 20; b -= 16; } }
|
||||||
|
else { r -= 34; g -= 30; b -= 24; }
|
||||||
|
}
|
||||||
|
else if (m === GATE) { if ((x & 1) === 0 && (y & 3) !== 0) { r -= 40; g -= 24; b -= 20; } }
|
||||||
|
else if (m === CUTST) { const lx = x & 3, ly = y & 3; if (ly === 1 || ly === 3 || lx === (ly < 2 ? 3 : 1)) { r -= 34; g -= 33; b -= 30; } else if (ly === 0 || ly === 2) { r += 10; g += 10; b += 9; } }
|
||||||
|
else if (m === GRINDER || m === SAWB || m === PRESSB || m === RACKB) {
|
||||||
|
const lx = x & 3, ly = y & 3, frame = lx === 0 || lx === 3 || ly === 0 || ly === 3;
|
||||||
|
r = frame ? 96 : 38; g = frame ? 102 : 42; b = frame ? 112 : 50;
|
||||||
|
}
|
||||||
|
else if (m === WOOD) { if ((y & 3) === 2 || ((x + (y >> 2) * 3) & 7) === 0) { r -= 26; g -= 22; b -= 16; } } // planks
|
||||||
|
else if (m === LWOOD) { if (((x * 3 + y) & 3) === 0) { r -= 30; g -= 26; b -= 18; } } // stringy fibres
|
||||||
|
else if (m === HULL) { if (((x * 7 + y * 3) & 15) === 0) { r += 20; g += 20; b += 22; } }
|
||||||
|
else if (m === SPRING) { if ((nz & 7) === 0) { r = 90; g = 150; b = 220; } }
|
||||||
|
const al = ALPHA[m];
|
||||||
|
if (al < 1) { r = br + (r - br) * al; g = bgc + (g - bgc) * al; b = bb + (b - bb) * al; }
|
||||||
|
const dp = digp[i];
|
||||||
|
if (dp) {
|
||||||
|
// shaken rock pales with dust and shivers before it shatters (the cracks are drawn over it)
|
||||||
|
const k = dp / 255, lift = k * 30 + Math.sin(anim * 1.9 + x * 1.3 + y * 0.7) * 6 * k;
|
||||||
|
r += lift; g += lift; b += lift * 1.08;
|
||||||
|
}
|
||||||
|
// built wall blocks read as tiles: light top-left edge, dark bottom-right edge
|
||||||
|
if (!sm && PH[m] === PH_SOLID && !(flags[i] & F_NAT) && isWall(blockP[(y >> 2) * BW + (x >> 2)])) {
|
||||||
|
const lx = x & 3, ly = y & 3;
|
||||||
|
const wc = blockP[(y >> 2) * BW + (x >> 2)];
|
||||||
|
const edge = isSlope(wc) && (wc === BP_SLOPE_BL || wc === BP_SLOPE_TR ? lx === ly : lx + ly === 3);
|
||||||
|
if (edge || lx === 0 || ly === 0) { r += 16; g += 16; b += 16; }
|
||||||
|
else if (lx === 3 || ly === 3) { r -= 18; g -= 18; b -= 18; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// field parts drawn over contents
|
||||||
|
const sb = slopeBeltAt(i, x, y, sm);
|
||||||
|
let p = part[i];
|
||||||
|
if (p === P_FILTER && (sb >= 0 || isSlopeBelt(blockP[slopeSealOwner(i)]))) p = 0;
|
||||||
|
const db = diagonalScrewBlock(x, y, sm);
|
||||||
|
let diagonal = false;
|
||||||
|
if (db >= 0) {
|
||||||
|
const v = blockV[db], joins = diagonalJoins[db];
|
||||||
|
const sd = diagonalScrewShape(x + 0.5, y + 0.5, db, v, joins);
|
||||||
|
p = sm || sd <= 0 ? SCREW_PARTS[v] : 0;
|
||||||
|
diagonal = !!p;
|
||||||
|
if (sm) tpx[row + bx] = (joins << 24) | ((y - ((db / BW) | 0) * BS + 8) << 16) | ((x - (db % BW) * BS + 8) << 8);
|
||||||
|
}
|
||||||
|
if (p && sm && (IS_SCREW[p] || p === P_FILTER)) { if (m === EMPTY || PH[m] === PH_GAS) { r = 0; g = 0; b = 0; } else { r = Math.max(1, r); } }
|
||||||
|
else if (p) {
|
||||||
|
if (IS_SCREW[p]) {
|
||||||
|
const on = netSat(i) > 0;
|
||||||
|
const vert = p === P_SCREW_U || p === P_SCREW_D;
|
||||||
|
const axis = diagonal ? Math.floor(diagonalAlong) : vert ? y : x;
|
||||||
|
const across = diagonal ? diagonalAcross + 2 : vert ? x & 3 : y & 3;
|
||||||
|
const dir = diagonal && blockV[db] >= 4 ? (blockV[db] >= 6 ? 1 : -1) : (p === P_SCREW_U || p === P_SCREW_L) ? 1 : -1;
|
||||||
|
if (diagonal ? Math.abs(diagonalAcross) > 1.38 || diagonalCap > -0.55 : across === 0 || across === 3) { // copper casing on the two long sides; its load shows through
|
||||||
|
if (m === EMPTY) { r = 150; g = 96; b = 64; } else { r = r * 0.72 + 44; g = g * 0.72 + 26; b = b * 0.72 + 14; }
|
||||||
|
if (!diagonal && (axis & 3) === 0) { r -= 24; g -= 18; b -= 12; }
|
||||||
|
} else {
|
||||||
|
// screw flights: a diagonal stripe that walks along the tube when powered
|
||||||
|
const ch = (((axis + across + dir * (on ? anim >> 1 : 0)) & 3) === 0);
|
||||||
|
if (m === EMPTY) { r = ch ? 120 : 46; g = ch ? 112 : 46; b = ch ? 104 : 52; }
|
||||||
|
else { r = r * 0.75 + (ch ? 34 : 8); g = g * 0.75 + (ch ? 30 : 8); b = b * 0.75 + (ch ? 28 : 10); }
|
||||||
|
}
|
||||||
|
} else if (p === P_FILTER) {
|
||||||
|
const lx = x & 3, ly = y & 3, fb = (y >> 2) * BW + (x >> 2), dir = blockV[fb] & 128 ? -1 : 1;
|
||||||
|
const fc = filterCfg.get(fb), fms = fc ? fc.m : null, fn = fms ? fms.length : 0, inv = fc && fc.inv;
|
||||||
|
if (ly === 0) { // rollers on top, turning the way the load rides; in carry mode, striped with what rides
|
||||||
|
const st = ((x + dir * (anim >> 1)) >> 1) & 1;
|
||||||
|
if (m === EMPTY || st) {
|
||||||
|
if (inv && fn && !st) { const c = fms[((x + dir * (anim >> 1)) >> 2 & 0xffff) % fn]; r = COLR[c] * 0.8; g = COLG[c] * 0.8; b = COLB[c] * 0.8; }
|
||||||
|
else { r = st ? 92 : 56; g = st ? 96 : 58; b = st ? 104 : 66; }
|
||||||
|
}
|
||||||
|
} else if (ly === 3) { // the belt's return run carries on under it, so belts and filters read as one belt
|
||||||
|
const st = ((x - dir * (anim >> 1)) >> 1) & 1;
|
||||||
|
if (m === EMPTY || st) { r = (st ? 92 : 56) * 0.72; g = (st ? 96 : 58) * 0.72; b = (st ? 104 : 66) * 0.72; }
|
||||||
|
} else if ((lx === 0 && !beltLike(fb, 0)) || (lx === 3 && !beltLike(fb, 1))) { if (m === EMPTY || ((x + y) & 1)) { r = 64; g = 66; b = 74; } } // frame only where the line ends
|
||||||
|
else if (m === EMPTY) { // mesh holes, tinted with what falls through them
|
||||||
|
if ((x + y) & 1) { r = 40; g = 42; b = 48; }
|
||||||
|
else if (inv) { r = 58; g = 74; b = 72; } // carry mode: everything else drops
|
||||||
|
else if (fn) { const c = fms[(((x >> 1) + (y >> 1)) & 0xffff) % fn]; r = COLR[c] * 0.75; g = COLG[c] * 0.75; b = COLB[c] * 0.75; }
|
||||||
|
else { r = 24; g = 25; b = 28; }
|
||||||
|
}
|
||||||
|
} else if (p === P_GRATE) {
|
||||||
|
if ((x + y) & 1) { r = 150; g = 78; b = 58; } else if (m === EMPTY) { r *= 0.6; g *= 0.6; b *= 0.6; }
|
||||||
|
} else if (p === P_GOPEN) {
|
||||||
|
if ((x & 3) === 0) { r = r * 0.6 + 50; g = g * 0.6 + 30; b = b * 0.6 + 26; }
|
||||||
|
} else if (p === P_INTAKE) {
|
||||||
|
if (m === EMPTY) { r = 30; g = 32; b = 36; }
|
||||||
|
if ((x & 1) === 0) { r += 50; g += 54; b += 60; }
|
||||||
|
} else if (p === P_PIPE) {
|
||||||
|
if (pipeBodyCell(x, y)) { thinPipeColor(x, y, anim); r = PC[0]; g = PC[1]; b = PC[2]; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const slopeArt = sb >= 0 && (m === EMPTY || PH[m] === PH_GAS || m === BELTL || m === BELTR);
|
||||||
|
if (slopeArt && !sm) {
|
||||||
|
const color = slopeBeltColor(sb, x, y, anim, br, bgc, bb);
|
||||||
|
r = color & 255; g = color >> 8 & 255; b = color >> 16 & 255;
|
||||||
|
}
|
||||||
|
const slopeDetail = slopeDetailAt(i, x, y, sm);
|
||||||
|
if (slopeDetail) {
|
||||||
|
const wave = slopeDetail === 2 && ((x + beltGrade(blockP[slopeWallOwner]) * y - slopePhase[slopeWallOwner]) & 3) < 2;
|
||||||
|
// ribs: light rubber with a pale cap; the far wall behind them: muted, corrugated rubber
|
||||||
|
const cap = slopeDetail === 1 && slopeWallOwner >= 0 && !cleatMask[i - W + beltGrade(blockP[slopeWallOwner])];
|
||||||
|
r = slopeDetail === 1 ? (cap ? 178 : 146) : wave ? 64 : 56;
|
||||||
|
g = slopeDetail === 1 ? (cap ? 182 : 150) : wave ? 68 : 60;
|
||||||
|
b = slopeDetail === 1 ? (cap ? 190 : 158) : wave ? 76 : 68;
|
||||||
|
}
|
||||||
|
// incandescence
|
||||||
|
if (T > 450 && m !== EMPTY && incand) {
|
||||||
|
const gi = glowIndex(PH[m] === PH_SOLID ? T + nz * 0.9 : m === MAGMA ? T + Math.sin(x * 0.19 + y * 0.11 + anim * 0.03 + nz * 0.04) * 55 - 30 : T), w = GLOW_W[gi];
|
||||||
|
r = r * (1 - w) + GLOW_R[gi] * w; g = g * (1 - w) + GLOW_G[gi] * w; b = b * (1 - w) + GLOW_B[gi] * w;
|
||||||
|
if (glowOn && fv >= 128 && w > 0.3 && ((bx | by) & 1) === 0) {
|
||||||
|
const gk = (by >> 1) * gw + (bx >> 1), s = (w - 0.3) * 1.4;
|
||||||
|
gpx[gk] = 0xff000000 | (Math.min(255, GLOW_B[gi] * s) << 16) | (Math.min(255, GLOW_G[gi] * s) << 8) | Math.min(255, GLOW_R[gi] * s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (r < 0) r = 0; if (g < 0) g = 0; if (b < 0) b = 0;
|
||||||
|
let top = 0xff000000;
|
||||||
|
if (sm) {
|
||||||
|
// what kind of thing this grain is drawn as: 0 open, 1 terrain, 2 loose grain, 3 liquid,
|
||||||
|
// 4 built piece (drawn crisp), 5 smoke and flame, 6 built wall block (drawn as a tile)
|
||||||
|
let cl = 0;
|
||||||
|
const pt = (p === P_PIPE && !pipeBodyCell(x, y)) || (p === P_FILTER && sb >= 0) ? 0 : p; // the open parts of a pipe block show what is in them
|
||||||
|
if (IS_SCREW[pt]) cl = SCREW_CLASS[pt];
|
||||||
|
else if (p === P_FILTER && sb < 0) {
|
||||||
|
// filter: 13 carries left, 14 right; the tree buffer carries its colour tag (first material) and mode
|
||||||
|
const fb = (y >> 2) * BW + (x >> 2), fc = filterCfg.get(fb), fm = fc && fc.m.length ? fc.m[0] : 0;
|
||||||
|
cl = blockV[fb] & 128 ? 14 : 13;
|
||||||
|
tpx[row + bx] = ((0x20 | (fc && fc.inv ? 0x10 : 0) | beltCaps[fb]) << 24) | (fm ? (COLB[fm] << 16) | (COLG[fm] << 8) | COLR[fm] : 0);
|
||||||
|
}
|
||||||
|
else if (m === BELTL || m === BELTR) {
|
||||||
|
cl = m === BELTL ? 7 : 8;
|
||||||
|
tpx[row + bx] = beltCaps[(y >> 2) * BW + (x >> 2)] << 24;
|
||||||
|
}
|
||||||
|
else if (pt || PARTMAT[m] || mmask[row + bx]) cl = 4;
|
||||||
|
else if (m !== EMPTY) {
|
||||||
|
const ph = PH[m];
|
||||||
|
if (ph === PH_SOLID) cl = (!(flags[i] & F_NAT) && isWall(blockP[(y >> 2) * BW + (x >> 2)])) ? 6 : 1;
|
||||||
|
else if (ph === PH_POWDER) cl = 2;
|
||||||
|
else if (ph === PH_LIQUID) cl = 3;
|
||||||
|
else cl = 5;
|
||||||
|
}
|
||||||
|
const wallCode = blockP[(y >> 2) * BW + (x >> 2)];
|
||||||
|
if (isSlope(wallCode) && ((m === EMPTY && !slopeCell(wallCode, x & 3, y & 3)) || (PH[m] === PH_SOLID && !(flags[i] & F_NAT)))) {
|
||||||
|
cl = wallCode;
|
||||||
|
const wm = WALL_OF[blockV[(y >> 2) * BW + (x >> 2)]];
|
||||||
|
r = COLR[wm]; g = COLG[wm]; b = COLB[wm];
|
||||||
|
}
|
||||||
|
if (sb >= 0 && (slopeArt || PH[m] === PH_POWDER)) {
|
||||||
|
const code = blockP[sb], dir = isFilter(code) ? (blockV[sb] & 128 ? 1 : 0) : blockV[sb];
|
||||||
|
const slopeClass = 23 + (code - BP_BELT_UP) * 2 + dir;
|
||||||
|
if (slopeArt) cl = slopeClass;
|
||||||
|
const f = isFilter(code) && filterCfg.get(sb), fm = f && f.m.length ? f.m[(Math.floor((x + beltGrade(code) * y) / 2) & 65535) % f.m.length] : 0;
|
||||||
|
tpx[row + bx] = ((slopeBeltJoins[sb] | 128) << 24) | slopeClass |
|
||||||
|
((y - ((sb / BW) | 0) * BS + 8) << 16) | ((x - (sb % BW) * BS + 8) << 8);
|
||||||
|
if (slopeArt) { r = fm ? COLR[fm] : 0; g = fm ? COLG[fm] : 0; b = fm ? COLB[fm] : 0; }
|
||||||
|
}
|
||||||
|
if (slopeDetail && slopeWallOwner >= 0) {
|
||||||
|
// ribs and the far wall are both drawn by the wall shader, ribs from the same phase as the simulation
|
||||||
|
const wb = slopeWallOwner;
|
||||||
|
cl = beltGrade(blockP[wb]) < 0 ? 31 : 32;
|
||||||
|
tpx[row + bx] = ((128 | slopePhase[wb] | ((slopeBeltJoins[wb] & 12) << 1) | (blockV[wb] & 1 ? 32 : 0)) << 24) | cl |
|
||||||
|
((y - ((wb / BW) | 0) * BS + 8) << 16) | ((x - wb % BW * BS + 8) << 8);
|
||||||
|
}
|
||||||
|
top = cl << 24;
|
||||||
|
fpx[row + bx] = (tb ? 0xff000000 : 0) | (cl === 5 ? 0xff0000 : 0) | (cl === 3 ? 0xff00 : 0) | (cl === 1 && !blockP[(y >> 2) * BW + (x >> 2)] && !mach[i] ? 0xff : 0); // terrain: any solid nothing owns, natural or not
|
||||||
|
}
|
||||||
|
if (fv < 255) {
|
||||||
|
const k = fv / 255; r *= k; g *= k; b *= k;
|
||||||
|
if (sm) {
|
||||||
|
const c = bpx[row + bx], t = tpx[row + bx];
|
||||||
|
bpx[row + bx] = fogColor(c, k); tpx[row + bx] = fogColor(t, k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
px[row + bx] = top | ((b > 255 ? 255 : b) << 16) | ((g > 255 ? 255 : g) << 8) | (r > 255 ? 255 : r);
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// The plain-cell rules mirror cellPixel; built geometry stays in the CPU override.
|
||||||
|
const GPU_COLOUR = `
|
||||||
|
struct Params { view: vec4f, world: vec4f, style: vec4f, effects: vec4f, camera: vec4f, light: vec4f, screen: vec4f, extra: vec4f }
|
||||||
|
@group(0) @binding(0) var<uniform> u: Params;
|
||||||
|
@group(0) @binding(1) var<storage, read> lut: array<vec4f>;
|
||||||
|
@group(0) @binding(2) var<storage, read> land: array<vec4f>;
|
||||||
|
${['mat','flags','life','part','bgm','digp','noise','fog'].map((n,i)=>`@group(0) @binding(${i+3}) var ${n}T: texture_2d<u32>;`).join('\n')}
|
||||||
|
@group(0) @binding(11) var tempT: texture_2d<f32>;
|
||||||
|
@group(0) @binding(12) var overrideT: texture_2d<f32>;
|
||||||
|
@group(0) @binding(13) var overrideGlow: texture_2d<f32>;
|
||||||
|
@group(0) @binding(14) var outputT: texture_storage_2d<rgba8unorm, write>;
|
||||||
|
@group(0) @binding(15) var glowT: texture_storage_2d<rgba8unorm, write>;
|
||||||
|
fn phase(p: vec2i) -> i32 { return i32(lut[textureLoad(matT,p,0).r*3u+1u].x); }
|
||||||
|
fn powder(p: vec2i) -> bool {
|
||||||
|
return all(p >= vec2i(0)) && all(p < vec2i(u.world.xy)) && phase(p)==${PH_POWDER} && textureLoad(partT,p,0).r==0u;
|
||||||
|
}
|
||||||
|
fn quant(c: vec3f) -> vec3f { return floor(clamp(c,vec3f(0),vec3f(255)))/255.; }
|
||||||
|
@compute @workgroup_size(8,8)
|
||||||
|
fn colour(@builtin(global_invocation_id) id: vec3u) {
|
||||||
|
let q=vec2i(id.xy); if (any(q>=vec2i(u.view.zw))) { return; }
|
||||||
|
let p=q+vec2i(u.view.xy); let x=p.x; let y=p.y;
|
||||||
|
var rgb=vec3f(0); var bloom=vec3f(0);
|
||||||
|
if (all(p>=vec2i(0)) && all(p<vec2i(u.world.xy))) {
|
||||||
|
let m=textureLoad(matT,p,0).r; let nz=f32(textureLoad(noiseT,p,0).r)-128.;
|
||||||
|
let T=textureLoad(tempT,p,0).r; let mc=lut[m*3u]; let props=lut[m*3u+1u];
|
||||||
|
var fv=255.;
|
||||||
|
if (u.style.x>0.) {
|
||||||
|
let f=vec2f(p)/${FOG}.; let a=vec2i(f); let b=min(a+1,vec2i(u.world.zw)-1); let t=fract(f);
|
||||||
|
fv=mix(mix(f32(textureLoad(fogT,a,0).r),f32(textureLoad(fogT,vec2i(b.x,a.y),0).r),t.x),mix(f32(textureLoad(fogT,vec2i(a.x,b.y),0).r),f32(textureLoad(fogT,b,0).r),t.x),t.y);
|
||||||
|
if (fv>0. && fv<255.) { var s=fv/255.; s=clamp(s+nz*(0.6/128.)*s*(1.-s),0.,1.); fv=max(1.,255.*s*s*(3.-2.*s)); }
|
||||||
|
}
|
||||||
|
if (fv>0.) {
|
||||||
|
var shade=1.;
|
||||||
|
let built=props.x==${PH_SOLID}. && (textureLoad(flagsT,p,0).r & ${F_NAT}u)==0u;
|
||||||
|
if (m!=${EMPTY}u && !built) {
|
||||||
|
if (props.x==${PH_POWDER}.) { shade=1.1+nz*0.00065; }
|
||||||
|
else if (props.x==${PH_SOLID}. && (powder(p+vec2i(-1,0)) || powder(p+vec2i(1,0)) || powder(p+vec2i(0,-1)) || powder(p+vec2i(0,1)))) { shade=0.72; }
|
||||||
|
}
|
||||||
|
if (u.effects.x>0.) {
|
||||||
|
let ti=u32(clamp(i32(T/4.),0,550)); let thermal=lut[${NMAT*3+700}u+u32(u.effects.y)*551u+ti].rgb;
|
||||||
|
rgb=select(thermal*0.78+vec3f((mc.r+mc.g+mc.b)/3.*0.25),thermal*0.55,m==${EMPTY}u)*shade;
|
||||||
|
} else {
|
||||||
|
let tb=textureLoad(bgmT,p,0).r; let dp=f32(textureLoad(digpT,p,0).r);
|
||||||
|
let column=land[x]; let sky=land[i32(u.world.x)+y].rgb;
|
||||||
|
var back: vec3f;
|
||||||
|
if (tb!=0u) { back=(lut[tb*3u+2u].rgb+vec3f(nz*lut[tb*3u].a/128.))*(1.+dp/700.); }
|
||||||
|
else if (f32(y)<column.x) {
|
||||||
|
back=sky;
|
||||||
|
if (u.style.y>0.01) { let gy=(sky.r+sky.g+sky.b)/3.*0.82; back+= (vec3f(gy)-sky)*u.style.y*vec3f(0.7,0.7,0.65); }
|
||||||
|
} else {
|
||||||
|
back=vec3f(${BG.basalt.join(',')});
|
||||||
|
if (f32(y)<column.y) { back=vec3f(${BG.dirt.join(',')}); }
|
||||||
|
else if (f32(y)<column.z) { back=vec3f(${BG.stone.join(',')}); }
|
||||||
|
else if (f32(y)<column.w) { back=vec3f(${BG.granite.join(',')}); }
|
||||||
|
back+=vec3f(nz*0.06);
|
||||||
|
}
|
||||||
|
rgb=back;
|
||||||
|
if (m!=${EMPTY}u) {
|
||||||
|
rgb=(mc.rgb+vec3f(nz*mc.a*u.style.z/128.))*shade;
|
||||||
|
if (m==${FIRE}u) { let f=f32(textureLoad(lifeT,p,0).r & 15u)*6.; rgb=vec3f(255.,120.+f,30.+f*0.5); }
|
||||||
|
else if (m==${WATER}u && ((x*3+y*5+(i32(u.style.w)>>2)) & 15)==0) { rgb+=vec3f(14.); }
|
||||||
|
rgb=back+(rgb-back)*props.y;
|
||||||
|
if (dp>0.) { let lift=dp/255.*(30.+sin(u.style.w*1.9+f32(x)*1.3+f32(y)*0.7)*6.); rgb+=lift*vec3f(1,1,1.08); }
|
||||||
|
}
|
||||||
|
if (T>450. && m!=${EMPTY}u && u.effects.z>0.) {
|
||||||
|
var gt=T;
|
||||||
|
if (props.x==${PH_SOLID}.) { gt+=nz*0.9; }
|
||||||
|
else if (m==${MAGMA}u) { gt+=sin(f32(x)*0.19+f32(y)*0.11+u.style.w*0.03+nz*0.04)*55.-30.; }
|
||||||
|
let gi=u32(clamp(i32((gt-400.)/4.),0,699)); let glow=lut[${NMAT*3}u+gi];
|
||||||
|
rgb=rgb*(1.-glow.a)+glow.rgb*glow.a;
|
||||||
|
if (u.effects.w>0. && fv>=128. && glow.a>0.3) { bloom=quant(glow.rgb*(glow.a-0.3)*1.4); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rgb=quant(rgb*fv/255.);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Flying grains replace colour, while the glow behind them remains.
|
||||||
|
let over=textureLoad(overrideT,q,0);
|
||||||
|
if (over.a>0.) { rgb=over.rgb; bloom=max(bloom,textureLoad(overrideGlow,q/2,0).rgb); }
|
||||||
|
textureStore(outputT,q,vec4f(rgb,1));
|
||||||
|
if (((q.x | q.y) & 1)==0) { textureStore(glowT,q/2,vec4f(bloom,1)); }
|
||||||
|
}`;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// Async startup shares the title screen's adapter; failures leave a usable legacy renderer.
|
||||||
|
const GPU = { ok: false, canvas: document.getElementById('gpu'), error: '', gpuMs: null };
|
||||||
|
async function gpuInit(adapter) {
|
||||||
|
try {
|
||||||
|
const features = adapter.features.has('timestamp-query') ? ['timestamp-query'] : [];
|
||||||
|
const d = GPU.device = await adapter.requestDevice({ requiredFeatures: features });
|
||||||
|
d.addEventListener('uncapturederror', e => gpuFail(e.error.message));
|
||||||
|
d.lost.then(info => gpuFail(info.message || 'Graphics device lost'));
|
||||||
|
GPU.context = GPU.canvas.getContext('webgpu');
|
||||||
|
GPU.format = navigator.gpu.getPreferredCanvasFormat();
|
||||||
|
GPU.context.configure({ device: d, format: GPU.format, alphaMode: 'opaque' });
|
||||||
|
d.pushErrorScope('validation');
|
||||||
|
GPU.colour = await d.createComputePipelineAsync({ layout: 'auto', compute: { module: d.createShaderModule({ code: GPU_COLOUR }), entryPoint: 'colour' } });
|
||||||
|
GPU.blur = await d.createComputePipelineAsync({ layout: 'auto', compute: { module: d.createShaderModule({ code: GPU_BLUR }), entryPoint: 'main' } });
|
||||||
|
GPU.draw = await d.createRenderPipelineAsync({ layout: 'auto', vertex: { module: d.createShaderModule({ code: GPU_DRAW }), entryPoint: 'vs' }, fragment: { module: d.createShaderModule({ code: GPU_DRAW }), entryPoint: 'fs', targets: [{ format: GPU.format }] }, primitive: { topology: 'triangle-list' } });
|
||||||
|
const error = await d.popErrorScope(); if (error) throw error;
|
||||||
|
GPU.uniform = d.createBuffer({ size: 128, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
||||||
|
GPU.params = new Float32Array(32);
|
||||||
|
GPU.sampler = d.createSampler({ magFilter: 'linear', minFilter: 'linear' });
|
||||||
|
if (features.length) {
|
||||||
|
GPU.queries = d.createQuerySet({ type: 'timestamp', count: 2 });
|
||||||
|
GPU.resolve = d.createBuffer({ size: 256, usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC });
|
||||||
|
GPU.readback = d.createBuffer({ size: 16, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
||||||
|
}
|
||||||
|
GPU.ok = true;
|
||||||
|
} catch (e) { gpuFail(e.message); }
|
||||||
|
}
|
||||||
|
function gpuFail(message) {
|
||||||
|
GPU.ok = false; GPU.error = message; SET.renderer = 'legacy';
|
||||||
|
GPU.canvas.hidden = true;
|
||||||
|
console.warn('WebGPU renderer: ' + message);
|
||||||
|
if (GAME.running) toast('WebGPU stopped drawing. Legacy renderer is on.');
|
||||||
|
}
|
||||||
|
// Tests sample this composite so a transparent overlay cannot pass world-colour checks.
|
||||||
|
function worldPixelContext() {
|
||||||
|
if (!R.gpuOn) return R.ctx;
|
||||||
|
// Canvas textures expire after presentation; redraw the retained result before copying.
|
||||||
|
const encoder = GPU.device.createCommandEncoder(); gpuPresent(encoder);
|
||||||
|
GPU.device.queue.submit([encoder.finish()]);
|
||||||
|
const c = document.createElement('canvas'); c.width = R.canvas.width; c.height = R.canvas.height;
|
||||||
|
const ctx = c.getContext('2d'); ctx.drawImage(GPU.canvas, 0, 0); ctx.drawImage(R.canvas, 0, 0);
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// Only exceptional blocks and their geometric neighbours run the CPU colour rules.
|
||||||
|
const GPU_PATTERN = new Uint8Array(NMAT);
|
||||||
|
for (const m of [BELTL, BELTR, PIPE, SHAFT, GATE, CUTST, GRINDER, SAWB, PRESSB, RACKB, WOOD, LWOOD, HULL, SPRING]) GPU_PATTERN[m] = 1;
|
||||||
|
function gpuOverrides() {
|
||||||
|
R.px.fill(0);
|
||||||
|
if (!GPU.blocks || GPU.blocks.length !== NB) GPU.blocks = new Uint8Array(NB);
|
||||||
|
const marked = GPU.blocks; marked.fill(0);
|
||||||
|
const bx0 = Math.max(0, (R.vx0 >> 2) - 3), by0 = Math.max(0, (R.vy0 >> 2) - 3);
|
||||||
|
const bx1 = Math.min(BW - 1, ((R.vx0 + R.bw) >> 2) + 3), by1 = Math.min(BH - 1, ((R.vy0 + R.bh) >> 2) + 3);
|
||||||
|
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
|
||||||
|
const b = by * BW + bx, code = blockP[b];
|
||||||
|
let rare = !!code;
|
||||||
|
if (!rare) for (let dy = 0; dy < BS && !rare; dy++) for (let dx = 0; dx < BS; dx++) {
|
||||||
|
const i = (by * BS + dy) * W + bx * BS + dx;
|
||||||
|
if (part[i] || mach[i] || occ[i] || GPU_PATTERN[mat[i]]) { rare = true; break; }
|
||||||
|
}
|
||||||
|
if (!rare) continue;
|
||||||
|
marked[b] = 1;
|
||||||
|
if (code === BP_SCREW || isSlopeBelt(code)) {
|
||||||
|
// Covers joins, the far wall and cleats, including cells outside the owning block.
|
||||||
|
for (let yy = Math.max(0, by - 3); yy <= Math.min(BH - 1, by + 2); yy++)
|
||||||
|
for (let xx = Math.max(0, bx - 2); xx <= Math.min(BW - 1, bx + 2); xx++) marked[yy * BW + xx] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GPU.overrideCount = 0;
|
||||||
|
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
|
||||||
|
if (!marked[by * BW + bx]) continue;
|
||||||
|
const x0 = Math.max(bx * BS, R.vx0), y0 = Math.max(by * BS, R.vy0);
|
||||||
|
const x1 = Math.min(bx * BS + BS, R.vx0 + R.bw, W), y1 = Math.min(by * BS + BS, R.vy0 + R.bh, H);
|
||||||
|
for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) cellPixel(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function gpuDraw(lighting, glowOn) {
|
||||||
|
const upload = performance.now();
|
||||||
|
gpuUpload(lighting, glowOn);
|
||||||
|
if (GPU.canvas.width !== R.canvas.width || GPU.canvas.height !== R.canvas.height) {
|
||||||
|
GPU.canvas.width = R.canvas.width; GPU.canvas.height = R.canvas.height;
|
||||||
|
}
|
||||||
|
const blit = performance.now(), d = GPU.device, encoder = d.createCommandEncoder();
|
||||||
|
const timed = GPU.queries && !GPU.timingBusy && (PERF_MARKS || SET.showPerf);
|
||||||
|
const compute = encoder.beginComputePass(timed ? { timestampWrites: { querySet: GPU.queries, beginningOfPassWriteIndex: 0 } } : {});
|
||||||
|
compute.setPipeline(GPU.colour); compute.setBindGroup(0, GPU.bindColour);
|
||||||
|
compute.dispatchWorkgroups(Math.ceil(R.bw / 8), Math.ceil(R.bh / 8)); compute.end();
|
||||||
|
if (glowOn) for (let i = 0; i < 2; i++) {
|
||||||
|
const pass = encoder.beginComputePass(); pass.setPipeline(GPU.blur); pass.setBindGroup(0, GPU.bindBlur[i]);
|
||||||
|
pass.dispatchWorkgroups(Math.ceil(R.glowC.width / 8), Math.ceil(R.glowC.height / 8)); pass.end();
|
||||||
|
}
|
||||||
|
gpuPresent(encoder, timed);
|
||||||
|
if (timed) { encoder.resolveQuerySet(GPU.queries, 0, 2, GPU.resolve, 0); encoder.copyBufferToBuffer(GPU.resolve, 0, GPU.readback, 0, 16); }
|
||||||
|
d.queue.submit([encoder.finish()]);
|
||||||
|
if (timed) {
|
||||||
|
GPU.timingBusy = true;
|
||||||
|
GPU.timingPromise = GPU.readback.mapAsync(GPUMapMode.READ).then(() => {
|
||||||
|
const ns = new BigUint64Array(GPU.readback.getMappedRange()); GPU.gpuMs = Number(ns[1] - ns[0]) / 1e6;
|
||||||
|
GPU.readback.unmap(); GPU.timingBusy = false;
|
||||||
|
}).catch(() => { GPU.timingBusy = false; });
|
||||||
|
}
|
||||||
|
if (PERF_MARKS || SET.showPerf) {
|
||||||
|
perfMarks.override = perfMarks.color; perfMarks.upload = blit - upload; perfMarks.blit = performance.now() - blit;
|
||||||
|
if (GPU.gpuMs !== null) perfMarks.gpu = GPU.gpuMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also used for readback after the browser has expired the presented canvas texture.
|
||||||
|
function gpuPresent(encoder, timed = false) {
|
||||||
|
const pass = encoder.beginRenderPass({ colorAttachments: [{ view: GPU.context.getCurrentTexture().createView(), loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 1] }],
|
||||||
|
...(timed ? { timestampWrites: { querySet: GPU.queries, endOfPassWriteIndex: 1 } } : {}) });
|
||||||
|
pass.setPipeline(GPU.draw); pass.setBindGroup(0, GPU.bindDraw); pass.draw(3); pass.end();
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// Half-resolution separable Gaussian bloom, then nearest colour + linear light.
|
||||||
|
const GPU_BLUR = `
|
||||||
|
@group(0) @binding(0) var source: texture_2d<f32>;
|
||||||
|
@group(0) @binding(1) var dest: texture_storage_2d<rgba8unorm, write>;
|
||||||
|
@group(0) @binding(2) var<uniform> blur: vec4f;
|
||||||
|
@compute @workgroup_size(8,8)
|
||||||
|
fn main(@builtin(global_invocation_id) id: vec3u) {
|
||||||
|
let p=vec2i(id.xy); let size=vec2i(textureDimensions(source)); if (any(p>=size)) { return; }
|
||||||
|
var sum=vec3f(0); var weight=0.; let sigma=blur.z;
|
||||||
|
let radius=i32(ceil(sigma*3.));
|
||||||
|
for (var j=-radius;j<=radius;j++) {
|
||||||
|
let w=exp(-f32(j*j)/(2.*sigma*sigma)); let q=p+vec2i(blur.xy)*j;
|
||||||
|
if (all(q>=vec2i(0)) && all(q<size)) { sum+=textureLoad(source,q,0).rgb*w; }
|
||||||
|
weight+=w;
|
||||||
|
}
|
||||||
|
textureStore(dest,p,vec4f(sum/weight,1));
|
||||||
|
}`;
|
||||||
|
const GPU_DRAW = `
|
||||||
|
struct Params { view: vec4f, world: vec4f, style: vec4f, effects: vec4f, camera: vec4f, light: vec4f, screen: vec4f, extra: vec4f }
|
||||||
|
@group(0) @binding(0) var<uniform> u: Params;
|
||||||
|
@group(0) @binding(1) var colour: texture_2d<f32>;
|
||||||
|
@group(0) @binding(2) var light: texture_2d<f32>;
|
||||||
|
@group(0) @binding(3) var glow: texture_2d<f32>;
|
||||||
|
@group(0) @binding(4) var linear: sampler;
|
||||||
|
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
|
||||||
|
let pos=array<vec2f,3>(vec2f(-1,-1),vec2f(3,-1),vec2f(-1,3)); return vec4f(pos[i],0,1);
|
||||||
|
}
|
||||||
|
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
||||||
|
let cell=(p.xy-u.camera.xy)/u.camera.z;
|
||||||
|
var c=textureLoad(colour,vec2i(floor(cell)),0).rgb;
|
||||||
|
if (u.light.w>0.) {
|
||||||
|
let world=p.xy/u.camera.z+u.extra.xy;
|
||||||
|
let uv=(world/u.light.z-u.light.xy)/vec2f(textureDimensions(light));
|
||||||
|
let l=textureSampleLevel(light,linear,uv,0).rgb;
|
||||||
|
c=round(c*l*255.)/255.;
|
||||||
|
}
|
||||||
|
if (u.effects.w>0.) {
|
||||||
|
let uv=cell/(2.*vec2f(textureDimensions(glow)));
|
||||||
|
c+=textureSampleLevel(glow,linear,uv,0).rgb*u.screen.z;
|
||||||
|
}
|
||||||
|
return vec4f(c,1);
|
||||||
|
}`;
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// Whole-world allocation, direct strided uploads; no cell packing or dirty tracking.
|
||||||
|
function gpuTexture(w, h, format = 'rgba8unorm', storage = false) {
|
||||||
|
return GPU.device.createTexture({ size: [w, h], format, usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | (storage ? GPUTextureUsage.STORAGE_BINDING : 0) });
|
||||||
|
}
|
||||||
|
function gpuWrite(texture, data, stride, x, y, w, h) {
|
||||||
|
if (w <= 0 || h <= 0) return;
|
||||||
|
const bytes = data.BYTES_PER_ELEMENT;
|
||||||
|
GPU.device.queue.writeTexture({ texture, origin: [x, y] }, data, { offset: (y * stride + x) * bytes, bytesPerRow: stride * bytes }, [w, h]);
|
||||||
|
}
|
||||||
|
function gpuBuffer(data) {
|
||||||
|
const b = GPU.device.createBuffer({ size: data.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
||||||
|
GPU.device.queue.writeBuffer(b, 0, data); return b;
|
||||||
|
}
|
||||||
|
function gpuWorldTextures() {
|
||||||
|
if (GPU.world === mat && GPU.sky === SKY_R) return;
|
||||||
|
if (GPU.worldTextures) for (const t of GPU.worldTextures) t.destroy();
|
||||||
|
if (GPU.lut) GPU.lut.destroy(); if (GPU.land) GPU.land.destroy();
|
||||||
|
GPU.world = mat; GPU.sky = SKY_R;
|
||||||
|
GPU.worldTextures = Array.from({ length: 8 }, (_, i) => gpuTexture(i === 7 ? FW : W, i === 7 ? FH : H, 'r8uint'));
|
||||||
|
GPU.temp = gpuTexture(W, H, 'r32float'); GPU.worldTextures.push(GPU.temp);
|
||||||
|
gpuWrite(GPU.worldTextures[6], noiseB, W, 0, 0, W, H);
|
||||||
|
const tables = new Float32Array((NMAT * 3 + 700 + 551 * 3) * 4);
|
||||||
|
for (let m = 0; m < NMAT; m++) {
|
||||||
|
tables.set([COLR[m], COLG[m], COLB[m], CVAR[m], PH[m], ALPHA[m], 0, 0, TREE_R[m], TREE_G[m], TREE_B[m], 0], m * 12);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 700; i++) tables.set([GLOW_R[i], GLOW_G[i], GLOW_B[i], GLOW_W[i]], (NMAT * 3 + i) * 4);
|
||||||
|
for (const [j, palette] of Object.values(THERM).entries()) for (let i = 0; i < 551; i++) tables.set([palette[i * 3], palette[i * 3 + 1], palette[i * 3 + 2], 0], (NMAT * 3 + 700 + j * 551 + i) * 4);
|
||||||
|
GPU.lut = gpuBuffer(tables);
|
||||||
|
GPU.landData = new Float32Array((W + H) * 4);
|
||||||
|
GPU.land = gpuBuffer(GPU.landData);
|
||||||
|
GPU.bindColour = null;
|
||||||
|
}
|
||||||
|
function gpuViewTextures() {
|
||||||
|
const key = `${R.bw}/${R.bh}/${R.lw}/${R.lh}`;
|
||||||
|
if (GPU.viewKey === key) return;
|
||||||
|
GPU.viewKey = key;
|
||||||
|
if (GPU.viewTextures) for (const t of GPU.viewTextures) t.destroy();
|
||||||
|
const bw = R.bw, bh = R.bh, gw = R.glowC.width, gh = R.glowC.height;
|
||||||
|
GPU.override = gpuTexture(bw, bh); GPU.overrideGlow = gpuTexture(gw, gh);
|
||||||
|
GPU.colourTex = gpuTexture(bw, bh, 'rgba8unorm', true);
|
||||||
|
GPU.glowTex = gpuTexture(gw, gh, 'rgba8unorm', true);
|
||||||
|
GPU.blurX = gpuTexture(gw, gh, 'rgba8unorm', true); GPU.blurY = gpuTexture(gw, gh, 'rgba8unorm', true);
|
||||||
|
GPU.lightTex = gpuTexture(Math.max(1, R.lw), Math.max(1, R.lh));
|
||||||
|
GPU.viewTextures = [GPU.override, GPU.overrideGlow, GPU.colourTex, GPU.glowTex, GPU.blurX, GPU.blurY, GPU.lightTex];
|
||||||
|
GPU.bindColour = null;
|
||||||
|
}
|
||||||
|
function gpuBindings() {
|
||||||
|
if (GPU.bindColour) return;
|
||||||
|
const d = GPU.device, buffer = b => ({ buffer: b }), view = t => t.createView();
|
||||||
|
const group = (pipeline, resources) => d.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: resources.map((resource, binding) => ({ binding, resource })) });
|
||||||
|
GPU.bindColour = group(GPU.colour, [buffer(GPU.uniform), buffer(GPU.lut), buffer(GPU.land), ...GPU.worldTextures.map(view), view(GPU.override), view(GPU.overrideGlow), view(GPU.colourTex), view(GPU.glowTex)]);
|
||||||
|
GPU.bindDraw = group(GPU.draw, [buffer(GPU.uniform), view(GPU.colourTex), view(GPU.lightTex), view(GPU.blurY), GPU.sampler]);
|
||||||
|
if (GPU.blurUniforms) for (const b of GPU.blurUniforms) b.destroy();
|
||||||
|
GPU.blurUniforms = [0, 1].map(() => d.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }));
|
||||||
|
GPU.bindBlur = [group(GPU.blur, [view(GPU.glowTex), view(GPU.blurX), buffer(GPU.blurUniforms[0])]), group(GPU.blur, [view(GPU.blurX), view(GPU.blurY), buffer(GPU.blurUniforms[1])])];
|
||||||
|
}
|
||||||
|
function gpuUpload(lighting, glowOn) {
|
||||||
|
gpuWorldTextures(); gpuViewTextures(); gpuBindings();
|
||||||
|
const x = Math.max(0, R.vx0 - 2), y = Math.max(0, R.vy0 - 2);
|
||||||
|
const w = Math.min(W, R.vx0 + R.bw + 2) - x, h = Math.min(H, R.vy0 + R.bh + 2) - y;
|
||||||
|
const arrays = [mat, flags, life, part, bgm, digp];
|
||||||
|
arrays.forEach((a, i) => gpuWrite(GPU.worldTextures[i], a, W, x, y, w, h));
|
||||||
|
gpuWrite(GPU.temp, temp, W, x, y, w, h);
|
||||||
|
const fx = Math.max(0, Math.floor(x / FOG)), fy = Math.max(0, Math.floor(y / FOG));
|
||||||
|
gpuWrite(GPU.worldTextures[7], fog, FW, fx, fy, Math.min(FW, Math.ceil((x + w) / FOG) + 1) - fx, Math.min(FH, Math.ceil((y + h) / FOG) + 1) - fy);
|
||||||
|
// Small per-row/column tables may change in scene tools independently of world allocation.
|
||||||
|
const land = GPU.landData;
|
||||||
|
for (let i = 0; i < W; i++) { const k = i * 4; land[k] = surfY[i]; land[k + 1] = dirtY[i]; land[k + 2] = graniteY[i]; land[k + 3] = basaltY[i]; }
|
||||||
|
for (let i = 0; i < H; i++) { const k = (W + i) * 4; land[k] = SKY_R[i]; land[k + 1] = SKY_G[i]; land[k + 2] = SKY_B[i]; land[k + 3] = AMB[i]; }
|
||||||
|
const q = GPU.device.queue;
|
||||||
|
q.writeBuffer(GPU.land, 0, land);
|
||||||
|
gpuWrite(GPU.override, R.px, R.bw, 0, 0, R.bw, R.bh);
|
||||||
|
gpuWrite(GPU.overrideGlow, R.glowPx, R.glowC.width, 0, 0, R.glowC.width, R.glowC.height);
|
||||||
|
if (lighting) gpuWrite(GPU.lightTex, R.lightPx, R.lw, 0, 0, R.lw, R.lh);
|
||||||
|
const zp = R.zp, ox = Math.round(-(R.camX - R.vx0) * zp), oy = Math.round(-(R.camY - R.vy0) * zp);
|
||||||
|
GPU.params.set([R.vx0, R.vy0, R.bw, R.bh, W, H, FW, FH,
|
||||||
|
+SET.fog, R.rain, SET.grain, R.frame, +R.lens, Math.max(0, Object.keys(THERM).indexOf(SET.thermalPalette)), +SET.incandescence, +glowOn,
|
||||||
|
ox, oy, zp, 0, R.lx0, R.ly0, R.lres, +lighting,
|
||||||
|
R.canvas.width, R.canvas.height, SET.glow === 'high' ? 0.9 : 0.5, 0, R.camX, R.camY, 0, 0]);
|
||||||
|
q.writeBuffer(GPU.uniform, 0, GPU.params);
|
||||||
|
const sigma = Math.max(2, zp * (SET.glow === 'high' ? 3.2 : 2.2)) / (2 * zp);
|
||||||
|
q.writeBuffer(GPU.blurUniforms[0], 0, new Float32Array([1, 0, sigma, 0]));
|
||||||
|
q.writeBuffer(GPU.blurUniforms[1], 0, new Float32Array([0, 1, sigma, 0]));
|
||||||
|
}
|
||||||
+1
-1
@@ -99,5 +99,5 @@ function computeLight() {
|
|||||||
r = r >= 1 ? 255 : (r * 255) | 0; g = g >= 1 ? 255 : (g * 255) | 0; b = b >= 1 ? 255 : (b * 255) | 0;
|
r = r >= 1 ? 255 : (r * 255) | 0; g = g >= 1 ? 255 : (g * 255) | 0; b = b >= 1 ? 255 : (b * 255) | 0;
|
||||||
px[k] = 0xff000000 | (b << 16) | (g << 8) | r;
|
px[k] = 0xff000000 | (b << 16) | (g << 8) | r;
|
||||||
}
|
}
|
||||||
if (!R.smoothOn) R.lightX.putImageData(R.lightImg, 0, 0);
|
if (!R.smoothOn && !R.gpuOn) R.lightX.putImageData(R.lightImg, 0, 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// Loose bars, flying grains and queued blueprint blocks in the grain buffer.
|
||||||
|
function worldObjects() {
|
||||||
|
const { bw, bh, px, vx0, vy0, smoothOn: sm, lens } = R;
|
||||||
|
const useFog = SET.fog, incand = SET.incandescence;
|
||||||
|
// bars and other loose objects
|
||||||
|
if (!lens) for (const e of ents) {
|
||||||
|
if (useFog && fogSeen(e.ix, e.iy) < 64) continue;
|
||||||
|
const ex = e.ix - vx0, ey = e.iy - vy0;
|
||||||
|
if (ex + BAR_W < 0 || ey + BAR_H < 0 || ex >= bw || ey >= bh) continue;
|
||||||
|
const m = e.m;
|
||||||
|
for (let dy = 0; dy < BAR_H; dy++) for (let dx = 0; dx < BAR_W; dx++) {
|
||||||
|
const sx = ex + dx, sy = ey + dy;
|
||||||
|
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
|
||||||
|
let r = COLR[m], g = COLG[m], b = COLB[m];
|
||||||
|
const end = dx === 0 || dx === BAR_W - 1;
|
||||||
|
if (dy === 0) { r += end ? 10 : 34; g += end ? 8 : 28; b += end ? 6 : 24; } else { r -= end ? 34 : 18; g -= end ? 30 : 16; b -= end ? 26 : 14; }
|
||||||
|
if (e.T > 450 && incand) { const gi = glowIndex(e.T), w = GLOW_W[gi]; r = r * (1 - w) + GLOW_R[gi] * w; g = g * (1 - w) + GLOW_G[gi] * w; b = b * (1 - w) + GLOW_B[gi] * w; }
|
||||||
|
if (r < 0) r = 0; if (g < 0) g = 0; if (b < 0) b = 0;
|
||||||
|
if (R.gpuOn && !px[sy * bw + sx]) GPU.overrideCount++;
|
||||||
|
px[sy * bw + sx] = (sm ? 0x04000000 : 0xff000000) | ((b > 255 ? 255 : b | 0) << 16) | ((g > 255 ? 255 : g | 0) << 8) | (r > 255 ? 255 : r | 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// grains in flight from the vacuum nozzle, a screw or a belt (rain and snow are drawn over the top, see drawRain)
|
||||||
|
if (!lens) for (const f of flyers) {
|
||||||
|
if (f.rain || (useFog && fogSeen(f.x, f.y) < 64)) continue;
|
||||||
|
const sx = Math.floor(f.x) - vx0, sy = Math.floor(f.y) - vy0;
|
||||||
|
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
|
||||||
|
const m = f.m, v = ((sx * 7 + sy * 13) & 15) - 8;
|
||||||
|
if (R.gpuOn && !px[sy * bw + sx]) GPU.overrideCount++;
|
||||||
|
px[sy * bw + sx] = (sm ? 0x02000000 : 0xff000000) | (Math.max(0, Math.min(255, COLB[m] + v)) << 16) | (Math.max(0, Math.min(255, COLG[m] + v)) << 8) | Math.max(0, Math.min(255, COLR[m] + v));
|
||||||
|
}
|
||||||
|
// blueprint ghosts: blocks waiting to be built
|
||||||
|
if (ghostBlocks.size) {
|
||||||
|
const bx0 = vx0 >> 2, by0 = vy0 >> 2, bx1 = (vx0 + bw) >> 2, by1 = (vy0 + bh) >> 2;
|
||||||
|
const pulse = (R.frame >> 3) & 3;
|
||||||
|
for (const [b, gv] of ghostBlocks) {
|
||||||
|
const gbx = b % BW, gby = (b / BW) | 0;
|
||||||
|
if (gbx < bx0 || gbx > bx1 || gby < by0 || gby > by1) continue;
|
||||||
|
const blocked = ghostBlocked.has(b), code = gv[0], v = gv[1];
|
||||||
|
let tr = 90, tg = 190, tb = 230;
|
||||||
|
if (blocked) { tr = 230; tg = 70; tb = 60; }
|
||||||
|
else if (isWall(code) && WALL_OF[v]) { const wm = WALL_OF[v]; tr = (COLR[wm] + 90) / 2; tg = (COLG[wm] + 190) / 2; tb = (COLB[wm] + 230) / 2; }
|
||||||
|
const slope = isSlopeBelt(code), diagonal = code === BP_SCREW && (v >= 4 || straightScrewJoins(gbx, gby, v, ghostBlocks));
|
||||||
|
const joins = slope ? slopeBeltConnections(gbx, gby, code, ghostBlocks) : diagonal ? (v >= 4 ? diagonalScrewJoins(gbx, gby, v, ghostBlocks) : straightScrewJoins(gbx, gby, v, ghostBlocks)) : 0;
|
||||||
|
const caps = code === BP_BELT || code === BP_FILTER ? flatBeltCaps(b, ghostBlocks) : 3;
|
||||||
|
for (let dy = slope ? -9 : diagonal ? -4 : 0; dy < (slope || diagonal ? 8 : BS); dy++) for (let dx = slope ? -5 : diagonal ? -4 : 0; dx < (slope ? 9 : diagonal ? 8 : BS); dx++) {
|
||||||
|
let sd = 0;
|
||||||
|
if (diagonal) {
|
||||||
|
if (v < 4 && ((v & 1) ? dx < 0 || dx >= BS : dy < 0 || dy >= BS)) continue;
|
||||||
|
const x = gbx * BS + dx, y = gby * BS + dy;
|
||||||
|
if (x < 0 || y < 0 || x >= W || y >= H) continue;
|
||||||
|
const i = y * W + x, n = (y >> 2) * BW + (x >> 2);
|
||||||
|
if (n !== b && (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || blockP[n])) continue;
|
||||||
|
sd = diagonalScrewShape(x + 0.5, y + 0.5, b, v, joins);
|
||||||
|
if (sd > 0) continue;
|
||||||
|
}
|
||||||
|
if (slope) {
|
||||||
|
const x = gbx * BS + dx, y = gby * BS + dy;
|
||||||
|
if (x < 0 || y < 0 || x >= W || y >= H) continue;
|
||||||
|
const i = y * W + x, n = (y >> 2) * BW + (x >> 2);
|
||||||
|
if (n !== b && (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || blockP[n])) continue;
|
||||||
|
sd = slopeBeltShape(x + 0.5, y + 0.5, b, joins, code);
|
||||||
|
const up = !isFilter(code) && slopeUphill(code, v), rib = up && slopePreviewCleat(x, y, b, code);
|
||||||
|
if (sd > 0 && !rib && (!up || slopeWallShape(x + 0.5, y + 0.5, b, code) > 0)) continue;
|
||||||
|
if (rib) sd = -0.1;
|
||||||
|
}
|
||||||
|
if (isSlope(code) && !slopeCell(code, dx, dy)) continue;
|
||||||
|
const sx = gbx * BS + dx - vx0, sy = gby * BS + dy - vy0;
|
||||||
|
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
|
||||||
|
const k = sy * bw + sx;
|
||||||
|
if (R.gpuOn && !px[k]) cellPixel(sx + vx0, sy + vy0);
|
||||||
|
const c = px[k];
|
||||||
|
const edge = slope || diagonal ? sd > -0.8 : (dx === 0 && (caps & 1)) || dy === 0 || (dx === BS - 1 && (caps & 2)) || dy === BS - 1;
|
||||||
|
const a = edge ? (((dx + dy + pulse) & 3) === 0 ? 0.85 : 0.6) : 0.28;
|
||||||
|
const pr = c & 255, pg = (c >> 8) & 255, pb = (c >> 16) & 255;
|
||||||
|
px[k] = (sm ? 0x04000000 : 0xff000000) | ((pb + (tb - pb) * a) << 16) | ((pg + (tg - pg) * a) << 8) | (pr + (tr - pr) * a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -138,7 +138,7 @@ function buildSky() {
|
|||||||
|
|
||||||
function initRender(canvas) {
|
function initRender(canvas) {
|
||||||
R.canvas = canvas;
|
R.canvas = canvas;
|
||||||
R.ctx = canvas.getContext('2d', { alpha: false });
|
R.ctx = canvas.getContext('2d');
|
||||||
R.buf = document.createElement('canvas');
|
R.buf = document.createElement('canvas');
|
||||||
R.bctx = R.buf.getContext('2d');
|
R.bctx = R.buf.getContext('2d');
|
||||||
R.glowC = document.createElement('canvas');
|
R.glowC = document.createElement('canvas');
|
||||||
|
|||||||
+15
-351
@@ -3,18 +3,20 @@ function fogColor(c, k) { return (c & 0xff000000) | (((c >>> 16 & 255) * k) << 1
|
|||||||
// drawWorld: the per-grain pixel buffer (pixel look), also the input of the smooth look.
|
// drawWorld: the per-grain pixel buffer (pixel look), also the input of the smooth look.
|
||||||
// ---------- World pixels ----------
|
// ---------- World pixels ----------
|
||||||
function drawWorld() {
|
function drawWorld() {
|
||||||
const perfStart = PERF_MARKS ? performance.now() : 0;
|
const perfStart = (PERF_MARKS || SET.showPerf) ? performance.now() : 0;
|
||||||
if (PERF_MARKS) perfMarks.light = 0;
|
if (PERF_MARKS || SET.showPerf) { perfMarks.light = 0; perfMarks.override = 0; delete perfMarks.gpu; }
|
||||||
const bw = R.bw, bh = R.bh, px = R.px;
|
R.gpuOn = SET.renderer === 'webgpu' && SET.look === 'pixel' && GPU.ok;
|
||||||
|
GPU.canvas.hidden = !R.gpuOn;
|
||||||
|
if (R.gpuOn) R.ctx.clearRect(0, 0, R.canvas.width, R.canvas.height);
|
||||||
|
const bw = R.bw, bh = R.bh;
|
||||||
R.vx0 = Math.floor(R.camX); R.vy0 = Math.floor(R.camY);
|
R.vx0 = Math.floor(R.camX); R.vy0 = Math.floor(R.camY);
|
||||||
const vx0 = R.vx0, vy0 = R.vy0;
|
const vx0 = R.vx0, vy0 = R.vy0;
|
||||||
// how hard it is raining around the camera, eased so the sky greys over gently
|
// how hard it is raining around the camera, eased so the sky greys over gently
|
||||||
R.rain = (R.rain || 0) * 0.98 + rainAt(Math.max(0, Math.min(W - 1, Math.floor(R.camX + bw / 2)))) * 0.02;
|
R.rain = (R.rain || 0) * 0.98 + rainAt(Math.max(0, Math.min(W - 1, Math.floor(R.camX + bw / 2)))) * 0.02;
|
||||||
const rk = R.rain;
|
|
||||||
// Smooth look: the colours below are still worked out per grain, but each grain also gets a
|
// Smooth look: the colours below are still worked out per grain, but each grain also gets a
|
||||||
// class (alpha byte) and the GPU draws rounded shapes from them (see smooth-shaders.js)
|
// class (alpha byte) and the GPU draws rounded shapes from them (see smooth-shaders.js)
|
||||||
const sm = R.smoothOn = SET.look === 'smooth' && !R.lens && typeof smoothAvailable === 'function' && smoothAvailable();
|
const sm = R.smoothOn = SET.look === 'smooth' && !R.lens && typeof smoothAvailable === 'function' && smoothAvailable();
|
||||||
const bpx = R.backPx, tpx = R.treePx, fpx = R.fieldPx, mmask = R.moldMask;
|
const mmask = R.moldMask;
|
||||||
if (sm) {
|
if (sm) {
|
||||||
mmask.fill(0);
|
mmask.fill(0);
|
||||||
for (const mo of molds) {
|
for (const mo of molds) {
|
||||||
@@ -30,361 +32,23 @@ function drawWorld() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
prepareSlopeBelts(vx0, vy0, vx0 + bw, vy0 + bh);
|
prepareSlopeBelts(vx0, vy0, vx0 + bw, vy0 + bh);
|
||||||
const useFog = SET.fog;
|
|
||||||
const OUT = sm ? 0x04000000 : 0xff000000; // unexplored and off the map: black
|
|
||||||
const lighting = SET.lighting !== 'off' && !R.lens;
|
const lighting = SET.lighting !== 'off' && !R.lens;
|
||||||
if (lighting && (R.frame % SET.lightEvery === 0 || !R.lw)) {
|
if (lighting && (R.frame % SET.lightEvery === 0 || !R.lw)) {
|
||||||
const lightStart = PERF_MARKS ? performance.now() : 0;
|
const lightStart = PERF_MARKS ? performance.now() : 0;
|
||||||
computeLight();
|
computeLight();
|
||||||
if (PERF_MARKS) perfMarks.light = performance.now() - lightStart;
|
if (PERF_MARKS) perfMarks.light = performance.now() - lightStart;
|
||||||
}
|
}
|
||||||
const colorStart = PERF_MARKS ? performance.now() : 0;
|
const colorStart = (PERF_MARKS || SET.showPerf) ? performance.now() : 0;
|
||||||
const grain = SET.grain, incand = SET.incandescence;
|
const glowOn = SET.glow !== 'off' && !R.lens;
|
||||||
const anim = R.frame;
|
if (glowOn) R.glowPx.fill(0);
|
||||||
const lens = R.lens, tl = THERM[SET.thermalPalette] || THERM.inferno;
|
if (R.gpuOn) gpuOverrides();
|
||||||
const glowOn = SET.glow !== 'off' && !lens;
|
else for (let by = 0; by < bh; by++) for (let bx = 0; bx < bw; bx++) cellPixel(vx0 + bx, vy0 + by);
|
||||||
const gpx = R.glowPx, gw = R.glowC.width;
|
worldObjects();
|
||||||
if (glowOn) gpx.fill(0);
|
if (PERF_MARKS || SET.showPerf) {
|
||||||
for (let by = 0; by < bh; by++) {
|
|
||||||
const y = vy0 + by;
|
|
||||||
const row = by * bw;
|
|
||||||
if (y < 0 || y >= H) { for (let bx = 0; bx < bw; bx++) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } } continue; }
|
|
||||||
const fy = y / FOG, fj = Math.floor(fy), ft = fy - fj, fr = fj * FW, fn = Math.min(FH - 1, fj + 1) * FW;
|
|
||||||
const yw = y * W;
|
|
||||||
let skr = SKY_R[y], skg = SKY_G[y], skb = SKY_B[y];
|
|
||||||
if (rk > 0.01) { const gy = (skr + skg + skb) / 3 * 0.82; skr += (gy - skr) * rk * 0.7; skg += (gy - skg) * rk * 0.7; skb += (gy - skb) * rk * 0.65; }
|
|
||||||
for (let bx = 0; bx < bw; bx++) {
|
|
||||||
const x = vx0 + bx;
|
|
||||||
if (x < 0 || x >= W) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } continue; }
|
|
||||||
let fv = 255;
|
|
||||||
if (useFog) {
|
|
||||||
const fx = x / FOG, j = Math.floor(fx), t = fx - j, j1 = Math.min(FW - 1, j + 1);
|
|
||||||
const a = fog[fr + j], b = fog[fr + j1], c = fog[fn + j], d = fog[fn + j1];
|
|
||||||
if ((a & b & c & d) !== 255) fv = (a + (b - a) * t) * (1 - ft) + (c + (d - c) * t) * ft;
|
|
||||||
if (!fv) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } continue; }
|
|
||||||
// ease the fade into black (smoothstep) and break up the half-resolution steps with grain
|
|
||||||
// noise that is strongest mid-fade and nothing at either end, so the outer edge has no rim
|
|
||||||
if (fv < 255) {
|
|
||||||
let s = fv / 255;
|
|
||||||
s = Math.min(1, Math.max(0, s + (noiseB[yw + x] - 128) * (0.6 / 128) * s * (1 - s)));
|
|
||||||
fv = Math.max(1, 255 * s * s * (3 - 2 * s));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const i = yw + x, m = mat[i], nz = noiseB[i] - 128;
|
|
||||||
const T = temp[i];
|
|
||||||
// Also used by the thermal lens, which always draws in pixels.
|
|
||||||
let looseShade = 1;
|
|
||||||
if (!sm && m !== EMPTY && !builtForLight(i, x, y)) {
|
|
||||||
if (PH[m] === PH_POWDER) looseShade = 1.1 + nz * 0.00065;
|
|
||||||
else if (PH[m] === PH_SOLID && (
|
|
||||||
(x > 0 && PH[mat[i - 1]] === PH_POWDER && !part[i - 1]) ||
|
|
||||||
(x + 1 < W && PH[mat[i + 1]] === PH_POWDER && !part[i + 1]) ||
|
|
||||||
(y > 0 && PH[mat[i - W]] === PH_POWDER && !part[i - W]) ||
|
|
||||||
(y + 1 < H && PH[mat[i + W]] === PH_POWDER && !part[i + W])
|
|
||||||
)) looseShade = 0.72;
|
|
||||||
}
|
|
||||||
let r, g, b;
|
|
||||||
if (lens) {
|
|
||||||
const ti = thermIdx(T);
|
|
||||||
if (m === EMPTY) { r = tl[ti] * 0.55; g = tl[ti + 1] * 0.55; b = tl[ti + 2] * 0.55; }
|
|
||||||
else { const v = (COLR[m] + COLG[m] + COLB[m]) / 3 * 0.25; r = tl[ti] * 0.78 + v; g = tl[ti + 1] * 0.78 + v; b = tl[ti + 2] * 0.78 + v; }
|
|
||||||
const p = part[i];
|
|
||||||
r *= looseShade; g *= looseShade; b *= looseShade;
|
|
||||||
if (p && ((x + y) & 1)) { r = r * 0.7 + 40; g = g * 0.7 + 40; b = b * 0.7 + 40; }
|
|
||||||
if (fv < 255) { r *= fv / 255; g *= fv / 255; b *= fv / 255; }
|
|
||||||
px[row + bx] = 0xff000000 | ((b > 255 ? 255 : b) << 16) | ((g > 255 ? 255 : g) << 8) | (r > 255 ? 255 : r);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// background
|
|
||||||
let br, bgc, bb, sky = false;
|
|
||||||
const tb = bgm[i];
|
|
||||||
if (tb && !sm) { const v = nz * CVAR[tb] / 128; br = TREE_R[tb] + v; bgc = TREE_G[tb] + v; bb = TREE_B[tb] + v; if (digp[i]) { const f = 1 + digp[i] / 700; br *= f; bgc *= f; bb *= f; } }
|
|
||||||
else if (y < surfY[x]) { br = skr; bgc = skg; bb = skb; sky = true; }
|
|
||||||
else {
|
|
||||||
const L = y < dirtY[x] ? BG.dirt : y < graniteY[x] ? BG.stone : y < basaltY[x] ? BG.granite : BG.basalt;
|
|
||||||
const v = sm ? 0 : nz * 0.06;
|
|
||||||
br = L[0] + v; bgc = L[1] + v; bb = L[2] + v;
|
|
||||||
}
|
|
||||||
if (sm) {
|
|
||||||
// the colour behind the grain (alpha marks open sky), and a tree in front of that if there is one
|
|
||||||
bpx[row + bx] = (sky ? 0xff000000 : 0) | ((bb & 255) << 16) | ((bgc & 255) << 8) | (br & 255);
|
|
||||||
if (tb) {
|
|
||||||
const v = nz * CVAR[tb] / 256, f = digp[i] ? 1 + digp[i] / 700 : 1;
|
|
||||||
const tr = Math.max(0, Math.min(255, (TREE_R[tb] + v) * f)), tg = Math.max(0, Math.min(255, (TREE_G[tb] + v) * f)), tbb = Math.max(0, Math.min(255, (TREE_B[tb] + v) * f));
|
|
||||||
tpx[row + bx] = ((tb === LEAVES ? 0xff : 0x80) << 24) | (tbb << 16) | (tg << 8) | tr;
|
|
||||||
} else tpx[row + bx] = 0;
|
|
||||||
}
|
|
||||||
if (m === EMPTY) { r = br; g = bgc; b = bb; }
|
|
||||||
else {
|
|
||||||
const v = nz * CVAR[m] * (sm ? (PH[m] === PH_SOLID ? 0 : grain * 0.35) : grain) / 128;
|
|
||||||
r = COLR[m] + v; g = COLG[m] + v; b = COLB[m] + v;
|
|
||||||
r *= looseShade; g *= looseShade; b *= looseShade;
|
|
||||||
if (m === FIRE) { const f = (life[i] & 15) * 6; r = 255; g = 120 + f; b = 30 + f * 0.5; }
|
|
||||||
else if (m === WATER && !sm) { const s = ((x * 3 + y * 5 + (anim >> 2)) & 15) === 0 ? 14 : 0; r += s; g += s; b += s; }
|
|
||||||
else if (m === BELTL || m === BELTR) {
|
|
||||||
const dir = m === BELTR ? -1 : 1, on = netSat(i) > 0, ly = y & 3, lx = x & 3;
|
|
||||||
if (ly === 0 || ly === 3) {
|
|
||||||
// top run moves with the load, bottom run returns the other way
|
|
||||||
const st = ((x + (ly === 0 ? dir : -dir) * (on ? anim >> 1 : 0)) >> 1) & 1;
|
|
||||||
const k = ly === 0 ? 1 : 0.72;
|
|
||||||
r = (st ? 92 : 56) * k; g = (st ? 96 : 58) * k; b = (st ? 104 : 66) * k;
|
|
||||||
} else if (lx === 1 || lx === 2) {
|
|
||||||
const spin = on ? ((anim >> 2) + (dir < 0 ? lx : 3 - lx) + ly) & 1 : (lx + ly) & 1;
|
|
||||||
r = spin ? 132 : 104; g = spin ? 120 : 94; b = spin ? 108 : 84; // copper rollers
|
|
||||||
} else { r = 46; g = 48; b = 54; }
|
|
||||||
} else if (m === PIPE) {
|
|
||||||
pipeCellColor(x, y, anim); r = PC[0]; g = PC[1]; b = PC[2];
|
|
||||||
} else if (m === SHAFT) {
|
|
||||||
const lx = x & 3, ly = y & 3;
|
|
||||||
if (lx === 0 || lx === 3 || ly === 0 || ly === 3) { if (((x + y) & 3) === 0) { r -= 22; g -= 20; b -= 16; } }
|
|
||||||
else { r -= 34; g -= 30; b -= 24; }
|
|
||||||
}
|
|
||||||
else if (m === GATE) { if ((x & 1) === 0 && (y & 3) !== 0) { r -= 40; g -= 24; b -= 20; } }
|
|
||||||
else if (m === CUTST) { const lx = x & 3, ly = y & 3; if (ly === 1 || ly === 3 || lx === (ly < 2 ? 3 : 1)) { r -= 34; g -= 33; b -= 30; } else if (ly === 0 || ly === 2) { r += 10; g += 10; b += 9; } }
|
|
||||||
else if (m === GRINDER || m === SAWB || m === PRESSB || m === RACKB) {
|
|
||||||
const lx = x & 3, ly = y & 3, frame = lx === 0 || lx === 3 || ly === 0 || ly === 3;
|
|
||||||
r = frame ? 96 : 38; g = frame ? 102 : 42; b = frame ? 112 : 50;
|
|
||||||
}
|
|
||||||
else if (m === WOOD) { if ((y & 3) === 2 || ((x + (y >> 2) * 3) & 7) === 0) { r -= 26; g -= 22; b -= 16; } } // planks
|
|
||||||
else if (m === LWOOD) { if (((x * 3 + y) & 3) === 0) { r -= 30; g -= 26; b -= 18; } } // stringy fibres
|
|
||||||
else if (m === HULL) { if (((x * 7 + y * 3) & 15) === 0) { r += 20; g += 20; b += 22; } }
|
|
||||||
else if (m === SPRING) { if ((nz & 7) === 0) { r = 90; g = 150; b = 220; } }
|
|
||||||
const al = ALPHA[m];
|
|
||||||
if (al < 1) { r = br + (r - br) * al; g = bgc + (g - bgc) * al; b = bb + (b - bb) * al; }
|
|
||||||
const dp = digp[i];
|
|
||||||
if (dp) {
|
|
||||||
// shaken rock pales with dust and shivers before it shatters (the cracks are drawn over it)
|
|
||||||
const k = dp / 255, lift = k * 30 + Math.sin(anim * 1.9 + x * 1.3 + y * 0.7) * 6 * k;
|
|
||||||
r += lift; g += lift; b += lift * 1.08;
|
|
||||||
}
|
|
||||||
// built wall blocks read as tiles: light top-left edge, dark bottom-right edge
|
|
||||||
if (!sm && PH[m] === PH_SOLID && !(flags[i] & F_NAT) && isWall(blockP[(y >> 2) * BW + (x >> 2)])) {
|
|
||||||
const lx = x & 3, ly = y & 3;
|
|
||||||
const wc = blockP[(y >> 2) * BW + (x >> 2)];
|
|
||||||
const edge = isSlope(wc) && (wc === BP_SLOPE_BL || wc === BP_SLOPE_TR ? lx === ly : lx + ly === 3);
|
|
||||||
if (edge || lx === 0 || ly === 0) { r += 16; g += 16; b += 16; }
|
|
||||||
else if (lx === 3 || ly === 3) { r -= 18; g -= 18; b -= 18; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// field parts drawn over contents
|
|
||||||
const sb = slopeBeltAt(i, x, y, sm);
|
|
||||||
let p = part[i];
|
|
||||||
if (p === P_FILTER && (sb >= 0 || isSlopeBelt(blockP[slopeSealOwner(i)]))) p = 0;
|
|
||||||
const db = diagonalScrewBlock(x, y, sm);
|
|
||||||
let diagonal = false;
|
|
||||||
if (db >= 0) {
|
|
||||||
const v = blockV[db], joins = diagonalJoins[db];
|
|
||||||
const sd = diagonalScrewShape(x + 0.5, y + 0.5, db, v, joins);
|
|
||||||
p = sm || sd <= 0 ? SCREW_PARTS[v] : 0;
|
|
||||||
diagonal = !!p;
|
|
||||||
if (sm) tpx[row + bx] = (joins << 24) | ((y - ((db / BW) | 0) * BS + 8) << 16) | ((x - (db % BW) * BS + 8) << 8);
|
|
||||||
}
|
|
||||||
if (p && sm && (IS_SCREW[p] || p === P_FILTER)) { if (m === EMPTY || PH[m] === PH_GAS) { r = 0; g = 0; b = 0; } else { r = Math.max(1, r); } }
|
|
||||||
else if (p) {
|
|
||||||
if (IS_SCREW[p]) {
|
|
||||||
const on = netSat(i) > 0;
|
|
||||||
const vert = p === P_SCREW_U || p === P_SCREW_D;
|
|
||||||
const axis = diagonal ? Math.floor(diagonalAlong) : vert ? y : x;
|
|
||||||
const across = diagonal ? diagonalAcross + 2 : vert ? x & 3 : y & 3;
|
|
||||||
const dir = diagonal && blockV[db] >= 4 ? (blockV[db] >= 6 ? 1 : -1) : (p === P_SCREW_U || p === P_SCREW_L) ? 1 : -1;
|
|
||||||
if (diagonal ? Math.abs(diagonalAcross) > 1.38 || diagonalCap > -0.55 : across === 0 || across === 3) { // copper casing on the two long sides; its load shows through
|
|
||||||
if (m === EMPTY) { r = 150; g = 96; b = 64; } else { r = r * 0.72 + 44; g = g * 0.72 + 26; b = b * 0.72 + 14; }
|
|
||||||
if (!diagonal && (axis & 3) === 0) { r -= 24; g -= 18; b -= 12; }
|
|
||||||
} else {
|
|
||||||
// screw flights: a diagonal stripe that walks along the tube when powered
|
|
||||||
const ch = (((axis + across + dir * (on ? anim >> 1 : 0)) & 3) === 0);
|
|
||||||
if (m === EMPTY) { r = ch ? 120 : 46; g = ch ? 112 : 46; b = ch ? 104 : 52; }
|
|
||||||
else { r = r * 0.75 + (ch ? 34 : 8); g = g * 0.75 + (ch ? 30 : 8); b = b * 0.75 + (ch ? 28 : 10); }
|
|
||||||
}
|
|
||||||
} else if (p === P_FILTER) {
|
|
||||||
const lx = x & 3, ly = y & 3, fb = (y >> 2) * BW + (x >> 2), dir = blockV[fb] & 128 ? -1 : 1;
|
|
||||||
const fc = filterCfg.get(fb), fms = fc ? fc.m : null, fn = fms ? fms.length : 0, inv = fc && fc.inv;
|
|
||||||
if (ly === 0) { // rollers on top, turning the way the load rides; in carry mode, striped with what rides
|
|
||||||
const st = ((x + dir * (anim >> 1)) >> 1) & 1;
|
|
||||||
if (m === EMPTY || st) {
|
|
||||||
if (inv && fn && !st) { const c = fms[((x + dir * (anim >> 1)) >> 2 & 0xffff) % fn]; r = COLR[c] * 0.8; g = COLG[c] * 0.8; b = COLB[c] * 0.8; }
|
|
||||||
else { r = st ? 92 : 56; g = st ? 96 : 58; b = st ? 104 : 66; }
|
|
||||||
}
|
|
||||||
} else if (ly === 3) { // the belt's return run carries on under it, so belts and filters read as one belt
|
|
||||||
const st = ((x - dir * (anim >> 1)) >> 1) & 1;
|
|
||||||
if (m === EMPTY || st) { r = (st ? 92 : 56) * 0.72; g = (st ? 96 : 58) * 0.72; b = (st ? 104 : 66) * 0.72; }
|
|
||||||
} else if ((lx === 0 && !beltLike(fb, 0)) || (lx === 3 && !beltLike(fb, 1))) { if (m === EMPTY || ((x + y) & 1)) { r = 64; g = 66; b = 74; } } // frame only where the line ends
|
|
||||||
else if (m === EMPTY) { // mesh holes, tinted with what falls through them
|
|
||||||
if ((x + y) & 1) { r = 40; g = 42; b = 48; }
|
|
||||||
else if (inv) { r = 58; g = 74; b = 72; } // carry mode: everything else drops
|
|
||||||
else if (fn) { const c = fms[(((x >> 1) + (y >> 1)) & 0xffff) % fn]; r = COLR[c] * 0.75; g = COLG[c] * 0.75; b = COLB[c] * 0.75; }
|
|
||||||
else { r = 24; g = 25; b = 28; }
|
|
||||||
}
|
|
||||||
} else if (p === P_GRATE) {
|
|
||||||
if ((x + y) & 1) { r = 150; g = 78; b = 58; } else if (m === EMPTY) { r *= 0.6; g *= 0.6; b *= 0.6; }
|
|
||||||
} else if (p === P_GOPEN) {
|
|
||||||
if ((x & 3) === 0) { r = r * 0.6 + 50; g = g * 0.6 + 30; b = b * 0.6 + 26; }
|
|
||||||
} else if (p === P_INTAKE) {
|
|
||||||
if (m === EMPTY) { r = 30; g = 32; b = 36; }
|
|
||||||
if ((x & 1) === 0) { r += 50; g += 54; b += 60; }
|
|
||||||
} else if (p === P_PIPE) {
|
|
||||||
if (pipeBodyCell(x, y)) { thinPipeColor(x, y, anim); r = PC[0]; g = PC[1]; b = PC[2]; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const slopeArt = sb >= 0 && (m === EMPTY || PH[m] === PH_GAS || m === BELTL || m === BELTR);
|
|
||||||
if (slopeArt && !sm) {
|
|
||||||
const color = slopeBeltColor(sb, x, y, anim, br, bgc, bb);
|
|
||||||
r = color & 255; g = color >> 8 & 255; b = color >> 16 & 255;
|
|
||||||
}
|
|
||||||
const slopeDetail = slopeDetailAt(i, x, y, sm);
|
|
||||||
if (slopeDetail) {
|
|
||||||
const wave = slopeDetail === 2 && ((x + beltGrade(blockP[slopeWallOwner]) * y - slopePhase[slopeWallOwner]) & 3) < 2;
|
|
||||||
// ribs: light rubber with a pale cap; the far wall behind them: muted, corrugated rubber
|
|
||||||
const cap = slopeDetail === 1 && slopeWallOwner >= 0 && !cleatMask[i - W + beltGrade(blockP[slopeWallOwner])];
|
|
||||||
r = slopeDetail === 1 ? (cap ? 178 : 146) : wave ? 64 : 56;
|
|
||||||
g = slopeDetail === 1 ? (cap ? 182 : 150) : wave ? 68 : 60;
|
|
||||||
b = slopeDetail === 1 ? (cap ? 190 : 158) : wave ? 76 : 68;
|
|
||||||
}
|
|
||||||
// incandescence
|
|
||||||
if (T > 450 && m !== EMPTY && incand) {
|
|
||||||
const gi = glowIndex(PH[m] === PH_SOLID ? T + nz * 0.9 : m === MAGMA ? T + Math.sin(x * 0.19 + y * 0.11 + anim * 0.03 + nz * 0.04) * 55 - 30 : T), w = GLOW_W[gi];
|
|
||||||
r = r * (1 - w) + GLOW_R[gi] * w; g = g * (1 - w) + GLOW_G[gi] * w; b = b * (1 - w) + GLOW_B[gi] * w;
|
|
||||||
if (glowOn && fv >= 128 && w > 0.3 && ((bx | by) & 1) === 0) {
|
|
||||||
const gk = (by >> 1) * gw + (bx >> 1), s = (w - 0.3) * 1.4;
|
|
||||||
gpx[gk] = 0xff000000 | (Math.min(255, GLOW_B[gi] * s) << 16) | (Math.min(255, GLOW_G[gi] * s) << 8) | Math.min(255, GLOW_R[gi] * s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (r < 0) r = 0; if (g < 0) g = 0; if (b < 0) b = 0;
|
|
||||||
let top = 0xff000000;
|
|
||||||
if (sm) {
|
|
||||||
// what kind of thing this grain is drawn as: 0 open, 1 terrain, 2 loose grain, 3 liquid,
|
|
||||||
// 4 built piece (drawn crisp), 5 smoke and flame, 6 built wall block (drawn as a tile)
|
|
||||||
let cl = 0;
|
|
||||||
const pt = (p === P_PIPE && !pipeBodyCell(x, y)) || (p === P_FILTER && sb >= 0) ? 0 : p; // the open parts of a pipe block show what is in them
|
|
||||||
if (IS_SCREW[pt]) cl = SCREW_CLASS[pt];
|
|
||||||
else if (p === P_FILTER && sb < 0) {
|
|
||||||
// filter: 13 carries left, 14 right; the tree buffer carries its colour tag (first material) and mode
|
|
||||||
const fb = (y >> 2) * BW + (x >> 2), fc = filterCfg.get(fb), fm = fc && fc.m.length ? fc.m[0] : 0;
|
|
||||||
cl = blockV[fb] & 128 ? 14 : 13;
|
|
||||||
tpx[row + bx] = ((0x20 | (fc && fc.inv ? 0x10 : 0) | beltCaps[fb]) << 24) | (fm ? (COLB[fm] << 16) | (COLG[fm] << 8) | COLR[fm] : 0);
|
|
||||||
}
|
|
||||||
else if (m === BELTL || m === BELTR) {
|
|
||||||
cl = m === BELTL ? 7 : 8;
|
|
||||||
tpx[row + bx] = beltCaps[(y >> 2) * BW + (x >> 2)] << 24;
|
|
||||||
}
|
|
||||||
else if (pt || PARTMAT[m] || mmask[row + bx]) cl = 4;
|
|
||||||
else if (m !== EMPTY) {
|
|
||||||
const ph = PH[m];
|
|
||||||
if (ph === PH_SOLID) cl = (!(flags[i] & F_NAT) && isWall(blockP[(y >> 2) * BW + (x >> 2)])) ? 6 : 1;
|
|
||||||
else if (ph === PH_POWDER) cl = 2;
|
|
||||||
else if (ph === PH_LIQUID) cl = 3;
|
|
||||||
else cl = 5;
|
|
||||||
}
|
|
||||||
const wallCode = blockP[(y >> 2) * BW + (x >> 2)];
|
|
||||||
if (isSlope(wallCode) && ((m === EMPTY && !slopeCell(wallCode, x & 3, y & 3)) || (PH[m] === PH_SOLID && !(flags[i] & F_NAT)))) {
|
|
||||||
cl = wallCode;
|
|
||||||
const wm = WALL_OF[blockV[(y >> 2) * BW + (x >> 2)]];
|
|
||||||
r = COLR[wm]; g = COLG[wm]; b = COLB[wm];
|
|
||||||
}
|
|
||||||
if (sb >= 0 && (slopeArt || PH[m] === PH_POWDER)) {
|
|
||||||
const code = blockP[sb], dir = isFilter(code) ? (blockV[sb] & 128 ? 1 : 0) : blockV[sb];
|
|
||||||
const slopeClass = 23 + (code - BP_BELT_UP) * 2 + dir;
|
|
||||||
if (slopeArt) cl = slopeClass;
|
|
||||||
const f = isFilter(code) && filterCfg.get(sb), fm = f && f.m.length ? f.m[(Math.floor((x + beltGrade(code) * y) / 2) & 65535) % f.m.length] : 0;
|
|
||||||
tpx[row + bx] = ((slopeBeltJoins[sb] | 128) << 24) | slopeClass |
|
|
||||||
((y - ((sb / BW) | 0) * BS + 8) << 16) | ((x - (sb % BW) * BS + 8) << 8);
|
|
||||||
if (slopeArt) { r = fm ? COLR[fm] : 0; g = fm ? COLG[fm] : 0; b = fm ? COLB[fm] : 0; }
|
|
||||||
}
|
|
||||||
if (slopeDetail && slopeWallOwner >= 0) {
|
|
||||||
// ribs and the far wall are both drawn by the wall shader, ribs from the same phase as the simulation
|
|
||||||
const wb = slopeWallOwner;
|
|
||||||
cl = beltGrade(blockP[wb]) < 0 ? 31 : 32;
|
|
||||||
tpx[row + bx] = ((128 | slopePhase[wb] | ((slopeBeltJoins[wb] & 12) << 1) | (blockV[wb] & 1 ? 32 : 0)) << 24) | cl |
|
|
||||||
((y - ((wb / BW) | 0) * BS + 8) << 16) | ((x - wb % BW * BS + 8) << 8);
|
|
||||||
}
|
|
||||||
top = cl << 24;
|
|
||||||
fpx[row + bx] = (tb ? 0xff000000 : 0) | (cl === 5 ? 0xff0000 : 0) | (cl === 3 ? 0xff00 : 0) | (cl === 1 && !blockP[(y >> 2) * BW + (x >> 2)] && !mach[i] ? 0xff : 0); // terrain: any solid nothing owns, natural or not
|
|
||||||
}
|
|
||||||
if (fv < 255) {
|
|
||||||
const k = fv / 255; r *= k; g *= k; b *= k;
|
|
||||||
if (sm) {
|
|
||||||
const c = bpx[row + bx], t = tpx[row + bx];
|
|
||||||
bpx[row + bx] = fogColor(c, k); tpx[row + bx] = fogColor(t, k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
px[row + bx] = top | ((b > 255 ? 255 : b) << 16) | ((g > 255 ? 255 : g) << 8) | (r > 255 ? 255 : r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// bars and other loose objects
|
|
||||||
if (!lens) for (const e of ents) {
|
|
||||||
if (useFog && fogSeen(e.ix, e.iy) < 64) continue;
|
|
||||||
const ex = e.ix - vx0, ey = e.iy - vy0;
|
|
||||||
if (ex + BAR_W < 0 || ey + BAR_H < 0 || ex >= bw || ey >= bh) continue;
|
|
||||||
const m = e.m;
|
|
||||||
for (let dy = 0; dy < BAR_H; dy++) for (let dx = 0; dx < BAR_W; dx++) {
|
|
||||||
const sx = ex + dx, sy = ey + dy;
|
|
||||||
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
|
|
||||||
let r = COLR[m], g = COLG[m], b = COLB[m];
|
|
||||||
const end = dx === 0 || dx === BAR_W - 1;
|
|
||||||
if (dy === 0) { r += end ? 10 : 34; g += end ? 8 : 28; b += end ? 6 : 24; } else { r -= end ? 34 : 18; g -= end ? 30 : 16; b -= end ? 26 : 14; }
|
|
||||||
if (e.T > 450 && incand) { const gi = glowIndex(e.T), w = GLOW_W[gi]; r = r * (1 - w) + GLOW_R[gi] * w; g = g * (1 - w) + GLOW_G[gi] * w; b = b * (1 - w) + GLOW_B[gi] * w; }
|
|
||||||
if (r < 0) r = 0; if (g < 0) g = 0; if (b < 0) b = 0;
|
|
||||||
px[sy * bw + sx] = (sm ? 0x04000000 : 0xff000000) | ((b > 255 ? 255 : b | 0) << 16) | ((g > 255 ? 255 : g | 0) << 8) | (r > 255 ? 255 : r | 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// grains in flight from the vacuum nozzle, a screw or a belt (rain and snow are drawn over the top, see drawRain)
|
|
||||||
if (!lens) for (const f of flyers) {
|
|
||||||
if (f.rain || (useFog && fogSeen(f.x, f.y) < 64)) continue;
|
|
||||||
const sx = Math.floor(f.x) - vx0, sy = Math.floor(f.y) - vy0;
|
|
||||||
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
|
|
||||||
const m = f.m, v = ((sx * 7 + sy * 13) & 15) - 8;
|
|
||||||
px[sy * bw + sx] = (sm ? 0x02000000 : 0xff000000) | (Math.max(0, Math.min(255, COLB[m] + v)) << 16) | (Math.max(0, Math.min(255, COLG[m] + v)) << 8) | Math.max(0, Math.min(255, COLR[m] + v));
|
|
||||||
}
|
|
||||||
// blueprint ghosts: blocks waiting to be built
|
|
||||||
if (ghostBlocks.size) {
|
|
||||||
const bx0 = vx0 >> 2, by0 = vy0 >> 2, bx1 = (vx0 + bw) >> 2, by1 = (vy0 + bh) >> 2;
|
|
||||||
const pulse = (R.frame >> 3) & 3;
|
|
||||||
for (const [b, gv] of ghostBlocks) {
|
|
||||||
const gbx = b % BW, gby = (b / BW) | 0;
|
|
||||||
if (gbx < bx0 || gbx > bx1 || gby < by0 || gby > by1) continue;
|
|
||||||
const blocked = ghostBlocked.has(b), code = gv[0], v = gv[1];
|
|
||||||
let tr = 90, tg = 190, tb = 230;
|
|
||||||
if (blocked) { tr = 230; tg = 70; tb = 60; }
|
|
||||||
else if (isWall(code) && WALL_OF[v]) { const wm = WALL_OF[v]; tr = (COLR[wm] + 90) / 2; tg = (COLG[wm] + 190) / 2; tb = (COLB[wm] + 230) / 2; }
|
|
||||||
const slope = isSlopeBelt(code), diagonal = code === BP_SCREW && (v >= 4 || straightScrewJoins(gbx, gby, v, ghostBlocks));
|
|
||||||
const joins = slope ? slopeBeltConnections(gbx, gby, code, ghostBlocks) : diagonal ? (v >= 4 ? diagonalScrewJoins(gbx, gby, v, ghostBlocks) : straightScrewJoins(gbx, gby, v, ghostBlocks)) : 0;
|
|
||||||
const caps = code === BP_BELT || code === BP_FILTER ? flatBeltCaps(b, ghostBlocks) : 3;
|
|
||||||
for (let dy = slope ? -9 : diagonal ? -4 : 0; dy < (slope || diagonal ? 8 : BS); dy++) for (let dx = slope ? -5 : diagonal ? -4 : 0; dx < (slope ? 9 : diagonal ? 8 : BS); dx++) {
|
|
||||||
let sd = 0;
|
|
||||||
if (diagonal) {
|
|
||||||
if (v < 4 && ((v & 1) ? dx < 0 || dx >= BS : dy < 0 || dy >= BS)) continue;
|
|
||||||
const x = gbx * BS + dx, y = gby * BS + dy;
|
|
||||||
if (x < 0 || y < 0 || x >= W || y >= H) continue;
|
|
||||||
const i = y * W + x, n = (y >> 2) * BW + (x >> 2);
|
|
||||||
if (n !== b && (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || blockP[n])) continue;
|
|
||||||
sd = diagonalScrewShape(x + 0.5, y + 0.5, b, v, joins);
|
|
||||||
if (sd > 0) continue;
|
|
||||||
}
|
|
||||||
if (slope) {
|
|
||||||
const x = gbx * BS + dx, y = gby * BS + dy;
|
|
||||||
if (x < 0 || y < 0 || x >= W || y >= H) continue;
|
|
||||||
const i = y * W + x, n = (y >> 2) * BW + (x >> 2);
|
|
||||||
if (n !== b && (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || blockP[n])) continue;
|
|
||||||
sd = slopeBeltShape(x + 0.5, y + 0.5, b, joins, code);
|
|
||||||
const up = !isFilter(code) && slopeUphill(code, v), rib = up && slopePreviewCleat(x, y, b, code);
|
|
||||||
if (sd > 0 && !rib && (!up || slopeWallShape(x + 0.5, y + 0.5, b, code) > 0)) continue;
|
|
||||||
if (rib) sd = -0.1;
|
|
||||||
}
|
|
||||||
if (isSlope(code) && !slopeCell(code, dx, dy)) continue;
|
|
||||||
const sx = gbx * BS + dx - vx0, sy = gby * BS + dy - vy0;
|
|
||||||
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
|
|
||||||
const k = sy * bw + sx, c = px[k];
|
|
||||||
const edge = slope || diagonal ? sd > -0.8 : (dx === 0 && (caps & 1)) || dy === 0 || (dx === BS - 1 && (caps & 2)) || dy === BS - 1;
|
|
||||||
const a = edge ? (((dx + dy + pulse) & 3) === 0 ? 0.85 : 0.6) : 0.28;
|
|
||||||
const pr = c & 255, pg = (c >> 8) & 255, pb = (c >> 16) & 255;
|
|
||||||
px[k] = (sm ? 0x04000000 : 0xff000000) | ((pb + (tb - pb) * a) << 16) | ((pg + (tg - pg) * a) << 8) | (pr + (tr - pr) * a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (PERF_MARKS) {
|
|
||||||
perfMarks.color = performance.now() - colorStart;
|
perfMarks.color = performance.now() - colorStart;
|
||||||
perfMarks.prepare = colorStart - perfStart - perfMarks.light;
|
perfMarks.prepare = colorStart - perfStart - perfMarks.light;
|
||||||
}
|
}
|
||||||
|
if (R.gpuOn) { gpuDraw(lighting, glowOn); return; }
|
||||||
if (sm) { smoothDraw(lighting, glowOn); return; }
|
if (sm) { smoothDraw(lighting, glowOn); return; }
|
||||||
const uploadStart = PERF_MARKS ? performance.now() : 0;
|
const uploadStart = PERF_MARKS ? performance.now() : 0;
|
||||||
R.bctx.putImageData(R.img, 0, 0);
|
R.bctx.putImageData(R.img, 0, 0);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
/*__CSS__*/
|
/*__CSS__*/
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<canvas id="gpu" aria-hidden="true" hidden></canvas>
|
||||||
<canvas id="game" aria-label="Game world"></canvas>
|
<canvas id="game" aria-label="Game world"></canvas>
|
||||||
<div id="ovframe" aria-hidden="true"><i class="c1"></i><i class="c2"></i><i class="c3"></i><i class="c4"></i></div>
|
<div id="ovframe" aria-hidden="true"><i class="c1"></i><i class="c2"></i><i class="c3"></i><i class="c4"></i></div>
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ body {
|
|||||||
font-family: var(--font-ui); font-size: calc(13.5px * var(--ui)); line-height: 1.4;
|
font-family: var(--font-ui); font-size: calc(13.5px * var(--ui)); line-height: 1.4;
|
||||||
-webkit-user-select: none; user-select: none; touch-action: none;
|
-webkit-user-select: none; user-select: none; touch-action: none;
|
||||||
}
|
}
|
||||||
canvas#game { position: fixed; inset: 0; width: 100vw; height: 100vh; display: block; image-rendering: pixelated; cursor: none; } /* the game draws its own aim */
|
canvas#gpu, canvas#game { position: fixed; inset: 0; width: 100vw; height: 100vh; display: block; image-rendering: pixelated; cursor: none; } /* the game draws its own aim */
|
||||||
/* overlay modes (thermal lens, info overlay): a coloured frame with corner brackets, a light wash
|
/* overlay modes (thermal lens, info overlay): a coloured frame with corner brackets, a light wash
|
||||||
for the info overlay so the world stays clear, and a named badge at the bottom of the screen */
|
for the info overlay so the world stays clear, and a named badge at the bottom of the screen */
|
||||||
#ovframe { position: fixed; inset: 0; pointer-events: none; opacity: 0; transition: opacity 0.2s; --ov: 92, 186, 166; }
|
#ovframe { position: fixed; inset: 0; pointer-events: none; opacity: 0; transition: opacity 0.2s; --ov: 92, 186, 166; }
|
||||||
@@ -399,3 +399,6 @@ input.text { background: var(--soot-0); border: 1px solid var(--soot-4); border-
|
|||||||
#hud.building:has(#minimap:not([hidden])) ~ #perf { bottom: calc(350px * var(--ui)); }
|
#hud.building:has(#minimap:not([hidden])) ~ #perf { bottom: calc(350px * var(--ui)); }
|
||||||
#hud.building:has(#minimap:not([hidden])) ~ #saving { bottom: calc(390px * var(--ui)); }
|
#hud.building:has(#minimap:not([hidden])) ~ #saving { bottom: calc(390px * var(--ui)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canvas#gpu { pointer-events: none; }
|
||||||
|
canvas#gpu[hidden] { display: none; }
|
||||||
|
|||||||
+1
-1
@@ -101,7 +101,7 @@ function frame(now) {
|
|||||||
fpsSm = fpsSm * 0.95 + (1 / Math.max(0.001, dt)) * 0.05;
|
fpsSm = fpsSm * 0.95 + (1 / Math.max(0.001, dt)) * 0.05;
|
||||||
if (SET.showPerf && R.frame % 15 === 0) {
|
if (SET.showPerf && R.frame % 15 === 0) {
|
||||||
let aw = 0, ht = 0; for (let c = 0; c < NCH; c++) { if (awake[c]) aw++; if (hot[c]) ht++; }
|
let aw = 0, ht = 0; for (let c = 0; c < NCH; c++) { if (awake[c]) aw++; if (hot[c]) ht++; }
|
||||||
$('perf').textContent = `${fpsSm.toFixed(0)} fps · frame ${frameMs.toFixed(1)} ms · sim ${simMs.toFixed(1)} ms/tick (move ${(perfTimes.simMove || 0).toFixed(1)}, heat ${(perfTimes.heatPass || 0).toFixed(1)}, rest ${Math.max(0, simMs - (perfTimes.simMove || 0) - (perfTimes.heatPass || 0)).toFixed(1)})\nworld ${(perfTimes.drawWorld || 0).toFixed(1)} ms · light ${(perfTimes.computeLight || 0).toFixed(1)} ms · overlays ${(perfTimes.overlays || 0).toFixed(1)} ms\nmoving areas ${aw} · hot areas ${ht} / ${NCH} · bars ${ents.length} · view ${R.bw}×${R.bh}`;
|
$('perf').textContent = `${fpsSm.toFixed(0)} fps · frame ${frameMs.toFixed(1)} ms · sim ${simMs.toFixed(1)} ms/tick (move ${(perfTimes.simMove || 0).toFixed(1)}, heat ${(perfTimes.heatPass || 0).toFixed(1)}, rest ${Math.max(0, simMs - (perfTimes.simMove || 0) - (perfTimes.heatPass || 0)).toFixed(1)})\nworld ${(perfTimes.drawWorld || 0).toFixed(1)} ms · light ${(perfTimes.computeLight || 0).toFixed(1)} ms${R.gpuOn ? ` · overrides ${(perfMarks.override || 0).toFixed(1)} · upload ${(perfMarks.upload || 0).toFixed(1)} · GPU ${GPU.gpuMs === null ? "—" : GPU.gpuMs.toFixed(1)}` : ""} · overlays ${(perfTimes.overlays || 0).toFixed(1)} ms\nmoving areas ${aw} · hot areas ${ht} / ${NCH} · bars ${ents.length} · view ${R.bw}×${R.bh}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const SET_TABS = {
|
|||||||
{ k: 'shake', label: 'Screen shake', type: 'toggle' },
|
{ k: 'shake', label: 'Screen shake', type: 'toggle' },
|
||||||
],
|
],
|
||||||
Graphics: [
|
Graphics: [
|
||||||
|
{ k: 'renderer', label: 'Renderer', d: 'WebGPU colours the pixel look on the graphics card. Smooth uses the original renderer.', type: 'select', opts: [['webgpu', 'WebGPU'], ['legacy', 'Legacy']] },
|
||||||
{ k: 'look', label: 'Look', d: 'Smooth draws rounded, outlined ground, round grains and glossy liquids on the graphics card. Pixel is the original look, one square per grain.', type: 'select', opts: [['smooth', 'Smooth'], ['pixel', 'Pixel']] },
|
{ k: 'look', label: 'Look', d: 'Smooth draws rounded, outlined ground, round grains and glossy liquids on the graphics card. Pixel is the original look, one square per grain.', type: 'select', opts: [['smooth', 'Smooth'], ['pixel', 'Pixel']] },
|
||||||
{ k: 'lighting', label: 'Lighting', d: 'Full is colored light at grain resolution. Fast uses half resolution.', type: 'select', opts: [['off', 'Off'], ['fast', 'Fast'], ['full', 'Full']] },
|
{ k: 'lighting', label: 'Lighting', d: 'Full is colored light at grain resolution. Fast uses half resolution.', type: 'select', opts: [['off', 'Off'], ['fast', 'Fast'], ['full', 'Full']] },
|
||||||
{ k: 'lightEvery', label: 'Light updates', type: 'select', opts: [[1, 'Every frame'], [2, 'Every 2nd frame'], [3, 'Every 3rd frame']] },
|
{ k: 'lightEvery', label: 'Light updates', type: 'select', opts: [[1, 'Every frame'], [2, 'Every 2nd frame'], [3, 'Every 3rd frame']] },
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
// ---------- Settings ----------
|
// ---------- Settings ----------
|
||||||
const SET_DEFAULT = {
|
const SET_DEFAULT = {
|
||||||
zoom: 12, integerScale: true, renderScale: 1, uiScale: 1, fpsCap: 0, showPerf: false, shake: true, cameraSmooth: 0.7, cameraLead: true,
|
zoom: 12, integerScale: true, renderScale: 1, uiScale: 1, fpsCap: 0, showPerf: false, shake: true, cameraSmooth: 0.7, cameraLead: true,
|
||||||
look: 'smooth', lighting: 'full', lightEvery: 1, ambient: 0.12, daylight: 1, glow: 'low', grain: 1, incandescence: true, particles: true,
|
renderer: 'webgpu', look: 'smooth', lighting: 'full', lightEvery: 1, ambient: 0.12, daylight: 1, glow: 'low', grain: 1, incandescence: true, particles: true,
|
||||||
minimap: true, minimapZoom: 4, fog: true, thermalPalette: 'inferno', simSpeed: 1, maxSteps: 3,
|
minimap: true, minimapZoom: 4, fog: true, thermalPalette: 'inferno', simSpeed: 1, maxSteps: 3,
|
||||||
volMaster: 0.8, volMusic: 0.5, volSfx: 0.8, muted: false, muteUnfocused: true,
|
volMaster: 0.8, volMusic: 0.5, volSfx: 0.8, muted: false, muteUnfocused: true,
|
||||||
deadzone: 0.18, aimSpeed: 1, autosave: 3, cardSeconds: 9, cursorLabel: 'ores', sv: 2,
|
deadzone: 0.18, aimSpeed: 1, autosave: 3, cardSeconds: 9, cursorLabel: 'ores', sv: 2,
|
||||||
|
|||||||
+3
-1
@@ -5,9 +5,11 @@ async function checkCapabilities() {
|
|||||||
try {
|
try {
|
||||||
const adapter = navigator.gpu && await navigator.gpu.requestAdapter();
|
const adapter = navigator.gpu && await navigator.gpu.requestAdapter();
|
||||||
CAPS.webgpu = !!adapter;
|
CAPS.webgpu = !!adapter;
|
||||||
|
if (adapter) await gpuInit(adapter);
|
||||||
|
else SET.renderer = 'legacy';
|
||||||
const info = adapter && adapter.info;
|
const info = adapter && adapter.info;
|
||||||
if (info) CAPS.adapterInfo = { vendor: info.vendor, architecture: info.architecture, device: info.device, description: info.description };
|
if (info) CAPS.adapterInfo = { vendor: info.vendor, architecture: info.architecture, device: info.device, description: info.description };
|
||||||
} catch (e) { CAPS.webgpu = false; }
|
} catch (e) { CAPS.webgpu = false; SET.renderer = 'legacy'; }
|
||||||
CAPS.ready = true;
|
CAPS.ready = true;
|
||||||
renderCapabilityNotice();
|
renderCapabilityNotice();
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -1308,10 +1308,10 @@ with launch(viewport={'width': 1280, 'height': 800}) as (b, pg, url):
|
|||||||
snapCamera(); GAME.hidePreviews = true; $('cards').replaceChildren();
|
snapCamera(); GAME.hidePreviews = true; $('cards').replaceChildren();
|
||||||
}""")
|
}""")
|
||||||
fp.wait_for_timeout(300)
|
fp.wait_for_timeout(300)
|
||||||
sample = "(() => { drawWorld(); const k = (235 - R.vy0) * R.bw + 300 - R.vx0; return R.px[k] & 0xffffff; })()"
|
sample = "(() => { drawWorld(); const p=worldToScreenPx(300.5,235.5), c=worldPixelContext().getImageData(p.x,p.y,1,1).data; return c[0]|c[1]<<8|c[2]<<16; })()"
|
||||||
check('unexplored pixel is black', fp.evaluate(sample) == 0)
|
check('unexplored pixel is black', fp.evaluate(sample) == 0)
|
||||||
check('sky starts seen and draws colour', fp.evaluate("(() => { const cy=R.camY; R.camY=0; drawWorld(); const c=R.px[20*R.bw+20]&0xffffff; R.camY=cy; drawWorld(); return fogSeen(300,0)===255 && c!==0x0a0a0c; })()"))
|
check('sky starts seen and draws colour', fp.evaluate("(() => { const cy=R.camY; R.camY=0; drawWorld(); const d=worldPixelContext().getImageData(20*R.zp,20*R.zp,1,1).data,c=d[0]|d[1]<<8|d[2]<<16; R.camY=cy; drawWorld(); return fogSeen(300,0)===255 && c>0 && c!==0x0a0a0c; })()"))
|
||||||
check('unexplored canvas pixel is black', fp.evaluate("(() => {const s=worldToScreenPx(300.5,235.5);return Array.from(R.ctx.getImageData(s.x,s.y,1,1).data).slice(0,3).every(v=>v<=12)})()"))
|
check('unexplored canvas pixel is black', fp.evaluate("(() => {const s=worldToScreenPx(300.5,235.5);return Array.from(worldPixelContext().getImageData(s.x,s.y,1,1).data).slice(0,3).every(v=>v<=12)})()"))
|
||||||
check('hover and cursor share only Unexplored', fp.evaluate("JSON.stringify(hoverInfo(300,235)) === JSON.stringify({t1:'Unexplored',t2:[],t3:[]})"))
|
check('hover and cursor share only Unexplored', fp.evaluate("JSON.stringify(hoverInfo(300,235)) === JSON.stringify({t1:'Unexplored',t2:[],t3:[]})"))
|
||||||
fp.evaluate("GAME.aim = {x:300,y:235}; renderInfo()")
|
fp.evaluate("GAME.aim = {x:300,y:235}; renderInfo()")
|
||||||
check('info box says only Unexplored', fp.locator('#info').inner_text() == 'Unexplored')
|
check('info box says only Unexplored', fp.locator('#info').inner_text() == 'Unexplored')
|
||||||
|
|||||||
+1
-1
@@ -45,7 +45,7 @@ def main():
|
|||||||
if browser:
|
if browser:
|
||||||
subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT, check=True)
|
subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT, check=True)
|
||||||
# one at a time: two headless browsers at once can starve each other and flake
|
# one at a time: two headless browsers at once can starve each other and flake
|
||||||
for name in ['play.py', 't_smooth.py', 't_dev_console.py', 't_caps.py', 'firefox_smoke.py']:
|
for name in ['play.py', 't_smooth.py', 't_dev_console.py', 't_caps.py', 't_gpu.py', 'firefox_smoke.py']:
|
||||||
results.append(run_py(name))
|
results.append(run_py(name))
|
||||||
bad = 0
|
bad = 0
|
||||||
for name, code, out, secs in results:
|
for name, code, out, secs in results:
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
"""WebGPU pixel parity, rare-cell selection, world replacement, resize and device loss."""
|
||||||
|
import pathlib, sys
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / 'tools'))
|
||||||
|
from use_venv import ensure_playwright; ensure_playwright()
|
||||||
|
from browser import launch, consume_gpu_option
|
||||||
|
gpu_mode = consume_gpu_option()
|
||||||
|
root = pathlib.Path(__file__).resolve().parent.parent
|
||||||
|
with launch(viewport={'width': 960, 'height': 600}) as (browser, pg, url):
|
||||||
|
errors = []
|
||||||
|
pg.on('pageerror', lambda e: errors.append(str(e)))
|
||||||
|
pg.on('console', lambda m: errors.append(m.text) if m.type == 'error' else None)
|
||||||
|
pg.add_init_script('requestAnimationFrame = () => 0;')
|
||||||
|
pg.goto(url); pg.evaluate('CAPS_CHECK')
|
||||||
|
if gpu_mode == 'soft' and not pg.evaluate('CAPS.webgpu'):
|
||||||
|
print('SKIP WebGPU parity: software browser has no WebGPU adapter')
|
||||||
|
sys.exit(0)
|
||||||
|
assert pg.evaluate('GPU.ok'), pg.evaluate('GPU.error')
|
||||||
|
pg.evaluate('generateWorld(424242, 640, 384, null); enterGame(); GAME.paused=true;')
|
||||||
|
pg.evaluate('() => {' + (root / 'tools/scenes/t88-renderer.js').read_text() + '}')
|
||||||
|
result = pg.evaluate("""() => {
|
||||||
|
SET.look='pixel'; SET.zoom=6; SET.glow='off'; SET.lighting='off';
|
||||||
|
resizeRender(); bot.x=CAM[0]-bot.w/2; bot.y=CAM[1]-bot.h/2; snapCamera(); R.frame=1000;
|
||||||
|
const results=[];
|
||||||
|
for(const rain of [0,0.7])for(const lit of [false,true])for(const lens of [false,true])for(const palette of ['inferno','ironbow','gray'])for(const fogOn of [false,true]) {
|
||||||
|
SET.lighting=lit?'full':'off'; SET.glow=lit?'low':'off';
|
||||||
|
R.lens=lens; SET.thermalPalette=palette; SET.fog=fogOn;
|
||||||
|
R.rain=rain; SET.renderer='legacy';drawWorld(); const a=worldPixelContext().getImageData(0,0,960,600).data;
|
||||||
|
R.rain=rain; SET.renderer='webgpu';drawWorld(); const b=worldPixelContext().getImageData(0,0,960,600).data;
|
||||||
|
let max=0, sum=0, bad=0;
|
||||||
|
for(let i=0;i<a.length;i+=4) {let d=0;for(let j=0;j<3;j++){const e=Math.abs(a[i+j]-b[i+j]);sum+=e;d=Math.max(d,e);}max=Math.max(max,d);bad+=d>8;}
|
||||||
|
results.push({rain,lit,lens,palette,fogOn,max,mean:sum/(960*600*3),bad});
|
||||||
|
}
|
||||||
|
R.lens=false;
|
||||||
|
return results;
|
||||||
|
}""")
|
||||||
|
for r in result:
|
||||||
|
assert r['bad'] == 0, r
|
||||||
|
print('PASS plain cells, built geometry, ghosts, rain and all thermal palettes match legacy within 8', len(result), 'configurations; max', max(r['max'] for r in result))
|
||||||
|
assert pg.evaluate('GPU.overrideCount > 0 && GPU.overrideCount < R.bw*R.bh*0.25')
|
||||||
|
alpha = pg.evaluate('[R.gpuOn,GPU.ok,GPU.error,R.ctx.getImageData(10,10,1,1).data[3],worldPixelContext().getImageData(10,10,1,1).data[3],R.canvas.width,R.canvas.height]')
|
||||||
|
assert alpha[3:5] == [0,255], alpha
|
||||||
|
print('PASS most visible grains use GPU colouring; pixel reads include the opaque GPU world')
|
||||||
|
glow = pg.evaluate("""() => {
|
||||||
|
SET.fog=false; SET.lighting='off'; SET.glow='high'; R.lens=false;
|
||||||
|
spawnBar(BARFE,CAM[0]-30,CAM[1]+15,0,0,20);
|
||||||
|
const x=CAM[0]-64,y=CAM[1]-36;
|
||||||
|
setCell(y*W+x,MAGMA,1450,F_NAT);
|
||||||
|
flyers.push({x,y,m:COAL,rain:false});
|
||||||
|
SET.renderer='legacy';drawWorld();const a=worldPixelContext().getImageData(0,0,960,600).data;
|
||||||
|
SET.renderer='webgpu';drawWorld();const b=worldPixelContext().getImageData(0,0,960,600).data;
|
||||||
|
let max=0;for(let i=0;i<a.length;i++)if((i&3)!==3)max=Math.max(max,Math.abs(a[i]-b[i]));return max;
|
||||||
|
}""")
|
||||||
|
assert glow <= 8, f'Objects must retain the hot grain glow beneath them: max error {glow}'
|
||||||
|
print('PASS bars and flying grains preserve the glow behind them', glow)
|
||||||
|
# Fresh arrays, same dimensions: catches stale world/static textures after a load.
|
||||||
|
pg.evaluate('generateWorld(777, 640, 384, null); buildSky(); SET.renderer="webgpu"; SET.zoom=2; resizeRender(); snapCamera(); drawWorld();')
|
||||||
|
assert pg.evaluate('GPU.world===mat && GPU.colourTex.width===R.bw && GPU.ok')
|
||||||
|
parity = pg.evaluate("""() => {
|
||||||
|
const a=worldPixelContext().getImageData(0,0,960,600).data;
|
||||||
|
SET.renderer='legacy';drawWorld();const b=worldPixelContext().getImageData(0,0,960,600).data;
|
||||||
|
SET.renderer='webgpu'; let max=0,nonblack=0;
|
||||||
|
for(let i=0;i<a.length;i+=4){nonblack+=a[i]+a[i+1]+a[i+2]>0;for(let j=0;j<3;j++)max=Math.max(max,Math.abs(a[i+j]-b[i+j]));}
|
||||||
|
return {max,nonblack};
|
||||||
|
}""")
|
||||||
|
assert parity['max'] <= 8 and parity['nonblack'] > 1000, parity
|
||||||
|
print('PASS world replacement and zoom reallocate textures and draw the new world', parity)
|
||||||
|
pg.evaluate('SET.look="smooth"; drawWorld();')
|
||||||
|
assert pg.evaluate('!R.gpuOn && R.smoothOn && GPU.canvas.hidden')
|
||||||
|
print('PASS smooth keeps the legacy path')
|
||||||
|
pg.evaluate('SET.look="pixel"; SET.renderer="webgpu"; PERF_MARKS=true; drawWorld();')
|
||||||
|
pg.wait_for_function('!GPU.timingBusy')
|
||||||
|
assert pg.evaluate('!GPU.queries || GPU.gpuMs>=0')
|
||||||
|
assert pg.evaluate('perfMarks.override>=0 && perfMarks.upload>=0')
|
||||||
|
print('PASS separate override/upload clocks and optional GPU timestamps')
|
||||||
|
pg.evaluate('GPU.device.destroy()')
|
||||||
|
pg.wait_for_function('!GPU.ok')
|
||||||
|
assert pg.evaluate('SET.renderer==="legacy"')
|
||||||
|
pg.evaluate('drawWorld()')
|
||||||
|
print('PASS device loss restores the legacy renderer')
|
||||||
|
assert not errors, errors
|
||||||
|
print('PASS no page or WebGPU validation errors')
|
||||||
+10
-9
@@ -21,7 +21,7 @@ with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
|||||||
ev = pg.evaluate
|
ev = pg.evaluate
|
||||||
check('the smooth look is the default and is drawing', ev("SET.look === 'smooth' && SM.ok && R.smoothOn"), ev('[SET.look, SM.ok, SM.why, R.smoothOn]'))
|
check('the smooth look is the default and is drawing', ev("SET.look === 'smooth' && SM.ok && R.smoothOn"), ev('[SET.look, SM.ok, SM.why, R.smoothOn]'))
|
||||||
# the canvas shows the GPU image: sample a sky pixel and a ground pixel, they must differ and not be black
|
# the canvas shows the GPU image: sample a sky pixel and a ground pixel, they must differ and not be black
|
||||||
px = ev("""(() => { const c = R.ctx.getImageData(R.canvas.width >> 1, 4, 1, 1).data, g = worldToScreenPx(bot.x + 2, surfY[Math.floor(bot.x)] + 6), d = R.ctx.getImageData(Math.round(g.x), Math.round(g.y), 1, 1).data; return [Array.from(c), Array.from(d)]; })()""")
|
px = ev("""(() => { const c = worldPixelContext().getImageData(R.canvas.width >> 1, 4, 1, 1).data, g = worldToScreenPx(bot.x + 2, surfY[Math.floor(bot.x)] + 6), d = worldPixelContext().getImageData(Math.round(g.x), Math.round(g.y), 1, 1).data; return [Array.from(c), Array.from(d)]; })()""")
|
||||||
check('it draws sky and ground', sum(px[0][:3]) > 200 and px[0][:3] != px[1][:3], px)
|
check('it draws sky and ground', sum(px[0][:3]) > 200 and px[0][:3] != px[1][:3], px)
|
||||||
pg.screenshot(path=str(out / 'smooth.png'))
|
pg.screenshot(path=str(out / 'smooth.png'))
|
||||||
ev("player.lens = true"); pg.keyboard.press('h'); pg.wait_for_timeout(1500)
|
ev("player.lens = true"); pg.keyboard.press('h'); pg.wait_for_timeout(1500)
|
||||||
@@ -43,8 +43,9 @@ with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
|||||||
const i = y * W + x;
|
const i = y * W + x;
|
||||||
setCell(i, STONE, 22, F_NAT); bgm[i] = 0; noiseB[i] = 128;
|
setCell(i, STONE, 22, F_NAT); bgm[i] = 0; noiseB[i] = 128;
|
||||||
}
|
}
|
||||||
|
GPU.world = null; // The fixture changed static noise in place.
|
||||||
const fill = m => { for (let y = 100; y < 140; y++) for (let x = 100; x < 180; x++) setCell(y * W + x, m, 22); };
|
const fill = m => { for (let y = 100; y < 140; y++) for (let x = 100; x < 180; x++) setCell(y * W + x, m, 22); };
|
||||||
const at = (x, y) => { const c = R.px[(y - R.vy0) * R.bw + x - R.vx0]; return (c & 255) + (c >>> 8 & 255) + (c >>> 16 & 255); };
|
const at = (x, y) => { const p=worldToScreenPx(x+0.5,y+0.5), c=worldPixelContext().getImageData(p.x,p.y,1,1).data; return c[0]+c[1]+c[2]; };
|
||||||
const out = {};
|
const out = {};
|
||||||
for (const lens of [false, true]) {
|
for (const lens of [false, true]) {
|
||||||
R.lens = lens; SET.look = 'pixel'; fill(RSTONE); drawWorld();
|
R.lens = lens; SET.look = 'pixel'; fill(RSTONE); drawWorld();
|
||||||
@@ -59,7 +60,7 @@ with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
|||||||
fill(RSTONE); SET.look = 'smooth'; SM.scale = 1; drawWorld();
|
fill(RSTONE); SET.look = 'smooth'; SM.scale = 1; drawWorld();
|
||||||
const sample = (x, y) => {
|
const sample = (x, y) => {
|
||||||
const p = worldToScreenPx(x, y);
|
const p = worldToScreenPx(x, y);
|
||||||
const c = R.ctx.getImageData(Math.floor(p.x), Math.floor(p.y), 1, 1).data;
|
const c = worldPixelContext().getImageData(Math.floor(p.x), Math.floor(p.y), 1, 1).data;
|
||||||
return c[0] + c[1] + c[2];
|
return c[0] + c[1] + c[2];
|
||||||
};
|
};
|
||||||
out.smooth = [sample(140.5, 140.08), sample(140.5, 142.5)];
|
out.smooth = [sample(140.5, 140.08), sample(140.5, 142.5)];
|
||||||
@@ -140,8 +141,8 @@ with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
|||||||
const x=cell%W,y=(cell/W)|0,control=80*W+180;
|
const x=cell%W,y=(cell/W)|0,control=80*W+180;
|
||||||
bot.x=118;bot.y=78;snapCamera();
|
bot.x=118;bot.y=78;snapCamera();
|
||||||
setCell(cell,SAND,22,0);setCell(control,SAND,22,0);
|
setCell(cell,SAND,22,0);setCell(control,SAND,22,0);
|
||||||
noiseB[cell]=noiseB[control]=128;
|
noiseB[cell]=noiseB[control]=128; GPU.world=null;
|
||||||
const at=i=>R.px[(((i/W)|0)-R.vy0)*R.bw+i%W-R.vx0];
|
const at=i=>{ if(R.smoothOn) return R.px[(((i/W)|0)-R.vy0)*R.bw+i%W-R.vx0]; const p=worldToScreenPx(i%W+0.5,((i/W)|0)+0.5), c=worldPixelContext().getImageData(p.x,p.y,1,1).data; return c[0]|c[1]<<8|c[2]<<16; };
|
||||||
SET.look='pixel';drawWorld();const colors=[at(cell)&0xffffff,at(control)&0xffffff];
|
SET.look='pixel';drawWorld();const colors=[at(cell)&0xffffff,at(control)&0xffffff];
|
||||||
SET.look='smooth';drawWorld();const grainClass=at(cell)>>>24;
|
SET.look='smooth';drawWorld();const grainClass=at(cell)>>>24;
|
||||||
return {colors,grainClass};
|
return {colors,grainClass};
|
||||||
@@ -162,7 +163,7 @@ with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
|||||||
for(const look of ["pixel","smooth"]) {
|
for(const look of ["pixel","smooth"]) {
|
||||||
SET.look=look;SM.scale=1;drawWorld();
|
SET.look=look;SM.scale=1;drawWorld();
|
||||||
const bad=[], copper=c=>c[0]>c[1]*1.15 && c[1]>c[2]*1.1;
|
const bad=[], copper=c=>c[0]>c[1]*1.15 && c[1]>c[2]*1.1;
|
||||||
const sample=(x,y)=>{const p=worldToScreenPx(x,y);return Array.from(R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data);};
|
const sample=(x,y)=>{const p=worldToScreenPx(x,y);return Array.from(worldPixelContext().getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data);};
|
||||||
for(const j of JOIN_CASES.filter(j=>j.code===BP_SCREW)) {
|
for(const j of JOIN_CASES.filter(j=>j.code===BP_SCREW)) {
|
||||||
const slope=j.v===4 || j.v===6?-1:1, side=j.end?1:-1;
|
const slope=j.v===4 || j.v===6?-1:1, side=j.end?1:-1;
|
||||||
const joins=diagonalScrewJoins(j.bx,j.by,j.v);
|
const joins=diagonalScrewJoins(j.bx,j.by,j.v);
|
||||||
@@ -211,7 +212,7 @@ with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
|||||||
const b=j.by*BW+j.bx,nb=j.ny*BW+j.nx;
|
const b=j.by*BW+j.bx,nb=j.ny*BW+j.nx;
|
||||||
const plan=new Map([[b,[BP_SCREW,j.v]],[nb,[BP_SCREW,j.straight]]]);
|
const plan=new Map([[b,[BP_SCREW,j.v]],[nb,[BP_SCREW,j.straight]]]);
|
||||||
removeBlock(j.bx,j.by);removeBlock(j.nx,j.ny);
|
removeBlock(j.bx,j.by);removeBlock(j.nx,j.ny);
|
||||||
const sample=(x,y)=>{const p=worldToScreenPx(j.nx*4+x,j.ny*4+y);return Array.from(R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data).join(',');};
|
const sample=(x,y)=>{const p=worldToScreenPx(j.nx*4+x,j.ny*4+y);return Array.from(worldPixelContext().getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data).join(',');};
|
||||||
SM.scale=1;drawWorld();const bg=sample(ux,uy),empty=sample(2,2);
|
SM.scale=1;drawWorld();const bg=sample(ux,uy),empty=sample(2,2);
|
||||||
for(const [n,piece] of plan)ghostBlocks.set(n,piece);
|
for(const [n,piece] of plan)ghostBlocks.set(n,piece);
|
||||||
drawWorld();
|
drawWorld();
|
||||||
@@ -236,7 +237,7 @@ with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
|||||||
try {
|
try {
|
||||||
const j=JOIN_CASES.find(j=>j.v===5 && j.end===1 && !j.vertical && j.spot===2 && j.code===BP_SCREW);
|
const j=JOIN_CASES.find(j=>j.v===5 && j.end===1 && !j.vertical && j.spot===2 && j.code===BP_SCREW);
|
||||||
const bx=j.nx+1,by=j.ny;
|
const bx=j.nx+1,by=j.ny;
|
||||||
const sample=()=>{const p=worldToScreenPx(bx*4,by*4);return Array.from(R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),4*R.zp,4*R.zp).data).join(',');};
|
const sample=()=>{const p=worldToScreenPx(bx*4,by*4);return Array.from(worldPixelContext().getImageData(Math.floor(p.x),Math.floor(p.y),4*R.zp,4*R.zp).data).join(',');};
|
||||||
drawWorld();const plain=sample(),bad=[];
|
drawWorld();const plain=sample(),bad=[];
|
||||||
for(const m of [WOOD,LEAVES]) {
|
for(const m of [WOOD,LEAVES]) {
|
||||||
for(let y=by*4;y<by*4+4;y++)for(let x=bx*4;x<bx*4+4;x++)bgm[y*W+x]=m;
|
for(let y=by*4;y<by*4+4;y++)for(let x=bx*4;x<bx*4+4;x++)bgm[y*W+x]=m;
|
||||||
@@ -252,7 +253,7 @@ with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
|||||||
const j=JOIN_CASES.find(j=>j.v===4&&j.end===0&&j.code===BP_BELT&&j.spot===0);
|
const j=JOIN_CASES.find(j=>j.v===4&&j.end===0&&j.code===BP_BELT&&j.spot===0);
|
||||||
try {
|
try {
|
||||||
slopeDetailAt=()=>0;performance.now=()=>frozen;
|
slopeDetailAt=()=>0;performance.now=()=>frozen;
|
||||||
const sample=()=>{const p=worldToScreenPx(j.bx*BS+0.4,j.by*BS+2.5);return Array.from(R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data);};
|
const sample=()=>{const p=worldToScreenPx(j.bx*BS+0.4,j.by*BS+2.5);return Array.from(worldPixelContext().getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data);};
|
||||||
for(const look of ['pixel','smooth']) {
|
for(const look of ['pixel','smooth']) {
|
||||||
SET.look=look;SM.scale=1;drawWorld();const joined=sample();
|
SET.look=look;SM.scale=1;drawWorld();const joined=sample();
|
||||||
removeBlock(j.nx,j.ny);drawWorld();const free=sample();
|
removeBlock(j.nx,j.ny);drawWorld();const free=sample();
|
||||||
|
|||||||
+14
-5
@@ -29,7 +29,7 @@ function perfSim(names, scene, w, h) {
|
|||||||
return { total_ms: total, ticks: n, start, end: perfState(), steps,
|
return { total_ms: total, ticks: n, start, end: perfState(), steps,
|
||||||
rest_ms: total - Object.values(data).reduce((s, d) => s + d.ms / n, 0) };
|
rest_ms: total - Object.values(data).reduce((s, d) => s + d.ms / n, 0) };
|
||||||
}
|
}
|
||||||
function perfRender(lighting, overlays) {
|
async function perfRender(lighting, overlays) {
|
||||||
SET.lighting = lighting;
|
SET.lighting = lighting;
|
||||||
PERF_MARKS = true;
|
PERF_MARKS = true;
|
||||||
try {
|
try {
|
||||||
@@ -48,13 +48,22 @@ function perfRender(lighting, overlays) {
|
|||||||
// Exercise the actual overlay calls and their arguments/conditions in frame().
|
// Exercise the actual overlay calls and their arguments/conditions in frame().
|
||||||
PERF_MARKS = false;
|
PERF_MARKS = false;
|
||||||
for (let k = 0; k < 5; k++) frame(lastT + 1000 / 60);
|
for (let k = 0; k < 5; k++) frame(lastT + 1000 / 60);
|
||||||
const data = {}, restore = perfWrap(overlays, data);
|
const data = {}, restore = perfWrap(overlays, data), frameStart = performance.now();
|
||||||
try { for (let k = 0; k < n; k++) frame(lastT + 1000 / 60); }
|
try { for (let k = 0; k < n; k++) frame(lastT + 1000 / 60); }
|
||||||
finally { restore(); }
|
finally { restore(); }
|
||||||
return { drawWorld_ms: draw, computeLight_ms: light, marks_ms: marks,
|
const frame_ms = (performance.now() - frameStart) / n;
|
||||||
|
// One completed timestamp sample for this configuration, outside the CPU measurements.
|
||||||
|
let gpu_ms = null;
|
||||||
|
if (R.gpuOn && GPU.queries) {
|
||||||
|
await GPU.timingPromise;
|
||||||
|
PERF_MARKS = true; drawWorld();
|
||||||
|
await GPU.timingPromise;
|
||||||
|
PERF_MARKS = false; gpu_ms = GPU.gpuMs; marks.gpu = gpu_ms;
|
||||||
|
}
|
||||||
|
return { frame_ms, drawWorld_ms: draw, computeLight_ms: light, marks_ms: marks,
|
||||||
overlays: Object.fromEntries(Object.entries(data).map(([key, d]) => [key, { ms_per_frame: d.ms / n, calls: d.calls }])),
|
overlays: Object.fromEntries(Object.entries(data).map(([key, d]) => [key, { ms_per_frame: d.ms / n, calls: d.calls }])),
|
||||||
overlay_ms: Object.values(data).reduce((sum, d) => sum + d.ms / n, 0),
|
overlay_ms: Object.values(data).reduce((sum, d) => sum + d.ms / n, 0),
|
||||||
scale: SM.scale, actual_look: SET.look };
|
scale: SM.scale, actual_look: SET.look, renderer: R.gpuOn ? 'webgpu' : 'legacy', override_cells: GPU.overrideCount || 0, gpu_ms };
|
||||||
} finally { PERF_MARKS = false; }
|
} finally { PERF_MARKS = false; }
|
||||||
}
|
}
|
||||||
function perfRun(seconds) {
|
function perfRun(seconds) {
|
||||||
@@ -72,7 +81,7 @@ function perfRun(seconds) {
|
|||||||
requestAnimationFrame(frame);
|
requestAnimationFrame(frame);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.__perfHalt(); PERF_FRAME = null; GAME.paused = true;
|
window.__perfHalt(); PERF_FRAME = null; GAME.paused = true;
|
||||||
resolve({ samples, end: perfState(), scale: SM.scale, actual_look: SET.look });
|
resolve({ samples, end: perfState(), scale: SM.scale, actual_look: SET.look, renderer: R.gpuOn ? 'webgpu' : 'legacy', override_cells: GPU.overrideCount || 0, gpu_ms: R.gpuOn ? GPU.gpuMs : null });
|
||||||
}, seconds * 1000);
|
}, seconds * 1000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-4
@@ -54,14 +54,14 @@ def report(result, baseline=None):
|
|||||||
m = result['machine']
|
m = result['machine']
|
||||||
print(f"GPU: {m['renderer']}\nCores: {m['cores']} | isolated: {m['crossOriginIsolated']} | navigator.gpu: {m['webgpu']}")
|
print(f"GPU: {m['renderer']}\nCores: {m['cores']} | isolated: {m['crossOriginIsolated']} | navigator.gpu: {m['webgpu']}")
|
||||||
print(f"Viewport {result['options']['viewport']} | world {result['options']['world']} | seed 424242 | ms (CPU wall time)")
|
print(f"Viewport {result['options']['viewport']} | world {result['options']['world']} | seed 424242 | ms (CPU wall time)")
|
||||||
print('\nScene Look Zoom Light Grains World Light Prep Colour Upload Blit Overlays Scale')
|
print('\nScene Look Zoom Light Grains World Frame Light Prep Colour Upload Blit Overlays Scale')
|
||||||
for scene, data in result['scenes'].items():
|
for scene, data in result['scenes'].items():
|
||||||
for key, view in data['views'].items():
|
for key, view in data['views'].items():
|
||||||
look, zoom = key.split('/')
|
look, zoom = key.split('/')
|
||||||
for light, r in view['lighting'].items():
|
for light, r in view['lighting'].items():
|
||||||
p = r['marks_ms']
|
p = r['marks_ms']
|
||||||
print(f"{scene:8} {look:6} {zoom:>4} {light:>5} {view['grains']:6d} "
|
print(f"{scene:8} {look:6} {zoom:>4} {light:>5} {view['grains']:6d} "
|
||||||
f"{r['drawWorld_ms']:7.2f} {p['light']:6.2f} {p['prepare']:5.2f} {p['color']:6.2f} "
|
f"{r['drawWorld_ms']:7.2f} {r.get('frame_ms', 0):6.2f} {p['light']:6.2f} {p['prepare']:5.2f} {p['color']:6.2f} "
|
||||||
f"{p['upload']:6.2f} {p['blit']:6.2f} {r['overlay_ms']:8.3f} {r['scale']:5.2f}")
|
f"{p['upload']:6.2f} {p['blit']:6.2f} {r['overlay_ms']:8.3f} {r['scale']:5.2f}")
|
||||||
print('\nSimulation: average ms/tick (periodic work amortized over 240 ticks)')
|
print('\nSimulation: average ms/tick (periodic work amortized over 240 ticks)')
|
||||||
names = list(next(iter(result['scenes'].values()))['sim']['steps'])
|
names = list(next(iter(result['scenes'].values()))['sim']['steps'])
|
||||||
@@ -125,8 +125,10 @@ def main():
|
|||||||
options = dict(viewport=list(args.viewport), world=list(args.world), zooms=zooms,
|
options = dict(viewport=list(args.viewport), world=list(args.world), zooms=zooms,
|
||||||
looks=looks, scenes=scenes, seconds=args.seconds)
|
looks=looks, scenes=scenes, seconds=args.seconds)
|
||||||
baseline = json.loads(args.compare.read_text()) if args.compare else None
|
baseline = json.loads(args.compare.read_text()) if args.compare else None
|
||||||
if baseline and (baseline.get('schema') != 1 or baseline['options'] != options):
|
if baseline and (baseline.get('schema') != 1
|
||||||
ap.error('baseline schema/options differ; use the same matrix, dimensions and seconds')
|
or any(baseline['options'].get(k) != v for k, v in options.items() if k not in ('looks', 'scenes', 'zooms'))
|
||||||
|
or any(not set(options[k]).issubset(baseline['options'][k]) for k in ('looks', 'scenes', 'zooms'))):
|
||||||
|
ap.error('baseline schema/options differ; use a subset of the baseline matrix with the same dimensions and seconds')
|
||||||
ensure_playwright()
|
ensure_playwright()
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT, check=True, stdout=sys.stderr)
|
subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT, check=True, stdout=sys.stderr)
|
||||||
@@ -154,6 +156,9 @@ def main():
|
|||||||
const id = raf(t => { pending.delete(id); f(t); }); pending.add(id); return id;
|
const id = raf(t => { pending.delete(id); f(t); }); pending.add(id); return id;
|
||||||
};''')
|
};''')
|
||||||
pg.goto(url)
|
pg.goto(url)
|
||||||
|
pg.evaluate('CAPS_CHECK')
|
||||||
|
if not pg.evaluate('GPU.ok'):
|
||||||
|
raise RuntimeError('WebGPU unavailable: ' + pg.evaluate('GPU.error'))
|
||||||
pg.add_script_tag(content=(ROOT / 'tools/perf-scenes.js').read_text())
|
pg.add_script_tag(content=(ROOT / 'tools/perf-scenes.js').read_text())
|
||||||
pg.add_script_tag(content=(ROOT / 'tools/perf-measure.js').read_text())
|
pg.add_script_tag(content=(ROOT / 'tools/perf-measure.js').read_text())
|
||||||
result['machine'] = pg.evaluate('''() => {
|
result['machine'] = pg.evaluate('''() => {
|
||||||
@@ -162,6 +167,7 @@ def main():
|
|||||||
return { renderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER),
|
return { renderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER),
|
||||||
vendor: dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR),
|
vendor: dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR),
|
||||||
cores: navigator.hardwareConcurrency, crossOriginIsolated, webgpu: !!navigator.gpu,
|
cores: navigator.hardwareConcurrency, crossOriginIsolated, webgpu: !!navigator.gpu,
|
||||||
|
adapter: CAPS.adapterInfo, timestamp_query: !!GPU.queries,
|
||||||
user_agent: navigator.userAgent, dpr: devicePixelRatio };
|
user_agent: navigator.userAgent, dpr: devicePixelRatio };
|
||||||
}''')
|
}''')
|
||||||
if not result['machine']['crossOriginIsolated']:
|
if not result['machine']['crossOriginIsolated']:
|
||||||
@@ -185,6 +191,8 @@ def main():
|
|||||||
view['lighting'][light] = pg.evaluate('([l,o]) => perfRender(l,o)', [light, overlays])
|
view['lighting'][light] = pg.evaluate('([l,o]) => perfRender(l,o)', [light, overlays])
|
||||||
if view['lighting']['full']['actual_look'] != look or view['lighting']['off']['actual_look'] != look:
|
if view['lighting']['full']['actual_look'] != look or view['lighting']['off']['actual_look'] != look:
|
||||||
raise RuntimeError('Look fell back while measuring')
|
raise RuntimeError('Look fell back while measuring')
|
||||||
|
if look == 'pixel' and any(r['renderer'] != 'webgpu' for r in view['lighting'].values()):
|
||||||
|
raise RuntimeError('WebGPU fell back while measuring: ' + pg.evaluate('GPU.error'))
|
||||||
data['views'][f'{look}/{zoom:g}'] = view
|
data['views'][f'{look}/{zoom:g}'] = view
|
||||||
for look in looks:
|
for look in looks:
|
||||||
for zoom in [2, 6]:
|
for zoom in [2, 6]:
|
||||||
@@ -193,6 +201,8 @@ def main():
|
|||||||
r = pg.evaluate('seconds => perfRun(seconds)', args.seconds)
|
r = pg.evaluate('seconds => perfRun(seconds)', args.seconds)
|
||||||
if r['actual_look'] != look:
|
if r['actual_look'] != look:
|
||||||
raise RuntimeError('Look fell back during real run')
|
raise RuntimeError('Look fell back during real run')
|
||||||
|
if look == 'pixel' and r['renderer'] != 'webgpu':
|
||||||
|
raise RuntimeError('WebGPU fell back during real run: ' + pg.evaluate('GPU.error'))
|
||||||
for key in ['interval', 'work', 'sim']:
|
for key in ['interval', 'work', 'sim']:
|
||||||
r[key + '_ms'] = quantiles([s[key] for s in r['samples']])
|
r[key + '_ms'] = quantiles([s[key] for s in r['samples']])
|
||||||
data['runs'][f'{look}/{zoom}'] = r
|
data['runs'][f'{look}/{zoom}'] = r
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Paused comparison scene: ordinary grains, fog, heat, trees and exceptional geometry.
|
||||||
|
const cx = (W / 3) & ~3, cy = 180;
|
||||||
|
window.CAM = [cx, cy];
|
||||||
|
SET.fog = true; SET.shake = false; SET.cameraLead = false;
|
||||||
|
weather.next = 1e15; R.rain = 0;
|
||||||
|
for (let y = cy - 52; y < cy + 52; y++) for (let x = cx - 90; x < cx + 90; x++) {
|
||||||
|
const i = y * W + x;
|
||||||
|
setCell(i, y >= cy + 24 ? STONE : EMPTY, 20, y >= cy + 24 ? F_NAT : 0);
|
||||||
|
bgm[i] = part[i] = mach[i] = occ[i] = 0; blockP[(y >> 2) * BW + (x >> 2)] = 0;
|
||||||
|
}
|
||||||
|
for (let x = cx - 90; x < cx + 90; x++) { surfY[x] = cy + 16; dirtY[x] = cy + 30; graniteY[x] = cy + 38; basaltY[x] = cy + 46; }
|
||||||
|
const fill = (x0,y0,w,h,m,T=20) => { for(let y=y0;y<y0+h;y++)for(let x=x0;x<x0+w;x++)setCell(y*W+x,m,T,F_NAT); };
|
||||||
|
fill(cx-62,cy+6,22,16,WATER); fill(cx-34,cy+11,16,11,MAGMA,1450);
|
||||||
|
fill(cx-33,cy+3,8,8,FIRE,900); fill(cx-12,cy+17,15,6,SAND);
|
||||||
|
fill(cx-10,cy+24,12,5,STONE,1100);
|
||||||
|
for(let y=cy-20;y<cy+16;y++)for(let x=cx-58;x<cx-54;x++)bgm[y*W+x]=WOOD;
|
||||||
|
for(let y=cy-28;y<cy-12;y++)for(let x=cx-68;x<cx-44;x++)if((x-cx+56)**2+(y-cy+20)**2<130)bgm[y*W+x]=LEAVES;
|
||||||
|
for(let x=cx-40;x<cx-34;x++)digp[(cy+24)*W+x]=160;
|
||||||
|
const place=(code,v,dx,dy)=>{const e=placeBlock(code,v,(cx+dx)>>2,(cy+dy)>>2,true);if(e)throw Error(e);};
|
||||||
|
for(let dx=-36;dx<-12;dx+=4)place(BP_BELT,1,dx,-12);
|
||||||
|
place(BP_SCREW,1,-4,-12); place(BP_SCREW,4,0,-12);
|
||||||
|
place(BP_BELT_UP,1,16,-12);place(BP_BELT_UP,1,20,-16);
|
||||||
|
place(BP_PIPE,0,32,-12);place(BP_PIPE,0,36,-12);
|
||||||
|
addMold('ingot',(cx+8)>>2,(cy+8)>>2,RSTONE,true);
|
||||||
|
spawnBar(BARCU,cx-25,cy-16,0,0,700);
|
||||||
|
flyers.push({x:cx-8,y:cy-18,m:SAND,rain:false});
|
||||||
|
ghostBlocks.set(((cy-4)>>2)*BW+((cx-4)>>2),[BP_WALL,RSTONE]);
|
||||||
|
for(let j=0;j<FH;j++)for(let i=0;i<FW;i++)fog[j*FW+i]=Math.max(0,Math.min(255,(cx+72-i*FOG)*5));
|
||||||
|
updateRoof(); rebuildPower();
|
||||||
+7
-1
@@ -13,6 +13,7 @@ code and the bot is moved there before the shot. SX, SY are the bot's starting p
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
--look pixel|smooth which look to draw (default: pixel; use smooth only to check graphics)
|
--look pixel|smooth which look to draw (default: pixel; use smooth only to check graphics)
|
||||||
|
--renderer webgpu|legacy pixel renderer (default: webgpu)
|
||||||
--seed N world seed (default 424242)
|
--seed N world seed (default 424242)
|
||||||
--size WxH world size in grains (default 2048x1024)
|
--size WxH world size in grains (default 2048x1024)
|
||||||
--setup FILE.js code to run once the world is ready (see tools/scenes/ for examples)
|
--setup FILE.js code to run once the world is ready (see tools/scenes/ for examples)
|
||||||
@@ -51,6 +52,7 @@ def main():
|
|||||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
ap.add_argument('out')
|
ap.add_argument('out')
|
||||||
ap.add_argument('--look', choices=['pixel', 'smooth'], default='pixel')
|
ap.add_argument('--look', choices=['pixel', 'smooth'], default='pixel')
|
||||||
|
ap.add_argument('--renderer', choices=['webgpu', 'legacy'], default='webgpu')
|
||||||
ap.add_argument('--seed', type=int, default=424242)
|
ap.add_argument('--seed', type=int, default=424242)
|
||||||
ap.add_argument('--size', default='2048x1024')
|
ap.add_argument('--size', default='2048x1024')
|
||||||
ap.add_argument('--setup')
|
ap.add_argument('--setup')
|
||||||
@@ -75,6 +77,8 @@ def main():
|
|||||||
pg.add_init_script(FREEZE if a.freeze else '')
|
pg.add_init_script(FREEZE if a.freeze else '')
|
||||||
pg.goto(url)
|
pg.goto(url)
|
||||||
pg.evaluate("SET.fog = false")
|
pg.evaluate("SET.fog = false")
|
||||||
|
pg.evaluate('CAPS_CHECK')
|
||||||
|
pg.evaluate('(r) => { SET.renderer = r; if (r === \'webgpu\' && !GPU.ok) throw Error(GPU.error || \'WebGPU unavailable\'); }', a.renderer)
|
||||||
pg.wait_for_timeout(300)
|
pg.wait_for_timeout(300)
|
||||||
# pause the moment the world exists, so the shot depends only on the seed, the setup
|
# pause the moment the world exists, so the shot depends only on the seed, the setup
|
||||||
# code and --ticks (simTick is deterministic), not on how fast the browser ran
|
# code and --ticks (simTick is deterministic), not on how fast the browser ran
|
||||||
@@ -99,6 +103,8 @@ def main():
|
|||||||
pg.evaluate("() => { window.__frozenNow = 5000; window.requestAnimationFrame = () => 0; }")
|
pg.evaluate("() => { window.__frozenNow = 5000; window.requestAnimationFrame = () => 0; }")
|
||||||
pg.wait_for_timeout(100)
|
pg.wait_for_timeout(100)
|
||||||
pg.evaluate("() => { R.frame = 999; if (typeof SM !== 'undefined') SM.t0 = 0; frame(lastRender + 1000); }")
|
pg.evaluate("() => { R.frame = 999; if (typeof SM !== 'undefined') SM.t0 = 0; frame(lastRender + 1000); }")
|
||||||
|
if a.look == 'pixel' and a.renderer == 'webgpu' and not pg.evaluate('R.gpuOn'):
|
||||||
|
raise RuntimeError('WebGPU fell back: ' + pg.evaluate('GPU.error'))
|
||||||
kw = {}
|
kw = {}
|
||||||
if a.clip:
|
if a.clip:
|
||||||
x, y, w, h = (float(v) for v in a.clip.split(','))
|
x, y, w, h = (float(v) for v in a.clip.split(','))
|
||||||
@@ -107,7 +113,7 @@ def main():
|
|||||||
pg.screenshot(path=a.out, **kw)
|
pg.screenshot(path=a.out, **kw)
|
||||||
note = pg.evaluate("SET.look === 'smooth' ? (SM.ok ? 'smooth look' : 'smooth look FAILED: ' + SM.why) : 'pixel look'")
|
note = pg.evaluate("SET.look === 'smooth' ? (SM.ok ? 'smooth look' : 'smooth look FAILED: ' + SM.why) : 'pixel look'")
|
||||||
b.close()
|
b.close()
|
||||||
print(f'saved {a.out} ({note})')
|
print(f"saved {a.out} ({note}, {a.renderer if a.look == 'pixel' else 'legacy'})")
|
||||||
for e in errors:
|
for e in errors:
|
||||||
print('ERROR', e)
|
print('ERROR', e)
|
||||||
sys.exit(1 if errors else 0)
|
sys.exit(1 if errors else 0)
|
||||||
|
|||||||
+3
-2
@@ -38,6 +38,7 @@ def main():
|
|||||||
pg.on('pageerror', lambda e: errors.append(str(e)))
|
pg.on('pageerror', lambda e: errors.append(str(e)))
|
||||||
pg.add_init_script('window.requestAnimationFrame = () => 0;')
|
pg.add_init_script('window.requestAnimationFrame = () => 0;')
|
||||||
pg.goto(url)
|
pg.goto(url)
|
||||||
|
pg.evaluate('CAPS_CHECK')
|
||||||
pg.add_script_tag(content=(ROOT / 'tools/perf-scenes.js').read_text())
|
pg.add_script_tag(content=(ROOT / 'tools/perf-scenes.js').read_text())
|
||||||
assert pg.evaluate('crossOriginIsolated')
|
assert pg.evaluate('crossOriginIsolated')
|
||||||
# Compare to the unwrapped functions, and hash the byte representation of all grid state.
|
# Compare to the unwrapped functions, and hash the byte representation of all grid state.
|
||||||
@@ -66,8 +67,8 @@ def main():
|
|||||||
performance.now = () => 5000;
|
performance.now = () => 5000;
|
||||||
R.rain = rainAt(Math.max(0, Math.min(W - 1, Math.floor(R.camX + R.bw / 2))));
|
R.rain = rainAt(Math.max(0, Math.min(W - 1, Math.floor(R.camX + R.bw / 2))));
|
||||||
try {
|
try {
|
||||||
PERF_MARKS = false; drawWorld(); const a = R.canvas.toDataURL();
|
PERF_MARKS = false; drawWorld(); const a = worldPixelContext().canvas.toDataURL();
|
||||||
PERF_MARKS = true; drawWorld(); const b = R.canvas.toDataURL();
|
PERF_MARKS = true; drawWorld(); const b = worldPixelContext().canvas.toDataURL();
|
||||||
return a === b;
|
return a === b;
|
||||||
} finally { PERF_MARKS = false; performance.now = now; }
|
} finally { PERF_MARKS = false; performance.now = now; }
|
||||||
}''', look)
|
}''', look)
|
||||||
|
|||||||
Reference in New Issue
Block a user