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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+8
-2
@@ -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)),
|
||||
|
||||
+4
-3
@@ -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;
|
||||
|
||||
@@ -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
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user