WebGPU pixel look: GPU colours the world, CPU only rare cells (t88 phase 2a)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+14
-5
@@ -29,7 +29,7 @@ function perfSim(names, scene, w, h) {
|
||||
return { total_ms: total, ticks: n, start, end: perfState(), steps,
|
||||
rest_ms: total - Object.values(data).reduce((s, d) => s + d.ms / n, 0) };
|
||||
}
|
||||
function perfRender(lighting, overlays) {
|
||||
async function perfRender(lighting, overlays) {
|
||||
SET.lighting = lighting;
|
||||
PERF_MARKS = true;
|
||||
try {
|
||||
@@ -48,13 +48,22 @@ function perfRender(lighting, overlays) {
|
||||
// Exercise the actual overlay calls and their arguments/conditions in frame().
|
||||
PERF_MARKS = false;
|
||||
for (let k = 0; k < 5; k++) frame(lastT + 1000 / 60);
|
||||
const data = {}, restore = perfWrap(overlays, data);
|
||||
const data = {}, restore = perfWrap(overlays, data), frameStart = performance.now();
|
||||
try { for (let k = 0; k < n; k++) frame(lastT + 1000 / 60); }
|
||||
finally { restore(); }
|
||||
return { drawWorld_ms: draw, computeLight_ms: light, marks_ms: marks,
|
||||
const frame_ms = (performance.now() - frameStart) / n;
|
||||
// One completed timestamp sample for this configuration, outside the CPU measurements.
|
||||
let gpu_ms = null;
|
||||
if (R.gpuOn && GPU.queries) {
|
||||
await GPU.timingPromise;
|
||||
PERF_MARKS = true; drawWorld();
|
||||
await GPU.timingPromise;
|
||||
PERF_MARKS = false; gpu_ms = GPU.gpuMs; marks.gpu = gpu_ms;
|
||||
}
|
||||
return { frame_ms, drawWorld_ms: draw, computeLight_ms: light, marks_ms: marks,
|
||||
overlays: Object.fromEntries(Object.entries(data).map(([key, d]) => [key, { ms_per_frame: d.ms / n, calls: d.calls }])),
|
||||
overlay_ms: Object.values(data).reduce((sum, d) => sum + d.ms / n, 0),
|
||||
scale: SM.scale, actual_look: SET.look };
|
||||
scale: SM.scale, actual_look: SET.look, renderer: R.gpuOn ? 'webgpu' : 'legacy', override_cells: GPU.overrideCount || 0, gpu_ms };
|
||||
} finally { PERF_MARKS = false; }
|
||||
}
|
||||
function perfRun(seconds) {
|
||||
@@ -72,7 +81,7 @@ function perfRun(seconds) {
|
||||
requestAnimationFrame(frame);
|
||||
setTimeout(() => {
|
||||
window.__perfHalt(); PERF_FRAME = null; GAME.paused = true;
|
||||
resolve({ samples, end: perfState(), scale: SM.scale, actual_look: SET.look });
|
||||
resolve({ samples, end: perfState(), scale: SM.scale, actual_look: SET.look, renderer: R.gpuOn ? 'webgpu' : 'legacy', override_cells: GPU.overrideCount || 0, gpu_ms: R.gpuOn ? GPU.gpuMs : null });
|
||||
}, seconds * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
+14
-4
@@ -54,14 +54,14 @@ def report(result, baseline=None):
|
||||
m = result['machine']
|
||||
print(f"GPU: {m['renderer']}\nCores: {m['cores']} | isolated: {m['crossOriginIsolated']} | navigator.gpu: {m['webgpu']}")
|
||||
print(f"Viewport {result['options']['viewport']} | world {result['options']['world']} | seed 424242 | ms (CPU wall time)")
|
||||
print('\nScene Look Zoom Light Grains World Light Prep Colour Upload Blit Overlays Scale')
|
||||
print('\nScene Look Zoom Light Grains World Frame Light Prep Colour Upload Blit Overlays Scale')
|
||||
for scene, data in result['scenes'].items():
|
||||
for key, view in data['views'].items():
|
||||
look, zoom = key.split('/')
|
||||
for light, r in view['lighting'].items():
|
||||
p = r['marks_ms']
|
||||
print(f"{scene:8} {look:6} {zoom:>4} {light:>5} {view['grains']:6d} "
|
||||
f"{r['drawWorld_ms']:7.2f} {p['light']:6.2f} {p['prepare']:5.2f} {p['color']:6.2f} "
|
||||
f"{r['drawWorld_ms']:7.2f} {r.get('frame_ms', 0):6.2f} {p['light']:6.2f} {p['prepare']:5.2f} {p['color']:6.2f} "
|
||||
f"{p['upload']:6.2f} {p['blit']:6.2f} {r['overlay_ms']:8.3f} {r['scale']:5.2f}")
|
||||
print('\nSimulation: average ms/tick (periodic work amortized over 240 ticks)')
|
||||
names = list(next(iter(result['scenes'].values()))['sim']['steps'])
|
||||
@@ -125,8 +125,10 @@ def main():
|
||||
options = dict(viewport=list(args.viewport), world=list(args.world), zooms=zooms,
|
||||
looks=looks, scenes=scenes, seconds=args.seconds)
|
||||
baseline = json.loads(args.compare.read_text()) if args.compare else None
|
||||
if baseline and (baseline.get('schema') != 1 or baseline['options'] != options):
|
||||
ap.error('baseline schema/options differ; use the same matrix, dimensions and seconds')
|
||||
if baseline and (baseline.get('schema') != 1
|
||||
or any(baseline['options'].get(k) != v for k, v in options.items() if k not in ('looks', 'scenes', 'zooms'))
|
||||
or any(not set(options[k]).issubset(baseline['options'][k]) for k in ('looks', 'scenes', 'zooms'))):
|
||||
ap.error('baseline schema/options differ; use a subset of the baseline matrix with the same dimensions and seconds')
|
||||
ensure_playwright()
|
||||
started = time.monotonic()
|
||||
subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT, check=True, stdout=sys.stderr)
|
||||
@@ -154,6 +156,9 @@ def main():
|
||||
const id = raf(t => { pending.delete(id); f(t); }); pending.add(id); return id;
|
||||
};''')
|
||||
pg.goto(url)
|
||||
pg.evaluate('CAPS_CHECK')
|
||||
if not pg.evaluate('GPU.ok'):
|
||||
raise RuntimeError('WebGPU unavailable: ' + pg.evaluate('GPU.error'))
|
||||
pg.add_script_tag(content=(ROOT / 'tools/perf-scenes.js').read_text())
|
||||
pg.add_script_tag(content=(ROOT / 'tools/perf-measure.js').read_text())
|
||||
result['machine'] = pg.evaluate('''() => {
|
||||
@@ -162,6 +167,7 @@ def main():
|
||||
return { renderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER),
|
||||
vendor: dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR),
|
||||
cores: navigator.hardwareConcurrency, crossOriginIsolated, webgpu: !!navigator.gpu,
|
||||
adapter: CAPS.adapterInfo, timestamp_query: !!GPU.queries,
|
||||
user_agent: navigator.userAgent, dpr: devicePixelRatio };
|
||||
}''')
|
||||
if not result['machine']['crossOriginIsolated']:
|
||||
@@ -185,6 +191,8 @@ def main():
|
||||
view['lighting'][light] = pg.evaluate('([l,o]) => perfRender(l,o)', [light, overlays])
|
||||
if view['lighting']['full']['actual_look'] != look or view['lighting']['off']['actual_look'] != look:
|
||||
raise RuntimeError('Look fell back while measuring')
|
||||
if look == 'pixel' and any(r['renderer'] != 'webgpu' for r in view['lighting'].values()):
|
||||
raise RuntimeError('WebGPU fell back while measuring: ' + pg.evaluate('GPU.error'))
|
||||
data['views'][f'{look}/{zoom:g}'] = view
|
||||
for look in looks:
|
||||
for zoom in [2, 6]:
|
||||
@@ -193,6 +201,8 @@ def main():
|
||||
r = pg.evaluate('seconds => perfRun(seconds)', args.seconds)
|
||||
if r['actual_look'] != look:
|
||||
raise RuntimeError('Look fell back during real run')
|
||||
if look == 'pixel' and r['renderer'] != 'webgpu':
|
||||
raise RuntimeError('WebGPU fell back during real run: ' + pg.evaluate('GPU.error'))
|
||||
for key in ['interval', 'work', 'sim']:
|
||||
r[key + '_ms'] = quantiles([s[key] for s in r['samples']])
|
||||
data['runs'][f'{look}/{zoom}'] = r
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Paused comparison scene: ordinary grains, fog, heat, trees and exceptional geometry.
|
||||
const cx = (W / 3) & ~3, cy = 180;
|
||||
window.CAM = [cx, cy];
|
||||
SET.fog = true; SET.shake = false; SET.cameraLead = false;
|
||||
weather.next = 1e15; R.rain = 0;
|
||||
for (let y = cy - 52; y < cy + 52; y++) for (let x = cx - 90; x < cx + 90; x++) {
|
||||
const i = y * W + x;
|
||||
setCell(i, y >= cy + 24 ? STONE : EMPTY, 20, y >= cy + 24 ? F_NAT : 0);
|
||||
bgm[i] = part[i] = mach[i] = occ[i] = 0; blockP[(y >> 2) * BW + (x >> 2)] = 0;
|
||||
}
|
||||
for (let x = cx - 90; x < cx + 90; x++) { surfY[x] = cy + 16; dirtY[x] = cy + 30; graniteY[x] = cy + 38; basaltY[x] = cy + 46; }
|
||||
const fill = (x0,y0,w,h,m,T=20) => { for(let y=y0;y<y0+h;y++)for(let x=x0;x<x0+w;x++)setCell(y*W+x,m,T,F_NAT); };
|
||||
fill(cx-62,cy+6,22,16,WATER); fill(cx-34,cy+11,16,11,MAGMA,1450);
|
||||
fill(cx-33,cy+3,8,8,FIRE,900); fill(cx-12,cy+17,15,6,SAND);
|
||||
fill(cx-10,cy+24,12,5,STONE,1100);
|
||||
for(let y=cy-20;y<cy+16;y++)for(let x=cx-58;x<cx-54;x++)bgm[y*W+x]=WOOD;
|
||||
for(let y=cy-28;y<cy-12;y++)for(let x=cx-68;x<cx-44;x++)if((x-cx+56)**2+(y-cy+20)**2<130)bgm[y*W+x]=LEAVES;
|
||||
for(let x=cx-40;x<cx-34;x++)digp[(cy+24)*W+x]=160;
|
||||
const place=(code,v,dx,dy)=>{const e=placeBlock(code,v,(cx+dx)>>2,(cy+dy)>>2,true);if(e)throw Error(e);};
|
||||
for(let dx=-36;dx<-12;dx+=4)place(BP_BELT,1,dx,-12);
|
||||
place(BP_SCREW,1,-4,-12); place(BP_SCREW,4,0,-12);
|
||||
place(BP_BELT_UP,1,16,-12);place(BP_BELT_UP,1,20,-16);
|
||||
place(BP_PIPE,0,32,-12);place(BP_PIPE,0,36,-12);
|
||||
addMold('ingot',(cx+8)>>2,(cy+8)>>2,RSTONE,true);
|
||||
spawnBar(BARCU,cx-25,cy-16,0,0,700);
|
||||
flyers.push({x:cx-8,y:cy-18,m:SAND,rain:false});
|
||||
ghostBlocks.set(((cy-4)>>2)*BW+((cx-4)>>2),[BP_WALL,RSTONE]);
|
||||
for(let j=0;j<FH;j++)for(let i=0;i<FW;i++)fog[j*FW+i]=Math.max(0,Math.min(255,(cx+72-i*FOG)*5));
|
||||
updateRoof(); rebuildPower();
|
||||
+7
-1
@@ -13,6 +13,7 @@ code and the bot is moved there before the shot. SX, SY are the bot's starting p
|
||||
|
||||
Options:
|
||||
--look pixel|smooth which look to draw (default: pixel; use smooth only to check graphics)
|
||||
--renderer webgpu|legacy pixel renderer (default: webgpu)
|
||||
--seed N world seed (default 424242)
|
||||
--size WxH world size in grains (default 2048x1024)
|
||||
--setup FILE.js code to run once the world is ready (see tools/scenes/ for examples)
|
||||
@@ -51,6 +52,7 @@ def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument('out')
|
||||
ap.add_argument('--look', choices=['pixel', 'smooth'], default='pixel')
|
||||
ap.add_argument('--renderer', choices=['webgpu', 'legacy'], default='webgpu')
|
||||
ap.add_argument('--seed', type=int, default=424242)
|
||||
ap.add_argument('--size', default='2048x1024')
|
||||
ap.add_argument('--setup')
|
||||
@@ -75,6 +77,8 @@ def main():
|
||||
pg.add_init_script(FREEZE if a.freeze else '')
|
||||
pg.goto(url)
|
||||
pg.evaluate("SET.fog = false")
|
||||
pg.evaluate('CAPS_CHECK')
|
||||
pg.evaluate('(r) => { SET.renderer = r; if (r === \'webgpu\' && !GPU.ok) throw Error(GPU.error || \'WebGPU unavailable\'); }', a.renderer)
|
||||
pg.wait_for_timeout(300)
|
||||
# pause the moment the world exists, so the shot depends only on the seed, the setup
|
||||
# code and --ticks (simTick is deterministic), not on how fast the browser ran
|
||||
@@ -99,6 +103,8 @@ def main():
|
||||
pg.evaluate("() => { window.__frozenNow = 5000; window.requestAnimationFrame = () => 0; }")
|
||||
pg.wait_for_timeout(100)
|
||||
pg.evaluate("() => { R.frame = 999; if (typeof SM !== 'undefined') SM.t0 = 0; frame(lastRender + 1000); }")
|
||||
if a.look == 'pixel' and a.renderer == 'webgpu' and not pg.evaluate('R.gpuOn'):
|
||||
raise RuntimeError('WebGPU fell back: ' + pg.evaluate('GPU.error'))
|
||||
kw = {}
|
||||
if a.clip:
|
||||
x, y, w, h = (float(v) for v in a.clip.split(','))
|
||||
@@ -107,7 +113,7 @@ def main():
|
||||
pg.screenshot(path=a.out, **kw)
|
||||
note = pg.evaluate("SET.look === 'smooth' ? (SM.ok ? 'smooth look' : 'smooth look FAILED: ' + SM.why) : 'pixel look'")
|
||||
b.close()
|
||||
print(f'saved {a.out} ({note})')
|
||||
print(f"saved {a.out} ({note}, {a.renderer if a.look == 'pixel' else 'legacy'})")
|
||||
for e in errors:
|
||||
print('ERROR', e)
|
||||
sys.exit(1 if errors else 0)
|
||||
|
||||
+3
-2
@@ -38,6 +38,7 @@ def main():
|
||||
pg.on('pageerror', lambda e: errors.append(str(e)))
|
||||
pg.add_init_script('window.requestAnimationFrame = () => 0;')
|
||||
pg.goto(url)
|
||||
pg.evaluate('CAPS_CHECK')
|
||||
pg.add_script_tag(content=(ROOT / 'tools/perf-scenes.js').read_text())
|
||||
assert pg.evaluate('crossOriginIsolated')
|
||||
# Compare to the unwrapped functions, and hash the byte representation of all grid state.
|
||||
@@ -66,8 +67,8 @@ def main():
|
||||
performance.now = () => 5000;
|
||||
R.rain = rainAt(Math.max(0, Math.min(W - 1, Math.floor(R.camX + R.bw / 2))));
|
||||
try {
|
||||
PERF_MARKS = false; drawWorld(); const a = R.canvas.toDataURL();
|
||||
PERF_MARKS = true; drawWorld(); const b = R.canvas.toDataURL();
|
||||
PERF_MARKS = false; drawWorld(); const a = worldPixelContext().canvas.toDataURL();
|
||||
PERF_MARKS = true; drawWorld(); const b = worldPixelContext().canvas.toDataURL();
|
||||
return a === b;
|
||||
} finally { PERF_MARKS = false; performance.now = now; }
|
||||
}''', look)
|
||||
|
||||
Reference in New Issue
Block a user