Keep a clip that starts later playing at its first frame
The e2e 68 check "a clip that starts later" was red in 3 of the last 8 suites (CI tasks 697, 703, 712). Two causes sat on top of each other. - The check measured too late. It started sampling after play(), and under CI load the clock was already at 0.05 to 0.22 s. It now starts before the click, like 68d does. - Measured early, a real bug showed: the video held for one frame at its first picture. The first picture triggers a redraw, and the game takes its first step in the same frame. readSpeed got both calls with the same frame time and divided by 1 ms. The speed came out at 16.7x, which counted as a jump, so the player paused. - readSpeed now measures a step without elapsed wall time against one frame at 60 fps. A real jump still counts as a jump. - e2e 34 "the pause holds the stream" also failed in task 712. Its cause is still open. The failure message now names the state before the pause, so the next red run shows where the samples got lost. Tests: - video-follow.test.ts 18/18, the new test is red without the fix - e2e 68 on melektaus-atx: ALL PASSED, 21 checks - Gate on melektaus-atx: CHECK EXIT 0, 314 files, 4853 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d7672a5af
commit
5b9d084dfe
4 files changed
+42
-12
No files matched your search
@@ -59,7 +59,11 @@ export function readSpeed(reading: SpeedReading | null, time: number, now: numbe
|
|||||||
while (samples.length > 2 && now - samples[1]!.at >= SPEED_SPAN_MS) samples.shift();
|
while (samples.length > 2 && now - samples[1]!.at >= SPEED_SPAN_MS) samples.shift();
|
||||||
const first = samples[0]!;
|
const first = samples[0]!;
|
||||||
const span = now - first.at;
|
const span = now - first.at;
|
||||||
const pace = ((time - first.time) * 1000) / Math.max(span, 1);
|
// Asked twice in one frame, the time moved in no time at all: the first picture of a video
|
||||||
|
// redraws the view at once, then the game takes its first step in the same frame. Read over a
|
||||||
|
// millisecond that step was a jump and the video held a frame at its start (CI task 712), so it
|
||||||
|
// counts over a frame at 60 fps.
|
||||||
|
const pace = ((time - first.time) * 1000) / (span > 0 ? span : 1000 / 60);
|
||||||
if (span >= SPEED_WINDOW_MS) return { samples, speed: pace };
|
if (span >= SPEED_WINDOW_MS) return { samples, speed: pace };
|
||||||
// A start from standing plays at 1x until the window has its reading: held and sought frame by
|
// A start from standing plays at 1x until the window has its reading: held and sought frame by
|
||||||
// frame instead, the first pictures of a cut stuck and were skipped. A step faster than a video
|
// frame instead, the first pictures of a cut stuck and were skipped. A step faster than a video
|
||||||
|
|||||||
@@ -44,6 +44,16 @@ describe('the speed of the asked time', () => {
|
|||||||
expect(readSpeed(readSpeed(null, 0.8, 0), 0.8 + 2 / 60, 16).speed).toBe(1);
|
expect(readSpeed(readSpeed(null, 0.8, 0), 0.8 + 2 / 60, 16).speed).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('counts a first step in the same frame as the ask before it as a start', () => {
|
||||||
|
// The first picture redraws the view at once, then the game steps within the same frame.
|
||||||
|
let reading = readSpeed(null, 0.8, 0);
|
||||||
|
reading = readSpeed(reading, 0.8, 16);
|
||||||
|
reading = readSpeed(reading, 0.8 + 1 / 60, 16);
|
||||||
|
expect(reading.speed).toBe(1);
|
||||||
|
// Within one frame a jump is still a jump.
|
||||||
|
expect(readSpeed(readSpeed(null, 0.8, 0), 0.9, 0).speed).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('does not count a jump faster than a video plays, or a time running back, as a start', () => {
|
it('does not count a jump faster than a video plays, or a time running back, as a start', () => {
|
||||||
expect(readSpeed(readSpeed(null, 0.8, 0), 0.9, 16).speed).toBe(0);
|
expect(readSpeed(readSpeed(null, 0.8, 0), 0.9, 16).speed).toBe(0);
|
||||||
expect(readSpeed(readSpeed(null, 0.8, 0), 0.8 - 1 / 60, 16).speed).toBe(0);
|
expect(readSpeed(readSpeed(null, 0.8, 0), 0.8 - 1 / 60, 16).speed).toBe(0);
|
||||||
|
|||||||
@@ -3986,7 +3986,15 @@ export async function sound(t) {
|
|||||||
resumedLong.streaming.skipped === 0 &&
|
resumedLong.streaming.skipped === 0 &&
|
||||||
stoppedLong.streaming.playing === 0 &&
|
stoppedLong.streaming.playing === 0 &&
|
||||||
stoppedLong.playing === 0,
|
stoppedLong.playing === 0,
|
||||||
JSON.stringify({ paused, pausedLater, resumedLong: resumedLong.streaming, stoppedLong }),
|
// Held before the pause too: it tells a stream that fell behind from the start from one the
|
||||||
|
// pause made lose its place (CI task 712).
|
||||||
|
JSON.stringify({
|
||||||
|
held: heldStreamed.streaming,
|
||||||
|
paused,
|
||||||
|
pausedLater,
|
||||||
|
resumedLong: resumedLong.streaming,
|
||||||
|
stoppedLong,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// The counter-proof: Stream off, the game plays it decoded, and the renderer holds all of it.
|
// The counter-proof: Stream off, the game plays it decoded, and the renderer holds all of it.
|
||||||
|
|||||||
@@ -244,20 +244,28 @@ export async function composition(t) {
|
|||||||
const p = __gm.state.project;
|
const p = __gm.state.project;
|
||||||
p.assets.find((a) => a.id === 'cp68-video').clips = [{ id: 'cp68-late', name: 'Late', props: { start: 0.8, end: 0 } }];
|
p.assets.find((a) => a.id === 'cp68-video').clips = [{ id: 'cp68-late', name: 'Late', props: { start: 0.8, end: 0 } }];
|
||||||
p.assets.find((a) => a.id === 'cp68-screen').props.texture = 'cp68-late';`);
|
p.assets.find((a) => a.id === 'cp68-screen').props.texture = 'cp68-late';`);
|
||||||
await play();
|
// Sampled from before the click on Play, as in 68d: under load play() settles after the wait is
|
||||||
const rows = JSON.parse(
|
// over, and the first row was drawn already with the clock running (CI tasks 697, 703, 712).
|
||||||
await evaluate(`new Promise((done) => {
|
await evaluate(`{
|
||||||
const rows = [];
|
const rows = [];
|
||||||
|
const until = performance.now() + 20000;
|
||||||
|
window.cp68Rows = new Promise((done) => {
|
||||||
const step = () => {
|
const step = () => {
|
||||||
const player = __gm.gameVideo('${id('v')}');
|
if (__gm.state.mode === 'play' && __gm.world()) {
|
||||||
rows.push({ time: ${clock}, drawn: __gm.gameDrawnRect('${id('v')}') !== null,
|
const player = __gm.gameVideo('${id('v')}');
|
||||||
at: player?.time ?? null, paused: player?.paused ?? null });
|
rows.push({ time: ${clock}, drawn: __gm.gameDrawnRect('${id('v')}') !== null,
|
||||||
if (rows.length >= 240 || !__gm.world()) return done(JSON.stringify(rows));
|
at: player?.time ?? null, paused: player?.paused ?? null });
|
||||||
|
}
|
||||||
|
if (rows.length >= 240 || (rows.length > 0 && !__gm.world()) || performance.now() > until)
|
||||||
|
return done(JSON.stringify(rows));
|
||||||
requestAnimationFrame(step);
|
requestAnimationFrame(step);
|
||||||
};
|
};
|
||||||
step();
|
step();
|
||||||
})`),
|
});
|
||||||
);
|
null;
|
||||||
|
}`);
|
||||||
|
await play();
|
||||||
|
const rows = JSON.parse(await evaluate('window.cp68Rows'));
|
||||||
const first = rows.findIndex((row) => row.drawn);
|
const first = rows.findIndex((row) => row.drawn);
|
||||||
const moving = rows.filter((row) => row.drawn && row.time > 0).slice(0, 3);
|
const moving = rows.filter((row) => row.drawn && row.time > 0).slice(0, 3);
|
||||||
check(
|
check(
|
||||||
|
|||||||
Reference in new issue
Block a user