#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::many_single_char_names,
clippy::similar_names
)]
use bytemuck::{Pod, Zeroable};
use roxlap_formats::kv6::Kv6;
use roxlap_formats::sprite::Sprite;
#[derive(Debug, Clone)]
pub struct SpriteModel {
pub dims: [u32; 3],
pub occ_words_per_col: u32,
pub pivot: [f32; 3],
pub occupancy: Vec<u32>,
pub colors: Vec<u32>,
pub dirs: Vec<u32>,
pub color_offsets: Vec<u32>,
pub voxel_world_size: f32,
}
#[must_use]
pub fn build_sprite_model(kv6: &Kv6) -> SpriteModel {
let (mx, my, mz) = (kv6.xsiz, kv6.ysiz, kv6.zsiz);
let occ_words_per_col = mz.div_ceil(32).max(1);
let cols = (mx * my) as usize;
let mut occupancy = vec![0u32; cols * occ_words_per_col as usize];
let mut color_offsets = vec![0u32; cols + 1];
let mut colors: Vec<u32> = Vec::with_capacity(kv6.voxels.len());
let mut dirs: Vec<u32> = Vec::with_capacity(kv6.voxels.len());
let mut buckets: Vec<Vec<(u16, u32, u8)>> = vec![Vec::new(); cols];
let mut voxel_iter = kv6.voxels.iter();
for x in 0..mx {
for y in 0..my {
let col = (x + y * mx) as usize;
let count = kv6.ylen[x as usize][y as usize];
for _ in 0..count {
let v = voxel_iter.next().expect("KV6 ylen / voxels.len mismatch");
buckets[col].push((v.z, v.col, v.dir));
}
}
}
for (col, bucket) in buckets.iter_mut().enumerate() {
color_offsets[col] = colors.len() as u32;
bucket.sort_by_key(|(z, _, _)| *z);
for &(z, col_rgba, dir) in bucket.iter() {
let z = u32::from(z);
let base = col * occ_words_per_col as usize + (z >> 5) as usize;
occupancy[base] |= 1u32 << (z & 31);
colors.push(col_rgba);
dirs.push(u32::from(dir));
}
}
color_offsets[cols] = colors.len() as u32;
SpriteModel {
dims: [mx, my, mz],
occ_words_per_col,
pivot: [kv6.xpiv, kv6.ypiv, kv6.zpiv],
occupancy,
color_offsets,
colors,
dirs,
voxel_world_size: 1.0,
}
}
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable, Debug)]
pub struct SpriteInstanceTransform {
pub inv_rot: [[f32; 4]; 3],
pub pos: [f32; 3],
_pad: f32,
}
impl SpriteInstanceTransform {
#[must_use]
pub fn from_sprite(sprite: &Sprite) -> Self {
let inv = mat3_inverse([sprite.s, sprite.h, sprite.f]);
Self {
inv_rot: [
[inv[0][0], inv[0][1], inv[0][2], 0.0],
[inv[1][0], inv[1][1], inv[1][2], 0.0],
[inv[2][0], inv[2][1], inv[2][2], 0.0],
],
pos: sprite.p,
_pad: 0.0,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SpriteModelRegistry {
entries: Vec<SpriteModel>,
chains: Vec<Vec<u32>>,
}
impl SpriteModelRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
fn push_entry(&mut self, model: SpriteModel) -> u32 {
let id = self.entries.len() as u32;
self.entries.push(model);
id
}
pub fn add(&mut self, model: SpriteModel) -> u32 {
let e = self.push_entry(model);
let id = self.chains.len() as u32;
self.chains.push(vec![e]);
id
}
pub fn add_lod(&mut self, model: SpriteModel, max_levels: u32) -> u32 {
let mut levels = vec![self.push_entry(model.clone())];
let mut cur = model;
for _ in 1..max_levels.max(1) {
if cur.dims == [1, 1, 1] {
break;
}
cur = cur.downsample();
levels.push(self.push_entry(cur.clone()));
}
let id = self.chains.len() as u32;
self.chains.push(levels);
id
}
pub fn fork(&mut self, parent: u32) -> u32 {
let src = self.chains[parent as usize].clone();
let levels: Vec<u32> = src
.iter()
.map(|&e| {
let copy = self.entries[e as usize].clone();
self.push_entry(copy)
})
.collect();
let id = self.chains.len() as u32;
self.chains.push(levels);
id
}
#[must_use]
pub fn model(&self, id: u32) -> &SpriteModel {
&self.entries[self.chains[id as usize][0] as usize]
}
pub fn model_mut(&mut self, id: u32) -> &mut SpriteModel {
let e = self.chains[id as usize][0] as usize;
&mut self.entries[e]
}
pub fn recolor_chain(&mut self, id: u32, f: impl Fn(u32) -> u32 + Copy) {
for li in 0..self.chains[id as usize].len() {
let e = self.chains[id as usize][li] as usize;
self.entries[e].recolor(f);
}
}
pub fn rebuild_lod(&mut self, id: u32) {
let levels = self.chains[id as usize].clone();
if levels.len() <= 1 {
return;
}
let mut cur = self.entries[levels[0] as usize].clone();
for &e in &levels[1..] {
cur = cur.downsample();
self.entries[e as usize] = cur.clone();
}
}
#[must_use]
pub fn len(&self) -> usize {
self.chains.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.chains.is_empty()
}
}
impl SpriteModel {
pub fn recolor(&mut self, f: impl Fn(u32) -> u32) {
for c in &mut self.colors {
*c = f(*c);
}
}
pub fn set_voxel(&mut self, x: u32, y: u32, z: u32, color: Option<u32>) -> bool {
if x >= self.dims[0] || y >= self.dims[1] || z >= self.dims[2] {
return false;
}
let owpc = self.occ_words_per_col as usize;
let cols = (self.dims[0] * self.dims[1]) as usize;
let col = (x + y * self.dims[0]) as usize;
let base = col * owpc;
let zw = (z >> 5) as usize;
let zb = z & 31;
let mut rank = 0usize;
for w in 0..zw {
rank += self.occupancy[base + w].count_ones() as usize;
}
let below_mask = if zb > 0 { (1u32 << zb) - 1 } else { 0 };
rank += (self.occupancy[base + zw] & below_mask).count_ones() as usize;
let idx = self.color_offsets[col] as usize + rank;
let was_set = (self.occupancy[base + zw] >> zb) & 1 == 1;
if let Some(rgba) = color {
if was_set {
self.colors[idx] = rgba; } else {
self.occupancy[base + zw] |= 1u32 << zb;
self.colors.insert(idx, rgba);
self.dirs.insert(idx, 0);
for c in &mut self.color_offsets[col + 1..=cols] {
*c += 1;
}
}
true
} else {
if !was_set {
return false;
}
self.occupancy[base + zw] &= !(1u32 << zb);
self.colors.remove(idx);
self.dirs.remove(idx);
for c in &mut self.color_offsets[col + 1..=cols] {
*c -= 1;
}
true
}
}
#[must_use]
pub fn bound_radius(&self) -> f32 {
let mut r2 = 0.0_f32;
for &cx in &[0.0, self.dims[0] as f32] {
for &cy in &[0.0, self.dims[1] as f32] {
for &cz in &[0.0, self.dims[2] as f32] {
let d = [cx - self.pivot[0], cy - self.pivot[1], cz - self.pivot[2]];
r2 = r2.max(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
}
}
}
r2.sqrt()
}
#[must_use]
#[allow(clippy::manual_checked_ops)] pub fn downsample(&self) -> SpriteModel {
let [fx, fy, fz] = self.dims;
let fidx = |x: u32, y: u32, z: u32| (x + y * fx + z * fx * fy) as usize;
let mut solid = vec![false; (fx * fy * fz) as usize];
let mut fine = vec![0u32; (fx * fy * fz) as usize];
let mut fine_dir = vec![0u32; (fx * fy * fz) as usize];
for x in 0..fx {
for y in 0..fy {
let col = (x + y * fx) as usize;
let base = col * self.occ_words_per_col as usize;
let off = self.color_offsets[col] as usize;
let mut seen = 0usize;
for z in 0..fz {
let w = base + (z >> 5) as usize;
if (self.occupancy[w] >> (z & 31)) & 1 == 1 {
fine[fidx(x, y, z)] = self.colors[off + seen];
fine_dir[fidx(x, y, z)] = self.dirs[off + seen];
solid[fidx(x, y, z)] = true;
seen += 1;
}
}
}
}
let nx = fx.div_ceil(2).max(1);
let ny = fy.div_ceil(2).max(1);
let nz = fz.div_ceil(2).max(1);
let owpc = nz.div_ceil(32).max(1);
let cols = (nx * ny) as usize;
let mut occupancy = vec![0u32; cols * owpc as usize];
let mut color_offsets = vec![0u32; cols + 1];
let mut colors: Vec<u32> = Vec::new();
let mut dirs: Vec<u32> = Vec::new();
for cy in 0..ny {
for cx in 0..nx {
let ccol = (cx + cy * nx) as usize;
color_offsets[ccol] = colors.len() as u32;
for cz in 0..nz {
let (mut a, mut r, mut g, mut b, mut n) = (0u32, 0u32, 0u32, 0u32, 0u32);
let mut rep_dir = 0u32;
for dz in 0..2 {
for dy in 0..2 {
for dx in 0..2 {
let (x, y, z) = (2 * cx + dx, 2 * cy + dy, 2 * cz + dz);
if x < fx && y < fy && z < fz && solid[fidx(x, y, z)] {
let c = fine[fidx(x, y, z)];
if n == 0 {
rep_dir = fine_dir[fidx(x, y, z)];
}
a += (c >> 24) & 0xff;
r += (c >> 16) & 0xff;
g += (c >> 8) & 0xff;
b += c & 0xff;
n += 1;
}
}
}
}
if n > 0 {
let avg = ((a / n) << 24) | ((r / n) << 16) | ((g / n) << 8) | (b / n);
let base = ccol * owpc as usize + (cz >> 5) as usize;
occupancy[base] |= 1u32 << (cz & 31);
colors.push(avg);
dirs.push(rep_dir);
}
}
}
}
color_offsets[cols] = colors.len() as u32;
SpriteModel {
dims: [nx, ny, nz],
occ_words_per_col: owpc,
pivot: [
self.pivot[0] * 0.5,
self.pivot[1] * 0.5,
self.pivot[2] * 0.5,
],
occupancy,
colors,
dirs,
color_offsets,
voxel_world_size: self.voxel_world_size * 2.0,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct ViewFrustum {
pub pos: [f32; 3],
pub right: [f32; 3],
pub down: [f32; 3],
pub forward: [f32; 3],
pub half_w: f32,
pub half_h: f32,
pub far: f32,
}
#[derive(Clone)]
struct CullInstance {
gpu: SpriteInstanceGpu,
chain_id: u32,
center: [f32; 3],
radius: f32,
colmul: Box<[u64; 256]>,
}
fn identity_colmul() -> Box<[u64; 256]> {
const LANE: u64 = 0x0100;
let w = LANE | (LANE << 16) | (LANE << 32) | (LANE << 48);
Box::new([w; 256])
}
fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[derive(Debug, Clone, Copy)]
pub struct SpriteInstance {
pub model_id: u32,
pub transform: SpriteInstanceTransform,
}
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable, Debug)]
struct SpriteModelMeta {
occupancy_offset: u32,
colors_offset: u32,
color_offsets_offset: u32,
occ_words_per_col: u32,
dims: [u32; 3],
_pad0: u32,
pivot: [f32; 3],
voxel_world_size: f32,
}
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable, Debug)]
struct SpriteInstanceGpu {
inv_rot0: [f32; 4],
inv_rot1: [f32; 4],
inv_rot2: [f32; 4],
pos: [f32; 3],
model_id: u32,
}
#[must_use]
fn mat3_inverse(cols: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
let [a, b, c] = cols; let cross = |u: [f32; 3], v: [f32; 3]| {
[
u[1] * v[2] - u[2] * v[1],
u[2] * v[0] - u[0] * v[2],
u[0] * v[1] - u[1] * v[0],
]
};
let bc = cross(b, c);
let ca = cross(c, a);
let ab = cross(a, b);
let det = a[0] * bc[0] + a[1] * bc[1] + a[2] * bc[2];
let inv_det = if det.abs() < 1e-12 { 0.0 } else { 1.0 / det };
[
[bc[0] * inv_det, ca[0] * inv_det, ab[0] * inv_det],
[bc[1] * inv_det, ca[1] * inv_det, ab[1] * inv_det],
[bc[2] * inv_det, ca[2] * inv_det, ab[2] * inv_det],
]
}
pub struct SpriteRegistryResident {
pub occupancy: wgpu::Buffer,
pub colors: wgpu::Buffer,
pub dirs: wgpu::Buffer,
pub color_offsets: wgpu::Buffer,
pub model_meta: wgpu::Buffer,
pub instances: wgpu::Buffer,
pub instance_capacity: u32,
pub colmul: wgpu::Buffer,
colmul_cap: u32,
pub tile_ranges: wgpu::Buffer,
tile_ranges_cap: u32,
pub tile_instances: wgpu::Buffer,
tile_instances_cap: u32,
cull: Vec<CullInstance>,
chains: Vec<Vec<u32>>,
}
impl SpriteRegistryResident {
#[must_use]
pub fn upload(
device: &wgpu::Device,
registry: &SpriteModelRegistry,
instances: &[SpriteInstance],
) -> Self {
let mut all_occ: Vec<u32> = Vec::new();
let mut all_colors: Vec<u32> = Vec::new();
let mut all_dirs: Vec<u32> = Vec::new();
let mut all_offsets: Vec<u32> = Vec::new();
let mut meta: Vec<SpriteModelMeta> = Vec::with_capacity(registry.entries.len());
for m in ®istry.entries {
meta.push(SpriteModelMeta {
occupancy_offset: all_occ.len() as u32,
colors_offset: all_colors.len() as u32,
color_offsets_offset: all_offsets.len() as u32,
occ_words_per_col: m.occ_words_per_col,
dims: m.dims,
_pad0: 0,
pivot: m.pivot,
voxel_world_size: m.voxel_world_size,
});
all_occ.extend_from_slice(&m.occupancy);
all_colors.extend_from_slice(&m.colors);
all_dirs.extend_from_slice(&m.dirs);
all_offsets.extend_from_slice(&m.color_offsets);
}
let cull: Vec<CullInstance> = instances
.iter()
.map(|i| CullInstance {
gpu: SpriteInstanceGpu {
inv_rot0: i.transform.inv_rot[0],
inv_rot1: i.transform.inv_rot[1],
inv_rot2: i.transform.inv_rot[2],
pos: i.transform.pos,
model_id: i.model_id, },
chain_id: i.model_id,
center: i.transform.pos,
radius: registry.model(i.model_id).bound_radius(),
colmul: identity_colmul(),
})
.collect();
let seed: Vec<SpriteInstanceGpu> = cull.iter().map(|c| c.gpu).collect();
let instances_buf = {
use wgpu::util::DeviceExt;
let one = [SpriteInstanceGpu::zeroed()];
let src: &[SpriteInstanceGpu] = if seed.is_empty() { &one } else { &seed };
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("roxlap-gpu sprite_reg.instances"),
contents: bytemuck::cast_slice(src),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
})
};
let tile_ranges = storage_dst_u32(device, "roxlap-gpu sprite_reg.tile_ranges", 1);
let tile_instances = storage_dst_u32(device, "roxlap-gpu sprite_reg.tile_instances", 1);
let colmul_cap = (cull.len() as u32).max(1) * 256 * 2;
let colmul = storage_dst_u32(device, "roxlap-gpu sprite_reg.colmul", colmul_cap);
Self {
occupancy: storage_u32(device, "roxlap-gpu sprite_reg.occupancy", &all_occ),
colors: storage_u32(device, "roxlap-gpu sprite_reg.colors", &all_colors),
dirs: storage_u32(device, "roxlap-gpu sprite_reg.dirs", &all_dirs),
color_offsets: storage_u32(device, "roxlap-gpu sprite_reg.color_offsets", &all_offsets),
model_meta: storage_pod(device, "roxlap-gpu sprite_reg.model_meta", &meta),
instances: instances_buf,
instance_capacity: cull.len() as u32,
colmul,
colmul_cap,
tile_ranges,
tile_ranges_cap: 1,
tile_instances,
tile_instances_cap: 1,
cull,
chains: registry.chains.clone(),
}
}
pub fn set_instance_colmul(&mut self, tables: &[[u64; 256]]) {
for (ci, t) in self.cull.iter_mut().zip(tables) {
ci.colmul.copy_from_slice(t);
}
}
pub fn update_transforms(&mut self, instances: &[SpriteInstance]) {
debug_assert_eq!(
instances.len(),
self.cull.len(),
"update_transforms instance count must match upload"
);
for (ci, inst) in self.cull.iter_mut().zip(instances) {
ci.gpu.inv_rot0 = inst.transform.inv_rot[0];
ci.gpu.inv_rot1 = inst.transform.inv_rot[1];
ci.gpu.inv_rot2 = inst.transform.inv_rot[2];
ci.gpu.pos = inst.transform.pos;
ci.center = inst.transform.pos;
}
}
#[allow(clippy::too_many_arguments)]
pub fn cull_bin_upload(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
f: &ViewFrustum,
screen_w: u32,
screen_h: u32,
tile_size: u32,
lod_px: f32,
) -> (u32, u32, u32) {
let tiles_x = screen_w.div_ceil(tile_size).max(1);
let tiles_y = screen_h.div_ceil(tile_size).max(1);
let n_tiles = (tiles_x * tiles_y) as usize;
let nw = (1.0 + f.half_w * f.half_w).sqrt();
let nh = (1.0 + f.half_h * f.half_h).sqrt();
let cx = screen_w as f32 * 0.5;
let cy = screen_h as f32 * 0.5;
let px_per_world = cx / f.half_w; let ts = tile_size as f32;
let tx_max = tiles_x as i32 - 1;
let ty_max = tiles_y as i32 - 1;
let mut visible: Vec<SpriteInstanceGpu> = Vec::with_capacity(self.cull.len());
let mut boxes: Vec<[i32; 4]> = Vec::with_capacity(self.cull.len());
let mut visible_colmul: Vec<u32> = Vec::with_capacity(self.cull.len() * 512);
let mut counts = vec![0u32; n_tiles];
for ci in &self.cull {
let rel = [
ci.center[0] - f.pos[0],
ci.center[1] - f.pos[1],
ci.center[2] - f.pos[2],
];
let z = dot3(rel, f.forward);
let r = ci.radius;
if z + r < 0.0 || z - r > f.far {
continue; }
let x = dot3(rel, f.right);
if (x - f.half_w * z) > r * nw || (-x - f.half_w * z) > r * nw {
continue; }
let y = dot3(rel, f.down);
if (y - f.half_h * z) > r * nh || (-y - f.half_h * z) > r * nh {
continue; }
let (tx0, tx1, ty0, ty1) = if z > 1e-3 {
let sx = cx + (x / z) * px_per_world;
let sy = cy + (y / z) * px_per_world;
let sr = (r / z) * px_per_world;
(
(((sx - sr) / ts).floor() as i32).clamp(0, tx_max),
(((sx + sr) / ts).floor() as i32).clamp(0, tx_max),
(((sy - sr) / ts).floor() as i32).clamp(0, ty_max),
(((sy + sr) / ts).floor() as i32).clamp(0, ty_max),
)
} else {
(0, tx_max, 0, ty_max)
};
let chain = &self.chains[ci.chain_id as usize];
let level = if z > 1e-3 && chain.len() > 1 {
let voxel_px = px_per_world / z; ((lod_px / voxel_px).log2().ceil().max(0.0) as usize).min(chain.len() - 1)
} else {
0
};
let mut g = ci.gpu;
g.model_id = chain[level];
visible.push(g);
boxes.push([tx0, tx1, ty0, ty1]);
for &w in ci.colmul.iter() {
visible_colmul.push((w & 0xffff_ffff) as u32);
visible_colmul.push((w >> 32) as u32);
}
for ty in ty0..=ty1 {
for tx in tx0..=tx1 {
counts[(ty * tiles_x as i32 + tx) as usize] += 1;
}
}
}
if visible.is_empty() {
return (0, tiles_x, tiles_y);
}
let mut tile_ranges = vec![0u32; n_tiles * 2];
let mut running = 0u32;
for t in 0..n_tiles {
tile_ranges[2 * t] = running; tile_ranges[2 * t + 1] = counts[t]; running += counts[t];
}
let total = running as usize;
let mut tile_instances = vec![0u32; total.max(1)];
let mut cursor: Vec<u32> = (0..n_tiles).map(|t| tile_ranges[2 * t]).collect();
for (vis_idx, b) in boxes.iter().enumerate() {
for ty in b[2]..=b[3] {
for tx in b[0]..=b[1] {
let t = (ty * tiles_x as i32 + tx) as usize;
tile_instances[cursor[t] as usize] = vis_idx as u32;
cursor[t] += 1;
}
}
}
queue.write_buffer(&self.instances, 0, bytemuck::cast_slice(&visible));
let need_ranges = tile_ranges.len() as u32;
if need_ranges > self.tile_ranges_cap {
self.tile_ranges_cap = need_ranges.next_power_of_two();
self.tile_ranges = storage_dst_u32(
device,
"roxlap-gpu sprite_reg.tile_ranges",
self.tile_ranges_cap,
);
}
let need_inst = tile_instances.len() as u32;
if need_inst > self.tile_instances_cap {
self.tile_instances_cap = need_inst.next_power_of_two();
self.tile_instances = storage_dst_u32(
device,
"roxlap-gpu sprite_reg.tile_instances",
self.tile_instances_cap,
);
}
queue.write_buffer(&self.tile_ranges, 0, bytemuck::cast_slice(&tile_ranges));
queue.write_buffer(
&self.tile_instances,
0,
bytemuck::cast_slice(&tile_instances),
);
let need_colmul = visible_colmul.len() as u32;
if need_colmul > self.colmul_cap {
self.colmul_cap = need_colmul.next_power_of_two();
self.colmul = storage_dst_u32(device, "roxlap-gpu sprite_reg.colmul", self.colmul_cap);
}
queue.write_buffer(&self.colmul, 0, bytemuck::cast_slice(&visible_colmul));
(visible.len() as u32, tiles_x, tiles_y)
}
}
fn storage_u32(device: &wgpu::Device, label: &str, data: &[u32]) -> wgpu::Buffer {
use wgpu::util::DeviceExt;
let bytes: &[u8] = if data.is_empty() {
bytemuck::cast_slice(&[0u32])
} else {
bytemuck::cast_slice(data)
};
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(label),
contents: bytes,
usage: wgpu::BufferUsages::STORAGE,
})
}
fn storage_dst_u32(device: &wgpu::Device, label: &str, cap: u32) -> wgpu::Buffer {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size: u64::from(cap.max(1)) * 4,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
fn storage_pod<T: Pod + Zeroable>(device: &wgpu::Device, label: &str, data: &[T]) -> wgpu::Buffer {
use wgpu::util::DeviceExt;
let one = [T::zeroed()];
let src: &[T] = if data.is_empty() { &one } else { data };
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(label),
contents: bytemuck::cast_slice(src),
usage: wgpu::BufferUsages::STORAGE,
})
}
#[cfg(test)]
mod tests {
use super::*;
use roxlap_formats::kv6::{Kv6, Voxel};
fn kv6_unsorted() -> Kv6 {
let mk = |z, col| Voxel {
col,
z,
vis: 0,
dir: 0,
};
Kv6 {
xsiz: 2,
ysiz: 1,
zsiz: 8,
xpiv: 0.0,
ypiv: 0.0,
zpiv: 0.0,
voxels: vec![mk(5, 0xAA), mk(1, 0xBB), mk(3, 0xCC)],
xlen: vec![2, 1],
ylen: vec![vec![2], vec![1]],
palette: None,
}
}
#[test]
fn occupancy_bits_set_at_voxel_z() {
let m = build_sprite_model(&kv6_unsorted());
assert_eq!(m.dims, [2, 1, 8]);
assert_eq!(m.occ_words_per_col, 1); assert_eq!(m.occupancy[0], (1 << 1) | (1 << 5));
assert_eq!(m.occupancy[1], 1 << 3);
}
#[test]
fn colors_are_ascending_z_for_rank_lookup() {
let m = build_sprite_model(&kv6_unsorted());
assert_eq!(m.color_offsets, vec![0, 2, 3]);
assert_eq!(&m.colors, &[0xBB, 0xAA, 0xCC]);
}
#[test]
fn identity_basis_inverts_to_identity() {
let inv = mat3_inverse([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
assert_eq!(inv, [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
}
#[test]
fn fork_is_independent_of_parent() {
let mut reg = SpriteModelRegistry::new();
let base = reg.add(build_sprite_model(&kv6_unsorted()));
let forked = reg.fork(base);
assert_ne!(base, forked);
reg.model_mut(forked).recolor(|_| 0x11);
assert_eq!(®.model(base).colors, &[0xBB, 0xAA, 0xCC]);
assert_eq!(®.model(forked).colors, &[0x11, 0x11, 0x11]);
}
#[test]
fn registry_gpu_structs_have_expected_sizes() {
assert_eq!(std::mem::size_of::<SpriteModelMeta>(), 48);
assert_eq!(std::mem::size_of::<SpriteInstanceGpu>(), 64);
}
#[test]
fn add_lod_builds_halving_mip_chain() {
let mut reg = SpriteModelRegistry::new();
let id = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
let m0 = reg.model(id);
assert_eq!(m0.dims, [2, 1, 8]);
assert!((m0.voxel_world_size - 1.0).abs() < 1e-6);
}
fn kv6_from(xsiz: u32, ysiz: u32, zsiz: u32, voxels: &[(u32, u32, u16, u32)]) -> Kv6 {
let mut ylen = vec![vec![0u16; ysiz as usize]; xsiz as usize];
let mut flat = Vec::new();
for x in 0..xsiz {
for y in 0..ysiz {
let mut col: Vec<(u16, u32)> = voxels
.iter()
.filter(|(vx, vy, _, _)| *vx == x && *vy == y)
.map(|(_, _, z, c)| (*z, *c))
.collect();
col.sort_by_key(|(z, _)| *z);
ylen[x as usize][y as usize] = col.len() as u16;
for (z, c) in col {
flat.push(Voxel {
col: c,
z,
vis: 0,
dir: 0,
});
}
}
}
let xlen = ylen
.iter()
.map(|c| c.iter().map(|&v| u32::from(v)).sum())
.collect();
Kv6 {
xsiz,
ysiz,
zsiz,
xpiv: 0.0,
ypiv: 0.0,
zpiv: 0.0,
voxels: flat,
xlen,
ylen,
palette: None,
}
}
fn offsets_consistent(m: &SpriteModel) -> bool {
let cols = (m.dims[0] * m.dims[1]) as usize;
if m.color_offsets.len() != cols + 1 {
return false;
}
for w in m.color_offsets.windows(2) {
if w[1] < w[0] {
return false;
}
}
m.color_offsets[cols] as usize == m.colors.len()
}
#[test]
fn carve_two_layers_keeps_offsets_consistent() {
let kv6 = kv6_from(
3,
2,
8,
&[
(0, 0, 0, 0xA0),
(0, 0, 1, 0xA1),
(0, 0, 5, 0xA5),
(1, 0, 1, 0xB1),
(2, 1, 0, 0xC0),
(2, 1, 3, 0xC3),
],
);
let mut m = build_sprite_model(&kv6);
assert!(offsets_consistent(&m));
for z in 0..2u32 {
for y in 0..m.dims[1] {
for x in 0..m.dims[0] {
m.set_voxel(x, y, z, None);
}
}
assert!(offsets_consistent(&m), "inconsistent after carving z={z}");
let _ = m.downsample();
}
}
#[test]
fn set_voxel_inserts_replaces_and_clears() {
let mut m = build_sprite_model(&kv6_unsorted());
assert!(m.set_voxel(0, 0, 3, Some(0x55)));
assert_eq!(m.occupancy[0], (1 << 1) | (1 << 3) | (1 << 5));
assert_eq!(m.color_offsets, vec![0, 3, 4]);
assert_eq!(&m.colors, &[0xBB, 0x55, 0xAA, 0xCC]);
assert!(m.set_voxel(0, 0, 3, Some(0x66)));
assert_eq!(&m.colors, &[0xBB, 0x66, 0xAA, 0xCC]);
assert_eq!(m.color_offsets, vec![0, 3, 4]);
assert!(m.set_voxel(0, 0, 1, None));
assert_eq!(m.occupancy[0], (1 << 3) | (1 << 5));
assert_eq!(m.color_offsets, vec![0, 2, 3]);
assert_eq!(&m.colors, &[0x66, 0xAA, 0xCC]);
assert!(!m.set_voxel(0, 0, 2, None));
assert!(!m.set_voxel(9, 0, 0, Some(1)));
}
#[test]
fn rebuild_lod_refreshes_coarse_levels_from_mip0() {
let mut reg = SpriteModelRegistry::new();
let id = reg.add_lod(build_sprite_model(&kv6_unsorted()), 3);
reg.model_mut(id).recolor(|_| 0x0000_2000);
reg.rebuild_lod(id);
let lvl1_entry = reg.chains[id as usize][1] as usize;
assert!(reg.entries[lvl1_entry]
.colors
.iter()
.all(|&c| c == 0x0000_2000));
}
}