All checks were successful
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (pull_request) Successful in 3m28s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 7s
wai-app and Create / wai-app · does this change touch it (pull_request) Successful in 6s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 3m14s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (pull_request) Successful in 45s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (pull_request) Successful in 11m20s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 13m49s
joule-code conformance gate / joule-moq-relay, every target (pull_request) Successful in 3m35s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 8s
WAI conformance / does this change touch it (pull_request) Successful in 8s
WAI examples / WAI examples · does this change touch them (pull_request) Successful in 9s
WAI Studio / does this change touch it (pull_request) Successful in 9s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 36s
WAI conformance / faces from keypoints under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m2s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 3m19s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 29s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (pull_request) Successful in 59s
WAI conformance / fast integer kernels, every x86_64 kernel set (AVX-512 silicon) (pull_request) Successful in 3m9s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 3m35s
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Successful in 5m43s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 1m16s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 6m1s
WAI examples / WAI examples · portable · linux (pull_request) Successful in 10m16s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 11m30s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 11m58s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 16m38s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 12m38s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (pull_request) Successful in 14m53s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 7m24s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 24s
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Successful in 11m47s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 18m5s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 11s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 33s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 1m51s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 5m27s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h5m58s
The room's compositor redrew every tile on a fixed timer, whether or not a new frame had arrived, on a canvas asking for software rendering so that a test hook could read it back. A still room spent draws at the full rate, and a frame waited up to a whole period for the next tick. A tile is now drawn when its video has a new frame, at the next display refresh, with whatever lies over it; the whole canvas only after a re-layout. Simply drawing the canvas on every new frame would be worse for a large grid, whose unsynchronised sources would repaint all of it at every refresh. `stats()` reports what drawing spent: counts a test can hold, since a busy host can't flake them, and the latency from a frame's arrival to its drawing, which is reported and not held. The room's log and timeline no longer grow without bound in the DOM. Remote control held every action for the next refresh. A pointer moving is many actions a refresh, and that suits it; a key, a button or a wheel is one thing that happened, and now goes at once with the moves before it. Coalesced moves also kept the pointer's oldest place in a refresh, because the batch held a copy of the move that later ones updated in vain. They now keep the latest. Co-Authored-By: Claude Opus 5.5 <[email protected]>
148 lines
5.2 KiB
JavaScript
148 lines
5.2 KiB
JavaScript
// The compositor's drawing, without a browser: it spends only on what changed (UI budget). Run with
|
|
// `node --test e2e/compose.test.mjs`.
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const refreshes = [];
|
|
globalThis.requestAnimationFrame = (f) => refreshes.push(f);
|
|
const refresh = () => refreshes.splice(0).forEach((f) => f());
|
|
|
|
const { Compositor } = await import("../web/room/compose.js");
|
|
|
|
const params = { width: 640, height: 360, fps: 30, sample_rate: 48000 };
|
|
const fakeWai = { composeDescriptor: () => JSON.stringify({ outcome: "read", params, background: "101218" }) };
|
|
|
|
function make() {
|
|
const calls = { images: [], fills: 0 };
|
|
const ctx = {
|
|
fillRect() { calls.fills++; },
|
|
drawImage(vf, ...r) { calls.images.push({ who: vf.who, dst: r.slice(4) }); },
|
|
set fillStyle(_) {},
|
|
};
|
|
const canvas = { getContext: (kind, opts) => { calls.opts = opts; return ctx; }, width: 0, height: 0 };
|
|
let t = 0;
|
|
const c = new Compositor(fakeWai, canvas, new Uint8Array(), { now: () => t });
|
|
return { c, calls, tick: (ms) => { t += ms; } };
|
|
}
|
|
|
|
const frame = (who) => ({ who, displayWidth: 16, displayHeight: 9, closed: false, close() { this.closed = true; } });
|
|
const tile = (principal, dst) => ({ principal, tile: dst, src: [0, 0, 16, 9], dst });
|
|
const room = (tiles) => ({ frame: () => JSON.stringify({ outcome: "compose", frames: [{ t: 0, set_digest: tiles.map((x) => x.principal).join(), tiles }] }) });
|
|
|
|
const A = tile("a", [0, 0, 320, 360]);
|
|
const B = tile("b", [320, 0, 320, 360]);
|
|
const PIP = tile("pip", [260, 200, 120, 90]); // over both A and B, last in drawing order
|
|
|
|
test("nothing new draws nothing", () => {
|
|
const { c, calls } = make();
|
|
c.relayout(room([A, B]), 0);
|
|
c.start();
|
|
refresh();
|
|
const { draws } = c.stats();
|
|
assert.equal(draws, 1, "the first draw, of the layout");
|
|
assert.equal(calls.fills, 1);
|
|
for (let i = 0; i < 10; i++) refresh();
|
|
assert.equal(c.stats().draws, draws, "no refresh is even asked for");
|
|
assert.equal(refreshes.length, 0);
|
|
});
|
|
|
|
test("nothing is drawn before it starts", () => {
|
|
const { c } = make();
|
|
c.relayout(room([A]), 0);
|
|
c.setVideo("a", frame("a"));
|
|
assert.equal(refreshes.length, 0);
|
|
c.start();
|
|
assert.equal(refreshes.length, 1);
|
|
refresh();
|
|
assert.equal(c.stats().tiles, 1);
|
|
});
|
|
|
|
test("a new frame draws its tile alone, not the canvas", () => {
|
|
const { c, calls } = make();
|
|
c.relayout(room([A, B]), 0);
|
|
c.start();
|
|
c.setVideo("a", frame("a"));
|
|
c.setVideo("b", frame("b"));
|
|
refresh();
|
|
calls.images.length = 0;
|
|
const fills = calls.fills;
|
|
c.setVideo("b", frame("b"));
|
|
refresh();
|
|
assert.deepEqual(calls.images.map((d) => d.who), ["b"]);
|
|
assert.equal(calls.fills, fills, "the background is not painted again");
|
|
});
|
|
|
|
test("frames between refreshes draw once, the newest, and the others are released", () => {
|
|
const { c, calls } = make();
|
|
c.relayout(room([A]), 0);
|
|
c.start();
|
|
const first = frame("a");
|
|
c.setVideo("a", first);
|
|
c.setVideo("a", frame("a"));
|
|
assert.equal(refreshes.length, 1, "one draw asked for");
|
|
assert.equal(first.closed, true);
|
|
refresh();
|
|
assert.equal(calls.images.length, 1);
|
|
const s = c.stats();
|
|
assert.equal(s.frames, 2);
|
|
assert.ok(s.tiles <= s.frames, `${s.tiles} tiles from ${s.frames} frames`);
|
|
});
|
|
|
|
test("a tile drawn again brings back what lay over it", () => {
|
|
const { c, calls } = make();
|
|
c.relayout(room([A, B, PIP]), 0);
|
|
c.start();
|
|
for (const who of ["a", "b", "pip"]) c.setVideo(who, frame(who));
|
|
refresh();
|
|
calls.images.length = 0;
|
|
c.setVideo("a", frame("a"));
|
|
refresh();
|
|
assert.deepEqual(calls.images.map((d) => d.who), ["a", "pip"], "the window over it, in drawing order; not the tile it doesn't touch");
|
|
calls.images.length = 0;
|
|
c.setVideo("pip", frame("pip"));
|
|
refresh();
|
|
assert.deepEqual(calls.images.map((d) => d.who), ["pip"], "nothing lies over the window");
|
|
});
|
|
|
|
test("tiles that share a column or a row but don't touch are left alone", () => {
|
|
const { c, calls } = make();
|
|
const [TL, BL, TR] = [tile("tl", [0, 0, 320, 180]), tile("bl", [0, 180, 320, 180]), tile("tr", [320, 0, 320, 180])];
|
|
c.relayout(room([TL, BL, TR]), 0);
|
|
c.start();
|
|
for (const who of ["tl", "bl", "tr"]) c.setVideo(who, frame(who));
|
|
refresh();
|
|
calls.images.length = 0;
|
|
c.setVideo("tl", frame("tl"));
|
|
refresh();
|
|
assert.deepEqual(calls.images.map((d) => d.who), ["tl"]);
|
|
});
|
|
|
|
test("a re-layout draws the whole canvas", () => {
|
|
const { c, calls } = make();
|
|
c.relayout(room([A, B]), 0);
|
|
c.start();
|
|
c.setVideo("a", frame("a"));
|
|
c.setVideo("b", frame("b"));
|
|
refresh();
|
|
calls.images.length = 0;
|
|
const fills = calls.fills;
|
|
c.relayout(room([tile("a", [0, 0, 640, 360])]), 1);
|
|
refresh();
|
|
assert.equal(calls.fills, fills + 1);
|
|
assert.deepEqual(calls.images.map((d) => d.who), ["a"]);
|
|
});
|
|
|
|
test("the canvas asks for no software rendering", () => {
|
|
const { calls } = make();
|
|
assert.ok(!calls.opts || !calls.opts.willReadFrequently, "willReadFrequently is a software canvas, for a test's readback alone");
|
|
});
|
|
|
|
test("arrival to draw is reported from when the frame arrived", () => {
|
|
const { c, tick } = make();
|
|
c.relayout(room([A]), 0);
|
|
c.start();
|
|
c.setVideo("a", frame("a"));
|
|
tick(16);
|
|
refresh();
|
|
assert.equal(c.stats().arrival_to_draw_p95_ms, 16);
|
|
});
|