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:
+34
-38
@@ -12,7 +12,9 @@ function setCell(i, m, t, fl) {
|
||||
if (t - AMB[y] > 20 || AMB[y] - t > 20 || (MELT[m] !== 0 && AMB[y] >= MELT[m]) || (m === WATER && AMB[y] < -0.5)) wakeHeatXY(x, y);
|
||||
}
|
||||
function swapCells(i, j) {
|
||||
renderDirtyIdx(i); renderDirtyIdx(j);
|
||||
renderDirtyIdx(i); renderDirtyIdx(j); swapCellData(i, j);
|
||||
}
|
||||
function swapCellData(i, j) {
|
||||
let a = mat[i]; mat[i] = mat[j]; mat[j] = a;
|
||||
const f = temp[i]; temp[i] = temp[j]; temp[j] = f;
|
||||
a = life[i]; life[i] = life[j]; life[j] = a;
|
||||
@@ -20,16 +22,16 @@ function swapCells(i, j) {
|
||||
stamp[i] = tickStamp; stamp[j] = tickStamp;
|
||||
}
|
||||
function move(i, j, x, y, nx, ny) {
|
||||
swapCells(i, j);
|
||||
swapCellData(i, j);
|
||||
wakeXY(x, y); wakeXY(nx, ny);
|
||||
if (ny === y && LFILM > 1 && PH[mat[j]] === PH_LIQUID && (y === 0 || PH[mat[i - W]] !== PH_LIQUID)) {
|
||||
// a top-layer grain stepping sideways may clear the path to a drop for surface grains far
|
||||
// along the row, so wake them; moves inside a body of water do not (settled lakes must sleep)
|
||||
const reach = SPREAD[mat[j]] * LFILM, row = (y >> 5) * CW;
|
||||
const first = Math.max(0, x - reach) >> 5, last = Math.min(W - 1, x + reach) >> 5;
|
||||
for (let cx = first; cx <= last; cx++) awake[row + cx] = 3;
|
||||
for (let cx = first; cx <= last; cx++) chunkWake(row + cx);
|
||||
}
|
||||
if (part[j] === P_WHEEL) { const mm = machines[mach[j]]; if (mm) mm.flow += PH[mat[j]] === PH_LIQUID ? 1 : 0.5; }
|
||||
if (part[j] === P_WHEEL) { if (chunkWork) chunkEvent('flow', mach[j], PH[mat[j]] === PH_LIQUID ? 1 : 0.5); else { const mm = machines[mach[j]]; if (mm) mm.flow += PH[mat[j]] === PH_LIQUID ? 1 : 0.5; } }
|
||||
const d = temp[j] - AMB[ny];
|
||||
if (d > 25 || d < -25) wakeHeatXY(nx, ny);
|
||||
}
|
||||
@@ -83,7 +85,7 @@ function powder(i, x, y, m, barriers = cleatChunks[(y >> 5) * CW + (x >> 5)]) {
|
||||
// a grain at least twice as dense works its way down through a loose pile. Unlit fuel
|
||||
// packs too tight for that, so a mixed furnace charge stays mixed until the fire gets in.
|
||||
const t = mat[b];
|
||||
if (PH[t] === PH_POWDER && (!barriers || (!cleatMask[i] && !cleatMask[b])) && DENS[m] >= DENS[t] * 2 && (!FUEL[t] || (flags[b] & F_BURN)) && stamp[b] !== tickStamp && !occ[b] && part[b] !== P_GRATE && part[b] !== P_FILTER && rnd() < (FUEL[t] ? SORTP_FUEL : SORTP)) { move(i, b, x, y, x, y + 1); awake[(y >> 5) * CW + (x >> 5)] = 40; return; }
|
||||
if (PH[t] === PH_POWDER && (!barriers || (!cleatMask[i] && !cleatMask[b])) && DENS[m] >= DENS[t] * 2 && (!FUEL[t] || (flags[b] & F_BURN)) && stamp[b] !== tickStamp && !occ[b] && part[b] !== P_GRATE && part[b] !== P_FILTER && rnd() < (FUEL[t] ? SORTP_FUEL : SORTP)) { move(i, b, x, y, x, y + 1); chunkWake((y >> 5) * CW + (x >> 5), 40); return; }
|
||||
// a grain lying right on a belt or filter is held by it: the surface carries it, it does not slide off the edge
|
||||
if ((t === BELTL || t === BELTR || part[b] === P_FILTER) &&
|
||||
!isSlopeBelt(blockP[((y + 1) >> 2) * BW + (x >> 2)])) return;
|
||||
@@ -132,11 +134,18 @@ function liquid(i, x, y, m) {
|
||||
if (rnd() > FLUID[m]) return;
|
||||
const sp = SPREAD[m] * (y === 0 || PH[mat[i - W]] !== PH_LIQUID ? LFILM : 1);
|
||||
const d0 = (RS & 4) ? 1 : -1;
|
||||
for (let pass = 0; pass < 2; pass++) {
|
||||
liquidSearch(i, x, y, m, sp, d0);
|
||||
}
|
||||
function liquidSearch(i, x, y, m, sp, d0, firstPass = 0, firstStep = 1) {
|
||||
for (let pass = firstPass; pass < 2; pass++) {
|
||||
const d = pass ? -d0 : d0;
|
||||
for (let s = 1; s <= sp; s++) {
|
||||
for (let s = pass === firstPass ? firstStep : 1; s <= sp; s++) {
|
||||
const nx = x + d * s;
|
||||
if (nx < 0 || nx >= W) break;
|
||||
// Same-colour chunks are separated by 32 cells; their maximum write halo is 8.
|
||||
if (chunkWork && (nx < chunkWork.x0 - 24 || nx >= chunkWork.x1 + 24)) {
|
||||
chunkEvent('liquid', i, m, sp, d0, pass, s); return;
|
||||
}
|
||||
const j = i + d * s;
|
||||
if (!passable(m, PH_LIQUID, j, d, 0)) break;
|
||||
if (y < H - 1 && passable(m, PH_LIQUID, j + W, 0, 1)) {
|
||||
@@ -149,8 +158,7 @@ function liquid(i, x, y, m) {
|
||||
}
|
||||
function gas(i, x, y, m) {
|
||||
// Even blocked gas must keep aging or condensing until it is gone.
|
||||
awake[(y >> 5) * CW + (x >> 5)] = 3;
|
||||
renderDirtyXY(x, y); // Gas can age without moving.
|
||||
chunkWake((y >> 5) * CW + (x >> 5));
|
||||
if (m === FIRE) {
|
||||
const l = life[i];
|
||||
if (l <= 1) { mat[i] = EMPTY; life[i] = 0; wakeXY(x, y); return; }
|
||||
@@ -193,35 +201,23 @@ function wakeBotPowder() {
|
||||
if (dx * dx + dy * dy <= 10000) awake[c] = 1;
|
||||
}
|
||||
}
|
||||
function simMove() {
|
||||
for (let cy = CH - 1; cy >= 0; cy--) {
|
||||
const rowBase = cy * CW;
|
||||
let any = false;
|
||||
for (let cx = 0; cx < CW; cx++) if (awake[rowBase + cx]) { any = true; powderArea[rowBase + cx] = 0; }
|
||||
if (!any) continue;
|
||||
const y0 = cy << 5, y1 = Math.min(H, y0 + 32);
|
||||
for (let y = y1 - 1; y >= y0; y--) {
|
||||
const ltr = ((y + tick) & 1) === 0;
|
||||
const yw = y * W;
|
||||
for (let k = 0; k < CW; k++) {
|
||||
const cx = ltr ? k : CW - 1 - k;
|
||||
if (!awake[rowBase + cx]) continue;
|
||||
roofDirty[cx] = 1;
|
||||
const x0 = cx << 5, x1 = Math.min(W, x0 + 32), barriers = cleatChunks[rowBase + cx];
|
||||
// alternate rows scan left to right and right to left, so nothing drifts one way
|
||||
const dx = ltr ? 1 : -1, xEnd = ltr ? x1 : x0 - 1;
|
||||
for (let x = ltr ? x0 : x1 - 1; x !== xEnd; x += dx) {
|
||||
const i = yw + x, m = mat[i];
|
||||
if (m === EMPTY) continue;
|
||||
const ph = PH[m];
|
||||
if (ph === PH_POWDER) powderArea[rowBase + cx] = 1;
|
||||
if (stamp[i] === tickStamp) continue;
|
||||
if (ph === PH_SOLID) continue;
|
||||
const p = part[i];
|
||||
if (IS_SCREW[p] && ph === PH_POWDER) continue; // a screw conveyor moves its own grains
|
||||
if (ph === PH_POWDER) powder(i, x, y, m, barriers); else if (ph === PH_LIQUID) liquid(i, x, y, m); else gas(i, x, y, m);
|
||||
}
|
||||
}
|
||||
function simMove() { chunkPasses(false); }
|
||||
function moveChunk(c) {
|
||||
const cx = c % CW, cy = (c / CW) | 0;
|
||||
const x0 = cx << 5, x1 = Math.min(W, x0 + 32), y0 = cy << 5, y1 = Math.min(H, y0 + 32);
|
||||
const barriers = cleatChunks[c];
|
||||
powderArea[c] = 0; roofDirty[cx] = 1; renderDirtyChunk(c);
|
||||
for (let y = y1 - 1; y >= y0; y--) {
|
||||
const ltr = ((y + tick) & 1) === 0, dx = ltr ? 1 : -1, end = ltr ? x1 : x0 - 1, yw = y * W;
|
||||
for (let x = ltr ? x0 : x1 - 1; x !== end; x += dx) {
|
||||
const i = yw + x, m = mat[i];
|
||||
if (m === EMPTY) continue;
|
||||
const ph = PH[m];
|
||||
if (ph === PH_POWDER) powderArea[c] = 1;
|
||||
if (stamp[i] === tickStamp || ph === PH_SOLID) continue;
|
||||
if (IS_SCREW[part[i]] && ph === PH_POWDER) continue;
|
||||
if (ph === PH_POWDER) powder(i, x, y, m, barriers);
|
||||
else if (ph === PH_LIQUID) liquid(i, x, y, m); else gas(i, x, y, m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ const FOG = 2;
|
||||
const FOG_SENSE = 24; // Reveal nearby ground through rock, in grains.
|
||||
const FOG_SENSE_FADE = 12;
|
||||
let fog, FW, FH;
|
||||
function fogAlloc() { FW = Math.ceil(W / FOG); FH = Math.ceil(H / FOG); fog = new Uint8Array(FW * FH); }
|
||||
function fogAlloc() { FW = Math.ceil(W / FOG); FH = Math.ceil(H / FOG); fog = sharedArray(Uint8Array, FW * FH); }
|
||||
function fogMark(x, y, v) {
|
||||
if (x < 0 || y < 0 || x >= W || y >= H) return;
|
||||
const i = Math.floor(y / FOG) * FW + Math.floor(x / FOG);
|
||||
|
||||
+9
-14
@@ -52,16 +52,9 @@ function condPair(a, b, sc, nc, da) {
|
||||
const q = d * k * sc;
|
||||
temp[a] -= q * ICAP[ma];
|
||||
temp[b] += q * ICAP[mb];
|
||||
if (nc >= 0 && (q > 0.15 || q < -0.15)) { if (!hot[nc]) hotLvl[nc] = 2; hot[nc] = 6; }
|
||||
}
|
||||
function heatPass() {
|
||||
for (let c = 0; c < NCH; c++) {
|
||||
if (!hot[c]) continue;
|
||||
let sc = 1;
|
||||
if (hotLvl[c] === 1) { if (((tick + c) & 3) !== 0) continue; sc = 4; }
|
||||
heatChunk(c, sc);
|
||||
}
|
||||
if (nc >= 0 && (q > 0.15 || q < -0.15)) { chunkHeat(nc, !hot[nc] ? 2 : 0); }
|
||||
}
|
||||
function heatPass() { chunkPasses(true); }
|
||||
function heatChunk(c, sc) {
|
||||
const cx = c % CW, cy = (c / CW) | 0;
|
||||
const x0 = cx << 5, y0 = cy << 5, x1 = Math.min(W, x0 + 32), y1 = Math.min(H, y0 + 32);
|
||||
@@ -93,7 +86,7 @@ function heatChunk(c, sc) {
|
||||
if ((pu === PH_EMPTY || pu === PH_GAS) && ex > 2) {
|
||||
const a = ex * (sc > 1 ? 0.45 : 0.25);
|
||||
t -= a; temp[u] += a;
|
||||
if (y === y0 && upCold && a > 0.5) { hot[c - CW] = 6; hotLvl[c - CW] = 2; }
|
||||
if (y === y0 && upCold && a > 0.5) { chunkHeat(c - CW); }
|
||||
}
|
||||
}
|
||||
rel = y < roofY[x] ? 0.02 : 0.002;
|
||||
@@ -110,8 +103,10 @@ function heatChunk(c, sc) {
|
||||
if (flags[i] & F_BURN) burning = true;
|
||||
}
|
||||
}
|
||||
if (burning || maxDev > 1.5) { hot[c] = 6; hotLvl[c] = (burning || maxDev > 60) ? 2 : 1; }
|
||||
else hot[c]--;
|
||||
const h = burning || maxDev > 1.5 ? 6 : hot[c] - 1;
|
||||
const level = burning || maxDev > 1.5 ? ((burning || maxDev > 60) ? 2 : 1) : hotLvl[c];
|
||||
if (chunkWork) { chunkWork.ownHot[c] = h; chunkWork.ownLevel[c] = level; }
|
||||
else { hot[c] = h; hotLvl[c] = level; }
|
||||
}
|
||||
function neighborDir(i, x, y, d) {
|
||||
if (d === 0) return y > 0 ? i - W : -1;
|
||||
@@ -125,7 +120,7 @@ function react(i, x, y, m) {
|
||||
if (fl & F_BURN) { burn(i, x, y, m, t); return; }
|
||||
if (IGN[m] !== 0 && t >= IGN[m] && (hasO2(i, x, y) || burningNear(i, x, y))) {
|
||||
flags[i] = fl | F_BURN; life[i] = FUEL[m];
|
||||
stats.ignitions++; discover(FIRE, x, y, m, 'ignite', t);
|
||||
if (chunkWork) chunkEvent('ignite'); else stats.ignitions++; discover(FIRE, x, y, m, 'ignite', t);
|
||||
return;
|
||||
}
|
||||
const ph = PH[m];
|
||||
@@ -183,7 +178,7 @@ function react(i, x, y, m) {
|
||||
if (rnd() < 0.03 + (t - mp) * 0.002) {
|
||||
let to = MELTTO[m];
|
||||
if (SMELTP[m] !== 0 && rnd() >= SMELTP[m]) to = t >= FRZ[MSLAG] ? MSLAG : SLAG; // the waste runs off molten when it is hot enough
|
||||
if (PARTMAT[m]) { partLost(i); if (m === HULL && mach[i]) brokenMachines.push(mach[i]); }
|
||||
if (PARTMAT[m]) { partLost(i); if (m === HULL && mach[i]) { if (chunkWork) chunkEvent('broken', mach[i]); else brokenMachines.push(mach[i]); } }
|
||||
mat[i] = to; flags[i] = 0; life[i] = PH[to] === PH_LIQUID ? 255 : 0; digp[i] = 0;
|
||||
wakeXY(x, y); discover(to, x, y, m, SMELTP[m] !== 0 ? 'smelt' : 'melt', mp);
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ function markPow(i, on) {
|
||||
else if (isPow[i]) { isPow[i] = 0; pcells.delete(i); netId[i] = 0; }
|
||||
powerDirty = true;
|
||||
}
|
||||
function partLost(i) { markPow(i, false); }
|
||||
function partLost(i) { if (chunkWork) chunkEvent('lost', i); else markPow(i, false); }
|
||||
let nextPowerBuild = 0;
|
||||
function rebuildPower() {
|
||||
powerDirty = false;
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ function worldWater() {
|
||||
for (const mm of machines) if (mm && mm.type === 'silo' && mm.sm === SNOW) n += mm.sn; // and snow in silos
|
||||
return Math.round(n + bag[WATER] + bag[SNOW]);
|
||||
}
|
||||
function absorbSteam(x) { const b = Math.min(weather.moist.length - 1, Math.max(0, (x / WBAND) | 0)); weather.moist[b] += 1; }
|
||||
function absorbSteam(x) { if (chunkWork) { chunkEvent('moist', x); return; } const b = Math.min(weather.moist.length - 1, Math.max(0, (x / WBAND) | 0)); weather.moist[b] += 1; }
|
||||
// Send about k drops (snowflakes where it is freezing) down from the clouds over x0..x1; returns how many fell
|
||||
function spawnRain(x0, x1, k) {
|
||||
if (flyers.length > 2500) return 0;
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
// The same chunk schedule runs inline or in workers. Workers only own a pass at a time.
|
||||
let simPool = null, chunkWork = null, inlineWork = null;
|
||||
function threadWorld() {
|
||||
return { W,H,N,CW,CH,NCH,BW,BH,NB,mat,temp,life,flags,stamp,part,mach,digp,noiseB,bgm,
|
||||
airS,netId,isPow,occ,cleatMask,cleatChunks,blockP,blockV,slopePhase,awake,hot,hotLvl,
|
||||
powderArea,roofDirty,renderDirty,AMB,roofY };
|
||||
}
|
||||
function threadBind(s) {
|
||||
({ W,H,N,CW,CH,NCH,BW,BH,NB,mat,temp,life,flags,stamp,part,mach,digp,noiseB,bgm,
|
||||
airS,netId,isPow,occ,cleatMask,cleatChunks,blockP,blockV,slopePhase,awake,hot,hotLvl,
|
||||
powderArea,roofDirty,renderDirty,AMB,roofY } = s);
|
||||
}
|
||||
function threadContext() {
|
||||
return { wake: new Uint8Array(NCH), heat: new Uint8Array(NCH), level: new Uint8Array(NCH),
|
||||
ownHot: new Uint8Array(NCH).fill(255), ownLevel: new Uint8Array(NCH), events: [], c: -1,
|
||||
x0: 0, x1: 0 };
|
||||
}
|
||||
function resetChunkWork(work) {
|
||||
work.wake.fill(0); work.heat.fill(0); work.level.fill(0); work.ownHot.fill(255); work.events.length = 0;
|
||||
return work;
|
||||
}
|
||||
function inlineChunkWork() {
|
||||
if (!inlineWork || inlineWork.wake.length !== NCH) inlineWork = threadContext();
|
||||
return resetChunkWork(inlineWork);
|
||||
}
|
||||
function chunkEvent(kind, ...args) { chunkWork.events.push([chunkWork.c, kind, ...args]); }
|
||||
function chunkWake(c, n = 3) {
|
||||
if (chunkWork) { if (chunkWork.wake[c] < n) chunkWork.wake[c] = n; }
|
||||
else awake[c] = n;
|
||||
}
|
||||
function chunkHeat(c, level = 2) {
|
||||
if (chunkWork) { chunkWork.heat[c] = 6; if (chunkWork.level[c] < level) chunkWork.level[c] = level; }
|
||||
else { hot[c] = 6; if (level) hotLvl[c] = level; }
|
||||
}
|
||||
function chunkSeed(c, pass) { return hash2(c, tick, worldSeed ^ Math.imul(pass + 1, 0x9e3779b9)) | 1; }
|
||||
function runChunks(chunks, pass, work) {
|
||||
const savedRS = RS;
|
||||
chunkWork = work;
|
||||
try {
|
||||
for (const c of chunks) {
|
||||
work.c = c; work.x0 = (c % CW) * 32; work.x1 = Math.min(W, work.x0 + 32);
|
||||
RS = chunkSeed(c, pass);
|
||||
if (pass < 4) moveChunk(c); else {
|
||||
const sc = hotLvl[c] === 1 ? 4 : 1;
|
||||
heatChunk(c, sc);
|
||||
}
|
||||
}
|
||||
} finally { chunkWork = null; RS = savedRS; }
|
||||
}
|
||||
function mergeChunkWork(works) {
|
||||
for (const work of works) for (let c = 0; c < NCH; c++) if (work.ownHot[c] !== 255) {
|
||||
hot[c] = work.ownHot[c]; hotLvl[c] = work.ownLevel[c];
|
||||
}
|
||||
for (const work of works) for (let c = 0; c < NCH; c++) {
|
||||
if (awake[c] < work.wake[c]) awake[c] = work.wake[c];
|
||||
if (hot[c] < work.heat[c]) hot[c] = work.heat[c];
|
||||
if (hotLvl[c] < work.level[c]) hotLvl[c] = work.level[c];
|
||||
}
|
||||
const events = works.flatMap(w => w.events).sort((a,b) => a[0] - b[0]);
|
||||
for (const [,kind,...a] of events) {
|
||||
if (kind === 'discover') discover(...a);
|
||||
else if (kind === 'emit') emit(a[0]);
|
||||
else if (kind === 'flow') { const mm = machines[a[0]]; if (mm) mm.flow += a[1]; }
|
||||
else if (kind === 'moist') absorbSteam(a[0]);
|
||||
else if (kind === 'lost') partLost(a[0]);
|
||||
else if (kind === 'broken') brokenMachines.push(a[0]);
|
||||
else if (kind === 'ignite') stats.ignitions++;
|
||||
else if (kind === 'liquid') {
|
||||
const i = a[0];
|
||||
if (stamp[i] !== tickStamp && mat[i] === a[1]) liquidSearch(i, i % W, (i / W) | 0, ...a.slice(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
function chunkPasses(heat) {
|
||||
for (let color = 0; color < 4; color++) {
|
||||
const chunks = [];
|
||||
for (let c = 0; c < NCH; c++) {
|
||||
if (((c % CW & 1) | (((c / CW) & 1) << 1)) !== color) continue;
|
||||
if (heat ? (!hot[c] || (hotLvl[c] === 1 && ((tick + c) & 3) !== 0)) : !awake[c]) continue;
|
||||
chunks.push(c);
|
||||
}
|
||||
if (!chunks.length) continue;
|
||||
const pass = color + (heat ? 4 : 0);
|
||||
if (simPool && simPool.world === mat && chunks.length >= (simPool.force ? 1 : (heat ? 16 : 64))) threadPass(simPool, chunks, pass);
|
||||
else { const work = inlineChunkWork(); runChunks(chunks, pass, work); mergeChunkWork([work]); }
|
||||
}
|
||||
}
|
||||
function threadParams() {
|
||||
return { tick,tickStamp,worldSeed,LSTEP,LFILM,SORTP,CORNER_SEAL,
|
||||
seen, bot: {...bot}, skyY: weather.skyY, barriers: !!cleatCells.length, filterCfg };
|
||||
}
|
||||
function threadSetParams(s) {
|
||||
({tick,tickStamp,worldSeed,LSTEP,LFILM,SORTP,CORNER_SEAL,filterCfg} = s);
|
||||
seen.set(s.seen); Object.assign(bot, s.bot);
|
||||
weather.skyY = s.skyY; cleatCells = s.barriers ? [1] : [];
|
||||
}
|
||||
// Transport is injected by the browser or Node harness; no browser globals in the simulation.
|
||||
function threadSlot() {
|
||||
// shortcut: 1 MiB of events per helper per pass; grow or stream it if dense reactions fill it.
|
||||
return { control: sharedArray(Int32Array, 2), bytes: sharedArray(Uint8Array, 1024 * 1024),
|
||||
wake: sharedArray(Uint8Array, NCH), heat: sharedArray(Uint8Array, NCH),
|
||||
level: sharedArray(Uint8Array, NCH), ownHot: sharedArray(Uint8Array, NCH),
|
||||
ownLevel: sharedArray(Uint8Array, NCH) };
|
||||
}
|
||||
function threadExecute(slot, chunks, pass) {
|
||||
slot.wake.fill(0); slot.heat.fill(0); slot.level.fill(0); slot.ownHot.fill(255);
|
||||
const work = { ...slot, events: [], c: -1 };
|
||||
runChunks(chunks, pass, work);
|
||||
const encoded = new TextEncoder().encode(JSON.stringify(work.events));
|
||||
if (encoded.length > slot.bytes.length) throw Error('Simulation event buffer full');
|
||||
slot.bytes.set(encoded); Atomics.store(slot.control, 1, encoded.length);
|
||||
}
|
||||
function threadPass(pool, chunks, pass) {
|
||||
const params = threadParams(), n = Math.min(pool.slots.length, (pool.force ? chunks.length : Math.max(1, Math.floor(chunks.length / (pass < 4 ? 32 : 8)))));
|
||||
pool.activeMax = Math.max(pool.activeMax || 0, n);
|
||||
const batches = Array.from({length:n + 1}, () => []);
|
||||
for (let k = 0; k < chunks.length; k++) batches[k % batches.length].push(chunks[k]);
|
||||
for (let k = 0; k < n; k++) {
|
||||
const slot = pool.slots[k]; Atomics.store(slot.control, 0, 0);
|
||||
pool.workers[k].postMessage({ chunks: batches[k], pass, params });
|
||||
}
|
||||
const local = inlineChunkWork(); runChunks(batches[n], pass, local);
|
||||
const works = [local], deadline = Date.now() + 10000;
|
||||
for (let k = 0; k < n; k++) {
|
||||
const slot = pool.slots[k];
|
||||
threadWait(slot, deadline);
|
||||
const events = JSON.parse(new TextDecoder().decode(slot.bytes.slice(0, Atomics.load(slot.control, 1))));
|
||||
if (Atomics.load(slot.control, 0) < 0) throw Error('Simulation worker: ' + events);
|
||||
works.push({ ...slot, events });
|
||||
}
|
||||
mergeChunkWork(works);
|
||||
}
|
||||
function threadWait(slot, deadline) {
|
||||
let spins = 0;
|
||||
// Atomics.wait is forbidden on the browser main thread. Bound polling to fail closed.
|
||||
while (Atomics.load(slot.control, 0) === 0) {
|
||||
if (++spins === 16384) { spins = 0; if (Date.now() > deadline) throw Error('Simulation worker timed out'); }
|
||||
}
|
||||
}
|
||||
function threadFailure(slot, error) {
|
||||
const bytes = new TextEncoder().encode(JSON.stringify(String(error.stack || error)));
|
||||
slot.bytes.set(bytes.subarray(0, slot.bytes.length));
|
||||
Atomics.store(slot.control, 1, Math.min(bytes.length, slot.bytes.length));
|
||||
Atomics.store(slot.control, 0, -1); Atomics.notify(slot.control, 0);
|
||||
}
|
||||
function threadMessage(data) {
|
||||
if (data.world) { threadBind(data.world); globalThis.threadWorkerSlot = data.slot; Atomics.store(data.slot.control, 0, 1); return; }
|
||||
const slot = globalThis.threadWorkerSlot;
|
||||
try {
|
||||
threadSetParams(data.params); threadExecute(slot, data.chunks, data.pass);
|
||||
Atomics.store(slot.control, 0, 1); Atomics.notify(slot.control, 0);
|
||||
} catch (e) { threadFailure(slot, e); }
|
||||
}
|
||||
+29
-19
@@ -29,36 +29,41 @@ let seen = new Uint8Array(NMAT);
|
||||
let ledger = {};
|
||||
const stats = { dug: 0, moved: 0, placed: 0, ignitions: 0, cast: 0, bars: 0 };
|
||||
|
||||
function emit(e) { if (EV.length < 400) EV.push(e); }
|
||||
function emit(e) { if (chunkWork) { chunkEvent('emit', e); return; } if (EV.length < 400) EV.push(e); }
|
||||
function bagCap() { return TANK_TIERS[player.tankTier].cap; }
|
||||
// Things count as discovered only when they happen near the player (or in the bag).
|
||||
// how: 'dig', 'pickup', 'melt', 'smelt', 'freeze', 'boil', 'ignite', 'burn', 'fire', 'blow', 'condense'
|
||||
function discover(m, x, y, from, how, T) {
|
||||
if (seen[m]) return;
|
||||
if (x !== undefined) { const dx = x - bot.x, dy = y - bot.y; if (dx * dx + dy * dy > 110 * 110) return; }
|
||||
if (chunkWork) { chunkEvent('discover', m, x, y, from, how, T); return; }
|
||||
seen[m] = 1; emit({ t: 'discover', m, from: from || 0, how: how || '', T: T || 0 });
|
||||
}
|
||||
|
||||
function sharedArray(Type, length) {
|
||||
const Buffer = typeof SharedArrayBuffer === 'function' ? SharedArrayBuffer : ArrayBuffer;
|
||||
return new Type(new Buffer(length * Type.BYTES_PER_ELEMENT));
|
||||
}
|
||||
function alloc(w, h) {
|
||||
resetDevConsole();
|
||||
W = w; H = h; N = w * h;
|
||||
fogAlloc();
|
||||
CW = Math.ceil(w / 32); CH = Math.ceil(h / 32); NCH = CW * CH;
|
||||
cleatMask = new Uint8Array(N); cleatCells = [];
|
||||
mat = new Uint8Array(N); temp = new Float32Array(N); life = new Uint8Array(N); flags = new Uint8Array(N);
|
||||
stamp = new Uint8Array(N); part = new Uint8Array(N); mach = new Uint16Array(N); digp = new Uint8Array(N);
|
||||
noiseB = new Uint8Array(N); bgm = new Uint8Array(N); airS = new Uint16Array(N); netId = new Uint16Array(N); isPow = new Uint8Array(N); occ = new Uint8Array(N);
|
||||
BW = Math.ceil(w / BS); BH = Math.ceil(h / BS); NB = BW * BH; blockP = new Uint8Array(NB); blockV = new Uint8Array(NB); slopePhase = new Uint8Array(NB); filtersDirty = true; filterCfg = new Map(); procDirty = true; procState.clear(); rackState.clear();
|
||||
cleatMask = sharedArray(Uint8Array, N); cleatCells = [];
|
||||
mat = sharedArray(Uint8Array, N); temp = sharedArray(Float32Array, N); life = sharedArray(Uint8Array, N); flags = sharedArray(Uint8Array, N);
|
||||
stamp = sharedArray(Uint8Array, N); part = sharedArray(Uint8Array, N); mach = sharedArray(Uint16Array, N); digp = sharedArray(Uint8Array, N);
|
||||
noiseB = sharedArray(Uint8Array, N); bgm = sharedArray(Uint8Array, N); airS = sharedArray(Uint16Array, N); netId = sharedArray(Uint16Array, N); isPow = sharedArray(Uint8Array, N); occ = sharedArray(Uint8Array, N);
|
||||
BW = Math.ceil(w / BS); BH = Math.ceil(h / BS); NB = BW * BH; blockP = sharedArray(Uint8Array, NB); blockV = sharedArray(Uint8Array, NB); slopePhase = sharedArray(Uint8Array, NB); filtersDirty = true; filterCfg = new Map(); procDirty = true; procState.clear(); rackState.clear();
|
||||
ents = []; flyers = []; ghostBlocks = new Map(); ghostMolds = []; treeBases = []; saplings = [];
|
||||
surfBase = Math.round(Math.min(h * 0.27, 175)); underD = h - surfBase; shallowD = 0; lapse = 0.36;
|
||||
surfY = new Int16Array(w); roofY = new Int16Array(w); dirtY = new Int16Array(w); graniteY = new Int16Array(w); basaltY = new Int16Array(w); magmaY = new Int16Array(w);
|
||||
AMB = new Float32Array(h);
|
||||
surfY = sharedArray(Int16Array, w); roofY = sharedArray(Int16Array, w); dirtY = sharedArray(Int16Array, w); graniteY = sharedArray(Int16Array, w); basaltY = sharedArray(Int16Array, w); magmaY = sharedArray(Int16Array, w);
|
||||
AMB = sharedArray(Float32Array, h);
|
||||
loadWakeOrder = loadWakePending = null; loadWakeIndex = 0;
|
||||
renderDirty = new Uint8Array(NCH).fill(1);
|
||||
cleatChunks = new Uint8Array(NCH);
|
||||
awake = new Uint8Array(NCH); hot = new Uint8Array(NCH); hotLvl = new Uint8Array(NCH);
|
||||
roofDirty = new Uint8Array(CW).fill(1);
|
||||
powderArea = new Uint8Array(NCH).fill(1); // unknown areas get one visit before being ruled out
|
||||
renderDirty = sharedArray(Uint8Array, NCH).fill(1);
|
||||
cleatChunks = sharedArray(Uint8Array, NCH);
|
||||
awake = sharedArray(Uint8Array, NCH); hot = sharedArray(Uint8Array, NCH); hotLvl = sharedArray(Uint8Array, NCH);
|
||||
roofDirty = sharedArray(Uint8Array, CW).fill(1);
|
||||
powderArea = sharedArray(Uint8Array, NCH).fill(1); // unknown areas get one visit before being ruled out
|
||||
machines = [null]; molds = []; nets = [null]; pcells = new Set(); powerDirty = true;
|
||||
beltL = []; beltR = []; screwLists = Array.from({ length: IS_SCREW.length }, () => []); brokenMachines = [];
|
||||
ghostMachines = []; ghostBlocked = new Set();
|
||||
@@ -93,21 +98,26 @@ function fillNoise() {
|
||||
|
||||
// ---------- Waking ----------
|
||||
function renderDirtyXY(x, y) { renderDirty[(y >> 5) * CW + (x >> 5)] = 1; }
|
||||
function renderDirtyChunk(c) {
|
||||
const cx = c % CW, cy = (c / CW) | 0;
|
||||
for (let yy = Math.max(0, cy - 1); yy <= Math.min(CH - 1, cy + 1); yy++)
|
||||
for (let xx = Math.max(0, cx - 1); xx <= Math.min(CW - 1, cx + 1); xx++) renderDirty[yy * CW + xx] = 1;
|
||||
}
|
||||
function renderDirtyIdx(i) { renderDirtyXY(i % W, (i / W) | 0); }
|
||||
function wakeXY(x, y) {
|
||||
renderDirtyXY(x, y);
|
||||
const cx = x >> 5, cy = y >> 5, c = cy * CW + cx;
|
||||
if (!chunkWork) renderDirty[c] = 1;
|
||||
roofDirty[cx] = 1;
|
||||
awake[c] = 3;
|
||||
chunkWake(c);
|
||||
const lx = x & 31, ly = y & 31;
|
||||
if (lx === 0) { if (cx > 0) awake[c - 1] = 3; } else if (lx === 31 && cx < CW - 1) awake[c + 1] = 3;
|
||||
if (ly === 0) { if (cy > 0) awake[c - CW] = 3; } else if (ly === 31 && cy < CH - 1) awake[c + CW] = 3;
|
||||
if (lx === 0) { if (cx > 0) chunkWake(c - 1); } else if (lx === 31 && cx < CW - 1) chunkWake(c + 1);
|
||||
if (ly === 0) { if (cy > 0) chunkWake(c - CW); } else if (ly === 31 && cy < CH - 1) chunkWake(c + CW);
|
||||
}
|
||||
function wakeHeatXY(x, y) {
|
||||
renderDirtyXY(x, y);
|
||||
const c = (y >> 5) * CW + (x >> 5);
|
||||
if (!chunkWork) renderDirty[c] = 1;
|
||||
roofDirty[x >> 5] = 1;
|
||||
hot[c] = 6; hotLvl[c] = 2;
|
||||
chunkHeat(c);
|
||||
}
|
||||
function wakeIdx(i) { wakeXY(i % W, (i / W) | 0); }
|
||||
function wakeHeatIdx(i) { wakeHeatXY(i % W, (i / W) | 0); }
|
||||
|
||||
Reference in New Issue
Block a user