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_layers →
engine::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()flipsplayingoff 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::gpuneeds for one timeline instant (itsLayerSetcontract): one bitmap per visual clip on screen att— both clips of a transition, which the renderer draws virtually extended past their own bounds — plus the subtitle bitmap underLayerSet::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, sogpu::render_sizemust not be applied again. Adjustment layers need no bitmap (they re-process the canvas). Effects thatneeds_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->newcan 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.