// Il peso della grafica (Andrea, 1 novembre 2026: «prima senza GPU scoprivamo anche gli effetti troppo pesanti»): le // altre prove disegnano sulla scheda video e un effetto pesante non le rallenta più. Questa usa sempre swiftshader, il // disegno sul processore, come un telefono lento: nel gioco vero, in ogni scena nelle tre fasi e in ogni primo piano, // lascia girare il gioco e misura i fotogrammi al secondo e l'arretrato grafico (il disegno chiesto e non ancora fatto). // node tests/peso.browser.mjs (PESO_TUTTI=1 stampa ogni misura). // // Come si misura, e perché così (vedi la memoria «effetti leggeri»): mai leggere i pixel della tela del gioco, perché // dopo qualche lettura Chrome la passa al processore e la misura cambia ciò che misura; l'arretrato si cronometra // leggendo una tela nuova, abbastanza grande da stare anche lei sulla grafica, che deve aspettare tutto il disegno // in coda prima della sua. Tarata il 1° novembre 2026 sul PC di Andrea: una scena normale, a immagini caricate, pochi ms // di arretrato e 60 fotogrammi; un ctx.filter blur a tutto schermo 816 ms e 34, quaranta tratti larghi trasparenti 718 ms // e 36, trenta veli trasparenti a tutto schermo 374 ms e 45. import { chromium } from 'playwright'; import assert from 'node:assert/strict'; import { staticServer } from './helpers.mjs'; import { SWIFTSHADER } from './chromium.mjs'; const MAX_BACKLOG = 500, MIN_FPS = 30; // la scena più cara, pieve_interno di pomeriggio, fino a 355 ms sotto carico // ms di arretrato dopo un secondo di gioco; fotogrammi al secondo const server = await staticServer(); let browser; const errors = [], slow = [], rows = []; try { browser = await chromium.launch({ args: SWIFTSHADER }); const page = await browser.newPage({ viewport: { width: 1280, height: 860 } }); page.on('pageerror', e => errors.push(e.message)); await page.goto(`http://127.0.0.1:${server.port}/`); await page.click('#splash'); await page.click('#new-game'); await page.click('#rione [data-rione="campomigliaio"]'); await page.click('#skip-intro'); await page.evaluate(async () => { const { settle } = await import('/tests/helpers.mjs'); settle(game); const probe = () => { const c = document.createElement('canvas'); c.width = 640; c.height = 360; const g = c.getContext('2d'); g.fillRect(0, 0, 9, 9); const t = performance.now(); g.getImageData(0, 0, 1, 1); return performance.now() - t; }; window.peso = async () => { // Prima si lascia smaltire l'arretrato di prima, compreso il caricamento delle immagini alla prima visita di una // scena (che costa una volta sola e non è un effetto): finché la lettura non torna svelta, al più due secondi. await new Promise(r => setTimeout(r, 150)); for (let i = 0; i < 20 && probe() > 30; i++) await new Promise(r => setTimeout(r, 100)); let n = 0; const t0 = performance.now(); await new Promise(done => { const tick = () => { n++; if (performance.now() - t0 < 800) requestAnimationFrame(tick); else done(); }; requestAnimationFrame(tick); }); const backlog = Math.round(probe()); return { fps: Math.round(n * 1000 / (performance.now() - t0)), backlog }; }; }); // Una misura storta per un attimo di carico del PC (le altre prove girano insieme) si ripete dopo un secondo di // respiro: boccia solo se è pesante due volte. const measure = async label => { let m = await page.evaluate(() => peso()); const heavy = m => m.backlog > MAX_BACKLOG || m.fps < MIN_FPS; if (heavy(m)) { await page.waitForTimeout(1000); m = await page.evaluate(() => peso()); } rows.push({ label, ...m }); if (heavy(m)) slow.push(`${label}: ${m.fps} fotogrammi al secondo, arretrato ${m.backlog} ms`); if (process.env.PESO_TUTTI) console.log(`${String(m.fps).padStart(3)} fps ${String(m.backlog).padStart(5)} ms ${label}`); }; // Le scene, nelle tre fasi (le condizioni delle fasi come in ambient.browser.mjs). Prima un giro in tutte, perché la // prima visita paga il caricamento delle immagini, che non è un effetto. const scenes = await page.evaluate(() => Object.keys(game.story.scenes)); for (const id of scenes) { await page.evaluate(id => { const sc = game.story.scenes[id]; game.state.scene = id; [game.state.x, game.state.y] = sc.entry; }, id); await page.waitForTimeout(120); } for (const [phase, flags] of [['pomeriggio', {}], ['tramonto', { indizio_regolo: true }], ['sera', { casetta_aperta: true }]]) { for (const id of scenes) { await page.evaluate(({ id, flags }) => { const g = game, sc = g.story.scenes[id]; Object.assign(g.state.flags, flags); g.state.scene = id; [g.state.x, g.state.y] = sc.entry; g.enteredAt = g.time; }, { id, flags }); await measure(`${id} (${phase})`); await page.evaluate(flags => { for (const k of Object.keys(flags)) delete game.state.flags[k]; }, flags); } } // I primi piani, aperti come li apre la storia; il flipper anche con la pallina in gioco e nel Regolo del futuro. const closeups = await page.evaluate(() => Object.keys(game.story.closeups)); for (const id of closeups) { const opened = await page.evaluate(id => { game.state.scene = 'piazza'; try { game.run({ closeup: id }); } catch (e) { return String(e); } return true; }, id); if (opened !== true) { console.log(`${id}: non si apre da solo (${opened})`); continue; } await measure(`primo piano ${id}`); if (await page.evaluate(() => game.story.closeups[game.closeup.id].type === 'flipper')) { await page.evaluate(async () => { const m = await import('/src/engine/flipper.js'); m.launch(game.closeup, 0.8); }); await measure(`primo piano ${id} (in gioco)`); const future = await page.evaluate(async () => { const m = await import('/src/engine/flipper.js'); if (!m.futureLayout) return false; Object.assign(game.closeup, m.futureLayout(), { table: 'future', future: { hits: { orbita: 0, ponte: 0, spira: 0 } } }); return true; }); if (future) await measure(`primo piano ${id} (Regolo del futuro)`); } await page.evaluate(() => { game.closeup = null; game.queue.length = 0; game.speech = null; }); } const worst = [...rows].sort((a, b) => b.backlog - a.backlog).slice(0, 4).map(r => `${r.label} ${r.backlog} ms`).join(', '); console.log(`Peso della grafica con swiftshader: ${rows.length} misure; gli arretrati più alti: ${worst}`); assert.deepEqual(slow, [], `troppo pesanti con swiftshader (soglie: ${MAX_BACKLOG} ms, ${MIN_FPS} fotogrammi al secondo)`); assert.deepEqual(errors, []); console.log('Prova del peso: OK'); } finally { await browser?.close(); server.kill(); }