| undefined;
+ if (readback && timed) {
+ await readback.mapAsync(GPUMapMode.READ);
+ const ticks = new BigUint64Array(readback.getMappedRange());
+ passes = Object.fromEntries(definition.passes.map((pass, i) => [pass.id, Number(ticks[i * 2 + 1] - ticks[i * 2]) / 1e6]));
+ gpuMs = Number(ticks[passCount * 2 - 1] - ticks[0]) / 1e6;
+ readback.unmap();
+ }
+ if (failures.length) throw new Error(failures.join("\n"));
+ return { seconds, renderScale, completedMs, ...(intervalMs === undefined ? {} : { intervalMs }), ...(gpuMs === undefined ? {} : { gpuMs, passes }) };
+ }
+ for (let frame = 0; frame < warmup; frame++) await render(frame, false);
+ for (let repetition = 0; repetition < repetitions; repetition++) {
+ for (let frame = 0; frame < frames; frame++) samples.push({ repetition, frame, ...await render(frame, true) });
+ }
+ // Refresh after the promise chain so the exported still has a live texture.
+ await render(90, false);
+ const png = canvas.toDataURL("image/png");
+ const info = adapter.info;
+ const intervals = samples.flatMap(sample => sample.intervalMs === undefined ? [] : [sample.intervalMs]);
+ return {
+ environment: { browser: navigator.userAgent, adapter: { vendor: info.vendor, architecture: info.architecture, device: info.device, description: info.description },
+ timestampQuery: timestamps, timestampQuerySupported: timestampSupported, paced, width, height, warmup, frames, repetitions, devicePixelRatio,
+ viewport: { width: innerWidth, height: innerHeight }, visibility: document.visibilityState },
+ methodology: `${paced ? "rAF-paced" : "Unpaced"} fixed-clock samples; completedMs includes uniform evaluation, writes, encode, submit and queue completion. GPU frame span is the last pass end minus the first pass beginning; pass scopes may overlap and are non-additive. GPU timestamps exclude CPU and presentation and are disabled in paced runs to avoid readback overhead. rAF cadence is browser callback cadence, not evidence of physical display presentation.`,
+ pipeline: { targets: definition.targets, passes: definition.passes.map(pass => ({ id: pass.id, kind: pass.kind })) },
+ renderScale: { min: Math.min(...samples.map(sample => sample.renderScale)), max: Math.max(...samples.map(sample => sample.renderScale)), final: samples.at(-1)!.renderScale },
+ completed: summarize(samples.map(sample => sample.completedMs)),
+ gpu: timestamps ? summarize(samples.map(sample => sample.gpuMs!)) : null,
+ passes: timestamps ? Object.fromEntries(definition.passes.map(pass => [pass.id, summarize(samples.map(sample => sample.passes![pass.id]))])) : null,
+ cadence: intervals.length ? { ...summarize(intervals), intervalCount: intervals.length,
+ observedFps: intervals.length * 1000 / intervals.reduce((sum, ms) => sum + ms, 0),
+ longIntervalsOver25Ms: intervals.filter(ms => ms > 25).length,
+ estimatedMissed60HzFrames: intervals.reduce((sum, ms) => sum + Math.max(0, Math.round(ms / (1000 / 60)) - 1), 0) } : null,
+ repetitions: Array.from({ length: repetitions }, (_, repetition) => ({ repetition,
+ completed: summarize(samples.filter(sample => sample.repetition === repetition).map(sample => sample.completedMs)),
+ gpu: timestamps ? summarize(samples.filter(sample => sample.repetition === repetition).map(sample => sample.gpuMs!)) : null })),
+ samples, png,
+ };
+ } finally {
+ context.unconfigure();
+ device.destroy();
+ }
+}
diff --git a/packages/slides-effects/scripts/world-benchmark.mjs b/packages/slides-effects/scripts/world-benchmark.mjs
new file mode 100644
index 000000000..2b80d8ae8
--- /dev/null
+++ b/packages/slides-effects/scripts/world-benchmark.mjs
@@ -0,0 +1,16 @@
+import { mkdir } from "node:fs/promises";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const args = process.argv.slice(2);
+if (args.includes("--help") || args.length !== 2 || args[0] !== "--output") {
+ console.log("Usage: bun packages/slides-effects/scripts/world-benchmark.mjs --output /absolute/path/harness.html\nGenerates a standalone offline WebGPU measurement page. In Chrome, call await benchmarkWorld({width:1920,height:1080,quality:'high'}). Add allEffects:true for the full scene, quality:'auto',paced:true for the shared adaptive controller (unpaced auto stays high/manual), renderScale:0.75 for a manual scale, or paced:true for rAF interval measurements (without GPU timestamp readback).\nResult includes raw samples, per-pass GPU timestamps when available, completed-submit wall times and a PNG data URL.\nGenerate and launch the browser through the heavy gate; call the measurement through the fps gate. Keep generated pages/results outside the repository.");
+ process.exit(args.includes("--help") ? 0 : 2);
+}
+const output = resolve(args[1]);
+const result = await Bun.build({ entrypoints: [fileURLToPath(new URL("./worldBenchmark.ts", import.meta.url))], target: "browser" });
+if (!result.success) throw new AggregateError(result.logs, "World benchmark bundling failed");
+const code = (await result.outputs[0].text()).replaceAll("World render benchmarkReady. Call benchmarkWorld({width:1920,height:1080,quality:"high"}) from DevTools.
`);
+console.log(output);
diff --git a/packages/slides-effects/scripts/worldBenchmark.ts b/packages/slides-effects/scripts/worldBenchmark.ts
new file mode 100644
index 000000000..f489fff52
--- /dev/null
+++ b/packages/slides-effects/scripts/worldBenchmark.ts
@@ -0,0 +1,73 @@
+import { customInstances, graphInstances, orbInstances, prepareWorldScene, primitive, pulseInstances, rowInstances, sampleWorldUniforms, sdfInstance, sprayInstances, voxelInstances, type WorldScene } from "../packs/world/scene";
+import { tree, wave } from "../packs/world/graph";
+import { buildWorldGraph } from "../packs/world/render";
+import { advanceResolution, createResolutionState } from "../packs/world/resolution";
+import { benchmarkRenderGraph, type BenchmarkOptions } from "./renderGraphBenchmark";
+
+type Options = Omit & { quality: NonNullable; allEffects?: boolean; renderScale?: number };
+
+export async function benchmarkWorld(options: Options) {
+ const data = tree("root", Array.from({ length: 3 }, (_, branch) => [
+ `branch-${branch}`,
+ Array.from({ length: 3 }, (_, group) => [`group-${branch}-${group}`, Array.from({ length: 3 }, (_, leaf) => `leaf-${branch}-${group}-${leaf}`)]),
+ ]) as Parameters[1]);
+ const effectSamples = options.quality === "low" ? 8 : options.quality === "medium" ? 12 : 16;
+ const effects: Partial = options.allEffects ? {
+ floor: { mirror: true, y: -5.5, roughness: 0.08, reflectivity: 0.8, size: 40, scale: 0.5 },
+ dof: { focus: "bench-pulse", aperture: 0.75, maxBlur: 12, scale: 0.5, samples: effectSamples },
+ shafts: { intensity: 0.6, density: 0.03, anisotropy: 0.3, scale: 0.25, samples: effectSamples }, chromatic: 0.0025,
+ rim: { color: "#b56bff", intensity: 1.1 },
+ lights: [
+ { kind: "spot", position: [-4, 7, 3], target: [0, -4, 0], color: "#22d3ee", intensity: 18, range: 40, angle: 0.6 },
+ { kind: "point", position: [5, 2, 2], color: "#b56bff", intensity: 8, range: 28 },
+ ],
+ counter: { source: "bench-voxels", position: [0.86, 0.14], size: 0.065, color: "#22d3ee" },
+ passes: [{ after: "bloom", wgsl: `fn customPass(uv:vec2f,color:vec4f,ctx:PassContext)->vec4f {
+ return vec4f(color.rgb*(.98+.02*sin(uv.y*800.+ctx.slideTime*.3)),color.a);
+ }` }],
+ } : {};
+ const scene = prepareWorldScene({
+ ...effects,
+ quality: options.quality,
+ camera: { shots: [{ at: 0, orbit: { center: [0, -0.5, 0], radius: 22, height: 8, speed: 0.06 }, fov: 42 }] },
+ instances: [
+ ...orbInstances({ count: 64, seed: 7, spread: [18, 9, 10], colors: ["#22d3ee", "#b56bff"], emissive: 5 }),
+ ...graphInstances({ data, node: { color: "#b56bff", size: 0.27 }, edge: { color: "#22d3ee", curve: 0.4, glow: 2 }, activate: [wave({ from: "root", step: 1, speed: 0.35 })] }),
+ primitive("ring", { size: 8, position: [0, -5, 0], color: "#22d3ee", emissive: 5 }),
+ ...(options.allEffects ? [
+ ...pulseInstances({ id: "bench-pulse", path: ["root", "branch-0", "group-0-0", "leaf-0-0-0"], step: 1, duration: 8, trail: 0.5, color: "#22d3ee", size: 0.12 }),
+ ...sprayInstances({ id: "bench-spray", under: "leaves:lit", rate: 24, lifetime: 2, seed: 11, color: "#b56bff" }),
+ ...rowInstances({ id: "bench-rows", count: 8, position: [-7, -1, 0], width: 2.4, spacing: 0.6, speed: 0.8, changes: [{ row: 2, step: 1 }] }),
+ ...voxelInstances({ id: "bench-voxels", grid: [4, 4, 4], position: [7, -1, 0], spacing: 0.45, size: 0.18, color: "#b56bff", collapse: { step: 1, to: 0.2, duration: 6 } }),
+ ...customInstances({ id: "bench-custom", count: 2, vertexCount: 6, position: [0, -1, 3], size: 0.5, wgsl: `
+ fn shape(id:u32,vid:u32,ctx:ShapeContext)->ShapeVertex {
+ let corners=array(vec2f(-1.,-1.),vec2f(1.,-1.),vec2f(-1.,1.),vec2f(-1.,1.),vec2f(1.,-1.),vec2f(1.,1.));
+ let p=corners[vid%6u];
+ return ShapeVertex(vec3f(p.x+(f32(id)-.5)*3.,p.y+sin(ctx.slideTime)*.4,0.),vec3f(0.,0.,1.));
+ }
+ fn shade(id:u32,world:vec3f,normal:vec3f,ctx:ShapeContext)->Material {
+ return Material(vec3f(.03,.3,.4),vec3f(.03,.7,.9)*2.,.4,.1);
+ }` }),
+ sdfInstance({ id: "bench-sdf", position: [-3, 0, 3], size: 0.7, color: "#22d3ee", bounds: [[-1, -1, -1], [1, 1, 1]], wgsl: "fn sdf(p:vec3f)->f32 { return length(p)-.7; }" }),
+ ] : []),
+ ],
+ bloom: { intensity: 1.4, threshold: 1, radius: 0.8 }, fog: { density: 0.025, color: "#05060a" },
+ vignette: 0.3, grain: 0.015, exposure: 1.1,
+ });
+ const adaptiveActive = options.quality === "auto" && options.paced === true;
+ let resolution = createResolutionState();
+ const result = await benchmarkRenderGraph({ ...options, definition: buildWorldGraph(scene),
+ renderScaleAt: timestamp => {
+ if (adaptiveActive) resolution = advanceResolution(resolution, { timestamp, playing: true, key: `${options.width}x${options.height}` });
+ return (options.renderScale ?? 1) * (adaptiveActive ? resolution.scale : 1);
+ },
+ uniformsAt: (seconds, renderScale) => sampleWorldUniforms(scene, { slideTime: seconds, stepTime: seconds, stepIndex: 1, stepOrigins: { 0: 0, 1: 0 } }, undefined, 1, renderScale) });
+ return { ...result, workload: { quality: options.quality, orbs: 64, graphNodes: 40, graphEdges: 39, rings: 1,
+ allEffects: !!options.allEffects, instanceCount: scene.instances.length, adaptiveActive, manualRenderScale: options.renderScale ?? 1,
+ effects: options.allEffects ? { reflection: { sceneScaleFactor: scene.floor?.scale }, dof: { sceneScaleFactor: scene.dof?.scale, samples: scene.dof?.samples },
+ shafts: { sceneScaleFactor: scene.shafts?.scale, samples: scene.shafts?.samples }, chromatic: 0.0025, lights: 2,
+ pulseInstances: 2, sprayParticles: 48, rows: 8, voxels: 64, customQuads: 2, sdfObjects: 1, customPost: true } : null,
+ clock: "2 + frame / 60 seconds, step 1 at time zero; each repetition replays the same samples", seed: 7 } };
+}
+
+Object.assign(window, { benchmarkWorld });
diff --git a/packages/slides-effects/tests/world.test.ts b/packages/slides-effects/tests/world.test.ts
new file mode 100644
index 000000000..326d8c4b4
--- /dev/null
+++ b/packages/slides-effects/tests/world.test.ts
@@ -0,0 +1,407 @@
+import { describe, expect, test } from "bun:test";
+import { evaluateCamera, flyTo, type CameraOptions, type ClockSample, type Vec3 } from "../packs/world/camera";
+import { activationAt, layoutTree, subset, tree, wave } from "../packs/world/graph";
+import { buildWorldGraph } from "../packs/world/render";
+import { compileTrack, customInstances, graphInstances, instanceOpacity, instancePosition, orbInstances, prepareWorldScene, primitive, pulseInstances,
+ resolveAnchor, rowInstances, rowLogicalIndex, sampleTrack, sampleWorldCamera, sampleWorldUniforms, sdfInstance, sprayInstances, voxelInstances, worldHeaderSize, worldInstanceSize } from "../packs/world/scene";
+import { advancePlayback, createPlaybackState, transitionPlayback } from "../../presentation/src/playbackState";
+
+function clock(time: number, step = 0, origins: Record = { 0: 0 }): ClockSample {
+ return { slideTime: time, stepTime: time - (origins[step] ?? 0), stepIndex: step, stepOrigins: origins };
+}
+describe("world camera", () => {
+ test("samples step changes and scrubs without integration history", () => {
+ const options: CameraOptions = { duration: 2, ease: "linear", shots: [
+ { step: 0, position: [0, 0, 20], target: [0, 0, 0], fov: 40 },
+ { step: 1, position: [8, 4, 12], fov: 60 },
+ ] };
+ expect(evaluateCamera(options, clock(4, 1, { 0: 0, 1: 4 })).position).toEqual([0, 0, 20]);
+ expect(evaluateCamera(options, clock(5, 1, { 0: 0, 1: 4 })).position).toEqual([4, 2, 16]);
+ expect(evaluateCamera(options, clock(6, 1, { 0: 0, 1: 4 })).fov).toBe(60);
+ expect(evaluateCamera(options, clock(5, 1, { 0: 0, 1: 4 })).position).toEqual([4, 2, 16]);
+ expect(evaluateCamera(options, clock(1))).toEqual(evaluateCamera(options, clock(0)));
+ // A directly entered step still has an explicit initial pose.
+ expect(evaluateCamera(options, clock(0, 1, { 1: 0 })).position).toEqual([0, 0, 20]);
+ });
+ test("freezes the previous moving shot at the next keyframe's origin", () => {
+ const first: CameraOptions = { ease: "linear", shots: [{ at: 0, orbit: { center: [0, 0, 0], radius: 10, speed: 0.2, height: 3 } }] };
+ const options: CameraOptions = { ...first, duration: 2, shots: [...first.shots!, { at: 4, position: [0, 3, 20] }] };
+ const boundary = evaluateCamera(first, clock(4));
+ expect(evaluateCamera(options, clock(4)).position).toEqual(boundary.position);
+ const halfway = evaluateCamera(options, clock(5));
+ expect(halfway.position[0]).toBeCloseTo(boundary.position[0] * 0.5);
+ expect(halfway.position[2]).toBeCloseTo((boundary.position[2] + 20) * 0.5);
+ });
+ test("evaluates follow, dolly, orbit and continuity from the same clock", () => {
+ const follow: CameraOptions = { shots: [{ position: [0, 0, 20], follow: "node:a", dolly: 5 }] };
+ const pose = evaluateCamera(follow, clock(4), { "node:a": [0, 0, 10] });
+ expect(pose.target).toEqual([0, 0, 10]);
+ expect(pose.position).toEqual([0, 0, 15]);
+ const previous = { position: [10, 0, 10] as [number, number, number], target: [0, 0, 0] as [number, number, number], fov: 40, roll: 0 };
+ expect(evaluateCamera(follow, clock(0), { "node:a": [0, 0, 10] }, previous, 1)).toEqual(previous);
+ expect(evaluateCamera(follow, clock(1), { "node:a": [0, 0, 10] }, previous, 1)).toEqual(pose);
+ const orbit: CameraOptions = { shots: [{ orbit: { center: [1, 2, 3], radius: 10, height: 4, speed: Math.PI / 2 } }] };
+ expect(evaluateCamera(orbit, clock(1)).position[0]).toBeCloseTo(11);
+ expect(evaluateCamera(orbit, clock(1)).position[1]).toBe(6);
+ });
+ test("a direct later-step entry completes missing earlier shots before starting its own", () => {
+ const options: CameraOptions = { ease: "linear", duration: 1, shots: [
+ { step: 0, position: [0, 0, 20] }, { step: 1, position: [5, 0, 15] },
+ { step: 2, position: [10, 0, 10] }, { step: 3, position: [15, 0, 5] },
+ ] };
+ expect(evaluateCamera(options, clock(0, 3, { 3: 0 })).position).toEqual([10, 0, 10]);
+ expect(evaluateCamera(options, clock(0.5, 3, { 3: 0 })).position).toEqual([12.5, 0, 7.5]);
+ expect(evaluateCamera(options, clock(4, 3, { 3: 0 })).position).toEqual([15, 0, 5]);
+ expect(evaluateCamera(options, clock(0, 1, { 1: 0 })).position).toEqual([0, 0, 20]);
+ const orbit: CameraOptions = { ease: "linear", duration: 1, shots: [
+ { step: 0, position: [0, 0, 20] }, { step: 1, orbit: { center: [0, 0, 0], radius: 10, height: 3, speed: 0.5 } },
+ { step: 2, target: [0, 0, 0], dolly: 5 },
+ ] };
+ expect(evaluateCamera(orbit, clock(0, 2, { 2: 0 })).position).toEqual([0, 3, 10]);
+ const settled = evaluateCamera(orbit, clock(1, 2, { 2: 0 })).position;
+ expect(settled[0]).toBe(0);
+ expect(Math.hypot(...settled)).toBeCloseTo(5);
+ expect(evaluateCamera(orbit, clock(4, 2, { 2: 0 })).position).toEqual(settled);
+ });
+ test("leaving a moving follow target samples its exact keyframe-boundary position", () => {
+ const options: CameraOptions = { ease: "linear", duration: 2, shots: [
+ { at: 0, position: [0, 0, 20], follow: "moving" }, { at: 4, position: [10, 0, 10], target: [0, 0, 0] },
+ ] };
+ const objects = (_id: string, sample: ClockSample): [number, number, number] => [sample.slideTime * 2, 0, 0];
+ expect(evaluateCamera(options, clock(4), objects).target).toEqual([8, 0, 0]);
+ expect(evaluateCamera(options, clock(5), objects).target).toEqual([4, 0, 0]);
+ const moving = orbInstances({ id: "moving", count: 1, drift: 1 })[0];
+ const scene = prepareWorldScene({ instances: [moving], camera: { ...options, shots: [
+ { at: 0, follow: "moving:0" }, options.shots![1],
+ ] } });
+ const boundary = instancePosition(moving, clock(4));
+ expect(sampleWorldCamera(scene, clock(4)).target).toEqual(boundary);
+ const halfway = sampleWorldCamera(scene, clock(5)).target;
+ halfway.forEach((value, index) => expect(value).toBeCloseTo(boundary[index] / 2));
+ });
+});
+
+describe("world graph", () => {
+ const data = tree("root", [["a", ["a1", "a2"]], ["b", ["b1", "b2"]]]);
+ test("lays out unique finite nodes on widening cone levels", () => {
+ const layout = layoutTree(data);
+ expect(layout.nodes).toHaveLength(7);
+ expect(layout.edges).toHaveLength(6);
+ expect(layout.nodes.find(node => node.id === "a1")?.ancestors).toEqual(["root", "a"]);
+ expect(layout.nodes.every(node => node.position.every(Number.isFinite))).toBe(true);
+ const a = layout.nodes.find(node => node.id === "a")!, a1 = layout.nodes.find(node => node.id === "a1")!;
+ expect(a1.position[1]).toBeLessThan(a.position[1]);
+ expect(Math.hypot(a1.position[0], a1.position[2])).toBeGreaterThan(Math.hypot(a.position[0], a.position[2]));
+ expect(() => layoutTree(tree("root", ["root"]))).toThrow("duplicate node id");
+ });
+ test("wave cascades through descendants; a later subset dims untouched branches", () => {
+ const { nodes } = layoutTree(data);
+ const root = nodes[0], leaf = nodes.find(node => node.id === "a1")!, b = nodes.find(node => node.id === "b")!;
+ const activate = [wave({ from: "root", step: 1, speed: 0.5, duration: 0.25 }), subset({ branches: ["b"], step: 2, duration: 0.5 })];
+ expect(activationAt(root, nodes, activate, clock(1, 0))).toBe(0);
+ expect(activationAt(root, nodes, activate, clock(2.25, 1, { 0: 0, 1: 2 }))).toBe(1);
+ expect(activationAt(leaf, nodes, activate, clock(2.25, 1, { 0: 0, 1: 2 }))).toBe(0);
+ expect(activationAt(leaf, nodes, activate, clock(3.25, 1, { 0: 0, 1: 2 }))).toBe(1);
+ expect(activationAt(leaf, nodes, activate, clock(5, 2, { 0: 0, 1: 2, 2: 4 }))).toBe(0);
+ expect(activationAt(b, nodes, activate, clock(5, 2, { 0: 0, 1: 2, 2: 4 }))).toBe(1);
+ // Backwards samples produce the same activation, independent of previous samples.
+ expect(activationAt(leaf, nodes, activate, clock(3.25, 1, { 0: 0, 1: 2 }))).toBe(1);
+ });
+ test("animated node positions keep attached curved tube endpoints in sync", () => {
+ const scene = prepareWorldScene({ instances: graphInstances({ data, nodes: { a: { position: { 0: [1, 2, 3], 1: [5, 6, 7] } } } }) });
+ const values = sampleWorldUniforms(scene, clock(3, 1, { 0: 0, 1: 2 }));
+ const nodeIndex = worldHeaderSize + scene.instances.findIndex(instance => instance.id === "node:a") * worldInstanceSize;
+ const edgeIndex = worldHeaderSize + scene.instances.findIndex(instance => instance.id === "edge:root:a") * worldInstanceSize;
+ expect(values.slice(nodeIndex, nodeIndex + 3)).toEqual([5, 6, 7]);
+ expect(values.slice(edgeIndex + 12, edgeIndex + 15)).toEqual([5, 6, 7]);
+ });
+ test("missing historical steps are settled during direct-entry preparation and previews", () => {
+ const { nodes } = layoutTree(data);
+ const leaf = nodes.find(node => node.id === "a1")!;
+ const activate = [wave({ from: "root", step: 1 }), subset({ branches: ["b"], step: 3 })];
+ expect(activationAt(leaf, nodes, activate, clock(0, 3, { 3: 0 }))).toBe(1);
+ expect(activationAt(leaf, nodes, activate, clock(4, 3, { 3: 0 }))).toBe(0);
+ expect(activationAt(leaf, nodes, activate, clock(0, 1, { 1: 0 }))).toBe(0);
+ const track = compileTrack({ 0: 0, 1: 10, 2: 20, 3: 30 });
+ expect(sampleTrack(track, clock(0, 3, { 3: 0 }))).toBe(20);
+ expect(sampleTrack(track, clock(4, 3, { 3: 0 }))).toBe(30);
+ });
+});
+
+test("scalar/vector keyframes and post settings are clock-driven", () => {
+ const track = compileTrack({ 0: 1, 1: 3 });
+ expect(sampleTrack(track, clock(1.325, 1, { 0: 0, 1: 1 }))).toBeCloseTo(2);
+ const scene = prepareWorldScene({ instances: [], exposure: { 0: 1, 1: 2 }, fog: { density: { 0: 0.01, 1: 0.1 }, color: { 0: "#000000", 1: "#ffffff" } }, bloom: { intensity: { 0: 1, 1: 2 } } });
+ const values = sampleWorldUniforms(scene, clock(2, 1, { 0: 0, 1: 1 }));
+ expect(values[8]).toBe(2); expect(values[9]).toBe(2); expect(values[12]).toBe(0.1);
+ expect(values.slice(20, 23)).toEqual([1, 1, 1]);
+ const overlapping = compileTrack([{ at: 0, value: 0 }, { at: 1, value: 10, duration: 2, ease: "linear" }, { at: 2, value: 20, duration: 2, ease: "linear" }]);
+ expect(sampleTrack(overlapping, clock(2))).toBe(5);
+ expect(sampleTrack(overlapping, clock(3))).toBe(12.5);
+});
+
+test("HDR scene and five-level half-resolution bloom describe independent attachments", () => {
+ const scene = prepareWorldScene({ instances: [...orbInstances({ count: 3 }), primitive("box"), primitive("ring"), primitive("tube")], bloom: { intensity: 1.5 } });
+ const graph = buildWorldGraph(scene);
+ expect(graph.targets.find(target => target.id === "hdr")?.format).toBe("rgba16float");
+ expect(graph.targets.find(target => target.id === "depth")?.format).toBe("depth32float");
+ expect(graph.targets.find(target => target.id === "bloom")).toMatchObject({ scale: 0.5, mips: 5 });
+ const render = graph.passes.filter(pass => pass.kind === "render");
+ expect(render.map(pass => pass.instanceCount)).toEqual([1, 1, 3, 1]);
+ expect(render[0].clear).toBeArray();
+ expect(render.slice(1).every(pass => pass.clear === false)).toBe(true);
+ expect(graph.passes.at(-1)).toMatchObject({ id: "screen", fragment: "fxaa", output: "screen" });
+ expect(graph.targets.filter(target => target.samples === 4).map(target => target.id)).toEqual(["hdr", "depth"]);
+ expect(render.every(pass => pass.vertexCount > 0)).toBe(true);
+ expect(graph.uniformsSize).toBe(sampleWorldUniforms(scene, clock(0)).length);
+ expect(orbInstances({ count: 3, seed: 7 })).toEqual(orbInstances({ count: 3, seed: 7 }));
+ expect(orbInstances({ count: 3, seed: 7 })).not.toEqual(orbInstances({ count: 3, seed: 8 }));
+ expect(() => prepareWorldScene({ instances: [...orbInstances({ count: 1 }), ...orbInstances({ count: 1 })] })).toThrow("id is duplicated");
+ expect(() => prepareWorldScene({ instances: [...orbInstances({ id: "near", count: 1 }), ...orbInstances({ id: "far", count: 1 })] })).not.toThrow();
+});
+
+test("pulse geometry, camera follow and focus share the exact curved path across clock transitions", () => {
+ const scene = prepareWorldScene({ instances: [...graphInstances({ data: tree("root", [["a", ["leaf"]]]) }),
+ ...pulseInstances({ id: "pulse", path: ["root", "a", "leaf"], step: 1, duration: 4 })],
+ camera: { shots: [{ step: 0, position: [0, 4, 20] }, { step: 1, follow: "pulse", dolly: 6, duration: 0 }] }, dof: { focus: "pulse" } });
+ const pulse = scene.instances.find(instance => instance.id === "pulse")!;
+ let state = createPlaybackState({ phase: "playing", slideKey: "pulse", stepKey: "1", stepIndex: 1 });
+ state = advancePlayback(advancePlayback(state, 0), 1000);
+ const position = instancePosition(pulse, state), camera = sampleWorldCamera(scene, state), uniforms = sampleWorldUniforms(scene, state);
+ expect(camera.target).toEqual(position);
+ expect(uniforms[32]).toBeCloseTo(6);
+ const source = resolveAnchor(scene, "root", state)!, target = resolveAnchor(scene, "a", state)!;
+ expect(position[0]).toBeCloseTo((source[0] + target[0]) / 2);
+ expect(position[1]).toBeGreaterThan((source[1] + target[1]) / 2);
+ const trail = scene.instances.find(instance => instance.id === "pulse:trail")!;
+ const index = worldHeaderSize + scene.instances.indexOf(trail) * worldInstanceSize;
+ expect(uniforms.slice(index + 12, index + 15)).toEqual(position);
+ state = transitionPlayback(state, { ...state, phase: "paused" });
+ state = advancePlayback(state, 10000);
+ expect(instancePosition(pulse, state)).toEqual(position);
+ const jump = createPlaybackState({ phase: "playing", slideKey: "jump", stepKey: "3", stepIndex: 3 });
+ expect(instanceOpacity(pulse, jump)).toBe(0);
+ expect(instancePosition(pulse, jump)).toEqual(resolveAnchor(scene, "leaf", jump)!);
+ expect(() => prepareWorldScene({ instances: pulseInstances({ id: "bad", path: ["bad", [0, 0, 0]] }) })).toThrow("cycle");
+});
+
+test("pulse keeps a spherical head distinct from its thin, fainter curved trail", () => {
+ const scene = prepareWorldScene({ instances: pulseInstances({ id: "pulse", path: [[0, 3, 0], [4, 0, 0]], size: 0.2, emissive: 12 }) });
+ const head = scene.instances.find(instance => instance.id === "pulse")!;
+ const trail = scene.instances.find(instance => instance.id === "pulse:trail")!;
+ const values = sampleWorldUniforms(scene, clock(1));
+ const at = (instance: typeof head) => worldHeaderSize + scene.instances.indexOf(instance) * worldInstanceSize;
+ expect(head.kind).toBe("sphere"); expect(trail.kind).toBe("tube");
+ expect(values.slice(at(head) + 8, at(head) + 11)).toEqual([1, 1, 1]);
+ expect(values[at(head) + 3]).toBe(0.2);
+ expect(values[at(trail) + 19]).toBeCloseTo(0.036);
+ expect(values[at(trail) + 7]).toBeCloseTo(3.6);
+ expect(values[at(head) + 7]).toBe(12);
+ const sphere = buildWorldGraph(scene).passes.find(pass => pass.id === "scene-sphere");
+ expect(sphere).toMatchObject({ kind: "render", vertexCount: 6, instanceCount: 1 });
+});
+
+test("pulse trails reconstruct exact curved subsegments and restart at a turn without extra instances", () => {
+ const scene = prepareWorldScene({ instances: [...graphInstances({ data: tree("root", [["a", ["leaf"]]]), edge: { curve: 0.65 } }),
+ ...pulseInstances({ id: "pulse", path: ["root", "a", "leaf"], step: 1, duration: 4, trail: 0.18 })] });
+ const trail = scene.instances.find(instance => instance.id === "pulse:trail")!;
+ const index = worldHeaderSize + scene.instances.indexOf(trail) * worldInstanceSize;
+ const point = (from: Vec3, to: Vec3, curve: number, t: number): Vec3 => {
+ const length = Math.hypot(to[0] - from[0], to[1] - from[1], to[2] - from[2]);
+ return [from[0] + (to[0] - from[0]) * t, from[1] + (to[1] - from[1]) * t + 2 * (1 - t) * t * length * curve, from[2] + (to[2] - from[2]) * t];
+ };
+ const check = (time: number, sourceId: string, targetId: string, tailT: number, headT: number) => {
+ const sample = clock(time, 1, { 1: 0 }), values = sampleWorldUniforms(scene, sample);
+ const source = resolveAnchor(scene, sourceId, sample)!, target = resolveAnchor(scene, targetId, sample)!;
+ const start = values.slice(index, index + 3) as Vec3, end = values.slice(index + 12, index + 15) as Vec3;
+ for (const t of [0, 0.25, 0.5, 0.75, 1]) {
+ const actual = point(start, end, values[index + 15], t), expected = point(source, target, 0.65, tailT + (headT - tailT) * t);
+ actual.forEach((value, axis) => expect(value).toBeCloseTo(expected[axis], 10));
+ }
+ return values;
+ };
+ check(1, "root", "a", 0.14, 0.5);
+ const before = check(1.99, "root", "a", 0.635, 0.995);
+ const turn = sampleWorldUniforms(scene, clock(2, 1, { 1: 0 }));
+ expect(turn[index + 23]).toBe(0); expect(turn[index + 15]).toBe(0);
+ const after = check(2.01, "a", "leaf", 0, 0.005);
+ expect(after.slice(index, index + 3)).toEqual(resolveAnchor(scene, "a", clock(2.01, 1, { 1: 0 }))!);
+ expect(before[index + 23]).toBe(1); expect(after[index + 23]).toBe(1);
+ expect(scene.instances.filter(instance => instance.pulse)).toHaveLength(2);
+ expect(sampleWorldUniforms(scene, clock(1), undefined, 1, 0.75)[35]).toBe(0.75);
+ expect(sampleWorldUniforms(scene, clock(1))[35]).toBe(1);
+});
+
+test("changed rows stay highlighted for the whole step while the others dim", () => {
+ const scene = prepareWorldScene({ instances: rowInstances({ id: "rows", count: 4, speed: 0, color: "#808080", flashColor: "#ff0000", changes: [{ row: 1, step: 1 }, { row: 2, step: 2 }], flashDuration: 0.5 }),
+ camera: { shots: [{ position: [0, 0, 10], target: [0, 0, 0] }] } });
+ const row = (slot: number, sample: ClockSample) => {
+ const values = sampleWorldUniforms(scene, sample), index = worldHeaderSize + slot * worldInstanceSize;
+ return { color: [...values.slice(index + 4, index + 7)], emissive: values[index + 7] };
+ };
+ const grey = row(0, clock(5)).color;
+ expect(row(1, clock(5)).emissive).toBe(0.1);
+ const entry = row(1, clock(1.1, 1, { 1: 1 })), settled = row(1, clock(9, 1, { 1: 1 }));
+ expect(entry.emissive).toBeGreaterThan(settled.emissive);
+ expect(entry.emissive).toBeLessThan(4);
+ expect(settled.color).toEqual([1, 0, 0]);
+ expect(settled.emissive).toBeCloseTo(0.6);
+ expect(row(0, clock(9, 1, { 1: 1 })).color[0]).toBeCloseTo(grey[0] * 0.2);
+ expect(row(2, clock(9, 1, { 1: 1 })).emissive).toBe(0.1);
+ const later = clock(20, 2, { 1: 1, 2: 10 });
+ expect(row(1, later).color).toEqual([1, 0, 0]);
+ expect(row(2, later).color).toEqual([1, 0, 0]);
+ expect(row(3, later).color[0]).toBeCloseTo(grey[0] * 0.2);
+});
+
+test("recycling transfers a flash with logical row identity and never to a reused slot", () => {
+ const scene = prepareWorldScene({ instances: rowInstances({ id: "rows", count: 4, speed: 1, spacing: 1, changes: [{ row: 1, step: 1 }], flashDuration: 2 }),
+ dof: { focus: "rows:row:1" }, camera: { shots: [{ position: [0, 0, 10], target: [0, 0, 0] }] } });
+ const before = clock(0.9, 1, { 1: 0.8 }), after = clock(1.1, 1, { 1: 0.8 });
+ const first = scene.instances[0], second = scene.instances[1];
+ expect(rowLogicalIndex(first, before)).toBe(0); expect(rowLogicalIndex(second, before)).toBe(1);
+ expect(rowLogicalIndex(first, after)).toBe(1); expect(rowLogicalIndex(second, after)).toBe(2);
+ const brightness = (slot: number, sample: ClockSample) => sampleWorldUniforms(scene, sample)[worldHeaderSize + slot * worldInstanceSize + 7];
+ expect(brightness(1, before)).toBeGreaterThan(1);
+ expect(brightness(0, before)).toBe(0.1);
+ expect(brightness(0, after)).toBeGreaterThan(1);
+ expect(brightness(1, after)).toBe(0.1);
+ expect(resolveAnchor(scene, "rows:row:1", after)).toEqual(instancePosition(first, after));
+ expect(sampleWorldUniforms(scene, after)[32]).toBe(10);
+ expect(brightness(0, clock(0, 3, { 3: 0 }))).toBe(0.1);
+ let state = createPlaybackState({ phase: "playing", slideKey: "rows", stepKey: "1", stepIndex: 1 });
+ state = advancePlayback(advancePlayback(state, 0), 990);
+ const departing = instanceOpacity(first, state), retained = instanceOpacity(second, state);
+ const retainedPosition = instancePosition(second, state);
+ state = advancePlayback(state, 1010);
+ expect(departing).toBeCloseTo(0.01);
+ expect(retained).toBe(1);
+ expect(instanceOpacity(first, state)).toBeCloseTo(0.99);
+ expect(instanceOpacity(scene.instances.at(-1)!, state)).toBeCloseTo(0.01);
+ expect(instancePosition(first, state)[1] - retainedPosition[1]).toBeCloseTo(0.02);
+ expect(brightness(0, state)).toBeGreaterThan(1);
+});
+
+test("voxel compaction keeps topology fixed and its counter matches the surviving cells", () => {
+ const scene = prepareWorldScene({ instances: voxelInstances({ id: "cells", grid: [8, 8, 8], collapse: { step: 1, to: 0.2, duration: 2 } }), counter: { source: "cells" } });
+ const graph = buildWorldGraph(scene);
+ const initial = sampleWorldUniforms(scene, clock(0)), middle = sampleWorldUniforms(scene, clock(1, 1, { 1: 0 })), final = sampleWorldUniforms(scene, clock(2, 1, { 1: 0 }));
+ expect(initial[56]).toBe(512); expect(middle[56]).toBe(307); expect(final[56]).toBe(102);
+ expect(scene.instances.reduce((sum, instance) => sum + instanceOpacity(instance, clock(2, 1, { 1: 0 })), 0)).toBe(102);
+ expect(graph.passes.filter(pass => pass.kind === "render")[0].instanceCount).toBe(512);
+ expect(final.length).toBe(graph.uniformsSize);
+ expect(sampleWorldUniforms(scene, clock(0, 3, { 3: 0 }))[56]).toBe(102);
+ expect(sampleWorldUniforms(scene, clock(0))[56]).toBe(512);
+});
+
+test("reused uniforms keep one buffer and match fresh sampling while static and collapsing slots are skipped", () => {
+ const scene = prepareWorldScene({ instances: [
+ ...voxelInstances({ id: "cells", grid: [4, 4, 4], collapse: { step: 1, to: 0.2, duration: 2 } }), primitive("box", { id: "still", position: [2, 0, 0] }),
+ ...orbInstances({ count: 3, drift: 0.4 }), ...graphInstances({ data: tree("root", ["a"]), activate: [wave({ from: "root", step: 1 })] }),
+ ...pulseInstances({ id: "pulse", path: ["root", "a"], step: 1 }),
+ ], counter: { source: "cells" } });
+ expect(scene.instances.filter(instance => instance.motion === "voxel")).toHaveLength(64);
+ expect(scene.instances.filter(instance => instance.anchor).map(instance => instance.id).sort()).toEqual(["node:a", "node:root", "pulse"]);
+ const reused = sampleWorldUniforms(scene, clock(0), undefined, 1, 1, true);
+ for (const sample of [clock(0.5), clock(0.5, 1, { 0: 0, 1: 0.5 }), clock(1.5, 1, { 0: 0, 1: 0.5 }), clock(4, 1, { 0: 0, 1: 0.5 }), clock(5, 1, { 0: 0, 1: 0.5 }), clock(0.2)]) {
+ expect(sampleWorldUniforms(scene, sample, undefined, 1, 1, true)).toBe(reused);
+ expect(reused).toEqual(Float32Array.from(sampleWorldUniforms(scene, sample)));
+ }
+});
+
+test("removed voxels shrink in place, including collapse to zero and backwards sampling", () => {
+ const scene = prepareWorldScene({ instances: voxelInstances({ id: "cells", grid: [5, 1, 1], collapse: { step: 1, to: 0.2, duration: 2, ease: "linear" } }), counter: { source: "cells" } });
+ let state = createPlaybackState({ phase: "playing", slideKey: "shrink", stepKey: "1", stepIndex: 1 });
+ const removed = scene.instances[3], position = instancePosition(removed, state);
+ state = advancePlayback(advancePlayback(state, 0), 750);
+ expect(instancePosition(removed, state)).toEqual(position);
+ expect(instanceOpacity(removed, state)).toBeCloseTo(0.5);
+ expect(sampleWorldUniforms(scene, state)[worldHeaderSize + 3 * worldInstanceSize + 3]).toBeCloseTo(0.065);
+ state = advancePlayback(state, 2000);
+ expect(instancePosition(removed, state)).toEqual(position);
+ expect(instanceOpacity(removed, state)).toBe(0);
+ expect(instanceOpacity(removed, clock(0))).toBe(1);
+ const empty = prepareWorldScene({ instances: voxelInstances({ id: "empty", grid: [2, 2, 2], collapse: { step: 1, to: 0, duration: 1 } }), counter: { source: "empty" } });
+ const initial = empty.instances.map(instance => instancePosition(instance, clock(0)));
+ const finished = clock(1, 1, { 1: 0 });
+ expect(sampleWorldUniforms(empty, finished)[56]).toBe(0);
+ expect(empty.instances.every(instance => instanceOpacity(instance, finished) === 0)).toBe(true);
+ expect(empty.instances.map(instance => instancePosition(instance, finished))).toEqual(initial);
+});
+
+test("stages, material references, light direction and HDR post settings share animated uniforms", () => {
+ const stage = { id: "b", position: compileTrack<[number, number, number]>({ 0: [10, 0, 0], 1: [12, 0, 0] }), glow: compileTrack({ 0: 1, 1: 0.5 }) };
+ const scene = prepareWorldScene({ instances: [{ ...primitive("tube", { id: "tube", position: [0, 0, 0], end: [2, 0, 0], material: "stone", emissive: 4 }), stages: [stage] }],
+ stages: [{ stage, parents: [] }], materials: { stone: { albedo: "#ffffff", roughness: 0.7, metal: 0.2, rim: 0.9 } },
+ camera: { shots: [flyTo("b", { step: 0, offset: [0, 0, 10] })] },
+ dof: { focus: "stage:b", aperture: 0.8 }, floor: { mirror: true, y: -3 }, shafts: { intensity: 0.7 }, chromatic: 0.003,
+ lights: [{ kind: "spot", position: [0, 5, 0], target: [0, 0, 0], angle: 0.4 }], rim: { color: "#22d3ee", intensity: 2 } });
+ const sample = clock(2, 1, { 0: 0, 1: 1 }), values = sampleWorldUniforms(scene, sample);
+ expect(resolveAnchor(scene, "stage:b", sample)).toEqual([12, 0, 0]);
+ expect(sampleWorldCamera(scene, sample).position).toEqual([12, 0, 10]);
+ expect(values.slice(worldHeaderSize + 12, worldHeaderSize + 15)).toEqual([14, 0, 0]);
+ expect(values[worldHeaderSize + 7]).toBe(2);
+ expect(values.slice(worldHeaderSize + 24, worldHeaderSize + 27)).toEqual([1, 1, 1]);
+ expect(values[worldHeaderSize + 27]).toBe(0.7); expect(values[worldHeaderSize + 28]).toBe(0.2);
+ expect(values[32]).toBe(10); expect(values[33]).toBe(0.8); expect(values[36]).toBe(-3); expect(values[44]).toBe(0.003);
+ expect(values[51]).toBe(2);
+ expect(sampleWorldUniforms(prepareWorldScene({ instances: [] }), sample)[51]).toBe(0);
+ expect(values.slice(72, 75)).toEqual([0, -1, 0]); expect(values[75]).toBeCloseTo(Math.cos(0.4));
+ const named = prepareWorldScene({ instances: [primitive("sphere", { id: "b", position: [1, 2, 3] })], stages: scene.stages });
+ expect(resolveAnchor(named, "b", sample)).toEqual([1, 2, 3]);
+ expect(resolveAnchor(named, "stage:b", sample)).toEqual([12, 0, 0]);
+});
+
+test("spray sampling is stateless and gates against lit leaves", () => {
+ const scene = prepareWorldScene({ instances: [...graphInstances({ data: tree("root", ["leaf"]), activate: [wave({ from: "root", step: 1 })] }),
+ ...sprayInstances({ id: "spray", under: "leaves:lit", rate: 4, lifetime: 2 })] });
+ const particles = scene.instances.filter(instance => instance.spray);
+ expect(particles).toHaveLength(8);
+ expect(particles.every(instance => instanceOpacity(instance, clock(0)) === 0)).toBe(true);
+ expect(particles.some(instance => instanceOpacity(instance, clock(2, 1, { 1: 0 })) > 0)).toBe(true);
+ const positions = particles.map(instance => instancePosition(instance, clock(2, 1, { 1: 0 })));
+ particles.forEach(instance => instancePosition(instance, clock(10, 3, { 3: 0 })));
+ expect(particles.map(instance => instancePosition(instance, clock(2, 1, { 1: 0 })))).toEqual(positions);
+});
+
+test("each spray particle carries only the leaf it samples", () => {
+ const scene = prepareWorldScene({ instances: [...graphInstances({ data: tree("root", [["a", ["a1", "a2", "a3"]], ["b", ["b1", "b2", "b3"]]]) }),
+ ...sprayInstances({ id: "spray", under: "leaves:lit", rate: 10, lifetime: 2 })] });
+ const leaves = scene.instances.filter(instance => instance.kind === "sphere" && /^node:[ab]\d$/.test(instance.id ?? ""));
+ const carried = scene.instances.filter(instance => instance.spray).map(particle => {
+ const found = new Set();
+ const walk = (value: unknown) => { if (!value || typeof value !== "object") return; if (leaves.includes(value as never)) { found.add(value); return; } Object.values(value).forEach(walk); };
+ walk(particle.spray);
+ expect(found.size).toBe(1);
+ return [...found][0];
+ });
+ expect(new Set(carried).size).toBe(leaves.length);
+});
+
+test("explicit materials and rim lights preserve dim graph nodes until activation", () => {
+ const scene = prepareWorldScene({ instances: graphInstances({ data: tree("root"), nodes: { root: { material: "bright" } }, activate: [wave({ from: "root", step: 1 })] }),
+ materials: { bright: { albedo: "#ffffff", rim: 2 } }, rim: { intensity: 3 } });
+ const dim = scene.instances[0].graph!.dim;
+ let state = createPlaybackState({ phase: "playing", slideKey: "material", stepKey: "0", stepIndex: 0 });
+ const dark = sampleWorldUniforms(scene, state);
+ expect(dark.slice(worldHeaderSize + 24, worldHeaderSize + 27)).toEqual(dim);
+ expect(dark[worldHeaderSize + 7]).toBe(0);
+ expect(dark[worldHeaderSize + 29]).toBe(0); expect(dark[worldHeaderSize + 30]).toBe(0);
+ state = transitionPlayback(state, { ...state, stepKey: "1", stepIndex: 1 });
+ state = advancePlayback(advancePlayback(state, 0), 1000);
+ const lit = sampleWorldUniforms(scene, state);
+ expect(lit.slice(worldHeaderSize + 24, worldHeaderSize + 27)).toEqual([1, 1, 1]);
+ expect(lit[worldHeaderSize + 29]).toBe(2); expect(lit[worldHeaderSize + 30]).toBe(1);
+ expect(sampleWorldUniforms(scene, clock(0)).slice(worldHeaderSize + 24, worldHeaderSize + 27)).toEqual(dim);
+});
+
+test("custom and bounded geometry keep their own draw groups and validate resources before setup", () => {
+ const a = customInstances({ wgsl: "shape-a", count: 2, vertexCount: 6 }), b = customInstances({ wgsl: "shape-a", count: 3, vertexCount: 6 });
+ expect(a[0].custom).toBe(a[1].custom); expect(a[0].custom).not.toBe(b[0].custom);
+ const scene = prepareWorldScene({ instances: [...a, primitive("sphere"), ...b, sdfInstance({ wgsl: "distance", bounds: [[-1, -1, -1], [1, 1, 1]] })] });
+ expect(scene.instances[0].kind).toBe("sphere");
+ expect(scene.instances.at(-1)?.sdf).toBeDefined();
+ expect(() => prepareWorldScene({ instances: [primitive("box", { material: "missing" })] })).toThrow("material");
+ expect(() => prepareWorldScene({ instances: [], dof: { scale: 0 } })).toThrow("scale");
+ expect(() => prepareWorldScene({ instances: [], lights: Array.from({ length: 5 }, () => ({})) })).toThrow("four");
+});
diff --git a/packages/slides-effects/tests/worldBackgroundPlayback.test.tsx b/packages/slides-effects/tests/worldBackgroundPlayback.test.tsx
new file mode 100644
index 000000000..fe2d8553c
--- /dev/null
+++ b/packages/slides-effects/tests/worldBackgroundPlayback.test.tsx
@@ -0,0 +1,87 @@
+// @ts-nocheck Exercise actual background registration and presentation clocks without a GPU.
+import "../../../apps/slides/src/__tests__/nativeMock";
+import { frames, flushUI } from "../../../apps/slides/src/__tests__/uiClockMock";
+import { expect, test } from "bun:test";
+import { observable } from "@legendapp/state";
+import { Background, BackgroundHost, GPUShaderProvider, PresentationObservableProvider, usePlayback } from "@legend-apps/presentation";
+import React from "react";
+import { act, create } from "react-test-renderer";
+import { Camera, Sphere, World } from "../packs/world/World";
+import { worldHeaderSize } from "../packs/world/scene";
+
+test("selected background worlds restart slide time while preserving camera continuity and step sampling", async () => {
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
+ const runtime = slideIndex => observable({ currentSlide: 0, currentStep: 0, slideIndex, stepIndex: 0,
+ slideCount: 2, stepCount: 2, startedAt: 0, stepStartedAt: 0, isActive: slideIndex === 0,
+ isPreview: false, isPreparing: slideIndex !== 0, playbackPhase: slideIndex === 0 ? "playing" : "preparing",
+ playbackScope: "slide", goTo() {}, next() {}, previous() {} });
+ const first$ = runtime(0), second$ = runtime(1);
+ function Renderer(props) { return ; }
+ function BackgroundClock() { return ; }
+ const declarations = <>
+
+
+
+
+
+
+
+ >;
+ const content = (slideIndex, isPreview = false) =>
+ {declarations}
+ ;
+ let renderer;
+ const graph = () => renderer.root.findByType("world-gpu").props;
+ const values = () => graph().frameData.value;
+ const tick = timestamp => frames.forEach(frame => frame.tick(timestamp));
+ try {
+ await act(() => { renderer = create(content(0)); });
+ flushUI(); tick(0); tick(10000);
+ const previous = values().slice(0, 3);
+ expect(graph().clockState.value.slideTime).toBe(10);
+ expect(previous[0]).toBeCloseTo(Math.sin(0.45) * 20);
+
+ await act(() => {
+ first$.assign({ currentSlide: 1, playbackPhase: "outgoing", isActive: false });
+ second$.assign({ currentSlide: 1, currentStep: 1, stepIndex: 1, startedAt: 10000, stepStartedAt: 10000,
+ isActive: true, isPreparing: false, playbackPhase: "playing" });
+ renderer.update(content(1));
+ });
+ flushUI(); tick(10000);
+ expect(graph().clockState.value.slideTime).toBe(0);
+ expect(graph().clockState.value.slideKey).toBe("1:10000");
+ const backgroundClock = renderer.root.findByType("background-clock").props.value.value;
+ expect(backgroundClock.slideTime).toBe(10);
+ expect(backgroundClock.slideKey).toBe("background");
+ values().slice(0, 3).forEach((value, index) => expect(value).toBeCloseTo(previous[index]));
+ expect(values()[worldHeaderSize]).toBe(0);
+ tick(11000);
+ expect(graph().clockState.value.slideTime).toBe(1);
+ expect(values()[worldHeaderSize]).toBeCloseTo(2);
+ tick(11500);
+ expect(values().slice(0, 3)).toEqual([8, 5, 20]);
+ tick(13350);
+ expect(values()[2]).toBeGreaterThan(20);
+ expect(values()[2]).toBeLessThan(34);
+ tick(17800);
+ expect(values().slice(0, 3)).toEqual([0, 7, 34]);
+
+ await act(() => second$.assign({ currentStep: 0, stepIndex: 0, stepStartedAt: 17800 }));
+ flushUI(); tick(17800);
+ expect(values()[worldHeaderSize]).toBe(0);
+ expect(graph().clockState.value.slideTime).toBeCloseTo(7.8);
+ await act(() => renderer.unmount());
+ await act(() => {
+ second$.assign({ currentStep: 1, stepIndex: 1, stepStartedAt: 18000 });
+ renderer = create(content(1, true));
+ });
+ flushUI();
+ const preview = values();
+ expect(graph().clockState.value.phase).toBe("preview");
+ expect(preview[worldHeaderSize]).toBe(4);
+ expect(preview[2]).toBeGreaterThan(20);
+ expect(preview[2]).toBeLessThan(34);
+ tick(40000);
+ expect(values()).toEqual(preview);
+ } finally { await act(() => renderer?.unmount()); flushUI(); }
+});
diff --git a/packages/slides-effects/tests/worldDataFlow.test.tsx b/packages/slides-effects/tests/worldDataFlow.test.tsx
new file mode 100644
index 000000000..c4751b1f5
--- /dev/null
+++ b/packages/slides-effects/tests/worldDataFlow.test.tsx
@@ -0,0 +1,131 @@
+// @ts-nocheck Inspect the real compiled deck and clock uniforms without a GPU.
+import "../../../apps/slides/src/__tests__/nativeMock";
+import { afterAll, beforeAll, expect, test } from "bun:test";
+import { readFileSync } from "node:fs";
+import { createRequire } from "node:module";
+import path from "node:path";
+import React from "react";
+import { act, create } from "react-test-renderer";
+import { compileDeck } from "../../presentation/src/compiler";
+import { layoutTree } from "../packs/world/graph";
+import { colorVector, worldHeaderSize, worldInstanceSize } from "../packs/world/scene";
+
+const playback = { value: { phase: "playing", slideTime: 0, stepTime: 0, stepIndex: 0, stepOrigins: { 0: 0 } } };
+let renderer, network;
+// World uploads float32 uniforms, so exact expectations round the same way.
+const f32 = Math.fround, f32Vector = vector => vector.map(f32);
+const closeVector = (actual, expected) => expected.forEach((value, axis) => expect(actual[axis]).toBeCloseTo(value, 5));
+const values = index => Array.from(renderer.root.findAllByType("world-gpu")[index].props.uniforms.value);
+const instances = index => {
+ const data = values(index), result = [];
+ for (let offset = worldHeaderSize; offset < data.length; offset += worldInstanceSize) result.push(data.slice(offset, offset + worldInstanceSize));
+ return result;
+};
+function sample(time, step = 0) {
+ playback.value = { ...playback.value, slideTime: time, stepTime: time, stepIndex: step, stepOrigins: { 0: 0, [step]: 0 } };
+}
+
+beforeAll(async () => {
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
+ const root = path.resolve(import.meta.dirname, "../../..");
+ const deckPath = path.join(root, "apps/slides/examples/3d/data-flow.mdx");
+ const compiled = await compileDeck(deckPath, {
+ source: readFileSync(deckPath, "utf8"), effectPacksDirectory: path.join(root, "packages/slides-effects/packs"),
+ });
+ if (!compiled.success) throw new Error(compiled.errors.join("\n"));
+ const require = createRequire(import.meta.url), module = { exports: {} };
+ Function("require", "module", "exports", compiled.code)(name => {
+ if (name === "@legend-apps/presentation") return {
+ defineTransition: definition => definition,
+ Background: "background", GPURenderGraph: props => React.createElement("world-gpu", props),
+ usePresentationValue: key => key === "playbackScope" ? "slide" : "playing",
+ useBackgroundSize: () => ({ width: 1920, height: 1080 }),
+ usePlayback: () => playback, samplePlayback: (state, _preview, clock) => clock === "step" ? state.stepTime : state.slideTime,
+ };
+ if (name === "react-native-reanimated") return {
+ useSharedValue: value => React.useRef({ value }).current,
+ useDerivedValue: read => ({ get value() { return read(); } }), useAnimatedReaction() {},
+ };
+ return require(name);
+ }, module, module.exports);
+ network = module.exports.network;
+ await act(() => {
+ renderer = create(React.createElement(module.exports.default, { components: {
+ Deck: "deck", Slide: "slide", View: "view",
+ Steps: ({ children }) => children(playback.value.stepIndex),
+ } }));
+ });
+}, 30000);
+afterAll(async () => { await act(() => renderer?.unmount()); });
+
+test("all five scenes fill the background and retain short overlay titles and navigation states", () => {
+ const slides = renderer.root.findAllByType("slide");
+ expect(slides).toHaveLength(5);
+ for (const slide of slides) {
+ const backgrounds = slide.findAllByType("background");
+ expect(backgrounds).toHaveLength(1);
+ const graph = backgrounds[0].findByType("world-gpu").props;
+ expect(graph.background).toBe(true);
+ expect([graph.width, graph.height]).toEqual([1920, 1080]);
+ expect(slide.findAllByType("text").some(text => text.props.className?.includes("text-6xl"))).toBe(true);
+ }
+ const navigation = renderer.root.findAll(node => typeof node.type === "function" && node.type.name === "Steps");
+ expect(navigation.map(node => node.props.count)).toEqual([3, 2, 3, 3, 2]);
+});
+
+test("the 53-node cascade heats descendants then prunes to one branch while keeping dim geometry", () => {
+ expect(layoutTree(network).nodes).toHaveLength(53);
+ sample(0);
+ const nodes = () => instances(0).filter(item => item[3] === f32(0.24));
+ const edges = instances(0).filter(item => item[15] === f32(0.45));
+ expect(nodes()).toHaveLength(53); expect(edges).toHaveLength(52);
+ for (const item of [...nodes(), ...edges]) {
+ expect(item[23]).toBe(1);
+ expect(item.slice(4, 7)).toEqual(f32Vector(colorVector("#647b9a")));
+ }
+ expect(values(0)[1]).toBeCloseTo(1.8);
+ expect(values(0)[9]).toBe(f32(1.8)); expect(values(0)[12]).toBe(f32(0.035));
+ sample(0.5, 1);
+ const partial = nodes().filter(item => item[30] > 0);
+ expect(partial.length).toBeGreaterThan(1); expect(partial.length).toBeLessThan(53);
+ sample(2, 1);
+ expect(nodes().every(item => item[30] === 1)).toBe(true);
+ closeVector(nodes()[0].slice(4, 7), colorVector("#ff36c8"));
+ sample(2, 2);
+ expect(nodes().filter(item => item[30] === 1)).toHaveLength(13);
+ const target = layoutTree(network, { radius: 6.8, levelHeight: 1.7, position: [0, 3.2, 0] }).nodes.find(node => node.id === "e2");
+ expect(target, "node e2").toBeDefined();
+ closeVector(values(0).slice(4, 7), target.position);
+ expect(values(0)[32]).toBeCloseTo(10);
+});
+
+test("the cyan orb follows one curved edge with a fainter trail and matching camera focus", () => {
+ sample(2, 1);
+ const head = instances(1).find(item => item[7] === 14);
+ const trail = instances(1).find(item => item[7] === f32(14 * 0.3));
+ expect(head, "orb head").toBeDefined();
+ expect(trail, "orb trail").toBeDefined();
+ expect(head.slice(4, 7)).toEqual(f32Vector(colorVector("#22d3ee")));
+ expect(head.slice(8, 11)).toEqual([1, 1, 1]);
+ expect(trail[19]).toBeCloseTo(head[3] * 0.18);
+ expect(trail.slice(12, 15)).toEqual(head.slice(0, 3));
+ expect(trail[15]).toBeGreaterThan(0);
+ expect(values(1).slice(4, 7)).toEqual(head.slice(0, 3));
+ expect(values(1)[32]).toBeCloseTo(7);
+ expect(instances(1).filter(item => item[7] > 0 && item[3] === f32(0.2))).toHaveLength(2);
+});
+
+test("rows, stage flights and voxel reduction retain their clock-driven behavior", () => {
+ sample(1, 1);
+ expect(instances(2).filter(item => item[3] === 0.5 && item[7] > 1)).toHaveLength(1);
+ expect(renderer.root.findAllByType("world-gpu")[2].props.passes.find(pass => pass.id === "scene-box").instanceCount).toBe(15);
+ sample(3, 2);
+ expect(values(3).slice(4, 7)).toEqual([10, 0, 0]);
+ expect(values(3).slice(0, 3)).toEqual([12, 2.5, 10]);
+ expect(values(3)[32]).toBeCloseTo(Math.hypot(2, 2.5, 10));
+ sample(0);
+ expect(values(4)[56]).toBe(1000);
+ sample(2.4, 1);
+ expect(values(4)[56]).toBe(200);
+ expect(instances(4).filter(item => item[3] > 0 && item[3] <= f32(0.17))).toHaveLength(200);
+});
diff --git a/packages/slides-effects/tests/worldLook.test.ts b/packages/slides-effects/tests/worldLook.test.ts
new file mode 100644
index 000000000..64b4bd498
--- /dev/null
+++ b/packages/slides-effects/tests/worldLook.test.ts
@@ -0,0 +1,39 @@
+import { expect, test } from "bun:test";
+import { buildWorldGraph } from "../packs/world/render";
+import { orbInstances, prepareWorldScene, primitive, sprayInstances } from "../packs/world/scene";
+
+for (const quality of ["low", "medium", "high"] as const) {
+ test(`round particles use one instanced quad with surface depth at ${quality} quality`, () => {
+ const scene = prepareWorldScene({ quality, floor: { mirror: true }, instances: [
+ primitive("box"), ...orbInstances({ count: 1000 }), ...sprayInstances({ rate: 1000, lifetime: 2 }),
+ ] });
+ const graph = buildWorldGraph(scene);
+ for (const id of ["scene-sphere", "reflection-sphere"]) {
+ const draw = graph.passes.find(pass => pass.id === id)!;
+ expect(draw.kind).toBe("render");
+ if (draw.kind !== "render") throw new Error("Expected sphere render pass");
+ expect(draw.vertexCount).toBe(6);
+ const vertex = draw.code.slice(draw.code.indexOf("@vertex"), draw.code.indexOf("struct SphereResult"));
+ expect(vertex).toContain("result.opacity<=.001 || center.w+extent<=.05 || center.w-extent>=500.");
+ expect(draw.instanceCount).toBe(3000);
+ expect(draw.code).toContain("@builtin(frag_depth)");
+ expect(draw.code).toContain("if(field<=0.");
+ expect(draw.code).toContain("fwidth(field)");
+ expect(draw.code).toContain("normal=normalize(local/radii)");
+ expect(draw.code).toContain("(iid+1u)");
+ }
+ const reflection = graph.targets.find(target => target.id === "reflect")!;
+ const hdr = graph.targets.find(target => target.id === "hdr")!;
+ expect(reflection).toMatchObject({ scale: hdr.scale, samples: hdr.samples });
+ expect(graph.passes.filter(pass => pass.kind === "render")).toHaveLength(5);
+ });
+}
+
+test("bloom uses round kernels with a soft threshold knee", () => {
+ const graph = buildWorldGraph(prepareWorldScene({ instances: [], bloom: {} }));
+ const code = (id: string) => graph.passes.find(pass => pass.id === id)!.code;
+ expect(code("bloom-threshold")).toContain("downsample13(uv,true)");
+ expect(code("bloom-threshold")).toContain("soft*soft");
+ expect(code("bloom-down-1")).toContain("downsample13(uv,false)");
+ expect(code("bloom-up-0")).toContain("tent9(uv)");
+});
diff --git a/packages/slides-effects/tests/worldPlayback.test.tsx b/packages/slides-effects/tests/worldPlayback.test.tsx
new file mode 100644
index 000000000..4dc6c5eb1
--- /dev/null
+++ b/packages/slides-effects/tests/worldPlayback.test.tsx
@@ -0,0 +1,327 @@
+// @ts-nocheck Exercise the pack's UI-clock boundary without a native GPU.
+import "../../../apps/slides/src/__tests__/nativeMock";
+import { reactUI } from "../../../apps/slides/src/__tests__/uiClockMock";
+import { expect, mock, test } from "bun:test";
+import React, { Profiler } from "react";
+import { act, create } from "react-test-renderer";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+import { createRequire } from "node:module";
+import { createPlaybackState, advancePlayback, transitionPlayback } from "../../presentation/src/playbackState";
+import { samplePlayback } from "../../presentation/src/playback";
+import { PresentationObservableProvider, usePresentation$ } from "../../presentation/src/runtime";
+import { useRuntimeProjection } from "../../presentation/src/useRuntimeProjection";
+import { worldHeaderSize } from "../packs/world/scene";
+
+const playback = { value: createPlaybackState({ phase: "playing", slideKey: "a", stepKey: "0", stepIndex: 0 }) };
+const animated = { ...await import("react-native-reanimated") };
+mock.module("react-native-reanimated", () => ({ ...animated, useDerivedValue(read) {
+ const latest = React.useRef(read);
+ latest.current = read;
+ return React.useRef({ _isReanimatedSharedValue: true, get value() { return latest.current(); } }).current;
+} }));
+mock.module("@legend-apps/presentation", () => ({
+ PresentationObservableProvider, usePresentation$, useRuntimeProjection,
+ GPURenderGraph: props => React.createElement("render-graph", props),
+ usePlayback: () => playback, samplePlayback, useBackgroundSize: () => ({ width: 1280, height: 720 }),
+ usePresentationValue: () => playback.value.phase,
+}));
+const { World, Camera, Orbs, Sphere } = await import("../packs/world/World");
+
+test("world uniforms sample preparation/preview and retain live continuity without React frames", async () => {
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
+ let tree, commits = 0;
+ const scene = <>>;
+ const next = ;
+ const draw = content => commits++}>{content};
+ const mount = async (content, settle = true) => { await act(() => { tree = create(draw(content)); }); if (settle) reactUI(); };
+ const unmount = async () => { await act(() => tree.unmount()); };
+ const values = () => tree.root.findByType("render-graph").props.uniforms.value;
+ try {
+ await mount({scene});
+ expect(tree.root.findByType("render-graph").props.width).toBe(1280);
+ const initialCommits = commits;
+ playback.value = advancePlayback(advancePlayback(playback.value, 0), 2000);
+ reactUI();
+ const lastLive = values().slice(0, 3);
+ expect(lastLive[0]).toBeCloseTo(Math.sin(0.4) * 10);
+ expect(commits).toBe(initialCommits);
+ await unmount();
+
+ // A thumbnail of the next world must not replace the last live camera.
+ playback.value = createPlaybackState({ phase: "preview", slideKey: "b-preview", stepKey: "0", stepIndex: 0 });
+ await mount({next});
+ expect(values().slice(0, 3)).toEqual([0, 5, 30]);
+ await unmount();
+
+ playback.value = createPlaybackState({ phase: "preparing", slideKey: "b-prepare", stepKey: "0", stepIndex: 0 });
+ await mount({next});
+ expect(values().slice(0, 3)).toEqual([0, 5, 30]);
+ await unmount();
+
+ // Explicitly inactive worlds also leave the retained live pose alone.
+ playback.value = createPlaybackState({ phase: "playing", slideKey: "b-inactive", stepKey: "0", stepIndex: 0 });
+ await mount({next});
+ await unmount();
+
+ playback.value = createPlaybackState({ phase: "playing", slideKey: "b", stepKey: "0", stepIndex: 0 });
+ await mount({next}, false);
+ expect(values().slice(0, 3)).toEqual(lastLive);
+ reactUI();
+ expect(values().slice(0, 3)).toEqual(lastLive);
+ playback.value = advancePlayback(advancePlayback(playback.value, 3000), 5000);
+ reactUI();
+ expect(values().slice(0, 3)).toEqual([0, 5, 30]);
+ } finally { await act(() => tree?.unmount()); }
+});
+
+test("equal-content parent renders and resize retain pipeline definitions while uniforms change", async () => {
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
+ playback.value = createPlaybackState({ phase: "playing", slideKey: "identity", stepKey: "0", stepIndex: 0 });
+ let tree;
+ const content = (width = 800, x = 1, quality = "high") =>
+ {"\n "}
+ {"\n"}
+ ;
+ try {
+ await act(() => { tree = create(content()); });
+ const graph = () => tree.root.findByType("render-graph").props;
+ const passes = graph().passes, targets = graph().targets;
+ expect(tree.root.findAllByType("view")).toHaveLength(1);
+ await act(() => tree.update(content()));
+ expect(graph().passes).toBe(passes);
+ expect(graph().targets).toBe(targets);
+ await act(() => tree.update(content(1000, 4)));
+ expect(graph().passes).toBe(passes);
+ expect(graph().uniforms.value[worldHeaderSize]).toBe(4);
+ expect(graph().width).toBe(1000);
+ await act(() => tree.update(content(1000, 4, "low")));
+ expect(graph().passes).not.toBe(passes);
+ } finally { await act(() => tree?.unmount()); }
+});
+
+test("topology transitions retire a uniform generation with its original capacity", async () => {
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
+ playback.value = createPlaybackState({ phase: "playing", slideKey: "generation-a", stepKey: "0", stepIndex: 0 });
+ let renderer, overlayMounts = 0, overlayUnmounts = 0;
+ function Overlay() {
+ const [identity] = React.useState(() => ++overlayMounts);
+ React.useEffect(() => () => { overlayUnmounts++; }, []);
+ return ;
+ }
+ const content = (count, width = 800, exposure = 1) =>
+
+ ;
+ const graph = () => renderer.root.findByType("render-graph").props;
+ try {
+ await act(() => { renderer = create(content(24)); });
+ const first = graph();
+ expect(first.uniforms.value).toHaveLength(first.uniformsSize);
+ await act(() => renderer.update(content(24, 1000, 2)));
+ expect(graph().uniforms).toBe(first.uniforms);
+ expect(graph().passes).toBe(first.passes);
+ expect(graph().uniforms.value[8]).toBe(2);
+ playback.value = createPlaybackState({ phase: "playing", slideKey: "generation-b", stepKey: "0", stepIndex: 0 });
+ await act(() => renderer.update(content(64)));
+ const second = graph();
+ expect(second.uniforms).not.toBe(first.uniforms);
+ expect(second.uniforms.value).toHaveLength(second.uniformsSize);
+ expect(first.uniforms.value).toHaveLength(first.uniformsSize);
+ playback.value = createPlaybackState({ phase: "playing", slideKey: "generation-c", stepKey: "0", stepIndex: 0 });
+ await act(() => renderer.update(content(24)));
+ expect(graph().uniforms).not.toBe(first.uniforms);
+ expect(graph().uniforms).not.toBe(second.uniforms);
+ expect(graph().uniforms.value).toHaveLength(first.uniformsSize);
+ expect(second.uniforms.value).toHaveLength(second.uniformsSize);
+ expect(first.uniforms.value).toHaveLength(first.uniformsSize);
+ expect(renderer.root.findByType("world-overlay").props.identity).toBe(1);
+ expect(overlayMounts).toBe(1); expect(overlayUnmounts).toBe(0);
+ for (let timestamp = 0; timestamp <= 6000; timestamp += 20) {
+ playback.value = advancePlayback(playback.value, timestamp);
+ reactUI();
+ }
+ const scale = graph().renderScale.value;
+ expect(scale).toBeLessThan(1);
+ await act(() => renderer.update(content(64)));
+ reactUI();
+ expect(graph().renderScale.value).toBe(scale);
+ } finally { await act(() => renderer?.unmount()); }
+});
+
+test("a topology change during camera entry preserves the original slide anchor", async () => {
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
+ playback.value = createPlaybackState({ phase: "playing", slideKey: "entry-origin", stepKey: "0", stepIndex: 0 });
+ let renderer;
+ const content = (count, entering = false) =>
+
+ ;
+ const values = () => renderer.root.findByType("render-graph").props.uniforms.value;
+ try {
+ await act(() => { renderer = create(content(24)); });
+ reactUI();
+ playback.value = createPlaybackState({ phase: "playing", slideKey: "entry-target", stepKey: "0", stepIndex: 0 });
+ await act(() => renderer.update(content(24, true)));
+ reactUI();
+ playback.value = advancePlayback(advancePlayback(playback.value, 0), 1000);
+ reactUI();
+ expect(values()[2]).toBe(20);
+ await act(() => renderer.update(content(64, true)));
+ reactUI();
+ expect(values()[2]).toBe(20);
+ playback.value = advancePlayback(playback.value, 1500);
+ reactUI();
+ expect(values()[2]).toBeCloseTo(28.75);
+ playback.value = advancePlayback(playback.value, 2000);
+ reactUI();
+ expect(values()[2]).toBe(30);
+ } finally { await act(() => renderer?.unmount()); }
+});
+
+test("auto resolution adapts without React frames and preserves the frozen pause/outgoing scale", async () => {
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
+ playback.value = createPlaybackState({ phase: "playing", slideKey: "adaptive", stepKey: "0", stepIndex: 0 });
+ let renderer, commits = 0;
+ const content = (quality, active = true) => commits++}>;
+ try {
+ await act(() => { renderer = create(content("auto")); });
+ const graph = () => renderer.root.findByType("render-graph").props;
+ const passes = graph().passes, initialCommits = commits;
+ for (let timestamp = 0; timestamp <= 6000; timestamp += 20) {
+ playback.value = advancePlayback(playback.value, timestamp);
+ reactUI();
+ }
+ expect(graph().renderScale.value).toBeLessThan(0.8);
+ expect(graph().uniforms.value[35]).toBe(graph().renderScale.value);
+ expect(graph().passes).toBe(passes); expect(commits).toBe(initialCommits);
+ expect(playback.value.slideTime).toBeCloseTo(6);
+ const frozenScale = graph().renderScale.value;
+ playback.value = transitionPlayback(playback.value, { ...playback.value, phase: "paused" });
+ reactUI();
+ expect(graph().renderScale.value).toBe(frozenScale); expect(graph().uniforms.value[35]).toBe(frozenScale);
+ playback.value = transitionPlayback(playback.value, { ...playback.value, phase: "playing" });
+ playback.value = advancePlayback(playback.value, 50000);
+ reactUI();
+ expect(graph().renderScale.value).toBe(frozenScale); expect(playback.value.slideTime).toBeCloseTo(6);
+ playback.value = transitionPlayback(playback.value, { ...playback.value, phase: "outgoing" });
+ reactUI();
+ expect(graph().renderScale.value).toBe(frozenScale);
+ expect(graph().passes).toBe(passes); expect(commits).toBe(initialCommits);
+ await act(() => renderer.update(content("auto", false)));
+ reactUI();
+ expect(graph().renderScale.value).toBe(frozenScale); expect(graph().passes).toBe(passes);
+ playback.value = createPlaybackState({ phase: "preview", slideKey: "preview", stepKey: "1", stepIndex: 1 });
+ reactUI();
+ expect(graph().renderScale.value).toBe(0.8);
+ playback.value = createPlaybackState({ phase: "playing", slideKey: "fixed", stepKey: "0", stepIndex: 0 });
+ await act(() => renderer.update(content("high")));
+ const fixedCommits = commits;
+ for (let timestamp = 0; timestamp <= 6000; timestamp += 20) {
+ playback.value = advancePlayback(playback.value, timestamp);
+ reactUI();
+ }
+ expect(graph().renderScale.value).toBe(0.8); expect(commits).toBe(fixedCommits);
+ } finally { await act(() => renderer?.unmount()); }
+});
+
+test("compiled recursive world evaluators survive an isolated UI-worklet closure", async () => {
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), "world-worklets-"));
+ try {
+ const { compileDeck } = await import("../../presentation/src/compiler");
+ const deck = path.join(directory, "world.mdx");
+ fs.writeFileSync(deck, 'export { World, sampleWorldUniforms, prepareWorldScene, graphInstances, pulseInstances, sprayInstances, rowInstances, voxelInstances, flyTo, tree, wave } from "slides:world/World"\n\n# World');
+ const compiled = await compileDeck(deck, { effectPacksDirectory: path.resolve(import.meta.dirname, "../packs") });
+ if (!compiled.success) throw new Error(compiled.errors.join("\n"));
+ const require = createRequire(import.meta.url);
+ const module = { exports: {} };
+ const callbacks = [], mutables = [], preparations = [], reactions = [];
+ const presentation = { PresentationObservableProvider, usePresentation$, useRuntimeProjection,
+ GPURenderGraph: props => React.createElement("compiled-graph", props),
+ usePlayback: () => playback, samplePlayback, useBackgroundSize: () => ({ width: 800, height: 500 }),
+ usePresentationValue: () => playback.value.phase };
+ const reanimated = {
+ makeMutable: value => { const shared = { value }; mutables.push(shared); return shared; },
+ useSharedValue: value => React.useRef({ value }).current,
+ useAnimatedReaction: (prepare, react) => { preparations.push(prepare); reactions.push(react); },
+ useDerivedValue: fn => { callbacks.push(fn); return { get value() { return fn(); } }; },
+ };
+ Function("require", "module", "exports", compiled.code)(name => {
+ if (name === "react" || name === "react/compiler-runtime" || name === "react/jsx-runtime") return require(name);
+ if (name === "react-native") return { View: "view" };
+ if (name === "@legend-apps/presentation") return presentation;
+ if (name === "react-native-reanimated") return reanimated;
+ return {};
+ }, module, module.exports);
+ const cache = new Map();
+ function restore(fn) {
+ if (!fn.__workletHash) return fn;
+ if (cache.has(fn)) return cache.get(fn);
+ const context = { __closure: {}, _recur: null };
+ const restored = Function(`return (${fn.__initData.code});`)().bind(context);
+ context._recur = restored;
+ cache.set(fn, restored);
+ for (const [key, value] of Object.entries(fn.__closure)) {
+ expect(value).not.toBe(presentation); expect(value).not.toBe(reanimated);
+ context.__closure[key] = typeof value === "function" ? restore(value) : value;
+ }
+ return restored;
+ }
+ const api = module.exports;
+ expect(api.sampleWorldUniforms.__workletHash).toBeNumber();
+ const sample = restore(api.sampleWorldUniforms);
+ const stage = { id: "zone", position: { initial: [5, 0, 0], frames: [] }, glow: { initial: 0.5, frames: [] } };
+ const scene = api.prepareWorldScene({
+ instances: [
+ ...api.graphInstances({ data: api.tree("root", [["a", ["a1", "a2"]]]), activate: [api.wave({ from: "root", step: 1 })] }).map(instance => ({ ...instance, stages: [stage] })),
+ ...api.pulseInstances({ id: "pulse", path: ["root", "a", "a1"], step: 1, duration: 4 }),
+ ...api.sprayInstances({ id: "mist", under: "leaves:lit", rate: 4, lifetime: 2 }),
+ ...api.rowInstances({ id: "rows", count: 4, changes: [{ row: 2, step: 2 }], speed: 1 }).map(instance => ({ ...instance, stages: [stage] })),
+ ...api.voxelInstances({ id: "voxels", grid: [2, 2, 2], collapse: { step: 1, to: 0.2, duration: 2 } }),
+ ],
+ stages: [{ stage, parents: [] }], counter: { source: "voxels" }, dof: {},
+ camera: { shots: [{ step: 0, position: [0, 4, 20] }, { step: 1, follow: "pulse", dolly: 5, focus: "pulse" },
+ { step: 2, follow: "rows:row:2", dolly: 7, focus: "rows:row:2" }, api.flyTo("zone", { step: 3, arc: 3 })] },
+ exposure: { 0: 1, 1: 1.5, 2: 0.8 }, bloom: { intensity: 1.4 },
+ });
+ for (const clock of [
+ { slideTime: 0, stepTime: 0, stepIndex: 0, stepOrigins: { 0: 0 } },
+ { slideTime: 3, stepTime: 1, stepIndex: 1, stepOrigins: { 0: 0, 1: 2 } },
+ { slideTime: 3.99, stepTime: 1.99, stepIndex: 1, stepOrigins: { 0: 0, 1: 2 } },
+ { slideTime: 4.01, stepTime: 2.01, stepIndex: 1, stepOrigins: { 0: 0, 1: 2 } },
+ { slideTime: 1, stepTime: 1, stepIndex: 2, stepOrigins: { 2: 0 } },
+ { slideTime: 0.5, stepTime: 0.5, stepIndex: 3, stepOrigins: { 3: 0 } },
+ ]) expect(sample(scene, clock)).toEqual(api.sampleWorldUniforms(scene, clock));
+ // Mapper inputs come from the serialized closure, including unread values.
+ let renderer;
+ try {
+ playback.value = createPlaybackState({ phase: "playing", slideKey: "closure", stepKey: "0", stepIndex: 0 });
+ await act(() => { renderer = create(React.createElement(api.World, { id: "closure" })); });
+ const autoClosure = reactions.at(-2).__closure;
+ const advanceScale = restore(autoClosure.resolutionEvaluator);
+ let isolated = autoClosure.resolution.value, expected = autoClosure.resolution.value;
+ for (let timestamp = 0; timestamp <= 6000; timestamp += 20) {
+ const input = { timestamp, playing: true, key: "compiled" };
+ isolated = advanceScale(isolated, input); expected = autoClosure.resolutionEvaluator(expected, input);
+ expect(isolated).toEqual(expected);
+ }
+ expect(isolated.scale).toBeLessThan(1);
+ const retained = mutables[0];
+ expect(Object.values(callbacks.at(-1).__closure)).not.toContain(retained);
+ await act(() => renderer.unmount());
+ playback.value = createPlaybackState({ phase: "preview", slideKey: "closure-preview", stepKey: "0", stepIndex: 0 });
+ await act(() => { renderer = create(React.createElement(api.World, { id: "closure", enter: { from: "previous" } })); });
+ expect(Object.values(callbacks.at(-1).__closure)).not.toContain(retained);
+ await act(() => renderer.unmount());
+ playback.value = createPlaybackState({ phase: "playing", slideKey: "closure-next", stepKey: "0", stepIndex: 0 });
+ await act(() => { renderer = create(React.createElement(api.World, { id: "closure", enter: { from: "previous" } })); });
+ expect(Object.values(callbacks.at(-1).__closure)).toContain(retained);
+ await act(() => renderer.unmount());
+ const count = mutables.length;
+ await act(() => { renderer = create(React.createElement(api.World, { quality: "high" })); });
+ expect(mutables.length).toBe(count);
+ expect(Object.values(preparations.at(-2).__closure)).not.toContain(playback);
+ expect(restore(preparations.at(-2))()).toBeNull();
+ expect(Object.values(callbacks.at(-2).__closure)).not.toContain(playback);
+ } finally { await act(() => renderer?.unmount()); }
+ } finally { fs.rmSync(directory, { recursive: true, force: true }); }
+}, 30000);
diff --git a/packages/slides-effects/tests/worldRender.test.ts b/packages/slides-effects/tests/worldRender.test.ts
new file mode 100644
index 000000000..a5e6e3867
--- /dev/null
+++ b/packages/slides-effects/tests/worldRender.test.ts
@@ -0,0 +1,136 @@
+import { expect, test } from "bun:test";
+import { buildWorldGraph, worldGraphTopology } from "../packs/world/render";
+import { customInstances, prepareWorldScene, primitive, rowInstances, sdfInstance, worldHeaderSize, worldInstanceSize, type WorldScene } from "../packs/world/scene";
+
+const quad = `
+fn shape(id:u32,vid:u32,ctx:ShapeContext)->ShapeVertex {
+ let p=array(vec2f(-1.,-1.),vec2f(1.,-1.),vec2f(-1.,1.),vec2f(-1.,1.),vec2f(1.,-1.),vec2f(1.,1.));
+ return ShapeVertex(vec3f(p[vid],f32(id)),vec3f(0.,0.,1.));
+}
+fn shade(id:u32,world:vec3f,normal:vec3f,ctx:ShapeContext)->Material { return Material(vec3f(.2,.5,.8),vec3f(.1),.4,0.); }
+`;
+const scanlines = "fn customPass(uv:vec2f,color:vec4f,ctx:PassContext)->vec4f { return color*vec4f(vec3f(.9+.1*sin(uv.y*200.)),1.); }";
+const scene = (options: Partial = {}) => prepareWorldScene({ instances: [primitive("sphere")], ...options });
+
+for (const quality of ["low", "medium", "high"] as const) test(`all enabled post effects remain present at ${quality} quality`, () => {
+ const graph = buildWorldGraph(scene({ quality, floor: { mirror: true }, dof: { aperture: .7 }, shafts: { intensity: 1 }, lights: [{ position: [0, 8, 0] }] }));
+ const ids = graph.passes.map(pass => pass.id);
+ for (const id of ["reflection-sphere", "floor", "shafts", "dof-gather", "dof-composite", "bloom-composite", "tonemap"]) expect(ids).toContain(id);
+ const scale = quality === "low" ? .65 : quality === "medium" ? .85 : 1;
+ const target = (id: string) => graph.targets.find(target => target.id === id)!;
+ expect(target("reflect").scale).toBe(scale);
+ expect(target("reflectDepth").scale).toBe(target("reflect").scale);
+ expect(target("dof").scale).toBe(scale * .5);
+ expect(target("shafts").scale).toBe(scale * .5);
+ expect(graph.uniformsSize).toBe(worldHeaderSize + worldInstanceSize);
+});
+
+test("animated floor, light and focus changes preserve topology while static quality controls change it", () => {
+ const a = scene({ dof: { focus: 12, aperture: .5 }, floor: { mirror: true, roughness: .1 }, shafts: { intensity: .5 }, lights: [{ intensity: 1 }] });
+ const b = scene({ dof: { focus: { 0: 12, 2: 5 }, aperture: 1 }, floor: { mirror: true, roughness: .8 }, shafts: { intensity: 2 }, lights: [{ intensity: { 0: 1, 2: 4 } }] });
+ expect(worldGraphTopology(a)).toEqual(worldGraphTopology(b));
+ const graph = buildWorldGraph(scene({ dof: { scale: .25, samples: 7 }, floor: { mirror: true, scale: .3 }, shafts: { scale: .125, samples: 5 } }));
+ expect(graph.targets.find(target => target.id === "dof")?.scale).toBe(.25);
+ expect(graph.targets.find(target => target.id === "reflect")?.scale).toBe(.3);
+ expect(graph.targets.find(target => target.id === "shafts")?.scale).toBe(.125);
+});
+
+test("opaque boxes drop back faces so a base resting on the floor cannot z-fight with it", () => {
+ const graph = buildWorldGraph(scene({ floor: { mirror: true }, instances: [primitive("box"), ...customInstances({ wgsl: quad, vertexCount: 6, count: 1 })] }));
+ const code = (id: string) => graph.passes.find(pass => pass.id === id)!.code;
+ for (const id of ["scene-box", "reflection-box"]) expect(code(id)).toContain("dot(normal,eye()-in.world)<0.");
+ expect(code("scene-custom-0")).not.toContain("dot(normal,eye()-in.world)<0.");
+});
+
+test("opaque floor clears the scene before translucent geometry loads its color and depth", () => {
+ const graph = buildWorldGraph(scene({ floor: { mirror: true }, instances: rowInstances({ id: "rows", count: 3 }) }));
+ const draws = graph.passes.filter(pass => pass.kind === "render");
+ expect(draws.map(pass => pass.id)).toEqual(["reflection-box", "floor", "scene-box"]);
+ expect(draws[0].clear).not.toBe(false);
+ expect(draws[1].clear).toEqual(draws[0].clear);
+ expect(draws[2].clear).toBe(false);
+ const empty = buildWorldGraph(scene({ floor: {}, instances: [] }));
+ expect(empty.passes.filter(pass => pass.kind === "render").map(pass => pass.id)).toEqual(["floor"]);
+ const noFloor = buildWorldGraph(scene());
+ expect(noFloor.passes.find(pass => pass.kind === "render")?.clear).not.toBe(false);
+});
+
+test("custom draw groups keep marker-local instance ids and SDF ranges outside primitive draws", () => {
+ const prepared = scene({ instances: [primitive("sphere"), ...customInstances({ wgsl: quad, vertexCount: 6, count: 3 }),
+ ...customInstances({ wgsl: quad, vertexCount: 6, count: 2 }), sdfInstance({ wgsl: "fn sdf(p:vec3f)->f32 { return length(p)-1.; }", bounds: [[-2, -2, -2], [2, 2, 2]] })] });
+ const topology = worldGraphTopology(prepared);
+ expect(topology.counts).toEqual({ box: 0, ring: 0, sphere: 1, tube: 0 });
+ expect(topology.customs?.map(draw => [draw.offset, draw.count])).toEqual([[1, 3], [4, 2]]);
+ expect(topology.sdfs?.map(draw => [draw.offset, draw.count])).toEqual([[6, 1]]);
+ const graph = buildWorldGraph(topology);
+ expect(graph.passes.filter(pass => pass.kind === "render").map(pass => pass.instanceCount)).toEqual([1, 3, 2, 1]);
+ expect(graph.uniformsSize).toBe(worldHeaderSize + 7 * worldInstanceSize);
+});
+
+test("custom draw order compares WGSL by code unit rather than locale", () => {
+ const lower = quad.replace("vec3f(.2,.5,.8)", "vec3f(.2,.5,.8)*alpha"), upper = quad.replace("vec3f(.2,.5,.8)", "vec3f(.2,.5,.8)*Beta");
+ const topology = worldGraphTopology(scene({ instances: [primitive("sphere"), ...customInstances({ wgsl: lower, vertexCount: 6, count: 2 }),
+ ...customInstances({ wgsl: upper, vertexCount: 3, count: 3 })] }));
+ expect(topology.customs?.map(draw => [draw.wgsl, draw.offset, draw.count])).toEqual([[upper, 1, 3], [lower, 4, 2]]);
+ const graph = buildWorldGraph(topology);
+ expect(graph.passes.filter(pass => pass.kind === "render").map(pass => [pass.vertexCount, pass.instanceCount])).toEqual([[expect.any(Number), 1], [3, 3], [6, 2]]);
+ expect(graph.uniformsSize).toBe(worldHeaderSize + 6 * worldInstanceSize);
+});
+
+test("postOrder rewires HDR stages and custom hooks retain the declared color-space boundary", () => {
+ const graph = buildWorldGraph(scene({ dof: {}, postOrder: ["bloom", "dof", "fog"], passes: [
+ { id: "before-focus", wgsl: scanlines, before: "dof" }, { id: "ldr", wgsl: scanlines, after: "tonemap" },
+ ] }));
+ const pass = (id: string) => graph.passes.find(pass => pass.id === id)!;
+ const output = (id: string) => { const item = pass(id); return item.kind === "fullscreen" ? item.output : undefined; };
+ expect(pass("bloom-threshold").reads).toEqual(["hdr"]);
+ expect(pass("before-focus").reads?.[0]).toBe(output("bloom-composite"));
+ expect(pass("dof-gather").reads?.[0]).toBe(output("before-focus"));
+ expect(pass("fog").reads?.[0]).toBe(output("dof-composite"));
+ expect(pass("tonemap").kind).toBe("fullscreen");
+ expect(pass("ldr").reads?.[0]).toBe(output("tonemap"));
+ expect(pass("screen").reads).toEqual([output("ldr")]);
+ expect(graph.passes.filter(pass => pass.id === "tonemap")).toHaveLength(1);
+});
+
+test("HDR transients reuse two targets without aliasing DOF inputs, while explicit custom reads retain intermediates", () => {
+ for (const postOrder of [["fog", "dof", "bloom"], ["dof", "bloom", "fog"], ["bloom", "fog", "dof"]] as const) {
+ const graph = buildWorldGraph(scene({ dof: {}, postOrder: [...postOrder], passes: [{ wgsl: scanlines, after: "tonemap" }] }));
+ expect(graph.targets.filter(target => target.id.startsWith("post-"))).toHaveLength(2);
+ expect(graph.targets.some(target => target.id === "hdr")).toBe(true);
+ const gather = graph.passes.find(pass => pass.id === "dof-gather")!;
+ const composite = graph.passes.find(pass => pass.id === "dof-composite")!;
+ expect(composite.reads?.[0]).toBe(gather.reads?.[0]);
+ for (const pass of graph.passes) if (pass.kind === "fullscreen") expect(pass.reads).not.toContain(pass.output);
+ }
+ const retained = buildWorldGraph(scene({ dof: {}, passes: [{ wgsl: scanlines, after: "bloom", reads: ["fog"] }] }));
+ expect(retained.targets.some(target => target.id === "fog")).toBe(true);
+ expect(retained.targets.some(target => target.id.startsWith("post-"))).toBe(false);
+ expect(retained.passes.find(pass => pass.id === "custom-post-0")?.reads).toContain("fog");
+});
+
+test("invalid post stage permutations and placements fail rather than silently skipping hooks", () => {
+ for (const postOrder of [["fog", "fog", "bloom"], ["fog", "bloom"], ["fog", "dof", "unknown"]]) {
+ expect(() => buildWorldGraph({ quality: "high", counts: { box: 0, ring: 0, sphere: 0, tube: 0 }, backgroundVector: [0, 0, 0], postOrder })).toThrow("postOrder");
+ }
+ for (const placement of [{ before: "scene" }, { after: "unknown" }, { before: "fog", after: "dof" }]) {
+ expect(() => buildWorldGraph(scene({ passes: [{ wgsl: scanlines, ...placement }] }))).toThrow("placement");
+ }
+});
+
+test("custom ids cannot shadow built-in passes or another custom pass", () => {
+ for (const id of ["fog", "scene-sphere", "tonemap", "screen"]) {
+ expect(() => buildWorldGraph(scene({ passes: [{ id, wgsl: scanlines, after: "tonemap" }] }))).toThrow("pass id is duplicated");
+ }
+ expect(() => buildWorldGraph(scene({ passes: [
+ { id: "scanlines", wgsl: scanlines, after: "fog" }, { id: "scanlines", wgsl: scanlines, after: "bloom" },
+ ] }))).toThrow("pass id is duplicated");
+});
+
+test("auto quality drops MSAA only where the drawable already supersamples the slide", () => {
+ const scene = prepareWorldScene({ quality: "auto", instances: [primitive("box")], floor: { mirror: true } });
+ const samples = (topology: object) => buildWorldGraph({ ...worldGraphTopology(scene), ...topology }).targets.filter(target => target.samples === 4).map(target => target.id);
+ expect(samples({})).toEqual(["hdr", "depth", "reflect", "reflectDepth"]);
+ expect(samples({ supersampled: true })).toEqual([]);
+ expect(samples({ supersampled: true, quality: "high" })).toHaveLength(4);
+});
diff --git a/packages/slides-effects/tests/worldResolution.test.ts b/packages/slides-effects/tests/worldResolution.test.ts
new file mode 100644
index 000000000..83d16d265
--- /dev/null
+++ b/packages/slides-effects/tests/worldResolution.test.ts
@@ -0,0 +1,101 @@
+import { expect, test } from "bun:test";
+import { advanceResolution, createResolutionState, type ResolutionState } from "../packs/world/resolution";
+import { advancePlayback, createPlaybackState, transitionPlayback } from "../../presentation/src/playbackState";
+
+function frames(state: ResolutionState, interval: number, count: number, key = "scene") {
+ let timestamp = state.lastTimestamp ?? 0;
+ const scales: number[] = [];
+ for (let index = 0; index < count; index++) {
+ timestamp += interval;
+ state = advanceResolution(state, { timestamp, playing: true, key });
+ scales.push(state.scale);
+ }
+ return { state, scales };
+}
+
+test("ordinary 60 Hz and 120 Hz cadence keeps full resolution", () => {
+ for (const interval of [1000 / 60, 1000 / 120]) {
+ const { state, scales } = frames(createResolutionState(), interval, 2400);
+ expect(state.scale).toBe(1);
+ expect(scales.every(scale => scale === 1)).toBe(true);
+ }
+});
+
+test("sustained 50 fps keeps full resolution", () => {
+ expect(frames(createResolutionState(), 20, 1000).scales.every(scale => scale === 1)).toBe(true);
+});
+
+test("collapsed cadence steps down to the 0.75 floor and healthy cadence recovers with hysteresis", () => {
+ const slow = frames(createResolutionState(), 30, 1000);
+ expect(slow.scales.slice(0, 40).every(scale => scale === 1)).toBe(true);
+ expect([...new Set(slow.scales)]).toEqual([1, 0.875, 0.75]);
+ expect(slow.state.scale).toBe(0.75);
+ expect(frames(createResolutionState(), 120, 200).state.scale).toBe(0.75);
+ const neutral = frames(slow.state, 20, 400);
+ expect(neutral.state.scale).toBe(0.75);
+ const shortRecovery = frames(neutral.state, 1000 / 60, 240);
+ expect(shortRecovery.state.scale).toBe(0.75);
+ const recovered = frames(shortRecovery.state, 1000 / 60, 1600);
+ expect(recovered.state.scale).toBe(1);
+ expect(recovered.scales.every(scale => scale >= 0.75 && scale <= 1)).toBe(true);
+});
+
+test("startup, isolated stalls, paused gaps and timestamp resets do not lower resolution", () => {
+ let state = frames(createResolutionState(), 1000 / 60, 120).state;
+ state = advanceResolution(state, { timestamp: state.lastTimestamp! + 50, playing: true, key: "scene" });
+ state = frames(state, 1000 / 60, 120).state;
+ expect(state.scale).toBe(1);
+ const same = advanceResolution(state, { timestamp: state.lastTimestamp, playing: true, key: "scene" });
+ expect(same).toBe(state);
+ const warmup = state.warmupUntil;
+ state = advanceResolution(state, { timestamp: state.lastTimestamp! + 5000, playing: true, key: "scene" });
+ expect(state.scale).toBe(1); expect(state.warmupUntil).toBe(warmup);
+ state = advanceResolution(state, { timestamp: null, playing: false, key: "scene" });
+ expect(state.lastTimestamp).toBeNull();
+ state = advanceResolution(state, { timestamp: 20000, playing: true, key: "scene" });
+ expect(state.warmupUntil).toBe(20500); expect(state.frames).toBe(0);
+ state = advanceResolution(state, { timestamp: 100, playing: true, key: "scene" });
+ expect(state.warmupUntil).toBe(600); expect(state.scale).toBe(1);
+ state = advanceResolution({ ...state, scale: 0.5 }, { timestamp: 200, playing: true, key: "next" });
+ expect(state.scale).toBe(1); expect(state.key).toBe("next");
+});
+
+test("an isolated hitch preserves the current cooldown and recovery history", () => {
+ const state = frames(createResolutionState(), 30, 70).state;
+ expect(state.scale).toBe(0.875);
+ expect(state.cooldownUntil).toBeGreaterThan(state.lastTimestamp!);
+ const hitch = advanceResolution(state, { timestamp: state.lastTimestamp! + 500, playing: true, key: "scene" });
+ expect(hitch.scale).toBe(state.scale);
+ expect(hitch.cooldownUntil).toBe(state.cooldownUntil);
+ expect(hitch.warmupUntil).toBe(state.warmupUntil);
+ const paused = advanceResolution(state, { timestamp: null, playing: false, key: "scene" });
+ const resumed = advanceResolution(paused, { timestamp: state.lastTimestamp! + 20, playing: true, key: "scene" });
+ expect(resumed.cooldownUntil).toBe(state.cooldownUntil); expect(resumed.scale).toBe(state.scale);
+ expect(advanceResolution(resumed, { timestamp: state.lastTimestamp! + 40, playing: true, key: "next" }).cooldownUntil).toBe(0);
+ const healthy = frames(createResolutionState(), 1000 / 60, 240).state;
+ expect(healthy.healthyWindows).toBeGreaterThan(0);
+ const interrupted = advanceResolution(healthy, { timestamp: healthy.lastTimestamp! + 500, playing: true, key: "scene" });
+ expect(interrupted.healthyWindows).toBe(healthy.healthyWindows);
+});
+
+test("adaptation never changes playback clocks across a real pause and resume", () => {
+ let playback = createPlaybackState({ phase: "playing", slideKey: "clock", stepKey: "0", stepIndex: 0 });
+ let resolution = createResolutionState();
+ for (let timestamp = 0; timestamp <= 10000; timestamp += 40) {
+ playback = advancePlayback(playback, timestamp);
+ resolution = advanceResolution(resolution, { timestamp: playback.lastTimestamp, playing: playback.phase === "playing", key: playback.slideKey });
+ }
+ expect(resolution.scale).toBeLessThan(1);
+ expect(playback.slideTime).toBeCloseTo(10);
+ playback = transitionPlayback(playback, { ...playback, phase: "paused" });
+ resolution = advanceResolution(resolution, { timestamp: playback.lastTimestamp, playing: false, key: playback.slideKey });
+ const paused = resolution.scale;
+ playback = advancePlayback(playback, 50000);
+ expect(playback.slideTime).toBeCloseTo(10);
+ playback = transitionPlayback(playback, { ...playback, phase: "playing" });
+ playback = advancePlayback(playback, 50000);
+ resolution = advanceResolution(resolution, { timestamp: playback.lastTimestamp, playing: true, key: playback.slideKey });
+ expect(resolution.scale).toBe(paused);
+ expect(resolution.frames).toBe(0);
+ expect(playback.slideTime).toBeCloseTo(10);
+});
diff --git a/packages/slides-web/export.ts b/packages/slides-web/export.ts
index 820d89479..6004cafa0 100644
--- a/packages/slides-web/export.ts
+++ b/packages/slides-web/export.ts
@@ -20,7 +20,7 @@ const workletsPlugin = require("react-native-worklets/plugin");
const src = path.join(webRoot, "src");
// How the web player renders each effect pack capability; any other capability is unsupported on web.
const webCapabilities: Record = {
- reanimated: "Reanimated web", skia: "CanvasKit", lottie: "Skottie", "native-video": "HTML video", webgpu: "WebGPU (requires a WebGPU browser)",
+ reanimated: "Reanimated web", skia: "CanvasKit", lottie: "Skottie", "native-video": "HTML video", webgpu: "WebGPU (requires a WebGPU browser)", "webgpu-multipass": "WebGPU multipass (requires a WebGPU browser)",
};
export function webEffectSupport(found: { id: string; capabilities?: unknown }[]) {
diff --git a/packages/slides-web/tests/export.test.mjs b/packages/slides-web/tests/export.test.mjs
index 20771ef7c..43cabbe4f 100644
--- a/packages/slides-web/tests/export.test.mjs
+++ b/packages/slides-web/tests/export.test.mjs
@@ -19,6 +19,7 @@ test("reports how each effect pack runs on web and flags native-only capabilitie
expect(effects).toEqual({ animation: { reanimated: "Reanimated web", lottie: "Skottie" }, camera: { skia: "CanvasKit", arkit: "unsupported" } });
expect(unsupported).toEqual(['Effect pack "camera" is unsupported on web: it requires the native "arkit" capability.']);
expect(webEffectSupport([{ id: "local" }])).toEqual({ effects: { local: {} }, unsupported: [] });
+ expect(webEffectSupport([{ id: "world", capabilities: ["webgpu-multipass"] }])).toEqual({ effects: { world: { "webgpu-multipass": "WebGPU multipass (requires a WebGPU browser)" } }, unsupported: [] });
});
test("Skia detection follows compiled host imports and built-in effects, not slide prose", () => {
diff --git a/patches/react-native-webgpu@0.8.5.patch b/patches/react-native-webgpu@0.8.5.patch
index 0d36becd4..2abae7dab 100644
--- a/patches/react-native-webgpu@0.8.5.patch
+++ b/patches/react-native-webgpu@0.8.5.patch
@@ -30,3 +30,18 @@ diff --git a/cpp/rnwgpu/SurfaceRegistry.h b/cpp/rnwgpu/SurfaceRegistry.h
}
SurfaceRegistry(const SurfaceRegistry &) = delete;
+diff --git a/cpp/rnwgpu/api/GPUDevice.cpp b/cpp/rnwgpu/api/GPUDevice.cpp
+--- a/cpp/rnwgpu/api/GPUDevice.cpp
++++ b/cpp/rnwgpu/api/GPUDevice.cpp
+@@ -161,7 +161,10 @@ GPUDevice::createTexture(std::shared_ptr descriptor) {
+ throw std::runtime_error("Error with GPUTextureDescriptor");
+ }
+ auto texture = _instance.CreateTexture(&desc);
+- return std::make_shared(texture, descriptor->label.value_or(""));
++ // GPU memory is not Hermes heap: charging it as external memory lets a few
++ // full-screen render targets exhaust the runtime's heap limit and abort.
++ return std::make_shared(texture, descriptor->label.value_or(""),
++ false);
+ }
+
+ std::shared_ptr GPUDevice::createShaderModule(
diff --git a/patches/react-native-worklets@0.8.3.patch b/patches/react-native-worklets@0.8.3.patch
index e27ec6d9e..742c82f19 100644
--- a/patches/react-native-worklets@0.8.3.patch
+++ b/patches/react-native-worklets@0.8.3.patch
@@ -1,22 +1,171 @@
diff --git a/apple/worklets/apple/AnimationFrameQueue.mm b/apple/worklets/apple/AnimationFrameQueue.mm
-index b93cd2c625ef8a03415ce23c6fc584e51d3696a9..08e340e6e7cf9025192ffc3c835395e676862b49 100644
+index b93cd2c625ef8a03415ce23c6fc584e51d3696a9..e955364572c134b57ae54bf0a2bce1c9c35dd05a 100644
--- a/apple/worklets/apple/AnimationFrameQueue.mm
+++ b/apple/worklets/apple/AnimationFrameQueue.mm
-@@ -87,7 +87,6 @@ - (void)executeQueue:(WorkletsDisplayLink *)displayLink
+@@ -6,7 +6,9 @@
+ #import
+
+ #if TARGET_OS_OSX
++#import
+ #import
++#import
+ #endif // TARGET_OS_OSX
+
+ #import
+@@ -20,6 +22,124 @@
+ const auto POOR = CAFrameRateRangeMake(10, 24, 24);
+ } // namespace FrameRateRange
+
++#if TARGET_OS_OSX
++// RCTPlatformDisplayLink queues a main run loop tick for every display pulse, so ticks queued while the
++// main thread is busy run back to back. This link keeps at most one tick queued or running, and an
++// invalidated link never restarts or delivers a queued tick.
++@interface WorkletsFrameLink : RCTPlatformDisplayLink
++@end
++
++@implementation WorkletsFrameLink {
++ CVDisplayLinkRef link_;
++ __weak id target_;
++ SEL selector_;
++ NSRunLoop *runLoop_;
++ NSRunLoopMode mode_;
++ // Orders Start/Stop with invalidation. The display link callback never takes it.
++ std::mutex controlMutex_;
++ std::atomic tickPending_;
++ std::atomic invalidated_;
++}
++
+++ (instancetype)displayLinkWithTarget:(id)target selector:(SEL)selector
++{
++ WorkletsFrameLink *link = [self new];
++ link->target_ = target;
++ link->selector_ = selector;
++ return link;
++}
++
++static CVReturn WorkletsFrameLinkCallback(
++ CVDisplayLinkRef,
++ const CVTimeStamp *,
++ const CVTimeStamp *,
++ CVOptionFlags,
++ CVOptionFlags *,
++ void *context)
++{
++ WorkletsFrameLink *link = (__bridge WorkletsFrameLink *)context;
++ if (link->invalidated_ || link->tickPending_.exchange(true)) {
++ return kCVReturnSuccess;
++ }
++ CFRunLoopRef runLoop = link->runLoop_.getCFRunLoop;
++ CFRunLoopPerformBlock(runLoop, (__bridge CFTypeRef)link->mode_, ^{
++ @autoreleasepool {
++ id target = link->target_;
++ if (!link->invalidated_ && target) {
++ ((void (*)(id, SEL, WorkletsFrameLink *))[target methodForSelector:link->selector_])(
++ target, link->selector_, link);
++ }
++ link->tickPending_ = false;
++ }
++ });
++ CFRunLoopWakeUp(runLoop);
++ return kCVReturnSuccess;
++}
++
++- (void)dealloc
++{
++ if (link_ != NULL) {
++ CVDisplayLinkStop(link_);
++ CVDisplayLinkRelease(link_);
++ }
++}
++
++- (void)addToRunLoop:(NSRunLoop *)runLoop forMode:(NSRunLoopMode)mode
++{
++ runLoop_ = runLoop;
++ mode_ = mode;
++ if (CVDisplayLinkCreateWithActiveCGDisplays(&link_) != kCVReturnSuccess) {
++ CVDisplayLinkCreateWithCGDisplay(CGMainDisplayID(), &link_);
++ }
++ RCTAssert(link_ != NULL, @"Cannot create display link");
++ CVDisplayLinkSetOutputCallback(link_, &WorkletsFrameLinkCallback, (__bridge void *)self);
++}
++
++- (void)setPaused:(BOOL)paused
++{
++ std::lock_guard lock(controlMutex_);
++ if (link_ == NULL || invalidated_) {
++ return;
++ }
++ if (paused) {
++ CVDisplayLinkStop(link_);
++ } else {
++ CVDisplayLinkStart(link_);
++ }
++}
++
++- (void)invalidate
++{
++ std::lock_guard