Module playback

Source
Expand description

Player: owns the render thread (Compositor + DecoderPool → latest Frame) and the audio thread (Mixer + DecoderPool → ring buffer → cpal WASAPI output). The wall clock is the master when playing: time = base_time + elapsed. Video frames are rendered on the project fps grid (t quantized to frame index / fps, exactly like export) and published via take_frame; ctx.request_repaint() is called whenever a new frame is ready.

Finished work is cached in RAM (Cache, ~512 MB LRU): composited frames on the CPU path, decoded layer sets on the GPU path, keyed by frame index. Replays and scrub-backs over recently seen footage are served without touching a decoder. Any edit (SetProject), canvas resize, backend or GPU-mode switch clears the cache — entries can never go stale. While playing, the pacing gap until the next frame is due is spent pre-rendering upcoming frames into the cache instead of sleeping, so a decode hiccup lands in the prefetch window and not on a visible frame.

Commands (mpsc from the UI thread): SetProject, Seek, Play, Pause, Canvas, Backend, ClearDecoders(ack), Quit. The payload (project, clock, canvas) lives in Shared, so draining the queue and reading the latest state gives “latest request wins” for free. While paused, every Seek/SetProject/Canvas re-renders the current time; the render thread blocks on the channel when idle (zero CPU). Audio thread mirrors the clock: when playing it keeps ~120 ms of mixed audio ahead in the ring; seek/pause flush the ring. Reaching the project end pauses (the UI sees is_playing() flip).

GPU mode (set_gpu(true), settings.gpu + a working engine::gpu::GpuRenderer): the render thread stops compositing and instead publishes the layers for the current instant (take_layersengine::gpu::LayerSet); the UI thread renders them with the GL context it owns. Everything else (clock, pacing, decoder pool, audio) is identical, so falling back to the CPU compositor is a single set_gpu(false).

Structs§

Cache 🔒
Byte-budgeted LRU keyed by frame index on the project fps grid. The whole cache is cleared on any change that could affect the picture, so entries never go stale.
Clock 🔒
Wall clock + canvas shared by the UI and both threads. now() flips playing off at the end.
Player
Shared 🔒

Enums§

Cmd 🔒

Constants§

BLOCK 🔒
Audio mixed per block (frames) and how far ahead of the clock the ring is kept.
CACHE_BYTES 🔒
RAM the render thread’s finished-work cache may hold. ponytail: fixed 512 MB — a settings knob (or scaling to installed RAM) is the upgrade if it matters.
FPOOL_KEEP 🔒
Recycled frame buffers kept for the CPU compositor (cache evictions feed it).
LEAD_SECS 🔒
READ_AHEAD_SECS 🔒
How far past the playhead the prefetcher keeps frames ready (seconds).
STALL_BEHIND 🔒
How far the clock may run past the newest published frame before playback is declared buffering (the UI pauses the clock and shows a spinner until the cache refills).
TRAIL_SECS 🔒
Recently shown frames kept eviction-protected behind the playhead (seconds), so a short scrub-back replays from the cache.

Functions§

audio_thread 🔒
cpu_cached 🔒
Composited frame for grid index idx — from the cache when possible, else rendered and cached. false = the render panicked (nothing was cached; the returned frame is stale/blank).
decode_layers 🔒
Everything engine::gpu needs for one timeline instant (its LayerSet contract): one bitmap per visual clip on screen at tboth clips of a transition, which the renderer draws virtually extended past their own bounds — plus the subtitle bitmap under LayerSet::SUBTITLES. Media is decoded at the size the compositor would have used (placement, capped at the source’s native size); text/shape clips are rasterised and nested sequences composited on the CPU, all at (w, h) — which is already the quality-scaled render size, so gpu::render_size must not be applied again. Adjustment layers need no bitmap (they re-process the canvas). Effects that needs_motion() also get neighbours.
decode_one 🔒
One decode into a recycled buffer. None when the decoder or the frame is unavailable.
gpu_cached 🔒
Decoded layer set for grid index idx — from the cache when possible, else decoded and cached.
guarded 🔒
Run one decode+compose/mix under catch_unwind: a panicking decoder must not kill the render or audio thread for the rest of the session (the UI would freeze on the last frame with no error). On a panic the decoders are dropped so the next call starts clean. False = the call panicked and its output is garbage.
layer_for 🔒
One clip’s layer: video/images decode, text/shapes rasterise, a nested sequence is composited on the CPU. Kinds the renderer draws without a bitmap (Adjustment) or not at all (Audio) push nothing.
lock 🔒
Poison-tolerant lock: a panicking worker must never take the UI down with it.
open_output 🔒
Default output device as 48 kHz stereo f32; None = no audio (playback still runs off the wall clock).
reclaim 🔒
Take a frame buffer nobody uses any more back into the compositor’s pool.
recycle 🔒
Take the buffers of a layer set nobody uses any more back into the spare pool.
render_thread 🔒
video_dirty_spans 🔒
Timeline spans (seconds) whose cached frames the edit old -> new can have changed. None = the change can affect any frame (or is too entangled to bound): clear everything. Edits that cannot change pixels (markers, in/out points, audio tracks, buses, planner, notes, labels, folders) produce no spans at all — the whole cache survives them.