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) -> Option<usize> {
crate::memory::allocation_size(len)
}
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,
peak_with_mapped: usize,
memory_limit: usize,
cache_limit: usize,
working_set: usize,
system_allocations: u64,
mapped: usize,
residency: Residency,
}
impl State {
fn note_peaks(&mut self) {
self.peak = self.peak.max(self.active);
self.peak_with_mapped = self.peak_with_mapped.max(self.active + self.mapped);
}
fn take(&mut self, size: usize, gpu: bool) -> Option<Allocation> {
let limit = size.saturating_mul(2).min(size.saturating_add(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,
peak_with_mapped: 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).ok_or(GpuError::OutOfMemory(len))?;
if size == 0 {
return Ok(None);
}
if size > device.maxBufferLength() {
return Err(GpuError::OutOfMemory(len));
}
let mut s = self.lock();
let fits = |s: &State, size: usize| {
s.active
.checked_add(size)
.filter(|&total| total.saturating_add(s.mapped) <= s.memory_limit)
};
let Some(after) = fits(&s, size) else {
s.evict_to(0);
return Err(GpuError::OutOfMemory(len));
};
if let Some((raw, bucket, busy)) = s.take(size, gpu) {
if let Some(total) = fits(&s, bucket) {
s.active = total;
s.note_peaks();
return Ok(Some((raw, bucket, busy)));
}
s.put(raw, bucket, busy);
}
let gc = s.gc_limit();
let used = after.saturating_add(s.mapped);
if used.saturating_add(s.cached) >= gc {
let target = gc.saturating_sub(used);
s.evict_to(target);
}
s.active = after;
s.note_peaks();
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,
active_with_mapped: s.active + s.mapped,
peak_with_mapped: s.peak_with_mapped,
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) -> Result<()> {
let size = objc2_metal::MTLResource::allocatedSize(&**raw);
let mut s = self.lock();
let total = s.active.saturating_add(s.mapped).saturating_add(size);
if total > s.memory_limit {
return Err(GpuError::OutOfMemory(size));
}
s.mapped += size;
s.note_peaks();
s.residency.insert(raw, true);
Ok(())
}
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;
s.peak_with_mapped = s.active + s.mapped;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rounding() {
assert_eq!(rounded(0), Some(0));
assert_eq!(rounded(1), Some(256));
assert_eq!(rounded(256), Some(256));
assert_eq!(rounded(257), Some(512));
assert_eq!(rounded(PAGE), Some(PAGE));
assert_eq!(rounded(PAGE + 1), Some(2 * PAGE));
assert_eq!(rounded(usize::MAX - PAGE + 1), Some(usize::MAX - PAGE + 1));
assert_eq!(rounded(usize::MAX - PAGE + 2), None);
}
#[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);
}
}