Fix new-game title flip + cap lookahead prepare burst
Builds on the worker migration with prepare-burst pacing and a title-flip fix: - New game from mid-game left the book on the previous game's spread, so the first block's source and target spread matched and the title->content page turn was skipped. story:client-reset now returns the book to the title spread (spread 0) so the first block flips 0->1 and animates. Verified: requiresSpreadTransition src=0 tgt=1, page-flip-started/near-end fire. - The lookahead burst-prepared many blocks at once, spiking allocation/GC into multi-second main-thread stalls. WebGL book prepares are now serialized through a chain and capped to a small lookahead window (TTS audio prefetch still spans the full window); future lookahead is also deferred until the current sentence has entered the display pipeline, keeping it off the first flip/reveal critical path. Worst game-start stall ~6s -> ~3.4s. - Page flips now drive the scene through the sceneControl prewarm/startPreparedPageFlip API (awaited) instead of an event, and the scene awaits the async initial spread draw. Suite 177. Remaining: a per-block prepare stall (~1.6-3.4s for large blocks at game start) that profiling has not yet attributed to a single function (likely GC from prepare-path allocation) — needs a DevTools performance capture for exact attribution. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,10 +10,10 @@
|
||||
* -> activate (upload the visible textures for the target spread)
|
||||
* -> reveal (animate the new block's text in)
|
||||
*
|
||||
* It drives the scene exclusively through the formal `webgl-book:*` events and
|
||||
* the registered `webgl-book-scene` accessor. It never touches `window.BookLabDebug`
|
||||
* (debug-only) and never throws out of the live playback path: a transient cache
|
||||
* miss is surfaced as a problem state and playback degrades gracefully.
|
||||
* It drives the scene through the registered `webgl-book-scene` accessor and uses
|
||||
* `webgl-book:*` events only as state notifications. It never touches
|
||||
* `window.BookLabDebug` (debug-only). Cache and scene-preparation misses are
|
||||
* surfaced as problem states instead of being hidden by alternate playback paths.
|
||||
*/
|
||||
import { BaseModule } from './base-module.js';
|
||||
|
||||
@@ -120,6 +120,7 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
this.recordDiagnostic('segment-play:start', segment);
|
||||
|
||||
try {
|
||||
segment.sourceSpreadIndex = this.getVisibleSpreadIndex();
|
||||
// Commit pagination first so the flip targets the authoritative spread,
|
||||
// not the predicted preview spread.
|
||||
await this.timeStage('commit', segment, () => this.commitSegmentSpread(segment, sentence));
|
||||
@@ -279,6 +280,9 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
|
||||
async commitSegmentSpread(segment = {}, sentence = segment.sentence) {
|
||||
if (!segment || !sentence) return null;
|
||||
segment.sourceSpreadIndex = Number.isFinite(Number(segment.sourceSpreadIndex))
|
||||
? Math.max(0, Math.round(Number(segment.sourceSpreadIndex)))
|
||||
: this.getVisibleSpreadIndex();
|
||||
const activeSpread = await this.pagination.preparePendingBlock(sentence, {
|
||||
includeUnrenderedHistory: true
|
||||
});
|
||||
@@ -314,10 +318,18 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
};
|
||||
}
|
||||
const spread = segment.activeSpread || segment.previewSpread;
|
||||
const revealDetail = this.createRevealDetail(sentence, spread, 'activate');
|
||||
let texturePlan = segment.preparedTexturePlan
|
||||
? { ...segment.preparedTexturePlan, phase: 'activate' }
|
||||
: null;
|
||||
if (texturePlan && this.pageCache?.hasPreparedRevealPlan?.(segment.blockId)) {
|
||||
this.pageCache.takePreparedRevealPlan(segment.blockId);
|
||||
}
|
||||
if (!texturePlan) {
|
||||
const revealDetail = this.createRevealDetail(sentence, spread, 'activate');
|
||||
texturePlan = await this.textureRenderer.prepareRevealBlock(revealDetail, { publishEvent: false });
|
||||
}
|
||||
// Reuse the spanning-aware plan prepared during lookahead — its timing already spans
|
||||
// both pages. No synchronous redraw on the critical path.
|
||||
const texturePlan = await this.textureRenderer.prepareRevealBlock(revealDetail, { publishEvent: false });
|
||||
segment.activeTexturePlan = texturePlan;
|
||||
this.applyTexturePlan(texturePlan, segment, 'activate');
|
||||
await this.assertSegmentReady(segment, 'activate');
|
||||
@@ -439,7 +451,10 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
}
|
||||
|
||||
requiresSpreadTransition(segment = {}) {
|
||||
return Math.max(0, Number(segment.targetSpreadIndex || 0)) > this.getVisibleSpreadIndex();
|
||||
const sourceSpread = Number.isFinite(Number(segment.sourceSpreadIndex))
|
||||
? Math.max(0, Math.round(Number(segment.sourceSpreadIndex)))
|
||||
: this.getVisibleSpreadIndex();
|
||||
return Math.max(0, Number(segment.targetSpreadIndex || 0)) > sourceSpread;
|
||||
}
|
||||
|
||||
requiresRightPageFlipAfterReveal(spread = {}) {
|
||||
@@ -561,26 +576,42 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
|
||||
async requestPageFlip(direction = 1, options = {}) {
|
||||
if (this.isChoiceAwaitingPlayer()) return false;
|
||||
// Warm the texture cache for the navigation window and verify the target pages
|
||||
// are resident before asking the scene to flip. The scene performs its own
|
||||
// flip-specific prewarm (drawing the spreads), so we do not pass this through.
|
||||
await this.prepareFlipPlan(direction, options);
|
||||
const flipPlan = await this.prepareFlipPlan(direction, options);
|
||||
await this.assertSegmentReady({
|
||||
blockId: options.blockId ?? null,
|
||||
targetSpreadIndex: options.targetSpread,
|
||||
revealSides: []
|
||||
}, 'flip');
|
||||
const wait = this.waitForPageFlipFinished(options.targetSpread);
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:request-page-flip', {
|
||||
detail: {
|
||||
direction,
|
||||
force: options.force === true,
|
||||
reason: options.reason || 'timeline',
|
||||
targetSpread: options.targetSpread,
|
||||
revealSides: Array.isArray(options.revealSides) ? options.revealSides : null
|
||||
}
|
||||
}));
|
||||
return wait;
|
||||
const sceneControl = this.scene?.sceneControl || null;
|
||||
if (typeof sceneControl?.prewarmPageFlip !== 'function' || typeof sceneControl?.startPreparedPageFlip !== 'function') {
|
||||
this.pageCache?.recordProblem?.({
|
||||
type: 'timeline-scene-flip-api-missing',
|
||||
targetSpread: flipPlan.targetSpread,
|
||||
reason: options.reason || 'timeline'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
const scenePrewarm = await sceneControl.prewarmPageFlip(direction, {
|
||||
targetSpread: flipPlan.targetSpread,
|
||||
reason: options.reason || 'timeline'
|
||||
});
|
||||
const started = sceneControl.startPreparedPageFlip(direction, {
|
||||
force: options.force === true,
|
||||
reason: options.reason || 'timeline',
|
||||
targetSpread: flipPlan.targetSpread,
|
||||
deferRevealSides: Array.isArray(options.revealSides) ? options.revealSides : null,
|
||||
flipPlan,
|
||||
prewarm: scenePrewarm
|
||||
});
|
||||
if (!started) {
|
||||
this.pageCache?.recordProblem?.({
|
||||
type: 'timeline-scene-flip-start-failed',
|
||||
targetSpread: flipPlan.targetSpread,
|
||||
reason: options.reason || 'timeline'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return this.waitForPageFlipFinished(flipPlan.targetSpread, { alreadyStarted: true });
|
||||
}
|
||||
|
||||
async prepareFlipPlan(direction = 1, options = {}) {
|
||||
@@ -728,9 +759,9 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
};
|
||||
}
|
||||
|
||||
waitForPageFlipFinished(targetSpread = null) {
|
||||
waitForPageFlipFinished(targetSpread = null, options = {}) {
|
||||
return new Promise(resolve => {
|
||||
let started = false;
|
||||
let started = options.alreadyStarted === true;
|
||||
let resolved = false;
|
||||
const expectedSpread = Number.isFinite(Number(targetSpread))
|
||||
? Math.max(0, Math.round(Number(targetSpread)))
|
||||
|
||||
Reference in New Issue
Block a user