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
+7 -1
View File
@@ -40,7 +40,13 @@ function frame(now) {
GAME.padRun = pm > 0.92 ? GAME.padRun + 1 : 0;
botStep(ix, iy, keys.has('Shift') || GAME.padRun > 24);
toolTick();
const s0 = performance.now(); simTick(); const elapsed = performance.now() - s0;
const s0 = performance.now();
try { simTick(); }
catch (e) {
GAME.paused = true; acc = 0; console.error(e);
toast('The world stopped updating. Reload this world to try again.', {big:true}); break;
}
const elapsed = performance.now() - s0;
simMs = simMs * 0.9 + elapsed * 0.1; frameSim += elapsed; frameSteps++;
if (devPaused) devSteps--; else acc -= step;
steps++;
+1
View File
@@ -29,6 +29,7 @@ const SET_TABS = {
{ note: 'Upscaling (DLSS, FSR), frame generation and hardware ray tracing need native graphics APIs that browsers do not expose. They are planned for the compiled build.' },
],
Simulation: [
{ k: 'workerCap', label: 'CPU workers', d: 'Maximum helpers for grains and heat. Applies when you next load a world.', type: 'range', min: 1, max: 16, step: 1, fmt: v => v },
{ k: 'simSpeed', label: 'Simulation speed', type: 'select', opts: [[0.5, '0.5×'], [1, '1×'], [2, '2×']] },
{ k: 'maxSteps', label: 'Catch-up steps per frame', d: 'How hard the game tries to keep real time when frames are slow.', type: 'range', min: 1, max: 4, step: 1, fmt: v => v },
{ note: 'The whole world keeps simulating, wherever you are. Your factories keep running when you fly away. Areas where nothing moves or changes temperature go to sleep and cost almost nothing; busy areas cost the same near you or far away.' },
+1 -1
View File
@@ -5,7 +5,7 @@
const SET_DEFAULT = {
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,
minimap: true, minimapZoom: 4, fog: true, thermalPalette: 'inferno', simSpeed: 1, maxSteps: 3,
minimap: true, minimapZoom: 4, fog: true, thermalPalette: 'inferno', simSpeed: 1, maxSteps: 3, workerCap: 16,
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,
};
+39
View File
@@ -0,0 +1,39 @@
// Browser transport only; the simulation owns the schedule and shared state.
let threadStarting = null;
async function startThreads() {
const count = Math.max(1, Math.min(16, Math.floor(SET.workerCap) || 16, (navigator.hardwareConcurrency || 3) - 2));
if (simPool && simPool.world === mat && simPool.workers.length === count) return;
if (threadStarting) await threadStarting;
if (simPool && simPool.world === mat && simPool.workers.length === count) return;
threadStarting = (async () => {
if (simPool) for (const worker of simPool.workers) worker.terminate();
simPool = null;
if (!(mat.buffer instanceof SharedArrayBuffer)) throw Error('Shared memory is unavailable');
const url = URL.createObjectURL(new Blob([SIM_WORKER_SOURCE, '\nonmessage=e=>threadMessage(e.data);'], {type:'text/javascript'}));
const pool = { world: mat, workers: [], slots: [] };
try {
const ready = [];
for (let k = 0; k < count; k++) {
const worker = new Worker(url), slot = threadSlot();
pool.workers.push(worker); pool.slots.push(slot);
ready.push(new Promise((resolve,reject) => {
const deadline = performance.now() + 10000;
worker.onerror = e => {
const error = Error(e.message || 'Simulation worker failed');
threadFailure(slot, error); reject(error);
};
const poll = () => {
if (Atomics.load(slot.control,0)) resolve();
else if (performance.now() > deadline) reject(Error('Simulation worker startup timed out'));
else setTimeout(poll, 1);
};
poll();
}));
worker.postMessage({world:threadWorld(),slot});
}
await Promise.all(ready); simPool = pool;
} catch (e) { for (const worker of pool.workers) worker.terminate(); throw e; }
finally { URL.revokeObjectURL(url); }
})();
try { await threadStarting; } finally { threadStarting = null; }
}
+1 -1
View File
@@ -1,6 +1,6 @@
// Startup capabilities and title art.
const REQUIRE_WEBGPU = true;
const REQUIRE_THREADS = false;
const REQUIRE_THREADS = true;
const CAPS = { isolated: self.crossOriginIsolated === true, webgpu: false, adapterInfo: null, ready: false };
async function checkCapabilities() {
try {