use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use fovea::border::{Clamp, Skip};
use fovea::image::{Image, SeparableKernel};
use fovea::pixel::{Mono8, MonoF32};
use fovea::sigma;
use fovea::transform::{SeparableScratch, gaussian_blur_into};
thread_local! {
static ALLOCS: Cell<u64> = const { Cell::new(0) };
}
struct CountingAllocator;
fn record() {
let _ = ALLOCS.try_with(|c| c.set(c.get() + 1));
}
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
record();
unsafe { System.alloc(layout) }
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
record();
unsafe { System.alloc_zeroed(layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
record();
unsafe { System.realloc(ptr, layout, new_size) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static ALLOCATOR: CountingAllocator = CountingAllocator;
fn allocations_of<T>(f: impl FnOnce() -> T) -> (T, u64) {
let before = ALLOCS.with(Cell::get);
let value = f();
let after = ALLOCS.with(Cell::get);
(value, after - before)
}
#[test]
fn counter_observes_a_known_allocation() {
let (v, allocations) = allocations_of(|| vec![0u8; 4096]);
assert_eq!(v.len(), 4096);
assert!(
allocations >= 1,
"the counting allocator saw {allocations} allocations for one Vec"
);
}
#[test]
fn one_shot_blur_allocates_its_working_set() {
let src = Image::generate(64, 64, |x, y| Mono8::new(((x * 3 + y) % 256) as u8));
let mut out = Image::<Mono8>::zero(64, 64);
let sigma = sigma!(1.5);
gaussian_blur_into(&src, sigma, &Clamp, &mut out);
let (_, allocations) = allocations_of(|| {
gaussian_blur_into(&src, sigma, &Clamp, &mut out);
});
assert!(
allocations >= 3,
"expected the one-shot path to allocate its working set, saw {allocations}"
);
}
#[test]
fn second_same_size_blur_allocates_nothing() {
let src = Image::generate(64, 64, |x, y| Mono8::new(((x + y * 7) % 256) as u8));
let mut out = Image::<Mono8>::zero(64, 64);
let mut scratch = SeparableScratch::<MonoF32>::new();
let sigma = sigma!(1.5);
scratch.gaussian_blur_into(&src, sigma, &Clamp, &mut out);
let (_, allocations) = allocations_of(|| {
scratch.gaussian_blur_into(&src, sigma, &Clamp, &mut out);
});
assert_eq!(
allocations, 0,
"a second same-size blur through the scratch must not allocate",
);
}
#[test]
fn shrinking_pyramid_of_blurs_allocates_only_on_the_first_level() {
let sizes = [64usize, 32, 16, 8];
let sources: Vec<Image<Mono8>> = sizes
.iter()
.map(|&n| Image::generate(n, n, |x, y| Mono8::new(((x + y) % 256) as u8)))
.collect();
let mut outputs: Vec<Image<Mono8>> = sizes.iter().map(|&n| Image::zero(n, n)).collect();
let mut scratch = SeparableScratch::<MonoF32>::new();
let sigma = sigma!(1.0);
scratch.gaussian_blur_into(&sources[0], sigma, &Clamp, &mut outputs[0]);
for level in 1..sizes.len() {
let (_, allocations) = allocations_of(|| {
scratch.gaussian_blur_into(&sources[level], sigma, &Clamp, &mut outputs[level]);
});
assert_eq!(
allocations, 0,
"level {level} ({0}×{0}) allocated after warm-up",
sizes[level],
);
}
}
#[test]
fn separable_convolution_through_scratch_allocates_nothing_after_warmup() {
let src = Image::generate(48, 40, |x, y| MonoF32::new((x + y) as f32));
let kernel = SeparableKernel::gaussian_5();
let mut scratch = SeparableScratch::<MonoF32>::new();
let mut out = Image::<MonoF32>::zero(44, 36);
scratch.convolve_separable_into(&src, &kernel, &Skip, &mut out);
let (_, allocations) = allocations_of(|| {
scratch.convolve_separable_into(&src, &kernel, &Skip, &mut out);
});
assert_eq!(
allocations, 0,
"a second identical separable convolution must not allocate",
);
}