#![cfg(target_os = "macos")]
use nuvec_gpu::{Arg, Device, GpuError};
const PAGE: usize = 16 * 1024;
fn device() -> Option<Device> {
let device = Device::system_default();
if device.is_none() {
eprintln!("skipped: no Metal device");
}
device
}
#[test]
fn steady_state_reuses_one_buffer() {
let Some(device) = device() else { return };
for _ in 0..10_000 {
let buffer = device.new_buffer(4096).unwrap();
drop(buffer);
}
let stats = device.memory();
assert_eq!(stats.system_allocations, 1);
assert_eq!((stats.active, stats.cache), (0, 4096));
}
#[test]
fn stats_are_exact() {
let Some(device) = device() else { return };
let a = device.new_buffer(1000).unwrap(); let b = device.new_buffer(PAGE + 1).unwrap(); let stats = device.memory();
assert_eq!(
(stats.active, stats.peak, stats.cache),
(1024 + 2 * PAGE, 1024 + 2 * PAGE, 0)
);
drop(a);
let stats = device.memory();
assert_eq!(
(stats.active, stats.peak, stats.cache),
(2 * PAGE, 1024 + 2 * PAGE, 1024)
);
device.reset_peak();
assert_eq!(device.memory().peak, 2 * PAGE);
drop(b);
device.clear_cache();
let stats = device.memory();
assert_eq!((stats.active, stats.cache), (0, 0));
}
#[test]
fn large_cached_buffers_are_not_wasted_on_small_requests() {
let Some(device) = device() else { return };
drop(device.new_buffer(1 << 20).unwrap());
let small = device.new_buffer(256).unwrap();
let stats = device.memory();
assert_eq!(
stats.system_allocations, 2,
"1 MiB is more than twice 256 B: not reused"
);
assert_eq!(stats.cache, 1 << 20);
drop(small);
let near = device.new_buffer((1 << 20) - PAGE).unwrap();
assert_eq!(device.memory().system_allocations, 2);
assert_eq!(near.len(), (1 << 20) - PAGE);
}
#[test]
fn cache_limit_evicts_oldest_first() {
let Some(device) = device() else { return };
device.set_cache_limit(3 * PAGE);
let buffers: Vec<_> = (1..=4)
.map(|pages| device.new_buffer(pages * PAGE).unwrap())
.collect();
drop(buffers); let stats = device.memory();
assert!(stats.cache <= 3 * PAGE, "{stats:?}");
assert_eq!(
stats.cache, 0,
"the 4-page buffer alone exceeds the limit, older ones were evicted before it"
);
device.set_cache_limit(10 * PAGE);
let kept: Vec<_> = [1, 2, 3]
.iter()
.map(|&p| device.new_buffer(p * PAGE).unwrap())
.collect();
drop(kept);
assert_eq!(device.memory().cache, 6 * PAGE);
assert_eq!(
device.set_cache_limit(2 * PAGE),
10 * PAGE,
"setter returns the previous limit"
);
assert_eq!(
device.memory().cache,
0,
"3 pages (newest) alone exceed 2 pages; everything older went first"
);
}
#[test]
fn gc_threshold_releases_cache_before_allocating() {
let Some(device) = device() else { return };
device.set_memory_limit(4 * PAGE);
drop(device.new_buffer(2 * PAGE).unwrap());
assert_eq!(device.memory().cache, 2 * PAGE);
let big = device.new_buffer(3 * PAGE).unwrap(); let stats = device.memory();
assert_eq!((stats.active, stats.cache), (3 * PAGE, 0));
drop(big);
}
#[test]
fn memory_limit_is_enforced() {
let Some(device) = device() else { return };
device.set_memory_limit(2 * PAGE);
let a = device.new_buffer(PAGE).unwrap();
assert_eq!(
device.new_buffer(2 * PAGE).unwrap_err(),
GpuError::OutOfMemory(2 * PAGE)
);
assert_eq!(
device.memory().active,
PAGE,
"a failed allocation reserves nothing"
);
drop(a);
assert!(device.new_buffer(2 * PAGE).is_ok());
}
#[test]
fn memory_in_use_is_reused_only_by_gpu_work_ordered_after_it() {
let Some(device) = device() else { return };
let n = 1 << 20;
let a = device.buffer_from(&vec![0u8; n * 4]).unwrap();
let add = device.pipeline("nuvec_add_f32").unwrap();
let encode = |out: &mut nuvec_gpu::Buffer| {
let mut cmd = device.ordered_command_buffer().unwrap();
cmd.dispatch(
&add,
&[
Arg::read(&a),
Arg::read(&a),
Arg::write(out),
Arg::bytes(&(n as u32).to_le_bytes()),
],
n,
)
.unwrap();
cmd
};
device.clear_cache();
let mut out = device.new_buffer(n * 4).unwrap();
let cmd = encode(&mut out);
drop(out); let stats = device.memory();
assert_eq!((stats.active, stats.cache), (a.len(), a.len()));
let allocations = stats.system_allocations;
drop((
device.new_buffer(n * 4).unwrap(),
device.new_gpu_buffer(n * 4).unwrap(),
));
assert_eq!(device.memory().system_allocations, allocations + 2);
device.clear_cache();
let mut out = device.new_buffer(n * 4).unwrap();
drop(cmd); let pending = encode(&mut out).commit_then(|r| r.unwrap());
drop(out);
let allocations = device.memory().system_allocations;
let reused = device.new_gpu_buffer(n * 4).unwrap();
assert_eq!(
device.memory().system_allocations,
allocations,
"reused by GPU work"
);
assert_eq!(reused.contents().unwrap().len(), n * 4);
drop(reused);
pending.wait();
}
#[test]
fn zero_bytes_cost_nothing() {
let Some(device) = device() else { return };
let empty = device.new_buffer(0).unwrap();
assert!(empty.contents().unwrap().is_empty());
let stats = device.memory();
assert_eq!((stats.active, stats.system_allocations), (0, 0));
}
#[test]
fn concurrent_alloc_and_free() {
let Some(device) = device() else { return };
std::thread::scope(|s| {
for t in 0..8 {
let device = device.clone();
s.spawn(move || {
for i in 0..500 {
let mut b = device.new_buffer(256 * (1 + (i + t) % 64)).unwrap();
b.contents_mut().unwrap()[0] = t as u8;
}
});
}
});
assert_eq!(device.memory().active, 0);
}
#[test]
fn shared_device_is_a_singleton() {
let (Some(a), Some(b)) = (Device::shared(), Device::shared()) else {
return;
};
assert!(std::ptr::eq(a, b));
}
#[test]
fn wired_limit_wires_buffers_up_to_it() {
let Some(device) = device() else { return };
let wired = |d: &Device| d.memory().wired;
assert_eq!((device.memory().wired_limit, wired(&device)), (0, 0)); let too_much = device.info().recommended_working_set as usize + 1;
assert!(matches!(
device.set_wired_limit(too_much),
Err(GpuError::InvalidArgument(_))
));
assert_eq!(device.set_wired_limit(4 * PAGE).unwrap(), 0);
let mut buffers: Vec<_> = (0..3)
.map(|_| device.new_buffer(2 * PAGE).unwrap())
.collect();
if !device.can_wire() {
assert_eq!(wired(&device), 0);
return;
}
assert_eq!(wired(&device), 4 * PAGE);
device.set_wired_limit(2 * PAGE).unwrap();
assert_eq!(wired(&device), 2 * PAGE);
assert_eq!(device.set_wired_limit(8 * PAGE).unwrap(), 2 * PAGE);
assert_eq!(wired(&device), 6 * PAGE);
let add = device.pipeline("nuvec_binary_add_float32_c").unwrap();
let n = 2 * PAGE / 4;
let (a, b) = (buffers.pop().unwrap(), buffers.pop().unwrap());
let (offset, scalars) = (0i64.to_le_bytes(), 0u32.to_le_bytes());
let mut cmd = device.command_buffer().unwrap();
cmd.dispatch(
&add,
&[
Arg::read(&a),
Arg::read(&b),
Arg::write(&mut buffers[0]),
Arg::bytes(&offset),
Arg::bytes(&offset),
Arg::bytes(&scalars),
],
n,
)
.unwrap();
cmd.commit().wait().unwrap();
drop((a, b, buffers));
assert_eq!(wired(&device), 6 * PAGE);
device.clear_cache();
assert_eq!(wired(&device), 0);
let path = std::env::temp_dir().join(format!("nuvec-wired-{}", std::process::id()));
std::fs::write(&path, vec![1u8; 3 * PAGE]).unwrap();
let mapped = device.map_file(&path).unwrap();
assert_eq!(wired(&device), 3 * PAGE);
drop(mapped);
assert_eq!(wired(&device), 0);
std::fs::remove_file(&path).unwrap();
device.set_wired_limit(0).unwrap();
}