WebGPU renderer complete: dirty uploads, default limits, legacy removed, WebGPU required, warm-up (t88 phase 2d)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 23:17:17 -04:00
co-authored by Claude Opus 5.5
parent 452b6aed32
commit 20963c776e
83 changed files with 776 additions and 1240 deletions
+2 -2
View File
@@ -60,7 +60,7 @@ function toggleGateAt(x, y) {
const m = mat[i];
if (m !== EMPTY && PH[m] !== PH_GAS) {
const u = i - W;
if (u >= 0 && mat[u] === EMPTY && !part[u]) { mat[u] = m; temp[u] = temp[i]; life[u] = life[i]; flags[u] = flags[i]; }
if (u >= 0 && mat[u] === EMPTY && !part[u]) { mat[u] = m; temp[u] = temp[i]; life[u] = life[i]; flags[u] = flags[i]; renderDirtyIdx(u); }
}
part[i] = 0; mat[i] = GATE; flags[i] = 0; life[i] = 0;
}
@@ -82,7 +82,7 @@ function interactAt(x, y) {
return open ? 'Rack open.' : 'Rack closed.';
}
if (blockP[pb] === BP_PUMP || blockP[pb] === BP_OUTLET) {
blockV[pb] ^= 4; emit({ t: 'sfx', s: 'ui' });
blockV[pb] ^= 4; renderDirtyXY((pb % BW) * BS, ((pb / BW) | 0) * BS); emit({ t: 'sfx', s: 'ui' });
return blockP[pb] === BP_PUMP ? (blockV[pb] & 4 ? 'Pump off.' : 'Pump on.') : (blockV[pb] & 4 ? 'Outlet closed.' : 'Outlet open.');
}
const id = mach[y * W + x];
+2
View File
@@ -83,6 +83,7 @@ function placeBlock(code, v, bx, by, free, extra) {
else if (isPipeCode(code)) setCell(i, PIPE, AMB[y], 0);
}
if (isPipeCode(code)) pipesDirty = true;
renderDirtyXY(bx * BS, by * BS);
blockP[by * BW + bx] = code; blockV[by * BW + bx] = v;
if (isFilter(code)) { blockV[by * BW + bx] = filterVariant(code, v); filterCfg.set(by * BW + bx, filterRec(extra)); filtersDirty = true; }
if (isProcCode(code)) procDirty = true;
@@ -169,6 +170,7 @@ function removeBlock(bx, by) {
rackState.delete(b);
if (s) for (const [m, count] of s.q) giveOrDrop(m, count, bot.x, bot.y);
}
renderDirtyXY(bx * BS, by * BS);
blockP[b] = 0; blockV[b] = 0;
const cleared = blockCellList(bx, by);
if (isSlope(code) || isSlopeBelt(code)) {
+2
View File
@@ -12,6 +12,7 @@ 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);
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;
@@ -149,6 +150,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.
if (m === FIRE) {
const l = life[i];
if (l <= 1) { mat[i] = EMPTY; life[i] = 0; wakeXY(x, y); return; }
+1
View File
@@ -114,6 +114,7 @@ function carryBeltsAndFilters(belts, filters, d, bm, carrying) {
}
// Change what a filter does; grains inside it that no longer pass are pushed out on top.
function setFilter(b, cfg) {
renderDirtyXY((b % BW) * BS, ((b / BW) | 0) * BS);
if (!isFilter(blockP[b])) return false;
const rec = filterRec(cfg);
filterCfg.set(b, rec);
+1 -1
View File
@@ -151,7 +151,7 @@ function pulseTick() {
const thin = open > 1 && !THIN_BONUS_OFF ? 1 + (open - 1) * THIN_BONUS : 1;
if (h > tier.hard) { e *= 0.35; continue; }
const q = digp[i] + e * thin * tier.speed * PULSE_K / (1 + h * h * 0.6);
if (q >= 255) { shatterCell(i, x, y, m); if (!p.tune) crumbleAround(x, y); } else digp[i] = q;
if (q >= 255) { shatterCell(i, x, y, m); if (!p.tune) crumbleAround(x, y); } else { digp[i] = q; renderDirtyIdx(i); }
e *= p.tune ? 0.96 : p.damp;
}
p.en[k] = e; p.s[k] = sd;
+3 -3
View File
@@ -7,7 +7,7 @@ function fogAlloc() { FW = Math.ceil(W / FOG); FH = Math.ceil(H / FOG); fog = ne
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);
if (v > fog[i]) fog[i] = v;
if (v > fog[i]) { fog[i] = v; renderDirtyXY(x, y); }
}
function fogSeen(x, y) {
if (x < 0 || y < 0 || x >= W || y >= H) return 255;
@@ -39,7 +39,7 @@ function fogLook(cx, cy) {
const dx = (fx + 0.5) * FOG - cx, dy = (fy + 0.5) * FOG - cy, d2 = dx * dx + dy * dy, i = fy * FW + fx;
if (d2 >= R2) continue;
const v = d2 <= in2 ? 255 : (FOG_SENSE - Math.sqrt(d2)) * 255 / FOG_SENSE_FADE;
if (v > fog[i]) fog[i] = v;
if (v > fog[i]) { fog[i] = v; renderDirtyXY(fx * FOG, fy * FOG); }
}
}
function fogRevealAround(x, y, r) {
@@ -49,4 +49,4 @@ function fogRevealAround(x, y, r) {
fogMark(xx, yy, r >= 6 && d2 <= (r - 6) ** 2 ? 255 : Math.min(255, (r - Math.sqrt(d2)) * 255 / 6));
}
}
function fogRevealAll() { fog.fill(255); }
function fogRevealAll() { fog.fill(255); renderDirty.fill(1); }
+5
View File
@@ -65,6 +65,11 @@ function heatPass() {
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);
renderDirty[c] = 1;
if (cx > 0) renderDirty[c - 1] = 1;
if (cx + 1 < CW) renderDirty[c + 1] = 1;
if (cy > 0) renderDirty[c - CW] = 1;
if (cy + 1 < CH) renderDirty[c + CW] = 1;
roofDirty[cx] = 1;
const leftCold = cx > 0 && !hot[c - 1], upCold = cy > 0 && !hot[c - CW];
let maxDev = 0, burning = false;
+1 -1
View File
@@ -57,7 +57,7 @@ function netSat(i) { if (POWER_FREE) return 1; const n = nets[netId[i]]; return
// Move loose grains out of the given cells, to the nearest open cell above
// the footprint in the same column (they then fall back on top). Gas just vanishes.
function evictCells(cells, topY) {
for (const i of cells) bgm[i] = 0;
for (const i of cells) { bgm[i] = 0; renderDirtyIdx(i); }
for (const i of cells) {
const m = mat[i], ph = PH[m];
if (m === EMPTY || ph === PH_SOLID) continue;
+4 -2
View File
@@ -4,6 +4,7 @@
// no longer reaches the ground comes down: every trunk cell drops as wood chunks.
function cutTree(i, x, y, tier, amt = 1) {
const b = bgm[i];
renderDirtyIdx(i);
const p = digp[i] + amt * tier.speed * 70 / (1 + HARD[b]);
if (p < 255) { digp[i] = p; return false; }
digp[i] = 0; bgm[i] = 0;
@@ -104,6 +105,7 @@ function plantTick() {
if (y <= 6 || y >= H - 1) continue;
const i = y * W + x;
if (mat[i] !== ACORN || (flags[i] & F_BURN)) continue;
renderDirtyIdx(i);
if (!SOIL[mat[i + W]] || temp[i] < 2 || temp[i] > 45) { life[i] = 0; continue; }
let room = true;
for (let k = 1; k <= 6 && room; k++) { const j = i - k * W; if (PH[mat[j]] !== PH_EMPTY && PH[mat[j]] !== PH_GAS || bgm[j] === WOOD) room = false; }
@@ -120,7 +122,7 @@ function setTreeCell(x, y, m) {
const i = y * W + x;
if (mat[i] !== EMPTY && PH[mat[i]] !== PH_GAS) return;
if (m === LEAVES && bgm[i]) return;
bgm[i] = m;
bgm[i] = m; renderDirtyIdx(i);
if (treeDraw) treeDraw.push(i);
}
function saplingTick() {
@@ -134,7 +136,7 @@ function saplingTick() {
sp.age++;
const s = Math.min(1, sp.age / 180);
// wipe the previous stage, then draw this one
if (sp.cells) for (const i of sp.cells) if (bgm[i] === LEAVES || (bgm[i] === WOOD && i !== base - W)) bgm[i] = 0;
if (sp.cells) for (const i of sp.cells) if (bgm[i] === LEAVES || (bgm[i] === WOOD && i !== base - W)) { bgm[i] = 0; renderDirtyIdx(i); }
treeDraw = sp.cells = [];
const th = Math.max(1, Math.round(sp.th * s)), wide = s > 0.5 ? 2 : 1;
for (let j = 1; j <= th; j++) for (let dx = 0; dx < wide; dx++) setTreeCell(sp.x + dx, sp.y - j, WOOD);
+1 -1
View File
@@ -50,7 +50,7 @@ function toolDig(cx, cy, r) {
if (isWall(b) && !blockIntegrity(((y / BS) | 0) * BW + ((x / BS) | 0))) removeBlock((x / BS) | 0, (y / BS) | 0);
dug++;
if ((dug & 3) === 0) emit({ t: 'debris', x, y, m });
} else digp[i] = p;
} else { digp[i] = p; renderDirtyIdx(i); }
});
stats.dug += dug;
let status = '';
+6
View File
@@ -13,6 +13,7 @@ let ents = []; // loose objects (bars)
let bgm; // background layer: trees. Liquids, powders and gases pass in front of it.
let flyers = []; // grains in flight, blown out of the bot
let ghostBlocks = new Map(), ghostMolds = [];
let renderDirty; // GPU world textures, one invalidation bit per 32x32 chunk.
let awake, hot, hotLvl, powderArea, roofDirty;
let loadWakeOrder = null, loadWakePending = null, loadWakeIndex = 0;
let tick = 0, tickStamp = 1;
@@ -53,6 +54,7 @@ function alloc(w, h) {
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);
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);
@@ -90,7 +92,10 @@ function fillNoise() {
}
// ---------- Waking ----------
function renderDirtyXY(x, y) { renderDirty[(y >> 5) * CW + (x >> 5)] = 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;
roofDirty[cx] = 1;
awake[c] = 3;
@@ -99,6 +104,7 @@ function wakeXY(x, y) {
if (ly === 0) { if (cy > 0) awake[c - CW] = 3; } else if (ly === 31 && cy < CH - 1) awake[c + CW] = 3;
}
function wakeHeatXY(x, y) {
renderDirtyXY(x, y);
const c = (y >> 5) * CW + (x >> 5);
roofDirty[x >> 5] = 1;
hot[c] = 6; hotLvl[c] = 2;