#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::doc_markdown,
clippy::many_single_char_names,
clippy::redundant_closure_for_method_calls
)]
use std::sync::Mutex;
use roxlap_formats::vxl::Vxl;
use roxlap_gpu::{
decompress_chunk, Camera, GpuInitError, GpuRendererSettings, GpuSceneResident, GridUpload,
HeadlessGpu, HeadlessSceneRenderer, SceneUpload,
};
static GPU_TEST_LOCK: Mutex<()> = Mutex::new(());
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");
None
}
Err(e) => {
eprintln!("[skip] GPU init failed ({e})");
None
}
}
}
fn floor_chunk(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 block_chunk(vsid: u32, top: u8, bot: u8) -> Vxl {
let n_cols = (vsid as usize) * (vsid as usize);
let n_vox = (bot - top + 1) as usize;
let mut data: Vec<u8> = Vec::with_capacity(n_cols * (4 + n_vox * 4));
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, top, bot, 0]); for _ in 0..n_vox {
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 is_block_color(p: u32) -> bool {
let (r, g, b) = (p & 0xff, (p >> 8) & 0xff, (p >> 16) & 0xff);
r > 180 && (80..=175).contains(&g) && b < 70
}
fn wall_chunk(vsid: u32, surf: u8) -> 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, surf, surf, 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,
}
}
#[test]
fn scene_dda_marches_coarse_mip_for_distant_chunk() {
let Some((gpu, _lock)) = try_init() else {
return;
};
eprintln!("mip_render: adapter = {}", gpu.adapter_info);
let vsid = 32u32;
let chunk = decompress_chunk(&block_chunk(vsid, 0, 31));
assert!(chunk.mips.len() >= 5, "need a deep ladder for mip-4");
let grid = GridUpload {
vsid,
origin_chunk: [0, 0, 0],
chunks_dims: [1, 8, 1],
pool_dims: [1, 8, 1],
chunks: vec![([0, 4, 0], chunk)],
};
let scene = GpuSceneResident::upload(&gpu.device, &SceneUpload { grids: vec![grid] });
let (w, h) = (64u32, 64u32);
let renderer = HeadlessSceneRenderer::new(&gpu.device, w, h);
let cam = Camera {
position: [vsid as f32 * 0.5, 0.0, 16.0],
right: [1.0, 0.0, 0.0],
down: [0.0, 0.0, 1.0],
forward: [0.0, 1.0, 0.0],
fov_y_rad: 30f32.to_radians(),
};
let centre = (h / 2 * w + w / 2) as usize;
let fb0 = renderer.render(
&gpu.device,
&gpu.queue,
&scene,
&[cam],
cam.fov_y_rad,
64,
0.0,
);
assert!(
is_block_color(fb0[centre]),
"mip-0 centre should be the block, got {:#08x}",
fb0[centre],
);
let fb4 = renderer.render(
&gpu.device,
&gpu.queue,
&scene,
&[cam],
cam.fov_y_rad,
64,
8.0,
);
eprintln!(
"mip_render: centre mip0={:#08x} mip4={:#08x}",
fb0[centre], fb4[centre]
);
assert!(
is_block_color(fb4[centre]),
"coarse-mip centre should still be the block, got {:#08x}",
fb4[centre],
);
let agree = fb0
.iter()
.zip(&fb4)
.filter(|(a, b)| is_block_color(**a) == is_block_color(**b))
.count();
let frac = agree as f32 / fb0.len() as f32;
eprintln!("mip_render: block/sky agreement = {frac:.3}");
assert!(
frac > 0.9,
"mip-0 vs mip-4 block coverage diverged: {frac:.3}"
);
}
#[test]
fn scene_dda_aabb_early_out_away_is_sky() {
let Some((gpu, _lock)) = try_init() else {
return;
};
let vsid = 32u32;
let chunk = decompress_chunk(&block_chunk(vsid, 0, 31));
let grid = GridUpload {
vsid,
origin_chunk: [0, 0, 0],
chunks_dims: [1, 8, 1],
pool_dims: [1, 8, 1],
chunks: vec![([0, 4, 0], chunk)],
};
let scene = GpuSceneResident::upload(&gpu.device, &SceneUpload { grids: vec![grid] });
assert_eq!(scene.static_meta[0].aabb_min, [0, 4, 0]);
assert_eq!(scene.static_meta[0].aabb_max, [0, 4, 0]);
let (w, h) = (64u32, 64u32);
let renderer = HeadlessSceneRenderer::new(&gpu.device, w, h);
let cam = Camera {
position: [vsid as f32 * 0.5, 0.0, 16.0],
right: [-1.0, 0.0, 0.0],
down: [0.0, 0.0, 1.0],
forward: [0.0, -1.0, 0.0], fov_y_rad: 30f32.to_radians(),
};
let fb = renderer.render(
&gpu.device,
&gpu.queue,
&scene,
&[cam],
cam.fov_y_rad,
64,
0.0,
);
let block_px = fb.iter().filter(|&&p| is_block_color(p)).count();
assert_eq!(
block_px, 0,
"looking away from the only chunk must be all sky, got {block_px} block pixels",
);
}
#[test]
fn aabb_tracks_streaming_refresh_and_evict() {
let Some((gpu, _lock)) = try_init() else {
return;
};
let vsid = 32u32;
let grid = GridUpload {
vsid,
origin_chunk: [0, 0, 0],
chunks_dims: [1, 1, 1],
pool_dims: [8, 8, 1], chunks: vec![([0, 0, 0], decompress_chunk(&block_chunk(vsid, 0, 31)))],
};
let mut scene = GpuSceneResident::upload(&gpu.device, &SceneUpload { grids: vec![grid] });
assert_eq!(scene.static_meta[0].aabb_min, [0, 0, 0]);
assert_eq!(scene.static_meta[0].aabb_max, [0, 0, 0]);
let far = decompress_chunk(&block_chunk(vsid, 0, 31));
scene.refresh_chunk(&gpu.queue, 0, [3, 2, 0], &far);
assert_eq!(scene.static_meta[0].aabb_min, [0, 0, 0]);
assert_eq!(scene.static_meta[0].aabb_max, [3, 2, 0]);
scene.evict_chunk(&gpu.queue, 0, [3, 2, 0]);
assert_eq!(scene.static_meta[0].aabb_min, [0, 0, 0]);
assert_eq!(scene.static_meta[0].aabb_max, [0, 0, 0]);
}
#[test]
fn scene_dda_renders_bedrock_wall_face_solid() {
let Some((gpu, _lock)) = try_init() else {
return;
};
eprintln!("wall_render: adapter = {}", gpu.adapter_info);
let vsid = 32u32;
let chunk = decompress_chunk(&wall_chunk(vsid, 40));
let grid = GridUpload {
vsid,
origin_chunk: [0, 0, 0],
chunks_dims: [1, 8, 1],
pool_dims: [1, 8, 1],
chunks: vec![([0, 4, 0], chunk)],
};
let scene = GpuSceneResident::upload(&gpu.device, &SceneUpload { grids: vec![grid] });
let (w, h) = (64u32, 64u32);
let renderer = HeadlessSceneRenderer::new(&gpu.device, w, h);
let cam = Camera {
position: [vsid as f32 * 0.5, 0.0, 128.0], right: [1.0, 0.0, 0.0],
down: [0.0, 0.0, 1.0],
forward: [0.0, 1.0, 0.0],
fov_y_rad: 30f32.to_radians(),
};
let fb = renderer.render(
&gpu.device,
&gpu.queue,
&scene,
&[cam],
cam.fov_y_rad,
64,
0.0,
);
let centre = fb[(h / 2 * w + w / 2) as usize];
eprintln!("wall_render: centre pixel = {centre:#08x}");
assert!(
is_block_color(centre),
"bedrock wall face should be solid surface colour, got {centre:#08x} (sky = regression)",
);
}
#[test]
fn scene_dda_renders_floor_through_mip_layout() {
let Some((gpu, _lock)) = try_init() else {
return;
};
eprintln!("scene_render: adapter = {}", gpu.adapter_info);
let vsid = 64u32;
let chunk = decompress_chunk(&floor_chunk(vsid));
assert!(chunk.mips.len() >= 2, "expected a mip ladder");
let grid = GridUpload {
vsid,
origin_chunk: [0, 0, 0],
chunks_dims: [1, 1, 1],
pool_dims: [1, 1, 1],
chunks: vec![([0, 0, 0], chunk)],
};
let scene = GpuSceneResident::upload(&gpu.device, &SceneUpload { grids: vec![grid] });
eprintln!("scene_render: resident {} bytes", scene.resident_bytes());
let (w, h) = (64u32, 64u32);
let renderer = HeadlessSceneRenderer::new(&gpu.device, w, h);
let cam = Camera {
position: [vsid as f32 * 0.5, vsid as f32 * 0.5, 20.0],
right: [1.0, 0.0, 0.0],
down: [0.0, 1.0, 0.0],
forward: [0.0, 0.0, 1.0],
fov_y_rad: 30f32.to_radians(),
};
let fb = renderer.render(
&gpu.device,
&gpu.queue,
&scene,
&[cam],
30f32.to_radians(),
64,
0.0, );
assert_eq!(fb.len(), (w * h) as usize);
let centre = fb[(h / 2 * w + w / 2) as usize];
let (r, g, b) = (centre & 0xff, (centre >> 8) & 0xff, (centre >> 16) & 0xff);
eprintln!("scene_render: centre pixel = ({r}, {g}, {b})");
assert!(r > 200, "floor R should be ~255, got {r}");
assert!((100..=160).contains(&g), "floor G should be ~128, got {g}");
assert!(b < 40, "floor B should be ~0, got {b}");
let floor_px = fb
.iter()
.filter(|&&p| {
let (r, g, b) = (p & 0xff, (p >> 8) & 0xff, (p >> 16) & 0xff);
r > 200 && (90..=170).contains(&g) && b < 50
})
.count();
let frac = floor_px as f32 / fb.len() as f32;
eprintln!("scene_render: floor fraction = {frac:.3}");
assert!(frac > 0.6, "expected floor to fill the view, got {frac:.3}");
}