Fix WebGL page cache and flip sequencing

This commit is contained in:
2026-06-08 23:08:13 +02:00
parent a73dc5725f
commit 419691000c
7 changed files with 364 additions and 60 deletions
+98 -8
View File
@@ -38,9 +38,11 @@ class BookTextureRendererModule extends BaseModule {
this.lastDrawSkipLoggedAt = 0;
this.animationFrameId = null;
this.lastAnimationFrameAt = 0;
this.targetFrameDurationMs = 1000 / 30;
this.targetFrameDurationMs = 1000 / 60;
this.pipelineTimings = [];
this.imageCache = new Map();
this.pendingPageCacheWrites = new Map();
this.pageContentVersions = new Map();
this.bindMethods([
'initialize',
@@ -84,6 +86,8 @@ class BookTextureRendererModule extends BaseModule {
'tickAnimations',
'publishSpread',
'cachePublishedPages',
'getPageCacheWriteKey',
'isOlderPageMeta',
'schedulePageCacheWrite',
'getPageCanvas',
'getHitMap',
@@ -641,7 +645,7 @@ class BookTextureRendererModule extends BaseModule {
});
this.pendingRevealBlockIds.delete(String(blockId));
this.revealPublishBlockIds = new Set([String(blockId)]);
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getBlockSides(blockId));
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), ['left', 'right']);
document.dispatchEvent(new CustomEvent('webgl-book:page-reveal-start', {
detail: {
blockId
@@ -692,7 +696,7 @@ class BookTextureRendererModule extends BaseModule {
this.pendingRevealBlockIds.delete(id);
this.revealPublishBlockIds = new Set([id]);
const spread = detail.spread || this.currentSpread || this.pagination?.getCurrentSpread?.();
const sides = this.getBlockSides(blockId);
const sides = ['left', 'right'];
const published = this.drawSpread(spread, sides, { preloadOnly });
if (preloadOnly && published) {
this.preparedRevealCache.set(id, {
@@ -728,6 +732,7 @@ class BookTextureRendererModule extends BaseModule {
left: prepared.left || null,
right: prepared.right || null,
reveal: prepared.reveal || {},
pageMeta: prepared.pageMeta || {},
preparedFromCache: true
}
}));
@@ -856,7 +861,7 @@ class BookTextureRendererModule extends BaseModule {
metrics: this.metrics,
hitMaps: this.hitMaps,
sides: sidesToPublish,
pageMeta: this.currentSpread?.pageMeta || {}
pageMeta: this.buildPublishPageMeta(sidesToPublish)
};
if (options.preloadOnly) detail.preloadOnly = true;
if (sidesToPublish.includes('left')) {
@@ -907,6 +912,35 @@ class BookTextureRendererModule extends BaseModule {
return detail;
}
buildPublishPageMeta(sides = []) {
const baseMeta = this.currentSpread?.pageMeta || {};
return sides.reduce((meta, side) => {
const source = baseMeta[side] || null;
if (!source) {
meta[side] = null;
return meta;
}
const lines = Array.isArray(this.currentSpread?.[side]) ? this.currentSpread[side] : [];
const maxBlockId = lines.reduce((max, line) => Math.max(max, Number(line?.blockId || 0)), 0);
const lineCount = lines.length;
const pageIndex = Number(source.pageIndex);
const key = Number.isFinite(pageIndex) ? pageIndex : side;
const nextVersion = Math.max(1, Number(this.pageContentVersions.get(key) || 0) + 1);
this.pageContentVersions.set(key, nextVersion);
meta[side] = {
...source,
contentVersion: nextVersion,
completenessScore: (maxBlockId * 1000) + lineCount,
maxBlockId,
lineCount
};
return meta;
}, {
left: Object.prototype.hasOwnProperty.call(baseMeta, 'left') ? baseMeta.left : null,
right: Object.prototype.hasOwnProperty.call(baseMeta, 'right') ? baseMeta.right : null
});
}
cachePublishedPages(sides = [], detail = {}) {
if (!this.pageCache || typeof this.pageCache.cachePageCanvas !== 'function') return;
sides.forEach((side) => {
@@ -919,10 +953,66 @@ class BookTextureRendererModule extends BaseModule {
schedulePageCacheWrite(pageMeta, canvas) {
const frozenCanvas = this.cloneCanvas(canvas);
const scheduler = window.requestIdleCallback || ((callback) => window.setTimeout(callback, 16));
scheduler(() => {
this.pageCache?.cachePageCanvas?.(pageMeta, frozenCanvas);
}, { timeout: 250 });
const key = this.getPageCacheWriteKey(pageMeta, frozenCanvas);
const pending = this.pendingPageCacheWrites.get(key);
if (pending && this.isOlderPageMeta(pageMeta, pending.pageMeta)) return pending.promise;
const previousWrite = pending?.promise || Promise.resolve();
const write = previousWrite.catch(() => false).then(() => this.pageCache?.cachePageCanvas?.(pageMeta, frozenCanvas))
.then((stored) => {
if (!stored) {
document.dispatchEvent(new CustomEvent('webgl-book:page-cache-problem', {
detail: {
type: 'db-write-failed',
pageIndex: pageMeta?.pageIndex ?? null,
key
}
}));
}
return stored;
})
.catch((error) => {
document.dispatchEvent(new CustomEvent('webgl-book:page-cache-problem', {
detail: {
type: 'db-write-error',
pageIndex: pageMeta?.pageIndex ?? null,
key,
message: error?.message || String(error)
}
}));
return false;
})
.finally(() => {
if (this.pendingPageCacheWrites.get(key)?.promise === write) {
this.pendingPageCacheWrites.delete(key);
}
});
this.pendingPageCacheWrites.set(key, {
promise: write,
pageMeta: { ...(pageMeta || {}) }
});
return write;
}
isOlderPageMeta(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;
}
getPageCacheWriteKey(pageMeta = {}, canvas = null) {
if (this.pageCache && typeof this.pageCache.makePageKey === 'function') {
return this.pageCache.makePageKey({
...pageMeta,
width: canvas?.width ?? pageMeta.width,
height: canvas?.height ?? pageMeta.height
});
}
return `${pageMeta.cacheKey || window.MODULE_CACHE_BUSTER || 'dev'}:page:${pageMeta.pageIndex}:${canvas?.width || pageMeta.width}x${canvas?.height || pageMeta.height}`;
}
getPageCanvas(side) {