use std::collections::{BTreeMap, HashSet};
use std::sync::{Mutex, MutexGuard, PoisonError};
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{MTLBuffer, MTLDevice, MTLResourceOptions};
use crate::{GpuError, Result};
pub(crate) const PAGE: usize = 16 * 1024;
const SMALL: usize = 256;
const SMALL_CLASSES: usize = PAGE / SMALL + 1;
pub(crate) type Raw = Retained<ProtocolObject<dyn MTLBuffer>>;
struct Cached(Raw);
unsafe impl Send for Cached {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MemoryStats {
pub active: usize,
pub peak: usize,
pub cache: usize,
pub memory_limit: usize,
pub cache_limit: usize,
pub system_allocations: u64,
}
pub(crate) fn rounded(len: usize) -> usize {
match len {
0 => 0,
n if n > PAGE => n.div_ceil(PAGE) * PAGE,
n => n.div_ceil(SMALL) * SMALL,
}
}
pub(crate) struct Pool {
state: Mutex<State>,
}
struct State {
small: Vec<Vec<(u64, Cached)>>,
large: BTreeMap<(usize, u64), Cached>,
next_stamp: u64,
cached: usize,
active: usize,
peak: usize,
memory_limit: usize,
cache_limit: usize,
working_set: usize,
system_allocations: u64,
}
impl State {
fn take(&mut self, size: usize) -> Option<(Raw, usize)> {
let limit = (2 * size).min(size + 2 * PAGE);
let found = if size <= PAGE {
let last = ((limit - 1) / SMALL).min(SMALL_CLASSES - 1);
(size / SMALL..=last)
.find_map(|c| self.small[c].pop().map(|(_, Cached(raw))| (raw, c * SMALL)))
} else {
let key = self
.large
.range((size, 0)..(limit, 0))
.next()
.map(|(k, _)| *k);
key.and_then(|k| self.large.remove(&k).map(|Cached(raw)| (raw, k.0)))
};
if let Some((_, bucket)) = &found {
self.cached -= bucket;
}
found
}
fn put(&mut self, raw: Raw, size: usize) {
let stamp = self.next_stamp;
self.next_stamp += 1;
if size <= PAGE {
self.small[size / SMALL].push((stamp, Cached(raw)));
} else {
self.large.insert((size, stamp), Cached(raw));
}
self.cached += size;
}
fn evict_to(&mut self, target: usize) {
if self.cached <= target {
return;
}
if target == 0 {
self.small.iter_mut().for_each(Vec::clear);
self.large.clear();
self.cached = 0;
return;
}
let mut ages: Vec<(u64, usize)> = self
.small
.iter()
.enumerate()
.flat_map(|(class, v)| v.iter().map(move |(stamp, _)| (*stamp, class * SMALL)))
.chain(self.large.keys().map(|&(size, stamp)| (stamp, size)))
.collect();
ages.sort_unstable();
let mut evict = HashSet::new();
for (stamp, size) in ages {
if self.cached <= target {
break;
}
evict.insert(stamp);
self.cached -= size;
}
self.small
.iter_mut()
.for_each(|v| v.retain(|(stamp, _)| !evict.contains(stamp)));
self.large.retain(|&(_, stamp), _| !evict.contains(&stamp));
}
fn gc_limit(&self) -> usize {
((self.working_set as f64 * 0.95) as usize).min(self.memory_limit)
}
}
impl Pool {
pub(crate) fn new(working_set: u64, ram: u64) -> Self {
let memory_limit = (1.5 * working_set as f64).min(0.95 * ram as f64) as usize;
Pool {
state: Mutex::new(State {
small: (0..SMALL_CLASSES).map(|_| Vec::new()).collect(),
large: BTreeMap::new(),
next_stamp: 0,
cached: 0,
active: 0,
peak: 0,
memory_limit,
cache_limit: memory_limit,
working_set: working_set as usize,
system_allocations: 0,
}),
}
}
fn lock(&self) -> MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(crate) fn alloc(
&self,
device: &ProtocolObject<dyn MTLDevice>,
len: usize,
) -> Result<Option<(Raw, usize)>> {
let size = rounded(len);
if size == 0 {
return Ok(None);
}
let mut s = self.lock();
if s.active + size > s.memory_limit {
s.evict_to(0);
return Err(GpuError::OutOfMemory(len));
}
if let Some((raw, bucket)) = s.take(size) {
s.active += bucket;
s.peak = s.peak.max(s.active);
return Ok(Some((raw, bucket)));
}
let gc = s.gc_limit();
if s.active + s.cached + size >= gc {
let target = gc.saturating_sub(s.active + size);
s.evict_to(target);
}
s.active += size;
s.peak = s.peak.max(s.active);
drop(s);
let options =
MTLResourceOptions::StorageModeShared | MTLResourceOptions::HazardTrackingModeUntracked;
match device.newBufferWithLength_options(size, options) {
Some(raw) => {
self.lock().system_allocations += 1;
Ok(Some((raw, size)))
}
None => {
self.lock().active -= size;
Err(GpuError::OutOfMemory(len))
}
}
}
pub(crate) fn free(&self, raw: Raw, size: usize, reusable: bool) {
let mut s = self.lock();
s.active -= size;
if reusable {
s.put(raw, size);
let limit = s.cache_limit;
s.evict_to(limit);
}
}
pub(crate) fn stats(&self) -> MemoryStats {
let s = self.lock();
MemoryStats {
active: s.active,
peak: s.peak,
cache: s.cached,
memory_limit: s.memory_limit,
cache_limit: s.cache_limit,
system_allocations: s.system_allocations,
}
}
pub(crate) fn set_memory_limit(&self, limit: usize) -> usize {
std::mem::replace(&mut self.lock().memory_limit, limit)
}
pub(crate) fn set_cache_limit(&self, limit: usize) -> usize {
let mut s = self.lock();
let previous = std::mem::replace(&mut s.cache_limit, limit);
s.evict_to(limit);
previous
}
pub(crate) fn clear_cache(&self) {
self.lock().evict_to(0);
}
pub(crate) fn reset_peak(&self) {
let mut s = self.lock();
s.peak = s.active;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rounding() {
assert_eq!(rounded(0), 0);
assert_eq!(rounded(1), 256);
assert_eq!(rounded(256), 256);
assert_eq!(rounded(257), 512);
assert_eq!(rounded(PAGE), PAGE);
assert_eq!(rounded(PAGE + 1), 2 * PAGE);
}
#[test]
fn default_limits_follow_mlx() {
let pool = Pool::new(40_000_000_000, 48_000_000_000);
let stats = pool.stats();
assert_eq!(stats.memory_limit, 45_600_000_000); assert_eq!(stats.cache_limit, stats.memory_limit);
}
}