The fps line of the performance overlay stops at the display's rate, 60 on most screens. It does not tell how much room a game has left. The new top line answers that from the times the overlay measures. - possibleFps in src/editor/frameMeter.ts is 1000 ms over the sum of the five areas, rounded down. It has no number while nothing took any time, and the overlay shows a dash then. - The editor's share is in the sum, because it costs time while playing in the editor. A released game can do a little better. - It is an upper bound. The GPU's time is not measured and not in it. - e2e 59e reads the lines one further down and checks the new line against the times it measured itself. Tests: - Unit frame-meter: 11 passed (+2 for possibleFps) - Gate: 197 files, 2855 tests, CHECK EXIT 0 (2853 + 2) - e2e 59: ALL PASSED, 19 checks (+1), on melektaus-atx - Counter-check: possibleFps without the editor's share fails the unit test - The e2e total of 1602 is counted (1601 + 1), not measured Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
204 lines
8.1 KiB
JavaScript
204 lines
8.1 KiB
JavaScript
/**
|
||
* While playing (#265): how closely the panels follow a running game. The preset is picked in the
|
||
* Project settings with the mouse; Live brings the panels up to date every frame, Balanced four
|
||
* times a second, and neither holds the game back. On Balanced the scene view stops drawing behind
|
||
* the game tab and catches up when it comes back; on Minimal the hierarchy waits for a pause. A
|
||
* crate spawned at the top level is listed in a group named "Crate".
|
||
*/
|
||
export async function whilePlaying(t) {
|
||
const {
|
||
check,
|
||
json,
|
||
evaluate,
|
||
click,
|
||
clickText,
|
||
clickToolbar,
|
||
press,
|
||
play,
|
||
stop,
|
||
shot,
|
||
showTab,
|
||
undo,
|
||
sleep,
|
||
} = t;
|
||
const playInfo = () => json('__gm.playInfo()');
|
||
/** Panel updates so far and the page's clock, so a rate is measured in the page, not over CDP. */
|
||
const sample = () => json('({ at: performance.now(), ...__gm.playInfo().panelUpdates })');
|
||
const valuesPerSecond = async (ms) => {
|
||
const a = await sample();
|
||
await sleep(ms);
|
||
const b = await sample();
|
||
return { rate: ((b.values - a.values) * 1000) / (b.at - a.at), a, b };
|
||
};
|
||
const setWhilePlaying = async (label) => {
|
||
await clickToolbar('menu');
|
||
await clickText('Project settings…');
|
||
const before = (await json('__gm.projectSettings()')).whilePlaying;
|
||
const at = await json('__gm.projectSettingsWhilePlayingAt()');
|
||
await click(at.x, at.y);
|
||
await clickText(label);
|
||
const chosen = (await json('__gm.projectSettings()')).whilePlaying;
|
||
await clickText('Apply');
|
||
return { before, chosen };
|
||
};
|
||
const undoSteps = () => json('__gm.state.commands.undoStack.length');
|
||
/** Whether the hierarchy *shows* the row – `hierarchyOrder` reads the model, not the list on screen. */
|
||
const listed = (rowId) => json(`__gm.hierarchyRow(${JSON.stringify(rowId)}) !== null`);
|
||
const crateGroups = () => json(`(__gm.state.activeScene.groups ?? []).filter((g) => g.name === 'Crate')`);
|
||
|
||
await press('Escape');
|
||
await evaluate('__gm.state.select(null)');
|
||
await showTab('scene');
|
||
|
||
// 59a. The fixture was written without the setting, so it reads as Balanced; Live is picked with
|
||
// the mouse and is one undo step.
|
||
check(
|
||
'a project written before there was a choice plays Balanced',
|
||
(await json('__gm.state.project.whilePlaying')) === 'balanced',
|
||
);
|
||
const steps = await undoSteps();
|
||
const picked = await setWhilePlaying('Live');
|
||
check(
|
||
'the Project settings show Balanced and take Live from the menu',
|
||
picked.before === 'balanced' &&
|
||
picked.chosen === 'live' &&
|
||
(await json('__gm.state.project.whilePlaying')) === 'live' &&
|
||
(await undoSteps()) === steps + 1,
|
||
JSON.stringify({ picked, steps: await undoSteps(), before: steps }),
|
||
);
|
||
|
||
// 59b. Live follows every frame; Balanced four times a second.
|
||
await play();
|
||
const live = await valuesPerSecond(1500);
|
||
await stop();
|
||
await undo();
|
||
check('undo sets it back to Balanced', (await json('__gm.state.project.whilePlaying')) === 'balanced');
|
||
await play();
|
||
const balanced = await valuesPerSecond(1500);
|
||
check(
|
||
'Live brings the panels up to date every frame, Balanced at most four times a second',
|
||
live.rate > 20 && balanced.rate > 2 && balanced.rate <= 4.5,
|
||
JSON.stringify({ live: live.rate, balanced: balanced.rate }),
|
||
);
|
||
|
||
// 59c. Behind the game tab the scene view does not draw on Balanced – three crates spawned, not
|
||
// one redraw – and catches up when its tab comes back. The crates are in a group "Crate".
|
||
const hidden = (await json('__gm.viewportInfo()')).redraws;
|
||
const entitiesBefore = (await json('__gm.world()')).entities.length;
|
||
for (let i = 0; i < 3; i++) await press('Space');
|
||
await sleep(600);
|
||
const stillHidden = (await json('__gm.viewportInfo()')).redraws;
|
||
const groups = await crateGroups();
|
||
const members = await json(
|
||
`__gm.state.activeScene.objects.filter((o) => o.groupId === ${JSON.stringify(groups[0]?.id ?? '')}).length`,
|
||
);
|
||
check(
|
||
'the game ran on – three crates spawned – and they are listed in one group named Crate',
|
||
(await json('__gm.world()')).entities.length === entitiesBefore + 3 &&
|
||
groups.length === 1 &&
|
||
members === 3,
|
||
JSON.stringify({ entitiesBefore, groups, members }),
|
||
);
|
||
check('the hierarchy shows the group after the next update', await listed(`group:${groups[0]?.id}`));
|
||
check(
|
||
'behind the game tab the scene view did not draw once',
|
||
stillHidden === hidden,
|
||
JSON.stringify({ hidden, stillHidden }),
|
||
);
|
||
await showTab('scene');
|
||
const shown = (await json('__gm.viewportInfo()')).redraws;
|
||
check(
|
||
'and it draws again when its tab comes back',
|
||
shown > stillHidden,
|
||
JSON.stringify({ stillHidden, shown }),
|
||
);
|
||
await shot('59a-balanced-group');
|
||
await stop();
|
||
|
||
// 59d. Minimal: the hierarchy waits while the game runs, and a pause brings it up to date at once.
|
||
await showTab('scene');
|
||
await setWhilePlaying('Minimal');
|
||
await play();
|
||
await press('Space');
|
||
await sleep(600);
|
||
const spawnedGroups = await crateGroups();
|
||
const groupRow = `group:${spawnedGroups[0]?.id}`;
|
||
const listedRunning = await listed(groupRow);
|
||
check(
|
||
'on Minimal a spawn reaches the scene, but the hierarchy waits',
|
||
spawnedGroups.length === 1 && !listedRunning,
|
||
JSON.stringify({ spawnedGroups, listedRunning }),
|
||
);
|
||
await clickToolbar('pause');
|
||
check('and a pause lists it at once', (await playInfo()).paused && (await listed(groupRow)));
|
||
await shot('59b-minimal-paused');
|
||
await stop();
|
||
|
||
await undo();
|
||
check(
|
||
'the fixture is left on Balanced with the undo stack as found',
|
||
(await json('__gm.state.project.whilePlaying')) === 'balanced' && (await undoSteps()) === steps,
|
||
JSON.stringify({ steps: await undoSteps(), before: steps }),
|
||
);
|
||
|
||
// 59e. The performance overlay (#275): switched on in the game tab's header, it shows over a
|
||
// running game only, top left – the fps the times allow, fps, the entities the world has,
|
||
// and a time per area.
|
||
const performance = () => json('__gm.gamePerformance()');
|
||
await showTab('game');
|
||
await click((await performance()).button.x, (await performance()).button.y);
|
||
const waiting = await performance();
|
||
check(
|
||
'switched on in the header, the overlay waits for a game to run',
|
||
waiting.button.lit && !waiting.shown,
|
||
JSON.stringify(waiting),
|
||
);
|
||
await play();
|
||
await sleep(1200);
|
||
const running = await json(
|
||
'({ overlay: __gm.gamePerformance(), measured: __gm.playInfo().performance, entities: __gm.world().entities.length })',
|
||
);
|
||
const { overlay, measured } = running;
|
||
const times = Object.values(measured?.times ?? {});
|
||
check(
|
||
'while playing it shows in the top left corner of the game view',
|
||
overlay.shown &&
|
||
overlay.box.x - overlay.view.x >= 0 &&
|
||
overlay.box.x - overlay.view.x < 20 &&
|
||
overlay.box.y - overlay.view.y >= 0 &&
|
||
overlay.box.y - overlay.view.y < 20 &&
|
||
overlay.box.w > 100,
|
||
JSON.stringify(overlay),
|
||
);
|
||
check(
|
||
'it counts the entities the running world has',
|
||
measured?.entities === running.entities && overlay.lines[1]?.includes(`· ${running.entities} entities`),
|
||
JSON.stringify({ measured, entities: running.entities, line: overlay.lines[1] }),
|
||
);
|
||
check(
|
||
'and shows the fps and a time for each of the five areas, drawing included',
|
||
/^\d+ fps/.test(overlay.lines[1] ?? '') &&
|
||
overlay.lines.length === 7 &&
|
||
overlay.lines.slice(2).every((line) => / \d+\.\d\d ms$/.test(line)) &&
|
||
times.length === 5 &&
|
||
times.every((ms) => Number.isFinite(ms) && ms >= 0) &&
|
||
measured.times.drawing > 0,
|
||
JSON.stringify(overlay.lines),
|
||
);
|
||
const possible = Math.floor(1000 / times.reduce((sum, ms) => sum + ms, 0));
|
||
check(
|
||
'on top it shows the fps the measured times would allow, a second over their sum',
|
||
new RegExp(`^Possible +${possible} fps$`).test(overlay.lines[0] ?? '') && possible > 0,
|
||
JSON.stringify({ line: overlay.lines[0], possible }),
|
||
);
|
||
await shot('59c-performance');
|
||
await click(overlay.button.x, overlay.button.y);
|
||
const switchedOff = await performance();
|
||
await stop();
|
||
check(
|
||
'a second click switches it off again',
|
||
!switchedOff.button.lit && !switchedOff.shown,
|
||
JSON.stringify(switchedOff),
|
||
);
|
||
}
|