Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/core/src/runtime/heldScenes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,25 @@ describe("scene scripts held until the playhead nears", () => {
expect(ran()).toBe("near tied far ");
});

it("posts the timeline a far scene's mount changes after the jump's frame paints, not before", async () => {
await boot(true);
// Boot's own posts settle first.
await vi.waitFor(() => expect(window.__renderReady).toBe(true));
await new Promise((done) => setTimeout(done, 500));
const post = vi.spyOn(window, "postMessage");
const timelinePosts = () =>
post.mock.calls.filter(([message]) => (message as { type?: string }).type === "timeline")
.length;
window.__player!.seek(25);
await window.__hfWaitForSeekCompletion?.();
expect(ran()).toContain("far");
expect(timelinePosts()).toBe(0);
await vi.waitFor(() => expect(timelinePosts()).toBe(1));
// The transport saw the mount too: the one post discharged it.
await new Promise((done) => setTimeout(done, 500));
expect(timelinePosts()).toBe(1);
});

it("runs a far scene the playback reaches without pausing the clock", async () => {
await boot(true);
window.__player!.seek(19.5);
Expand Down
36 changes: 31 additions & 5 deletions packages/core/src/runtime/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3473,6 +3473,23 @@ export function initSandboxRuntimeModular(): void {
if (manifest !== lastPostedManifest) postTimeline();
};

// Whole-film work a jump's frame does not need: one post once the frame has painted (a task after the next frame).
let timelinePostQueued = false;
const postTimelineAfterPaint = () => {
if (timelinePostQueued) return;
timelinePostQueued = true;
window.requestAnimationFrame(() =>
setTimeout(() => {
timelinePostQueued = false;
if (state.tornDown) return;
// Discharges what the transport saw meanwhile, as its own post does.
lastSeenTimingRevision = readCompositionTimingRevision();
postTimeline();
compositionChangePending = false;
}, 0),
);
};

const finitePositiveDuration = (value: number | null | undefined): number =>
typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;

Expand Down Expand Up @@ -3718,7 +3735,10 @@ export function initSandboxRuntimeModular(): void {
// Swap edited scenes in place from a rebuilt preview document. Refuses before changing anything unless
// the documents differ only inside existing scenes; a later failure is left to the caller's reload.
// Nests the timelines a scene's scripts just made and redraws the current frame with them.
const nestRanScenes = (hosts: Element[], { drawnByTick = false } = {}) => {
const nestRanScenes = (
hosts: Element[],
{ drawnByTick = false, postAfterPaint = false } = {},
) => {
childrenBound = false;
bindRootTimelineIfAvailable();
// Probed before the scene was nested, so the new media found no volume envelope.
Expand All @@ -3741,7 +3761,8 @@ export function initSandboxRuntimeModular(): void {
if (!(drawnByTick && clock.isPlaying() && !cutShort))
transport.seek(cutShort ? duration : state.currentTime, { keepPlaying: !cutShort });
syncTimedElementVisibility(state.currentTime);
postTimeline();
if (postAfterPaint) postTimelineAfterPaint();
else postTimeline();
};
const swapScenes = async (html: string, signal?: AbortSignal): Promise<void> => {
const generation = sceneSwapGeneration;
Expand Down Expand Up @@ -3949,7 +3970,7 @@ export function initSandboxRuntimeModular(): void {
for (const host of hosts) applyVariableBindings(document, host);
bindMediaMetadataListeners();
initVfx(document.body, state.canonicalFps);
nestRanScenes(hosts, { drawnByTick: true });
nestRanScenes(hosts, { drawnByTick: true, postAfterPaint: true });
});
sceneMounts = mounted.catch(() => undefined);
registerSeekCompletion(mounted);
Expand Down Expand Up @@ -4920,7 +4941,12 @@ export function initSandboxRuntimeModular(): void {
postTimeline();
}
}
if (changeDrivenService || pausedCounterDue(TIMELINE_POST_INTERVAL_FRAMES)) {
// A queued after-paint post carries whatever changed meanwhile; posting here would run before the paint.
const postOwedHere = !timelinePostQueued;
if (
postOwedHere &&
(changeDrivenService || pausedCounterDue(TIMELINE_POST_INTERVAL_FRAMES))
) {
// The manifest is what carries a composition change to its consumers,
// so posting it is what discharges the pending flag — whichever path
// got here. Clearing it when the change is merely SEEN would drop it
Expand All @@ -4933,7 +4959,7 @@ export function initSandboxRuntimeModular(): void {
// change happens to wake the loop again.
postTimeline();
compositionChangePending = false;
} else if (playingPollDue || stoppedPlaying) {
} else if (postOwedHere && (playingPollDue || stoppedPlaying)) {
postTimelineIfManifestChanged();
}
if (changeDrivenService || pausedCounterDue(MEDIA_BIND_INTERVAL_FRAMES)) {
Expand Down
Loading