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:
+20
-31
@@ -1,4 +1,4 @@
|
||||
"""The smooth look: the WebGPU pass compiles, draws, falls back cleanly, and the thermal lens still works."""
|
||||
"""The smooth look: the WebGPU pass compiles and draws, and the thermal lens still works."""
|
||||
import sys, pathlib
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / 'tools'))
|
||||
from use_venv import ensure_playwright; ensure_playwright()
|
||||
@@ -19,7 +19,7 @@ with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
||||
pg.click('#btn-new'); pg.fill('#nw-seed', '777'); pg.click('#btn-generate')
|
||||
pg.wait_for_function('GAME.running === true', timeout=20000); pg.wait_for_timeout(1500)
|
||||
ev = pg.evaluate
|
||||
check('the smooth look is the default and is drawing', ev("SET.look === 'smooth' && GPU.ok && R.gpuOn && R.smoothOn"), ev('[SET.look, SM.ok, SM.why, R.smoothOn]'))
|
||||
check('the smooth look is the default and is drawing', ev("SET.look === 'smooth' && GPU.ok && R.gpuOn && R.smoothOn"), ev('[SET.look, GPU.ok, GPU.error, R.smoothOn]'))
|
||||
# the canvas shows the GPU image: sample a sky pixel and a ground pixel, they must differ and not be black
|
||||
px = ev("""(() => { const c = worldPixelContext().getImageData(R.canvas.width >> 1, 4, 1, 1).data, g = worldToScreenPx(bot.x + 2, surfY[Math.floor(bot.x)] + 6), d = worldPixelContext().getImageData(Math.round(g.x), Math.round(g.y), 1, 1).data; return [Array.from(c), Array.from(d)]; })()""")
|
||||
check('it draws sky and ground', sum(px[0][:3]) > 200 and px[0][:3] != px[1][:3], px)
|
||||
@@ -27,15 +27,11 @@ with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
||||
ev("player.lens = true"); pg.keyboard.press('h'); pg.wait_for_timeout(1500)
|
||||
check('the thermal lens draws in pixels', ev('R.lens && !R.smoothOn'))
|
||||
pg.keyboard.press('h'); pg.wait_for_timeout(1500)
|
||||
check('and the smooth look comes back after it', ev('!R.lens && R.smoothOn'), ev('[R.lens, R.smoothOn, SET.look, SM.ok, SM.why, SM.ms, SM.scale]'))
|
||||
check('and the smooth look comes back after it', ev('!R.lens && R.smoothOn'), ev('[R.lens, R.smoothOn, SET.look, GPU.ok, GPU.error]'))
|
||||
ev("SET.look = 'pixel'"); pg.wait_for_timeout(1500)
|
||||
check('the pixel look can be chosen', ev('!R.smoothOn'))
|
||||
pg.screenshot(path=str(out / 'pixel.png'))
|
||||
ev("SET.look = 'smooth'"); pg.wait_for_timeout(1500)
|
||||
ev("SET.renderer = 'legacy'; SM.scale = 0.5"); pg.wait_for_timeout(1500)
|
||||
st = ev('[R.smoothOn, SM.scale, SM.canvas.width, R.canvas.width, SET.look, SM.ok, SM.why]'); check('half-resolution smooth still draws', (st[0] and st[2] == round(st[3] * st[1])) or st[1] > 0.5, st)
|
||||
ev("SET.renderer = 'webgpu'"); pg.wait_for_timeout(300)
|
||||
check('smooth returns to WebGPU after legacy comparison', ev('R.gpuOn && R.smoothOn'))
|
||||
# Uniform colours isolate the solid/loose seam from material noise and lighting.
|
||||
ground = ev('''() => {
|
||||
GAME.paused = true; requestAnimationFrame = () => 0;
|
||||
@@ -50,16 +46,16 @@ with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
||||
const at = (x, y) => { const p=worldToScreenPx(x+0.5,y+0.5), c=worldPixelContext().getImageData(p.x,p.y,1,1).data; return c[0]+c[1]+c[2]; };
|
||||
const out = {};
|
||||
for (const lens of [false, true]) {
|
||||
R.lens = lens; SET.look = 'pixel'; fill(RSTONE); drawWorld();
|
||||
R.lens = lens; SET.look = 'pixel'; fill(RSTONE); renderDirty.fill(1); GPU.world=null; drawWorld();
|
||||
const edge = at(140, 140), inside = at(140, 142), loose = at(140, 130);
|
||||
fill(STONE); drawWorld();
|
||||
fill(STONE); renderDirty.fill(1); GPU.world=null; drawWorld();
|
||||
out[lens ? 'thermal' : 'pixel'] = [edge, inside, loose, at(140, 140)];
|
||||
}
|
||||
R.lens = false;
|
||||
const i = 140 * W + 140;
|
||||
flags[i] = 0; fill(RSTONE); drawWorld(); const built = at(140, 140);
|
||||
fill(STONE); drawWorld(); out.built = [built, at(140, 140)]; flags[i] = F_NAT;
|
||||
fill(RSTONE); SET.look = 'smooth'; SM.scale = 1; drawWorld();
|
||||
flags[i] = 0; fill(RSTONE); renderDirty.fill(1); GPU.world=null; drawWorld(); const built = at(140, 140);
|
||||
fill(STONE); renderDirty.fill(1); GPU.world=null; drawWorld(); out.built = [built, at(140, 140)]; flags[i] = F_NAT;
|
||||
fill(RSTONE); SET.look = 'smooth'; renderDirty.fill(1); GPU.world=null; drawWorld();
|
||||
const sample = (x, y) => {
|
||||
const p = worldToScreenPx(x, y);
|
||||
const c = worldPixelContext().getImageData(Math.floor(p.x), Math.floor(p.y), 1, 1).data;
|
||||
@@ -125,8 +121,8 @@ with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
||||
check('off hides both label and vein outline', not cursor['off'] and cursor['offStrokes'] == 0)
|
||||
for look in ['pixel', 'smooth']:
|
||||
ev("""look => {
|
||||
SET.look=look; SM.scale=1; SET.cursorLabel='ores'; GAME.aim={x:124,y:122}; cursorLabelKey=null;
|
||||
$('cards').replaceChildren(); renderInfo(); drawWorld(); drawHoveredVein(); drawBot();
|
||||
SET.look=look; SET.cursorLabel='ores'; GAME.aim={x:124,y:122}; cursorLabelKey=null;
|
||||
$('cards').replaceChildren(); renderInfo(); renderDirty.fill(1); GPU.world=null; drawWorld(); drawHoveredVein(); drawBot();
|
||||
const p=worldToScreenPx(124,122); drawCursorLabel(p.x,p.y,1);
|
||||
}""", look)
|
||||
pg.screenshot(path=str(root / 'shots' / ('t77-vein.png' if look == 'pixel' else 't77-vein-smooth.png')))
|
||||
@@ -145,8 +141,8 @@ with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
||||
setCell(cell,SAND,22,0);setCell(control,SAND,22,0);
|
||||
noiseB[cell]=noiseB[control]=128; GPU.world=null;
|
||||
const at=i=>{ if(R.smoothOn) return R.px[(((i/W)|0)-R.vy0)*R.bw+i%W-R.vx0]; const p=worldToScreenPx(i%W+0.5,((i/W)|0)+0.5), c=worldPixelContext().getImageData(p.x,p.y,1,1).data; return c[0]|c[1]<<8|c[2]<<16; };
|
||||
SET.look='pixel';drawWorld();const colors=[at(cell)&0xffffff,at(control)&0xffffff];
|
||||
SET.look='smooth';drawWorld();const grainClass=at(cell)>>>24;
|
||||
SET.look='pixel';renderDirty.fill(1); GPU.world=null; drawWorld();const colors=[at(cell)&0xffffff,at(control)&0xffffff];
|
||||
SET.look='smooth';renderDirty.fill(1); GPU.world=null; drawWorld();const grainClass=at(cell)>>>24;
|
||||
return {colors,grainClass};
|
||||
}''')
|
||||
check('sidewall stays behind the grain colour in pixels and smooth',
|
||||
@@ -163,7 +159,7 @@ with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
||||
try {
|
||||
const out={};
|
||||
for(const look of ["pixel","smooth"]) {
|
||||
SET.look=look;SM.scale=1;drawWorld();
|
||||
SET.look=look;renderDirty.fill(1); GPU.world=null; drawWorld();
|
||||
const bad=[], copper=c=>c[0]>c[1]*1.15 && c[1]>c[2]*1.1;
|
||||
const sample=(x,y)=>{const p=worldToScreenPx(x,y);return Array.from(worldPixelContext().getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data);};
|
||||
for(const j of JOIN_CASES.filter(j=>j.code===BP_SCREW)) {
|
||||
@@ -215,9 +211,9 @@ with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
||||
const plan=new Map([[b,[BP_SCREW,j.v]],[nb,[BP_SCREW,j.straight]]]);
|
||||
removeBlock(j.bx,j.by);removeBlock(j.nx,j.ny);
|
||||
const sample=(x,y)=>{const p=worldToScreenPx(j.nx*4+x,j.ny*4+y);return Array.from(worldPixelContext().getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data).join(',');};
|
||||
SM.scale=1;drawWorld();const bg=sample(ux,uy),empty=sample(2,2);
|
||||
renderDirty.fill(1); GPU.world=null; drawWorld();const bg=sample(ux,uy),empty=sample(2,2);
|
||||
for(const [n,piece] of plan)ghostBlocks.set(n,piece);
|
||||
drawWorld();
|
||||
renderDirty.fill(1); GPU.world=null; drawWorld();
|
||||
if(sample(ux,uy)!==bg || sample(2,2)===empty)bad.push(['ghost elbow silhouette',look,j]);
|
||||
ghostBlocks.clear();
|
||||
const ctx=R.ctx,canvas=document.createElement('canvas');canvas.width=canvas.height=192;
|
||||
@@ -234,16 +230,16 @@ with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
||||
}''')
|
||||
check('both looks: ghosts and planned previews leave the unused elbow corner empty', not previews, previews[:2])
|
||||
tree_tags = ev('''() => {
|
||||
SET.look='smooth';SM.scale=1;
|
||||
SET.look='smooth';
|
||||
const now=performance.now.bind(performance),frozen=now();performance.now=()=>frozen;
|
||||
try {
|
||||
const j=JOIN_CASES.find(j=>j.v===5 && j.end===1 && !j.vertical && j.spot===2 && j.code===BP_SCREW);
|
||||
const bx=j.nx+1,by=j.ny;
|
||||
const sample=()=>{const p=worldToScreenPx(bx*4,by*4);return Array.from(worldPixelContext().getImageData(Math.floor(p.x),Math.floor(p.y),4*R.zp,4*R.zp).data).join(',');};
|
||||
drawWorld();const plain=sample(),bad=[];
|
||||
renderDirty.fill(1); GPU.world=null; drawWorld();const plain=sample(),bad=[];
|
||||
for(const m of [WOOD,LEAVES]) {
|
||||
for(let y=by*4;y<by*4+4;y++)for(let x=bx*4;x<bx*4+4;x++)bgm[y*W+x]=m;
|
||||
drawWorld();if(sample()!==plain)bad.push(m);
|
||||
renderDirty.fill(1); GPU.world=null; drawWorld();if(sample()!==plain)bad.push(m);
|
||||
}
|
||||
for(let y=by*4;y<by*4+4;y++)for(let x=bx*4;x<bx*4+4;x++)bgm[y*W+x]=0;
|
||||
return bad;
|
||||
@@ -257,8 +253,8 @@ with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
||||
slopeDetailAt=()=>0;performance.now=()=>frozen;
|
||||
const sample=()=>{const p=worldToScreenPx(j.bx*BS+0.4,j.by*BS+2.5);return Array.from(worldPixelContext().getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data);};
|
||||
for(const look of ['pixel','smooth']) {
|
||||
SET.look=look;SM.scale=1;drawWorld();const joined=sample();
|
||||
removeBlock(j.nx,j.ny);drawWorld();const free=sample();
|
||||
SET.look=look;renderDirty.fill(1); GPU.world=null; drawWorld();const joined=sample();
|
||||
removeBlock(j.nx,j.ny);renderDirty.fill(1); GPU.world=null; drawWorld();const free=sample();
|
||||
placeBlock(BP_BELT,j.straight,j.nx,j.ny,true);
|
||||
out[look]=[joined,free];
|
||||
}
|
||||
@@ -289,12 +285,5 @@ with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
||||
check('no errors', not errs, errs[:3])
|
||||
b.close()
|
||||
|
||||
# a browser without hardware drawing gets the pixel look and a note
|
||||
with launch(gpu='soft', viewport={'width': 800, 'height': 500}) as (_, pg2, soft_url):
|
||||
pg2.add_init_script('window.__allowSoftwareGL = false; localStorage.clear();')
|
||||
pg2.goto(soft_url); pg2.evaluate("SET.fog = false; SET.renderer='legacy'"); pg2.wait_for_timeout(300)
|
||||
pg2.click('#btn-new'); pg2.fill('#nw-seed', '5'); pg2.click('#btn-generate')
|
||||
pg2.wait_for_function('GAME.running === true', timeout=20000); pg2.wait_for_timeout(1200)
|
||||
check('software rendering falls back to pixels with a note', pg2.evaluate("SET.look === 'pixel' && !R.smoothOn && SM.why === 'software'") and pg2.locator('.toast', has_text='graphics card').count() >= 1, pg2.evaluate('[SET.look, SM.why]'))
|
||||
print('FAILS:', fails)
|
||||
sys.exit(1 if fails else 0)
|
||||
|
||||
Reference in New Issue
Block a user