#![allow(clippy::cast_possible_truncation)]
use roxlap_formats::kv6::Kv6;
use roxlap_formats::sprite::Sprite;
use roxlap_formats::VoxColor;
use roxlap_gpu::sprite_model::ViewFrustum;
use roxlap_gpu::{
build_sprite_model, GpuRendererSettings, HeadlessGpu, SpriteInstance, SpriteInstanceTransform,
SpriteModelRegistry, SpriteRegistryResident,
};
fn read_u32(gpu: &HeadlessGpu, buf: &wgpu::Buffer, words: u64) -> Vec<u32> {
let staging = gpu.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("sprite_cull readback"),
size: words * 4,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut enc = gpu
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
enc.copy_buffer_to_buffer(buf, 0, &staging, 0, words * 4);
gpu.queue.submit(std::iter::once(enc.finish()));
let slice = staging.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
gpu.device.poll(wgpu::PollType::wait_indefinitely()).ok();
rx.recv().unwrap().unwrap();
let data = slice.get_mapped_range();
bytemuck::cast_slice(&data).to_vec()
}
fn axis_instance(model: u32, pos: [f32; 3]) -> SpriteInstance {
let sprite = Sprite::axis_aligned(Kv6::solid_cube(8, VoxColor(0x80_ff_80_40)), pos);
SpriteInstance::new(model, SpriteInstanceTransform::from_sprite(&sprite))
}
#[test]
fn cull_cache_and_identity_colmul() {
let Ok(gpu) = HeadlessGpu::new_blocking(GpuRendererSettings::default()) else {
eprintln!("[skip] no GPU adapter reachable");
return;
};
let mut registry = SpriteModelRegistry::new();
let model = registry.add(build_sprite_model(&Kv6::solid_cube(
8,
VoxColor(0x80_ff_80_40),
)));
let instances = vec![
axis_instance(model, [0.0, 30.0, 0.0]),
axis_instance(model, [4.0, 50.0, 0.0]),
axis_instance(model, [10_000.0, 30.0, 0.0]),
];
let mut reg = SpriteRegistryResident::upload(&gpu.device, ®istry, &instances);
let f = ViewFrustum {
pos: [0.0, 0.0, 0.0],
right: [1.0, 0.0, 0.0],
down: [0.0, 0.0, 1.0],
forward: [0.0, 1.0, 0.0],
half_w: 1.0,
half_h: 0.6,
far: 1.0e9,
};
let r1 = reg.cull_bin_upload(&gpu.device, &gpu.queue, &f, 640, 360, 16, 4.0);
assert_eq!(r1.0, 2, "two of three instances are in view");
let lane = 0x0100u64;
let w = lane | (lane << 16) | (lane << 32) | (lane << 48);
let (lo, hi) = ((w & 0xffff_ffff) as u32, (w >> 32) as u32);
let colmul = read_u32(&gpu, ®.colmul, u64::from(r1.0) * 512);
for (i, &v) in colmul.iter().enumerate() {
assert_eq!(v, if i & 1 == 0 { lo } else { hi }, "identity word {i}");
}
let r2 = reg.cull_bin_upload(&gpu.device, &gpu.queue, &f, 640, 360, 16, 4.0);
assert_eq!(r1, r2, "same-key call must return the cached result");
let mut moved = instances.clone();
moved[1] = axis_instance(model, [-10_000.0, 30.0, 0.0]);
reg.update_transforms(&moved);
let r3 = reg.cull_bin_upload(&gpu.device, &gpu.queue, &f, 640, 360, 16, 4.0);
assert_eq!(r3.0, 1, "moved instance must drop out of the visible set");
let mut custom = [w; 256];
custom[0] = 0x0042_0042_0042_0042;
let tables: Vec<[u64; 256]> = vec![custom, [w; 256], [w; 256]];
reg.set_instance_colmul(&tables);
let r4 = reg.cull_bin_upload(&gpu.device, &gpu.queue, &f, 640, 360, 16, 4.0);
assert_eq!(r4.0, 1);
let packed = read_u32(&gpu, ®.colmul, 2);
assert_eq!(
(packed[0], packed[1]),
(0x0042_0042u32, 0x0042_0042u32),
"custom table must be packed for the visible instance",
);
}