#![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, GpuLight, GpuRendererSettings, GpuSceneResident,
GridUpload, GridWorldTransform, HeadlessGpu, HeadlessSceneRenderer, SceneLights, 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 floor_with_wall_chunk(vsid: u32, wx0: u32, wx1: u32, wtop: u8) -> Vxl {
let n_cols = (vsid as usize) * (vsid as usize);
let mut data: Vec<u8> = Vec::new();
let mut column_offset: Vec<u32> = Vec::with_capacity(n_cols + 1);
let bgra = [0x00u8, 0x80, 0xff, 0x80];
for i in 0..n_cols {
let x = (i as u32) % vsid;
column_offset.push(u32::try_from(data.len()).expect("offset fits"));
if x >= wx0 && x < wx1 {
let n_vox = (100 - wtop + 1) as usize;
data.extend_from_slice(&[0, wtop, 100, 0]); for _ in 0..n_vox {
data.extend_from_slice(&bgra);
}
} else {
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 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, &gpu.queue, 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, &gpu.queue, 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 scene_dda_zero_grids_renders_sky() {
let Some((gpu, _lock)) = try_init() else {
return;
};
let scene = GpuSceneResident::upload(&gpu.device, &SceneUpload { grids: vec![] });
assert_eq!(
scene.grid_count, 0,
"empty SceneUpload → zero-grid resident"
);
let (w, h) = (32u32, 32u32);
let renderer = HeadlessSceneRenderer::new(&gpu.device, &gpu.queue, w, h);
let fb = renderer.render(
&gpu.device,
&gpu.queue,
&scene,
&[],
30f32.to_radians(),
64,
0.0,
);
assert_eq!(fb.len(), (w * h) as usize);
let first = fb[0];
assert!(
fb.iter().all(|&p| p == first),
"zero-grid frame must be a uniform sky, got varied pixels (first={first:#08x})",
);
assert_ne!(
first & 0x00ff_ffff,
0,
"sky must not be black, got {first:#08x}"
);
let (r, _g, b) = (first & 0xff, (first >> 8) & 0xff, (first >> 16) & 0xff);
assert!(b > r, "headless sky is bluish (b>r), got {first:#08x}");
}
#[test]
fn scene_dda_side_shades_darken_floor() {
let Some((gpu, _lock)) = try_init() else {
return;
};
let vsid = 32u32;
let chunk = decompress_chunk(&floor_chunk(vsid)); 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] });
let (w, h) = (64u32, 64u32);
let mut renderer = HeadlessSceneRenderer::new(&gpu.device, &gpu.queue, w, h);
let cam = Camera {
position: [16.0, 16.0, 50.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: 60f32.to_radians(),
};
let centre = (h / 2 * w + w / 2) as usize;
let lum = |p: u32| (p & 0xff) + ((p >> 8) & 0xff) + ((p >> 16) & 0xff);
let fb0 = renderer.render(
&gpu.device,
&gpu.queue,
&scene,
&[cam],
cam.fov_y_rad,
64,
0.0,
);
let base = fb0[centre];
assert!(
is_block_color(base),
"centre should be the lit floor, got {base:#08x}",
);
renderer.set_side_shades([0, 64, 0, 0, 0, 0]);
let fb1 = renderer.render(
&gpu.device,
&gpu.queue,
&scene,
&[cam],
cam.fov_y_rad,
64,
0.0,
);
let shaded = fb1[centre];
assert!(
lum(shaded) < lum(base),
"side-shade should darken the floor: {base:#08x} -> {shaded:#08x}",
);
assert_ne!(
shaded & 0x00ff_ffff,
0,
"half-shaded floor must not be black"
);
}
#[test]
fn scene_dda_sun_lights_floor_by_facing() {
let Some((gpu, _lock)) = try_init() else {
return;
};
let vsid = 32u32;
let chunk = decompress_chunk(&floor_chunk(vsid)); 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] });
let (w, h) = (64u32, 64u32);
let mut renderer = HeadlessSceneRenderer::new(&gpu.device, &gpu.queue, w, h);
let cam = Camera {
position: [16.0, 16.0, 50.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: 60f32.to_radians(),
};
let centre = (h / 2 * w + w / 2) as usize;
let lum = |p: u32| (p & 0xff) + ((p >> 8) & 0xff) + ((p >> 16) & 0xff);
let render = |r: &mut HeadlessSceneRenderer| {
r.render(
&gpu.device,
&gpu.queue,
&scene,
&[cam],
cam.fov_y_rad,
64,
0.0,
)[centre]
};
let baked = render(&mut renderer);
assert!(
is_block_color(baked),
"centre should be the floor: {baked:#08x}"
);
let sun = |to_sun: [f32; 3]| SceneLights {
enabled: true,
grid_sun_dirs: vec![to_sun],
sun_color: [1.0; 3],
sun_intensity: 1.0,
ambient: [1.0; 3],
..SceneLights::default()
};
renderer.set_scene_lights(sun([0.0, 0.0, -1.0]));
let lit_above = render(&mut renderer);
renderer.set_scene_lights(sun([0.0, 0.0, 1.0]));
let lit_below = render(&mut renderer);
assert!(
lum(lit_above) > lum(baked),
"sun from above must brighten the floor: baked {baked:#08x} -> {lit_above:#08x}",
);
assert!(
lum(lit_above) > lum(lit_below),
"sun facing the surface must beat a back-facing sun: {lit_above:#08x} vs {lit_below:#08x}",
);
}
#[test]
fn scene_dda_cel_banding_terraces_sun() {
let Some((gpu, _lock)) = try_init() else {
return;
};
let vsid = 32u32;
let chunk = decompress_chunk(&floor_chunk(vsid));
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] });
let (w, h) = (64u32, 64u32);
let mut renderer = HeadlessSceneRenderer::new(&gpu.device, &gpu.queue, w, h);
let cam = Camera {
position: [16.0, 16.0, 50.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: 60f32.to_radians(),
};
let centre = (h / 2 * w + w / 2) as usize;
let render = |r: &mut HeadlessSceneRenderer| {
r.render(
&gpu.device,
&gpu.queue,
&scene,
&[cam],
cam.fov_y_rad,
64,
0.0,
)[centre]
};
let a = [0.6_f32, 0.0, -0.8]; let b = [0.435_89_f32, 0.0, -0.9]; let rig = |to_sun: [f32; 3], bands: u32| SceneLights {
enabled: true,
grid_sun_dirs: vec![to_sun],
sun_color: [1.0; 3],
sun_intensity: 1.0,
ambient: [0.1; 3],
style_bands: bands,
shadow_tint: [0.1, 0.1, 0.2],
..SceneLights::default()
};
renderer.set_scene_lights(rig(a, 0));
let smooth_a = render(&mut renderer);
renderer.set_scene_lights(rig(b, 0));
let smooth_b = render(&mut renderer);
assert_ne!(
smooth_a, smooth_b,
"smooth diffuse must vary with N·L: {smooth_a:#08x} vs {smooth_b:#08x}",
);
renderer.set_scene_lights(rig(a, 2));
let cel_a = render(&mut renderer);
renderer.set_scene_lights(rig(b, 2));
let cel_b = render(&mut renderer);
assert_eq!(
cel_a, cel_b,
"cel banding must terrace both N·L to one level: {cel_a:#08x} vs {cel_b:#08x}",
);
}
#[test]
fn scene_dda_point_light_brightens_by_distance_and_facing() {
let Some((gpu, _lock)) = try_init() else {
return;
};
let vsid = 32u32;
let chunk = decompress_chunk(&floor_chunk(vsid)); 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] });
let (w, h) = (64u32, 64u32);
let mut renderer = HeadlessSceneRenderer::new(&gpu.device, &gpu.queue, w, h);
let cam = Camera {
position: [16.0, 16.0, 50.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: 60f32.to_radians(),
};
let centre = (h / 2 * w + w / 2) as usize;
let lum = |p: u32| (p & 0xff) + ((p >> 8) & 0xff) + ((p >> 16) & 0xff);
let render = |r: &mut HeadlessSceneRenderer| {
r.render(
&gpu.device,
&gpu.queue,
&scene,
&[cam],
cam.fov_y_rad,
64,
0.0,
)[centre]
};
let baked = render(&mut renderer);
assert!(
is_block_color(baked),
"centre should be the floor: {baked:#08x}"
);
let one_point = |pos: [f32; 3]| SceneLights {
enabled: true,
ambient: [1.0; 3],
grid_point_lights: vec![vec![GpuLight {
position: pos,
radius: 64.0,
color: [1.0; 3],
intensity: 2.0,
casts_shadow: false,
spot_dir: [0.0, 0.0, 1.0],
cos_inner: -1.0,
cos_outer: -1.0,
}]],
..SceneLights::default()
};
renderer.set_scene_lights(one_point([16.0, 16.0, 98.0])); let near_above = render(&mut renderer);
renderer.set_scene_lights(one_point([16.0, 16.0, 60.0])); let far_above = render(&mut renderer);
renderer.set_scene_lights(one_point([16.0, 16.0, 110.0])); let below = render(&mut renderer);
assert!(
lum(near_above) > lum(baked),
"a near point light must brighten the floor: baked {baked:#08x} -> {near_above:#08x}",
);
assert!(
lum(near_above) > lum(far_above),
"distance falloff: near must beat far: {near_above:#08x} vs {far_above:#08x}",
);
assert!(
lum(below) <= lum(baked) + 2,
"a back-facing point light must not light the top face: {below:#08x} vs baked {baked:#08x}",
);
}
#[test]
fn scene_dda_spot_cone_matches_point_on_axis_and_masks_off_axis() {
let Some((gpu, _lock)) = try_init() else {
return;
};
let vsid = 32u32;
let chunk = decompress_chunk(&floor_chunk(vsid)); 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] });
let (w, h) = (64u32, 64u32);
let mut renderer = HeadlessSceneRenderer::new(&gpu.device, &gpu.queue, w, h);
let cam = Camera {
position: [16.0, 16.0, 50.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: 60f32.to_radians(),
};
let centre = (h / 2 * w + w / 2) as usize;
let lum = |p: u32| (p & 0xff) + ((p >> 8) & 0xff) + ((p >> 16) & 0xff);
let render = |r: &mut HeadlessSceneRenderer| {
r.render(
&gpu.device,
&gpu.queue,
&scene,
&[cam],
cam.fov_y_rad,
64,
0.0,
)[centre]
};
let pos = [16.0, 16.0, 98.0];
let cos_inner = 40f32.to_radians().cos();
let cos_outer = 60f32.to_radians().cos();
let light = |spot_dir: [f32; 3], ci: f32, co: f32| SceneLights {
enabled: true,
ambient: [1.0; 3],
grid_point_lights: vec![vec![GpuLight {
position: pos,
radius: 64.0,
color: [1.0; 3],
intensity: 2.0,
casts_shadow: false,
spot_dir,
cos_inner: ci,
cos_outer: co,
}]],
..SceneLights::default()
};
let baked = render(&mut renderer); renderer.set_scene_lights(light([0.0, 0.0, 1.0], -1.0, -1.0));
let point = render(&mut renderer);
renderer.set_scene_lights(light([0.0, 0.0, 1.0], cos_inner, cos_outer));
let spot_on = render(&mut renderer);
renderer.set_scene_lights(light([1.0, 0.0, 0.0], cos_inner, cos_outer));
let spot_off = render(&mut renderer);
assert!(
lum(point) > lum(baked),
"sanity: the point light must brighten the floor: {baked:#08x} -> {point:#08x}",
);
assert_eq!(
spot_on, point,
"on-axis spot (cd=1, inside inner cone) must equal the point light: \
{spot_on:#08x} vs {point:#08x}",
);
assert_eq!(
spot_off, baked,
"off-cone spot must contribute nothing (masked to zero): \
{spot_off:#08x} vs baked {baked:#08x}",
);
}
#[test]
fn scene_dda_sun_shadow_darkens_occluded_floor() {
let Some((gpu, _lock)) = try_init() else {
return;
};
let vsid = 32u32;
let chunk = decompress_chunk(&floor_with_wall_chunk(vsid, 16, 18, 90));
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] });
let (w, h) = (64u32, 64u32);
let mut renderer = HeadlessSceneRenderer::new(&gpu.device, &gpu.queue, w, h);
let cam = Camera {
position: [14.0, 16.0, 50.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: 60f32.to_radians(),
};
let centre = (h / 2 * w + w / 2) as usize;
let lum = |p: u32| (p & 0xff) + ((p >> 8) & 0xff) + ((p >> 16) & 0xff);
let render = |r: &mut HeadlessSceneRenderer| {
r.render(
&gpu.device,
&gpu.queue,
&scene,
&[cam],
cam.fov_y_rad,
64,
0.0,
)[centre]
};
let s = std::f32::consts::FRAC_1_SQRT_2;
let sun = |casts_shadow: bool| SceneLights {
enabled: true,
grid_sun_dirs: vec![[s, 0.0, -s]],
sun_color: [1.0; 3],
sun_intensity: 3.0,
sun_casts_shadow: casts_shadow,
ambient: [0.5; 3],
shadow_strength: 1.0, shadow_bias: 1.5,
shadow_max_dist: 512.0,
shadow_max_steps: 256,
..SceneLights::default()
};
renderer.set_scene_lights(sun(false));
let lit = render(&mut renderer); renderer.set_scene_lights(sun(true));
let shadowed = render(&mut renderer);
let blue = |p: u32| (p >> 16) & 0xff;
assert!(
blue(lit) < 70 && blue(shadowed) < 70,
"expected floor, not sky"
);
assert!(
lum(shadowed) < lum(lit),
"wall must cast a sun shadow on the floor: lit {lit:#08x} -> shadowed {shadowed:#08x}",
);
}
#[test]
fn scene_dda_cross_grid_sun_shadow() {
let Some((gpu, _lock)) = try_init() else {
return;
};
let vsid = 32u32;
let grid_a = GridUpload {
vsid,
origin_chunk: [0, 0, 0],
chunks_dims: [1, 1, 1],
pool_dims: [1, 1, 1],
chunks: vec![([0, 0, 0], decompress_chunk(&floor_chunk(vsid)))],
};
let grid_b = GridUpload {
vsid,
origin_chunk: [0, 0, 0],
chunks_dims: [1, 1, 1],
pool_dims: [1, 1, 1],
chunks: vec![(
[0, 0, 0],
decompress_chunk(&floor_with_wall_chunk(vsid, 16, 18, 90)),
)],
};
let scene = GpuSceneResident::upload(
&gpu.device,
&SceneUpload {
grids: vec![grid_a, grid_b],
},
);
let (w, h) = (64u32, 64u32);
let mut renderer = HeadlessSceneRenderer::new(&gpu.device, &gpu.queue, w, h);
let cam = Camera {
position: [14.0, 16.0, 50.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: 60f32.to_radians(),
};
let cam_b = Camera {
position: [14.0 - 2.0, 16.0, 50.0],
..cam
};
let xf_a = GridWorldTransform::default();
let xf_b = GridWorldTransform {
origin: [2.0, 0.0, 0.0],
..GridWorldTransform::default()
};
let centre = (h / 2 * w + w / 2) as usize;
let lum = |p: u32| (p & 0xff) + ((p >> 8) & 0xff) + ((p >> 16) & 0xff);
let render = |r: &mut HeadlessSceneRenderer| {
r.render_with_transforms(
&gpu.device,
&gpu.queue,
&scene,
&[cam, cam_b],
&[xf_a, xf_b],
cam.fov_y_rad,
64,
0.0,
)[centre]
};
let s = std::f32::consts::FRAC_1_SQRT_2;
let sun = |casts_shadow: bool| SceneLights {
enabled: true,
grid_sun_dirs: vec![[s, 0.0, -s], [s, 0.0, -s]], sun_color: [1.0; 3],
sun_intensity: 3.0,
sun_casts_shadow: casts_shadow,
ambient: [0.5; 3],
shadow_strength: 1.0,
shadow_bias: 1.5,
shadow_max_dist: 512.0,
shadow_max_steps: 256,
..SceneLights::default()
};
renderer.set_scene_lights(sun(false));
let lit = render(&mut renderer);
renderer.set_scene_lights(sun(true));
let shadowed = render(&mut renderer);
let blue = |p: u32| (p >> 16) & 0xff;
assert!(
blue(lit) < 70 && blue(shadowed) < 70,
"expected A's floor, not sky: lit={lit:#08x} shadowed={shadowed:#08x}"
);
assert!(
lum(shadowed) < lum(lit),
"grid B's wall must cast a cross-grid sun shadow on grid A: lit {lit:#08x} -> shadowed {shadowed:#08x}",
);
}
fn dense_floor_chunk(vsid: u32, surf: u8, nvox: u8) -> Vxl {
let n_cols = (vsid as usize) * (vsid as usize);
let bot = surf + nvox - 1;
let mut data: Vec<u8> = Vec::with_capacity(n_cols * (4 + nvox as usize * 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, surf, bot, 0]);
for _ in 0..nvox {
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_dense_chunk_colours_not_truncated() {
let Some((gpu, _lock)) = try_init() else {
return;
};
let vsid = 128u32;
let chunk = decompress_chunk(&dense_floor_chunk(vsid, 100, 8));
assert!(
chunk.mips[0].colors.len() > roxlap_gpu::scene::COLORS_PER_CHUNK_WORDS as usize,
"test chunk must exceed the colour stride to exercise truncation \
({} colours)",
chunk.mips[0].colors.len()
);
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] });
let (w, h) = (64u32, 64u32);
let renderer = HeadlessSceneRenderer::new(&gpu.device, &gpu.queue, w, h);
let cam = Camera {
position: [vsid as f32 * 0.5, vsid as f32 * 0.5, 2.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: 60f32.to_radians(),
};
let fb = renderer.render(
&gpu.device,
&gpu.queue,
&scene,
&[cam],
cam.fov_y_rad,
64,
0.0,
);
let half = (h / 2) as usize;
let count = |rows: std::ops::Range<usize>| {
let mut n = 0;
for y in rows {
for x in 0..w as usize {
if is_block_color(fb[y * w as usize + x]) {
n += 1;
}
}
}
n
};
let top = count(0..half);
let bottom = count(half..h as usize);
eprintln!("dense_chunk: top-half floor px {top}, bottom-half floor px {bottom}");
assert!(top > 0, "top half should show the floor, got {top}");
assert!(
bottom * 2 > top,
"bottom (high-y) half is mostly black — colours were truncated \
(top {top} vs bottom {bottom})",
);
}
#[test]
fn scene_dda_per_grid_cameras_are_independent() {
let Some((gpu, _lock)) = try_init() else {
return;
};
let vsid = 32u32;
let mk_floor = || GridUpload {
vsid,
origin_chunk: [0, 0, 0],
chunks_dims: [1, 1, 1],
pool_dims: [1, 1, 1],
chunks: vec![([0, 0, 0], decompress_chunk(&floor_chunk(vsid)))],
};
let scene = GpuSceneResident::upload(
&gpu.device,
&SceneUpload {
grids: vec![mk_floor(), mk_floor()],
},
);
assert_eq!(scene.grid_count, 2, "two-grid scene");
let (w, h) = (64u32, 64u32);
let renderer = HeadlessSceneRenderer::new(&gpu.device, &gpu.queue, w, h);
let cam_down = Camera {
position: [16.0, 16.0, 50.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: 60f32.to_radians(),
};
let cam_up = Camera {
forward: [0.0, 0.0, -1.0],
..cam_down
};
let fov = cam_down.fov_y_rad;
let fb = renderer.render(
&gpu.device,
&gpu.queue,
&scene,
&[cam_up, cam_down],
fov,
64,
0.0,
);
let floor_px = fb.iter().filter(|&&p| is_block_color(p)).count();
assert!(
floor_px > 0,
"grid 1's floor must be visible via grid_cameras[1] — got {floor_px} floor px \
(per-grid camera indexing broken?)",
);
let fb2 = renderer.render(
&gpu.device,
&gpu.queue,
&scene,
&[cam_up, cam_up],
fov,
64,
0.0,
);
let floor_px2 = fb2.iter().filter(|&&p| is_block_color(p)).count();
assert_eq!(
floor_px2, 0,
"both grids looking away must be all sky — got {floor_px2} floor px",
);
}
#[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, &gpu.queue, 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, &gpu.queue, 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}");
}