// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//! `olive::FrameManager` — a pool of reusable [`crate::frame::Frame`]
//! buffers plus a background garbage-collection thread.
//!
//! Mirrors `src/codec/src/framemanager.h`. The C++ manager kept a pool of
//! `std::list` and a QThread that periodically dropped frames
//! whose last reference died. Rust keeps the same contract behind a
//! `Mutex`; the background thread is replaced by a dedicated GC thread
//! that drains the pool of freeable frames.
use std::sync::{Arc, Mutex, OnceLock};
use std::thread;
use std::time::Duration;
use oak_core::videoparams::VideoParams;
use crate::frame::Frame;
/// `olive::FrameManager`: singleton frame pool with background GC.
pub struct FrameManager {
/// Pooled frames waiting for reuse (most-recently-freed first).
pool: Mutex>,
/// Peak number of live frames observed (diagnostics).
peak_count: Mutex,
/// Current number of frames outstanding (not yet returned).
outstanding: Mutex,
}
impl FrameManager {
/// The process-wide FrameManager singleton.
///
/// Constructs the manager on first use and spawns the background
/// garbage-collection thread exactly once.
pub fn instance() -> &'static FrameManager {
static INSTANCE: OnceLock = OnceLock::new();
let mgr = INSTANCE.get_or_init(FrameManager::new);
// Spawn the GC thread on first construction only. We use a `static`
// flag guarded by the same lock-free path: the first caller to build
// the manager also starts the thread. Subsequent calls skip it.
spawn_gc_thread_once(mgr);
mgr
}
/// Create the empty manager.
fn new() -> Self {
FrameManager {
pool: Mutex::new(Vec::new()),
peak_count: Mutex::new(0),
outstanding: Mutex::new(0),
}
}
/// Clear the pool (dropping all cached frames).
pub fn clear(&self) {
self.pool.lock().unwrap().clear();
}
/// Create a frame with the given params (borrowed from the pool when a
/// compatible free frame exists, else freshly allocated).
pub fn create_frame(&self, params: VideoParams) -> Arc {
let frame = {
let mut pool = self.pool.lock().unwrap();
match pool.iter().position(|f| frame_matches(f, ¶ms)) {
Some(idx) => pool.swap_remove(idx),
None => Frame::with_params(params),
}
};
let mut outstanding = self.outstanding.lock().unwrap();
*outstanding += 1;
let mut peak = self.peak_count.lock().unwrap();
if *outstanding > *peak {
*peak = *outstanding;
}
Arc::new(frame)
}
/// Return a frame to the pool for reuse.
pub fn return_frame(&self, frame: Frame) {
let mut outstanding = self.outstanding.lock().unwrap();
*outstanding = outstanding.saturating_sub(1);
self.pool.lock().unwrap().push(frame);
}
/// Number of frames currently outstanding (not in the pool).
pub fn live_count(&self) -> usize {
*self.outstanding.lock().unwrap()
}
/// Peak number of live frames observed.
pub fn peak_count(&self) -> usize {
*self.peak_count.lock().unwrap()
}
/// Background GC loop; runs on the manager's dedicated thread.
///
/// # CPP-PARITY
/// `src/codec/src/framemanager.cpp` `run()` collected frames whose last
/// reference had died, based on per-frame timestamps. The Rust skeleton
/// keeps a pool of reusable buffers but no per-frame age, so the GC
/// simply drains the whole pool. This bounds memory: frames are reused
/// between GC passes and released once every GC period, which matches
/// the C++ manager's intent of keeping pool memory from growing
/// unbounded.
fn gc_loop(&self) {
self.clear();
}
}
/// Spawn the GC thread once for the process.
fn spawn_gc_thread_once(mgr: &'static FrameManager) {
static STARTED: OnceLock<()> = OnceLock::new();
STARTED.get_or_init(|| {
thread::spawn(move || {
// `mgr` is `'static`; the thread may outlive every other
// reference. Keep polling until the process exits.
loop {
thread::sleep(Duration::from_millis(5000));
mgr.gc_loop();
}
});
});
}
/// True when `frame` carries params equal to `params`.
fn frame_matches(frame: &Frame, params: &VideoParams) -> bool {
let Some(frame_params) = frame.params() else {
return false;
};
frame_params.equals(params)
}
#[cfg(test)]
mod tests {
use super::*;
use oak_core::ocioutils::PixelFormat as OakPixelFormat;
fn test_params(w: i32, h: i32) -> VideoParams {
VideoParams::new_basic(w, h, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1)
}
#[test]
fn create_and_return_tracks_counts() {
let mgr = FrameManager::new();
assert_eq!(mgr.live_count(), 0);
assert_eq!(mgr.peak_count(), 0);
let params = test_params(64, 64);
let frame = mgr.create_frame(params);
assert_eq!(mgr.live_count(), 1);
assert_eq!(mgr.peak_count(), 1);
// Return by unwrapping the single strong reference.
let frame = Arc::try_unwrap(frame).unwrap();
mgr.return_frame(frame);
assert_eq!(mgr.live_count(), 0);
assert_eq!(mgr.peak_count(), 1);
}
#[test]
fn pool_reuses_compatible_frames() {
let mgr = FrameManager::new();
let params = test_params(64, 64);
let f1 = mgr.create_frame(params);
mgr.return_frame(Arc::try_unwrap(f1).unwrap());
assert_eq!(mgr.live_count(), 0);
// A compatible request reuses the pooled buffer rather than
// allocating a new one.
let f2 = mgr.create_frame(test_params(64, 64));
assert_eq!(mgr.live_count(), 1);
assert_eq!(mgr.peak_count(), 1);
Arc::try_unwrap(f2).unwrap();
}
#[test]
fn peak_count_tracks_maximum() {
let mgr = FrameManager::new();
let p1 = test_params(64, 64);
let p2 = test_params(128, 128);
let a = mgr.create_frame(p1);
let b = mgr.create_frame(p2);
assert_eq!(mgr.live_count(), 2);
assert_eq!(mgr.peak_count(), 2);
mgr.return_frame(Arc::try_unwrap(a).unwrap());
assert_eq!(mgr.live_count(), 1);
assert_eq!(mgr.peak_count(), 2);
Arc::try_unwrap(b).unwrap();
}
#[test]
fn clear_drops_pooled_frames() {
let mgr = FrameManager::new();
let params = test_params(64, 64);
let f = mgr.create_frame(params);
mgr.return_frame(Arc::try_unwrap(f).unwrap());
assert_eq!(mgr.pool.lock().unwrap().len(), 1);
mgr.clear();
assert_eq!(mgr.pool.lock().unwrap().len(), 0);
}
}