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:
+2
-1
@@ -89,7 +89,8 @@ async function openNewClaim() {
|
||||
showLoading(true, 0.9, 'Saving');
|
||||
if (!await saveWorld('auto', true)) throw new Error('Save failed');
|
||||
showLoading(true, 1, 'Saving');
|
||||
showLoading(false); closeAllModals(); enterGame();
|
||||
await gpuWarmWorld();
|
||||
showLoading(false); closeAllModals(); enterGame(true);
|
||||
showCard({ kind: 'New claim', title: name, body: [`Claim ${n}. You owe ${fmtMoney(debt)} on this claim. Your money, bag, upgrades and what you’ve learned came along. Buildings stayed on the old claim.`], hold: 1.8 });
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
||||
+3
-8
@@ -12,7 +12,9 @@ function scanVisible() {
|
||||
}
|
||||
let ambientT = 0;
|
||||
function frame(now) {
|
||||
if (GPU.stopped) return;
|
||||
requestAnimationFrame(frame);
|
||||
if (GPU.recovering) { lastT = now; return; }
|
||||
const dtReal = Math.min(0.25, (now - lastT) / 1000); lastT = now;
|
||||
if (SET.fpsCap && now - lastRender < 1000 / SET.fpsCap - 1) return;
|
||||
const dt = Math.min(0.1, (now - (lastRender || now)) / 1000) || 1 / 60; lastRender = now;
|
||||
@@ -58,19 +60,12 @@ function frame(now) {
|
||||
if (!GAME.drag || Math.hypot(bot.vx, bot.vy) > 0.08) updateCamera(dt, lookX, lookY);
|
||||
{ const v = viewCells(); setBuildRange(R.camX + v.w / 2, R.camY + v.h / 2, v.w, v.h); }
|
||||
R.frame++;
|
||||
if (SET.showPerf) perfLast.computeLight = 0;
|
||||
drawWorld();
|
||||
if (SET.showPerf) perfTimes.computeLight = (perfTimes.computeLight || 0) * 0.9 + perfLast.computeLight * 0.1;
|
||||
const overlayStart = SET.showPerf ? performance.now() : 0;
|
||||
drawCracks();
|
||||
if (!weather.seen && (R.rain || 0) > 0.12) {
|
||||
weather.seen = true;
|
||||
showCard({ kind: 'Weather', title: 'Rain', swatch: WATER, body: ['Steam that rises into the open sky gathers into clouds and comes back down as rain nearby. On freezing heights it falls as snow.', 'Water is never used up, only moved around. If some leaves the world (dropped into the dock, say), a shower from beyond the claim makes it up.'] });
|
||||
}
|
||||
if (SET.look === 'smooth' && !R.gpuOn && SM.tried && !SM.ok) {
|
||||
SET.look = 'pixel'; saveSettings();
|
||||
toast(SM.why === 'slow' ? 'The smooth look was too slow on this computer, so the pixel look is on. You can try it again in Settings.' : SM.why === 'software' ? 'This browser is drawing without the graphics card, so the smooth look would crawl. The pixel look is on.' : 'This browser cannot draw the smooth look, so the pixel look is on.');
|
||||
}
|
||||
drawMachines();
|
||||
drawMolds(moldAt(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)));
|
||||
drawParticles();
|
||||
@@ -101,7 +96,7 @@ function frame(now) {
|
||||
fpsSm = fpsSm * 0.95 + (1 / Math.max(0.001, dt)) * 0.05;
|
||||
if (SET.showPerf && R.frame % 15 === 0) {
|
||||
let aw = 0, ht = 0; for (let c = 0; c < NCH; c++) { if (awake[c]) aw++; if (hot[c]) ht++; }
|
||||
$('perf').textContent = `${fpsSm.toFixed(0)} fps · frame ${frameMs.toFixed(1)} ms · sim ${simMs.toFixed(1)} ms/tick (move ${(perfTimes.simMove || 0).toFixed(1)}, heat ${(perfTimes.heatPass || 0).toFixed(1)}, rest ${Math.max(0, simMs - (perfTimes.simMove || 0) - (perfTimes.heatPass || 0)).toFixed(1)})\nworld ${(perfTimes.drawWorld || 0).toFixed(1)} ms ${R.gpuOn ? "" : `· light ${(perfTimes.computeLight || 0).toFixed(1)} ms`}${R.gpuOn ? ` · overrides ${(perfMarks.override || 0).toFixed(1)} · upload ${(perfMarks.upload || 0).toFixed(1)} · GPU ${GPU.gpuMs === null ? "—" : GPU.gpuMs.toFixed(1)} ms (light ${(GPU.lightMs || 0).toFixed(1)})` : ""} · overlays ${(perfTimes.overlays || 0).toFixed(1)} ms\nmoving areas ${aw} · hot areas ${ht} / ${NCH} · bars ${ents.length} · view ${R.bw}×${R.bh}`;
|
||||
$('perf').textContent = `${fpsSm.toFixed(0)} fps · frame ${frameMs.toFixed(1)} ms · sim ${simMs.toFixed(1)} ms/tick (move ${(perfTimes.simMove || 0).toFixed(1)}, heat ${(perfTimes.heatPass || 0).toFixed(1)}, rest ${Math.max(0, simMs - (perfTimes.simMove || 0) - (perfTimes.heatPass || 0)).toFixed(1)})\nworld ${(perfTimes.drawWorld || 0).toFixed(1)} ms · overrides ${(perfMarks.override || 0).toFixed(1)} · upload ${(perfMarks.upload || 0).toFixed(1)} · GPU ${GPU.gpuMs === null ? "—" : GPU.gpuMs.toFixed(1)} ms (light ${(GPU.lightMs || 0).toFixed(1)}) · overlays ${(perfTimes.overlays || 0).toFixed(1)} ms\nmoving areas ${aw} · hot areas ${ht} / ${NCH} · bars ${ents.length} · view ${R.bw}×${R.bh}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
// Browser-only clocks; simulation code stays usable without a browser or a clock.
|
||||
let PERF_MARKS = false, PERF_FRAME = null;
|
||||
const perfMarks = {}, perfTimes = {}, perfLast = {};
|
||||
for (const name of ['drawWorld', 'computeLight', 'simMove', 'heatPass']) {
|
||||
for (const name of ['drawWorld', 'simMove', 'heatPass']) {
|
||||
const original = window[name];
|
||||
window[name] = function (...args) {
|
||||
if (!SET.showPerf && !PERF_MARKS) return original.apply(this, args);
|
||||
@@ -9,7 +9,7 @@ for (const name of ['drawWorld', 'computeLight', 'simMove', 'heatPass']) {
|
||||
const result = original.apply(this, args);
|
||||
const ms = performance.now() - start;
|
||||
perfLast[name] = ms;
|
||||
if (name !== 'computeLight') perfTimes[name] = (perfTimes[name] || 0) * 0.9 + ms * 0.1;
|
||||
perfTimes[name] = (perfTimes[name] || 0) * 0.9 + ms * 0.1;
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
+1
-41
@@ -1,44 +1,4 @@
|
||||
// Drill previews: shaken rock cracks, pulse wavefronts and aiming fan.
|
||||
// Rock the drill has shaken but not yet broken shows cracks, more and finer as it nears breaking.
|
||||
// Each crack is a contour line of a noise field (marching squares, cell by cell), so the lines
|
||||
// run on unbroken from one cell to the next and look like real fractures, not a pattern.
|
||||
const CRACK_N = [makeNoise(mulberry32(0xc7ac1)), makeNoise(mulberry32(0xc7ac2)), makeNoise(mulberry32(0xc7ac3))];
|
||||
const CRACK_LV = [{ s: 1 / 7, k: 0.1, w: 0.2 }, { s: 1 / 3.6, k: 0.42, w: 0.14 }, { s: 1 / 2, k: 0.72, w: 0.09 }];
|
||||
function crackSeg(ctx, x, y, n, sc, zp, oy) {
|
||||
const v = (cx, cy) => n(cx * sc + 0.37, cy * sc + 0.61);
|
||||
const a = v(x, y), b = v(x + 1, y), c = v(x + 1, y + 1), d = v(x, y + 1);
|
||||
const pts = [];
|
||||
if ((a < 0) !== (b < 0)) pts.push(x + a / (a - b), y);
|
||||
if ((b < 0) !== (c < 0)) pts.push(x + 1, y + b / (b - c));
|
||||
if ((d < 0) !== (c < 0)) pts.push(x + d / (d - c), y + 1);
|
||||
if ((a < 0) !== (d < 0)) pts.push(x, y + a / (a - d));
|
||||
for (let k = 0; k + 3 < pts.length; k += 4) {
|
||||
ctx.moveTo((pts[k] - R.camX) * zp, (pts[k + 1] - R.camY) * zp + oy);
|
||||
ctx.lineTo((pts[k + 2] - R.camX) * zp, (pts[k + 3] - R.camY) * zp + oy);
|
||||
}
|
||||
}
|
||||
function drawCracks() {
|
||||
const ctx = R.ctx, zp = R.zp;
|
||||
if (zp < 2.5) return;
|
||||
const x0 = Math.max(0, Math.floor(R.camX)), y0 = Math.max(0, Math.floor(R.camY));
|
||||
const x1 = Math.min(W - 1, Math.ceil(R.camX + R.canvas.width / zp)), y1 = Math.min(H - 1, Math.ceil(R.camY + R.canvas.height / zp));
|
||||
const cells = [];
|
||||
for (let y = y0; y <= y1; y++) { const row = y * W; for (let x = x0; x <= x1; x++) { const dp = digp[row + x]; if (dp && PH[mat[row + x]] === PH_SOLID) cells.push(x, y, dp / 255); } }
|
||||
if (!cells.length) return;
|
||||
ctx.save(); ctx.lineCap = 'round';
|
||||
for (let l = 0; l < CRACK_LV.length; l++) {
|
||||
const L = CRACK_LV[l];
|
||||
// a soft lit lip just below each crack, then the dark crack itself
|
||||
for (const pass of [0, 1]) {
|
||||
ctx.beginPath();
|
||||
for (let k = 0; k < cells.length; k += 3) if (cells[k + 2] >= L.k) crackSeg(ctx, cells[k], cells[k + 1], CRACK_N[l], L.s, zp, pass ? 0 : Math.max(1, zp * 0.12));
|
||||
ctx.strokeStyle = pass ? `rgba(18,12,10,${0.78 - l * 0.12})` : 'rgba(255,240,220,0.13)';
|
||||
ctx.lineWidth = Math.max(1, zp * L.w * (pass ? 1 : 1.3));
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
// Drill pulse wavefronts and aiming fan.
|
||||
// The pulses in flight: a wavefront running out from the face of the bot, fading as it goes
|
||||
function pulseRGB(tune) {
|
||||
if (!tune) return '150,215,255';
|
||||
|
||||
+7
-5
@@ -10,14 +10,15 @@ function showLoading(on, p, label, title) {
|
||||
$('loading').hidden = !on;
|
||||
if (on) { $('loadbar').style.width = Math.round((p || 0) * 100) + '%'; $('loadtext').textContent = Math.round((p || 0) * 100) + '%'; }
|
||||
}
|
||||
function enterGame() {
|
||||
function enterGame(renderReady = false) {
|
||||
closeDevConsole(); $('dev-output').textContent = ''; $('dev-input').value = '';
|
||||
$('cards').innerHTML = '';
|
||||
if (R.floaters) R.floaters.length = 0; R.particles.length = 0;
|
||||
GAME.status = ''; GAME.statusT = 0;
|
||||
$('title').hidden = true; $('hud').hidden = false;
|
||||
GAME.running = true; GAME.paused = false;
|
||||
buildSky(); resizeRender(); snapCamera(); resetMap();
|
||||
if (!renderReady) { buildSky(); resizeRender(); snapCamera(); }
|
||||
resetMap();
|
||||
GAME.lastAutosave = performance.now();
|
||||
GAME.drag = null; GAME.lmb = GAME.rmb = false; lastBagKey = '';
|
||||
hudDirty = true; renderHotbar();
|
||||
@@ -41,7 +42,8 @@ async function startNewWorld(seed, w, h, name = '') {
|
||||
showLoading(true, 0.9, 'Saving');
|
||||
await saveWorld('auto');
|
||||
showLoading(true, 1, 'Saving');
|
||||
showLoading(false); closeAllModals(); enterGame();
|
||||
await gpuWarmWorld();
|
||||
showLoading(false); closeAllModals(); enterGame(true);
|
||||
showCard({ kind: 'Welcome', title: 'The dig site', body: ['Fly with WASD. The Drill (1) breaks rock loose. The Pneumatic nozzle (2) picks up and blows the selection. Click or Q/E in the bag selects; Ctrl+click mixes. Hold Ctrl to see everything found. Clear returns to picking what you click. Shift: everything.', 'You owe $20,000 on this claim. Sell what you dig and make at the trade dock next to you: anything that drops through its grate is paid for on the spot, and buyers post orders that pay far more. Every tip lives in the notebook (N).'], hold: 1.8 });
|
||||
} catch (e) { showLoading(false); toast('Could not create a world. Browser storage is unavailable.'); }
|
||||
}
|
||||
@@ -67,8 +69,8 @@ async function startFromSave(sv, onProgress) {
|
||||
GAME.drag = null; GAME.blow.clear();
|
||||
onProgress(0.95, 'Your gear and notebook');
|
||||
await loadCareer();
|
||||
showLoading(false);
|
||||
enterGame();
|
||||
await gpuWarmWorld();
|
||||
showLoading(false); enterGame(true);
|
||||
return true;
|
||||
}
|
||||
async function loadSavedWorld(id) {
|
||||
|
||||
@@ -16,7 +16,6 @@ const SET_TABS = {
|
||||
{ k: 'shake', label: 'Screen shake', type: 'toggle' },
|
||||
],
|
||||
Graphics: [
|
||||
{ k: 'renderer', label: 'Renderer', d: 'WebGPU draws both looks on the graphics card. Legacy uses the original renderer.', type: 'select', opts: [['webgpu', 'WebGPU'], ['legacy', 'Legacy']] },
|
||||
{ k: 'look', label: 'Look', d: 'Smooth draws rounded, outlined ground, round grains and glossy liquids on the graphics card. Pixel is the original look, one square per grain.', type: 'select', opts: [['smooth', 'Smooth'], ['pixel', 'Pixel']] },
|
||||
{ k: 'lighting', label: 'Lighting', d: 'Full is colored light at grain resolution. Fast uses half resolution.', type: 'select', opts: [['off', 'Off'], ['fast', 'Fast'], ['full', 'Full']] },
|
||||
{ k: 'lightEvery', label: 'Light updates', type: 'select', opts: [[1, 'Every frame'], [2, 'Every 2nd frame'], [3, 'Every 3rd frame']] },
|
||||
@@ -98,8 +97,6 @@ function renderSettings() {
|
||||
}
|
||||
}
|
||||
function applySettings() {
|
||||
// choosing Smooth again after it was too slow: try once more, at half resolution
|
||||
if (SET.look === 'smooth' && SM.tried && !SM.ok && SM.why === 'slow' && SM.gl) { SM.ok = true; SM.why = ''; SM.scale = 0.5; SM.n = 0; }
|
||||
saveSettings();
|
||||
document.documentElement.style.setProperty('--ui', SET.uiScale);
|
||||
if (R.canvas) { resizeRender(); R.snapNext = true; }
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
// ---------- Settings ----------
|
||||
const SET_DEFAULT = {
|
||||
zoom: 12, integerScale: true, renderScale: 1, uiScale: 1, fpsCap: 0, showPerf: false, shake: true, cameraSmooth: 0.7, cameraLead: true,
|
||||
renderer: 'webgpu', look: 'smooth', lighting: 'full', lightEvery: 1, ambient: 0.12, daylight: 1, glow: 'low', grain: 1, incandescence: true, particles: 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,
|
||||
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,
|
||||
|
||||
+7
-6
@@ -1,15 +1,15 @@
|
||||
// Startup capabilities and title art.
|
||||
const REQUIRE_GPU_AND_THREADS = false;
|
||||
const REQUIRE_WEBGPU = true;
|
||||
const REQUIRE_THREADS = false;
|
||||
const CAPS = { isolated: self.crossOriginIsolated === true, webgpu: false, adapterInfo: null, ready: false };
|
||||
async function checkCapabilities() {
|
||||
try {
|
||||
const adapter = navigator.gpu && await navigator.gpu.requestAdapter();
|
||||
CAPS.webgpu = !!adapter;
|
||||
if (adapter) await gpuInit(adapter);
|
||||
else SET.renderer = 'legacy';
|
||||
CAPS.webgpu = !!adapter && gpuLimitsOK(adapter) && await gpuInit(adapter);
|
||||
CAPS.requiredLimits = GPU_LIMITS;
|
||||
const info = adapter && adapter.info;
|
||||
if (info) CAPS.adapterInfo = { vendor: info.vendor, architecture: info.architecture, device: info.device, description: info.description };
|
||||
} catch (e) { CAPS.webgpu = false; SET.renderer = 'legacy'; }
|
||||
} catch (e) { CAPS.webgpu = false; }
|
||||
CAPS.ready = true;
|
||||
renderCapabilityNotice();
|
||||
}
|
||||
@@ -19,13 +19,14 @@ function renderCapabilityNotice() {
|
||||
if (!CAPS.webgpu) messages.push(/Firefox\//.test(navigator.userAgent)
|
||||
? 'WebGPU is unavailable. In Firefox, open about:config and set dom.webgpu.enabled to true, then restart. On Linux, use Firefox Nightly. If no graphics adapter is available, try Chrome or Edge.'
|
||||
: 'WebGPU is unavailable. Use a browser with WebGPU (Chrome or Edge).');
|
||||
if (GPU.error) messages.push('WebGPU could not start: ' + GPU.error);
|
||||
const notice = $('cap-notice');
|
||||
notice.replaceChildren(...messages.map(text => { const p = document.createElement('p'); p.textContent = text; return p; }));
|
||||
notice.hidden = !messages.length;
|
||||
}
|
||||
async function canStartGame() {
|
||||
await CAPS_CHECK;
|
||||
return !REQUIRE_GPU_AND_THREADS || (CAPS.isolated && CAPS.webgpu);
|
||||
return (!REQUIRE_WEBGPU || (CAPS.webgpu && GPU.ok)) && (!REQUIRE_THREADS || CAPS.isolated);
|
||||
}
|
||||
const CAPS_CHECK = checkCapabilities();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user