use std::num::NonZeroUsize;
use std::sync::Mutex;
use crate::Error;
pub(crate) trait Alloc {
type Buffer;
fn alloc(&self, len: usize) -> Result<Self::Buffer, Error>;
}
pub(crate) struct Pool<A: Alloc> {
alloc: A,
capacity: usize,
state: Mutex<State<A::Buffer>>,
}
struct State<B> {
live: usize,
idle: Vec<(usize, B)>,
}
impl<A: Alloc> Pool<A> {
pub(crate) fn new(alloc: A, capacity: NonZeroUsize) -> Self {
Self {
alloc,
capacity: capacity.get(),
state: Mutex::new(State {
live: 0,
idle: Vec::new(),
}),
}
}
pub(crate) fn capacity(&self) -> usize {
self.capacity
}
pub(crate) fn take(&self, len: usize) -> Result<A::Buffer, Error> {
let mut state = self.state.lock().expect("GPU frame pool poisoned");
let fits = state
.idle
.iter()
.enumerate()
.filter(|(_, (have, _))| *have >= len)
.min_by_key(|(_, (have, _))| *have)
.map(|(index, _)| index);
if let Some(index) = fits {
let (_, buffer) = state.idle.swap_remove(index);
state.live += 1;
return Ok(buffer);
}
if state.live + state.idle.len() >= self.capacity {
if state.live >= self.capacity {
return Err(Error::Unsupported(format!(
"GPU frame pool capacity {} exhausted; drop a frame before converting another",
self.capacity
)));
}
let smallest = state
.idle
.iter()
.enumerate()
.min_by_key(|(_, (have, _))| *have)
.map(|(index, _)| index)
.expect("an idle buffer exists when live is under capacity");
state.idle.swap_remove(smallest);
}
let buffer = self.alloc.alloc(len)?;
state.live += 1;
Ok(buffer)
}
pub(crate) fn put(&self, len: usize, buffer: A::Buffer) {
let mut state = self.state.lock().expect("GPU frame pool poisoned");
state.live -= 1;
state.idle.push((len, buffer));
}
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use super::*;
struct Counting(Cell<usize>);
impl Alloc for Counting {
type Buffer = usize;
fn alloc(&self, len: usize) -> Result<usize, Error> {
self.0.set(self.0.get() + 1);
Ok(len)
}
}
fn pool(capacity: usize) -> Pool<Counting> {
Pool::new(Counting(Cell::new(0)), NonZeroUsize::new(capacity).unwrap())
}
#[test]
fn a_full_pool_refuses_rather_than_grows() {
let pool = pool(2);
assert_eq!(pool.capacity(), 2);
let a = pool.take(100).unwrap();
let _b = pool.take(100).unwrap();
let err = pool.take(100).unwrap_err();
assert!(matches!(err, Error::Unsupported(_)), "{err}");
assert_eq!(pool.alloc.0.get(), 2);
pool.put(100, a);
pool.take(100).unwrap();
assert_eq!(pool.alloc.0.get(), 2, "a returned buffer is reused, not reallocated");
}
#[test]
fn reuse_picks_the_smallest_buffer_that_fits() {
let pool = pool(3);
let small = pool.take(10).unwrap();
let medium = pool.take(50).unwrap();
let large = pool.take(100).unwrap();
pool.put(10, small);
pool.put(50, medium);
pool.put(100, large);
assert_eq!(pool.take(40).unwrap(), 50);
assert_eq!(pool.take(40).unwrap(), 100, "the next fit, not a fresh allocation");
assert_eq!(pool.alloc.0.get(), 3);
}
#[test]
fn an_idle_buffer_too_small_is_replaced_within_capacity() {
let pool = pool(2);
let _live = pool.take(10).unwrap();
let idle = pool.take(10).unwrap();
pool.put(10, idle);
assert_eq!(pool.take(100).unwrap(), 100);
assert_eq!(pool.alloc.0.get(), 3);
assert!(pool.take(10).is_err(), "both buffers are live now");
}
#[test]
fn an_allocation_failure_leaves_the_count_intact() {
struct Failing;
impl Alloc for Failing {
type Buffer = ();
fn alloc(&self, _len: usize) -> Result<(), Error> {
Err(Error::Codec(anyhow::anyhow!("out of device memory")))
}
}
let pool = Pool::new(Failing, NonZeroUsize::new(1).unwrap());
assert!(matches!(pool.take(8), Err(Error::Codec(_))));
assert_eq!(pool.state.lock().unwrap().live, 0);
}
}