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

8.9 KiB

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.