Multi-core simulation: grain movement and heat across a worker pool (t88 phase 3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 03:23:43 -04:00
co-authored by Claude Opus 5.5
parent 20963c776e
commit 7468d00efb
35 changed files with 578 additions and 113 deletions
+19 -8
View File
@@ -244,9 +244,9 @@ Starter furnace designs and the milestone list shown in the notebook.
- functions: smelterBlueprint
- variables: STARTER_DESIGNS, MILESTONES
### src/sim/world.js (113 lines)
### src/sim/world.js (123 lines)
World state: the per-cell typed arrays, alloc(), ambient temperature by depth, waking.
- functions: emit, bagCap, discover, alloc, depthU, depthY, meanSurface, computeAmbient, fillNoise, renderDirtyXY, renderDirtyIdx, wakeXY, wakeHeatXY, wakeIdx, wakeHeatIdx
- functions: emit, bagCap, discover, sharedArray, alloc, depthU, depthY, meanSurface, computeAmbient, fillNoise, renderDirtyXY, renderDirtyChunk, renderDirtyIdx, wakeXY, wakeHeatXY, wakeIdx, wakeHeatIdx
- variables: W, H, N, CW, CH, NCH, mat, temp, life, flags, stamp, part, mach, digp, noiseB, airS, netId, isPow, occ, surfY, dirtY, graniteY, basaltY, magmaY, roofY, AMB, worldSeed, surfBase, underD, shallowD, lapse, BW, BH, NB, blockP, blockV, ents, bgm, flyers, ghostBlocks, ghostMolds, renderDirty, awake, hot, hotLvl, powderArea, roofDirty, loadWakeOrder, loadWakePending, loadWakeIndex, tick, tickStamp, machines, molds, nets, pcells, powerDirty, beltL, beltR, screwLists, brokenMachines, ghostMachines, ghostBlocked, EV, bot, bag, bagTotal, player, seen, ledger, stats
### src/sim/fog.js (52 lines)
@@ -254,12 +254,12 @@ Exploration only: never changes grains or the random stream.
- functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll
- variables: FOG, FOG_SENSE, FOG_SENSE_FADE, fog, FW, FH
### src/sim/cells.js (227 lines)
### src/sim/cells.js (223 lines)
Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids and gases (simMove).
- functions: setCell, swapCells, move, enter, enterFree, powder, passable, liquid, gas, wakeBotPowder, simMove
- functions: setCell, swapCells, swapCellData, move, enter, enterFree, powder, passable, liquid, liquidSearch, gas, wakeBotPowder, simMove, moveChunk
- variables: SORTP, SORTP_FUEL, LSTEP, LFILM, CORNER_SEAL
### src/sim/heat.js (271 lines)
### src/sim/heat.js (266 lines)
Heat: conduction, melting and freezing, reactions, fire and air, steel.
- functions: curAir, airflowAt, alloyMix, airy, hasO2, condPair, heatPass, heatChunk, neighborDir, react, char, quench, burningNear, burn, steelCheck
- variables: POROUS, SOIL, HOTLIQ, REACT_MIN, ALLOY
@@ -369,6 +369,11 @@ Weather: clouds from steam, rain and snow, and the world water count.
- functions: wrnd, resetWeather, rainGap, worldWater, absorbSteam, spawnRain, rainAt, weatherTick
- variables: WBAND, WS, weather
### src/sim/workers.js (153 lines)
The same chunk schedule runs inline or in workers. Workers only own a pass at a time.
- functions: threadWorld, threadBind, threadContext, resetChunkWork, inlineChunkWork, chunkEvent, chunkWake, chunkHeat, chunkSeed, runChunks, mergeChunkWork, chunkPasses, threadParams, threadSetParams, threadSlot, threadExecute, threadPass, threadWait, threadFailure, threadMessage
- variables: simPool, chunkWork, inlineWork
### src/sim/tick.js (29 lines)
simTick: one simulation step, in order.
- functions: simTick
@@ -451,7 +456,7 @@ Field mip levels preserve the legacy trilinear AO sample at level 2.2.
- functions: gpuSmoothInit, gpuSmoothTextures, gpuSmoothBindings, gpuSmoothUpload, gpuSmoothMips
- variables: GPU_FIELD_MIP
### src/render/gpu/device.js (89 lines)
### src/render/gpu/device.js (90 lines)
All pipelines compile during startup; rendering never falls back.
- functions: gpuLimitsOK, gpuInit, gpuFail, gpuRecover, gpuWarmWorld, worldPixelContext
- variables: GPU_LIMITS, GPU
@@ -492,6 +497,11 @@ Game shell (src/ui/): browser only. This file: settings, the GAME state object,
- functions: loadSettings, saveSettings, el, esc, rgbOf, fmtT, toast, setStatus
- variables: SET_DEFAULT, SET, TOOLS, ICONS, BAG_ORDER, GAME, keys, hudDirty, $
### src/ui/threads.js (39 lines)
Browser transport only; the simulation owns the schedule and shared state.
- functions: startThreads
- variables: threadStarting
### src/ui/title.js (77 lines)
Startup capabilities and title art.
- functions: checkCapabilities, renderCapabilityNotice, canStartGame, buildTitleCells, drawTitle
@@ -511,7 +521,7 @@ Shared player state; the world save remains a complete standalone fallback.
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
### src/ui/settings.js (123 lines)
### src/ui/settings.js (124 lines)
The settings screen.
- functions: openSettings, renderSettings, applySettings, toggleFullscreen, confirmWipe
- variables: SET_TABS, setTab
@@ -607,7 +617,7 @@ Shared world colours, minimap and full map.
Browser-only clocks; simulation code stays usable without a browser or a clock.
- variables: PERF_MARKS, PERF_FRAME, perfMarks, perfTimes, perfLast
### src/ui/loop.js (143 lines)
### src/ui/loop.js (149 lines)
The frame loop and boot.
- functions: scanVisible, frame, boot
- variables: lastT, acc, lastRender, frameMs, simMs, fpsSm, ambientT
@@ -656,6 +666,7 @@ The frame loop and boot.
- `test/t_slopes.js`: Sloped walls: flow, sealed joints, building, refunds and persistence.
- `test/t_smooth.py`: The smooth look: the WebGPU pass compiles and draws, and the thermal lens still works.
- `test/t_stone.js`: Uses for waste rock and sand: glass bricks from a fan furnace, the Saw, the silo and builders' orders.
- `test/t_threads.js`: Shared allocation, checkerboard independence, and real worker-count determinism.
- `test/t_water.js`: Water conservation, settling, ledge pouring, boundaries, pipes and phase changes. Tall worlds keep geothermal heat out of the ponds.
- `test/t_weather.js`: Weather: no more springs; steam rises into clouds and rains back; showers; snow when it is freezing.
- `test/t_world.js`: Trees from acorns, charcoal, brick, density, water/lava/snow/ice, full-bag refunds, drill reach, ballistic blowing, snowy mountains.
+15 -3
View File
@@ -49,7 +49,7 @@ done(); // prints ALL PASS or N FAILED, sets the
- A new suite named `test/t_<topic>.js` is picked up by `run_all.py` automatically.
**Determinism.** The simulation's randomness comes from seeded generators. The same code and the
same test give the same output every run, apart from lines with timings in ms. To prove a
same test give outputs identical across thread counts and between runs, apart from lines with timings in ms. To prove a
refactor changed nothing, save every suite's output before and after, and diff them:
```sh
@@ -58,6 +58,17 @@ for t in test/t_*.js; do node $t > /tmp/after/$(basename $t).txt; done # afte
diff -r /tmp/before /tmp/after
```
`test/t_threads.js` runs the real shared-memory scheduler inline and with 1, 2 and 8
Node workers, including repeats, comparing all mutable cell/chunk bytes and JS side effects.
The browser creates up to `min(16, hardwareConcurrency - 2, CPU workers setting)` helpers.
Small passes run inline; larger passes use batches to amortize dispatch. Wakes and heat flags
merge after each pass; long liquid searches are deferred until that merge. Global machine and
weather randomness remains separate from the chunk streams. World generation uses the same
inline schedule while settling. The t88 re-baseline changes worldgen material hashes and the
seeded sand pile bounds in `t_corner_seal.js`; the heat oracle now supports pass outputs.
The conveyor fixture uses a wider feed pile, the screen fixture warms longer, and `t_load.js`
checks waking within eight ticks separately from the probabilistic consumption of fuel.
## Browser tests
- They need the `.venv` from README.md → Develop; plain `python3` switches to it on its own.
@@ -73,7 +84,8 @@ diff -r /tmp/before /tmp/after
both looks at zoom 12 and 2. ImageMagick decodes RGB; the per-channel tolerance is 8,
mean RGB error must stay below 0.5/255 and fewer than 0.3% of pixels may exceed 8
(smooth AO/edge rounding). These are frozen pre-removal legacy images, not generated
from the new renderer. Inspect the matching `shots/golden-*.png` after visible changes.
from the new renderer. The compressed `golden-world-640x*.json.gz` fixtures retain the
Phase 2 world bytes that produced them, isolating renderer checks from worldgen RNG changes. Inspect the matching `shots/golden-*.png` after visible changes.
- `test/t_gpu.py` checks default limits (four storage textures per stage), dirty-upload
readback after 240 busy ticks with scrolling and both looks, exact dirty/full image equality
with a frozen clock, world replacement, transparent overlays, drill contours, shader compilation timing, successful device recreation and failed recovery with
@@ -98,7 +110,7 @@ diff -r /tmp/before /tmp/after
- `python3 tools/caps.py` prints isolation, actual WebGPU adapter info and the GL renderer for
Chromium and installed Firefox. Real mode requires a hardware Chromium adapter.
- `test/t_caps.py` checks absent, null, rejected and insufficient-limit adapters, notices and
the mandatory WebGPU startup gate. Threads remain optional until Phase 3. Firefox smoke
the mandatory WebGPU startup gate. Cross-origin isolation is required for threads. Firefox smoke
checks that a browser without WebGPU shows the notice and cannot start a world.
- Install Firefox with `.venv/bin/python -m playwright install firefox`.
`python3 test/firefox_smoke.py` checks isolation and the WebGPU notice, starts a pixel world
+82
View File
@@ -4,6 +4,88 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details.
## 2026-10-28: t88 phase 3 resume, final review and verification (Codex)
- Preserved the interrupted uncommitted implementation and reviewed the schedule, shared
writes, deferred effects, startup gate, test changes and profiler against the Phase 3 brief.
- Reproduced a deferred-liquid bug before fixing it: replaying liquid() rolled the movement
chance twice. Extracted liquidSearch() so the deferred search resumes at its exact direction
and distance without another random draw; removed the now-unneeded RNG replay records.
Added a failing-then-passing regression and a long water shelf to the repeated 320-tick
inline/1/2/8-worker and reversed-order fixture.
- Chunk passes now skip per-cell renderDirty stores in wakeXY/wakeHeatXY; the scanned chunk
and write halo provide their invalidation. Outside passes, explicit invalidation remains.
- All headless suites passed. Initial sandboxed browser gate failed (no GPU device); an
elevated capability check confirms RTX 5070 WebGPU, isolation and 24 cores. Firefox still
has no adapter and correctly requires WebGPU. Final host-GPU browser gate in progress.
- Files additionally changed on resume: sim/{cells,workers,world}, test/t_threads.js,
PROJECT_MAP and tracking. Full fresh profile, two-minute live run and screenshot inspection
pending. No commit; DESIGN and owner's Context untouched.
## 2026-10-27: t88 phase 3, multi-core simulation (Codex; verification in progress)
- SharedArrayBuffer allocation for world arrays (ArrayBuffer only when unavailable in Node).
Four (chunkX&1, chunkY&1) movement passes then four heat passes. Each chunk scopes RS to
hash(world seed,tick,chunk,pass), restoring the global machine/worldgen RNG afterwards.
Long liquid reads outside a 24-cell horizontal halo are deferred and replayed in chunk order;
movement writes reach at most 8 cells, so parallel read/write regions cannot overlap.
- Pass-local wake/heat flags merge by maximum; per-chunk heat outputs merge first. Discoveries,
events, fuel ignition counts, wheel flow, cloud moisture, broken machines and partLost merge
in chunk order. Flyers/launchGrain remain reachable only from main-thread transport, not these
cell passes; their existing state is included in the determinism hash. Direct shared writes
are disjoint cell/powder outputs or idempotent render/roof bits.
- Browser Blob pool uses min(16,hardwareConcurrency-2,setting), at least one. Main thread owns
machines, transport and game logic, processes a batch in each pass, then polls Atomics.load.
No Atomics.wait on the document: it is prohibited by
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait
and https://tc39.es/ecma262/2024/multipage/structured-data.html. Polling is synchronous and
occupies the main core; it preserves atomic ticks so input/save/render cannot race workers.
No whole-simulation worker or asynchronous rendering overlap. Worker timeouts/errors stop
frame simulation. Small jobs run inline and larger batches use only enough helpers to
amortize dispatch (32 movement or 8 heat chunks per helper). Pool setting applies on load.
- Invalidation: remove swap's duplicate hot marks, gas marks and repeated coordinate-to-chunk
function calls. Each scanned movement chunk marks its halo once; heat marks conduction edges.
wake functions reuse their already computed chunk index for one direct render bit store;
rare swaps/building/fog/dig/background writes retain explicit marks. Paused clean draws upload
zero world cells. GPU equality passes without changing any tolerance.
- Measurements so far: fresh Phase 2 settled/factory/heat 3.537/4.077/5.824 ms/tick vs frozen
baseline 3.715/3.657/4.938. Isolated removal of hot dirty calls in a temporary Phase 2 copy
gave 3.530/3.921/5.936: it did NOT recover the full 15% on its own. Do not claim it did.
Initial unbatched pool factory 4.21; batched trial 3.04 ms/tick. Full final matrix pending.
- Final full matrix (`/tmp/t88-phase3-final.json`, `/tmp/t88-phase3-final-perf.txt`, 218.0 s),
RTX 5070, 24 logical cores, 1920x1080, 2048x1024 world. Pool 16 helpers, peak active 4
(settled) / 5 (factory, heat), plus main-thread batch. All values below are ms.
settled: baseline 3.715 -> wall/main occupied 1.897 (1.96x); main compute excluding polling 1.624, polling 0.272.
factory: baseline 3.657 -> wall/main occupied 2.495 (1.47x); main compute excluding polling 2.037, polling 0.458.
heat: baseline 4.938 -> wall/main occupied 2.649 (1.86x); main compute excluding polling 2.236, polling 0.412.
Real-loop CPU work p50/p95, zoom 2 / zoom 6; pixel then smooth:
settled: pixel 7.08/8.58 / 2.77/3.83; smooth 8.46/9.98 / 2.98/4.13.
factory: pixel 8.48/10.05 / 4.96/5.95; smooth 9.59/10.80 / 5.13/6.12.
heat: pixel 8.53/10.56 / 4.88/6.03; smooth 10.05/11.51 / 5.17/6.21.
Frame intervals p50/p95 are 16.67/16.67 for all these runs (60 fps). Tick wall time and
occupied main-thread time are equal: polling blocks this core; rendering does not overlap.
The final threaded sim meets the baseline target in every scene; standalone dirty-only
recovery remains unproven/unmet (numbers above). Recommend a future whole-simulation worker
to remove the remaining 1.90-2.65 ms from the main thread; it would require an explicit
ownership/input/save boundary and is outside this phase.
- Re-baseline: three worldgen material hashes and seeded pile bounds (57x17 -> 46x19 grains).
The heat oracle retains its unoptimized per-cell computation, updated for buffered pass
outputs, and is compared against all four new passes. Same throughput assertions retained:
conveyor feed heap widened, downhill-screen warm-up lengthened; load-wake deadline kept at
eight ticks, fuel consumption checked later instead of demanding a random burn on that tick.
Frozen renderer PNGs and tolerances unchanged: original Phase 2 world bytes captured from the
Phase 2 simulation (only invalidation edits) in the temporary copy, compressed as golden-world fixtures, so changed
terrain doesn't get counted as changed renderer pixels.
- Headless suites pass; repeated inline/1/2/8 real-worker hashes pass (world arrays + JS effects).
Extended test passes 320 busy ticks including stamp wrap, reversed chunk order, Node fallback.
First full browser gate: gameplay 361, smooth 28, console 18, capabilities 7, GPU equality 12,
light 71 and Firefox 2 pass; golden sky failed due changed worldgen terrain. Frozen-input
golden rerun, final browser gate, full profile and 2-minute play-through pending.
- Files: sim/{world,cells,heat,workers,fog,machines,weather}, ui/{threads,title,state,settings,loop},
render/gpu/device, build.py/manifest; harness, threads/heat/load/corner/conveyor/screen/capability/
golden tests and fixtures; profiler, README, TESTING, PROJECT_MAP and t88 tracking.
No commit; DESIGN and owner's task context untouched. Phase 3 remains in progress.
## 2026-10-27: t88 phase 2d, required WebGPU and dirty uploads (Codex)
- Per-32x32-chunk `renderDirty` marks cell writes, heat with its conduction halo, stationary