#![allow(clippy::cast_precision_loss)]
use std::sync::Mutex;
use std::time::Instant;
use roxlap_formats::vxl::Vxl;
use roxlap_gpu::{
decompress_chunk, GpuChunkResident, GpuInitError, GpuRendererSettings, HeadlessGpu, CHUNK_Z,
};
static GPU_TEST_LOCK: Mutex<()> = Mutex::new(());
fn fixture_one_voxel_per_column(vsid: u32) -> Vxl {
let n_cols = (vsid as usize) * (vsid as usize);
let mut data: Vec<u8> = Vec::with_capacity(n_cols * 8);
let mut column_offset: Vec<u32> = Vec::with_capacity(n_cols + 1);
let bgra = [0x00u8, 0x80, 0xff, 0x80];
for _ in 0..n_cols {
column_offset.push(u32::try_from(data.len()).expect("offset fits"));
data.extend_from_slice(&[0, 100, 100, 0]); data.extend_from_slice(&bgra);
}
column_offset.push(u32::try_from(data.len()).expect("offset fits"));
Vxl {
vsid,
ipo: [0.0; 3],
ist: [1.0, 0.0, 0.0],
ihe: [0.0, 0.0, 1.0],
ifo: [0.0, 1.0, 0.0],
data: data.into_boxed_slice(),
column_offset: column_offset.into_boxed_slice(),
mip_base_offsets: Box::new([0, n_cols + 1]),
vbit: Box::new([]),
vbiti: 0,
}
}
fn try_init() -> Option<(HeadlessGpu, std::sync::MutexGuard<'static, ()>)> {
let guard = GPU_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
match HeadlessGpu::new_blocking(GpuRendererSettings::default()) {
Ok(gpu) => Some((gpu, guard)),
Err(GpuInitError::NoAdapter) => {
eprintln!("[skip] no GPU adapter reachable — set up Vulkan/Metal/DX12 to run");
None
}
Err(e) => {
eprintln!("[skip] GPU init failed ({e}) — driver issue");
None
}
}
}
#[test]
fn round_trip_textured_voxel_matches_cpu() {
let Some((gpu, _gpu_lock)) = try_init() else {
return;
};
eprintln!("round_trip: adapter = {}", gpu.adapter_info);
let vxl = fixture_one_voxel_per_column(4);
let chunk = decompress_chunk(&vxl);
let resident = GpuChunkResident::upload(&gpu.device, &chunk);
eprintln!("resident bytes: {}", resident.resident_bytes());
for y in 0..vxl.vsid {
for x in 0..vxl.vsid {
let v = resident.read_voxel_blocking(&gpu.device, &gpu.queue, x, y, 100);
assert_eq!(
v,
Some(0x80ff_8000),
"GPU voxel at ({x}, {y}, 100) should be 0x80ff_8000"
);
assert_eq!(
chunk.voxel_at(x, y, 100),
v,
"CPU/GPU disagree at ({x}, {y}, 100)"
);
}
}
}
#[test]
fn round_trip_air_above_returns_none() {
let Some((gpu, _gpu_lock)) = try_init() else {
return;
};
let vxl = fixture_one_voxel_per_column(4);
let chunk = decompress_chunk(&vxl);
let resident = GpuChunkResident::upload(&gpu.device, &chunk);
for &z in &[0u32, 1, 50, 99] {
let v = resident.read_voxel_blocking(&gpu.device, &gpu.queue, 1, 2, z);
assert!(
v.is_none(),
"GPU should report empty at (1, 2, {z}) — got {v:?}"
);
assert_eq!(chunk.voxel_at(1, 2, z), v);
}
}
#[test]
fn round_trip_bedrock_below_now_returns_air() {
let Some((gpu, _gpu_lock)) = try_init() else {
return;
};
let vxl = fixture_one_voxel_per_column(4);
let chunk = decompress_chunk(&vxl);
let resident = GpuChunkResident::upload(&gpu.device, &chunk);
for &z in &[101u32, 150, CHUNK_Z - 1] {
let v = resident.read_voxel_blocking(&gpu.device, &gpu.queue, 1, 2, z);
assert!(
v.is_none(),
"bedrock at (1, 2, {z}) should be empty — got {v:?}"
);
assert_eq!(chunk.voxel_at(1, 2, z), v);
}
}
#[test]
fn bench_single_chunk_upload() {
let Some((gpu, _gpu_lock)) = try_init() else {
return;
};
let vxl = fixture_one_voxel_per_column(128);
let chunk = decompress_chunk(&vxl);
let t0 = Instant::now();
let resident = GpuChunkResident::upload(&gpu.device, &chunk);
gpu.device.poll(wgpu::Maintain::Wait);
let upload_dt = t0.elapsed();
let bytes = resident.resident_bytes();
eprintln!(
"bench: vsid=128 upload {:.1} KiB in {:.3?} → {:.1} MiB/s",
bytes as f64 / 1024.0,
upload_dt,
bytes as f64 / (1024.0 * 1024.0) / upload_dt.as_secs_f64(),
);
let v = resident.read_voxel_blocking(&gpu.device, &gpu.queue, 7, 13, 100);
assert_eq!(v, Some(0x80ff_8000));
}