"""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) dust=pg.evaluate("""()=>{ const particles=SET.particles,fogOn=SET.fog;SET.particles=true;SET.fog=false; R.particles=[];EV.length=0;emit({t:'debris',x:bot.x,y:bot.y,m:RSTONE,dust:true});drainEvents(); const spawned=R.particles.length===1 && R.particles[0].dust===true; R.ctx.clearRect(0,0,R.canvas.width,R.canvas.height);drawParticles(); const data=R.ctx.getImageData(0,0,R.canvas.width,R.canvas.height).data; let visible=0,soft=0;for(let i=3;i4 and dust['soft']==dust['visible'] and dust['expired'] and dust['disabled'],dust print('PASS pulse dust events draw a visible soft puff, expire and respect particles setting',dust) pg.add_script_tag(path=str(root/'tools/perf-scenes.js')) pg.evaluate("async()=>{perfScene('factory',1024,640);await startThreads();simPool.force=true;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,workers:simPool.workers.length,active:simPool.activeMax}; }''') assert min(result['checked'])>1000 and result['cleanCells']==0 and result['workers']>0 and result['active']>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{ const code=GPU_SMOOTH.slice(GPU_SMOOTH.indexOf('fn h21'),GPU_SMOOTH.indexOf('fn lin'))+` @vertex fn vs(@builtin(vertex_index) i:u32)->@builtin(position) vec4f { let p=array(vec2f(-1,-1),vec2f(3,-1),vec2f(-1,3));return vec4f(p[i],0,1); } @fragment fn fs(@builtin(position) p:vec4f)->@location(0) vec4f { let edge=floor(p.x/2.);let along=floor(p.y)*0.1; let epsilon=select(-0.0001,0.0001,u32(p.x)%2u==1u); return vec4f(vn(vec2f(edge+epsilon,along)),vn(vec2f(along,edge+epsilon)),0,1); }`; const module=GPU.device.createShaderModule({code}); const pipeline=await GPU.device.createRenderPipelineAsync({layout:'auto', vertex:{module,entryPoint:'vs'},fragment:{module,entryPoint:'fs',targets:[{format:'rgba8unorm'}]}}); const texture=GPU.device.createTexture({size:[960,100],format:'rgba8unorm', usage:GPUTextureUsage.RENDER_ATTACHMENT|GPUTextureUsage.COPY_SRC}); const buffer=GPU.device.createBuffer({size:960*100*4,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ}); const encoder=GPU.device.createCommandEncoder(); const pass=encoder.beginRenderPass({colorAttachments:[{ view:texture.createView(),loadOp:'clear',storeOp:'store'}]}); pass.setPipeline(pipeline);pass.draw(3);pass.end(); encoder.copyTextureToBuffer({texture},{buffer,bytesPerRow:960*4},[960,100]); GPU.device.queue.submit([encoder.finish()]);await buffer.mapAsync(GPUMapMode.READ); const pixels=new Uint8Array(buffer.getMappedRange());let max=0; for(let y=0;y<100;y++)for(let x=0;x<960;x+=2)for(let c=0;c<2;c++) { const i=(y*960+x)*4+c;max=Math.max(max,Math.abs(pixels[i]-pixels[i+4])); } buffer.unmap();buffer.destroy();texture.destroy();return max; }""") assert noise<=1,noise print('PASS smooth noise is continuous across horizontal and vertical lattice boundaries',noise) motion=pg.evaluate("""async()=>{ const results=[],now=performance.now.bind(performance);performance.now=()=>5000; try { SET.fog=false;SET.glow='low';SET.lightEvery=1;R.lens=false; for(const look of ['pixel','smooth'])for(const light of ['full','fast','off']) { SET.look=look;SET.lighting=light; for(let n=0;n<12;n++) { SET.zoom=6-n*0.37;resizeRender();R.camX=80.25+n*7.3;R.camY=5.5+n*0.43; R.rain=0;R.frame++;drawWorld();await GPU.device.queue.onSubmittedWorkDone(); } const a=worldPixelContext().getImageData(0,0,960,600).data; const cleanCells=GPU.uploadCells; // Discard all view textures/light history as well as the resident-world cache. GPU.resident.fill(0);GPU.viewKey=null;GPU.lightFrame=undefined; 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')