Commit Graph

63 Commits

Author SHA1 Message Date
Georg 8e87f935b8 Fix page label and restore nav code after bad clone-removal revert
The previous commit's clone removal corrupted the flip (shared live canvas overwritten mid-
animation -> flicker); reverting it fixed the flip but also discarded code that had ridden into
that commit and broke the page label. Recovering:

- Page label: the right-page pageNumber lookup returned null (meta not populated for the queried
  index) so every spread read 0. Now derive the printed number from the index (frontmatter
  pages 0-2 are unnumbered, so right-page index N prints as N-2), preferring the paginated
  pageNumber when present. Title still reads 0.
- Restored the manual-navigation-busy guard and the written-content navigation cap (no flipping
  forward into blank leaves before content exists; title stays on its own spread).

The flip flicker fix is the clone restoration in the prior revert; this restores the label and
navigation behavior on top of it. Suite 182. Flip-flicker and per-paragraph stutter still need
verification on a real (non-throttled) foreground tab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 07:45:29 +02:00
Georg 7f60ce0d63 Revert "Remove per-draw canvas clones; title nav cap + right-page-number labels"
This reverts commit 0f66dae4eb.
2026-06-20 07:37:12 +02:00
Georg 0f66dae4eb Remove per-draw canvas clones; title nav cap + right-page-number labels
Two changes:

Eliminate cloning in the publish path. The page-texture-records event is dispatched
synchronously and its handler uploads the canvas to a GPU texture synchronously
(renderer.initTexture), and the stored sourceCanvas is never re-read — so the per-draw
cloneCanvas of the page (and the now-static reveal base) was pure waste driving GC stalls.
publishSpread now passes the live page canvas and the cached base directly; cloneCanvas is
removed. Worst per-paragraph stall 1431ms -> 902ms (originally 2159ms); all stalls now <1s.

Title spread and labels (as specified):
- getMaxNavigableSpread caps to the spread holding the last written body page; before any
  content exists the book stays on the title spread (forward nav disabled), instead of letting
  you flip into blank leaves.
- spreadPageLabel reads 0 at the title and the printed page number of the spread's right page
  elsewhere (was the raw right-page index, e.g. "3" before a game).

Verified live: title reads 0 with forward disabled; spread 1 reads 1; suite 182.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 07:33:21 +02:00
Georg 705d1ea6bf Fix new-game title flip + cap lookahead prepare burst
Builds on the worker migration with prepare-burst pacing and a title-flip fix:

- New game from mid-game left the book on the previous game's spread, so the first block's
  source and target spread matched and the title->content page turn was skipped. story:client-reset
  now returns the book to the title spread (spread 0) so the first block flips 0->1 and animates.
  Verified: requiresSpreadTransition src=0 tgt=1, page-flip-started/near-end fire.

- The lookahead burst-prepared many blocks at once, spiking allocation/GC into multi-second
  main-thread stalls. WebGL book prepares are now serialized through a chain and capped to a
  small lookahead window (TTS audio prefetch still spans the full window); future lookahead is
  also deferred until the current sentence has entered the display pipeline, keeping it off the
  first flip/reveal critical path. Worst game-start stall ~6s -> ~3.4s.

- Page flips now drive the scene through the sceneControl prewarm/startPreparedPageFlip API
  (awaited) instead of an event, and the scene awaits the async initial spread draw.

Suite 177. Remaining: a per-block prepare stall (~1.6-3.4s for large blocks at game start)
that profiling has not yet attributed to a single function (likely GC from prepare-path
allocation) — needs a DevTools performance capture for exact attribution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 00:59:01 +02:00
Georg 004c077181 Don't recompute AO/shadow/reflection on page-texture content changes
handlePageTextureRecords() called markStaticSceneBuffersDirty() on every page-texture update,
forcing a full SSAO + shadow + reflection recompute (23-47ms frames) on every block during
playback — even though a page-text change moves no geometry. AO and shadows depend only on
geometry; the soft tabletop reflection picks up the new page on its throttled cadence. Removed
the forced dirty so only real geometry changes (flips, camera, rebuild, resize) recompute the
static buffers. Playback median ~60->63fps; the per-block forced heavy frames are gone (the
remaining periodic ~23ms frames are the normal 8Hz throttled refresh).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 19:38:13 +02:00
Georg b0175b7cdc Harden worker page rendering: error/timeout recovery and awaited flip prewarm
Two robustness gaps from the worker migration, both raised in review:

- The raster worker had no failure recovery: a thrown createImageBitmap/font error or a
  dropped message would leave the draw promise pending forever, stalling the serialized draw
  chain and hanging prepare/playback. Added worker.onerror and a per-job timeout; both settle
  the in-flight draw to a logged miss (texture-worker-error / -timeout) so the pipeline
  degrades to last-good per the spec instead of hanging. A single settleRasterization path
  clears the timer and resolves.
- prepareSpreadTextureRecordsForFlip() called drawSpread() without awaiting it. That was safe
  when drawSpread was synchronous, but now that it is async (worker) the flip could race ahead
  of the worker draw and miss the resident texture. prewarmFlipTextures now awaits both spread
  draws before the resident-texture lookup.

Suite 168 (added assertions for worker error/timeout recovery and the awaited prewarm).
Normal draw path is behaviorally unchanged from the verified worker commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 19:29:20 +02:00
Georg 97f0b913be Cursor reflects game state over the 3D scene again
Two regressions made the cursor stop communicating game state:

- The canvas had a hardcoded `cursor: grab`, overriding the document-level process-state
  cursor everywhere over the 3D scene (always a hand). Removed it so the canvas inherits the
  state cursor; grab is now shown only transiently while right-drag-rotating the camera.

- normalizeProcessState pinned ready/waiting-generating to the playing (feather) cursor
  whenever playbackCoordinator.isPlaying was set, which lingered at choice prompts — so an
  open choice showed the feather instead of the input cursor. Now, when an input prompt is
  open AND no sentence is actively playing (timeline's webglBookPlaybackActive), the playback
  overlay is stripped (playing-ready->ready, playing-generating->waiting-generating) and the
  input/server cursor shows. Opening an input mode also refreshes the cursor immediately.

Verified live over the canvas: feather while a sentence plays, input arrow at a choice/idle,
and they switch correctly with playback state (no stuck feather, no constant grab).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 10:00:49 +02:00
Georg 0e3e2abdb6 Front-load post-processing compile into the loader
primeSceneForLoader() compiled scene materials and rendered the shadow/reflection passes
once, but never ran the full composer pipeline — so the SSAO and output passes compiled
their programs and allocated their render targets on the first live frame after the loader
faded, tanking FPS for ~1-2s before it climbed to full.

Now the loader runs composer.render() twice during prime, and precompiles the flip page
materials (created lazily on first flip, so previously missed by renderer.compile) via a
throwaway probe mesh. The heavy first-frame work is paid behind the loader overlay instead.

Verified live: loader timings show composerWarmup taking ~1499ms during load (exactly the
cost that used to hit the first frame); after fade-out there are no over-budget tank frames
in the slow-frame log and idle settles at ~72fps. Static suite passes (165).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 09:45:57 +02:00
Georg b180637ea7 Hold 60fps: throttle shadow/reflection passes when book geometry is static
The table reflection (full scene re-render) and the book shadow maps each cost ~11ms/frame
and were refreshing at 30Hz even when nothing moved, so every idle/reveal frame paid for one
heavy pass on top of the ~12ms scene render — ~45-52fps.

These passes only need full-rate updates while the book geometry is actually moving (a page
flip). At idle, or during a text reveal where only the page texture mask animates, they now
refresh at 8Hz (candle flicker is the only thing changing them then, captured imperceptibly).
Most non-flip frames are then just the scene render.

pixelRatio is deliberately left at 2x: the book is tilted, so page glyphs are minified along
the tilt and the supersampling is the scene's antialiasing (the composer MSAA is disabled in
app-integration mode). Reducing it blurs text and exposes edge aliasing, so 60fps is bought
from the geometry-independent passes instead. Expressed pixelRatio via devicePixelRatio so it
stays native on HiDPI.

Verified live at WQHD/2x (screenshot-checked crisp text + clean edges): idle ~64fps median
(was 52), reveal ~66fps median (was ~33). Remaining single-digit dips are main-thread page
rasterization during background prepare — addressed by the worker migration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 09:39:29 +02:00
Georg 6bd1f45362 Reset per-game reveal state on new game so reveal animates over cached content
Starting a new game reuses block ids (1,2,3...). The reveal clock's per-block
start times (activeRevealBlockStarts in the lab) and the renderer's animation/
revealed sets are keyed by block id and were never cleared on a client reset, so
a new game over already-cached content inherited the previous run's start times.
beginPageReveal then computed a huge elapsed and the shader treated the reveal as
already complete — showing everything at once instead of animating.

resetClientPlaybackAndDisplay (run on new game and restore) now emits
story:client-reset; the lab clears activeRevealBlockStarts/pending reveal state,
the texture renderer clears active animations and revealed-block ids, and the
timeline invalidates prepared segments. So each game starts with a clean reveal
clock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 16:40:31 +02:00
Georg a845108c43 Make WebGL book navigation spread-based and clear stale flip reveal mask
- Navigation now operates on spread indices, not the non-contiguous page-position
  scheme that mapped a forward step onto the same spread (so forward stalled and
  triggered a no-op multi-flip). Forward/back move one spread; start/end and the
  slider use spread indices. The page readout shows the odd page of the visible
  pair (2*spread+1) or 0 at the title spread.
- Flipping forward could show the source page with its last word still masked: a
  stale reveal mask left on the flip surface by a previous playback flip was not
  cleared when the (finished) source page had no active reveal. Reset the flip
  surface reveal shader in that case so the full page shows during the turn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 16:27:18 +02:00
Georg ab194062bb Fix WebGL reveal pacing on spanning pages and page-reveal-on-flip
- Reveal timing is now word-proportional per page: when a block's reveal only
  covers part of the block (the continuation spread is not paginated at reveal
  time), the page reveals only its share of the TTS, offset by the words before
  it. The right page no longer absorbs the whole TTS before flipping; it flips at
  normal pace and the continuation resumes on the next spread while TTS plays. No
  effect when the regions already cover the whole block (unified plan / one page).
- Page flip start now shows the target spread's same-side page beneath the lifting
  page (revealed as it turns away) instead of a blank that pops in after the flip.
  Deferred (pending-reveal) sides stay blank so the masked reveal still lands via
  activate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:01:41 +02:00
Georg 8bb18fa201 Rework WebGL book playback to single ownership and fix flip/reveal pipeline
Establish book-playback-timeline as the sole playback owner driving the
scene through formal webgl-book:* events (not the BookLabDebug surface),
with a single reveal clock in the scene render loop and webgl-page-cache as
the only texture cache. Remove the legacy dual playback path and the
ownsPageFlipCommit gating.

Fixes:
- Flip page detached/folded at the spine: restore the raw page-cap line for
  flip geometry (matches the prototype/pre-regression), removing
  normalizeFlipLineToVisiblePage which moved the pivot off the spine arc.
- Flip textures: distance-based UVs (no horizontal compression),
  direction-aware face material (source on the camera-facing side), source
  meta derived from the visible spread (manual flips), prewarm shape fix.
- Reveal: flash removed on the static page and the flip back surface;
  spanning blocks rebuild the reveal plan at activate and continue the
  reveal on the next spread after the fill flip.
- Cache staleness is contentVersion-primary; nav clamps to spreadCount.

Docs updated to describe the intended single-owner architecture. Regression
checks updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 15:30:12 +02:00
Georg c19ebe3089 Stabilize WebGL title and timeline texture flow 2026-06-17 08:31:46 +02:00
Georg ef358c5cfd Stabilize WebGL flip reveal handoff 2026-06-10 15:10:57 +02:00
Georg 97eab216b7 Fix WebGL reveal timing and flip texture readiness 2026-06-10 13:54:54 +02:00
Georg e3d66686b9 Fix WebGL page readiness gating 2026-06-10 10:46:01 +02:00
Georg ce8147b5b1 Enforce explicit WebGL book playback timeline 2026-06-10 09:35:00 +02:00
Georg 5a84923884 Restore WebGL reveal timing diagnostics 2026-06-10 08:09:02 +02:00
Georg 10bf23b10b Add timeline owner for WebGL book playback 2026-06-10 02:00:57 +02:00
Georg b41340151d Checkpoint WebGL book playback refactor state 2026-06-10 01:07:22 +02:00
Georg 171cafeb65 Stabilize WebGL book pagination restore 2026-06-09 16:42:12 +02:00
Georg fe51410a3b Fix WebGL reveal timing and flip prewarm 2026-06-09 10:05:23 +02:00
Georg d665a0f237 Fix WebGL line reveal renderer 2026-06-09 09:02:54 +02:00
Georg 419691000c Fix WebGL page cache and flip sequencing 2026-06-08 23:08:13 +02:00
Georg a73dc5725f Add WebGL page cache and runtime checks 2026-06-08 14:39:42 +02:00
Georg efd1e6cfff Implement WebGL page reserve navigation 2026-06-08 10:25:54 +02:00
Georg 86b6fa0419 Implement WebGL book spread flip groundwork 2026-06-08 09:13:37 +02:00
Georg c86a304364 Checkpoint WebGL book reveal optimization 2026-06-08 08:19:20 +02:00
Georg 7abd3387f3 Correct WebGL dropcap texture layout 2026-06-07 17:59:01 +02:00
Georg da37608197 Reduce WebGL page texture runtime stalls 2026-06-07 17:37:31 +02:00
Georg 53c24e4fae Stabilize WebGL reveal timing 2026-06-07 16:42:09 +02:00
Georg 74ddd1de1c Gate WebGL book texture fonts 2026-06-07 14:35:00 +02:00
Georg 9434950826 Queue WebGL book reveal masks 2026-06-07 13:52:07 +02:00
Georg 7fc083fb58 Add shader page reveal checkpoint 2026-06-07 13:10:17 +02:00
Georg 7725ce9c73 Soften WebGL paper rendering 2026-06-07 12:22:26 +02:00
Georg de81a7c5c5 Stage WebGL scene loading 2026-06-07 12:08:13 +02:00
Georg 1b593c8c7b Restore WebGL book quality settings 2026-06-07 11:13:05 +02:00
Georg 777e39a650 Correct WebGL book page projection 2026-06-07 09:56:56 +02:00
Georg 081cfa9902 Optimize WebGL book texture reveal 2026-06-06 16:44:15 +02:00
Georg 1b8c8f8bce Add texture drop cap pagination 2026-06-06 15:39:53 +02:00
Georg 431e305df9 Add WebGL FPS cap and texture word reveal 2026-06-06 15:37:44 +02:00
Georg bc736513d4 Restore WebGL control overlay and page grid 2026-06-06 15:17:50 +02:00
Georg 62215b280f Start texture-space book renderer 2026-06-06 14:51:07 +02:00
Georg b734d83227 Checkpoint WebGL book renderer work 2026-06-06 14:35:37 +02:00
Georg 83ca095d54 Document WebGL page texture pipeline 2026-06-06 11:24:50 +02:00
Georg 0cb1e7c6f5 Fine tune WebGL book indirect lighting 2026-06-06 10:55:49 +02:00
Georg 965be72ea4 Tune WebGL book bounce lighting 2026-06-06 10:41:00 +02:00
Georg 0956d2ef1f Add WebGL book headbands and bounce lighting 2026-06-06 10:29:18 +02:00
Georg 925caa57bb Refine WebGL paper and spine materials 2026-06-06 08:53:29 +02:00