From a8b7d599c216d8ecfa630180bf5864f97dd55a1d Mon Sep 17 00:00:00 2001 From: Andrea Manzi Date: Sun, 1 Nov 2026 18:54:37 +0100 Subject: [PATCH] =?UTF-8?q?La=20prova=20del=20peso:=20sempre=20con=20swift?= =?UTF-8?q?shader,=20come=20un=20telefono=20lento,=20misura=20fotogrammi?= =?UTF-8?q?=20e=20arretrato=20grafico=20di=20ogni=20scena=20nelle=20tre=20?= =?UTF-8?q?fasi=20e=20di=20ogni=20primo=20piano=20(la=20scheda=20video=20n?= =?UTF-8?q?asconde=20gli=20effetti=20pesanti);=20si=20rif=C3=A0=20quando?= =?UTF-8?q?=20cambia=20un=20disegno,=20nella=20pipeline=20sempre;=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T --- .gitea/workflows/test.yml | 2 +- CLAUDE.md | 4 +- docs/STATO.md | 9 ++++ scripts/verify.mjs | 10 +++- tests/chromium.mjs | 7 +-- tests/peso.browser.mjs | 100 ++++++++++++++++++++++++++++++++++++++ tests/verify.test.mjs | 17 +++++-- 7 files changed, 138 insertions(+), 11 deletions(-) create mode 100644 tests/peso.browser.mjs diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 0ca0da1..b4dc767 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -30,7 +30,7 @@ jobs: strategy: fail-fast: false matrix: - prova: [principale, minigiochi, telefoni, ambient, veicoli, ingorgo, smarronata, briscola, tressette, scopa, biliardo, flipper, micro, cerbottana, condor, campana, bruciate, gara, telefono, risveglio] + prova: [principale, minigiochi, telefoni, ambient, veicoli, ingorgo, smarronata, briscola, tressette, scopa, biliardo, flipper, micro, cerbottana, condor, campana, bruciate, gara, telefono, risveglio, peso] steps: - uses: actions/checkout@v4 - name: Playwright diff --git a/CLAUDE.md b/CLAUDE.md index 73e6ba9..d4a208e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,7 +18,9 @@ i suoi ricordi: non inventare persone, luoghi o fatti del paese al posto suoi; p - Ogni funzionalità con il suo test; un test automatico percorre tutta la soluzione dell'avventura. - Le prove nel browser sono a sezioni (`scripts/verify.mjs`, dal 2026-11-01): `make verify` rifà solo quelle i cui ingressi sono cambiati dall'ultimo verde (la briscola se cambia la briscola, la partita intera se cambiano storia, motore, stile - o risorse). Un `chromium.launch` nuovo nelle prove usa `args: ARGS` di `tests/chromium.mjs`. Una prova nuova in `tests/*.browser.mjs` va aggiunta a `SUITES` (il test `verify.test.mjs` lo pretende). + o risorse). Un `chromium.launch` nuovo nelle prove usa `args: ARGS` di `tests/chromium.mjs`. + Gli effetti pesanti li scopre la prova `peso` (`tests/peso.browser.mjs`): sempre con swiftshader, misura arretrato grafico e + fotogrammi di ogni scena e primo piano; si rifà quando cambia un disegno (`*-render.js`, `render.js`, effetti). Una prova nuova in `tests/*.browser.mjs` va aggiunta a `SUITES` (il test `verify.test.mjs` lo pretende). - Verifica visiva nel browser con Playwright prima di dichiarare finito qualcosa di grafico. Chromium disegna sulla scheda video del PC se c'è, altrimenti con swiftshader (`tests/chromium.mjs`, dal 2026-11-01: giro intero ~4 min invece di ~16). - Pubblicare (tag/bump/push) solo quando Andrea lo chiede. diff --git a/docs/STATO.md b/docs/STATO.md index 7299391..c9a5e1f 100644 --- a/docs/STATO.md +++ b/docs/STATO.md @@ -326,6 +326,15 @@ versione nuova. `REGOLO_GPU=0/1` forza. Con 6 o 8 prove insieme il giro dura uguale: il limite è la partita intera (243 s, il tempo del gioco, non della grafica). La pipeline dà la GPU ai job (`--gpus all`, runtime NVIDIA di Docker); runner a 6 job. +- **La prova del peso (1 novembre, Andrea: «senza GPU scoprivamo anche gli effetti troppo pesanti»):** con la scheda + video le prove non vedono più un effetto pesante. `tests/peso.browser.mjs` usa sempre swiftshader e, nel gioco vero, + in ogni scena nelle tre fasi e in ogni primo piano (il flipper anche in gioco e nel Regolo del futuro) misura + fotogrammi al secondo e arretrato grafico: soglie 30 e 400 ms (una scena normale pochi ms; un `ctx.filter` blur a + tutto schermo 816, quaranta tratti larghi trasparenti 718). Due trappole trovate: leggere i pixel della tela del + gioco la fa passare al processore (Chrome) e la misura cambia ciò che misura, quindi si legge una tela nuova + 640×360; la prima visita di una scena paga il caricamento delle immagini, quindi prima si smaltisce. ~5 minuti, in + parallelo con le altre; si rifà quando cambia un disegno (`watch` in verify.mjs), e nella pipeline sempre. + - **Flipper da tastiera (1 novembre, Andrea):** la freccia giù carica la molla come Spazio, i due Shift comandano le palette (sinistro e destro, `e.code` passato da main.js ai minigiochi). Test unitario e nel browser. diff --git a/scripts/verify.mjs b/scripts/verify.mjs index a0c3b1c..d5c35c2 100644 --- a/scripts/verify.mjs +++ b/scripts/verify.mjs @@ -27,7 +27,9 @@ export const IGNORED = [/^docs\//, /^[^/]*\.md$/, /^src\/version\.js$/, /^packag // Le prove. `core: true` = dipende da tutto il gioco tranne i file dei minigiochi a sé (che hanno la loro prova); altrimenti // solo dal motore e dai propri file: quelli che si chiamano come la prova (src/engine/flipper.js, flipper-render.js, -// assets/closeups/flipper-*.png, docs/BRISCOLA.md…) più gli `extra`. `run` sono gli argomenti di node. +// assets/closeups/flipper-*.png, docs/BRISCOLA.md…) più gli `extra`. `watch`: file che la prova guarda senza toglierli +// alle altre (il peso della grafica si rifà quando cambia un disegno, ma quel disegno resta anche della sua prova). +// `run` sono gli argomenti di node. export const SUITES = { principale: { core: true, run: ['tests/browser.mjs', '--parte=principale'], note: 'la partita intera, pausa e salvataggi' }, minigiochi: { core: true, run: ['tests/browser.mjs', '--parte=minigiochi'], note: 'libro, bocce, tiri, pesca e biliardo dalla partita' }, @@ -49,6 +51,9 @@ export const SUITES = { gara: { run: ['tests/gara.browser.mjs'], extra: ['src/engine/corsa.js', 'src/story/piloti.js', /^assets\/[^/]*\/(gara|gp|pilot)/] }, telefono: { run: ['tests/telefono.browser.mjs'], extra: [/^assets\/muro\//] }, risveglio: { run: ['tests/risveglio.browser.mjs'] }, + // Sempre con swiftshader, come un telefono lento: con la scheda video le altre prove non vedono gli effetti pesanti. + peso: { run: ['tests/peso.browser.mjs'], note: 'il peso della grafica, senza scheda video', + watch: [/-render\.js$/, /^src\/engine\/(ambient|incanto|risveglio|torch|viaggio|corsa|telefono)\.js$/, /^src\/story\/ambience\.js$/, /^assets\/effects\//] }, }; // Prove insieme con la scheda video (misurato il 1° novembre 2026 sul PC di Andrea, 16 thread e RTX 3080 Ti). @@ -64,7 +69,8 @@ export function ownFiles(name, files) { // Gli ingressi di una prova fra i file dati (tutti quelli di git più quelli nuovi nella cartella). export function inputsOf(name, files) { const s = SUITES[name], live = files.filter(p => !ignored(p)); - if (!s.core) return [...new Set([...ENGINE.filter(p => files.includes(p)), ...ownFiles(name, live)])].sort(); + if (!s.core) return [...new Set([...ENGINE.filter(p => files.includes(p)), ...ownFiles(name, live), + ...live.filter(p => (s.watch || []).some(r => matches(p, r)))])].sort(); const others = Object.keys(SUITES).filter(n => !SUITES[n].core); // I file degli altri minigiochi, e i file di prova delle altre prove (tests/ambient.browser.mjs non riguarda la partita). const foreign = new Set([...others.flatMap(n => ownFiles(n, live)), diff --git a/tests/chromium.mjs b/tests/chromium.mjs index c5e1a60..5d3d9af 100644 --- a/tests/chromium.mjs +++ b/tests/chromium.mjs @@ -5,6 +5,7 @@ import { existsSync } from 'node:fs'; export const GPU = process.env.REGOLO_GPU ? process.env.REGOLO_GPU === '1' : ['/dev/dri/renderD128', '/dev/nvidia0'].some(existsSync); -const GL = GPU ? ['--use-gl=angle', '--use-angle=gl-egl', '--ignore-gpu-blocklist', '--enable-gpu-rasterization'] - : ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader']; -export const ARGS = ['--autoplay-policy=no-user-gesture-required', ...GL]; +const AUTOPLAY = '--autoplay-policy=no-user-gesture-required'; +// Sempre sul processore: la prova del peso (peso.browser.mjs) misura così gli effetti, come su un telefono lento. +export const SWIFTSHADER = [AUTOPLAY, '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader']; +export const ARGS = GPU ? [AUTOPLAY, '--use-gl=angle', '--use-angle=gl-egl', '--ignore-gpu-blocklist', '--enable-gpu-rasterization'] : SWIFTSHADER; diff --git a/tests/peso.browser.mjs b/tests/peso.browser.mjs new file mode 100644 index 0000000..590746e --- /dev/null +++ b/tests/peso.browser.mjs @@ -0,0 +1,100 @@ +// 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 = 400, MIN_FPS = 30; // 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 si ripete: 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)) 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). + const scenes = await page.evaluate(() => Object.keys(game.story.scenes)); + 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(); +} diff --git a/tests/verify.test.mjs b/tests/verify.test.mjs index e65c1e4..ca4ff02 100644 --- a/tests/verify.test.mjs +++ b/tests/verify.test.mjs @@ -7,7 +7,7 @@ import { SUITES, ENGINE, inputsOf, ignored, fingerprint, plan } from '../scripts const FILES = ['index.html', 'style.css', 'src/main.js', 'src/version.js', 'package.json', 'package-lock.json', 'Makefile', 'README.md', 'src/engine/game.js', 'src/engine/render.js', 'src/engine/ambient.js', 'src/engine/bocce.js', 'src/engine/nuovo.js', - 'src/engine/briscola.js', 'src/engine/briscola-render.js', 'src/engine/flipper.js', 'src/engine/corsa.js', 'src/story/piloti.js', + 'src/engine/briscola.js', 'src/engine/briscola-render.js', 'src/engine/flipper.js', 'src/engine/flipper-render.js', 'src/engine/corsa.js', 'src/story/piloti.js', 'src/story/story.js', 'src/story/solution.js', 'assets/scenes/piazza.png', 'assets/closeups/micro-parcheggio.png', 'assets/closeups/gp/boccino.png', 'assets/muro/index.html', 'assets/video/briscola.mp4', 'docs/BRISCOLA.md', 'docs/STATO.md', 'tests/helpers.mjs', 'tests/chromium.mjs', 'tests/browser.mjs', 'tests/briscola.browser.mjs', 'tests/briscola.test.mjs', 'tests/story.test.mjs', @@ -26,12 +26,12 @@ test('ogni prova nel browser di tests/ ha la sua voce, e una sola', () => { test('la briscola si riprova solo se cambia la briscola o il motore', () => { assert.deepEqual(suitesWith('src/engine/briscola.js'), ['briscola']); - assert.deepEqual(suitesWith('src/engine/briscola-render.js'), ['briscola']); + assert.deepEqual(suitesWith('src/engine/briscola-render.js'), ['briscola', 'peso'], 'un disegno fa rifare anche il peso della grafica'); assert.deepEqual(suitesWith('assets/video/briscola.mp4'), ['briscola']); assert.deepEqual(suitesWith('tests/briscola.browser.mjs'), ['briscola']); assert.deepEqual(suitesWith('docs/BRISCOLA.md'), [], 'i documenti non fanno rifare niente'); assert.ok(!inputsOf('briscola', FILES).includes('src/story/story.js'), 'la storia non fa riprovare la briscola'); - assert.deepEqual(suitesWith('src/engine/corsa.js'), ['gara']); + assert.deepEqual(suitesWith('src/engine/corsa.js'), ['gara', 'peso']); assert.deepEqual(suitesWith('src/story/piloti.js'), ['gara']); assert.deepEqual(suitesWith('assets/closeups/gp/boccino.png'), ['gara']); assert.deepEqual(suitesWith('assets/muro/index.html'), ['telefono']); @@ -40,9 +40,18 @@ test('la briscola si riprova solo se cambia la briscola o il motore', () => { test('la storia, le scene, i minigiochi dentro la partita e un modulo nuovo fanno rifare la partita intera, non i minigiochi a sé', () => { assert.deepEqual(suitesWith('tests/browser.mjs'), ['principale', 'minigiochi', 'telefoni'], 'il file di una prova conta solo per lei'); - for (const path of ['src/story/story.js', 'src/story/solution.js', 'assets/scenes/piazza.png', 'src/engine/bocce.js', 'src/engine/ambient.js', 'src/engine/nuovo.js']) { + for (const path of ['src/story/story.js', 'src/story/solution.js', 'assets/scenes/piazza.png', 'src/engine/bocce.js', 'src/engine/nuovo.js']) { assert.deepEqual(suitesWith(path), cores, path); } + assert.deepEqual(suitesWith('src/engine/ambient.js'), [...cores, 'peso'], 'gli effetti delle scene: la partita e il peso'); +}); + +test('il peso della grafica si rifà quando cambia un disegno o un effetto, non la storia, e non toglie quei file alle altre prove', () => { + const peso = inputsOf('peso', FILES); + for (const path of ['src/engine/render.js', 'src/engine/ambient.js', 'src/engine/briscola-render.js', 'src/engine/flipper-render.js']) assert.ok(peso.includes(path), path); + for (const path of ['src/story/story.js', 'src/engine/briscola.js', 'src/engine/flipper.js']) assert.ok(!peso.includes(path), path); + assert.ok(inputsOf('principale', FILES).includes('src/engine/ambient.js')); + assert.deepEqual(suitesWith('src/engine/flipper-render.js'), ['flipper', 'peso']); }); test('il motore comune fa rifare tutto', () => {