Skip to main content

media_pp/core/
pool.rs

1//! Recycling the backing storage of video frames.
2//!
3//! Decoding into a fresh `ffmpeg::frame::Video` every time means allocating and
4//! freeing a full frame per buffer. [`UnboundObjectPool`] hands out
5//! [`UnboundObjectPoolRef`]s that return their storage to the pool once every
6//! downstream clone has been dropped, so steady-state playback stops allocating
7//! entirely.
8//!
9//! "Unbound" is the contract that matters: the pool grows to whatever depth
10//! downstream buffering turns out to need instead of blocking at a fixed count.
11//! Fixed-size pools do exist in this crate, but they belong to hardware decoders
12//! whose surface count the driver fixes for them — see those elements' own docs.
13
14use std::{
15    ops::{Deref, DerefMut},
16    sync::Arc,
17};
18
19use crossbeam_queue::SegQueue;
20
21/// A growable pool of reusable `T`s — lets a frame-producing element
22/// (see [`crate::elements::SwDecoder`], [`crate::elements::SwScaler`]) hand
23/// out the *same* buffers over and over instead of allocating a fresh one
24/// (and freeing the old one) every single frame. Reusing an
25/// already-allocated `ffmpeg_next::frame::Video` also lets ffmpeg's own
26/// `avcodec_receive_frame`/`sws_scale` skip *their* internal buffer
27/// allocation when the reused frame already matches — not just savings
28/// on this crate's side.
29///
30/// Has no capacity wait or "pool exhausted" result:
31/// [`UnboundObjectPool::get`] pops a previously-returned item if one's
32/// available, or calls `init` to build a fresh one on the spot otherwise.
33/// The pool therefore grows to whatever depth turns out to be needed (e.g.
34/// however many frames a downstream `Queue` lets pile up at once). As with
35/// any caller-provided closure, a panic inside `init` still propagates.
36///
37/// Deliberately a private implementation detail owned by whichever
38/// element produces the frames (a struct field, initialized once in that
39/// element's own constructor) — not something the `Pipeline` holds or
40/// passes around. Nothing outside that one element needs to know a pool
41/// is involved at all; sharing the *frames themselves* downstream still
42/// goes through the ordinary `Arc<UnboundObjectPoolRef<T>>` in
43/// [`crate::buffer::MediaBuffer::Video`].
44pub struct UnboundObjectPool<T: Send> {
45    share: Arc<Share<T>>,
46    init: Box<dyn Fn() -> T + Send + Sync>,
47}
48
49struct Share<T: Send> {
50    pool: SegQueue<Box<T>>,
51    release: Box<dyn Fn(&mut T) + Send + Sync>,
52}
53
54impl<T: Send + Sync> UnboundObjectPool<T> {
55    /// Pre-fills with `size` items built via `init` (`0` is fine — the
56    /// pool still grows on demand, it just starts out empty and pays for
57    /// the first `size`-ish `get()` calls up front instead of amortized
58    /// over the stream). `release` runs on an item right before it goes
59    /// back into the pool (e.g. to reset state) — a no-op closure is
60    /// fine if there's nothing to reset, which is the common case for a
61    /// video frame: the next `consume` overwrites every pixel (and every
62    /// piece of metadata it cares about, like `pts`) before anyone
63    /// downstream sees it again.
64    pub fn new(
65        size: usize,
66        init: impl Fn() -> T + Send + Sync + 'static,
67        release: impl Fn(&mut T) + Send + Sync + 'static,
68    ) -> UnboundObjectPool<T> {
69        let pool = SegQueue::new();
70        for _ in 0..size {
71            pool.push(Box::new(init()));
72        }
73
74        UnboundObjectPool {
75            share: Arc::new(Share {
76                pool,
77                release: Box::new(release),
78            }),
79            init: Box::new(init),
80        }
81    }
82
83    /// Never waits for a pooled item and has no exhaustion error — see the
84    /// type docs. If the pool is empty, this calls the supplied `init`
85    /// closure directly.
86    pub fn get(&self) -> UnboundObjectPoolRef<T> {
87        let item = self
88            .share
89            .pool
90            .pop()
91            .unwrap_or_else(|| Box::new((self.init)()));
92        UnboundObjectPoolRef {
93            share: self.share.clone(),
94            item: Some(item),
95        }
96    }
97
98    /// How many items are currently sitting in the pool, unused. Mainly
99    /// for tests/diagnostics — nothing in this crate depends on this
100    /// number for correctness.
101    pub fn size(&self) -> usize {
102        self.share.pool.len()
103    }
104}
105
106/// One item borrowed from an [`UnboundObjectPool`] — `Deref`/`DerefMut`
107/// to `T` for normal use, and returns itself to the pool (after running
108/// `release` on it) when dropped.
109///
110/// Meant to be wrapped in an `Arc` wherever it needs to be shared/cloned
111/// downstream (see [`crate::buffer::MediaBuffer::Video`]) — cloning the
112/// `Arc` is what lets e.g. [`crate::elements::Tee`] fan the same frame
113/// out to multiple branches cheaply, and the item only actually goes
114/// back to the pool once every one of those clones has been dropped.
115/// This type itself is deliberately *not* `Clone`: only one thing can
116/// hold the actual boxed value at a time, or "return it once the last
117/// reference drops" wouldn't mean anything.
118pub struct UnboundObjectPoolRef<T: Send> {
119    share: Arc<Share<T>>,
120    item: Option<Box<T>>,
121}
122
123impl<T: Send> Deref for UnboundObjectPoolRef<T> {
124    type Target = T;
125
126    fn deref(&self) -> &Self::Target {
127        self.item.as_deref().expect("item only taken in Drop")
128    }
129}
130
131impl<T: Send> DerefMut for UnboundObjectPoolRef<T> {
132    fn deref_mut(&mut self) -> &mut Self::Target {
133        self.item.as_deref_mut().expect("item only taken in Drop")
134    }
135}
136
137impl<T: Send> Drop for UnboundObjectPoolRef<T> {
138    fn drop(&mut self) {
139        let mut item = self.item.take().expect("item only taken once, here");
140        (self.share.release)(&mut item);
141        self.share.pool.push(item);
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use std::sync::{
148        Arc,
149        atomic::{AtomicUsize, Ordering},
150    };
151
152    use super::*;
153
154    #[test]
155    fn returned_item_is_reused_by_the_next_get() {
156        let pool = UnboundObjectPool::new(0, || 0i32, |_| {});
157        assert_eq!(pool.size(), 0);
158
159        let item = pool.get();
160        assert_eq!(pool.size(), 0, "checked out, not sitting in the pool");
161
162        drop(item);
163        assert_eq!(pool.size(), 1, "returned automatically on drop");
164
165        let _item2 = pool.get();
166        assert_eq!(pool.size(), 0, "reused, not left behind");
167    }
168
169    #[test]
170    fn get_never_fails_even_when_empty() {
171        let pool = UnboundObjectPool::new(0, || 5i32, |_| {});
172        // Nothing's ever been returned, so both of these fall back to
173        // `init` — proves `get` doesn't block/panic/return `Option` when
174        // the pool has nothing to give out.
175        assert_eq!(*pool.get(), 5);
176        assert_eq!(*pool.get(), 5);
177    }
178
179    #[test]
180    fn release_runs_before_the_item_goes_back_into_the_pool() {
181        let release_calls = Arc::new(AtomicUsize::new(0));
182        let counted = release_calls.clone();
183        let pool = UnboundObjectPool::new(
184            1,
185            || 0i32,
186            move |_| {
187                counted.fetch_add(1, Ordering::SeqCst);
188            },
189        );
190
191        drop(pool.get());
192        assert_eq!(release_calls.load(Ordering::SeqCst), 1);
193    }
194
195    #[test]
196    fn dropping_the_last_arc_clone_returns_the_item() {
197        // Mirrors how `MediaBuffer::Video` actually uses this: wrapped in
198        // an `Arc` so it can be cheaply cloned downstream (e.g. by
199        // `Tee`), only going back to the pool once every clone is gone —
200        // not on the first `Arc` that happens to drop.
201        let pool = UnboundObjectPool::new(0, || 0i32, |_| {});
202        let shared = Arc::new(pool.get());
203        let clone = shared.clone();
204
205        drop(shared);
206        assert_eq!(pool.size(), 0, "one clone still alive — not returned yet");
207
208        drop(clone);
209        assert_eq!(pool.size(), 1, "last clone dropped — now it's returned");
210    }
211}