Fix WebGL page cache and flip sequencing
This commit is contained in:
+136
-25
@@ -157,7 +157,7 @@ updateCameraRig(0);
|
||||
configureScenePostprocessing();
|
||||
|
||||
const clock = new THREE.Clock();
|
||||
const targetFrameDurationMs = 1000 / 30;
|
||||
const targetFrameDurationMs = 1000 / 60;
|
||||
let lastRenderFrameAt = 0;
|
||||
let fpsDisplay = null;
|
||||
let fpsWindowStartedAt = performance.now();
|
||||
@@ -253,13 +253,15 @@ const preparedPageTextures = {
|
||||
right: new Map()
|
||||
};
|
||||
const residentPageTextures = new Map();
|
||||
const maxResidentPageTextures = 18;
|
||||
const maxResidentPageTextures = 192;
|
||||
let blankPageTexture = null;
|
||||
const pageCacheProblemLog = [];
|
||||
let currentPageMeta = {
|
||||
left: null,
|
||||
right: null
|
||||
};
|
||||
let pendingRightPageFlip = false;
|
||||
let pendingRightPageFlipAutoplay = false;
|
||||
const pageRevealState = {
|
||||
left: null,
|
||||
right: null
|
||||
@@ -575,6 +577,16 @@ window.BookLabDebug = {
|
||||
debug: getPageTextureDebugState()
|
||||
};
|
||||
},
|
||||
getRuntimeInvariants() {
|
||||
return {
|
||||
targetFrameDurationMs,
|
||||
residentPageTextureCount: residentPageTextures.size,
|
||||
maxResidentPageTextures,
|
||||
pageCacheProblemCount: pageCacheProblemLog.length,
|
||||
flipFrontBackShareMaterial: materials.flipPageSurface === materials.flipPageBackSurface,
|
||||
mirrorRefreshesEveryFrame: true
|
||||
};
|
||||
},
|
||||
projectPointerToPage(clientX, clientY) {
|
||||
return projectPointerToPage(clientX, clientY);
|
||||
},
|
||||
@@ -596,6 +608,9 @@ document.addEventListener('webgl-book:page-reveal-fast-forward', (event) => {
|
||||
document.addEventListener('webgl-book:reveal-committed', (event) => {
|
||||
handleRevealCommittedForPageFlip(event.detail || {});
|
||||
});
|
||||
document.addEventListener('webgl-book:page-cache-problem', (event) => {
|
||||
recordPageCacheProblem(event.detail || {});
|
||||
});
|
||||
document.addEventListener('book-pagination:spread-updated', (event) => {
|
||||
const detail = event.detail || {};
|
||||
const previousPageCount = bookPageCount;
|
||||
@@ -611,6 +626,7 @@ document.addEventListener('book-pagination:spread-updated', (event) => {
|
||||
window.WebGLBookPreferenceBridge?.updatePageCount?.(bookPageCount);
|
||||
}
|
||||
syncBookControls();
|
||||
if (pendingRightPageFlip) tryStartPendingRightPageFlip('spread-updated');
|
||||
});
|
||||
document.addEventListener('webgl-book:page-reserve-directive', (event) => {
|
||||
const detail = event.detail || {};
|
||||
@@ -634,8 +650,7 @@ document.addEventListener('webgl-book:request-page-flip', (event) => {
|
||||
});
|
||||
document.addEventListener('ui:command', (event) => {
|
||||
if (event.detail?.type === 'continue' && pendingRightPageFlip) {
|
||||
pendingRightPageFlip = false;
|
||||
startPageFlip(1);
|
||||
tryStartPendingRightPageFlip('continue', { force: true });
|
||||
}
|
||||
});
|
||||
installBookControls();
|
||||
@@ -1976,9 +1991,11 @@ function syncBottomNavigation() {
|
||||
function handlePageCanvases(event) {
|
||||
const detail = event.detail || {};
|
||||
if (detail.pageMeta) {
|
||||
const hasLeftMeta = Object.prototype.hasOwnProperty.call(detail.pageMeta, 'left');
|
||||
const hasRightMeta = Object.prototype.hasOwnProperty.call(detail.pageMeta, 'right');
|
||||
currentPageMeta = {
|
||||
left: detail.pageMeta.left || currentPageMeta.left || null,
|
||||
right: detail.pageMeta.right || currentPageMeta.right || null
|
||||
left: hasLeftMeta ? detail.pageMeta.left : currentPageMeta.left || null,
|
||||
right: hasRightMeta ? detail.pageMeta.right : currentPageMeta.right || null
|
||||
};
|
||||
}
|
||||
markPageTextureTiming('handlePageCanvases:start', {
|
||||
@@ -1989,8 +2006,14 @@ function handlePageCanvases(event) {
|
||||
pageMeta: currentPageMeta
|
||||
});
|
||||
if (detail.preloadOnly) {
|
||||
if (detail.left) preloadPageTexture('left', detail.left, detail.reveal?.left);
|
||||
if (detail.right) preloadPageTexture('right', detail.right, detail.reveal?.right);
|
||||
if (detail.left) {
|
||||
const texture = preloadPageTexture('left', detail.left, detail.reveal?.left);
|
||||
rememberResidentPageTexture(currentPageMeta.left, texture, detail.left);
|
||||
}
|
||||
if (detail.right) {
|
||||
const texture = preloadPageTexture('right', detail.right, detail.reveal?.right);
|
||||
rememberResidentPageTexture(currentPageMeta.right, texture, detail.right);
|
||||
}
|
||||
markPageTextureTiming('handlePageCanvases:preloadOnly:end');
|
||||
return;
|
||||
}
|
||||
@@ -2041,7 +2064,7 @@ function preloadPageTexture(side, sourceCanvas, revealDetail = {}) {
|
||||
revealDetail,
|
||||
uploadedAt: performance.now()
|
||||
});
|
||||
if (preparedPageTextures[side].size > 12) {
|
||||
if (preparedPageTextures[side].size > 128) {
|
||||
const oldestKey = preparedPageTextures[side].keys().next().value;
|
||||
const oldest = preparedPageTextures[side].get(oldestKey);
|
||||
oldest?.texture?.dispose?.();
|
||||
@@ -2052,6 +2075,54 @@ function preloadPageTexture(side, sourceCanvas, revealDetail = {}) {
|
||||
return texture;
|
||||
}
|
||||
|
||||
function recordPageCacheProblem(detail = {}) {
|
||||
const entry = {
|
||||
...detail,
|
||||
at: performance.now()
|
||||
};
|
||||
pageCacheProblemLog.push(entry);
|
||||
if (pageCacheProblemLog.length > 80) pageCacheProblemLog.splice(0, pageCacheProblemLog.length - 80);
|
||||
document.documentElement.dataset.webglPageCacheProblems = JSON.stringify(pageCacheProblemLog);
|
||||
console.warn('WebGL page cache problem', entry);
|
||||
}
|
||||
|
||||
function rememberResidentPageTexture(pageMeta = null, texture = null, sourceCanvas = null, ownsTexture = true) {
|
||||
const pageIndex = Number(pageMeta?.pageIndex);
|
||||
if (!texture || !Number.isFinite(pageIndex) || pageIndex < 0) return null;
|
||||
const key = makePageMetaForCache(pageIndex).pageIndex;
|
||||
const existing = residentPageTextures.get(key);
|
||||
if (isOlderPageTextureMeta(pageMeta, existing?.pageMeta)) return existing?.texture || null;
|
||||
if (existing?.ownsTexture && existing.texture && existing.texture !== texture) existing.texture.dispose?.();
|
||||
residentPageTextures.set(key, {
|
||||
texture,
|
||||
sourceCanvas: sourceCanvas || existing?.sourceCanvas || null,
|
||||
lastUsedAt: performance.now(),
|
||||
ownsTexture,
|
||||
pageMeta: {
|
||||
...(existing?.pageMeta || {}),
|
||||
...(pageMeta || {})
|
||||
}
|
||||
});
|
||||
while (residentPageTextures.size > maxResidentPageTextures) {
|
||||
const oldestKey = residentPageTextures.keys().next().value;
|
||||
const oldest = residentPageTextures.get(oldestKey);
|
||||
if (oldest?.ownsTexture) oldest.texture?.dispose?.();
|
||||
residentPageTextures.delete(oldestKey);
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
function isOlderPageTextureMeta(incoming = {}, existing = null) {
|
||||
if (!existing) return false;
|
||||
const incomingCompleteness = Math.max(0, Number(incoming?.completenessScore || 0));
|
||||
const existingCompleteness = Math.max(0, Number(existing?.completenessScore || 0));
|
||||
if (incomingCompleteness < existingCompleteness) return true;
|
||||
if (incomingCompleteness > existingCompleteness) return false;
|
||||
const incomingVersion = Math.max(0, Number(incoming?.contentVersion || 0));
|
||||
const existingVersion = Math.max(0, Number(existing?.contentVersion || 0));
|
||||
return incomingVersion > 0 && existingVersion > incomingVersion;
|
||||
}
|
||||
|
||||
function makePageMetaForCache(pageIndex) {
|
||||
return {
|
||||
pageIndex: Math.max(0, Math.round(Number(pageIndex || 0))),
|
||||
@@ -2078,6 +2149,15 @@ function getResidentPageTexture(pageIndex) {
|
||||
return resident.texture || null;
|
||||
}
|
||||
|
||||
function getResidentPageTextureForMeta(pageMeta = null) {
|
||||
const pageIndex = Number(pageMeta?.pageIndex);
|
||||
if (!Number.isFinite(pageIndex)) return null;
|
||||
const key = makePageMetaForCache(pageIndex).pageIndex;
|
||||
const resident = residentPageTextures.get(key);
|
||||
if (!resident || isOlderPageTextureMeta(pageMeta, resident.pageMeta)) return null;
|
||||
return getResidentPageTexture(pageIndex);
|
||||
}
|
||||
|
||||
async function preloadCachedPageTexture(pageIndex) {
|
||||
const meta = makePageMetaForCache(pageIndex);
|
||||
if (residentPageTextures.has(meta.pageIndex)) {
|
||||
@@ -2086,17 +2166,28 @@ async function preloadCachedPageTexture(pageIndex) {
|
||||
}
|
||||
const cache = window.WebGLPageCache || window.moduleRegistry?.getModule?.('webgl-page-cache') || null;
|
||||
const sourceCanvas = await cache?.getPageCanvas?.(meta);
|
||||
if (!sourceCanvas) return null;
|
||||
if (!sourceCanvas) {
|
||||
recordPageCacheProblem({
|
||||
type: 'db-cache-miss',
|
||||
pageIndex: meta.pageIndex,
|
||||
width: meta.width,
|
||||
height: meta.height
|
||||
});
|
||||
return null;
|
||||
}
|
||||
const texture = createPageCanvasTexture(sourceCanvas);
|
||||
const cachedMeta = sourceCanvas.__webglPageCacheMeta || meta;
|
||||
residentPageTextures.set(meta.pageIndex, {
|
||||
texture,
|
||||
sourceCanvas,
|
||||
lastUsedAt: performance.now()
|
||||
lastUsedAt: performance.now(),
|
||||
ownsTexture: true,
|
||||
pageMeta: cachedMeta
|
||||
});
|
||||
while (residentPageTextures.size > maxResidentPageTextures) {
|
||||
const oldestKey = residentPageTextures.keys().next().value;
|
||||
const oldest = residentPageTextures.get(oldestKey);
|
||||
oldest?.texture?.dispose?.();
|
||||
if (oldest?.ownsTexture) oldest.texture?.dispose?.();
|
||||
residentPageTextures.delete(oldestKey);
|
||||
}
|
||||
return texture;
|
||||
@@ -2134,7 +2225,7 @@ function uploadPageTextureDirect(side, sourceCanvas, pageMeta = null) {
|
||||
const texture = side === 'left' ? leftTexture : rightTexture;
|
||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||
const residentTexture = Number.isFinite(Number(pageMeta?.pageIndex))
|
||||
? getResidentPageTexture(pageMeta.pageIndex)
|
||||
? getResidentPageTextureForMeta(pageMeta)
|
||||
: null;
|
||||
markPageTextureTiming('directUpload:start', {
|
||||
side,
|
||||
@@ -2155,6 +2246,7 @@ function uploadPageTextureDirect(side, sourceCanvas, pageMeta = null) {
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
bindPageTextureSource(side, texture, sourceCanvas);
|
||||
rememberResidentPageTexture(pageMeta, texture, sourceCanvas, false);
|
||||
markPageTextureTiming('directUpload:end', { side });
|
||||
}
|
||||
|
||||
@@ -2516,10 +2608,11 @@ async function startPageFlip(direction, options = {}) {
|
||||
function startPageFlipPrepared(direction, options = {}) {
|
||||
if (activeFlips.length || !currentProceduralBookModel) return false;
|
||||
if (!options.force && !canPageFlip(direction)) return false;
|
||||
pendingRightPageFlip = false;
|
||||
delete document.documentElement.dataset.webglPendingPageFlip;
|
||||
const flip = createPageFlip(direction, performance.now(), normalFlipDuration);
|
||||
if (!flip) return false;
|
||||
pendingRightPageFlip = false;
|
||||
pendingRightPageFlipAutoplay = false;
|
||||
delete document.documentElement.dataset.webglPendingPageFlip;
|
||||
flip.targetSpread = Number.isFinite(Number(options.targetSpread)) ? Math.max(0, Math.round(Number(options.targetSpread))) : null;
|
||||
prepareStaticPageForFlip(flip);
|
||||
activeFlips.push(flip);
|
||||
@@ -2586,14 +2679,13 @@ function createPageFlip(direction, startTime, duration) {
|
||||
function prepareStaticPageForFlip(flip) {
|
||||
if (!flip) return;
|
||||
const sourceMaterial = flip.sourcePageSide === 'left' ? materials.leftPage : materials.rightPage;
|
||||
const oppositeMaterial = flip.sourcePageSide === 'left' ? materials.rightPage : materials.leftPage;
|
||||
const sourceTexture = sourceMaterial?.map || (flip.sourcePageSide === 'left' ? leftTexture : rightTexture);
|
||||
const targetSpread = Number.isFinite(Number(flip.targetSpread))
|
||||
? Math.max(0, Math.round(Number(flip.targetSpread)))
|
||||
: Math.max(0, Math.round(Number(bookPaginationState.spreadIndex || 0)) + Math.sign(Number(flip.direction || 0)));
|
||||
const targetPages = spreadPageIndices(targetSpread);
|
||||
const targetBackPageIndex = flip.direction > 0 ? targetPages.left : targetPages.right;
|
||||
const backTexture = getResidentPageTexture(targetBackPageIndex) || oppositeMaterial?.map || getBlankPageTexture();
|
||||
const backTexture = getResidentPageTexture(targetBackPageIndex) || getBlankPageTexture();
|
||||
materials.flipPageSurface.map = sourceTexture;
|
||||
materials.flipPageBackSurface.map = backTexture;
|
||||
materials.flipPageSurface.normalMap = materials.pageSurface.normalMap;
|
||||
@@ -2604,6 +2696,7 @@ function prepareStaticPageForFlip(flip) {
|
||||
materials.flipPageBackSurface.needsUpdate = true;
|
||||
flip.sourceTexture = sourceTexture;
|
||||
flip.backTexture = backTexture;
|
||||
flip.targetBackPageIndex = targetBackPageIndex;
|
||||
if (flip.direction > 0) {
|
||||
const blankTexture = getBlankPageTexture();
|
||||
if (blankTexture && materials.rightPage.map !== blankTexture) {
|
||||
@@ -2633,12 +2726,30 @@ function handleRevealCommittedForPageFlip(detail = {}) {
|
||||
if (detail.side !== 'right' || !isRightBodyPageComplete()) return;
|
||||
if (activeFlips.length > 0 || pendingRightPageFlip) return;
|
||||
if (isChoiceAwaitingPlayer()) return;
|
||||
if (isTtsPlaybackActive()) {
|
||||
startPageFlip(1);
|
||||
return;
|
||||
}
|
||||
const autoplayFlip = isTtsPlaybackActive();
|
||||
pendingRightPageFlip = true;
|
||||
pendingRightPageFlipAutoplay = autoplayFlip;
|
||||
document.documentElement.dataset.webglPendingPageFlip = 'right';
|
||||
if (autoplayFlip) {
|
||||
tryStartPendingRightPageFlip('tts-active');
|
||||
}
|
||||
}
|
||||
|
||||
async function tryStartPendingRightPageFlip(reason = 'pending', options = {}) {
|
||||
if (!pendingRightPageFlip || activeFlips.length > 0 || isChoiceAwaitingPlayer()) return false;
|
||||
if (!options.force && !pendingRightPageFlipAutoplay) return false;
|
||||
const targetSpread = Math.max(0, Math.round(Number(bookPaginationState.spreadIndex || 0)) + 1);
|
||||
const flipped = await startPageFlip(1, {
|
||||
force: options.force === true || pendingRightPageFlipAutoplay,
|
||||
reason,
|
||||
targetSpread
|
||||
});
|
||||
if (flipped) {
|
||||
pendingRightPageFlip = false;
|
||||
pendingRightPageFlipAutoplay = false;
|
||||
delete document.documentElement.dataset.webglPendingPageFlip;
|
||||
}
|
||||
return flipped;
|
||||
}
|
||||
|
||||
function isRightBodyPageComplete() {
|
||||
@@ -2853,7 +2964,7 @@ function createFlippingPageGeometry(surface) {
|
||||
rowPoints.forEach((point, depthIndex) => {
|
||||
const v = depthSegments <= 0 ? 0 : depthIndex / depthSegments;
|
||||
topRow.push(push(point, pageThickness, u, v));
|
||||
bottomRow.push(push(point, 0, u, v));
|
||||
bottomRow.push(push(point, 0, u, 1 - v));
|
||||
});
|
||||
topGrid.push(topRow);
|
||||
bottomGrid.push(bottomRow);
|
||||
@@ -3961,12 +4072,12 @@ function renderMirrorDebugView() {
|
||||
function animate(now = performance.now()) {
|
||||
const elapsedSinceLastFrame = lastRenderFrameAt ? now - lastRenderFrameAt : targetFrameDurationMs;
|
||||
if (lastRenderFrameAt && elapsedSinceLastFrame < targetFrameDurationMs) {
|
||||
setTimeout(animate, Math.max(1, targetFrameDurationMs - elapsedSinceLastFrame));
|
||||
requestAnimationFrame(animate);
|
||||
return;
|
||||
}
|
||||
const frameElapsedMs = lastRenderFrameAt ? elapsedSinceLastFrame : targetFrameDurationMs;
|
||||
lastRenderFrameAt = now;
|
||||
setTimeout(animate, targetFrameDurationMs);
|
||||
requestAnimationFrame(animate);
|
||||
const delta = Math.min(0.1, frameElapsedMs / 1000);
|
||||
clock.getDelta();
|
||||
const t = clock.elapsedTime;
|
||||
@@ -4008,7 +4119,7 @@ function animate(now = performance.now()) {
|
||||
updateBookShadowMaps();
|
||||
lastFrameTiming.shadows = performance.now() - shadowStartedAt;
|
||||
const reflectionStartedAt = performance.now();
|
||||
const refreshStaticSceneBuffers = staticSceneBuffersDirty || activeFlips.length > 0;
|
||||
const refreshStaticSceneBuffers = true;
|
||||
if (refreshStaticSceneBuffers) {
|
||||
updateTableReflection();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user