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:
2026-06-20 00:59:01 +02:00
parent 004c077181
commit 705d1ea6bf
6 changed files with 237 additions and 67 deletions
+55 -24
View File
@@ -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)))
+80 -20
View File
@@ -7,6 +7,7 @@ import { BaseModule } from './base-module.js';
const TTS_GENERATION_TIMEOUT_MS = 60000;
const ASSET_PRELOAD_TIMEOUT_MS = 60000;
const USER_CANCEL_BLOCKING_WAIT_MIN_MS = 5000;
const WEBGL_BOOK_PREFETCH_LOOKAHEAD = 2;
class SentenceQueueModule extends BaseModule {
constructor() {
@@ -23,6 +24,7 @@ class SentenceQueueModule extends BaseModule {
// Cache prepared future queue items so the playback path can consume
// work that was already generated during lookahead.
this.prefetchingSpeech = new Map();
this.prefetchingWebGLBook = new Map();
this.preparedSentenceCache = new Map();
this.autoplay = true;
this.inputMode = 'text';
@@ -33,6 +35,7 @@ class SentenceQueueModule extends BaseModule {
this.generationRequests = new Map();
this.assetPreloadRequests = new Map();
this.queueGeneration = 0;
this.webglBookPrepareChain = Promise.resolve();
// Bind methods
this.bindMethods([
@@ -46,7 +49,10 @@ class SentenceQueueModule extends BaseModule {
'getPreparedSentence',
'prefetchAhead',
'prefetchWebGLBookPresentation',
'runWebGLBookPresentationPrepare',
'isWebGLBookPresentationPrepared',
'getWebGLBookPresentationKey',
'isWebGLBookPresentationEligible',
'prepareSpeechMetadata',
'preloadAssetsForItem',
'normalizeTtsText',
@@ -210,18 +216,25 @@ class SentenceQueueModule extends BaseModule {
}
if (!this.isCurrentQueueItem(item, queueGeneration)) return;
// Prefetch far enough ahead that media pauses do not block TTS
// generation for the next spoken paragraph.
this.prefetchAhead(6, queueGeneration);
if (!this.isCurrentQueueItem(item, queueGeneration)) return;
// Notify display handler with complete sentence
if (this.onSentenceReadyCallback) {
await new Promise(resolve => {
const playbackFinished = new Promise(resolve => {
sentence.onComplete = resolve;
sentence.playbackStartedAt = performance.now();
this.onSentenceReadyCallback(sentence, resolve);
});
// Start lookahead only after the current sentence has entered the display
// pipeline. This keeps future WebGL book preparation out of the first
// flip/reveal critical path while still overlapping it with playback.
window.requestAnimationFrame(() => {
if (this.isCurrentQueueItem(item, queueGeneration)) {
this.prefetchAhead(6, queueGeneration);
}
});
await playbackFinished;
if (!this.isCurrentQueueItem(item, queueGeneration)) return;
} else {
this.prefetchAhead(6, queueGeneration);
if (!this.isCurrentQueueItem(item, queueGeneration)) return;
}
@@ -890,12 +903,42 @@ class SentenceQueueModule extends BaseModule {
return this.prepareSentence(item);
}
getWebGLBookPresentationKey(sentence = {}) {
const blockId = sentence.blockId ?? sentence.metadata?.blockId ?? null;
if (blockId == null) return null;
return `${sentence.gameId || sentence.metadata?.gameId || 'game'}:${blockId}`;
}
isWebGLBookPresentationEligible(sentence = {}) {
if (!sentence) return false;
return ['paragraph', 'heading'].includes(sentence.kind || sentence.type);
}
async prefetchWebGLBookPresentation(sentence, options = {}) {
if (!sentence || !['paragraph', 'heading'].includes(sentence.kind || sentence.type)) return null;
if (!this.isWebGLBookPresentationEligible(sentence)) return null;
const isWebGLMode = document.body?.dataset?.webglUiMode === '3d'
|| document.body?.classList?.contains('webgl-mode');
if (!isWebGLMode) return null;
const key = this.getWebGLBookPresentationKey(sentence);
if (!key) return null;
const existing = this.prefetchingWebGLBook.get(key);
if (existing) return existing;
const queued = this.webglBookPrepareChain
.catch(() => null)
.then(() => this.runWebGLBookPresentationPrepare(sentence, options));
this.webglBookPrepareChain = queued.catch(() => null);
this.prefetchingWebGLBook.set(key, queued);
return queued.finally(() => {
if (this.prefetchingWebGLBook.get(key) === queued) {
this.prefetchingWebGLBook.delete(key);
}
});
}
async runWebGLBookPresentationPrepare(sentence, options = {}) {
if (!this.isWebGLBookPresentationEligible(sentence)) return null;
const blockId = sentence.blockId ?? sentence.metadata?.blockId ?? null;
if (blockId == null) return null;
const bookPlaybackTimeline = this.getModule('book-playback-timeline');
@@ -912,6 +955,7 @@ class SentenceQueueModule extends BaseModule {
const segment = await bookPlaybackTimeline.prepareSentence(sentence, {
immediate: options.immediate === true
});
if (options.queueGeneration !== undefined && options.queueGeneration !== this.queueGeneration) return null;
if (!segment) return null;
sentence.webglBookPresentation = {
prepared: true,
@@ -944,14 +988,33 @@ class SentenceQueueModule extends BaseModule {
}
let started = 0;
let spokenPrepared = 0;
let webglBookLookahead = 0;
const limit = Math.min(this.sentenceQueue.length, maxLookahead + 1);
const allowWebGLBookPrefetch = document.documentElement.dataset.webglBookPlaybackActive === 'true';
for (let index = 1; index < limit; index += 1) {
const nextItem = this.sentenceQueue[index];
const nextCacheKey = this.getCacheKey(nextItem);
const cachedPrepared = this.preparedSentenceCache.get(nextCacheKey);
const webglBookCandidate = this.isWebGLBookPresentationEligible(cachedPrepared || nextItem);
const shouldPrepareWebGLBook = allowWebGLBookPrefetch
&& webglBookCandidate
&& webglBookLookahead < WEBGL_BOOK_PREFETCH_LOOKAHEAD;
if (webglBookCandidate) webglBookLookahead += 1;
if (cachedPrepared && !this.prefetchingSpeech.has(nextCacheKey)) {
if (shouldPrepareWebGLBook && !this.isWebGLBookPresentationPrepared(cachedPrepared)) {
this.prefetchWebGLBookPresentation(cachedPrepared, {
queueGeneration,
queueIndex: index
}).catch(err => {
console.warn('SentenceQueue: WebGL book prefetch failed:', err);
});
}
continue;
}
if (this.prefetchingSpeech.has(nextCacheKey)) {
if (this.isSpeechItem(nextItem)) spokenPrepared += 1;
continue;
}
@@ -969,10 +1032,12 @@ class SentenceQueueModule extends BaseModule {
queueIndex: index
});
if (queueGeneration !== this.queueGeneration) return null;
await this.prefetchWebGLBookPresentation(prepared, {
queueGeneration,
queueIndex: index
});
if (shouldPrepareWebGLBook) {
await this.prefetchWebGLBookPresentation(prepared, {
queueGeneration,
queueIndex: index
});
}
if (queueGeneration !== this.queueGeneration) return null;
this.preparedSentenceCache.set(nextCacheKey, prepared);
return prepared;
@@ -997,13 +1062,6 @@ class SentenceQueueModule extends BaseModule {
this.prefetchingSpeech.set(nextCacheKey, promise);
started += 1;
if (this.isSpeechItem(nextItem)) {
spokenPrepared += 1;
}
if (spokenPrepared >= 1 && started >= 2) {
break;
}
}
if (started === 0) {
@@ -1409,7 +1467,9 @@ class SentenceQueueModule extends BaseModule {
this.cancelGenerationRequests('sentence-queue-cleared');
this.cancelAssetPreloads('sentence-queue-cleared');
this.prefetchingSpeech.clear();
this.prefetchingWebGLBook.clear();
this.preparedSentenceCache.clear();
this.webglBookPrepareChain = Promise.resolve();
this.pauseBeforeNextReason = null;
document.dispatchEvent(new CustomEvent('tts:queue-empty', {
detail: { reason: 'sentence-queue-cleared' }
+3 -2
View File
@@ -1023,9 +1023,10 @@ class UIDisplayHandlerModule extends BaseModule {
this.revealImageBlock(element);
} else if (sentence.kind === 'paragraph' || sentence.kind === 'heading') {
if (useWebGLBookReveal) {
await this.prepareWebGLBookReveal(sentence);
await this.playWebGLBookSentence(sentence);
} else {
await this.playbackCoordinator.play(sentence);
}
await this.playbackCoordinator.play(sentence);
if (useWebGLBookReveal && sentence.blockId != null) {
this.markBlockRendered(sentence.blockId);
}
+83 -14
View File
@@ -140,6 +140,7 @@ const dynamicBufferRefreshIntervalMs = 1000 / 30;
// frames are just the cheap scene render and hold 60fps. Candle flicker is the only thing
// changing them then, which 8Hz captures imperceptibly.
const staticGeometryBufferRefreshIntervalMs = 1000 / 8;
const revealGeometryBufferRefreshIntervalMs = 1000 / 4;
const flipDynamicBufferGraceMs = 180;
let lastBookShadowRefreshAt = -Infinity;
let lastTableReflectionRefreshAt = -Infinity;
@@ -184,6 +185,8 @@ const lastFrameTiming = {};
const slowFrameLog = [];
const loaderTimings = {};
const pageTextureTimings = [];
let queuedNavigationPrewarm = null;
let queuedNavigationPrewarmHandle = null;
function markLoaderTiming(name) {
loaderTimings[name] = performance.now();
@@ -385,6 +388,10 @@ const materials = {
}),
flipPageSurface: new THREE.MeshStandardMaterial({
color: 0xeee6cc,
map: getBlankPageTexture(),
normalMap: paperTextures.normal,
normalScale: new THREE.Vector2(0.004, 0.004),
roughnessMap: paperTextures.roughness,
roughness: 0.92,
metalness: 0,
emissive: 0x100d08,
@@ -440,6 +447,8 @@ const materials = {
};
materials.flipPageBackSurface = materials.flipPageSurface.clone();
materials.flipPageBackSurface.map = getBlankPageTexture();
materials.flipPageBackSurface.normalMap = paperTextures.normal;
materials.flipPageBackSurface.roughnessMap = paperTextures.roughness;
materials.flipPageBackSurface.side = THREE.FrontSide;
materials.flipPageEdge = materials.pageSurface.clone();
materials.flipPageEdge.map = paperTextures.edge;
@@ -622,6 +631,15 @@ window.BookLabDebug = {
requestPageFlip(direction = 1, options = {}) {
return startPageFlip(direction, options);
},
async prewarmPageFlip(direction = 1, options = {}) {
const targetSpread = Number.isFinite(Number(options.targetSpread))
? Math.max(0, Math.round(Number(options.targetSpread)))
: null;
return prewarmFlipTextures(direction, targetSpread);
},
startPreparedPageFlip(direction = 1, options = {}) {
return startPageFlipPrepared(direction, options);
},
getRevealDebugState() {
return getRevealDebugState();
},
@@ -680,7 +698,9 @@ if (webglBookSceneModule) {
setPageReserve: (value) => setPageReserve(value),
setMaxVisitedPagePosition: (value) => window.BookLabDebug.setMaxVisitedPagePosition(value),
redrawPageTextures: () => window.BookLabDebug.redrawPageTextures(),
projectPointerToPage: (clientX, clientY) => projectPointerToPage(clientX, clientY)
projectPointerToPage: (clientX, clientY) => projectPointerToPage(clientX, clientY),
prewarmPageFlip: (direction = 1, options = {}) => window.BookLabDebug.prewarmPageFlip(direction, options),
startPreparedPageFlip: (direction = 1, options = {}) => window.BookLabDebug.startPreparedPageFlip(direction, options)
};
}
@@ -716,6 +736,15 @@ document.addEventListener('story:client-reset', () => {
pageRevealFreezeAt = null;
clearPageReveal('left', 'client-reset');
clearPageReveal('right', 'client-reset');
// Return the book to the title spread so the new game's first block flips in from the
// title page. Otherwise the view stays on the previous game's spread, the segment's
// source and target spread match, and the title->content page turn is skipped.
if (Math.max(0, Math.round(Number(bookPaginationState.spreadIndex || 0))) !== 0) {
bookPaginationState = { ...bookPaginationState, spreadIndex: 0 };
const titleSpread = getPaginationSpread(0);
if (titleSpread) window.BookTextureRenderer?.drawSpread?.(titleSpread, ['left', 'right'], { force: true });
syncBookControls();
}
});
// Pagination spread updates only carry state. The playback owner decides when the
// visible spread changes (via flips). The scene jumps directly only for non-playback
@@ -2232,12 +2261,7 @@ function handlePageTextureRecords(event) {
source: 'book-texture-renderer'
});
markPageTextureTiming('handlePageTextureRecords:end');
prewarmNavigationTextureWindow('page-texture-records', { recordMiss: false }).catch((error) => {
pageTextureStore?.recordProblem?.({
type: 'navigation-window-prewarm-error',
message: error?.message || String(error)
});
});
scheduleNavigationTextureWindowPrewarm('page-texture-records', { recordMiss: false });
}
function normalizePageTextureRecordDetail(detail = {}) {
@@ -2441,6 +2465,35 @@ async function prewarmNavigationTextureWindow(reason = 'navigation-window', opti
return result || {};
}
function scheduleNavigationTextureWindowPrewarm(reason = 'navigation-window', options = {}) {
queuedNavigationPrewarm = {
reason,
options: { ...(options || {}) }
};
if (queuedNavigationPrewarmHandle !== null) return;
const run = () => {
queuedNavigationPrewarmHandle = null;
const queued = queuedNavigationPrewarm;
queuedNavigationPrewarm = null;
if (!queued) return;
if (activeFlips.length > 0 || hasActivePageReveal()) {
scheduleNavigationTextureWindowPrewarm(queued.reason, queued.options);
return;
}
prewarmNavigationTextureWindow(queued.reason, queued.options).catch((error) => {
pageTextureStore?.recordProblem?.({
type: 'navigation-window-prewarm-error',
message: error?.message || String(error)
});
});
};
if (typeof window.requestIdleCallback === 'function') {
queuedNavigationPrewarmHandle = window.requestIdleCallback(run, { timeout: 350 });
} else {
queuedNavigationPrewarmHandle = window.setTimeout(run, 80);
}
}
async function prewarmFlipTextures(direction, targetSpread = null) {
const currentSpread = Math.max(0, Number(bookPaginationState.spreadIndex || 0));
const nextSpread = Number.isFinite(Number(targetSpread))
@@ -2545,10 +2598,7 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
const baseTexture = prepared?.baseTexture || (revealDetail?.baseCanvas ? pageTextureStore?.createTextureFromCanvas?.(revealDetail.baseCanvas) : null);
const revealBlockIds = Array.isArray(revealDetail.blockIds) ? revealDetail.blockIds.map(value => String(value)) : [];
const activeStartedAt = revealBlockIds
.map(blockId => activeRevealBlockStarts.get(blockId))
.filter(value => Number.isFinite(Number(value)))
.sort((a, b) => a - b)[0] ?? null;
const activeStartedAt = getRevealStartTimeForBlockIds(revealBlockIds);
pageRevealState[side] = {
startedAt: activeStartedAt ?? (revealDetail.startNow ? performance.now() : null),
@@ -2600,6 +2650,22 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
markPageTextureTiming('revealUpload:end', { side });
}
function getRevealStartTimeForBlockIds(blockIds = []) {
const startedAt = (Array.isArray(blockIds) ? blockIds : [])
.map(blockId => activeRevealBlockStarts.get(String(blockId)))
.filter(value => Number.isFinite(Number(value)))
.sort((a, b) => a - b)[0] ?? null;
if (startedAt !== null) return startedAt;
const pendingBlockId = (Array.isArray(blockIds) ? blockIds : [])
.map(blockId => String(blockId))
.find(blockId => pendingRevealStartBlockIds.has(blockId));
if (!pendingBlockId) return null;
const now = performance.now();
activeRevealBlockStarts.set(pendingBlockId, now);
pendingRevealStartBlockIds.delete(pendingBlockId);
return now;
}
function applyPendingPageReveal(side, shader = getPageRevealShader(side)) {
const material = side === 'left' ? materials.leftPage : materials.rightPage;
const revealDetail = material?.userData?.pendingPageReveal;
@@ -3141,8 +3207,6 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
materials.flipPageBackSurface.normalMap = materials.pageSurface.normalMap;
materials.flipPageSurface.roughnessMap = materials.pageSurface.roughnessMap;
materials.flipPageBackSurface.roughnessMap = materials.pageSurface.roughnessMap;
materials.flipPageSurface.needsUpdate = true;
materials.flipPageBackSurface.needsUpdate = true;
syncFlipRevealShaderFromSource(sourceSide, materials.flipPageSurface);
syncFlipRevealShaderFromSource(targetBackSide, materials.flipPageBackSurface);
flip.sourceTexture = sourceTexture;
@@ -4733,7 +4797,12 @@ function animate(now = performance.now()) {
: Infinity;
const deferDynamicBuffersForFlipStart = activeFlips.length > 0 && newestFlipAge < flipDynamicBufferGraceMs;
const geometryAnimating = activeFlips.length > 0;
const bufferRefreshIntervalMs = geometryAnimating ? dynamicBufferRefreshIntervalMs : staticGeometryBufferRefreshIntervalMs;
const revealAnimating = hasActivePageReveal();
const bufferRefreshIntervalMs = geometryAnimating
? dynamicBufferRefreshIntervalMs
: revealAnimating
? revealGeometryBufferRefreshIntervalMs
: staticGeometryBufferRefreshIntervalMs;
const shadowRefreshDue = !deferDynamicBuffersForFlipStart && (
forceDynamicBufferRefresh || now - lastBookShadowRefreshAt >= bufferRefreshIntervalMs
);
+3 -3
View File
@@ -365,14 +365,14 @@ class WebGLBookSceneModule extends BaseModule {
async initializeScene() {
if (this.labImportPromise) return this.labImportPromise;
const cacheBuster = window.MODULE_CACHE_BUSTER || Date.now();
this.labImportPromise = import(`/js/webgl-book-lab.js?v=${encodeURIComponent(cacheBuster)}`);
const moduleVersion = window.MODULE_CACHE_BUSTER || 'dev';
this.labImportPromise = import(`/js/webgl-book-lab.js?v=${encodeURIComponent(moduleVersion)}`);
await this.labImportPromise;
this.reportProgress(94, 'Uploading initial book page textures');
const pagination = this.getModule('book-pagination');
const initialSpread = pagination?.getCurrentSpread?.();
if (initialSpread && typeof window.BookTextureRenderer?.drawSpread === 'function') {
window.BookTextureRenderer.drawSpread(initialSpread, ['left', 'right'], { force: true });
await window.BookTextureRenderer.drawSpread(initialSpread, ['left', 'right'], { force: true });
} else {
window.BookTextureRenderer?.publishSpread?.();
}