Restore WebGL reveal timing diagnostics
This commit is contained in:
@@ -17,6 +17,7 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
this.activeSegment = null;
|
this.activeSegment = null;
|
||||||
this.preparedSegments = new Map();
|
this.preparedSegments = new Map();
|
||||||
this.timelineDiagnostics = [];
|
this.timelineDiagnostics = [];
|
||||||
|
this.benchmarkEntries = [];
|
||||||
this.ownsPageFlipCommit = true;
|
this.ownsPageFlipCommit = true;
|
||||||
|
|
||||||
this.bindMethods([
|
this.bindMethods([
|
||||||
@@ -29,6 +30,7 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
'createRevealDetail',
|
'createRevealDetail',
|
||||||
'requiresSpreadTransition',
|
'requiresSpreadTransition',
|
||||||
'requiresRightPageFlipAfterReveal',
|
'requiresRightPageFlipAfterReveal',
|
||||||
|
'getBlockRevealSides',
|
||||||
'waitForVisualCompletion',
|
'waitForVisualCompletion',
|
||||||
'waitForRevealCommit',
|
'waitForRevealCommit',
|
||||||
'requestPageFlip',
|
'requestPageFlip',
|
||||||
@@ -37,6 +39,8 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
'getPageMetaForIndex',
|
'getPageMetaForIndex',
|
||||||
'getVisibleSpreadIndex',
|
'getVisibleSpreadIndex',
|
||||||
'isChoiceAwaitingPlayer',
|
'isChoiceAwaitingPlayer',
|
||||||
|
'markBenchmark',
|
||||||
|
'timeStage',
|
||||||
'recordDiagnostic',
|
'recordDiagnostic',
|
||||||
'getRuntimeState'
|
'getRuntimeState'
|
||||||
]);
|
]);
|
||||||
@@ -48,13 +52,33 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
this.pageCache = this.getModule('webgl-page-cache');
|
this.pageCache = this.getModule('webgl-page-cache');
|
||||||
this.playbackCoordinator = this.getModule('playback-coordinator');
|
this.playbackCoordinator = this.getModule('playback-coordinator');
|
||||||
this.sentenceQueue = this.getModule('sentence-queue');
|
this.sentenceQueue = this.getModule('sentence-queue');
|
||||||
|
this.addEventListener(document, 'webgl-book:page-reveal-start', (event) => {
|
||||||
|
this.markBenchmark('reveal-start', {
|
||||||
|
blockId: event.detail?.blockId ?? null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.addEventListener(document, 'webgl-book:reveal-committed', (event) => {
|
||||||
|
this.markBenchmark('reveal-committed', {
|
||||||
|
blockId: event.detail?.blockIds?.[0] ?? null,
|
||||||
|
side: event.detail?.side || null,
|
||||||
|
pageFlipAfterReveal: event.detail?.pageFlipAfterReveal === true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.addEventListener(document, 'webgl-book:page-flip-started', (event) => {
|
||||||
|
this.markBenchmark('flip-started', event.detail || {});
|
||||||
|
});
|
||||||
|
this.addEventListener(document, 'webgl-book:page-flip-finished', (event) => {
|
||||||
|
this.markBenchmark('flip-finished', event.detail || {});
|
||||||
|
});
|
||||||
window.BookPlaybackTimeline = this;
|
window.BookPlaybackTimeline = this;
|
||||||
this.reportProgress(100, 'Book playback timeline ready');
|
this.reportProgress(100, 'Book playback timeline ready');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async playSentence(sentence = {}) {
|
async playSentence(sentence = {}) {
|
||||||
const segment = await this.prepareSentence(sentence, { immediate: true });
|
const segment = await this.timeStage('prepare-current', { blockId: sentence.blockId ?? null }, () => {
|
||||||
|
return this.prepareSentence(sentence, { immediate: true });
|
||||||
|
});
|
||||||
if (!segment) {
|
if (!segment) {
|
||||||
return this.playbackCoordinator?.play?.(sentence);
|
return this.playbackCoordinator?.play?.(sentence);
|
||||||
}
|
}
|
||||||
@@ -63,11 +87,11 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
this.recordDiagnostic('segment-play:start', segment);
|
this.recordDiagnostic('segment-play:start', segment);
|
||||||
|
|
||||||
if (this.requiresSpreadTransition(segment)) {
|
if (this.requiresSpreadTransition(segment)) {
|
||||||
const flipped = await this.requestPageFlip(1, {
|
const flipped = await this.timeStage('preplay-flip', segment, () => this.requestPageFlip(1, {
|
||||||
reason: 'timeline-preplay-spread-transition',
|
reason: 'timeline-preplay-spread-transition',
|
||||||
targetSpread: segment.targetSpreadIndex,
|
targetSpread: segment.targetSpreadIndex,
|
||||||
force: true
|
force: true
|
||||||
});
|
}));
|
||||||
if (!flipped) {
|
if (!flipped) {
|
||||||
this.pageCache?.recordProblem?.({
|
this.pageCache?.recordProblem?.({
|
||||||
type: 'timeline-preplay-flip-failed',
|
type: 'timeline-preplay-flip-failed',
|
||||||
@@ -77,10 +101,12 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.activatePreparedSegment(segment, sentence);
|
await this.timeStage('activate', segment, () => this.activatePreparedSegment(segment, sentence));
|
||||||
|
|
||||||
const visualPromise = this.waitForVisualCompletion(segment);
|
const visualPromise = this.waitForVisualCompletion(segment);
|
||||||
const playbackPromise = this.playbackCoordinator?.play?.(sentence) || Promise.resolve();
|
const playbackPromise = this.timeStage('playback', segment, () => {
|
||||||
|
return this.playbackCoordinator?.play?.(sentence) || Promise.resolve();
|
||||||
|
});
|
||||||
await Promise.all([playbackPromise, visualPromise]);
|
await Promise.all([playbackPromise, visualPromise]);
|
||||||
|
|
||||||
this.recordDiagnostic('segment-play:end', segment);
|
this.recordDiagnostic('segment-play:end', segment);
|
||||||
@@ -94,7 +120,10 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
const existing = sentence.webglBookPresentation?.timelineSegment || this.preparedSegments.get(key);
|
const existing = sentence.webglBookPresentation?.timelineSegment || this.preparedSegments.get(key);
|
||||||
if (existing && options.force !== true) return existing;
|
if (existing && options.force !== true) return existing;
|
||||||
this.ensureAnimationTimings(sentence);
|
this.ensureAnimationTimings(sentence);
|
||||||
const segment = await this.createPreparedSegment(sentence, options);
|
const segment = await this.timeStage(options.immediate === true ? 'segment-prepare-immediate' : 'segment-prepare-lookahead', {
|
||||||
|
blockId: sentence.blockId,
|
||||||
|
id: sentence.id
|
||||||
|
}, () => this.createPreparedSegment(sentence, options));
|
||||||
if (!segment) return null;
|
if (!segment) return null;
|
||||||
this.preparedSegments.set(segment.key, segment);
|
this.preparedSegments.set(segment.key, segment);
|
||||||
sentence.webglBookPresentation = {
|
sentence.webglBookPresentation = {
|
||||||
@@ -121,6 +150,7 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
|
|
||||||
const targetSpreadIndex = Math.max(0, Number(previewSpread.index || 0));
|
const targetSpreadIndex = Math.max(0, Number(previewSpread.index || 0));
|
||||||
const currentSpreadIndex = this.getVisibleSpreadIndex();
|
const currentSpreadIndex = this.getVisibleSpreadIndex();
|
||||||
|
const revealSides = this.getBlockRevealSides(previewSpread, sentence.blockId);
|
||||||
const segment = {
|
const segment = {
|
||||||
key: `${sentence.gameId || sentence.metadata?.gameId || 'game'}:${sentence.blockId}`,
|
key: `${sentence.gameId || sentence.metadata?.gameId || 'game'}:${sentence.blockId}`,
|
||||||
id: sentence.id,
|
id: sentence.id,
|
||||||
@@ -129,13 +159,14 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
previewSpread,
|
previewSpread,
|
||||||
targetSpreadIndex,
|
targetSpreadIndex,
|
||||||
currentSpreadIndex,
|
currentSpreadIndex,
|
||||||
|
revealSides,
|
||||||
requiresPreFlip: targetSpreadIndex > currentSpreadIndex,
|
requiresPreFlip: targetSpreadIndex > currentSpreadIndex,
|
||||||
requiresRightFlip: this.requiresRightPageFlipAfterReveal(previewSpread),
|
requiresRightFlip: revealSides.includes('right') && this.requiresRightPageFlipAfterReveal(previewSpread),
|
||||||
preparedAt: performance.now(),
|
preparedAt: performance.now(),
|
||||||
status: 'prepared'
|
status: 'prepared'
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.prewarmSegmentTextures(segment);
|
await this.timeStage('texture-prewarm', segment, () => this.prewarmSegmentTextures(segment));
|
||||||
if (options.immediate !== true) {
|
if (options.immediate !== true) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
}
|
}
|
||||||
@@ -149,7 +180,9 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
});
|
});
|
||||||
segment.activeSpread = activeSpread || segment.previewSpread;
|
segment.activeSpread = activeSpread || segment.previewSpread;
|
||||||
segment.targetSpreadIndex = Math.max(0, Number(segment.activeSpread?.index ?? segment.targetSpreadIndex ?? 0));
|
segment.targetSpreadIndex = Math.max(0, Number(segment.activeSpread?.index ?? segment.targetSpreadIndex ?? 0));
|
||||||
segment.requiresRightFlip = this.requiresRightPageFlipAfterReveal(segment.activeSpread || segment.previewSpread);
|
segment.revealSides = this.getBlockRevealSides(segment.activeSpread || segment.previewSpread, sentence.blockId);
|
||||||
|
segment.requiresRightFlip = segment.revealSides.includes('right')
|
||||||
|
&& this.requiresRightPageFlipAfterReveal(segment.activeSpread || segment.previewSpread);
|
||||||
|
|
||||||
const revealDetail = this.createRevealDetail(sentence, segment.activeSpread || segment.previewSpread, 'activate');
|
const revealDetail = this.createRevealDetail(sentence, segment.activeSpread || segment.previewSpread, 'activate');
|
||||||
this.textureRenderer.prepareRevealBlock(revealDetail);
|
this.textureRenderer.prepareRevealBlock(revealDetail);
|
||||||
@@ -192,15 +225,27 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
return maxLine >= Math.max(1, Number(meta.linesPerPage || 25));
|
return maxLine >= Math.max(1, Number(meta.linesPerPage || 25));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBlockRevealSides(spread = {}, blockId = null) {
|
||||||
|
const id = String(blockId ?? '');
|
||||||
|
if (!id) return [];
|
||||||
|
return ['left', 'right'].filter((side) => {
|
||||||
|
const lines = Array.isArray(spread?.[side]) ? spread[side] : [];
|
||||||
|
return lines.some(line => String(line?.blockId ?? '') === id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async waitForVisualCompletion(segment = {}) {
|
async waitForVisualCompletion(segment = {}) {
|
||||||
if (!segment.requiresRightFlip) return;
|
if (!segment.requiresRightFlip || !Array.isArray(segment.revealSides) || !segment.revealSides.includes('right')) {
|
||||||
const committed = await this.waitForRevealCommit(segment);
|
this.recordDiagnostic('visual-completion:no-right-flip-wait', segment);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const committed = await this.timeStage('wait-right-reveal-commit', segment, () => this.waitForRevealCommit(segment));
|
||||||
if (!committed || this.isChoiceAwaitingPlayer()) return;
|
if (!committed || this.isChoiceAwaitingPlayer()) return;
|
||||||
await this.requestPageFlip(1, {
|
await this.timeStage('right-page-flip', segment, () => this.requestPageFlip(1, {
|
||||||
reason: 'timeline-right-page-filled',
|
reason: 'timeline-right-page-filled',
|
||||||
targetSpread: Math.max(0, Number(segment.targetSpreadIndex || this.getVisibleSpreadIndex()) + 1),
|
targetSpread: Math.max(0, Number(segment.targetSpreadIndex || this.getVisibleSpreadIndex()) + 1),
|
||||||
force: true
|
force: true
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForRevealCommit(segment = {}) {
|
waitForRevealCommit(segment = {}) {
|
||||||
@@ -350,18 +395,58 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
blockId: segment.blockId ?? null,
|
blockId: segment.blockId ?? null,
|
||||||
spreadIndex: segment.targetSpreadIndex ?? null,
|
spreadIndex: segment.targetSpreadIndex ?? null,
|
||||||
status: segment.status || null,
|
status: segment.status || null,
|
||||||
|
revealSides: Array.isArray(segment.revealSides) ? segment.revealSides : [],
|
||||||
at: Math.round(performance.now())
|
at: Math.round(performance.now())
|
||||||
});
|
});
|
||||||
while (this.timelineDiagnostics.length > 200) this.timelineDiagnostics.shift();
|
while (this.timelineDiagnostics.length > 200) this.timelineDiagnostics.shift();
|
||||||
document.documentElement.dataset.webglBookTimeline = type;
|
document.documentElement.dataset.webglBookTimeline = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markBenchmark(stage, detail = {}, startedAt = null) {
|
||||||
|
const now = performance.now();
|
||||||
|
const entry = {
|
||||||
|
stage,
|
||||||
|
blockId: detail.blockId ?? null,
|
||||||
|
spreadIndex: detail.targetSpreadIndex ?? detail.spreadIndex ?? detail.targetSpread ?? null,
|
||||||
|
durationMs: Number.isFinite(Number(startedAt)) ? Math.round((now - Number(startedAt)) * 100) / 100 : null,
|
||||||
|
at: Math.round(now),
|
||||||
|
detail: {
|
||||||
|
status: detail.status || null,
|
||||||
|
revealSides: Array.isArray(detail.revealSides) ? detail.revealSides : undefined,
|
||||||
|
reason: detail.reason || null,
|
||||||
|
side: detail.side || null,
|
||||||
|
pageFlipAfterReveal: detail.pageFlipAfterReveal === true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.benchmarkEntries.push(entry);
|
||||||
|
while (this.benchmarkEntries.length > 240) this.benchmarkEntries.shift();
|
||||||
|
document.documentElement.dataset.webglBookBenchmark = JSON.stringify(this.benchmarkEntries.slice(-40));
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
async timeStage(stage, detail = {}, callback = null) {
|
||||||
|
const startedAt = performance.now();
|
||||||
|
this.markBenchmark(`${stage}:start`, detail);
|
||||||
|
try {
|
||||||
|
const result = await callback?.();
|
||||||
|
this.markBenchmark(`${stage}:end`, detail, startedAt);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
this.markBenchmark(`${stage}:error`, {
|
||||||
|
...detail,
|
||||||
|
reason: error?.message || String(error)
|
||||||
|
}, startedAt);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getRuntimeState() {
|
getRuntimeState() {
|
||||||
return {
|
return {
|
||||||
activeBlockId: this.activeSegment?.blockId ?? null,
|
activeBlockId: this.activeSegment?.blockId ?? null,
|
||||||
preparedSegmentCount: this.preparedSegments.size,
|
preparedSegmentCount: this.preparedSegments.size,
|
||||||
ownsPageFlipCommit: this.ownsPageFlipCommit,
|
ownsPageFlipCommit: this.ownsPageFlipCommit,
|
||||||
diagnostics: this.timelineDiagnostics.slice(-20)
|
diagnostics: this.timelineDiagnostics.slice(-20),
|
||||||
|
benchmark: this.benchmarkEntries.slice(-40)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ const ModuleState = {
|
|||||||
ERROR: 'ERROR'
|
ERROR: 'ERROR'
|
||||||
};
|
};
|
||||||
|
|
||||||
const MODULE_CACHE_BUSTER = '20260610-book-timeline-a';
|
const MODULE_CACHE_BUSTER = '20260610-book-timeline-b';
|
||||||
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { RenderPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postproces
|
|||||||
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js';
|
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js';
|
||||||
import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js';
|
import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js';
|
||||||
import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
|
import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
|
||||||
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260610-book-timeline-a';
|
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260610-book-timeline-b';
|
||||||
|
|
||||||
const canvas = document.getElementById('scene');
|
const canvas = document.getElementById('scene');
|
||||||
canvas.style.cursor = 'grab';
|
canvas.style.cursor = 'grab';
|
||||||
@@ -163,6 +163,7 @@ let fpsDisplay = null;
|
|||||||
let fpsWindowStartedAt = performance.now();
|
let fpsWindowStartedAt = performance.now();
|
||||||
let fpsWindowFrames = 0;
|
let fpsWindowFrames = 0;
|
||||||
const lastFrameTiming = {};
|
const lastFrameTiming = {};
|
||||||
|
const slowFrameLog = [];
|
||||||
const loaderTimings = {};
|
const loaderTimings = {};
|
||||||
const pageTextureTimings = [];
|
const pageTextureTimings = [];
|
||||||
|
|
||||||
@@ -610,6 +611,14 @@ window.BookLabDebug = {
|
|||||||
lastFlipTexturePreflight
|
lastFlipTexturePreflight
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
getBenchmarkState() {
|
||||||
|
return {
|
||||||
|
frameTiming: { ...lastFrameTiming },
|
||||||
|
slowFrames: slowFrameLog.slice(-20),
|
||||||
|
pageTextureTimings: pageTextureTimings.slice(-40),
|
||||||
|
timeline: window.BookPlaybackTimeline?.getRuntimeState?.()?.benchmark || []
|
||||||
|
};
|
||||||
|
},
|
||||||
projectPointerToPage(clientX, clientY) {
|
projectPointerToPage(clientX, clientY) {
|
||||||
return projectPointerToPage(clientX, clientY);
|
return projectPointerToPage(clientX, clientY);
|
||||||
},
|
},
|
||||||
@@ -2424,6 +2433,13 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
|||||||
shaderReady: Boolean(shader?.uniforms),
|
shaderReady: Boolean(shader?.uniforms),
|
||||||
started: pageRevealState[side].startedAt != null
|
started: pageRevealState[side].startedAt != null
|
||||||
});
|
});
|
||||||
|
markPageTextureTiming('revealState:created', {
|
||||||
|
side,
|
||||||
|
blockIds: pageRevealState[side].blockIds,
|
||||||
|
started: pageRevealState[side].startedAt != null,
|
||||||
|
durationMs: pageRevealState[side].durationMs,
|
||||||
|
regionCount: Array.isArray(revealDetail.lineRects) ? revealDetail.lineRects.length : 0
|
||||||
|
});
|
||||||
markPageTextureTiming('revealUpload:end', { side });
|
markPageTextureTiming('revealUpload:end', { side });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2540,6 +2556,7 @@ function startPageRevealForBlock(blockId) {
|
|||||||
const id = String(blockId ?? '');
|
const id = String(blockId ?? '');
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
if (!activeRevealBlockStarts.has(id)) activeRevealBlockStarts.set(id, performance.now());
|
if (!activeRevealBlockStarts.has(id)) activeRevealBlockStarts.set(id, performance.now());
|
||||||
|
let matchedSides = 0;
|
||||||
if (activeFlips.length > 0) {
|
if (activeFlips.length > 0) {
|
||||||
pendingRevealStartBlockIds.add(id);
|
pendingRevealStartBlockIds.add(id);
|
||||||
markPageTextureTiming('revealStart:deferred-for-flip', {
|
markPageTextureTiming('revealStart:deferred-for-flip', {
|
||||||
@@ -2552,11 +2569,18 @@ function startPageRevealForBlock(blockId) {
|
|||||||
const state = pageRevealState[side];
|
const state = pageRevealState[side];
|
||||||
if (!state || state.startedAt != null) return;
|
if (!state || state.startedAt != null) return;
|
||||||
if (!state.blockIds.map(value => String(value)).includes(id)) return;
|
if (!state.blockIds.map(value => String(value)).includes(id)) return;
|
||||||
|
matchedSides += 1;
|
||||||
state.pendingStart = true;
|
state.pendingStart = true;
|
||||||
state.startedAt = activeRevealBlockStarts.get(id) || performance.now();
|
state.startedAt = activeRevealBlockStarts.get(id) || performance.now();
|
||||||
const shader = getPageRevealShader(side);
|
const shader = getPageRevealShader(side);
|
||||||
if (shader?.uniforms?.bookRevealElapsedMs) shader.uniforms.bookRevealElapsedMs.value = 0;
|
if (shader?.uniforms?.bookRevealElapsedMs) shader.uniforms.bookRevealElapsedMs.value = 0;
|
||||||
});
|
});
|
||||||
|
markPageTextureTiming('revealStart:applied', {
|
||||||
|
blockId: id,
|
||||||
|
matchedSides,
|
||||||
|
hasLeftState: Boolean(pageRevealState.left),
|
||||||
|
hasRightState: Boolean(pageRevealState.right)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function fastForwardPageReveals(blockIds = []) {
|
function fastForwardPageReveals(blockIds = []) {
|
||||||
@@ -4353,6 +4377,7 @@ function animate(now = performance.now()) {
|
|||||||
const delta = Math.min(0.1, frameElapsedMs / 1000);
|
const delta = Math.min(0.1, frameElapsedMs / 1000);
|
||||||
clock.getDelta();
|
clock.getDelta();
|
||||||
const t = clock.elapsedTime;
|
const t = clock.elapsedTime;
|
||||||
|
const updateStartedAt = performance.now();
|
||||||
updateCameraRig(delta);
|
updateCameraRig(delta);
|
||||||
scene.traverse((object) => {
|
scene.traverse((object) => {
|
||||||
if (!object.userData?.light) return;
|
if (!object.userData?.light) return;
|
||||||
@@ -4382,10 +4407,15 @@ function animate(now = performance.now()) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const hadActiveFlips = activeFlips.length > 0;
|
const hadActiveFlips = activeFlips.length > 0;
|
||||||
|
const flipStartedAt = performance.now();
|
||||||
updateActiveFlips(performance.now());
|
updateActiveFlips(performance.now());
|
||||||
|
lastFrameTiming.flips = performance.now() - flipStartedAt;
|
||||||
if (hadActiveFlips) markStaticSceneBuffersDirty();
|
if (hadActiveFlips) markStaticSceneBuffersDirty();
|
||||||
|
const revealStartedAt = performance.now();
|
||||||
updatePageRevealAnimations(now);
|
updatePageRevealAnimations(now);
|
||||||
|
lastFrameTiming.reveal = performance.now() - revealStartedAt;
|
||||||
updateCandleShadowUniforms();
|
updateCandleShadowUniforms();
|
||||||
|
lastFrameTiming.update = performance.now() - updateStartedAt;
|
||||||
renderedFrameCount += 1;
|
renderedFrameCount += 1;
|
||||||
const shadowStartedAt = performance.now();
|
const shadowStartedAt = performance.now();
|
||||||
updateBookShadowMaps();
|
updateBookShadowMaps();
|
||||||
@@ -4405,7 +4435,26 @@ function animate(now = performance.now()) {
|
|||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
}
|
}
|
||||||
lastFrameTiming.render = performance.now() - renderStartedAt;
|
lastFrameTiming.render = performance.now() - renderStartedAt;
|
||||||
lastFrameTiming.total = lastFrameTiming.shadows + lastFrameTiming.reflection + lastFrameTiming.render;
|
lastFrameTiming.total = lastFrameTiming.update + lastFrameTiming.shadows + lastFrameTiming.reflection + lastFrameTiming.render;
|
||||||
|
if (frameElapsedMs > targetFrameDurationMs * 1.75 || lastFrameTiming.total > targetFrameDurationMs * 1.25) {
|
||||||
|
slowFrameLog.push({
|
||||||
|
at: Math.round(now),
|
||||||
|
frameElapsedMs: Math.round(frameElapsedMs * 100) / 100,
|
||||||
|
activeFlips: activeFlips.length,
|
||||||
|
revealActive: Boolean(pageRevealState.left || pageRevealState.right),
|
||||||
|
timings: {
|
||||||
|
update: Math.round(lastFrameTiming.update * 100) / 100,
|
||||||
|
flips: Math.round(lastFrameTiming.flips * 100) / 100,
|
||||||
|
reveal: Math.round(lastFrameTiming.reveal * 100) / 100,
|
||||||
|
shadows: Math.round(lastFrameTiming.shadows * 100) / 100,
|
||||||
|
reflection: Math.round(lastFrameTiming.reflection * 100) / 100,
|
||||||
|
render: Math.round(lastFrameTiming.render * 100) / 100,
|
||||||
|
total: Math.round(lastFrameTiming.total * 100) / 100
|
||||||
|
}
|
||||||
|
});
|
||||||
|
while (slowFrameLog.length > 80) slowFrameLog.shift();
|
||||||
|
document.documentElement.dataset.webglSlowFrames = JSON.stringify(slowFrameLog.slice(-20));
|
||||||
|
}
|
||||||
if (refreshStaticSceneBuffers && activeFlips.length === 0) {
|
if (refreshStaticSceneBuffers && activeFlips.length === 0) {
|
||||||
staticSceneBuffersDirty = false;
|
staticSceneBuffersDirty = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ const checks = [
|
|||||||
['webgl debug test hook awaits the same async page flip path', /startPageFlipForTest\(direction, options = \{\}\) \{[\s\S]*return startPageFlip\(direction, options\)/.test(source)],
|
['webgl debug test hook awaits the same async page flip path', /startPageFlipForTest\(direction, options = \{\}\) \{[\s\S]*return startPageFlip\(direction, options\)/.test(source)],
|
||||||
['webgl debug test hook can deterministically finish an active page flip', /advancePageFlipForTest\(elapsedMs = normalFlipDuration \+ 16\)/.test(source) && /updateActiveFlips\(targetNow\)/.test(source)],
|
['webgl debug test hook can deterministically finish an active page flip', /advancePageFlipForTest\(elapsedMs = normalFlipDuration \+ 16\)/.test(source) && /updateActiveFlips\(targetNow\)/.test(source)],
|
||||||
['sentence queue skips duplicate current-item 3D book presentation when reveal is cached', /isWebGLBookPresentationPrepared/.test(sentenceQueueSource) && /if \(!this\.isWebGLBookPresentationPrepared\(sentence\)\) \{\s*await this\.prefetchWebGLBookPresentation/.test(sentenceQueueSource) && /sentence\.webglBookPresentation = \{\s*prepared: true/.test(sentenceQueueSource)],
|
['sentence queue skips duplicate current-item 3D book presentation when reveal is cached', /isWebGLBookPresentationPrepared/.test(sentenceQueueSource) && /if \(!this\.isWebGLBookPresentationPrepared\(sentence\)\) \{\s*await this\.prefetchWebGLBookPresentation/.test(sentenceQueueSource) && /sentence\.webglBookPresentation = \{\s*prepared: true/.test(sentenceQueueSource)],
|
||||||
['3D overflow reveal waits for timeline-owned page flip before activating future spread', /requiresSpreadTransition\(segment\)/.test(bookPlaybackTimelineSource) && /await this\.requestPageFlip\(1, \{[\s\S]*targetSpread: segment\.targetSpreadIndex/.test(bookPlaybackTimelineSource) && /await this\.activatePreparedSegment\(segment, sentence\)/.test(bookPlaybackTimelineSource) && /webgl-book:request-page-flip/.test(bookPlaybackTimelineSource) && /const targetSpread = Number\.isFinite\(Number\(detail\.targetSpread\)\)/.test(source) && /startPageFlip\(direction, \{[\s\S]*targetSpread/.test(source)],
|
['3D overflow reveal waits for timeline-owned page flip before activating future spread', /requiresSpreadTransition\(segment\)/.test(bookPlaybackTimelineSource) && /this\.requestPageFlip\(1, \{[\s\S]*targetSpread: segment\.targetSpreadIndex/.test(bookPlaybackTimelineSource) && /this\.activatePreparedSegment\(segment, sentence\)/.test(bookPlaybackTimelineSource) && /webgl-book:request-page-flip/.test(bookPlaybackTimelineSource) && /const targetSpread = Number\.isFinite\(Number\(detail\.targetSpread\)\)/.test(source) && /startPageFlip\(direction, \{[\s\S]*targetSpread/.test(source)],
|
||||||
['texture renderer paints inline bold and italic styles', /getInlineStyleState/.test(textureRendererSource) && /updateInlineStyleState/.test(textureRendererSource) && /getCanvasFont/.test(textureRendererSource) && /segment\?\.style/.test(textureRendererSource)],
|
['texture renderer paints inline bold and italic styles', /getInlineStyleState/.test(textureRendererSource) && /updateInlineStyleState/.test(textureRendererSource) && /getCanvasFont/.test(textureRendererSource) && /segment\?\.style/.test(textureRendererSource)],
|
||||||
['webgl lab can preload page textures without swapping visible page material through texture store', /preparePageTexture\(side = 'left'/.test(webglPageCacheSource) && /takePreparedPageTexture\(side = 'left'/.test(webglPageCacheSource) && /renderer\.initTexture\(texture\)/.test(webglPageCacheSource) && /takePreparedPageTexture/.test(source) && !/const preparedPageTextures/.test(source)],
|
['webgl lab can preload page textures without swapping visible page material through texture store', /preparePageTexture\(side = 'left'/.test(webglPageCacheSource) && /takePreparedPageTexture\(side = 'left'/.test(webglPageCacheSource) && /renderer\.initTexture\(texture\)/.test(webglPageCacheSource) && /takePreparedPageTexture/.test(source) && !/const preparedPageTextures/.test(source)],
|
||||||
['webgl page text textures avoid mipmap generation', /function configurePageCanvasTexture/.test(source) && /texture\.minFilter = THREE\.LinearFilter/.test(source) && /texture\.generateMipmaps = false/.test(source)],
|
['webgl page text textures avoid mipmap generation', /function configurePageCanvasTexture/.test(source) && /texture\.minFilter = THREE\.LinearFilter/.test(source) && /texture\.generateMipmaps = false/.test(source)],
|
||||||
@@ -211,13 +211,16 @@ const checks = [
|
|||||||
['webgl reveal clock follows absolute playback time and continues across page flips', /activeRevealBlockStarts/.test(source) && /state\.visualElapsedMs = Math\.max\(0, now - state\.startedAt\)/.test(source) && !/Math\.min\(revealFrameDeltaMs, targetFrameDurationMs\)/.test(source) && /prewarmFlipTextures\(1, targetSpread\)/.test(source)],
|
['webgl reveal clock follows absolute playback time and continues across page flips', /activeRevealBlockStarts/.test(source) && /state\.visualElapsedMs = Math\.max\(0, now - state\.startedAt\)/.test(source) && !/Math\.min\(revealFrameDeltaMs, targetFrameDurationMs\)/.test(source) && /prewarmFlipTextures\(1, targetSpread\)/.test(source)],
|
||||||
['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) && /this\.activeAnimations\.has\(id\)/.test(textureRendererSource)],
|
['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) && /this\.activeAnimations\.has\(id\)/.test(textureRendererSource)],
|
||||||
['webgl visible spread state ignores future prepared publishes before flip', /spreadUpdate:deferred-future-unrendered/.test(source) && /incomingSpreadIndex > Math\.max\(0, Number\(bookPaginationState\.spreadIndex/.test(source) && /this\.drawSpread\(this\.currentSpread, \['left', 'right'\], \{ phase: 'prepare' \}\)/.test(textureRendererSource)],
|
['webgl visible spread state ignores future prepared publishes before flip', /spreadUpdate:deferred-future-unrendered/.test(source) && /incomingSpreadIndex > Math\.max\(0, Number\(bookPaginationState\.spreadIndex/.test(source) && /this\.drawSpread\(this\.currentSpread, \['left', 'right'\], \{ phase: 'prepare' \}\)/.test(textureRendererSource)],
|
||||||
['3D overflow reveal preloads target spread before forced page flip', /createRevealDetail\(sentence, previewSpread, 'prepare'\)/.test(bookPlaybackTimelineSource) && /this\.textureRenderer\.prepareRevealBlock\(revealDetail, \{ phase: 'prepare' \}\)/.test(bookPlaybackTimelineSource) && /await this\.prewarmSegmentTextures\(segment\)/.test(bookPlaybackTimelineSource) && /reason: 'timeline-preplay-spread-transition'/.test(bookPlaybackTimelineSource)],
|
['3D overflow reveal preloads target spread before forced page flip', /createRevealDetail\(sentence, previewSpread, 'prepare'\)/.test(bookPlaybackTimelineSource) && /this\.textureRenderer\.prepareRevealBlock\(revealDetail, \{ phase: 'prepare' \}\)/.test(bookPlaybackTimelineSource) && /this\.prewarmSegmentTextures\(segment\)/.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)],
|
['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)],
|
||||||
['3D display playback is owned by book playback timeline', /book-playback-timeline/.test(uiDisplayHandlerSource) && /playWebGLBookSentence/.test(uiDisplayHandlerSource) && /timeline\.playSentence\(sentence\)/.test(uiDisplayHandlerSource) && /if \(useWebGLBookReveal\) \{[\s\S]*await this\.playWebGLBookSentence\(sentence\);[\s\S]*this\.markBlockRendered\(sentence\.blockId\);[\s\S]*return null;/.test(uiDisplayHandlerSource)],
|
['3D display playback is owned by book playback timeline', /book-playback-timeline/.test(uiDisplayHandlerSource) && /playWebGLBookSentence/.test(uiDisplayHandlerSource) && /timeline\.playSentence\(sentence\)/.test(uiDisplayHandlerSource) && /if \(useWebGLBookReveal\) \{[\s\S]*await this\.playWebGLBookSentence\(sentence\);[\s\S]*this\.markBlockRendered\(sentence\.blockId\);[\s\S]*return null;/.test(uiDisplayHandlerSource)],
|
||||||
['sentence queue lookahead prepares 3D book timeline segments', /book-playback-timeline/.test(sentenceQueueSource) && /bookPlaybackTimeline\.prepareSentence\(sentence/.test(sentenceQueueSource) && /timelineSegment: segment/.test(sentenceQueueSource)],
|
['sentence queue lookahead prepares 3D book timeline segments', /book-playback-timeline/.test(sentenceQueueSource) && /bookPlaybackTimeline\.prepareSentence\(sentence/.test(sentenceQueueSource) && /timelineSegment: segment/.test(sentenceQueueSource)],
|
||||||
['book playback timeline prewarms texture window before prepared playback and flips', /prewarmSegmentTextures/.test(bookPlaybackTimelineSource) && /pageCache\.prewarmNavigationWindow/.test(bookPlaybackTimelineSource) && /await this\.prewarmSegmentTextures\(segment\)/.test(bookPlaybackTimelineSource) && /await this\.pageCache\?\.prewarmNavigationWindow/.test(bookPlaybackTimelineSource)],
|
['book playback timeline prewarms texture window before prepared playback and flips', /prewarmSegmentTextures/.test(bookPlaybackTimelineSource) && /pageCache\.prewarmNavigationWindow/.test(bookPlaybackTimelineSource) && /this\.prewarmSegmentTextures\(segment\)/.test(bookPlaybackTimelineSource) && /await this\.pageCache\?\.prewarmNavigationWindow/.test(bookPlaybackTimelineSource)],
|
||||||
['webgl lab delegates right-page reveal commits to timeline owner', /BookPlaybackTimeline\?\.ownsPageFlipCommit === true/.test(source) && /handleRevealCommittedForPageFlip/.test(source)],
|
['webgl lab delegates right-page reveal commits to timeline owner', /BookPlaybackTimeline\?\.ownsPageFlipCommit === true/.test(source) && /handleRevealCommittedForPageFlip/.test(source)],
|
||||||
['webgl reveal clock explicitly freezes during physical flips', /pageRevealFreezeAt/.test(source) && /state\.startedAt \+= frozenMs/.test(source) && /activeRevealBlockStarts\.set\(blockId, Number\(value\) \+ frozenMs\)/.test(source)],
|
['webgl reveal clock explicitly freezes during physical flips', /pageRevealFreezeAt/.test(source) && /state\.startedAt \+= frozenMs/.test(source) && /activeRevealBlockStarts\.set\(blockId, Number\(value\) \+ frozenMs\)/.test(source)],
|
||||||
|
['book playback timeline waits for right reveal only when current block is on right page', /getBlockRevealSides/.test(bookPlaybackTimelineSource) && /revealSides\.includes\('right'\) && this\.requiresRightPageFlipAfterReveal/.test(bookPlaybackTimelineSource) && /visual-completion:no-right-flip-wait/.test(bookPlaybackTimelineSource)],
|
||||||
|
['book playback timeline exposes reveal lifecycle benchmark entries', /benchmarkEntries/.test(bookPlaybackTimelineSource) && /markBenchmark\('reveal-start'/.test(bookPlaybackTimelineSource) && /markBenchmark\('reveal-committed'/.test(bookPlaybackTimelineSource) && /webglBookBenchmark/.test(bookPlaybackTimelineSource)],
|
||||||
|
['webgl scene records reveal start and slow-frame benchmark diagnostics', /revealState:created/.test(source) && /revealStart:applied/.test(source) && /slowFrameLog/.test(source) && /getBenchmarkState/.test(source) && /webglSlowFrames/.test(source)],
|
||||||
['webgl navigation buttons use visited page limit instead of future prepared pages', /maxVisitedPagePosition/.test(source) && /navigateToPagePosition\(maxVisitedPagePosition\)/.test(source) && /const navigableLimit = Math\.min\(maxVisitedPagePosition, writableLimit\)/.test(source) && !/navigateToPagePosition\(bookPaginationState\.writtenPageLimit\)/.test(source)],
|
['webgl navigation buttons use visited page limit instead of future prepared pages', /maxVisitedPagePosition/.test(source) && /navigateToPagePosition\(maxVisitedPagePosition\)/.test(source) && /const navigableLimit = Math\.min\(maxVisitedPagePosition, writableLimit\)/.test(source) && !/navigateToPagePosition\(bookPaginationState\.writtenPageLimit\)/.test(source)],
|
||||||
['webgl save restore carries visited page limit for navigation', /maxVisitedPagePosition/.test(source) && /setMaxVisitedPagePosition/.test(source) && /state\.maxVisitedPagePosition \?\? state\.pagePosition/.test(webglSceneSource)],
|
['webgl save restore carries visited page limit for navigation', /maxVisitedPagePosition/.test(source) && /setMaxVisitedPagePosition/.test(source) && /state\.maxVisitedPagePosition \?\? state\.pagePosition/.test(webglSceneSource)],
|
||||||
['webgl page flips require resident nonblank back textures before animation starts', /prepareStaticPageForFlip\(flip, prewarm = null\)/.test(source) && /flip-back-texture-missing/.test(source) && /targetBackPageMeta\.kind !== 'blank'/.test(source) && /return false;/.test(methodBody(source, 'prepareStaticPageForFlip')) && /flipTexturePreflight:ready/.test(source) && /if \(!prepareStaticPageForFlip\(flip, options\.prewarm \|\| null\)\) \{[\s\S]*return false;[\s\S]*\}/.test(source)],
|
['webgl page flips require resident nonblank back textures before animation starts', /prepareStaticPageForFlip\(flip, prewarm = null\)/.test(source) && /flip-back-texture-missing/.test(source) && /targetBackPageMeta\.kind !== 'blank'/.test(source) && /return false;/.test(methodBody(source, 'prepareStaticPageForFlip')) && /flipTexturePreflight:ready/.test(source) && /if \(!prepareStaticPageForFlip\(flip, options\.prewarm \|\| null\)\) \{[\s\S]*return false;[\s\S]*\}/.test(source)],
|
||||||
|
|||||||
Reference in New Issue
Block a user