Faster roof scan and heat pass with identical results (t21)

- The roof is rescanned only in area columns that changed since the last scan: a quiet Huge
  world goes from 4.60 to 1.88 ms per scan (every 20 ticks).
- The heat pass skips react() for solids and powders below their first reaction threshold:
  Huge world digging 2.54 -> 2.34 ms/tick, quiet 1.39 -> 1.18.
- Every suite's output is identical apart from timing lines; seeds make the same worlds.
- test/t_sim_optimizations.js checks the dirty roof against a full scan and the heat loop
  against the reference loop byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 22:18:50 -04:00
co-authored by Claude Opus 5.5
parent d3fc1d2cbc
commit f57bdd6084
9 changed files with 160 additions and 11 deletions
+42
View File
@@ -0,0 +1,42 @@
// Reference heat loop before t21 phase 3; preserves conduction and Float32 store order.
function referenceHeatChunk(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);
const leftCold = cx > 0 && !hot[c - 1], upCold = cy > 0 && !hot[c - CW];
let maxDev = 0, burning = false;
for (let y = y0; y < y1; y++) {
const amb = AMB[y], yw = y * W;
const dDown = y + 1 < H ? amb - AMB[y + 1] : 0, dUp = y > 0 ? AMB[y - 1] - amb : 0;
for (let x = x0; x < x1; x++) {
const i = yw + x;
if (x + 1 < W) condPair(i, i + 1, sc, x + 1 === x1 ? c + 1 : -1, 0);
if (y + 1 < H) condPair(i, i + W, sc, y + 1 === y1 ? c + CW : -1, dDown);
if (x === x0 && leftCold) condPair(i - 1, i, sc, c - 1, 0);
if (y === y0 && upCold) condPair(i - W, i, sc, c - CW, dUp);
let m = mat[i];
const ph = PH[m];
let t = temp[i];
let rel;
if (ph === PH_EMPTY || ph === PH_GAS) {
if (y > 0) {
const u = i - W, pu = PH[mat[u]];
const ex = (t - amb) - (temp[u] - AMB[y - 1]);
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; }
}
}
rel = y < roofY[x] ? 0.02 : 0.002;
} else rel = RELAX[m];
t += (amb - t) * rel * sc;
temp[i] = t;
if (m !== EMPTY) { react(i, x, y, m); m = mat[i]; t = temp[i]; }
const dv = t > amb ? t - amb : amb - t;
if (dv > maxDev) maxDev = dv;
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]--;
}
+55
View File
@@ -0,0 +1,55 @@
// Optimized paths must match the full roof scan and the original heat loop exactly.
const fs = require('fs');
const { load, suite, flatWorld } = require('./harness');
const { run } = load();
const { check, done } = suite();
run(fs.readFileSync(__dirname + '/fixtures/heat-chunk.js', 'utf8'));
// Partial areas and changes at area boundaries, including material appearing in empty sky.
flatWorld(run, 99, 97, 80);
run('RS = 123; bot.x = 45; bot.y = 30;');
run(`
const originalWeatherTick = weatherTick;
weatherTick = function () {
if (tick % 20 === 1) {
const roof = roofY.slice(); updateRoof();
if (!roof.every((v, x) => v === roofY[x])) globalThis.__roofMiss = (globalThis.__roofMiss || 0) + 1;
}
originalWeatherTick();
};
`);
for (let k = 0; k < 200; k++) run(`
setCell((12 + ${k} % 60) * W + (${k} * 31 % W), ${k} % 3 ? WATER : EMPTY, 18, 0);
simTick();
`);
check('scheduled roof matches a full scan at area edges', !run('globalThis.__roofMiss'), run('globalThis.__roofMiss'));
run('weatherTick = originalWeatherTick;');
// Every material, threshold neighbours, fractional temperatures, burning, cold neighbours,
// alternating heat scales, and partial areas exercise both random calls and Float32 stores.
const snapshot = () => run(`JSON.stringify({
arrays: [mat, temp, life, flags, hot, hotLvl, part, digp, seen].map(a => Array.from(new Uint8Array(a.buffer))),
RS, stats, EV, brokenMachines
})`);
const setup = () => {
flatWorld(run, 67, 65, 60);
run(`{
RS = 456; seen.fill(0); EV.length = 0; stats.ignitions = 0; brokenMachines = [];
const ts = [-30, -0.5, 0, 0.5, 18, 97, 100, 260, 380, 600, 880, 1085, 1300, 1530, 1900];
for (let i = 0; i < N; i++) {
const m = (i * 37) % NMAT;
mat[i] = m; temp[i] = ts[i % ts.length] + ((i % 3) - 1) * 0.001;
life[i] = i % 256; flags[i] = i % 23 === 0 && FUEL[m] ? F_BURN : 0;
}
hot.fill(6); hot[0] = 0; hotLvl.fill(2); updateRoof();
}`);
};
for (const scale of [1, 4]) {
setup();
run(`for (let c = 1; c < NCH; c++) referenceHeatChunk(c, ${scale});`);
const expected = snapshot();
setup();
run(`for (let c = 1; c < NCH; c++) heatChunk(c, ${scale});`);
check('heat matches original bytes and random state at scale ' + scale, snapshot() === expected);
}
done();