Files
SubMiner/docs/architecture/subtitle-overlay-priming.md
T
sudacode 2003efa235 fix(subtitles): release prefetch pause across all priming paths
The repeated-subtitle pause leak was only fixed for ordinary subtitle
changes. Startup autoplay priming and visible-overlay priming pause the same
way and also ignored whether any tokenization was scheduled, so a cache miss
on text the controller already holds (mining a card while the line is on
screen) left prefetching idle for the rest of the cue.

Pause and release are now one operation via pausePrefetchUntilEmit, and both
controller entry points report whether an emit is expected. A repeat arriving
while a run is already in flight keeps the pause, since that run still emits.

Also invalidate the character dictionary lookups centrally from the sync
completion handler instead of at three manager call sites. Ordinary selection
sync never invalidated them, so a stale non-null name candidate list could
skip a newly added name for up to five seconds; a missing list falls back to
the exhaustive scan, but a stale one does not. Ordering matters: the
invalidation runs before the subtitle refreshes so they re-tokenize against
the new dictionary content.

Docs: subtitle-overlay-priming no longer claims every subtitle change calls
onSeek().
2026-08-04 01:56:44 -07:00

141 lines
8.9 KiB
Markdown

<!-- read_when: changing visible overlay startup, Linux/X11 overlay window shape, mpv subtitle callbacks, or subtitle tokenization emission -->
# Subtitle Overlay Priming
Status: active
Last verified: 2026-08-04
Owner: Kyle Yasuda
Read when: debugging subtitle state or blank Linux/X11 overlay windows when the visible overlay is shown or recreated
Visible-overlay subtitle priming fills the overlay from mpv's current subtitle properties before
waiting for the next live mpv subtitle event. This avoids a stale or blank overlay when the user
manually shows the visible overlay while playback is already sitting on a subtitle.
On Linux/X11, visible-overlay show and later mpv bounds refreshes restore the Electron window shape
to the full current overlay bounds. Electron's `BrowserWindow.setShape()` applies a bounding shape,
not an input-only region; stale shapes can leave a mapped 1920x1080 overlay with smaller X11 shape
extents such as `800x600+0+0`, so renderer and websocket subtitle state are correct while bottom
subtitles do not draw.
## Entry Points
- `src/main.ts` calls `primeCurrentSubtitleForVisibleOverlay()` when manual visible-overlay show
paths run.
- `src/main.ts` calls `restoreVisibleOverlayWindowShapeForShow()` before visible-overlay show
actions on Linux, and `resetVisibleOverlayInputState()` restores a full shape instead of applying
an empty shape.
- `src/main.ts` also restores the Linux/X11 shape after applying mpv overlay bounds, so a newly
created 800x600 hidden Electron window cannot keep clipping after it is resized to mpv geometry.
- `primeCurrentSubtitleForVisibleOverlay()` delegates to
`primeVisibleOverlaySubtitleFromMpv()` in `src/main/runtime/current-subtitle-snapshot.ts`.
- `restoreVisibleOverlayWindowShapeForShow()` delegates to `restoreLinuxOverlayWindowShape()` in
`src/main/runtime/linux-overlay-window-shape.ts`.
- Inputs are callback deps, not globals: `getMpvClient`, `setCurrentSubText`,
`getCurrentSubtitleData`, `consumeCachedSubtitle`, `onSubtitleChange`,
`refreshCurrentSubtitle`, `emitSubtitle`, optional secondary-subtitle callbacks, and `logDebug`.
## Primary Subtitle Flow
1. Read the connected mpv client through `getMpvClient()`. Exit if no connected client.
2. Request mpv `sub-text`. On failure, log a
`[visible-overlay-subtitle-prime] failed to read sub-text` debug line and exit.
3. Normalize non-string `sub-text` to `''`, then call `setCurrentSubText(text)` so app state
matches mpv before any overlay emission.
4. Empty text: call `onSubtitleChange(text)`, emit `{ text, tokens: null }`, then prime secondary
subtitles.
5. Current cached payload: if `getCurrentSubtitleData()?.text === text`, call
`emitSubtitle(payload)` and `refreshCurrentSubtitle(text)`, then prime secondary subtitles.
6. Tokenization cache hit: call `consumeCachedSubtitle(text)`, `onSubtitleChange(text)`, and
`emitSubtitle(cachedPayload)`, then prime secondary subtitles.
7. Cache miss: call `refreshCurrentSubtitle(text)`. Normal processing emits a plain payload
synchronously, then replaces it with the tokenized payload when ready.
Both `onSubtitleChange` and `refreshCurrentSubtitle` pause `subtitlePrefetchService` and then call
the matching `subtitleProcessingController` method, giving the visible overlay priority over
background prefetch work. Prefetch is not re-centered here: restarting the run per line
(`onSeek`) discarded the in-flight tokenization every time the subtitle changed, so only real
seeks restart it (see `onTimePosUpdate` in `src/main.ts`).
The pause is released by the emit that carries the tokenized payload. Both controller methods
return whether an emit is expected, and the caller resumes immediately when it is not — otherwise
a repeated subtitle (which schedules no work) would leave prefetching idle for the rest of the
cue.
## Live Cue Delivery
- A tokenization cache miss emits the plain cue synchronously. Tokenization remains serialized so
live work does not contend for Yomitan state.
- If a newer cue arrives while an older line is still tokenizing, the newer plain cue or empty
clear payload is emitted immediately. The older tokenization result is dropped before it can
replace the current cue.
- The current cue upgrades in place when its tokens and annotations are ready. This can reflow text
or character images, but cue visibility does not wait for that work.
## Emitted State
- `emitSubtitle(payload)` maps to `emitSubtitlePayload(payload)`. Overlay windows and annotation
websocket listeners receive both the immediate plain cue and its later annotation upgrade.
- The basic subtitle websocket receives the immediate plain cue only. Because its serialized
payload discards annotations, the later upgrade would be an identical duplicate and is skipped
when text and cue timing match.
- Secondary priming reads mpv `secondary-sub-text`, stores it in
`mpvClient.currentSecondarySubText`, and broadcasts `secondary-subtitle:set` to overlay windows.
- If secondary `requestProperty` fails, the primary flow stays complete and only a debug line is
written.
## Startup Ready Release
- `mpv.pauseUntilOverlayReady` waits for tokenization warmup plus visible-overlay readiness before
releasing the mpv startup gate.
- Visible-overlay startup creates the tray and visible overlay shell before tokenization and
annotation warmups continue. Cold `--start --background --managed-playback` launches still handle
initial args before the deferred Yomitan wait.
- Overlay-routed startup notifications are queued in the main process until an overlay window has
finished loading. Progress notifications with the same id are upserted so spinner ticks do not
flood a cold-start overlay, while events with distinct history ids are retained for phase-level
history such as character dictionary checking/building/importing.
- The mpv plugin has a 30-second fallback for cold starts; app-side retry/release budgets match that
window so readiness can still arrive before fallback resumes playback.
- If mpv is already on a subtitle, SubMiner still prefers the resolved current subtitle payload and
waits for a fresh measured subtitle rectangle before signaling readiness.
- If the startup subtitle has no cached annotations yet, autoplay priming emits a raw first-paint
subtitle payload before background tokenization. The tokenized payload replaces it when ready, but
the visible overlay can paint and measure the line before the mpv startup gate resumes playback.
- If startup `sub-text` is temporarily empty, autoplay priming refreshes the active subtitle source
and then awaits cue-based priming before synthetic warm readiness can proceed. A parsed current or
imminent cue is treated as the startup subtitle so the visible overlay can paint and measure it
before playback resumes.
- If mpv is before the first subtitle, SubMiner sends a synthetic warm readiness payload after
tokenization warmup and visible overlay content-ready. This releases playback without waiting for
a later subtitle event that cannot happen while mpv is paused.
- After a synthetic warm readiness release, SubMiner briefly polls/refreshes the current subtitle
again. This covers Linux/mpv startup cases where `sub-text` is still empty while paused but becomes
available right after playback resumes, without waiting for the next subtitle property change.
## Linux/X11 Window Shape
- `restoreLinuxOverlayWindowShape()` reads `BrowserWindow.getBounds()` and calls `setShape()` with
one full-window rectangle: `{ x: 0, y: 0, width, height }`.
- Restore the shape after `setBounds()`/mpv geometry updates, not only before showing the overlay.
Manual startup can create the hidden overlay at Electron's default 800x600 size before the window
tracker applies the real mpv bounds.
- Do not use `setShape([])` as a passive reset for the visible overlay. On the tested X11/XWayland
path, empty or stale bounding shapes produced invisible or clipped subtitles even though the
overlay window remained mapped above mpv.
- Pointer pass-through should continue to use `setIgnoreMouseEvents(true, { forward: true })` and
the Linux cursor-poll fallback, not bounding-shape clipping.
- Visible-overlay show/reset marks Linux pointer passthrough state dirty even when the logical
interaction state is already inactive. The next cursor-poll tick must still reapply
`setIgnoreMouseEvents(true, { forward: true })`; otherwise a newly shown Electron overlay can keep
full-window input capture and block both mpv and overlay controls before the first subtitle
measurement.
- Visible-overlay show also starts a short Linux input grace before the first content measurement.
Native Wayland surfaces can become inert while `setIgnoreMouseEvents(true)` is active; keeping the
overlay interactive during this startup gap lets notifications and overlay mouse bindings work
until subtitle/sidebar/notification rectangles are reported.
## Config And Migration
No config or schema migration. This workflow reuses existing mpv properties, overlay IPC events,
subtitle tokenization cache, and prefetch controls.