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:
@@ -0,0 +1,65 @@
|
||||
"""Shared isolated HTTP sessions; callers attach hooks before navigating to url."""
|
||||
import argparse
|
||||
from contextlib import contextmanager
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from urllib.parse import quote
|
||||
|
||||
from server import DIST, isolated_server
|
||||
|
||||
GPU_ARGS = ['--use-angle=vulkan', '--enable-features=Vulkan,VulkanFromANGLE', '--enable-gpu',
|
||||
'--ignore-gpu-blocklist', '--enable-gpu-rasterization', '--enable-unsafe-webgpu']
|
||||
SOFT_ARGS = ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader']
|
||||
DEFAULT_GPU = 'real'
|
||||
|
||||
|
||||
def gpu_option(parser):
|
||||
parser.add_argument('--gpu', choices=['real', 'soft'],
|
||||
default=os.environ.get('DEEPGRAIN_GPU', DEFAULT_GPU))
|
||||
|
||||
|
||||
def consume_gpu_option():
|
||||
"""Keep the older scripts' positional arguments."""
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
gpu_option(parser)
|
||||
options, remaining = parser.parse_known_args()
|
||||
os.environ['DEEPGRAIN_GPU'] = options.gpu
|
||||
sys.argv[1:] = remaining
|
||||
return options.gpu
|
||||
|
||||
|
||||
@contextmanager
|
||||
def launch(*, gpu=None, browser_name='chromium', viewport=None, page_path=None, args=()):
|
||||
from playwright.sync_api import sync_playwright
|
||||
gpu = gpu or os.environ.get('DEEPGRAIN_GPU', DEFAULT_GPU)
|
||||
if gpu not in ('real', 'soft'):
|
||||
raise ValueError('GPU must be real or soft')
|
||||
path = Path(page_path).resolve() if page_path else DIST / 'index.html'
|
||||
with isolated_server(path.parent) as base, sync_playwright() as p:
|
||||
flags = (GPU_ARGS if gpu == 'real' else SOFT_ARGS) if browser_name == 'chromium' else []
|
||||
browser = getattr(p, browser_name).launch(args=[*flags, *args])
|
||||
try:
|
||||
context = browser.new_context(viewport=viewport or {'width': 1280, 'height': 760})
|
||||
page = context.new_page()
|
||||
if gpu == 'soft':
|
||||
page.add_init_script('window.__allowSoftwareGL = true;')
|
||||
yield browser, page, base + '/' + quote(path.name)
|
||||
finally:
|
||||
browser.close()
|
||||
|
||||
|
||||
def installed(browser_name):
|
||||
from playwright.sync_api import sync_playwright
|
||||
with sync_playwright() as p:
|
||||
return Path(getattr(p, browser_name).executable_path).is_file()
|
||||
|
||||
|
||||
def capabilities(page):
|
||||
page.wait_for_function('CAPS.ready')
|
||||
return page.evaluate("""() => {
|
||||
const gl = document.createElement('canvas').getContext('webgl2');
|
||||
const dbg = gl && gl.getExtension('WEBGL_debug_renderer_info');
|
||||
return { isolated: CAPS.isolated, webgpu: CAPS.webgpu, adapterInfo: CAPS.adapterInfo,
|
||||
renderer: gl ? gl.getParameter(dbg ? dbg.UNMASKED_RENDERER_WEBGL : gl.RENDERER) : null };
|
||||
}""")
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Print isolation, WebGPU adapter info and GL renderer for installed browsers."""
|
||||
import argparse
|
||||
import json
|
||||
|
||||
from use_venv import ensure_playwright
|
||||
ensure_playwright()
|
||||
from browser import capabilities, gpu_option, installed, launch
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
gpu_option(parser)
|
||||
options = parser.parse_args()
|
||||
for name in ['chromium', 'firefox']:
|
||||
if not installed(name):
|
||||
print(f'SKIP {name}: Playwright browser is not installed')
|
||||
continue
|
||||
with launch(gpu=options.gpu, browser_name=name) as (browser, page, url):
|
||||
page.goto(url)
|
||||
caps = capabilities(page)
|
||||
print(f'{name} {browser.version}: {json.dumps(caps)}', flush=True)
|
||||
assert caps['isolated'], 'Server did not isolate the page'
|
||||
if name == 'chromium' and options.gpu == 'real':
|
||||
assert caps['webgpu'] and caps['adapterInfo'] is not None, 'No headless WebGPU adapter'
|
||||
assert caps['adapterInfo']['architecture'] != 'swiftshader', 'Software WebGPU in real mode'
|
||||
assert not any(s in (caps['renderer'] or '').lower() for s in ['swiftshader', 'llvmpipe', 'softpipe']), 'Software GL in real mode'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+7
-10
@@ -11,12 +11,10 @@ import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
from server import isolated_server
|
||||
from browser import launch, gpu_option, GPU_ARGS, SOFT_ARGS
|
||||
from use_venv import ensure_playwright
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
GPU_ARGS = ['--use-angle=vulkan', '--enable-features=Vulkan', '--enable-gpu',
|
||||
'--ignore-gpu-blocklist', '--enable-gpu-rasterization']
|
||||
|
||||
|
||||
def size(value):
|
||||
@@ -107,6 +105,7 @@ def main():
|
||||
ap.add_argument('--seconds', type=float, default=10)
|
||||
ap.add_argument('--json', type=Path)
|
||||
ap.add_argument('--compare', type=Path)
|
||||
gpu_option(ap)
|
||||
args = ap.parse_args()
|
||||
try:
|
||||
zooms = list(dict.fromkeys(float(z) for z in args.zooms.split(',')))
|
||||
@@ -129,7 +128,6 @@ def main():
|
||||
if baseline and (baseline.get('schema') != 1 or baseline['options'] != options):
|
||||
ap.error('baseline schema/options differ; use the same matrix, dimensions and seconds')
|
||||
ensure_playwright()
|
||||
from playwright.sync_api import sync_playwright
|
||||
started = time.monotonic()
|
||||
subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT, check=True, stdout=sys.stderr)
|
||||
# Derive coverage from the source so a newly added sim step cannot silently disappear.
|
||||
@@ -141,13 +139,12 @@ def main():
|
||||
# boot's title draw is outside frame().
|
||||
overlays.remove('drawTitle')
|
||||
result = dict(schema=1, created_utc=datetime.now(timezone.utc).isoformat(), options=options,
|
||||
gpu_args=GPU_ARGS, scenes={}, sim_targets=steps, overlay_targets=overlays)
|
||||
files = sorted((ROOT / 'src').rglob('*')) + [Path(__file__), ROOT / 'tools/perf-scenes.js', ROOT / 'tools/perf-measure.js']
|
||||
gpu_args=GPU_ARGS if args.gpu == 'real' else SOFT_ARGS, scenes={}, sim_targets=steps, overlay_targets=overlays)
|
||||
files = sorted((ROOT / 'src').rglob('*')) + [Path(__file__), ROOT / 'tools/browser.py', ROOT / 'tools/server.py',
|
||||
ROOT / 'tools/perf-scenes.js', ROOT / 'tools/perf-measure.js']
|
||||
result['source_sha256'] = hashlib.sha256(b''.join(p.read_bytes() for p in files if p.is_file())).hexdigest()
|
||||
errors = []
|
||||
with isolated_server() as url, sync_playwright() as p:
|
||||
browser = p.chromium.launch(args=GPU_ARGS)
|
||||
pg = browser.new_page(viewport=dict(width=args.viewport[0], height=args.viewport[1]))
|
||||
with launch(gpu=args.gpu, viewport=dict(width=args.viewport[0], height=args.viewport[1])) as (browser, pg, url):
|
||||
pg.on('pageerror', lambda e: errors.append(str(e)))
|
||||
pg.add_init_script('''window.__perfStop = true;
|
||||
const raf = window.requestAnimationFrame.bind(window), pending = new Set();
|
||||
@@ -169,7 +166,7 @@ def main():
|
||||
}''')
|
||||
if not result['machine']['crossOriginIsolated']:
|
||||
raise RuntimeError('Local server did not provide cross-origin isolation')
|
||||
if re.search('swiftshader|llvmpipe|softpipe|software', result['machine']['renderer'], re.I):
|
||||
if args.gpu == 'real' and re.search('swiftshader|llvmpipe|softpipe|software', result['machine']['renderer'], re.I):
|
||||
raise RuntimeError('Refusing a software-rendered baseline')
|
||||
result['machine']['browser_version'] = browser.version
|
||||
def setup(scene):
|
||||
|
||||
@@ -2,16 +2,14 @@
|
||||
import sys, time, pathlib
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
||||
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' / 'playthrough')
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
url = (root / 'dist' / 'index.html').as_uri()
|
||||
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(args=['--autoplay-policy=no-user-gesture-required'])
|
||||
pg = b.new_page(viewport={'width': 1440, 'height': 860})
|
||||
with launch(viewport={'width': 1440, 'height': 860}, args=['--autoplay-policy=no-user-gesture-required']) as (b, pg, url):
|
||||
logs = []
|
||||
pg.on('console', lambda m: logs.append(f'{m.type}: {m.text}'))
|
||||
pg.on('pageerror', lambda e: logs.append(f'PAGEERROR: {e}'))
|
||||
|
||||
+5
-4
@@ -1,5 +1,6 @@
|
||||
"""Serve dist/ with isolation headers; isolated_server() owns a free-port background server."""
|
||||
from contextlib import contextmanager
|
||||
from functools import partial
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
@@ -8,8 +9,8 @@ DIST = Path(__file__).resolve().parent.parent / 'dist'
|
||||
|
||||
|
||||
class Handler(SimpleHTTPRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, directory=str(DIST), **kwargs)
|
||||
def __init__(self, *args, directory=DIST, **kwargs):
|
||||
super().__init__(*args, directory=str(directory), **kwargs)
|
||||
|
||||
def end_headers(self):
|
||||
self.send_header('Cache-Control', 'no-store')
|
||||
@@ -22,8 +23,8 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
|
||||
|
||||
@contextmanager
|
||||
def isolated_server():
|
||||
with ThreadingHTTPServer(('127.0.0.1', 0), Handler) as server:
|
||||
def isolated_server(directory=DIST):
|
||||
with ThreadingHTTPServer(('127.0.0.1', 0), partial(Handler, directory=directory)) as server:
|
||||
thread = Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
|
||||
+5
-10
@@ -40,13 +40,9 @@ import argparse, pathlib, sys
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
||||
from where import translate
|
||||
from use_venv import ensure_playwright; ensure_playwright()
|
||||
from playwright.sync_api import sync_playwright
|
||||
from browser import launch, gpu_option
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
# Headless Chromium has no GPU. These flags give it WebGL2 through SwiftShader (software), and
|
||||
# window.__allowSoftwareGL tells the game to use the smooth look anyway (players on software
|
||||
# rendering get the pixel look instead).
|
||||
GL_ARGS = ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader']
|
||||
FREEZE = """(() => { const real = performance.now.bind(performance); window.__frozenNow = null;
|
||||
performance.now = () => window.__frozenNow === null ? real() : window.__frozenNow; })();"""
|
||||
|
||||
@@ -68,17 +64,16 @@ def main():
|
||||
ap.add_argument('--freeze', action='store_true')
|
||||
ap.add_argument('--no-light', action='store_true')
|
||||
ap.add_argument('--page', default=str(ROOT / 'dist' / 'index.html'))
|
||||
gpu_option(ap)
|
||||
a = ap.parse_args()
|
||||
ww, wh = (int(v) for v in a.size.split('x'))
|
||||
vw, vh = (int(v) for v in a.viewport.split('x'))
|
||||
errors = []
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(args=GL_ARGS)
|
||||
pg = b.new_page(viewport={'width': vw, 'height': vh})
|
||||
with launch(gpu=a.gpu, viewport={'width': vw, 'height': vh}, page_path=a.page) as (b, pg, url):
|
||||
pg.on('pageerror', lambda e: errors.append(translate(e.stack or str(e))))
|
||||
pg.on('console', lambda m: errors.append('console: ' + m.text) if m.type == 'error' else None)
|
||||
pg.add_init_script('window.__allowSoftwareGL = true;' + (FREEZE if a.freeze else ''))
|
||||
pg.goto(pathlib.Path(a.page).resolve().as_uri())
|
||||
pg.add_init_script(FREEZE if a.freeze else '')
|
||||
pg.goto(url)
|
||||
pg.evaluate("SET.fog = false")
|
||||
pg.wait_for_timeout(300)
|
||||
# pause the moment the world exists, so the shot depends only on the seed, the setup
|
||||
|
||||
+4
-6
@@ -6,8 +6,8 @@ import math
|
||||
import copy
|
||||
from urllib.request import urlopen
|
||||
|
||||
from perf import ROOT, GPU_ARGS, quantiles, measurements, report
|
||||
from server import isolated_server
|
||||
from perf import ROOT, quantiles, measurements, report
|
||||
from browser import launch, consume_gpu_option
|
||||
from use_venv import ensure_playwright
|
||||
|
||||
|
||||
@@ -28,14 +28,12 @@ def main():
|
||||
assert '/factory/sim/total_ms: +20.0% REGRESSION' in out.getvalue()
|
||||
assert 'n/a (baseline zero)' in out.getvalue()
|
||||
ensure_playwright()
|
||||
from playwright.sync_api import sync_playwright
|
||||
with isolated_server() as url, sync_playwright() as p:
|
||||
consume_gpu_option()
|
||||
with launch() as (browser, pg, url):
|
||||
with urlopen(url) as response:
|
||||
assert response.headers['Cross-Origin-Opener-Policy'] == 'same-origin'
|
||||
assert response.headers['Cross-Origin-Embedder-Policy'] == 'require-corp'
|
||||
response.read()
|
||||
browser = p.chromium.launch(args=GPU_ARGS)
|
||||
pg = browser.new_page(viewport={'width': 1280, 'height': 760})
|
||||
errors = []
|
||||
pg.on('pageerror', lambda e: errors.append(str(e)))
|
||||
pg.add_init_script('window.requestAnimationFrame = () => 0;')
|
||||
|
||||
Reference in New Issue
Block a user