Checkpoint WebGL book playback refactor state
This commit is contained in:
+244
-220
@@ -213,6 +213,7 @@ let activeFlips = [];
|
||||
let pendingPageFlips = 0;
|
||||
const pendingRevealStartBlockIds = new Set();
|
||||
const activeRevealBlockStarts = new Map();
|
||||
let lastFlipTexturePreflight = null;
|
||||
|
||||
const paperColor = new THREE.Color(0xece4ca);
|
||||
const inkColor = '#1a1009';
|
||||
@@ -246,19 +247,24 @@ function createPageCanvasTexture(sourceCanvas) {
|
||||
}
|
||||
|
||||
function getBlankPageTexture() {
|
||||
if (blankPageTexture) return blankPageTexture;
|
||||
blankPageTexture = createPageCanvasTexture(createPageCanvas('blank'));
|
||||
return blankPageTexture;
|
||||
return pageTextureStore?.getBlankTexture?.() || createPageCanvasTexture(createPageCanvas('blank'));
|
||||
}
|
||||
|
||||
const preparedPageTextures = {
|
||||
left: new Map(),
|
||||
right: new Map()
|
||||
};
|
||||
const residentPageTextures = new Map();
|
||||
const maxResidentPageTextures = 192;
|
||||
let blankPageTexture = null;
|
||||
const pageCacheProblemLog = [];
|
||||
const pageTextureStore = window.moduleRegistry?.getModule?.('webgl-page-cache') || window.WebGLPageCache || null;
|
||||
pageTextureStore?.configureTextureRuntime?.({
|
||||
THREE,
|
||||
renderer,
|
||||
configureTexture: configurePageCanvasTexture,
|
||||
createBlankCanvas: () => createPageCanvas('blank'),
|
||||
maxResidentTextureCount: maxResidentPageTextures,
|
||||
maxPreparedTextureCount: 128
|
||||
});
|
||||
pageTextureStore?.registerVisibleTexture?.('left', leftTexture, leftCanvas);
|
||||
pageTextureStore?.registerVisibleTexture?.('right', rightTexture, rightCanvas);
|
||||
await reportLabStep(50, 'Initializing page texture store VRAM window');
|
||||
pageTextureStore?.getBlankTexture?.();
|
||||
await prewarmNavigationTextureWindow('loader-prime', { recordMiss: false });
|
||||
let currentPageMeta = {
|
||||
left: null,
|
||||
right: null
|
||||
@@ -589,14 +595,18 @@ window.BookLabDebug = {
|
||||
};
|
||||
},
|
||||
getRuntimeInvariants() {
|
||||
const textureStoreState = pageTextureStore?.getRuntimeState?.() || {};
|
||||
return {
|
||||
targetFrameDurationMs,
|
||||
residentPageTextureCount: residentPageTextures.size,
|
||||
residentPageTextureCount: textureStoreState.residentTextureCount || 0,
|
||||
maxResidentPageTextures,
|
||||
pageCacheProblemCount: pageCacheProblemLog.length,
|
||||
pageCacheProblemCount: textureStoreState.problemCount || 0,
|
||||
preparedPageTextureCount: textureStoreState.preparedTextureCount || 0,
|
||||
singlePageTextureStore: true,
|
||||
flipFrontBackShareMaterial: materials.flipPageSurface === materials.flipPageBackSurface,
|
||||
mirrorRefreshesEveryFrame: true,
|
||||
mirrorRefreshesWhenStaticDirty: true
|
||||
mirrorRefreshesWhenStaticDirty: true,
|
||||
lastFlipTexturePreflight
|
||||
};
|
||||
},
|
||||
projectPointerToPage(clientX, clientY) {
|
||||
@@ -610,7 +620,7 @@ window.BookLabDebug = {
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
document.addEventListener('webgl-book:page-canvases', handlePageCanvases);
|
||||
document.addEventListener('webgl-book:page-texture-records', handlePageTextureRecords);
|
||||
document.addEventListener('webgl-book:page-reveal-start', (event) => {
|
||||
startPageRevealForBlock(event.detail?.blockId);
|
||||
});
|
||||
@@ -621,7 +631,7 @@ document.addEventListener('webgl-book:reveal-committed', (event) => {
|
||||
handleRevealCommittedForPageFlip(event.detail || {});
|
||||
});
|
||||
document.addEventListener('webgl-book:page-cache-problem', (event) => {
|
||||
recordPageCacheProblem(event.detail || {});
|
||||
pageTextureStore?.recordProblem?.(event.detail || {});
|
||||
});
|
||||
document.addEventListener('book-pagination:spread-updated', (event) => {
|
||||
const detail = event.detail || {};
|
||||
@@ -630,7 +640,7 @@ document.addEventListener('book-pagination:spread-updated', (event) => {
|
||||
const latestRenderedBlockId = Math.max(0, Number(detail.latestRenderedBlockId || 0));
|
||||
if (
|
||||
latestBlockId > latestRenderedBlockId
|
||||
&& detail.allowFutureUnrendered !== true
|
||||
&& detail.visibility !== 'future-ready'
|
||||
&& activeFlips.length === 0
|
||||
&& incomingSpreadIndex > Math.max(0, Number(bookPaginationState.spreadIndex || 0))
|
||||
) {
|
||||
@@ -2028,35 +2038,34 @@ function syncBottomNavigation() {
|
||||
bottomNavigation.endButton.disabled = activeFlips.length > 0 || currentPage >= navigableLimit;
|
||||
}
|
||||
|
||||
function handlePageCanvases(event) {
|
||||
const detail = event.detail || {};
|
||||
function handlePageTextureRecords(event) {
|
||||
const detail = normalizePageTextureRecordDetail(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: hasLeftMeta ? detail.pageMeta.left : currentPageMeta.left || null,
|
||||
right: hasRightMeta ? detail.pageMeta.right : currentPageMeta.right || null
|
||||
};
|
||||
currentPageMeta = normalizePageMetaPair(detail.pageMeta, currentPageMeta);
|
||||
}
|
||||
markPageTextureTiming('handlePageCanvases:start', {
|
||||
markPageTextureTiming('handlePageTextureRecords:start', {
|
||||
hasLeft: Boolean(detail.left),
|
||||
hasRight: Boolean(detail.right),
|
||||
revealSides: Object.keys(detail.reveal || {}),
|
||||
preloadOnly: Boolean(detail.preloadOnly),
|
||||
phase: detail.phase || 'activate',
|
||||
pageMeta: currentPageMeta
|
||||
});
|
||||
const leftReveal = attachRevealPageMeta(detail.reveal?.left, detail.pageMeta?.left || currentPageMeta.left || null);
|
||||
const rightReveal = attachRevealPageMeta(detail.reveal?.right, detail.pageMeta?.right || currentPageMeta.right || null);
|
||||
if (detail.preloadOnly) {
|
||||
const leftReveal = attachRevealPageMeta(detail.reveal?.left, currentPageMeta.left || null);
|
||||
const rightReveal = attachRevealPageMeta(detail.reveal?.right, currentPageMeta.right || null);
|
||||
if (detail.phase === 'prepare') {
|
||||
if (detail.left) {
|
||||
const texture = preloadPageTexture('left', detail.left, leftReveal, detail.pageMeta?.left || null);
|
||||
rememberResidentPageTexture(detail.pageMeta?.left || null, texture, detail.left);
|
||||
const texture = preloadPageTexture('left', detail.left, leftReveal, currentPageMeta.left);
|
||||
pageTextureStore?.rememberResidentTexture?.(currentPageMeta.left, texture, detail.left, true);
|
||||
} else if (currentPageMeta.left?.kind === 'blank') {
|
||||
pageTextureStore?.rememberResidentTexture?.(currentPageMeta.left, getBlankPageTexture(), null, false);
|
||||
}
|
||||
if (detail.right) {
|
||||
const texture = preloadPageTexture('right', detail.right, rightReveal, detail.pageMeta?.right || null);
|
||||
rememberResidentPageTexture(detail.pageMeta?.right || null, texture, detail.right);
|
||||
const texture = preloadPageTexture('right', detail.right, rightReveal, currentPageMeta.right);
|
||||
pageTextureStore?.rememberResidentTexture?.(currentPageMeta.right, texture, detail.right, true);
|
||||
} else if (currentPageMeta.right?.kind === 'blank') {
|
||||
pageTextureStore?.rememberResidentTexture?.(currentPageMeta.right, getBlankPageTexture(), null, false);
|
||||
}
|
||||
markPageTextureTiming('handlePageCanvases:preloadOnly:end');
|
||||
markPageTextureTiming('handlePageTextureRecords:prepare:end');
|
||||
return;
|
||||
}
|
||||
if (detail.left) {
|
||||
@@ -2073,13 +2082,110 @@ function handlePageCanvases(event) {
|
||||
uploadPageTextureDirect('right', detail.right, currentPageMeta.right);
|
||||
}
|
||||
}
|
||||
if (!detail.left && currentPageMeta.left?.kind === 'blank') {
|
||||
applyExplicitBlankPageTexture('left', currentPageMeta.left, 'page-texture-records');
|
||||
}
|
||||
if (!detail.right && currentPageMeta.right?.kind === 'blank') {
|
||||
applyExplicitBlankPageTexture('right', currentPageMeta.right, 'page-texture-records');
|
||||
}
|
||||
markStaticSceneBuffersDirty();
|
||||
document.documentElement.dataset.webglPageTextureMetrics = JSON.stringify({
|
||||
width: leftCanvas.width,
|
||||
height: leftCanvas.height,
|
||||
source: 'book-texture-renderer'
|
||||
});
|
||||
markPageTextureTiming('handlePageCanvases:end');
|
||||
markPageTextureTiming('handlePageTextureRecords:end');
|
||||
prewarmNavigationTextureWindow('page-texture-records').catch((error) => {
|
||||
pageTextureStore?.recordProblem?.({
|
||||
type: 'navigation-window-prewarm-error',
|
||||
message: error?.message || String(error)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function normalizePageTextureRecordDetail(detail = {}) {
|
||||
if (!Array.isArray(detail.records) || detail.records.length === 0) {
|
||||
return {
|
||||
...detail,
|
||||
phase: detail.phase === 'prepare' ? 'prepare' : 'activate'
|
||||
};
|
||||
}
|
||||
return detail.records.reduce((normalized, record) => {
|
||||
const side = record?.side === 'right' ? 'right' : 'left';
|
||||
normalized[side] = record.canvas || normalized[side] || null;
|
||||
normalized.pageMeta[side] = record.pageMeta || detail.pageMeta?.[side] || normalized.pageMeta[side] || null;
|
||||
if (record.reveal) normalized.reveal[side] = record.reveal;
|
||||
return normalized;
|
||||
}, {
|
||||
metrics: detail.metrics,
|
||||
hitMaps: detail.hitMaps || {},
|
||||
sides: detail.records.map(record => record?.side).filter(Boolean),
|
||||
records: detail.records,
|
||||
reveal: {},
|
||||
pageMeta: {},
|
||||
phase: detail.phase === 'prepare' ? 'prepare' : 'activate',
|
||||
preparedFromCache: detail.preparedFromCache === true
|
||||
});
|
||||
}
|
||||
|
||||
function normalizePageMetaPair(pageMeta = {}, previousMeta = currentPageMeta) {
|
||||
const spreadIndex = getSpreadIndexFromPageMeta(pageMeta)
|
||||
?? getSpreadIndexFromPageMeta(previousMeta)
|
||||
?? Math.max(0, Math.round(Number(bookPaginationState.spreadIndex || 0)));
|
||||
const pageIndices = spreadPageIndices(spreadIndex);
|
||||
return {
|
||||
left: normalizePageSideMeta(pageMeta.left, pageIndices.left, 'left'),
|
||||
right: normalizePageSideMeta(pageMeta.right, pageIndices.right, 'right')
|
||||
};
|
||||
}
|
||||
|
||||
function getSpreadIndexFromPageMeta(pageMeta = {}) {
|
||||
const leftIndex = Number(pageMeta?.left?.pageIndex);
|
||||
if (Number.isFinite(leftIndex)) return Math.floor(Math.max(0, leftIndex) / 2);
|
||||
const rightIndex = Number(pageMeta?.right?.pageIndex);
|
||||
if (Number.isFinite(rightIndex)) return Math.floor(Math.max(0, rightIndex) / 2);
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizePageSideMeta(meta = null, pageIndex = 0, side = 'left') {
|
||||
const index = Math.max(0, Math.round(Number(meta?.pageIndex ?? pageIndex)));
|
||||
if (!meta || meta.kind === 'blank') return makeBlankPageMeta(index, meta?.section || (index < 3 ? 'frontmatter' : 'body'));
|
||||
return {
|
||||
...meta,
|
||||
pageIndex: index,
|
||||
side
|
||||
};
|
||||
}
|
||||
|
||||
function makeBlankPageMeta(pageIndex = 0, section = 'body') {
|
||||
return {
|
||||
kind: 'blank',
|
||||
section,
|
||||
pageIndex: Math.max(0, Math.round(Number(pageIndex || 0))),
|
||||
pageNumber: null,
|
||||
omitPageNumber: true,
|
||||
lineCount: 0,
|
||||
maxBlockId: 0,
|
||||
completenessScore: 0,
|
||||
side: Math.max(0, Math.round(Number(pageIndex || 0))) % 2 === 0 ? 'left' : 'right'
|
||||
};
|
||||
}
|
||||
|
||||
function applyExplicitBlankPageTexture(side, pageMeta = null, reason = 'blank-page') {
|
||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||
const blankTexture = getBlankPageTexture();
|
||||
clearPageReveal(side, reason);
|
||||
if (material.map !== blankTexture) {
|
||||
material.map = blankTexture;
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
pageTextureStore?.rememberResidentTexture?.(pageMeta || makeBlankPageMeta(side === 'left' ? 0 : 1), blankTexture, null, false);
|
||||
markPageTextureTiming('explicitBlankTexture', {
|
||||
side,
|
||||
pageIndex: pageMeta?.pageIndex ?? null,
|
||||
reason
|
||||
});
|
||||
return blankTexture;
|
||||
}
|
||||
|
||||
function attachRevealPageMeta(revealDetail = null, pageMeta = null) {
|
||||
@@ -2099,30 +2205,15 @@ function getRevealCacheKey(revealDetail = {}) {
|
||||
|
||||
function preloadPageTexture(side, sourceCanvas, revealDetail = {}, pageMeta = null) {
|
||||
if (!sourceCanvas) return null;
|
||||
const texture = createPageCanvasTexture(sourceCanvas);
|
||||
const baseTexture = revealDetail?.baseCanvas ? createPageCanvasTexture(revealDetail.baseCanvas) : null;
|
||||
const key = getRevealCacheKey({ ...(revealDetail || {}), pageMeta: revealDetail?.pageMeta || pageMeta || null });
|
||||
markPageTextureTiming('preloadTexture:start', {
|
||||
side,
|
||||
key,
|
||||
width: sourceCanvas.width,
|
||||
height: sourceCanvas.height,
|
||||
hasBaseTexture: Boolean(baseTexture)
|
||||
hasBaseTexture: Boolean(revealDetail?.baseCanvas)
|
||||
});
|
||||
preparedPageTextures[side].set(key, {
|
||||
texture,
|
||||
baseTexture,
|
||||
sourceCanvas,
|
||||
revealDetail,
|
||||
uploadedAt: performance.now()
|
||||
});
|
||||
if (preparedPageTextures[side].size > 128) {
|
||||
const oldestKey = preparedPageTextures[side].keys().next().value;
|
||||
const oldest = preparedPageTextures[side].get(oldestKey);
|
||||
oldest?.texture?.dispose?.();
|
||||
oldest?.baseTexture?.dispose?.();
|
||||
preparedPageTextures[side].delete(oldestKey);
|
||||
}
|
||||
const texture = pageTextureStore?.preparePageTexture?.(side, key, pageMeta, sourceCanvas, revealDetail) || null;
|
||||
markPageTextureTiming('preloadTexture:end', { side, key });
|
||||
return texture;
|
||||
}
|
||||
@@ -2142,54 +2233,6 @@ function setPageFlipActiveFlag() {
|
||||
}
|
||||
}
|
||||
|
||||
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 = makeResidentPageTextureKey(pageMeta);
|
||||
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) {
|
||||
const index = Math.max(0, Math.round(Number(pageIndex || 0)));
|
||||
const paginationMeta = getPaginationPageMeta(index) || {};
|
||||
@@ -2201,16 +2244,6 @@ function makePageMetaForCache(pageIndex) {
|
||||
};
|
||||
}
|
||||
|
||||
function makeResidentPageTextureKey(pageMetaOrIndex = {}) {
|
||||
const pageMeta = typeof pageMetaOrIndex === 'number'
|
||||
? makePageMetaForCache(pageMetaOrIndex)
|
||||
: pageMetaOrIndex || {};
|
||||
const pageIndex = Math.max(0, Math.round(Number(pageMeta.pageIndex || 0)));
|
||||
const kind = String(pageMeta.kind || 'content');
|
||||
const section = String(pageMeta.section || 'body');
|
||||
return `${pageIndex}:${kind}:${section}`;
|
||||
}
|
||||
|
||||
function spreadPageIndices(spreadIndex) {
|
||||
const spread = Math.max(0, Math.round(Number(spreadIndex || 0)));
|
||||
return {
|
||||
@@ -2219,67 +2252,6 @@ function spreadPageIndices(spreadIndex) {
|
||||
};
|
||||
}
|
||||
|
||||
function getResidentPageTexture(pageIndex) {
|
||||
const key = makeResidentPageTextureKey(pageIndex);
|
||||
const resident = residentPageTextures.get(key);
|
||||
if (!resident) return null;
|
||||
resident.lastUsedAt = performance.now();
|
||||
residentPageTextures.delete(key);
|
||||
residentPageTextures.set(key, resident);
|
||||
return resident.texture || null;
|
||||
}
|
||||
|
||||
function getResidentPageTextureForMeta(pageMeta = null) {
|
||||
const pageIndex = Number(pageMeta?.pageIndex);
|
||||
if (!Number.isFinite(pageIndex)) return null;
|
||||
const key = makeResidentPageTextureKey(pageMeta);
|
||||
const resident = residentPageTextures.get(key);
|
||||
if (!resident || isOlderPageTextureMeta(pageMeta, resident.pageMeta)) return null;
|
||||
return getResidentPageTexture(pageIndex);
|
||||
}
|
||||
|
||||
async function preloadCachedPageTexture(pageIndex) {
|
||||
const meta = makePageMetaForCache(pageIndex);
|
||||
const residentKey = makeResidentPageTextureKey(meta);
|
||||
if (residentPageTextures.has(residentKey)) {
|
||||
getResidentPageTexture(meta.pageIndex);
|
||||
return residentPageTextures.get(residentKey)?.texture || null;
|
||||
}
|
||||
const cache = window.WebGLPageCache || window.moduleRegistry?.getModule?.('webgl-page-cache') || null;
|
||||
const sourceCanvas = await cache?.getPageCanvas?.(meta);
|
||||
if (!sourceCanvas) {
|
||||
const pageMeta = getPaginationPageMeta(meta.pageIndex);
|
||||
if (pageMeta?.kind === 'blank') {
|
||||
const blankTexture = getBlankPageTexture();
|
||||
rememberResidentPageTexture({ ...meta, ...pageMeta }, blankTexture, null, false);
|
||||
return blankTexture;
|
||||
}
|
||||
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(residentKey, {
|
||||
texture,
|
||||
sourceCanvas,
|
||||
lastUsedAt: performance.now(),
|
||||
ownsTexture: true,
|
||||
pageMeta: cachedMeta
|
||||
});
|
||||
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 getPaginationPageMeta(pageIndex) {
|
||||
const index = Math.max(0, Math.round(Number(pageIndex || 0)));
|
||||
const spreadIndex = Math.floor(index / 2);
|
||||
@@ -2294,27 +2266,47 @@ function getPaginationPageMeta(pageIndex) {
|
||||
}
|
||||
|
||||
async function prewarmSpreadTextures(spreadIndex) {
|
||||
const indices = spreadPageIndices(spreadIndex);
|
||||
const [left, right] = await Promise.all([
|
||||
preloadCachedPageTexture(indices.left),
|
||||
preloadCachedPageTexture(indices.right)
|
||||
]);
|
||||
return {
|
||||
return pageTextureStore?.prewarmSpreadTextures?.(spreadIndex, makePageMetaForCache) || {
|
||||
spreadIndex: Math.max(0, Math.round(Number(spreadIndex || 0))),
|
||||
left,
|
||||
right
|
||||
left: null,
|
||||
right: null
|
||||
};
|
||||
}
|
||||
|
||||
async function prewarmNavigationTextureWindow(reason = 'navigation-window', options = {}) {
|
||||
const currentSpread = Math.max(0, Math.round(Number(bookPaginationState.spreadIndex || 0)));
|
||||
const endSpread = Math.max(
|
||||
0,
|
||||
Math.round(Number(bookPaginationState.spreadCount || 1)) - 1,
|
||||
pageToSpreadIndex(maxVisitedPagePosition)
|
||||
);
|
||||
markPageTextureTiming('textureStorePrewarm:start', {
|
||||
reason,
|
||||
currentSpread,
|
||||
endSpread
|
||||
});
|
||||
const result = await pageTextureStore?.prewarmNavigationWindow?.({
|
||||
currentSpread,
|
||||
targetSpread: options.targetSpread,
|
||||
endSpread,
|
||||
getPageMetaForIndex: makePageMetaForCache,
|
||||
recordMiss: options.recordMiss !== false
|
||||
});
|
||||
markPageTextureTiming('textureStorePrewarm:end', {
|
||||
reason,
|
||||
spreadCount: result ? Object.keys(result).length : 0
|
||||
});
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function prewarmFlipTextures(direction, targetSpread = null) {
|
||||
const currentSpread = Math.max(0, Number(bookPaginationState.spreadIndex || 0));
|
||||
const nextSpread = Number.isFinite(Number(targetSpread))
|
||||
? Math.max(0, Math.round(Number(targetSpread)))
|
||||
: Math.max(0, currentSpread + Math.sign(Number(direction || 0)));
|
||||
const [current, next] = await Promise.all([
|
||||
prewarmSpreadTextures(currentSpread),
|
||||
prewarmSpreadTextures(nextSpread)
|
||||
]);
|
||||
const windowMap = await prewarmNavigationTextureWindow('flip-prewarm', { targetSpread: nextSpread });
|
||||
const current = windowMap?.[currentSpread] || await prewarmSpreadTextures(currentSpread);
|
||||
const next = windowMap?.[nextSpread] || await prewarmSpreadTextures(nextSpread);
|
||||
return {
|
||||
current,
|
||||
next
|
||||
@@ -2323,19 +2315,22 @@ async function prewarmFlipTextures(direction, targetSpread = null) {
|
||||
|
||||
function takePreparedPageTexture(side, revealDetail = {}) {
|
||||
const key = getRevealCacheKey(revealDetail);
|
||||
const prepared = preparedPageTextures[side].get(key);
|
||||
const prepared = pageTextureStore?.takePreparedPageTexture?.(side, key) || null;
|
||||
if (!prepared) return null;
|
||||
preparedPageTextures[side].delete(key);
|
||||
markPageTextureTiming('preloadTexture:activate', { side, key });
|
||||
return prepared;
|
||||
}
|
||||
|
||||
function uploadPageTextureDirect(side, sourceCanvas, pageMeta = null) {
|
||||
if (pageMeta?.kind === 'blank') {
|
||||
applyExplicitBlankPageTexture(side, pageMeta, 'direct-upload');
|
||||
return;
|
||||
}
|
||||
const texture = side === 'left' ? leftTexture : rightTexture;
|
||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||
const shouldUseResidentTexture = pageMeta?.kind !== 'title';
|
||||
const residentTexture = shouldUseResidentTexture && Number.isFinite(Number(pageMeta?.pageIndex))
|
||||
? getResidentPageTextureForMeta(pageMeta)
|
||||
? pageTextureStore?.getResidentTextureForMeta?.(pageMeta)
|
||||
: null;
|
||||
markPageTextureTiming('directUpload:start', {
|
||||
side,
|
||||
@@ -2356,7 +2351,7 @@ function uploadPageTextureDirect(side, sourceCanvas, pageMeta = null) {
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
bindPageTextureSource(side, texture, sourceCanvas);
|
||||
rememberResidentPageTexture(pageMeta, texture, sourceCanvas, false);
|
||||
pageTextureStore?.rememberResidentTexture?.(pageMeta, texture, sourceCanvas, false);
|
||||
markPageTextureTiming('directUpload:end', { side });
|
||||
}
|
||||
|
||||
@@ -2381,7 +2376,7 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
||||
}
|
||||
bindPageTextureSource(side, texture, sourceCanvas);
|
||||
}
|
||||
const baseTexture = prepared?.baseTexture || (revealDetail?.baseCanvas ? createPageCanvasTexture(revealDetail.baseCanvas) : null);
|
||||
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
|
||||
@@ -2397,6 +2392,7 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
||||
durationMs: Math.max(1, Number(revealDetail.durationMs || 1)),
|
||||
blockIds: revealBlockIds,
|
||||
baseTexture,
|
||||
pageFlipAfterReveal: revealDetail.pageFlipAfterReveal === true,
|
||||
fastForwarding: false,
|
||||
fastForwardStartedAt: null,
|
||||
fastForwardStartElapsedMs: 0,
|
||||
@@ -2408,12 +2404,12 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
||||
if (shader?.uniforms?.bookRevealElapsedMs) {
|
||||
shader.uniforms.bookRevealElapsedMs.value = pageRevealState[side].visualElapsedMs;
|
||||
}
|
||||
if (side === 'right' && isRightBodyPageComplete()) {
|
||||
if (side === 'right' && revealDetail.pageFlipAfterReveal === true) {
|
||||
const targetSpread = Math.max(0, Math.round(Number(bookPaginationState.spreadIndex || 0)) + 1);
|
||||
prewarmFlipTextures(1, targetSpread).then(() => {
|
||||
markPageTextureTiming('rightPageReveal:flip-prewarm-ready', { targetSpread });
|
||||
}).catch((error) => {
|
||||
recordPageCacheProblem({
|
||||
pageTextureStore?.recordProblem?.({
|
||||
type: 'right-page-flip-prewarm-error',
|
||||
targetSpread,
|
||||
message: error?.message || String(error)
|
||||
@@ -2618,7 +2614,8 @@ function updatePageRevealAnimations(now) {
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:reveal-committed', {
|
||||
detail: {
|
||||
side,
|
||||
blockIds: state.blockIds
|
||||
blockIds: state.blockIds,
|
||||
pageFlipAfterReveal: state.pageFlipAfterReveal === true
|
||||
}
|
||||
}));
|
||||
});
|
||||
@@ -2632,8 +2629,11 @@ function bindPageTextureSource(side, texture, sourceCanvas) {
|
||||
width: nextCanvas?.width || 0,
|
||||
height: nextCanvas?.height || 0
|
||||
});
|
||||
texture.image = sourceCanvas || fallbackCanvas;
|
||||
texture.needsUpdate = true;
|
||||
const boundTexture = pageTextureStore?.bindVisibleTextureSource?.(side, sourceCanvas) || null;
|
||||
if (!boundTexture) {
|
||||
texture.image = nextCanvas;
|
||||
texture.needsUpdate = true;
|
||||
}
|
||||
updatePageTextureDebugState(side, nextCanvas, sourceCanvas, true);
|
||||
markPageTextureTiming('bindPageTextureSource:end', { side });
|
||||
}
|
||||
@@ -2841,20 +2841,27 @@ function createPageFlip(direction, startTime, duration) {
|
||||
|
||||
function prepareStaticPageForFlip(flip, prewarm = null) {
|
||||
if (!flip) return false;
|
||||
const sourceMaterial = flip.sourcePageSide === 'left' ? materials.leftPage : materials.rightPage;
|
||||
const sourceTexture = sourceMaterial?.map || (flip.sourcePageSide === 'left' ? leftTexture : rightTexture);
|
||||
const sourceSide = flip.direction > 0 ? 'right' : 'left';
|
||||
const sourceTexture = resolveCurrentFlipSourceTexture(sourceSide);
|
||||
const sourcePageMeta = currentPageMeta?.[sourceSide] || getPaginationPageMeta(spreadPageIndices(bookPaginationState.spreadIndex)[sourceSide]) || null;
|
||||
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 targetBackSide = flip.direction > 0 ? 'left' : 'right';
|
||||
const targetBackPageIndex = targetPages[targetBackSide];
|
||||
const targetBackPageMeta = getPaginationPageMeta(targetBackPageIndex) || makeBlankPageMeta(targetBackPageIndex);
|
||||
const prewarmedBackTexture = flip.direction > 0 ? prewarm?.next?.left : prewarm?.next?.right;
|
||||
const residentBackTexture = prewarmedBackTexture || getResidentPageTexture(targetBackPageIndex);
|
||||
const requiresWrittenTexture = targetBackPageIndex <= Math.max(2, Number(bookPaginationState.writtenPageLimit || 0));
|
||||
if (!residentBackTexture && requiresWrittenTexture) {
|
||||
recordPageCacheProblem({
|
||||
type: 'flip-back-texture-missing',
|
||||
const backTexture = resolveFlipBackTexture(targetBackPageMeta, prewarmedBackTexture);
|
||||
const requiresWrittenTexture = targetBackPageMeta.kind !== 'blank'
|
||||
&& targetBackPageIndex <= Math.max(2, Number(bookPaginationState.writtenPageLimit || 0));
|
||||
if (!sourceTexture || (!backTexture && requiresWrittenTexture)) {
|
||||
pageTextureStore?.recordProblem?.({
|
||||
type: !sourceTexture ? 'flip-source-texture-missing' : 'flip-back-texture-missing',
|
||||
sourceSide,
|
||||
sourcePageIndex: sourcePageMeta?.pageIndex ?? null,
|
||||
targetBackPageIndex,
|
||||
targetBackKind: targetBackPageMeta.kind,
|
||||
targetSpread,
|
||||
direction: flip.direction,
|
||||
prewarmedCurrent: Boolean(prewarm?.current),
|
||||
@@ -2862,9 +2869,8 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
|
||||
});
|
||||
return false;
|
||||
}
|
||||
const backTexture = residentBackTexture || getBlankPageTexture();
|
||||
materials.flipPageSurface.map = sourceTexture;
|
||||
materials.flipPageBackSurface.map = backTexture;
|
||||
materials.flipPageBackSurface.map = backTexture || getBlankPageTexture();
|
||||
materials.flipPageSurface.normalMap = materials.pageSurface.normalMap;
|
||||
materials.flipPageBackSurface.normalMap = materials.pageSurface.normalMap;
|
||||
materials.flipPageSurface.roughnessMap = materials.pageSurface.roughnessMap;
|
||||
@@ -2872,8 +2878,24 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
|
||||
materials.flipPageSurface.needsUpdate = true;
|
||||
materials.flipPageBackSurface.needsUpdate = true;
|
||||
flip.sourceTexture = sourceTexture;
|
||||
flip.backTexture = backTexture;
|
||||
flip.sourcePageMeta = sourcePageMeta ? { ...sourcePageMeta } : null;
|
||||
flip.backTexture = backTexture || getBlankPageTexture();
|
||||
flip.backPageMeta = targetBackPageMeta ? { ...targetBackPageMeta } : null;
|
||||
flip.targetBackPageIndex = targetBackPageIndex;
|
||||
flip.sourcePageSide = sourceSide;
|
||||
lastFlipTexturePreflight = {
|
||||
direction: flip.direction,
|
||||
sourceSide,
|
||||
sourcePageIndex: sourcePageMeta?.pageIndex ?? null,
|
||||
sourceKind: sourcePageMeta?.kind || 'content',
|
||||
targetSpread,
|
||||
targetBackSide,
|
||||
targetBackPageIndex,
|
||||
targetBackKind: targetBackPageMeta.kind,
|
||||
hasSourceTexture: Boolean(sourceTexture),
|
||||
hasBackTexture: Boolean(backTexture || getBlankPageTexture()),
|
||||
sourceTextureMatchesBackTexture: sourceTexture === (backTexture || getBlankPageTexture())
|
||||
};
|
||||
if (flip.direction > 0) {
|
||||
const blankTexture = getBlankPageTexture();
|
||||
if (blankTexture && materials.rightPage.map !== blankTexture) {
|
||||
@@ -2890,15 +2912,27 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
|
||||
}
|
||||
}
|
||||
markPageTextureTiming('flipTexturePreflight:ready', {
|
||||
direction: flip.direction,
|
||||
sourceSide: flip.sourcePageSide,
|
||||
targetSpread,
|
||||
targetBackPageIndex,
|
||||
usedResidentBackTexture: Boolean(residentBackTexture)
|
||||
...lastFlipTexturePreflight,
|
||||
usedResidentBackTexture: Boolean(backTexture && backTexture !== getBlankPageTexture())
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolveCurrentFlipSourceTexture(side) {
|
||||
const pageMeta = currentPageMeta?.[side] || null;
|
||||
if (pageMeta?.kind === 'blank') return getBlankPageTexture();
|
||||
const resident = pageTextureStore?.getResidentTextureForMeta?.(pageMeta);
|
||||
if (resident) return resident;
|
||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||
return material?.map || null;
|
||||
}
|
||||
|
||||
function resolveFlipBackTexture(pageMeta = null, prewarmedTexture = null) {
|
||||
if (pageMeta?.kind === 'blank') return getBlankPageTexture();
|
||||
if (prewarmedTexture) return prewarmedTexture;
|
||||
return pageTextureStore?.getResidentTextureForMeta?.(pageMeta);
|
||||
}
|
||||
|
||||
function canPageFlip(direction) {
|
||||
if (!currentProceduralBookModel) return false;
|
||||
const currentPage = getCurrentPagePosition();
|
||||
@@ -2908,7 +2942,7 @@ function canPageFlip(direction) {
|
||||
}
|
||||
|
||||
function handleRevealCommittedForPageFlip(detail = {}) {
|
||||
if (detail.side !== 'right' || !isRightBodyPageComplete()) return;
|
||||
if (detail.side !== 'right' || detail.pageFlipAfterReveal !== true) return;
|
||||
if (activeFlips.length > 0 || pendingRightPageFlip) return;
|
||||
if (isChoiceAwaitingPlayer()) return;
|
||||
const autoplayFlip = isTtsPlaybackActive();
|
||||
@@ -2937,16 +2971,6 @@ async function tryStartPendingRightPageFlip(reason = 'pending', options = {}) {
|
||||
return flipped;
|
||||
}
|
||||
|
||||
function isRightBodyPageComplete() {
|
||||
const meta = currentPageMeta?.right || null;
|
||||
if (!meta || meta.section !== 'body' || meta.kind === 'blank' || meta.kind === 'title') return false;
|
||||
const rendererDebug = window.BookTextureRenderer?.currentSpread || null;
|
||||
const rightLines = Array.isArray(rendererDebug?.right) ? rendererDebug.right : [];
|
||||
const maxLine = rightLines.reduce((max, line) => Math.max(max, Number(line?.pageLine || 0) + Math.max(1, Number(line?.lineCount || 1))), 0);
|
||||
const expectedLines = Math.max(1, Number(meta.linesPerPage || 25));
|
||||
return maxLine >= expectedLines;
|
||||
}
|
||||
|
||||
function isChoiceAwaitingPlayer() {
|
||||
return document.documentElement.dataset.choiceAwaiting === 'true'
|
||||
|| document.body?.dataset?.choiceAwaiting === 'true'
|
||||
|
||||
Reference in New Issue
Block a user