diff --git a/public/js/book-playback-timeline-module.js b/public/js/book-playback-timeline-module.js index a8963bb..603f385 100644 --- a/public/js/book-playback-timeline-module.js +++ b/public/js/book-playback-timeline-module.js @@ -206,13 +206,12 @@ class BookPlaybackTimelineModule extends BaseModule { }); if (!previewSpread) return null; - // Detect a spanning block from the (not-yet-committed) preview layout so its reveal - // plan can be prepared spanning-aware in the background, off the critical path. Only - // during background prepares and when the preview layout is available; otherwise we - // fall back to the synchronous activate rebuild + continuation redraw (today's path). - const previewSpreads = options.immediate !== true && Array.isArray(previewSpread.previewSpreads) - ? previewSpread.previewSpreads - : null; + // Every block is prepared once, spanning-aware. The preview layout (attached to the + // preview spread by pagination) tells us whether the block overflows onto the next + // spread; if so we derive the start spread's timing across both spreads and prepare the + // continuation spread now. activate and revealContinuationSpread then reuse these — one + // prepare path, no synchronous rebuild or redraw on the critical path. + const previewSpreads = Array.isArray(previewSpread.previewSpreads) ? previewSpread.previewSpreads : null; const startIndex = Math.max(0, Number(previewSpread.index || 0)); const continuationSpread = previewSpreads ? (previewSpreads @@ -221,20 +220,14 @@ class BookPlaybackTimelineModule extends BaseModule { && this.getBlockRevealSides(spread, sentence.blockId).length > 0) .sort((a, b) => Number(a.index) - Number(b.index))[0] || null) : null; - const spanningPlanPrepared = Boolean(continuationSpread); const revealDetail = this.createRevealDetail(sentence, previewSpread, 'prepare'); - // For a spanning block, derive the start spread's timing across both preview spreads so - // the cached plan already spans both pages and activate can reuse it (no rebuild). const texturePlan = this.textureRenderer.prepareRevealBlock( - spanningPlanPrepared ? { ...revealDetail, previewSpreads } : revealDetail, + continuationSpread ? { ...revealDetail, previewSpreads } : revealDetail, { phase: 'prepare', publishEvent: false } ); - - // Also prepare the continuation spread's plan so revealContinuationSpread reuses it - // after the flip instead of redrawing synchronously. if (continuationSpread) { - this.textureRenderer.prepareContinuationRevealPlan?.({ + this.textureRenderer.prepareContinuationRevealPlan({ ...revealDetail, previewSpreads, continuationSpread @@ -256,9 +249,6 @@ class BookPlaybackTimelineModule extends BaseModule { revealSides, requiresPreFlip: targetSpreadIndex > currentSpreadIndex, requiresRightFlip: revealSides.includes('right') && this.requiresRightPageFlipAfterReveal(previewSpread), - // True when this block spans and its plan was prepared spanning-aware in the - // background, so activate can reuse it without a synchronous forceRebuild. - spanningPlanPrepared, // Snapshot the reveal timings now. A reused lookahead segment can be played by // a sentence instance whose animation timings were lost; without them the // reveal can't be word-paced and stretches across the whole TTS. @@ -325,15 +315,9 @@ class BookPlaybackTimelineModule extends BaseModule { } const spread = segment.activeSpread || segment.previewSpread; const revealDetail = this.createRevealDetail(sentence, spread, 'activate'); - // A spanning block needs timing across both pages. When the plan was prepared - // spanning-aware during lookahead (the common case), reuse it — no synchronous redraw - // on the critical path. Only when it spans but was NOT prepared spanning-aware (e.g. an - // immediate prepare with no preview layout) do we rebuild from the committed spreads. - const forceRebuild = segment.spansToNextSpread === true && segment.spanningPlanPrepared !== true; - const texturePlan = this.textureRenderer.prepareRevealBlock(revealDetail, { - publishEvent: false, - forceRebuild - }); + // Reuse the spanning-aware plan prepared during lookahead — its timing already spans + // both pages. No synchronous redraw on the critical path. + const texturePlan = this.textureRenderer.prepareRevealBlock(revealDetail, { publishEvent: false }); segment.activeTexturePlan = texturePlan; this.applyTexturePlan(texturePlan, segment, 'activate'); await this.assertSegmentReady(segment, 'activate'); @@ -510,11 +494,9 @@ class BookPlaybackTimelineModule extends BaseModule { async revealContinuationSpread(segment = {}, spread = null) { const sentence = segment.sentence; if (!sentence || !spread) return false; - // Reuse the continuation plan prepared during lookahead (no synchronous redraw on the - // critical path). Falls back to rebuilding the plan if none was prepared. - const reused = this.textureRenderer.takeContinuationRevealPlan?.(segment.blockId, spread.index); - const texturePlan = reused - || this.textureRenderer.prepareRevealBlock(this.createRevealDetail(sentence, spread, 'activate'), { publishEvent: false }); + // Reuse the continuation plan prepared during lookahead. It is always prepared when a + // block spans (createPreparedSegment), so a miss is a real bug, not a redraw cue. + const texturePlan = this.textureRenderer.takeContinuationRevealPlan(segment.blockId, spread.index); if (!texturePlan) { this.pageCache?.recordProblem?.({ type: 'timeline-reveal-continuation-missing', diff --git a/public/js/book-texture-renderer-module.js b/public/js/book-texture-renderer-module.js index d19ca8c..fbba799 100644 --- a/public/js/book-texture-renderer-module.js +++ b/public/js/book-texture-renderer-module.js @@ -893,10 +893,9 @@ class BookTextureRendererModule extends BaseModule { wordTimingCount: wordTimings.length, phase }); - // forceRebuild: the cached plan was built before the block's continuation was - // committed (it would be right-only). Discard it and redraw from current spreads. - if (options.forceRebuild === true) this.pageCache?.takePreparedRevealPlan?.(id); - if (phase === 'activate' && options.forceRebuild !== true && this.pageCache?.hasPreparedRevealPlan?.(id)) { + // At activate, reuse the plan prepared during lookahead (it is spanning-aware when the + // block overflows). Building only happens when no plan was prepared yet. + if (phase === 'activate' && this.pageCache?.hasPreparedRevealPlan?.(id)) { const cached = this.pageCache.takePreparedRevealPlan(id); this.activeAnimations.set(id, this.createAnimationState(blockId, wordTimings, detail)); this.publishPreparedReveal(cached, options); @@ -918,8 +917,7 @@ class BookTextureRendererModule extends BaseModule { const sides = ['left', 'right']; // When the caller supplies the (not-yet-committed) preview spreads for a spanning // block, derive this spread's reveal timing across all of them so the cached plan - // already spans both pages. activate can then reuse it instead of forcing a - // synchronous rebuild on the critical path. + // already spans both pages, letting activate reuse it directly. const spanningPreview = Array.isArray(detail.previewSpreads) && detail.previewSpreads.length > 1; const previousOverride = this.revealSpreadSourceOverride; if (spanningPreview) this.revealSpreadSourceOverride = detail.previewSpreads; @@ -938,8 +936,7 @@ class BookTextureRendererModule extends BaseModule { ...published, blockId, wordTimings, - totalDuration: detail.totalDuration || 0, - spanningTimingPrepared: spanningPreview === true + totalDuration: detail.totalDuration || 0 }); } this.markPipelineTiming('prepareRevealBlock:end', { diff --git a/scripts/check-webgl-book-lab.js b/scripts/check-webgl-book-lab.js index 5cff953..23994b7 100644 --- a/scripts/check-webgl-book-lab.js +++ b/scripts/check-webgl-book-lab.js @@ -226,7 +226,7 @@ const checks = [ ['texture renderer derives reveal regions from the just-drawn preview spread before committed pagination', /if \(this\.currentSpread\) sourceSpreads\.push\(this\.currentSpread\)/.test(textureRendererSource) && /paginationSpreads\.forEach/.test(textureRendererSource) && /Number\(spread\.index\) === Number\(this\.currentSpread\.index\)/.test(textureRendererSource)], ['texture renderer prepares a spanning block continuation spread in the background and reuses it (no synchronous redraw on the critical path)', /revealSpreadSourceOverride/.test(textureRendererSource) && /prepareContinuationRevealPlan/.test(textureRendererSource) && /takeContinuationRevealPlan/.test(textureRendererSource) && /`\$\{id\}:cont`/.test(textureRendererSource) && /prepareContinuationRevealPlan/.test(bookPlaybackTimelineSource) && /takeContinuationRevealPlan/.test(bookPlaybackTimelineSource) && /previewSpreads/.test(bookPaginationSource)], ['texture renderer preloads every spread touched by an active reveal block', /preloadAdditionalRevealSpreads/.test(textureRendererSource) && /spreadContainsBlock/.test(textureRendererSource) && /this\.drawSpread\(spread, \['left', 'right'\], \{ phase: 'prepare' \}\)/.test(textureRendererSource)], - ['book playback timeline reuses the spanning-aware prepared plan at activate instead of a synchronous forceRebuild', /spanningPlanPrepared/.test(bookPlaybackTimelineSource) && /forceRebuild = segment\.spansToNextSpread === true && segment\.spanningPlanPrepared !== true/.test(bookPlaybackTimelineSource) && /spanningTimingPrepared/.test(textureRendererSource) && /this\.revealSpreadSourceOverride = detail\.previewSpreads/.test(textureRendererSource)], + ['book playback timeline has one prepare path: spanning-aware plan reused at activate and continuation, no fallback', /this\.revealSpreadSourceOverride = detail\.previewSpreads/.test(textureRendererSource) && !/forceRebuild/.test(textureRendererSource) && !/forceRebuild/.test(bookPlaybackTimelineSource) && !/spanningPlanPrepared/.test(bookPlaybackTimelineSource) && /const texturePlan = this\.textureRenderer\.takeContinuationRevealPlan\(segment\.blockId, spread\.index\)/.test(bookPlaybackTimelineSource)], ['webgl visible spread is owned by scene flips, not pagination publishes', /spreadUpdate:state-only/.test(source) && /webglBookPlaybackActive/.test(source) && /spreadUpdate:jump/.test(source) && /window\.BookTextureRenderer\?\.drawSpread\?\.\(spread, \['left', 'right'\], \{ force: true \}\)/.test(source)], ['3D overflow reveal preloads target spread before forced page flip', /createRevealDetail\(sentence, previewSpread, 'prepare'\)/.test(bookPlaybackTimelineSource) && /this\.textureRenderer\.prepareRevealBlock\(\s*[\s\S]*revealDetail[\s\S]*phase: 'prepare'[\s\S]*publishEvent: false/.test(bookPlaybackTimelineSource) && /this\.prewarmSegmentTextures\(segment\)/.test(bookPlaybackTimelineSource) && /this\.assertSegmentReady\(segment, 'prepare'\)/.test(bookPlaybackTimelineSource) && /reason: 'timeline-preplay-spread-transition'/.test(bookPlaybackTimelineSource)], ['book playback timeline is loaded through module infrastructure', /book-playback-timeline-module\.js/.test(loaderSource) && /super\('book-playback-timeline'/.test(bookPlaybackTimelineSource) && /reportProgress\(100, 'Book playback timeline ready'\)/.test(bookPlaybackTimelineSource)],