← back to Quadrille Showroom
scripts/verify-slice3-fps-realgpu.mjs
74 lines
// Slice-3 chunk F — REAL-GPU FPS under nav sweep.
// The main verify-slice3.mjs runs headless SwiftShader (software WebGL) which caps
// at single-digit FPS regardless of the app — useless for the ≥55 gate. This launches
// a HEADED Chromium with the real Apple GPU (Metal/ANGLE) so window._perf reports the
// genuine frame rate, then runs the same spin+select+version-switch sweep.
import pw from '/Users/macstudio3/.npm-global/lib/node_modules/playwright/index.js';
const { chromium } = pw;
// Use the SYSTEM Chrome (real Apple GPU via Metal/ANGLE) — the bundled Playwright
// chromium is a software-only headless shell that can't report true FPS.
const browser = await chromium.launch({
channel: 'chrome',
headless: false,
args: ['--use-angle=metal', '--ignore-gpu-blocklist', '--enable-gpu-rasterization', '--window-size=1440,900'],
});
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
const errors = [];
page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); });
page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message));
await page.goto('http://localhost:7690/', { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => window._qh && window._qh.wingBoards && window._qh.wingBoards.length >= 50, { timeout: 25000 });
await page.waitForTimeout(3000); // let it warm up + thumbs stream + pixelRatio settle
// renderer / GL info to PROVE it's not software
const gl = await page.evaluate(() => {
try {
const r = window._qh.renderer;
const ctx = r.getContext();
const dbg = ctx.getExtension('WEBGL_debug_renderer_info');
return {
vendor: dbg ? ctx.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : ctx.getParameter(ctx.VENDOR),
renderer: dbg ? ctx.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : ctx.getParameter(ctx.RENDERER),
pixelRatio: r.getPixelRatio(),
};
} catch (e) { return { err: String(e) }; }
});
const samples = [];
async function sample(ms) {
const reps = Math.max(1, Math.round(ms / 1100));
for (let i = 0; i < reps; i++) { await page.waitForTimeout(1100); const p = await page.evaluate(() => window._perf || null); if (p) samples.push(p); }
}
async function holdYaw(key, ms) {
await page.evaluate(k => window.dispatchEvent(new KeyboardEvent('keydown', { key: k, code: k, bubbles: true })), key);
await page.waitForTimeout(ms);
await page.evaluate(k => window.dispatchEvent(new KeyboardEvent('keyup', { key: k, code: k, bubbles: true })), key);
}
// idle baseline
await sample(2200);
// SWEEP
await holdYaw('ArrowRight', 1400); await sample(1200);
await holdYaw('ArrowLeft', 1400); await sample(1200);
await holdYaw('ArrowLeft', 1400); await sample(1200);
await page.evaluate(() => { const wb = window._qh.wingBoards; window._qh.focusOnWing(wb[Math.floor(wb.length / 2) + 4]); });
await sample(2400);
await page.evaluate(() => window._versions.set('V3')); await sample(1200);
await page.evaluate(() => window._versions.set('V5')); await sample(1200);
await page.evaluate(() => window._versions.set('V1')); await sample(1500);
const fps = samples.map(s => s.fps);
console.log(JSON.stringify({
gl,
samples: samples.length,
fpsMin: Math.min(...fps), fpsMax: Math.max(...fps),
fpsAvg: +(fps.reduce((a, b) => a + b, 0) / fps.length).toFixed(1),
gate55_met: Math.min(...fps) >= 55,
drawsMin: Math.min(...samples.map(s => s.draws)), drawsMax: Math.max(...samples.map(s => s.draws)),
allSamples: samples,
errorCount: errors.length,
}, null, 2));
await browser.close();