use std::collections::{BTreeMap, HashSet};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{MTLBuffer, MTLDevice, MTLResourceOptions};
use super::buffer::Guard;
use super::residency::Residency;
use crate::{GpuError, MemoryStats, 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>>;
pub(crate) type Allocation = (Raw, usize, Option<Arc<Guard>>);
struct Cached(Raw);
struct Entry {
raw: Cached,
busy: Option<Arc<Guard>>,
}
impl Entry {
fn takeable(&self, gpu: bool) -> bool {
match &self.busy {
None => true,
Some(g) if g.idle() => true,
Some(g) => gpu && g.committed_uses(),
}
}
fn into_parts(self) -> (Raw, Option<Arc<Guard>>) {
let busy = self.busy.filter(|g| !g.idle());
(self.raw.0, busy)
}
}
unsafe impl Send for Cached {}
unsafe impl Sync for Cached {}
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>,
placeholder: Option<Cached>,
}
struct State {
small: Vec<Vec<(u64, Entry)>>,
large: BTreeMap<(usize, u64), Entry>,
next_stamp: u64,
cached: usize,
active: usize,
peak: usize,
memory_limit: usize,
cache_limit: usize,
working_set: usize,
system_allocations: u64,
mapped: usize,
residency: Residency,
}
impl State {
fn take(&mut self, size: usize, gpu: bool) -> Option<Allocation> {
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| {
let class = &mut self.small[c];
let i = class.iter().rposition(|(_, e)| e.takeable(gpu))?;
let (raw, busy) = class.remove(i).1.into_parts();
Some((raw, c * SMALL, busy))
})
} else {
let key = self
.large
.range((size, 0)..(limit, 0))
.find(|(_, e)| e.takeable(gpu))
.map(|(k, _)| *k);
key.and_then(|k| {
let (raw, busy) = self.large.remove(&k)?.into_parts();
Some((raw, k.0, busy))
})
};
if let Some((_, bucket, _)) = &found {
self.cached -= bucket;
}
found
}
fn put(&mut self, raw: Raw, size: usize, busy: Option<Arc<Guard>>) {
let stamp = self.next_stamp;
self.next_stamp += 1;
let entry = Entry {
raw: Cached(raw),
busy,
};
if size <= PAGE {
self.small[size / SMALL].push((stamp, entry));
} else {
self.large.insert((size, stamp), entry);
}
self.cached += size;
}
fn evict_to(&mut self, target: usize) {
if self.cached <= target {
return;
}
if target == 0 {
for (_, e) in self.small.iter().flatten() {
self.residency.erase(&e.raw.0);
}
for e in self.large.values() {
self.residency.erase(&e.raw.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;
}
let residency = &mut self.residency;
let mut keep = |stamp: &u64, e: &Entry| {
let evicted = evict.contains(stamp);
if evicted {
residency.erase(&e.raw.0);
}
!evicted
};
self.small
.iter_mut()
.for_each(|v| v.retain(|(stamp, e)| keep(stamp, e)));
self.large.retain(|&(_, stamp), e| keep(&stamp, e));
}
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,
placeholder: Option<Raw>,
residency: Residency,
) -> 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,
mapped: 0,
residency,
}),
placeholder: placeholder.map(Cached),
}
}
pub(crate) fn placeholder(&self) -> Option<&Raw> {
self.placeholder.as_ref().map(|Cached(raw)| raw)
}
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,
gpu: bool,
) -> Result<Option<Allocation>> {
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, busy)) = s.take(size, gpu) {
s.active += bucket;
s.peak = s.peak.max(s.active);
return Ok(Some((raw, bucket, busy)));
}
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) => {
let mut s = self.lock();
s.system_allocations += 1;
s.residency.insert(&raw, false);
Ok(Some((raw, size, None)))
}
None => {
self.lock().active -= size;
Err(GpuError::OutOfMemory(len))
}
}
}
pub(crate) fn free(&self, raw: Raw, size: usize, busy: Option<Arc<Guard>>) {
let mut s = self.lock();
s.active -= size;
s.put(raw, size, busy);
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,
wired_limit: s.residency.capacity(),
wired: s.residency.wired(),
mapped: s.mapped,
wired_mapped: s.residency.wired_mapped(),
}
}
pub(crate) fn track(&self, raw: &Raw) {
let mut s = self.lock();
s.mapped += objc2_metal::MTLResource::allocatedSize(&**raw);
s.residency.insert(raw, true);
}
pub(crate) fn untrack(&self, raw: &Raw) {
let mut s = self.lock();
s.mapped -= objc2_metal::MTLResource::allocatedSize(&**raw);
s.residency.erase(raw);
}
pub(crate) fn can_wire(&self) -> bool {
self.lock().residency.supported()
}
pub(crate) fn set_wired_limit(&self, limit: usize) -> usize {
let mut s = self.lock();
let previous = s.residency.capacity();
s.residency.resize(limit);
previous
}
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, None, Residency::new(None));
let stats = pool.stats();
assert_eq!(stats.memory_limit, 45_600_000_000); assert_eq!(stats.cache_limit, stats.memory_limit);
}
}