Served with isolation headers; start-screen capability notices; tests on the real GPU (t88 phase 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 19:11:42 -04:00
co-authored by Claude Opus 5.5
parent 9781aa3ee1
commit b2c9ad9533
22 changed files with 376 additions and 91 deletions
+38
View File
@@ -0,0 +1,38 @@
"""An isolated Firefox title screen and three seconds of a new pixel world."""
import json
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'tools'))
from use_venv import ensure_playwright
ensure_playwright()
from browser import capabilities, consume_gpu_option, installed, launch
consume_gpu_option()
if not installed('firefox'):
print('SKIP firefox_smoke.py: Playwright Firefox is not installed')
sys.exit(0)
with launch(browser_name='firefox') as (_, page, url):
errors = []
page.on('pageerror', lambda e: errors.append(str(e)))
page.add_init_script("localStorage.setItem('deepgrain.settings', JSON.stringify({look:'pixel', sv:2}));")
page.goto(url)
caps = capabilities(page)
print('Firefox: ' + json.dumps(caps))
assert caps['isolated']
if not caps['webgpu']:
assert page.locator('#cap-notice').is_visible()
text = page.inner_text('#cap-notice')
assert 'about:config' in text and 'dom.webgpu.enabled' in text and 'Firefox Nightly' in text
print('PASS Firefox isolation and capability notice')
page.click('#btn-new')
page.fill('#nw-seed', '424242')
page.select_option('#nw-size', '2048x1024')
page.click('#btn-generate')
page.wait_for_function('GAME.running && !GAME.loading', timeout=60000)
before = page.evaluate('tick')
page.wait_for_timeout(3000)
assert page.evaluate('tick') > before
assert not errors, errors
print('PASS Firefox new world runs for three seconds without page errors')
+28 -15
View File
@@ -4,20 +4,19 @@
import json, sys, pathlib, re
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / 'tools'))
from use_venv import ensure_playwright; ensure_playwright()
from playwright.sync_api import sync_playwright
from browser import launch, consume_gpu_option
consume_gpu_option()
root = pathlib.Path(__file__).resolve().parent.parent
out = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else root / 'shots' / 'play'); out.mkdir(parents=True, exist_ok=True)
url = (root / 'dist' / 'index.html').as_uri()
fails = []
def check(name, cond, extra=''):
print(('PASS ' if cond else 'FAIL ') + name + (f' [{extra}]' if extra != '' else ''))
if not cond: fails.append(name)
with sync_playwright() as p:
b = p.chromium.launch()
ctx = b.new_context(viewport={'width': 1280, 'height': 800})
pg = ctx.new_page()
with launch(viewport={'width': 1280, 'height': 800}) as (b, pg, url):
ctx = pg.context
logs = []
pg.add_init_script("if (!localStorage.getItem('deepgrain.settings')) localStorage.setItem('deepgrain.settings', JSON.stringify({look:'pixel', sv:2}));")
pg.on('pageerror', lambda e: logs.append(f'PAGEERROR: {e}'))
pg.on('console', lambda m: logs.append(f'{m.type}: {m.text}') if m.type == 'error' else None)
pg.goto(url); pg.evaluate("SET.fog = false"); pg.wait_for_timeout(300)
@@ -61,13 +60,13 @@ with sync_playwright() as p:
check('Continue loads the migrated seed and playtime', pg.evaluate('worldSeed === 12345 && GAME.playTime >= 3661 && !!GAME.worldId'))
pg.evaluate("""() => {
window.__pack = packBuffers;
packBuffers = async b => { await new Promise(r => setTimeout(r, 600)); return __pack(b); };
packBuffers = async b => { await new Promise(r => { window.__finishSave = r; }); return __pack(b); };
window.__saving = saveWorld('main', true);
}""")
pg.locator('#saving').wait_for(state='visible')
pg.screenshot(path=str(out / 'saving-progress.png'))
check('slow save shows the saving chip', pg.is_visible('#saving'))
pg.evaluate('async () => { await __saving; packBuffers = __pack; }')
pg.evaluate('async () => { __finishSave(); await __saving; packBuffers = __pack; }')
check('saving chip hides when the save ends', not pg.is_visible('#saving'))
pg.evaluate('quitToTitle()')
names = {}
@@ -248,7 +247,8 @@ with sync_playwright() as p:
led = ev(camera_offset)
check('camera leads toward the pointer when enabled', all(b - a > 2 for a, b in zip(centred, led)), led)
pg.mouse.move(*mouse)
ev(f'GAME.paused = {json.dumps(paused)}; saveSettings()')
# Keep world-to-screen targets fixed after checking both camera modes.
ev(f'GAME.paused = {json.dumps(paused)}; SET.cameraLead = false; saveSettings(); snapCamera()')
def cell_to_screen(x, y):
return ev(f'(() => {{ const dpr = R.canvas.width / innerWidth; const s = worldToScreenPx({x}+0.5, {y}+0.5); return [s.x/dpr, s.y/dpr]; }})()')
def blk(bx, by): return cell_to_screen(bx * 4 + 2, by * 4 + 2)
@@ -281,7 +281,11 @@ with sync_playwright() as p:
# 1. drill: rock breaks loose, nothing enters the bag
pg.mouse.move(*cell_to_screen(bx + 6, gy + 3)); settle(900)
sx, sy = cell_to_screen(bx + 6, gy + 3)
hold(sx, sy, 3500)
pg.mouse.move(sx, sy); pg.mouse.down()
try:
pg.wait_for_function('stats.dug > 20', timeout=15000)
finally:
pg.mouse.up()
settle(700)
check('drill digs with the mouse', ev('stats.dug') > 20, ev('stats.dug'))
check('drill does not fill the bag', ev('bagTotal') == 0, ev('bagTotal'))
@@ -307,7 +311,8 @@ with sync_playwright() as p:
ev("SET.cardSeconds = 0; $('cards').replaceChildren()") # centred cards cover the mouse work area; discovery was checked above
# 2. vacuum, left button: lock onto what you click
pg.keyboard.press('2'); settle(100)
pg.keyboard.press('2')
pg.wait_for_function("GAME.tool === 1 && $('selcard').querySelector('.name')?.textContent === 'Pneumatic nozzle'")
check('Pneumatic nozzle card title and width match the selected tool', ev("GAME.tool === 1 && $('selcard').querySelector('.name').textContent === 'Pneumatic nozzle' && $('selcard').firstElementChild.querySelector('.chip').textContent === `width ${GAME.brush[1]}` && !$('selcard').querySelector('.sub .chip')"))
check('Pneumatic nozzle name appears in the tool list and hotbar title', ev("TOOLS[1].name === 'Pneumatic nozzle' && $('hotbar').children[1].title.startsWith('Pneumatic nozzle')"))
tgt = ev(f'''(() => {{ for (let r = 0; r < 30; r++) for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {{
@@ -492,6 +497,7 @@ with sync_playwright() as p:
check('Shift lights every loose bag row', ev("blowAllList().every(m => [...$('baglist').querySelectorAll('.bagrow.on .nm')].some(e => e.textContent === MD[m].name))"))
pg.screenshot(path=str(out / '03b-blow-all-preview.png'))
pg.keyboard.up('Shift'); settle(150)
pg.wait_for_function("!$('selcard').querySelector('.vacchoice').textContent.includes('Everything') && document.querySelectorAll('#baglist .bagrow.on').length === 1")
check('releasing Shift restores the chosen blow material', ev("$('selcard').querySelector('.vacchoice').textContent.includes(MD[COAL].name) && !$('selcard').querySelector('.vacchoice').textContent.includes('Everything')"))
check('releasing Shift lights only coal', ev("(() => { const rows = [...$('baglist').querySelectorAll('.bagrow.on')]; return rows.length === 1 && rows[0].querySelector('.nm').textContent === MD[COAL].name; })()"))
bot_at = ev('[bot.x, bot.y]')
@@ -1156,6 +1162,12 @@ with sync_playwright() as p:
navigator.getGamepads = () => [window.__pad];
}''')
pg.keyboard.press('1'); settle(100)
# The preceding play-through can leave only air below the bot.
ev('''() => {
GAME.drillTune = 0;
const x0 = Math.floor(bot.x + bot.w / 2), y0 = Math.ceil(bot.y + bot.h) + 2;
for (let y = y0; y < y0 + 8; y++) for (let x = x0 - 4; x <= x0 + 4; x++) setCell(y * W + x, DIRT, 20, F_NAT);
}''')
d0 = ev('stats.dug')
ev("__pad.axes[3] = 0.9; __pad.buttons[7].pressed = true; __pad.buttons[7].value = 1;")
settle(1500)
@@ -1206,10 +1218,11 @@ with sync_playwright() as p:
pg.keyboard.press('Escape'); settle(150)
# Fog has its own paused scene; existing play checks keep the whole world visible.
fb = p.chromium.launch(args=['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'])
fp = fb.new_page(viewport={'width': 1280, 'height': 800})
fog_context = b.new_context(viewport={'width': 1280, 'height': 800})
fp = fog_context.new_page()
if pg.evaluate('!!window.__allowSoftwareGL'):
fp.add_init_script('window.__allowSoftwareGL = true')
fp.on('pageerror', lambda e: logs.append(f'PAGEERROR(fog): {e}'))
fp.add_init_script('window.__allowSoftwareGL = true')
fp.goto(url)
fp.evaluate('startNewWorld(4242, 512, 384)')
fp.wait_for_function('GAME.running')
@@ -1318,7 +1331,7 @@ with sync_playwright() as p:
check('fog draws in the smooth look', fp.evaluate('R.smoothOn && SM.ok'))
check('unseen smooth grains have no terrain shape', fp.evaluate("(() => {const k=(235-R.vy0)*R.bw+300-R.vx0;return R.px[k]===0x04000000 && R.backPx[k]===0 && R.treePx[k]===0 && R.fieldPx[k]===255})()"))
fp.screenshot(path=str(out / 'fog-smooth.png'))
fb.close()
fog_context.close()
# 11. the largest world from the title screen
pg2 = ctx.new_page()
+7 -1
View File
@@ -30,6 +30,9 @@ def run_py(name):
def main():
sys.path.insert(0, str(ROOT / 'tools'))
from browser import consume_gpu_option
consume_gpu_option()
args = [a for a in sys.argv[1:] if not a.startswith('--')]
browser = '--browser' in sys.argv or '--browser-only' in sys.argv
node = '--browser-only' not in sys.argv
@@ -42,7 +45,8 @@ def main():
if browser:
subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT, check=True)
# one at a time: two headless browsers at once can starve each other and flake
for name in ['play.py', 't_smooth.py', 't_dev_console.py']: results.append(run_py(name))
for name in ['play.py', 't_smooth.py', 't_dev_console.py', 't_caps.py', 'firefox_smoke.py']:
results.append(run_py(name))
bad = 0
for name, code, out, secs in results:
fails = [l for l in out.splitlines() if l.startswith('FAIL') or 'Error' in l or l.startswith('FAILS: [') and l != 'FAILS: []']
@@ -50,6 +54,8 @@ def main():
ok = code == 0 and not [l for l in fails if l.startswith('FAIL ') or 'Error' in l]
bad += not ok
print(f"{'ok ' if ok else 'FAIL'} {name:22s} {passes:3d} checks passed {secs:5.1f}s")
for line in out.splitlines():
if line.startswith('SKIP '): print(' ' + line)
if not ok:
for l in (fails or out.splitlines()[-15:]): print(' ' + l)
print('ALL PASSED' if not bad else f'{bad} SUITE(S) FAILED')
+52
View File
@@ -0,0 +1,52 @@
"""Capability detection, title warnings and the future startup gate."""
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'tools'))
from use_venv import ensure_playwright
ensure_playwright()
from browser import capabilities, consume_gpu_option, launch
consume_gpu_option()
for case in ['native', 'absent', 'null', 'reject', 'firefox', 'required']:
with launch() as (_, page, url):
errors = []
page.on('pageerror', lambda e: errors.append(str(e)))
if case != 'native':
gpu = 'undefined' if case == 'absent' else "{requestAdapter: async () => null}"
if case == 'reject': gpu = "{requestAdapter: async () => {throw new Error('adapter denied')}}"
page.add_init_script(f"Object.defineProperty(navigator, 'gpu', {{value: {gpu}}});")
page.add_init_script("Object.defineProperty(self, 'crossOriginIsolated', {value: false});")
if case == 'firefox':
page.add_init_script("Object.defineProperty(navigator, 'userAgent', {value: 'Firefox/157.0'});")
if case == 'required':
def require(route):
response = route.fetch()
route.fulfill(response=response, body=response.text().replace(
'const REQUIRE_GPU_AND_THREADS = false;', 'const REQUIRE_GPU_AND_THREADS = true;'))
page.route('**/index.html', require)
page.goto(url)
caps = capabilities(page)
page.wait_for_function("!$('title').hidden")
if case == 'native':
assert caps['isolated']
assert page.locator('#cap-notice').is_hidden() == caps['webgpu']
else:
assert not caps['webgpu'] and caps['adapterInfo'] is None
notice = page.inner_text('#cap-notice')
assert 'python3 tools/serve.py' in notice and "opened from a file it can't" in notice
assert ('Firefox Nightly' in notice and 'dom.webgpu.enabled' in notice) if case == 'firefox' else 'Chrome or Edge' in notice
if case == 'required':
assert not page.evaluate('canStartGame()')
page.evaluate('startNewWorld(424242, 512, 384)')
assert not page.evaluate('GAME.running || GAME.loading')
assert not page.evaluate('startFromSave({})')
else:
assert page.evaluate('canStartGame()')
if case == 'absent':
Path('shots').mkdir(exist_ok=True)
page.screenshot(path='shots/t88-p1-notice.png')
page.evaluate("SET.look='pixel'; startNewWorld(424242, 512, 384)")
page.wait_for_function('GAME.running && !GAME.loading', timeout=60000)
assert not errors, errors
print(f'PASS capabilities: {case}')
+4 -5
View File
@@ -2,7 +2,8 @@
import pathlib, sys
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / 'tools'))
from use_venv import ensure_playwright; ensure_playwright()
from playwright.sync_api import sync_playwright
from browser import launch, consume_gpu_option
consume_gpu_option()
ROOT = pathlib.Path(__file__).resolve().parent.parent
SHOTS = ROOT / 'shots'
@@ -12,13 +13,11 @@ def check(name, ok):
print(('PASS ' if ok else 'FAIL ') + name)
if not ok: fails.append(name)
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={'width': 1280, 'height': 760})
with launch() as (browser, page, url):
errors = []
page.on('pageerror', lambda e: errors.append(str(e)))
page.add_init_script("localStorage.setItem('deepgrain.settings', JSON.stringify({look:'pixel', sv:2}));")
page.goto((ROOT / 'dist/index.html').as_uri())
page.goto(url)
page.wait_for_function("!$('title').hidden")
page.evaluate("startNewWorld(424242, 512, 384, 'Console test')")
page.wait_for_function('GAME.running && !GAME.loading')
+13 -13
View File
@@ -2,21 +2,20 @@
import sys, pathlib
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / 'tools'))
from use_venv import ensure_playwright; ensure_playwright()
from playwright.sync_api import sync_playwright
from browser import launch, consume_gpu_option
consume_gpu_option()
root = pathlib.Path(__file__).resolve().parent.parent
out = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else root / 'shots' / 'smooth'); out.mkdir(parents=True, exist_ok=True)
fails = []
def check(name, cond, extra=''):
print(('PASS ' if cond else 'FAIL ') + name + (f' [{extra}]' if extra != '' else ''))
if not cond: fails.append(name)
with sync_playwright() as p:
b = p.chromium.launch(args=['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'])
pg = b.new_page(viewport={'width': 1024, 'height': 640})
with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
logs = []
pg.on('pageerror', lambda e: logs.append(f'PAGEERROR: {e}'))
pg.on('console', lambda m: logs.append(f'{m.type}: {m.text}') if m.type in ('error', 'warning') else None)
pg.add_init_script('window.__allowSoftwareGL = true; localStorage.clear();')
pg.goto((root / 'dist' / 'index.html').as_uri()); pg.evaluate('SET.fog = false'); pg.wait_for_timeout(300)
pg.add_init_script('localStorage.clear();')
pg.goto(url); pg.evaluate('SET.fog = false'); pg.wait_for_timeout(300)
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
@@ -283,15 +282,16 @@ with sync_playwright() as p:
}""")
check('filter drag previews pass stored travel bits to the join rule',
bool(filter_plans) and all(v == (128 if c == ev('BP_FILTER_DOWN') else 0) for c, v in filter_plans), filter_plans)
# a browser without hardware drawing gets the pixel look and a note
pg2 = b.new_page(viewport={'width': 800, 'height': 500})
pg2.add_init_script('localStorage.clear();')
pg2.goto((root / 'dist' / 'index.html').as_uri()); pg2.evaluate('SET.fog = false'); 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]'))
errs = [l for l in logs if 'PAGEERROR' in l or l.startswith('error')]
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'); 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)