use crate::declare_kernel;
use crate::types::{Configuration, ImageBuffer, MAX_MIP};
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct MipDownsampleParams {
pub src_lod: u32,
pub _pad0: u32,
pub _pad1: u32,
pub _pad2: u32,
}
declare_kernel!(mip_downsample, MipDownsampleParams);
pub unsafe fn generate_mips(config: &Configuration) -> Result<(), &'static str> {
let levels = config.outgoing_mip_levels.max(1).min(MAX_MIP);
if levels <= 1 {
return Ok(());
}
let Some(mip_ptr) = config.outgoing_data else {
return Err("generate_mips: outgoing_data is None");
};
for lod in 0..(levels - 1) {
let dst_w = (config.outgoing_width >> (lod + 1)).max(1);
let dst_h = (config.outgoing_height >> (lod + 1)).max(1);
let mut pass_cfg = *config;
pass_cfg.width = dst_w;
pass_cfg.height = dst_h;
pass_cfg.outgoing_data = Some(mip_ptr);
pass_cfg.incoming_data = Some(mip_ptr);
pass_cfg.dest_data = mip_ptr;
pass_cfg.dest_pitch_px = config.outgoing_pitch_px;
let params = MipDownsampleParams {
src_lod: lod,
_pad0: 0,
_pad1: 0,
_pad2: 0,
};
if !pass_cfg.device_handle.is_null() {
unsafe { mip_downsample(&pass_cfg, params)? };
} else {
unsafe {
crate::cpu::render::render_cpu_direct(
"mip_downsample",
&pass_cfg,
MIP_DOWNSAMPLE_CPU_DISPATCH_TILE,
¶ms,
);
}
}
}
Ok(())
}
pub unsafe fn prepare_mip_source(config: &mut Configuration, tag: u32) -> Result<ImageBuffer, &'static str> {
let levels = config.outgoing_mip_levels.max(1).min(MAX_MIP);
if levels <= 1 {
return Err("prepare_mip_source: outgoing_mip_levels must be >= 2");
}
let w = config.outgoing_width;
let h = config.outgoing_height;
let bpp = config.bytes_per_pixel;
let src_ptr = config.outgoing_data.ok_or("prepare_mip_source: outgoing_data is None")?;
let src_pitch_bytes = (config.outgoing_pitch_px as u32).saturating_mul(bpp);
let dst_pitch_bytes = w.saturating_mul(bpp);
if config.device_handle.is_null() {
let buf = crate::cpu::buffer::get_or_create_with_mips(w, h, bpp, levels, tag);
if buf.buf.raw.is_null() {
return Err("prepare_mip_source: CPU allocator returned null");
}
unsafe {
if src_pitch_bytes == dst_pitch_bytes {
std::ptr::copy_nonoverlapping(
src_ptr as *const u8,
buf.buf.raw as *mut u8,
(dst_pitch_bytes as usize) * (h as usize),
);
} else {
for y in 0..(h as usize) {
std::ptr::copy_nonoverlapping(
(src_ptr as *const u8).add(y * src_pitch_bytes as usize),
(buf.buf.raw as *mut u8).add(y * dst_pitch_bytes as usize),
dst_pitch_bytes as usize,
);
}
}
}
config.outgoing_data = Some(buf.buf.raw);
config.outgoing_pitch_px = w as i32;
return Ok(buf);
}
#[cfg(gpu_backend = "metal")]
unsafe {
use crate::DeviceHandleInit;
let buf = crate::gpu::backends::metal::buffer::get_or_create_with_mips(
DeviceHandleInit::FromPtr(config.device_handle),
w,
h,
bpp,
levels,
tag,
);
if buf.buf.raw.is_null() {
return Err("prepare_mip_source: Metal allocator returned null");
}
crate::gpu::backends::metal::buffer::copy_buffer(
config.command_queue_handle as *mut objc::runtime::Object,
src_ptr as *mut objc::runtime::Object,
0,
src_pitch_bytes,
buf.buf.raw as *mut objc::runtime::Object,
0,
dst_pitch_bytes,
dst_pitch_bytes,
h,
)?;
config.outgoing_data = Some(buf.buf.raw);
config.outgoing_pitch_px = w as i32;
return Ok(buf);
}
#[cfg(gpu_backend = "cuda")]
unsafe {
use crate::DeviceHandleInit;
let buf = crate::gpu::backends::cuda::buffer::get_or_create_with_mips(
DeviceHandleInit::FromPtr(config.device_handle),
w,
h,
bpp,
levels,
tag,
);
if buf.buf.raw.is_null() {
return Err("prepare_mip_source: CUDA allocator returned null");
}
crate::gpu::backends::cuda::buffer::copy_buffer(
config.device_handle,
src_ptr,
0,
src_pitch_bytes,
buf.buf.raw,
0,
dst_pitch_bytes,
dst_pitch_bytes,
h,
)?;
config.outgoing_data = Some(buf.buf.raw);
config.outgoing_pitch_px = w as i32;
return Ok(buf);
}
#[cfg(not(any(gpu_backend = "metal", gpu_backend = "cuda")))]
{
Err("prepare_mip_source: no GPU backend enabled")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cpu::buffer::get_or_create_with_mips;
use crate::types::fill_mip_desc;
#[test]
fn box_downsamples_known_32x32_pattern() {
const W: u32 = 32;
const H: u32 = 32;
const BPP: u32 = 4;
const LEVELS: u32 = 3;
let buf = get_or_create_with_mips(W, H, BPP, LEVELS, 0xBEEF);
let mut expected_l0 = vec![0u8; (W * H * BPP) as usize];
for y in 0..H {
for x in 0..W {
let off = ((y * W + x) * BPP) as usize;
expected_l0[off] = x as u8; expected_l0[off + 1] = y as u8; expected_l0[off + 2] = (x ^ y) as u8; expected_l0[off + 3] = 255; }
}
unsafe {
std::ptr::copy_nonoverlapping(
expected_l0.as_ptr(),
buf.buf.raw as *mut u8,
expected_l0.len(),
);
}
let mut config = Configuration::cpu(
buf.buf.raw,
buf.buf.raw,
W as i32,
W as i32,
W,
H,
BPP,
1, );
config.outgoing_data = Some(buf.buf.raw);
config.outgoing_pitch_px = W as i32;
config.outgoing_width = W;
config.outgoing_height = H;
config.outgoing_mip_levels = LEVELS;
unsafe { generate_mips(&config).expect("generate_mips failed"); }
let base = buf.buf.raw as *const u8;
let mut desc = crate::types::make_texture_desc(W, H, W, BPP, 1);
fill_mip_desc(&mut desc, W, H, W, BPP, LEVELS);
let l1_off = desc.mip_offset_bytes[1] as usize;
let l1_w = desc.mip_width[1];
let l1_h = desc.mip_height[1];
for y in 0..l1_h {
for x in 0..l1_w {
let off = l1_off + ((y * l1_w + x) * BPP) as usize;
let actual_b = unsafe { *base.add(off) };
let actual_g = unsafe { *base.add(off + 1) };
let actual_r = unsafe { *base.add(off + 2) };
let actual_a = unsafe { *base.add(off + 3) };
let p = |sx: u32, sy: u32| {
let o = ((sy * W + sx) * BPP) as usize;
(expected_l0[o] as f32, expected_l0[o + 1] as f32, expected_l0[o + 2] as f32, expected_l0[o + 3] as f32)
};
let sx = x * 2;
let sy = y * 2;
let (b0, g0, r0, a0) = p(sx, sy);
let (b1, g1, r1, a1) = p(sx + 1, sy);
let (b2, g2, r2, a2) = p(sx, sy + 1);
let (b3, g3, r3, a3) = p(sx + 1, sy + 1);
let expect = |v0: f32, v1: f32, v2: f32, v3: f32| -> u8 {
let avg = (v0 + v1 + v2 + v3) / 4.0 / 255.0;
(avg.clamp(0.0, 1.0) * 255.0) as u8
};
let exp_b = expect(b0, b1, b2, b3);
let exp_g = expect(g0, g1, g2, g3);
let exp_r = expect(r0, r1, r2, r3);
let exp_a = expect(a0, a1, a2, a3);
let diff = |a: u8, b: u8| a.abs_diff(b);
assert!(diff(actual_b, exp_b) <= 1, "L1 ({x},{y}) B: got {} expected {}", actual_b, exp_b);
assert!(diff(actual_g, exp_g) <= 1, "L1 ({x},{y}) G: got {} expected {}", actual_g, exp_g);
assert!(diff(actual_r, exp_r) <= 1, "L1 ({x},{y}) R: got {} expected {}", actual_r, exp_r);
assert!(diff(actual_a, exp_a) <= 1, "L1 ({x},{y}) A: got {} expected {}", actual_a, exp_a);
}
}
let l2_off = desc.mip_offset_bytes[2] as usize;
let corner_b = unsafe { *base.add(l2_off) };
assert!(corner_b <= 3, "L2 corner B drift: {}", corner_b);
}
#[test]
fn prepare_mip_source_copies_and_swaps_config() {
const W: u32 = 16;
const H: u32 = 16;
const BPP: u32 = 4;
const LEVELS: u32 = 3;
const SRC_PITCH_PX: u32 = 20;
let mut src = vec![0u8; (SRC_PITCH_PX * H * BPP) as usize];
for y in 0..H {
for x in 0..W {
let o = ((y * SRC_PITCH_PX + x) * BPP) as usize;
src[o] = (x + 1) as u8;
src[o + 1] = (y + 1) as u8;
src[o + 2] = ((x ^ y) + 1) as u8;
src[o + 3] = 255;
}
}
let mut config = Configuration::cpu(
src.as_mut_ptr() as *mut std::ffi::c_void,
src.as_mut_ptr() as *mut std::ffi::c_void,
SRC_PITCH_PX as i32,
SRC_PITCH_PX as i32,
W,
H,
BPP,
1,
);
config.outgoing_data = Some(src.as_mut_ptr() as *mut std::ffi::c_void);
config.outgoing_pitch_px = SRC_PITCH_PX as i32;
config.outgoing_width = W;
config.outgoing_height = H;
config.outgoing_mip_levels = LEVELS;
let _mip = unsafe { prepare_mip_source(&mut config, 0xF00D).expect("prepare_mip_source failed") };
assert_eq!(config.outgoing_pitch_px, W as i32);
let mip_ptr = config.outgoing_data.expect("outgoing_data lost");
assert_ne!(mip_ptr as *const _, src.as_ptr() as *const _);
let mip_base = mip_ptr as *const u8;
for y in 0..H {
for x in 0..W {
let src_o = ((y * SRC_PITCH_PX + x) * BPP) as usize;
let dst_o = ((y * W + x) * BPP) as usize;
for c in 0..4 {
let s = src[src_o + c];
let d = unsafe { *mip_base.add(dst_o + c) };
assert_eq!(s, d, "lod 0 pixel mismatch at ({x},{y}) channel {c}");
}
}
}
unsafe { generate_mips(&config).expect("generate_mips failed"); }
let mut desc = crate::types::make_texture_desc(W, H, W, BPP, 1);
crate::types::fill_mip_desc(&mut desc, W, H, W, BPP, LEVELS);
let l1_off = desc.mip_offset_bytes[1] as usize;
let p = |sx: u32, sy: u32| {
let o = ((sy * SRC_PITCH_PX + sx) * BPP) as usize;
(src[o] as u32, src[o + 1] as u32, src[o + 2] as u32, src[o + 3] as u32)
};
let (b0, g0, r0, a0) = p(0, 0);
let (b1, g1, r1, a1) = p(1, 0);
let (b2, g2, r2, a2) = p(0, 1);
let (b3, g3, r3, a3) = p(1, 1);
let expect_b = (b0 + b1 + b2 + b3) / 4;
let expect_g = (g0 + g1 + g2 + g3) / 4;
let expect_r = (r0 + r1 + r2 + r3) / 4;
let expect_a = (a0 + a1 + a2 + a3) / 4;
let actual_b = unsafe { *mip_base.add(l1_off) as u32 };
let actual_g = unsafe { *mip_base.add(l1_off + 1) as u32 };
let actual_r = unsafe { *mip_base.add(l1_off + 2) as u32 };
let actual_a = unsafe { *mip_base.add(l1_off + 3) as u32 };
let diff = |a: u32, b: u32| a.max(b) - a.min(b);
assert!(diff(actual_b, expect_b) <= 1);
assert!(diff(actual_g, expect_g) <= 1);
assert!(diff(actual_r, expect_r) <= 1);
assert!(diff(actual_a, expect_a) <= 1);
}
#[test]
fn prepare_mip_source_rejects_single_level() {
let mut config = Configuration::cpu(
std::ptr::null_mut(),
std::ptr::null_mut(),
1,
1,
1,
1,
4,
1,
);
config.outgoing_mip_levels = 1;
let res = unsafe { prepare_mip_source(&mut config, 0) };
match res {
Err(msg) => assert!(msg.contains(">= 2"), "unexpected error: {msg}"),
Ok(_) => panic!("prepare_mip_source should reject single-level configs"),
}
}
#[test]
fn generate_mips_is_noop_for_single_level() {
let mut config = Configuration::cpu(
std::ptr::null_mut(),
std::ptr::null_mut(),
1,
1,
1,
1,
4,
1,
);
config.outgoing_mip_levels = 1;
unsafe { generate_mips(&config).unwrap(); }
config.outgoing_mip_levels = 0;
unsafe { generate_mips(&config).unwrap(); }
}
}