pub use kael_gpu_budget::GpuMemoryBudget;
pub type GpuResourceId = u64;
struct Tracked {
id: GpuResourceId,
bytes: u64,
last_used: u64,
on_evict: Box<dyn FnMut() + Send + 'static>,
}
pub struct GpuMemoryManager {
budget_bytes: u64,
used_bytes: u64,
tick: u64,
next_id: GpuResourceId,
tracked: Vec<Tracked>,
}
impl GpuMemoryManager {
pub fn new(budget_bytes: u64) -> Self {
Self {
budget_bytes,
used_bytes: 0,
tick: 0,
next_id: 1,
tracked: Vec::new(),
}
}
pub fn budget_bytes(&self) -> u64 {
self.budget_bytes
}
pub fn set_budget(&mut self, budget_bytes: u64) {
self.budget_bytes = budget_bytes;
}
pub fn used_bytes(&self) -> u64 {
self.used_bytes
}
pub fn available_bytes(&self) -> u64 {
self.budget_bytes.saturating_sub(self.used_bytes)
}
pub fn tracked_count(&self) -> usize {
self.tracked.len()
}
pub fn register(
&mut self,
bytes: u64,
on_evict: impl FnMut() + Send + 'static,
) -> GpuResourceId {
let id = self.next_id;
self.next_id += 1;
self.tick += 1;
self.used_bytes = self.used_bytes.saturating_add(bytes);
self.tracked.push(Tracked {
id,
bytes,
last_used: self.tick,
on_evict: Box::new(on_evict),
});
id
}
pub fn touch(&mut self, id: GpuResourceId) -> bool {
self.tick += 1;
let tick = self.tick;
if let Some(resource) = self.tracked.iter_mut().find(|resource| resource.id == id) {
resource.last_used = tick;
true
} else {
false
}
}
pub fn release(&mut self, id: GpuResourceId) -> bool {
if let Some(index) = self.tracked.iter().position(|resource| resource.id == id) {
let resource = self.tracked.remove(index);
self.used_bytes = self.used_bytes.saturating_sub(resource.bytes);
true
} else {
false
}
}
pub fn evict_to_budget(&mut self) -> usize {
self.evict_until(self.budget_bytes)
}
pub fn ensure_available(&mut self, bytes: u64) -> usize {
let target = self.budget_bytes.saturating_sub(bytes);
self.evict_until(target)
}
fn evict_until(&mut self, target_used: u64) -> usize {
let mut evicted = 0;
while self.used_bytes > target_used {
let Some(index) = self.least_recently_used_index() else {
break;
};
let mut resource = self.tracked.remove(index);
self.used_bytes = self.used_bytes.saturating_sub(resource.bytes);
(resource.on_evict)();
evicted += 1;
}
evicted
}
fn least_recently_used_index(&self) -> Option<usize> {
self.tracked
.iter()
.enumerate()
.min_by_key(|(_, resource)| resource.last_used)
.map(|(index, _)| index)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
fn counting_callback() -> (Arc<AtomicU64>, impl FnMut() + Send + 'static) {
let counter = Arc::new(AtomicU64::new(0));
let handle = counter.clone();
(counter, move || {
handle.fetch_add(1, Ordering::SeqCst);
})
}
#[test]
fn evicts_least_recently_used_first() {
let mut manager = GpuMemoryManager::new(100);
let (count_a, evict_a) = counting_callback();
let (count_b, evict_b) = counting_callback();
let (_count_c, evict_c) = counting_callback();
let a = manager.register(40, evict_a);
let _b = manager.register(40, evict_b);
manager.register(40, evict_c);
manager.touch(a);
let evicted = manager.evict_to_budget();
assert_eq!(evicted, 1);
assert_eq!(
count_b.load(Ordering::SeqCst),
1,
"B was least recently used"
);
assert_eq!(count_a.load(Ordering::SeqCst), 0, "A was touched, kept");
assert_eq!(manager.used_bytes(), 80);
}
#[test]
fn ensure_available_frees_enough_space() {
let mut manager = GpuMemoryManager::new(100);
manager.register(30, || {});
manager.register(30, || {});
manager.register(30, || {});
assert_eq!(manager.used_bytes(), 90);
let evicted = manager.ensure_available(50);
assert!(manager.used_bytes() <= 50, "should free room for 50 bytes");
assert!(evicted >= 1);
}
#[test]
fn release_does_not_invoke_eviction_callback() {
let mut manager = GpuMemoryManager::new(100);
let (count, evict) = counting_callback();
let id = manager.register(40, evict);
assert!(manager.release(id));
assert_eq!(count.load(Ordering::SeqCst), 0);
assert_eq!(manager.used_bytes(), 0);
assert!(!manager.release(id));
}
#[test]
fn evict_to_budget_is_noop_within_budget() {
let mut manager = GpuMemoryManager::new(100);
manager.register(30, || panic!("should not evict"));
manager.register(30, || panic!("should not evict"));
assert_eq!(manager.evict_to_budget(), 0);
assert_eq!(manager.used_bytes(), 60);
}
#[test]
fn budget_snapshot_math() {
let budget = GpuMemoryBudget {
total_bytes: 1000,
used_bytes: 250,
has_unified_memory: true,
};
assert_eq!(budget.available_bytes(), 750);
assert!((budget.utilization() - 0.25).abs() < 1e-9);
}
#[test]
fn query_is_callable() {
let _ = GpuMemoryBudget::query();
}
}