"""Dirty uploads, default limits, warm pipelines, world replacement and device recovery.""" import pathlib, sys sys.path.insert(0,str(pathlib.Path(__file__).resolve().parent.parent/'tools')) from use_venv import ensure_playwright; ensure_playwright() from browser import launch, consume_gpu_option consume_gpu_option() root=pathlib.Path(__file__).resolve().parent.parent with launch(viewport={'width':960,'height':600}) as (_,pg,url): errors=[] pg.on('pageerror',lambda e:errors.append(str(e))) pg.on('console',lambda m:errors.append(m.text) if m.type=='error' else None) pg.add_init_script('requestAnimationFrame=()=>0;') pg.goto(url);pg.evaluate('CAPS_CHECK') assert pg.evaluate('GPU.ok'),pg.evaluate('GPU.error') assert pg.evaluate('GPU.device.limits.maxStorageTexturesPerShaderStage===4') print('PASS all pipelines compile with default limits',pg.evaluate('GPU_LIMITS')) startup=pg.evaluate("""async()=>{ await startNewWorld(424242,640,384); GAME.paused=true; const samples=[]; for(let n=0;n<10;n++) {const t=performance.now();drawWorld();await GPU.device.queue.onSubmittedWorkDone();samples.push(performance.now()-t);} return {compile:GPU.initMs,warm:GPU.warmMs,first:samples[0],normal:samples.slice(1)}; }""") assert startup['warm']>=0 print('PASS both looks submit behind loading screen; first visible frame ms',startup) pg.add_script_tag(path=str(root/'tools/perf-scenes.js')) pg.evaluate("perfScene('factory',1024,640); SET.look='pixel'; SET.zoom=2;resizeRender();snapCamera();drawWorld();") result=pg.evaluate('''async () => { const results=[]; for(let n=0;n<240;n++) { simTick(); if(n%20===0) { fogLook(bot.x,bot.y); R.camX=(n*7)%400; R.camY=(n*3)%280; SET.look=n%40?'smooth':'pixel';R.frame++;drawWorld(); results.push(await gpuCheckUploads()); } } drawWorld();await gpuCheckUploads(); drawWorld();return {checked:results,cleanCells:GPU.uploadCells}; }''') assert min(result['checked'])>1000 and result['cleanCells']==0,result print('PASS busy ticks, heat, fog, scrolling, both looks: GPU bytes equal full CPU data; clean frame uploads no world cells',result) parity=pg.evaluate("""async()=>{ const results=[]; const now=performance.now.bind(performance),frozen=now();performance.now=()=>frozen; try { for(const look of ['pixel','smooth'])for(const light of ['full','fast','off'])for(const lens of [false,true])for(const fogOn of [false,true]) { SET.look=look;SET.lighting=light;R.lens=lens;SET.fog=fogOn;R.frame++; R.rain=0;drawWorld();await GPU.device.queue.onSubmittedWorkDone(); const a=worldPixelContext().getImageData(0,0,960,600).data; GPU.resident.fill(0);R.rain=0;drawWorld();await GPU.device.queue.onSubmittedWorkDone(); const b=worldPixelContext().getImageData(0,0,960,600).data; let max=0;for(let i=0;i{ SET.lightEvery=3;R.frame=1002;drawWorld();const old=GPU.lightFrame,origin=R.lx0; R.camX+=4;R.frame=1003;drawWorld();const held=GPU.lightFrame===old && R.lx0===origin; R.frame=1005;drawWorld();const updated=GPU.lightFrame===1005 && R.lx0!==origin; SET.lightEvery=1;return {held,updated}; }""") assert all(cadence.values()),cadence print('PASS lighting cadence holds and then updates its camera origin') # Force paths that can mutate sleeping chunks, away from the busy factory. pg.evaluate('''() => { const i=500*W+800; setCell(i,STONE,20,F_NAT);digp[i]=120;renderDirtyIdx(i); setTreeCell(801,450,WOOD);fogRevealAll(); R.camX=650;R.camY=400;drawWorld(); }''') assert pg.evaluate('gpuCheckUploads()')>1000 pg.evaluate('generateWorld(777,640,384,null);buildSky();resizeRender();snapCamera();drawWorld();') assert pg.evaluate('GPU.world===mat && GPU.ok') assert pg.evaluate('gpuCheckUploads()')>1000 print('PASS world replacement refreshes resident chunks and static data') assert pg.evaluate('R.ctx.getImageData(10,10,1,1).data[3]===0 && worldPixelContext().getImageData(10,10,1,1).data[3]===255') print('PASS GPU world stays opaque under transparent overlays') cracks=pg.evaluate("""async()=>{ SET.zoom=12;SET.look='pixel';SET.fog=false;SET.lighting='off';SET.glow='off';SET.grain=0; resizeRender();R.camX=200;R.camY=160; for(let y=170;y<192;y++)for(let x=215;x<260;x++)setCell(y*W+x,STONE,20,F_NAT); drawWorld();const clean=worldPixelContext().getImageData(0,0,960,600).data; for(let y=170;y<192;y++)for(let x=215;x<260;x++){digp[y*W+x]=240;renderDirtyXY(x,y);} R.frame=1000;drawWorld();await GPU.device.queue.onSubmittedWorkDone(); const damaged=worldPixelContext().getImageData(0,0,960,600).data; let changed=0;for(let i=0;i1000,cracks pg.screenshot(path=str(root/'shots/t88-drill-pixel.png')) pg.evaluate("SET.look='smooth';drawWorld()") pg.screenshot(path=str(root/'shots/t88-drill-smooth.png')) print('PASS stationary drill damage changes GPU colour in both looks',cracks) timing=pg.evaluate('''async () => { const originalC=GPU.device.createComputePipeline,originalR=GPU.device.createRenderPipeline; GPU.device.createComputePipeline=GPU.device.createRenderPipeline=()=>{throw Error('synchronous pipeline compilation');}; const samples=[]; try { for(const [look,zoom] of [['pixel',12],['smooth',12],['smooth',2],['pixel',2],['smooth',6]]) { SET.look=look;SET.zoom=zoom;resizeRender();snapCamera();const start=performance.now();drawWorld(); await GPU.device.queue.onSubmittedWorkDone();samples.push({look,zoom,first:performance.now()-start}); const times=[];for(let n=0;n<8;n++){const t=performance.now();drawWorld();await GPU.device.queue.onSubmittedWorkDone();times.push(performance.now()-t);} samples.at(-1).normal=Math.max(...times); } } finally {GPU.device.createComputePipeline=originalC;GPU.device.createRenderPipeline=originalR;} return {init:GPU.initMs,samples}; }''') print('PASS look and zoom switches create no pipelines; timings ms',timing) # Successful recovery must recreate resources and preserve simulation arrays. pg.evaluate('window.savedMat=mat; GPU.device.destroy();') pg.wait_for_function('GPU.ok && !GPU.recovering',polling=20,timeout=60000) assert pg.evaluate('mat===savedMat') pg.evaluate('drawWorld()') assert pg.evaluate('gpuCheckUploads()')>1000 print('PASS device loss recreates the device once without changing the world') pg.evaluate("Object.defineProperty(navigator,'gpu',{value:{requestAdapter:async()=>null}});GPU.device.destroy();") pg.wait_for_function('GPU.stopped',polling=20,timeout=60000) assert pg.evaluate('GAME.paused') and 'Reload' in pg.inner_text('#m-dialog') assert pg.evaluate("DB.get(GAME.worldId+':auto').then(s=>!!s)") print('PASS failed recovery stops frames, autosaves and offers Reload') assert not errors,errors print('PASS no page or WebGPU validation errors')