La prova del peso: sempre con swiftshader, come un telefono lento, misura fotogrammi e arretrato grafico di ogni scena nelle tre fasi e di ogni primo piano (la scheda video nasconde gli effetti pesanti); si rifà quando cambia un disegno, nella pipeline sempre; test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
2026-11-01 18:54:37 +01:00
co-authored by Claude Opus 5.5
parent a9f8f88010
commit a8b7d599c2
7 changed files with 138 additions and 11 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: 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: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Playwright - name: Playwright
+3 -1
View File
@@ -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. - 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 - 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 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 - 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). 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. - Pubblicare (tag/bump/push) solo quando Andrea lo chiede.
+9
View File
@@ -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 `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. 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 - **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. palette (sinistro e destro, `e.code` passato da main.js ai minigiochi). Test unitario e nel browser.
+8 -2
View File
@@ -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 // 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, // 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 = { export const SUITES = {
principale: { core: true, run: ['tests/browser.mjs', '--parte=principale'], note: 'la partita intera, pausa e salvataggi' }, 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' }, 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)/] }, 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\//] }, telefono: { run: ['tests/telefono.browser.mjs'], extra: [/^assets\/muro\//] },
risveglio: { run: ['tests/risveglio.browser.mjs'] }, 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). // 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). // Gli ingressi di una prova fra i file dati (tutti quelli di git più quelli nuovi nella cartella).
export function inputsOf(name, files) { export function inputsOf(name, files) {
const s = SUITES[name], live = files.filter(p => !ignored(p)); 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); 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). // 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)), const foreign = new Set([...others.flatMap(n => ownFiles(n, live)),
+4 -3
View File
@@ -5,6 +5,7 @@
import { existsSync } from 'node:fs'; import { existsSync } from 'node:fs';
export const GPU = process.env.REGOLO_GPU ? process.env.REGOLO_GPU === '1' : ['/dev/dri/renderD128', '/dev/nvidia0'].some(existsSync); 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'] const AUTOPLAY = '--autoplay-policy=no-user-gesture-required';
: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader']; // Sempre sul processore: la prova del peso (peso.browser.mjs) misura così gli effetti, come su un telefono lento.
export const ARGS = ['--autoplay-policy=no-user-gesture-required', ...GL]; 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;
+100
View File
@@ -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();
}
+13 -4
View File
@@ -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', 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/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', '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', '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', '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', () => { 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.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('assets/video/briscola.mp4'), ['briscola']);
assert.deepEqual(suitesWith('tests/briscola.browser.mjs'), ['briscola']); assert.deepEqual(suitesWith('tests/briscola.browser.mjs'), ['briscola']);
assert.deepEqual(suitesWith('docs/BRISCOLA.md'), [], 'i documenti non fanno rifare niente'); 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.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('src/story/piloti.js'), ['gara']);
assert.deepEqual(suitesWith('assets/closeups/gp/boccino.png'), ['gara']); assert.deepEqual(suitesWith('assets/closeups/gp/boccino.png'), ['gara']);
assert.deepEqual(suitesWith('assets/muro/index.html'), ['telefono']); 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é', () => { 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'); 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(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', () => { test('il motore comune fa rifare tutto', () => {