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}