API

Module: Animation Parity Testing — Deterministic Frame Sync

Scope: lab/, scripts/capture-golden.ts, tests/lite/parity/

Purpose

Animated scenes (skeleton, morph targets) must be captured at the exact same animation frame in both the Babylon.js reference and Babylon Lite. Without synchronisation, frame-rate differences cause different poses and produce false parity failures. This module documents the deterministic frame synchronisation protocol that eliminates that class of flake.


Problem

Each machine/GPU renders at a different native frame rate. Babylon.js adapts its animation timestep to the real delta time, so a 144 Hz monitor and a 60 Hz monitor will show a different pose after the same wall-clock duration. Screenshot comparisons therefore produce spurious diffs unless both sides agree on which animation frame to capture and use identical timesteps to reach it.


Solution: Freeze-at-Frame-N + Constant Timestep

The protocol has two parts:

  1. Constant timestep — both engines advance animations by exactly 16 ms per frame, regardless of the real delta time.
  2. Frame freeze — at a predetermined frame count (e.g. frame 300), both engines pause all animations but continue rendering. A DOM signal tells the test harness that the frozen frame is ready for capture.

Constant Timestep (Both Sides)

BJS Reference (babylon-ref-sceneN.html)

engine.getDeltaTime = function () {
return 16;
};
scene.useConstantAnimationDeltaTime = true;

Lite (sceneN.ts)

engine._fixedDeltaMs = 16.0;

Both sides now advance at a fixed 16 ms / frame. Animation state after N frames is fully deterministic.


Frame Freeze

At a specific frame count both engines pause animation but keep rendering.

BJS Reference

let frameCount = 0;
scene.registerBeforeRender(() => {
frameCount++;
if (frameCount === 300) {
scene.animationGroups.forEach((g) => g.pause());
canvas.dataset.animationFrozen = "true";
}
});

Lite (gated behind ?freeze query param)

if (new URLSearchParams(location.search).has("freeze")) {
let frameCount = 0;
engine._beforeRender.push(() => {
frameCount++;
if (frameCount === 300) {
engine.pauseAnimations();
canvas.dataset.animationFrozen = "true";
}
});
}

The ?freeze query-param gate keeps interactive browsing unaffected — only tests and the golden-capture script append it.


Signal Protocol

canvas.dataset.animationFrozen = 'true' is the DOM signal consumed by:

  1. Golden capture (scripts/capture-golden.ts) — waits for this signal before taking the reference screenshot.
  2. Parity tests (tests/lite/parity/sceneN-*.spec.ts) — waits for the signal on the Lite page before pixel-comparing against the golden.

Parity Test Pattern

test("Scene N — Animated model matches reference", async ({ page }) => {
// Load Lite scene with freeze param
await page.goto("/sceneN.html?freeze");
await page.waitForFunction(() => document.querySelector("canvas")?.dataset.ready === "true", { timeout: 30_000 });
// Wait for exact animation frame
await page.waitForFunction(() => document.querySelector("canvas")?.dataset.animationFrozen === "true", { timeout: 30_000 });
// Screenshot and compare against golden
const screenshot = await page.screenshot({/* … */});
// … pixel comparison …
});

Golden Capture

Scene startup reliability (scenes 112 and 121)

Scene 112 serves the Flight Helmet KTX2 model from /models/FlightHelmetKTX/. pnpm fetch:flight-helmet restores the model, buffer and 15 textures from BabylonJS/Assets commit 183f26290601abe1d5282eed00c306f4c8228307. Every file is checked against its Git blob digest before being written or reused. Downloads have a bounded timeout and failures stop setup with the asset URL and HTTP status. The generated asset directory is ignored by git; binaries remain in BabylonJS/Assets, not the Lite package. Lab startup, static lab builds and bundle builds selecting scene 112 prepare these files before serving them.

Scene 121 uses the existing checked-in /splats/Halo_Believe.splat. Its BJS reference constructs an unloaded mesh and calls loadFileAsync once. Both scenes report initialization failures through canvas.dataset.error as well as console.error. Their parity tests use waitForCanvasReady, which polls from Node to keep the BrowserStack connection active and rejects immediately on data-error. The 150-second readiness budget fits inside the 180-second test deadline, preserving the actual startup error instead of a teardown-induced "browser closed" error.

These two parity tests only compare Lite output with committed goldens; they never open a live BJS reference or replace a golden at test time. The scene 112 golden is unchanged. The scene 121 golden is the reference attachment from its passing test in CI build 60995 / 20261019.6: data/37dcbe75cd41caf80e7133f2cb2675fa1e91e9b1.png. It was captured by Babylon.js, not Lite, and the run reported MAD 0.014.

Focused non-visual startup tests block jsDelivr, exercise both local asset paths, and inject asset failures to verify that the error reaches the readiness waiter. Downloader unit tests cover verified reuse, HTTP errors and corrupt downloads. Existing MAD and bundle-size ceilings are unchanged.

scripts/capture-golden.ts is a CLI tool that captures golden reference PNGs:

pnpm exec tsx scripts/capture-golden.ts 5 # Capture scene 5 golden

For animated scenes it:

  1. Opens the BJS reference HTML.
  2. Waits for canvas.dataset.animationFrozen === 'true'.
  3. Takes screenshot → reference/lite/sceneN-*/babylon-ref-golden.png.

Key Design Decisions

DecisionRationale
Exact frame (=== N), not >= N
Both BJS and Lite must freeze at the identical animation pose. An inequality would allow off-by-one drift.
Constant 16 ms delta
Without this, BJS adapts to actual frame rate, making poses non-deterministic across different hardware.
Query-param gating (?freeze)
Interactive browsing stays unaffected. Only tests and golden capture append the param.
Frame 300
Late enough that all assets are loaded and animations are mid-cycle (interesting poses), early enough for fast tests (~5 s at 60 fps).

File Manifest

FileRole
lab/lite/babylon-ref-scene5.html
BJS reference with frame sync
lab/lite/babylon-ref-scene7.html
BJS reference with frame sync
lab/lite/src/lite/scene5.ts
Lite scene with freeze support
lab/lite/src/lite/scene7.ts
Lite scene with freeze support
scripts/capture-golden.ts
Golden-capture CLI
tests/lite/parity/scene5-alien.spec.ts
Animated parity test
tests/lite/parity/scene7-chibirex.spec.ts
Animated parity test

Applying to New Animated Scenes

When adding a new animated scene:

  1. BJS ref — add getDeltaTime = () => 16, useConstantAnimationDeltaTime, frame counter, freeze + signal.
  2. Lite scene — add _fixedDeltaMs = 16, freeze behind ?freeze param, signal.
  3. Parity test — wait for both ready and animationFrozen signals before screenshotting.
  4. capture-golden.ts — add scene to the registry with animated: true and the freeze frame count.