1#![allow(
17 clippy::cast_precision_loss,
18 clippy::cast_possible_truncation,
19 clippy::cast_possible_wrap,
20 clippy::cast_sign_loss,
21 clippy::many_single_char_names,
22 clippy::similar_names
23)]
24
25use bytemuck::{Pod, Zeroable};
26use roxlap_formats::kv6::Kv6;
27use roxlap_formats::material::material_for_color;
28use roxlap_formats::sprite::Sprite;
29use roxlap_formats::voxel_clip::{DecodedClip, VoxelFrame};
30
31#[derive(Debug, Clone)]
33pub struct SpriteModel {
34 pub dims: [u32; 3],
36 pub occ_words_per_col: u32,
38 pub pivot: [f32; 3],
40 pub occupancy: Vec<u32>,
42 pub colors: Vec<u32>,
44 pub dirs: Vec<u32>,
49 pub color_offsets: Vec<u32>,
52 pub materials: Vec<u8>,
58 pub voxel_world_size: f32,
63}
64
65#[must_use]
73pub fn build_sprite_model(kv6: &Kv6) -> SpriteModel {
74 build_sprite_model_inner(kv6, &[])
75}
76
77#[must_use]
87pub fn build_sprite_model_with_materials(kv6: &Kv6, material_map: &[(u32, u8)]) -> SpriteModel {
88 build_sprite_model_inner(kv6, material_map)
89}
90
91fn build_sprite_model_inner(kv6: &Kv6, material_map: &[(u32, u8)]) -> SpriteModel {
92 let (mx, my, mz) = (kv6.xsiz, kv6.ysiz, kv6.zsiz);
93 let occ_words_per_col = mz.div_ceil(32).max(1);
94 let cols = (mx * my) as usize;
95 let want_mats = !material_map.is_empty();
96
97 let mut occupancy = vec![0u32; cols * occ_words_per_col as usize];
98 let mut color_offsets = vec![0u32; cols + 1];
99 let mut colors: Vec<u32> = Vec::with_capacity(kv6.voxels.len());
100 let mut dirs: Vec<u32> = Vec::with_capacity(kv6.voxels.len());
101 let mut materials: Vec<u8> = if want_mats {
102 Vec::with_capacity(kv6.voxels.len())
103 } else {
104 Vec::new()
105 };
106
107 let mut buckets: Vec<Vec<(u16, u32, u8)>> = vec![Vec::new(); cols];
111 let mut voxel_iter = kv6.voxels.iter();
112 for x in 0..mx {
113 for y in 0..my {
114 let col = (x + y * mx) as usize;
115 let count = kv6.ylen[x as usize][y as usize];
116 for _ in 0..count {
117 let v = voxel_iter.next().expect("KV6 ylen / voxels.len mismatch");
118 buckets[col].push((v.z, v.col, v.dir));
119 }
120 }
121 }
122
123 for (col, bucket) in buckets.iter_mut().enumerate() {
128 color_offsets[col] = colors.len() as u32;
129 bucket.sort_by_key(|(z, _, _)| *z);
130 for &(z, col_rgba, dir) in bucket.iter() {
131 let z = u32::from(z);
132 let base = col * occ_words_per_col as usize + (z >> 5) as usize;
133 occupancy[base] |= 1u32 << (z & 31);
134 colors.push(col_rgba);
135 dirs.push(u32::from(dir));
136 if want_mats {
137 materials.push(material_for_color(material_map, col_rgba));
138 }
139 }
140 }
141 color_offsets[cols] = colors.len() as u32;
142
143 SpriteModel {
144 dims: [mx, my, mz],
145 occ_words_per_col,
146 pivot: [kv6.xpiv, kv6.ypiv, kv6.zpiv],
147 occupancy,
148 color_offsets,
149 colors,
150 dirs,
151 materials,
152 voxel_world_size: 1.0,
153 }
154}
155
156#[must_use]
167pub fn sprite_model_from_voxel_frame(
168 frame: &VoxelFrame,
169 dirs: &[u32],
170 dims: [u32; 3],
171 pivot: [f32; 3],
172 voxel_world_size: f32,
173) -> SpriteModel {
174 let occ_words_per_col = dims[2].div_ceil(32).max(1);
175 let cols = (dims[0] * dims[1]) as usize;
176 debug_assert_eq!(frame.occupancy.len(), cols * occ_words_per_col as usize);
177 debug_assert_eq!(frame.color_offsets.len(), cols + 1);
178 debug_assert_eq!(dirs.len(), frame.colors.len());
179 SpriteModel {
180 dims,
181 occ_words_per_col,
182 pivot,
183 occupancy: frame.occupancy.clone(),
184 colors: frame.colors.clone(),
185 dirs: dirs.to_vec(),
186 color_offsets: frame.color_offsets.clone(),
187 materials: Vec::new(),
189 voxel_world_size,
190 }
191}
192
193#[must_use]
199pub fn sprite_model_from_clip_frame(clip: &DecodedClip, frame: usize) -> SpriteModel {
200 sprite_model_from_voxel_frame(
201 &clip.frames[frame],
202 &clip.dirs[frame],
203 clip.dims,
204 clip.pivot,
205 clip.voxel_world_size,
206 )
207}
208
209#[repr(C)]
214#[derive(Clone, Copy, Pod, Zeroable, Debug)]
215pub struct SpriteInstanceTransform {
216 pub inv_rot: [[f32; 4]; 3],
219 pub pos: [f32; 3],
221 _pad: f32,
222}
223
224impl SpriteInstanceTransform {
225 #[must_use]
228 pub fn from_sprite(sprite: &Sprite) -> Self {
229 let inv = mat3_inverse([sprite.s, sprite.h, sprite.f]);
230 Self {
231 inv_rot: [
232 [inv[0][0], inv[0][1], inv[0][2], 0.0],
233 [inv[1][0], inv[1][1], inv[1][2], 0.0],
234 [inv[2][0], inv[2][1], inv[2][2], 0.0],
235 ],
236 pos: sprite.p,
237 _pad: 0.0,
238 }
239 }
240}
241
242#[derive(Debug, Clone, Default)]
250pub struct SpriteModelRegistry {
251 entries: Vec<SpriteModel>,
253 chains: Vec<Vec<u32>>,
255}
256
257impl SpriteModelRegistry {
258 #[must_use]
259 pub fn new() -> Self {
260 Self::default()
261 }
262
263 fn push_entry(&mut self, model: SpriteModel) -> u32 {
264 let id = self.entries.len() as u32;
265 self.entries.push(model);
266 id
267 }
268
269 pub fn add(&mut self, model: SpriteModel) -> u32 {
271 let e = self.push_entry(model);
272 let id = self.chains.len() as u32;
273 self.chains.push(vec![e]);
274 id
275 }
276
277 pub fn add_lod(&mut self, model: SpriteModel, max_levels: u32) -> u32 {
281 let mut levels = vec![self.push_entry(model.clone())];
282 let mut cur = model;
283 for _ in 1..max_levels.max(1) {
284 if cur.dims == [1, 1, 1] {
285 break;
286 }
287 cur = cur.downsample();
288 levels.push(self.push_entry(cur.clone()));
289 }
290 let id = self.chains.len() as u32;
291 self.chains.push(levels);
292 id
293 }
294
295 pub fn fork(&mut self, parent: u32) -> u32 {
303 let src = self.chains[parent as usize].clone();
304 let levels: Vec<u32> = src
305 .iter()
306 .map(|&e| {
307 let copy = self.entries[e as usize].clone();
308 self.push_entry(copy)
309 })
310 .collect();
311 let id = self.chains.len() as u32;
312 self.chains.push(levels);
313 id
314 }
315
316 #[must_use]
318 pub fn model(&self, id: u32) -> &SpriteModel {
319 &self.entries[self.chains[id as usize][0] as usize]
320 }
321
322 #[must_use]
326 pub fn model_checked(&self, id: u32) -> Option<&SpriteModel> {
327 let entry = *self.chains.get(id as usize)?.first()?;
328 self.entries.get(entry as usize)
329 }
330
331 pub fn model_mut(&mut self, id: u32) -> &mut SpriteModel {
337 let e = self.chains[id as usize][0] as usize;
338 &mut self.entries[e]
339 }
340
341 pub fn recolor_chain(&mut self, id: u32, f: impl Fn(u32) -> u32 + Copy) {
344 for li in 0..self.chains[id as usize].len() {
345 let e = self.chains[id as usize][li] as usize;
346 self.entries[e].recolor(f);
347 }
348 }
349
350 pub fn rebuild_lod(&mut self, id: u32) {
355 let levels = self.chains[id as usize].clone();
356 if levels.len() <= 1 {
357 return;
358 }
359 let mut cur = self.entries[levels[0] as usize].clone();
360 for &e in &levels[1..] {
361 cur = cur.downsample();
362 self.entries[e as usize] = cur.clone();
363 }
364 }
365
366 pub fn remove(&mut self, chain_id: u32) {
380 let Some(entries) = self.chains.get(chain_id as usize) else {
381 return;
382 };
383 let entries = entries.clone();
385 for e in entries {
386 self.entries[e as usize] = SpriteModel::empty();
387 }
388 self.chains[chain_id as usize] = Vec::new(); }
390
391 #[must_use]
394 pub fn is_live(&self, chain_id: u32) -> bool {
395 self.chains
396 .get(chain_id as usize)
397 .is_some_and(|c| !c.is_empty())
398 }
399
400 #[must_use]
404 pub fn len(&self) -> usize {
405 self.chains.len()
406 }
407
408 #[must_use]
409 pub fn is_empty(&self) -> bool {
410 self.chains.is_empty()
411 }
412}
413
414impl SpriteModel {
415 #[must_use]
422 pub fn empty() -> Self {
423 Self {
424 dims: [0, 0, 0],
425 occ_words_per_col: 1,
426 pivot: [0.0, 0.0, 0.0],
427 occupancy: Vec::new(),
428 colors: Vec::new(),
429 dirs: Vec::new(),
430 color_offsets: vec![0],
431 materials: Vec::new(),
432 voxel_world_size: 1.0,
433 }
434 }
435
436 pub fn recolor(&mut self, f: impl Fn(u32) -> u32) {
442 for c in &mut self.colors {
443 *c = f(*c);
444 }
445 }
446
447 pub fn set_voxel(&mut self, x: u32, y: u32, z: u32, color: Option<u32>) -> bool {
458 if x >= self.dims[0] || y >= self.dims[1] || z >= self.dims[2] {
459 return false;
460 }
461 let owpc = self.occ_words_per_col as usize;
462 let cols = (self.dims[0] * self.dims[1]) as usize;
463 let col = (x + y * self.dims[0]) as usize;
464 let base = col * owpc;
465 let zw = (z >> 5) as usize;
466 let zb = z & 31;
467
468 let mut rank = 0usize;
470 for w in 0..zw {
471 rank += self.occupancy[base + w].count_ones() as usize;
472 }
473 let below_mask = if zb > 0 { (1u32 << zb) - 1 } else { 0 };
474 rank += (self.occupancy[base + zw] & below_mask).count_ones() as usize;
475 let idx = self.color_offsets[col] as usize + rank;
476 let was_set = (self.occupancy[base + zw] >> zb) & 1 == 1;
477
478 if let Some(rgba) = color {
479 if was_set {
480 self.colors[idx] = rgba; } else {
482 self.occupancy[base + zw] |= 1u32 << zb;
483 self.colors.insert(idx, rgba);
484 self.dirs.insert(idx, 0);
487 if !self.materials.is_empty() {
488 self.materials.insert(idx, 0); }
490 for c in &mut self.color_offsets[col + 1..=cols] {
491 *c += 1;
492 }
493 }
494 true
495 } else {
496 if !was_set {
497 return false;
498 }
499 self.occupancy[base + zw] &= !(1u32 << zb);
500 self.colors.remove(idx);
501 self.dirs.remove(idx);
502 if !self.materials.is_empty() {
503 self.materials.remove(idx);
504 }
505 for c in &mut self.color_offsets[col + 1..=cols] {
506 *c -= 1;
507 }
508 true
509 }
510 }
511
512 #[must_use]
517 pub fn bound_radius(&self) -> f32 {
518 let mut r2 = 0.0_f32;
519 for &cx in &[0.0, self.dims[0] as f32] {
520 for &cy in &[0.0, self.dims[1] as f32] {
521 for &cz in &[0.0, self.dims[2] as f32] {
522 let d = [cx - self.pivot[0], cy - self.pivot[1], cz - self.pivot[2]];
523 r2 = r2.max(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
524 }
525 }
526 }
527 r2.sqrt()
528 }
529
530 #[must_use]
536 #[allow(clippy::manual_checked_ops)] pub fn downsample(&self) -> SpriteModel {
538 let [fx, fy, fz] = self.dims;
539 let fidx = |x: u32, y: u32, z: u32| (x + y * fx + z * fx * fy) as usize;
540
541 let has_mats = !self.materials.is_empty();
544 let mut solid = vec![false; (fx * fy * fz) as usize];
545 let mut fine = vec![0u32; (fx * fy * fz) as usize];
546 let mut fine_dir = vec![0u32; (fx * fy * fz) as usize];
547 let mut fine_mat = vec![0u8; (fx * fy * fz) as usize];
548 for x in 0..fx {
549 for y in 0..fy {
550 let col = (x + y * fx) as usize;
551 let base = col * self.occ_words_per_col as usize;
552 let off = self.color_offsets[col] as usize;
553 let mut seen = 0usize;
554 for z in 0..fz {
555 let w = base + (z >> 5) as usize;
556 if (self.occupancy[w] >> (z & 31)) & 1 == 1 {
557 fine[fidx(x, y, z)] = self.colors[off + seen];
558 fine_dir[fidx(x, y, z)] = self.dirs[off + seen];
559 if has_mats {
560 fine_mat[fidx(x, y, z)] = self.materials[off + seen];
561 }
562 solid[fidx(x, y, z)] = true;
563 seen += 1;
564 }
565 }
566 }
567 }
568
569 let nx = fx.div_ceil(2).max(1);
570 let ny = fy.div_ceil(2).max(1);
571 let nz = fz.div_ceil(2).max(1);
572 let owpc = nz.div_ceil(32).max(1);
573 let cols = (nx * ny) as usize;
574 let mut occupancy = vec![0u32; cols * owpc as usize];
575 let mut color_offsets = vec![0u32; cols + 1];
576 let mut colors: Vec<u32> = Vec::new();
577 let mut dirs: Vec<u32> = Vec::new();
578 let mut materials: Vec<u8> = Vec::new();
579
580 for cy in 0..ny {
583 for cx in 0..nx {
584 let ccol = (cx + cy * nx) as usize;
585 color_offsets[ccol] = colors.len() as u32;
586 for cz in 0..nz {
587 let (mut a, mut r, mut g, mut b, mut n) = (0u32, 0u32, 0u32, 0u32, 0u32);
588 let mut rep_dir = 0u32;
592 let mut rep_mat = 0u8;
593 for dz in 0..2 {
594 for dy in 0..2 {
595 for dx in 0..2 {
596 let (x, y, z) = (2 * cx + dx, 2 * cy + dy, 2 * cz + dz);
597 if x < fx && y < fy && z < fz && solid[fidx(x, y, z)] {
598 let c = fine[fidx(x, y, z)];
599 if n == 0 {
600 rep_dir = fine_dir[fidx(x, y, z)];
601 rep_mat = fine_mat[fidx(x, y, z)];
602 }
603 a += (c >> 24) & 0xff;
604 r += (c >> 16) & 0xff;
605 g += (c >> 8) & 0xff;
606 b += c & 0xff;
607 n += 1;
608 }
609 }
610 }
611 }
612 if n > 0 {
613 let avg = ((a / n) << 24) | ((r / n) << 16) | ((g / n) << 8) | (b / n);
614 let base = ccol * owpc as usize + (cz >> 5) as usize;
615 occupancy[base] |= 1u32 << (cz & 31);
616 colors.push(avg);
617 dirs.push(rep_dir);
618 if has_mats {
619 materials.push(rep_mat);
620 }
621 }
622 }
623 }
624 }
625 color_offsets[cols] = colors.len() as u32;
626
627 SpriteModel {
628 dims: [nx, ny, nz],
629 occ_words_per_col: owpc,
630 pivot: [
631 self.pivot[0] * 0.5,
632 self.pivot[1] * 0.5,
633 self.pivot[2] * 0.5,
634 ],
635 occupancy,
636 colors,
637 dirs,
638 color_offsets,
639 materials,
640 voxel_world_size: self.voxel_world_size * 2.0,
641 }
642 }
643}
644
645#[derive(Clone, Copy, Debug)]
650pub struct ViewFrustum {
651 pub pos: [f32; 3],
652 pub right: [f32; 3],
653 pub down: [f32; 3],
654 pub forward: [f32; 3],
655 pub half_w: f32,
656 pub half_h: f32,
657 pub far: f32,
658}
659
660#[derive(Clone)]
663struct CullInstance {
664 gpu: SpriteInstanceGpu,
667 chain_id: u32,
669 center: [f32; 3],
670 radius: f32,
671 colmul: Box<[u64; 256]>,
677}
678
679fn identity_colmul() -> Box<[u64; 256]> {
682 const LANE: u64 = 0x0100;
683 let w = LANE | (LANE << 16) | (LANE << 32) | (LANE << 48);
684 Box::new([w; 256])
685}
686
687fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
688 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
689}
690
691fn make_cull(registry: &SpriteModelRegistry, i: &SpriteInstance) -> CullInstance {
697 CullInstance {
698 gpu: SpriteInstanceGpu {
699 inv_rot0: i.transform.inv_rot[0],
700 inv_rot1: i.transform.inv_rot[1],
701 inv_rot2: i.transform.inv_rot[2],
702 pos: i.transform.pos,
703 model_id: i.model_id, material: u32::from(i.material),
705 alpha_mul: f32::from(i.alpha_mul) / 255.0,
706 _pad0: 0,
707 _pad1: 0,
708 },
709 chain_id: i.model_id,
710 center: i.transform.pos,
711 radius: registry.model(i.model_id).bound_radius(),
712 colmul: identity_colmul(),
713 }
714}
715
716fn instances_buffer(device: &wgpu::Device, cap: u32) -> wgpu::Buffer {
721 device.create_buffer(&wgpu::BufferDescriptor {
722 label: Some("roxlap-gpu sprite_reg.instances"),
723 size: u64::from(cap.max(1)) * std::mem::size_of::<SpriteInstanceGpu>() as u64,
724 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
725 mapped_at_creation: false,
726 })
727}
728
729#[derive(Debug, Clone, Copy)]
731pub struct SpriteInstance {
732 pub model_id: u32,
733 pub transform: SpriteInstanceTransform,
734 pub material: u8,
738 pub alpha_mul: u8,
741}
742
743impl SpriteInstance {
744 #[must_use]
747 pub fn new(model_id: u32, transform: SpriteInstanceTransform) -> Self {
748 Self {
749 model_id,
750 transform,
751 material: 0,
752 alpha_mul: 255,
753 }
754 }
755}
756
757#[repr(C)]
761#[derive(Clone, Copy, Pod, Zeroable, Debug)]
762struct SpriteModelMeta {
763 occupancy_offset: u32,
764 colors_offset: u32,
765 color_offsets_offset: u32,
766 occ_words_per_col: u32,
767 dims: [u32; 3],
768 has_vox_materials: u32,
771 pivot: [f32; 3],
772 voxel_world_size: f32,
774}
775
776#[repr(C)]
780#[derive(Clone, Copy, Pod, Zeroable, Debug)]
781struct SpriteInstanceGpu {
782 inv_rot0: [f32; 4],
783 inv_rot1: [f32; 4],
784 inv_rot2: [f32; 4],
785 pos: [f32; 3],
786 model_id: u32,
787 material: u32,
789 alpha_mul: f32,
791 _pad0: u32,
792 _pad1: u32,
793}
794
795#[must_use]
799fn mat3_inverse(cols: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
800 let [a, b, c] = cols; let cross = |u: [f32; 3], v: [f32; 3]| {
803 [
804 u[1] * v[2] - u[2] * v[1],
805 u[2] * v[0] - u[0] * v[2],
806 u[0] * v[1] - u[1] * v[0],
807 ]
808 };
809 let bc = cross(b, c);
810 let ca = cross(c, a);
811 let ab = cross(a, b);
812 let det = a[0] * bc[0] + a[1] * bc[1] + a[2] * bc[2];
813 let inv_det = if det.abs() < 1e-12 { 0.0 } else { 1.0 / det };
814 [
817 [bc[0] * inv_det, ca[0] * inv_det, ab[0] * inv_det],
818 [bc[1] * inv_det, ca[1] * inv_det, ab[1] * inv_det],
819 [bc[2] * inv_det, ca[2] * inv_det, ab[2] * inv_det],
820 ]
821}
822
823pub struct SpriteRegistryResident {
830 pub occupancy: wgpu::Buffer,
831 pub colors: wgpu::Buffer,
832 pub dirs: wgpu::Buffer,
836 pub materials_vox: wgpu::Buffer,
841 pub color_offsets: wgpu::Buffer,
842 pub model_meta: wgpu::Buffer,
843 pub instances: wgpu::Buffer,
846 pub instance_capacity: u32,
847 pub colmul: wgpu::Buffer,
852 colmul_cap: u32,
853 pub tile_ranges: wgpu::Buffer,
856 tile_ranges_cap: u32,
857 pub tile_instances: wgpu::Buffer,
860 tile_instances_cap: u32,
861 cull: Vec<CullInstance>,
863 chains: Vec<Vec<u32>>,
867 meta: Vec<SpriteModelMeta>,
872 colors_alloc: ColorsAllocator,
876 occ_lens: Vec<u32>,
881 coloff_lens: Vec<u32>,
882 occ_used: u32,
887 occ_cap: u32,
888 coloff_used: u32,
891 coloff_cap: u32,
892 meta_cap: u32,
895 dead: Vec<bool>,
901}
902
903#[derive(Clone, Copy)]
906enum ConcatBuf {
907 Occupancy,
908 ColorOffsets,
909}
910
911fn concat_data(m: &SpriteModel, which: ConcatBuf) -> &[u32] {
914 match which {
915 ConcatBuf::Occupancy => &m.occupancy,
916 ConcatBuf::ColorOffsets => &m.color_offsets,
917 }
918}
919
920impl SpriteRegistryResident {
921 #[must_use]
926 pub fn upload(
927 device: &wgpu::Device,
928 registry: &SpriteModelRegistry,
929 instances: &[SpriteInstance],
930 ) -> Self {
931 let entry_lens: Vec<u32> = registry
936 .entries
937 .iter()
938 .map(|m| m.colors.len() as u32)
939 .collect();
940 let colors_alloc = ColorsAllocator::new(&entry_lens);
941 let cap_total = colors_alloc.cap_total();
942
943 let mut all_occ: Vec<u32> = Vec::new();
944 let mut all_offsets: Vec<u32> = Vec::new();
945 let mut all_colors: Vec<u32> = vec![0; cap_total as usize];
946 let mut all_dirs: Vec<u32> = vec![0; cap_total as usize];
947 let mut all_materials: Vec<u32> = vec![0; cap_total as usize];
948 let mut meta: Vec<SpriteModelMeta> = Vec::with_capacity(registry.entries.len());
949 let mut occ_lens: Vec<u32> = Vec::with_capacity(registry.entries.len());
950 let mut coloff_lens: Vec<u32> = Vec::with_capacity(registry.entries.len());
951
952 for (e, m) in registry.entries.iter().enumerate() {
954 let slot = colors_alloc.slot(e);
955 meta.push(SpriteModelMeta {
956 occupancy_offset: all_occ.len() as u32,
957 colors_offset: slot.off,
958 color_offsets_offset: all_offsets.len() as u32,
959 occ_words_per_col: m.occ_words_per_col,
960 dims: m.dims,
961 has_vox_materials: u32::from(!m.materials.is_empty()),
962 pivot: m.pivot,
963 voxel_world_size: m.voxel_world_size,
964 });
965 occ_lens.push(m.occupancy.len() as u32);
966 coloff_lens.push(m.color_offsets.len() as u32);
967 all_occ.extend_from_slice(&m.occupancy);
968 all_offsets.extend_from_slice(&m.color_offsets);
969 let off = slot.off as usize;
970 all_colors[off..off + m.colors.len()].copy_from_slice(&m.colors);
971 all_dirs[off..off + m.dirs.len()].copy_from_slice(&m.dirs);
972 for (i, &mat) in m.materials.iter().enumerate() {
973 all_materials[off + i] = u32::from(mat);
974 }
975 }
976
977 let cull: Vec<CullInstance> = instances.iter().map(|i| make_cull(registry, i)).collect();
982
983 let seed: Vec<SpriteInstanceGpu> = cull.iter().map(|c| c.gpu).collect();
986 let instances_buf = {
987 use wgpu::util::DeviceExt;
988 let one = [SpriteInstanceGpu::zeroed()];
989 let src: &[SpriteInstanceGpu] = if seed.is_empty() { &one } else { &seed };
990 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
991 label: Some("roxlap-gpu sprite_reg.instances"),
992 contents: bytemuck::cast_slice(src),
993 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
994 })
995 };
996
997 let tile_ranges = storage_dst_u32(device, "roxlap-gpu sprite_reg.tile_ranges", 1);
998 let tile_instances = storage_dst_u32(device, "roxlap-gpu sprite_reg.tile_instances", 1);
999 let colmul_cap = (cull.len() as u32).max(1) * 256 * 2;
1002 let colmul = storage_dst_u32(device, "roxlap-gpu sprite_reg.colmul", colmul_cap);
1003 Self {
1004 occupancy: storage_dst_u32_cap(
1005 device,
1006 "roxlap-gpu sprite_reg.occupancy",
1007 &all_occ,
1008 all_occ.len() as u32,
1009 ),
1010 colors: storage_dst_u32_cap(
1011 device,
1012 "roxlap-gpu sprite_reg.colors",
1013 &all_colors,
1014 cap_total,
1015 ),
1016 dirs: storage_dst_u32_cap(device, "roxlap-gpu sprite_reg.dirs", &all_dirs, cap_total),
1017 materials_vox: storage_dst_u32_cap(
1018 device,
1019 "roxlap-gpu sprite_reg.materials_vox",
1020 &all_materials,
1021 cap_total,
1022 ),
1023 color_offsets: storage_dst_u32_cap(
1024 device,
1025 "roxlap-gpu sprite_reg.color_offsets",
1026 &all_offsets,
1027 all_offsets.len() as u32,
1028 ),
1029 model_meta: storage_dst_pod(device, "roxlap-gpu sprite_reg.model_meta", &meta),
1030 instances: instances_buf,
1031 instance_capacity: cull.len() as u32,
1032 colmul,
1033 colmul_cap,
1034 tile_ranges,
1035 tile_ranges_cap: 1,
1036 tile_instances,
1037 tile_instances_cap: 1,
1038 cull,
1039 chains: registry.chains.clone(),
1040 occ_used: all_occ.len() as u32,
1041 occ_cap: all_occ.len() as u32,
1042 coloff_used: all_offsets.len() as u32,
1043 coloff_cap: all_offsets.len() as u32,
1044 meta_cap: meta.len() as u32,
1045 dead: vec![false; meta.len()],
1046 meta,
1047 colors_alloc,
1048 occ_lens,
1049 coloff_lens,
1050 }
1051 }
1052
1053 #[must_use]
1055 pub fn instance_count(&self) -> usize {
1056 self.cull.len()
1057 }
1058
1059 pub fn append_instances(
1081 &mut self,
1082 device: &wgpu::Device,
1083 registry: &SpriteModelRegistry,
1084 instances: &[SpriteInstance],
1085 ) -> u32 {
1086 let base = self.cull.len() as u32;
1087 if instances.is_empty() {
1088 return base;
1089 }
1090 for i in instances {
1091 debug_assert!(
1092 (i.model_id as usize) < self.chains.len(),
1093 "append_instances: model_id {} not resident (run upload to register new models)",
1094 i.model_id
1095 );
1096 self.cull.push(make_cull(registry, i));
1097 }
1098 let need = self.cull.len() as u32;
1099 if need > self.instance_capacity {
1100 self.instance_capacity = need.next_power_of_two();
1104 self.instances = instances_buffer(device, self.instance_capacity);
1105 }
1106 base
1107 }
1108
1109 pub fn remove_instance(&mut self, index: usize) -> Option<usize> {
1120 if index >= self.cull.len() {
1121 return None;
1122 }
1123 let last = self.cull.len() - 1;
1124 self.cull.swap_remove(index);
1125 (index != last).then_some(last)
1126 }
1127
1128 pub fn set_instance_colmul(&mut self, tables: &[[u64; 256]]) {
1134 for (ci, t) in self.cull.iter_mut().zip(tables) {
1135 ci.colmul.copy_from_slice(t);
1136 }
1137 }
1138
1139 pub fn update_transforms(&mut self, instances: &[SpriteInstance]) {
1147 debug_assert_eq!(
1148 instances.len(),
1149 self.cull.len(),
1150 "update_transforms instance count must match upload"
1151 );
1152 for (ci, inst) in self.cull.iter_mut().zip(instances) {
1153 ci.gpu.inv_rot0 = inst.transform.inv_rot[0];
1154 ci.gpu.inv_rot1 = inst.transform.inv_rot[1];
1155 ci.gpu.inv_rot2 = inst.transform.inv_rot[2];
1156 ci.gpu.pos = inst.transform.pos;
1157 ci.gpu.material = u32::from(inst.material);
1160 ci.gpu.alpha_mul = f32::from(inst.alpha_mul) / 255.0;
1161 ci.center = inst.transform.pos;
1163 }
1164 }
1165
1166 pub fn set_instance_model(
1181 &mut self,
1182 registry: &SpriteModelRegistry,
1183 idx: usize,
1184 chain_id: u32,
1185 ) {
1186 let Some(radius) = registry
1190 .model_checked(chain_id)
1191 .map(SpriteModel::bound_radius)
1192 else {
1193 return;
1194 };
1195 let Some(ci) = self.cull.get_mut(idx) else {
1196 return;
1197 };
1198 ci.chain_id = chain_id;
1199 ci.gpu.model_id = chain_id; ci.radius = radius;
1201 }
1202
1203 pub fn update_model(
1223 &mut self,
1224 device: &wgpu::Device,
1225 queue: &wgpu::Queue,
1226 registry: &SpriteModelRegistry,
1227 chain_id: u32,
1228 ) {
1229 let entries = self.chains[chain_id as usize].clone();
1230 let mut grew = false;
1231 for &e in &entries {
1232 let e = e as usize;
1233 let m = ®istry.entries[e];
1234
1235 debug_assert_eq!(
1237 m.occupancy.len() as u32,
1238 self.occ_lens[e],
1239 "update_model: entry {e} occupancy length changed (dims grew?)"
1240 );
1241 debug_assert_eq!(
1242 m.color_offsets.len() as u32,
1243 self.coloff_lens[e],
1244 "update_model: entry {e} color_offsets length changed (dims grew?)"
1245 );
1246 queue.write_buffer(
1247 &self.occupancy,
1248 u64::from(self.meta[e].occupancy_offset) * 4,
1249 bytemuck::cast_slice(&m.occupancy),
1250 );
1251 queue.write_buffer(
1252 &self.color_offsets,
1253 u64::from(self.meta[e].color_offsets_offset) * 4,
1254 bytemuck::cast_slice(&m.color_offsets),
1255 );
1256
1257 let new_len = m.colors.len() as u32;
1259 match self.colors_alloc.place(e, new_len) {
1260 Some(off) => {
1261 queue.write_buffer(
1262 &self.colors,
1263 u64::from(off) * 4,
1264 bytemuck::cast_slice(&m.colors),
1265 );
1266 queue.write_buffer(
1267 &self.dirs,
1268 u64::from(off) * 4,
1269 bytemuck::cast_slice(&m.dirs),
1270 );
1271 let mats: Vec<u32> = m.materials.iter().map(|&x| u32::from(x)).collect();
1272 queue.write_buffer(
1273 &self.materials_vox,
1274 u64::from(off) * 4,
1275 bytemuck::cast_slice(&mats),
1276 );
1277 if self.meta[e].colors_offset != off {
1278 self.meta[e].colors_offset = off;
1280 queue.write_buffer(
1281 &self.model_meta,
1282 (e * std::mem::size_of::<SpriteModelMeta>()) as u64,
1283 bytemuck::bytes_of(&self.meta[e]),
1284 );
1285 }
1286 }
1287 None => grew = true,
1288 }
1289 }
1290
1291 if grew {
1294 self.grow_and_repack(device, queue, registry);
1295 }
1296 }
1297
1298 fn grow_and_repack(
1305 &mut self,
1306 device: &wgpu::Device,
1307 queue: &wgpu::Queue,
1308 registry: &SpriteModelRegistry,
1309 ) {
1310 self.repack_colors_dirs(device, registry);
1311 queue.write_buffer(&self.model_meta, 0, bytemuck::cast_slice(&self.meta));
1313 }
1314
1315 fn repack_colors_dirs(&mut self, device: &wgpu::Device, registry: &SpriteModelRegistry) {
1323 let new_lens: Vec<u32> = registry
1326 .entries
1327 .iter()
1328 .enumerate()
1329 .map(|(e, m)| {
1330 if self.dead[e] {
1331 0
1332 } else {
1333 m.colors.len() as u32
1334 }
1335 })
1336 .collect();
1337 self.colors_alloc.repack(&new_lens);
1338 let cap_total = self.colors_alloc.cap_total();
1339
1340 let mut all_colors = vec![0u32; cap_total as usize];
1341 let mut all_dirs = vec![0u32; cap_total as usize];
1342 let mut all_materials = vec![0u32; cap_total as usize];
1343 for (e, m) in registry.entries.iter().enumerate() {
1344 if self.dead[e] {
1345 self.meta[e].colors_offset = 0;
1346 continue;
1347 }
1348 let off = self.colors_alloc.slot(e).off as usize;
1349 all_colors[off..off + m.colors.len()].copy_from_slice(&m.colors);
1350 all_dirs[off..off + m.dirs.len()].copy_from_slice(&m.dirs);
1351 for (i, &mat) in m.materials.iter().enumerate() {
1352 all_materials[off + i] = u32::from(mat);
1353 }
1354 self.meta[e].colors_offset = off as u32;
1355 }
1356 self.colors = storage_dst_u32_cap(
1357 device,
1358 "roxlap-gpu sprite_reg.colors",
1359 &all_colors,
1360 cap_total,
1361 );
1362 self.dirs = storage_dst_u32_cap(device, "roxlap-gpu sprite_reg.dirs", &all_dirs, cap_total);
1363 self.materials_vox = storage_dst_u32_cap(
1364 device,
1365 "roxlap-gpu sprite_reg.materials_vox",
1366 &all_materials,
1367 cap_total,
1368 );
1369 eprintln!(
1370 "roxlap-gpu: sprite registry colors/dirs/materials grew + repacked to {cap_total} words"
1371 );
1372 }
1373
1374 pub fn add_model(
1392 &mut self,
1393 device: &wgpu::Device,
1394 queue: &wgpu::Queue,
1395 registry: &SpriteModelRegistry,
1396 chain_id: u32,
1397 ) {
1398 let entries = registry.chains[chain_id as usize].clone();
1399 debug_assert_eq!(
1400 chain_id as usize,
1401 self.chains.len(),
1402 "add_model: chains must be appended in order"
1403 );
1404
1405 let mut need_colors_grow = false;
1409 for &e in &entries {
1410 let e = e as usize;
1411 debug_assert_eq!(
1412 e,
1413 self.meta.len(),
1414 "add_model: entries must be appended in order"
1415 );
1416 let m = ®istry.entries[e];
1417 let occ_off = self.occ_used;
1418 let coloff_off = self.coloff_used;
1419 self.occ_used += m.occupancy.len() as u32;
1420 self.coloff_used += m.color_offsets.len() as u32;
1421 let colors_off = match self.colors_alloc.push(m.colors.len() as u32) {
1422 Some(off) => off,
1423 None => {
1424 need_colors_grow = true;
1425 0 }
1427 };
1428 self.meta.push(SpriteModelMeta {
1429 occupancy_offset: occ_off,
1430 colors_offset: colors_off,
1431 color_offsets_offset: coloff_off,
1432 occ_words_per_col: m.occ_words_per_col,
1433 dims: m.dims,
1434 has_vox_materials: u32::from(!m.materials.is_empty()),
1435 pivot: m.pivot,
1436 voxel_world_size: m.voxel_world_size,
1437 });
1438 self.occ_lens.push(m.occupancy.len() as u32);
1439 self.coloff_lens.push(m.color_offsets.len() as u32);
1440 self.dead.push(false);
1441 }
1442 self.chains.push(entries.clone());
1443
1444 self.sync_concat(device, queue, registry, &entries, ConcatBuf::Occupancy);
1447 self.sync_concat(device, queue, registry, &entries, ConcatBuf::ColorOffsets);
1448
1449 if need_colors_grow {
1452 self.repack_colors_dirs(device, registry);
1453 } else {
1454 for &e in &entries {
1455 let e = e as usize;
1456 let m = ®istry.entries[e];
1457 let off = u64::from(self.meta[e].colors_offset) * 4;
1458 queue.write_buffer(&self.colors, off, bytemuck::cast_slice(&m.colors));
1459 queue.write_buffer(&self.dirs, off, bytemuck::cast_slice(&m.dirs));
1460 let mats: Vec<u32> = m.materials.iter().map(|&x| u32::from(x)).collect();
1461 queue.write_buffer(&self.materials_vox, off, bytemuck::cast_slice(&mats));
1462 }
1463 }
1464
1465 let count = self.meta.len() as u32;
1469 if count > self.meta_cap {
1470 self.meta_cap = grow_records(count);
1471 self.model_meta = storage_dst_pod_cap(
1472 device,
1473 "roxlap-gpu sprite_reg.model_meta",
1474 &self.meta,
1475 self.meta_cap,
1476 );
1477 } else {
1478 queue.write_buffer(&self.model_meta, 0, bytemuck::cast_slice(&self.meta));
1479 }
1480 }
1481
1482 fn sync_concat(
1488 &mut self,
1489 device: &wgpu::Device,
1490 queue: &wgpu::Queue,
1491 registry: &SpriteModelRegistry,
1492 new_entries: &[u32],
1493 which: ConcatBuf,
1494 ) {
1495 let (used, cap) = match which {
1496 ConcatBuf::Occupancy => (self.occ_used, self.occ_cap),
1497 ConcatBuf::ColorOffsets => (self.coloff_used, self.coloff_cap),
1498 };
1499 if used > cap {
1500 let new_cap = grow_words(used);
1501 let all: Vec<u32> = registry
1502 .entries
1503 .iter()
1504 .flat_map(|m| concat_data(m, which).iter().copied())
1505 .collect();
1506 let label = match which {
1507 ConcatBuf::Occupancy => "roxlap-gpu sprite_reg.occupancy",
1508 ConcatBuf::ColorOffsets => "roxlap-gpu sprite_reg.color_offsets",
1509 };
1510 let buf = storage_dst_u32_cap(device, label, &all, new_cap);
1511 match which {
1512 ConcatBuf::Occupancy => {
1513 self.occupancy = buf;
1514 self.occ_cap = new_cap;
1515 }
1516 ConcatBuf::ColorOffsets => {
1517 self.color_offsets = buf;
1518 self.coloff_cap = new_cap;
1519 }
1520 }
1521 } else {
1522 let target = match which {
1523 ConcatBuf::Occupancy => &self.occupancy,
1524 ConcatBuf::ColorOffsets => &self.color_offsets,
1525 };
1526 for &e in new_entries {
1527 let e = e as usize;
1528 let off = match which {
1529 ConcatBuf::Occupancy => self.meta[e].occupancy_offset,
1530 ConcatBuf::ColorOffsets => self.meta[e].color_offsets_offset,
1531 };
1532 queue.write_buffer(
1533 target,
1534 u64::from(off) * 4,
1535 bytemuck::cast_slice(concat_data(®istry.entries[e], which)),
1536 );
1537 }
1538 }
1539 }
1540
1541 #[must_use]
1546 pub fn dead_model_count(&self) -> usize {
1547 self.chains.iter().filter(|c| c.is_empty()).count()
1548 }
1549
1550 #[must_use]
1552 pub fn live_model_count(&self) -> usize {
1553 self.chains.iter().filter(|c| !c.is_empty()).count()
1554 }
1555
1556 pub fn remove_model(&mut self, chain_id: u32) {
1569 let Some(entries) = self.chains.get(chain_id as usize).cloned() else {
1570 return;
1571 };
1572 if entries.is_empty() {
1573 return; }
1575 for &e in &entries {
1576 let e = e as usize;
1577 self.dead[e] = true;
1578 self.colors_alloc.free(e);
1579 }
1580 self.chains[chain_id as usize] = Vec::new(); }
1582
1583 pub fn compact(
1593 &mut self,
1594 device: &wgpu::Device,
1595 queue: &wgpu::Queue,
1596 registry: &SpriteModelRegistry,
1597 ) {
1598 self.compact_concat(device, registry, ConcatBuf::Occupancy);
1601 self.compact_concat(device, registry, ConcatBuf::ColorOffsets);
1602 self.repack_colors_dirs(device, registry);
1604 queue.write_buffer(&self.model_meta, 0, bytemuck::cast_slice(&self.meta));
1607 }
1608
1609 fn compact_concat(
1614 &mut self,
1615 device: &wgpu::Device,
1616 registry: &SpriteModelRegistry,
1617 which: ConcatBuf,
1618 ) {
1619 let mut all: Vec<u32> = Vec::new();
1620 for e in 0..self.meta.len() {
1621 if self.dead[e] {
1622 match which {
1623 ConcatBuf::Occupancy => self.meta[e].occupancy_offset = 0,
1624 ConcatBuf::ColorOffsets => self.meta[e].color_offsets_offset = 0,
1625 }
1626 continue;
1627 }
1628 let off = all.len() as u32;
1629 match which {
1630 ConcatBuf::Occupancy => self.meta[e].occupancy_offset = off,
1631 ConcatBuf::ColorOffsets => self.meta[e].color_offsets_offset = off,
1632 }
1633 all.extend_from_slice(concat_data(®istry.entries[e], which));
1634 }
1635 let used = all.len() as u32;
1636 let cap = grow_words(used);
1637 let (label, buf) = match which {
1638 ConcatBuf::Occupancy => ("roxlap-gpu sprite_reg.occupancy", &mut self.occupancy),
1639 ConcatBuf::ColorOffsets => (
1640 "roxlap-gpu sprite_reg.color_offsets",
1641 &mut self.color_offsets,
1642 ),
1643 };
1644 *buf = storage_dst_u32_cap(device, label, &all, cap);
1645 match which {
1646 ConcatBuf::Occupancy => {
1647 self.occ_used = used;
1648 self.occ_cap = cap;
1649 }
1650 ConcatBuf::ColorOffsets => {
1651 self.coloff_used = used;
1652 self.coloff_cap = cap;
1653 }
1654 }
1655 }
1656
1657 #[allow(clippy::too_many_arguments)]
1665 pub fn cull_bin_upload(
1666 &mut self,
1667 device: &wgpu::Device,
1668 queue: &wgpu::Queue,
1669 f: &ViewFrustum,
1670 screen_w: u32,
1671 screen_h: u32,
1672 tile_size: u32,
1673 lod_px: f32,
1674 ) -> (u32, u32, u32) {
1675 let tiles_x = screen_w.div_ceil(tile_size).max(1);
1676 let tiles_y = screen_h.div_ceil(tile_size).max(1);
1677 let n_tiles = (tiles_x * tiles_y) as usize;
1678
1679 let nw = (1.0 + f.half_w * f.half_w).sqrt();
1680 let nh = (1.0 + f.half_h * f.half_h).sqrt();
1681 let cx = screen_w as f32 * 0.5;
1682 let cy = screen_h as f32 * 0.5;
1683 let px_per_world = cx / f.half_w; let ts = tile_size as f32;
1685 let tx_max = tiles_x as i32 - 1;
1686 let ty_max = tiles_y as i32 - 1;
1687
1688 let mut visible: Vec<SpriteInstanceGpu> = Vec::with_capacity(self.cull.len());
1689 let mut boxes: Vec<[i32; 4]> = Vec::with_capacity(self.cull.len());
1691 let mut visible_colmul: Vec<u32> = Vec::with_capacity(self.cull.len() * 512);
1695 let mut counts = vec![0u32; n_tiles];
1696
1697 for ci in &self.cull {
1698 if self.chains[ci.chain_id as usize].is_empty() {
1702 continue;
1703 }
1704 let rel = [
1705 ci.center[0] - f.pos[0],
1706 ci.center[1] - f.pos[1],
1707 ci.center[2] - f.pos[2],
1708 ];
1709 let z = dot3(rel, f.forward);
1710 let r = ci.radius;
1711 if z + r < 0.0 || z - r > f.far {
1712 continue; }
1714 let x = dot3(rel, f.right);
1715 if (x - f.half_w * z) > r * nw || (-x - f.half_w * z) > r * nw {
1716 continue; }
1718 let y = dot3(rel, f.down);
1719 if (y - f.half_h * z) > r * nh || (-y - f.half_h * z) > r * nh {
1720 continue; }
1722
1723 let (tx0, tx1, ty0, ty1) = if z > 1e-3 {
1725 let sx = cx + (x / z) * px_per_world;
1726 let sy = cy + (y / z) * px_per_world;
1727 let sr = (r / z) * px_per_world;
1728 (
1729 (((sx - sr) / ts).floor() as i32).clamp(0, tx_max),
1730 (((sx + sr) / ts).floor() as i32).clamp(0, tx_max),
1731 (((sy - sr) / ts).floor() as i32).clamp(0, ty_max),
1732 (((sy + sr) / ts).floor() as i32).clamp(0, ty_max),
1733 )
1734 } else {
1735 (0, tx_max, 0, ty_max)
1736 };
1737 let chain = &self.chains[ci.chain_id as usize];
1744 let level = if z > 1e-3 && chain.len() > 1 {
1745 let voxel_px = px_per_world / z; ((lod_px / voxel_px).log2().ceil().max(0.0) as usize).min(chain.len() - 1)
1747 } else {
1748 0
1749 };
1750 let mut g = ci.gpu;
1751 g.model_id = chain[level];
1752 visible.push(g);
1753 boxes.push([tx0, tx1, ty0, ty1]);
1754 for &w in ci.colmul.iter() {
1755 visible_colmul.push((w & 0xffff_ffff) as u32);
1756 visible_colmul.push((w >> 32) as u32);
1757 }
1758 for ty in ty0..=ty1 {
1759 for tx in tx0..=tx1 {
1760 counts[(ty * tiles_x as i32 + tx) as usize] += 1;
1761 }
1762 }
1763 }
1764
1765 if visible.is_empty() {
1766 return (0, tiles_x, tiles_y);
1767 }
1768
1769 let mut tile_ranges = vec![0u32; n_tiles * 2];
1772 let mut running = 0u32;
1773 for t in 0..n_tiles {
1774 tile_ranges[2 * t] = running; tile_ranges[2 * t + 1] = counts[t]; running += counts[t];
1777 }
1778 let total = running as usize;
1779 let mut tile_instances = vec![0u32; total.max(1)];
1780 let mut cursor: Vec<u32> = (0..n_tiles).map(|t| tile_ranges[2 * t]).collect();
1781 for (vis_idx, b) in boxes.iter().enumerate() {
1782 for ty in b[2]..=b[3] {
1783 for tx in b[0]..=b[1] {
1784 let t = (ty * tiles_x as i32 + tx) as usize;
1785 tile_instances[cursor[t] as usize] = vis_idx as u32;
1786 cursor[t] += 1;
1787 }
1788 }
1789 }
1790
1791 queue.write_buffer(&self.instances, 0, bytemuck::cast_slice(&visible));
1795 let need_ranges = tile_ranges.len() as u32;
1796 if need_ranges > self.tile_ranges_cap {
1797 self.tile_ranges_cap = need_ranges.next_power_of_two();
1798 self.tile_ranges = storage_dst_u32(
1799 device,
1800 "roxlap-gpu sprite_reg.tile_ranges",
1801 self.tile_ranges_cap,
1802 );
1803 }
1804 let need_inst = tile_instances.len() as u32;
1805 if need_inst > self.tile_instances_cap {
1806 self.tile_instances_cap = need_inst.next_power_of_two();
1807 self.tile_instances = storage_dst_u32(
1808 device,
1809 "roxlap-gpu sprite_reg.tile_instances",
1810 self.tile_instances_cap,
1811 );
1812 }
1813 queue.write_buffer(&self.tile_ranges, 0, bytemuck::cast_slice(&tile_ranges));
1814 queue.write_buffer(
1815 &self.tile_instances,
1816 0,
1817 bytemuck::cast_slice(&tile_instances),
1818 );
1819 let need_colmul = visible_colmul.len() as u32;
1820 if need_colmul > self.colmul_cap {
1821 self.colmul_cap = need_colmul.next_power_of_two();
1822 self.colmul = storage_dst_u32(device, "roxlap-gpu sprite_reg.colmul", self.colmul_cap);
1823 }
1824 queue.write_buffer(&self.colmul, 0, bytemuck::cast_slice(&visible_colmul));
1825
1826 (visible.len() as u32, tiles_x, tiles_y)
1827 }
1828}
1829
1830#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1836struct ColorSlot {
1837 off: u32,
1838 cap: u32,
1839 len: u32,
1840}
1841
1842#[derive(Debug, Default)]
1849struct ColorsAllocator {
1850 slots: Vec<ColorSlot>,
1852 free: Vec<(u32, u32)>,
1854 tail: u32,
1856 cap_total: u32,
1858}
1859
1860fn slot_cap(len: u32) -> u32 {
1863 len + len / 4 + 16
1864}
1865
1866fn grow_words(used: u32) -> u32 {
1870 used + used / 2 + 256
1871}
1872
1873fn grow_records(count: u32) -> u32 {
1875 count + count / 2 + 8
1876}
1877
1878impl ColorsAllocator {
1879 fn new(entry_lens: &[u32]) -> Self {
1883 let mut a = Self::default();
1884 a.repack(entry_lens);
1885 a
1886 }
1887
1888 fn slot(&self, entry: usize) -> ColorSlot {
1889 self.slots[entry]
1890 }
1891
1892 fn cap_total(&self) -> u32 {
1893 self.cap_total
1894 }
1895
1896 fn repack(&mut self, new_lens: &[u32]) {
1900 self.free.clear();
1901 let mut off = 0u32;
1902 let mut slots = Vec::with_capacity(new_lens.len());
1903 for &len in new_lens {
1904 let cap = if len == 0 { 0 } else { slot_cap(len) };
1907 slots.push(ColorSlot { off, cap, len });
1908 off += cap;
1909 }
1910 self.slots = slots;
1911 self.tail = off;
1912 self.cap_total = off + off / 2 + 256;
1914 }
1915
1916 fn place(&mut self, entry: usize, new_len: u32) -> Option<u32> {
1921 let cur = self.slots[entry];
1922 if new_len <= cur.cap {
1923 self.slots[entry] = ColorSlot {
1924 len: new_len,
1925 ..cur
1926 };
1927 return Some(cur.off);
1928 }
1929 let old = (cur.off, cur.cap);
1930 if let Some(i) = self.free.iter().position(|&(_, c)| c >= new_len) {
1932 let (off, cap) = self.free.remove(i);
1933 self.free.push(old);
1934 self.slots[entry] = ColorSlot {
1935 off,
1936 cap,
1937 len: new_len,
1938 };
1939 return Some(off);
1940 }
1941 let want = slot_cap(new_len);
1943 if self.tail + want <= self.cap_total {
1944 let off = self.tail;
1945 self.tail += want;
1946 self.free.push(old);
1947 self.slots[entry] = ColorSlot {
1948 off,
1949 cap: want,
1950 len: new_len,
1951 };
1952 return Some(off);
1953 }
1954 None
1955 }
1956
1957 fn push(&mut self, new_len: u32) -> Option<u32> {
1963 if let Some(i) = self.free.iter().position(|&(_, c)| c >= new_len) {
1964 let (off, cap) = self.free.remove(i);
1965 self.slots.push(ColorSlot {
1966 off,
1967 cap,
1968 len: new_len,
1969 });
1970 return Some(off);
1971 }
1972 let want = slot_cap(new_len);
1973 if self.tail + want <= self.cap_total {
1974 let off = self.tail;
1975 self.tail += want;
1976 self.slots.push(ColorSlot {
1977 off,
1978 cap: want,
1979 len: new_len,
1980 });
1981 return Some(off);
1982 }
1983 None
1984 }
1985
1986 fn free(&mut self, entry: usize) {
1991 let s = self.slots[entry];
1992 if s.cap > 0 {
1993 self.free.push((s.off, s.cap));
1994 }
1995 self.slots[entry] = ColorSlot {
1996 off: 0,
1997 cap: 0,
1998 len: 0,
1999 };
2000 }
2001}
2002
2003#[allow(dead_code)]
2006fn storage_u32(device: &wgpu::Device, label: &str, data: &[u32]) -> wgpu::Buffer {
2007 use wgpu::util::DeviceExt;
2008 let bytes: &[u8] = if data.is_empty() {
2009 bytemuck::cast_slice(&[0u32])
2010 } else {
2011 bytemuck::cast_slice(data)
2012 };
2013 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2014 label: Some(label),
2015 contents: bytes,
2016 usage: wgpu::BufferUsages::STORAGE,
2017 })
2018}
2019
2020fn storage_dst_u32(device: &wgpu::Device, label: &str, cap: u32) -> wgpu::Buffer {
2023 device.create_buffer(&wgpu::BufferDescriptor {
2024 label: Some(label),
2025 size: u64::from(cap.max(1)) * 4,
2026 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
2027 mapped_at_creation: false,
2028 })
2029}
2030
2031fn storage_dst_u32_cap(device: &wgpu::Device, label: &str, data: &[u32], cap: u32) -> wgpu::Buffer {
2039 let cap = cap.max(data.len() as u32).max(1);
2040 let buf = device.create_buffer(&wgpu::BufferDescriptor {
2041 label: Some(label),
2042 size: u64::from(cap) * 4,
2043 usage: wgpu::BufferUsages::STORAGE
2044 | wgpu::BufferUsages::COPY_DST
2045 | wgpu::BufferUsages::COPY_SRC,
2046 mapped_at_creation: true,
2047 });
2048 if !data.is_empty() {
2049 buf.slice(..(data.len() as u64 * 4))
2050 .get_mapped_range_mut()
2051 .copy_from_slice(bytemuck::cast_slice(data));
2052 }
2053 buf.unmap();
2054 buf
2055}
2056
2057fn storage_dst_pod<T: Pod + Zeroable>(
2063 device: &wgpu::Device,
2064 label: &str,
2065 data: &[T],
2066) -> wgpu::Buffer {
2067 let one = [T::zeroed()];
2068 let src: &[T] = if data.is_empty() { &one } else { data };
2069 let buf = device.create_buffer(&wgpu::BufferDescriptor {
2070 label: Some(label),
2071 size: std::mem::size_of_val(src) as u64,
2072 usage: wgpu::BufferUsages::STORAGE
2073 | wgpu::BufferUsages::COPY_DST
2074 | wgpu::BufferUsages::COPY_SRC,
2075 mapped_at_creation: true,
2076 });
2077 buf.slice(..)
2078 .get_mapped_range_mut()
2079 .copy_from_slice(bytemuck::cast_slice(src));
2080 buf.unmap();
2081 buf
2082}
2083
2084fn storage_dst_pod_cap<T: Pod + Zeroable>(
2089 device: &wgpu::Device,
2090 label: &str,
2091 data: &[T],
2092 cap: u32,
2093) -> wgpu::Buffer {
2094 let rec = std::mem::size_of::<T>() as u64;
2095 let cap = u64::from(cap.max(data.len() as u32).max(1));
2096 let buf = device.create_buffer(&wgpu::BufferDescriptor {
2097 label: Some(label),
2098 size: cap * rec,
2099 usage: wgpu::BufferUsages::STORAGE
2100 | wgpu::BufferUsages::COPY_DST
2101 | wgpu::BufferUsages::COPY_SRC,
2102 mapped_at_creation: true,
2103 });
2104 if !data.is_empty() {
2105 buf.slice(..(data.len() as u64 * rec))
2106 .get_mapped_range_mut()
2107 .copy_from_slice(bytemuck::cast_slice(data));
2108 }
2109 buf.unmap();
2110 buf
2111}
2112
2113#[allow(dead_code)]
2116fn storage_pod<T: Pod + Zeroable>(device: &wgpu::Device, label: &str, data: &[T]) -> wgpu::Buffer {
2117 use wgpu::util::DeviceExt;
2118 let one = [T::zeroed()];
2119 let src: &[T] = if data.is_empty() { &one } else { data };
2120 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2121 label: Some(label),
2122 contents: bytemuck::cast_slice(src),
2123 usage: wgpu::BufferUsages::STORAGE,
2124 })
2125}
2126
2127#[cfg(test)]
2128mod tests {
2129 use super::*;
2130 use roxlap_formats::kv6::{Kv6, Voxel};
2131
2132 fn kv6_unsorted() -> Kv6 {
2135 let mk = |z, col| Voxel {
2136 col,
2137 z,
2138 vis: 0,
2139 dir: 0,
2140 };
2141 Kv6 {
2142 xsiz: 2,
2143 ysiz: 1,
2144 zsiz: 8,
2145 xpiv: 0.0,
2146 ypiv: 0.0,
2147 zpiv: 0.0,
2148 voxels: vec![mk(5, 0xAA), mk(1, 0xBB), mk(3, 0xCC)],
2149 xlen: vec![2, 1],
2150 ylen: vec![vec![2], vec![1]],
2151 palette: None,
2152 }
2153 }
2154
2155 #[test]
2156 fn occupancy_bits_set_at_voxel_z() {
2157 let m = build_sprite_model(&kv6_unsorted());
2158 assert_eq!(m.dims, [2, 1, 8]);
2159 assert_eq!(m.occ_words_per_col, 1); assert_eq!(m.occupancy[0], (1 << 1) | (1 << 5));
2162 assert_eq!(m.occupancy[1], 1 << 3);
2163 }
2164
2165 #[test]
2166 fn colors_are_ascending_z_for_rank_lookup() {
2167 let m = build_sprite_model(&kv6_unsorted());
2168 assert_eq!(m.color_offsets, vec![0, 2, 3]);
2170 assert_eq!(&m.colors, &[0xBB, 0xAA, 0xCC]);
2171 }
2172
2173 #[test]
2174 fn identity_basis_inverts_to_identity() {
2175 let inv = mat3_inverse([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
2176 assert_eq!(inv, [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
2177 }
2178
2179 #[test]
2180 fn fork_is_independent_of_parent() {
2181 let mut reg = SpriteModelRegistry::new();
2182 let base = reg.add(build_sprite_model(&kv6_unsorted()));
2183 let forked = reg.fork(base);
2184 assert_ne!(base, forked);
2185 reg.model_mut(forked).recolor(|_| 0x11);
2187 assert_eq!(®.model(base).colors, &[0xBB, 0xAA, 0xCC]);
2189 assert_eq!(®.model(forked).colors, &[0x11, 0x11, 0x11]);
2190 }
2191
2192 #[test]
2193 fn remove_frees_chain_data_keeps_ids_stable() {
2194 let mut reg = SpriteModelRegistry::new();
2195 let a = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2196 let b = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2197 let len_before = reg.len();
2198 assert!(reg.is_live(a) && reg.is_live(b));
2199
2200 reg.remove(a);
2201 assert!(!reg.is_live(a));
2204 assert!(reg.is_live(b));
2206 assert_eq!(®.model(b).colors, &[0xBB, 0xAA, 0xCC]);
2207 assert_eq!(reg.len(), len_before);
2208
2209 let c = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2211 assert_eq!(c, len_before as u32);
2212 assert!(reg.is_live(c));
2213 assert_eq!(®.model(b).colors, &[0xBB, 0xAA, 0xCC]);
2215 }
2216
2217 #[test]
2218 fn model_checked_guards_out_of_range_and_tombstoned() {
2219 let mut reg = SpriteModelRegistry::new();
2222 let a = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2223 assert!(reg.model_checked(a).is_some());
2224 assert!(reg.model_checked(9999).is_none(), "out of range → None");
2225 reg.remove(a);
2226 assert!(reg.model_checked(a).is_none(), "tombstoned chain → None");
2227 }
2228
2229 #[test]
2230 fn remove_is_idempotent_and_bounds_safe() {
2231 let mut reg = SpriteModelRegistry::new();
2232 let a = reg.add(build_sprite_model(&kv6_unsorted()));
2233 reg.remove(a);
2234 reg.remove(a); reg.remove(999); assert!(!reg.is_live(a));
2237 assert!(!reg.is_live(999));
2238 }
2239
2240 #[test]
2241 fn registry_gpu_structs_have_expected_sizes() {
2242 assert_eq!(std::mem::size_of::<SpriteModelMeta>(), 48);
2243 assert_eq!(std::mem::size_of::<SpriteInstanceGpu>(), 80);
2246 }
2247
2248 #[test]
2249 fn add_lod_builds_halving_mip_chain() {
2250 let mut reg = SpriteModelRegistry::new();
2251 let id = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2254 let m0 = reg.model(id);
2255 assert_eq!(m0.dims, [2, 1, 8]);
2256 assert!((m0.voxel_world_size - 1.0).abs() < 1e-6);
2257 }
2258
2259 fn kv6_from(xsiz: u32, ysiz: u32, zsiz: u32, voxels: &[(u32, u32, u16, u32)]) -> Kv6 {
2262 let mut ylen = vec![vec![0u16; ysiz as usize]; xsiz as usize];
2263 let mut flat = Vec::new();
2264 for x in 0..xsiz {
2265 for y in 0..ysiz {
2266 let mut col: Vec<(u16, u32)> = voxels
2267 .iter()
2268 .filter(|(vx, vy, _, _)| *vx == x && *vy == y)
2269 .map(|(_, _, z, c)| (*z, *c))
2270 .collect();
2271 col.sort_by_key(|(z, _)| *z);
2272 ylen[x as usize][y as usize] = col.len() as u16;
2273 for (z, c) in col {
2274 flat.push(Voxel {
2275 col: c,
2276 z,
2277 vis: 0,
2278 dir: 0,
2279 });
2280 }
2281 }
2282 }
2283 let xlen = ylen
2284 .iter()
2285 .map(|c| c.iter().map(|&v| u32::from(v)).sum())
2286 .collect();
2287 Kv6 {
2288 xsiz,
2289 ysiz,
2290 zsiz,
2291 xpiv: 0.0,
2292 ypiv: 0.0,
2293 zpiv: 0.0,
2294 voxels: flat,
2295 xlen,
2296 ylen,
2297 palette: None,
2298 }
2299 }
2300
2301 fn offsets_consistent(m: &SpriteModel) -> bool {
2302 let cols = (m.dims[0] * m.dims[1]) as usize;
2303 if m.color_offsets.len() != cols + 1 {
2304 return false;
2305 }
2306 for w in m.color_offsets.windows(2) {
2309 if w[1] < w[0] {
2310 return false;
2311 }
2312 }
2313 m.color_offsets[cols] as usize == m.colors.len()
2314 }
2315
2316 #[test]
2317 fn carve_two_layers_keeps_offsets_consistent() {
2318 let kv6 = kv6_from(
2321 3,
2322 2,
2323 8,
2324 &[
2325 (0, 0, 0, 0xA0),
2326 (0, 0, 1, 0xA1),
2327 (0, 0, 5, 0xA5),
2328 (1, 0, 1, 0xB1),
2329 (2, 1, 0, 0xC0),
2330 (2, 1, 3, 0xC3),
2331 ],
2332 );
2333 let mut m = build_sprite_model(&kv6);
2334 assert!(offsets_consistent(&m));
2335 for z in 0..2u32 {
2336 for y in 0..m.dims[1] {
2337 for x in 0..m.dims[0] {
2338 m.set_voxel(x, y, z, None);
2339 }
2340 }
2341 assert!(offsets_consistent(&m), "inconsistent after carving z={z}");
2342 let _ = m.downsample();
2344 }
2345 }
2346
2347 #[test]
2348 fn set_voxel_inserts_replaces_and_clears() {
2349 let mut m = build_sprite_model(&kv6_unsorted());
2351
2352 assert!(m.set_voxel(0, 0, 3, Some(0x55)));
2354 assert_eq!(m.occupancy[0], (1 << 1) | (1 << 3) | (1 << 5));
2355 assert_eq!(m.color_offsets, vec![0, 3, 4]);
2357 assert_eq!(&m.colors, &[0xBB, 0x55, 0xAA, 0xCC]);
2358
2359 assert!(m.set_voxel(0, 0, 3, Some(0x66)));
2361 assert_eq!(&m.colors, &[0xBB, 0x66, 0xAA, 0xCC]);
2362 assert_eq!(m.color_offsets, vec![0, 3, 4]);
2363
2364 assert!(m.set_voxel(0, 0, 1, None));
2366 assert_eq!(m.occupancy[0], (1 << 3) | (1 << 5));
2367 assert_eq!(m.color_offsets, vec![0, 2, 3]);
2368 assert_eq!(&m.colors, &[0x66, 0xAA, 0xCC]);
2369
2370 assert!(!m.set_voxel(0, 0, 2, None));
2372 assert!(!m.set_voxel(9, 0, 0, Some(1)));
2373 }
2374
2375 #[test]
2376 fn rebuild_lod_refreshes_coarse_levels_from_mip0() {
2377 let mut reg = SpriteModelRegistry::new();
2378 let id = reg.add_lod(build_sprite_model(&kv6_unsorted()), 3);
2379 reg.model_mut(id).recolor(|_| 0x0000_2000);
2381 reg.rebuild_lod(id);
2382 let lvl1_entry = reg.chains[id as usize][1] as usize;
2384 assert!(reg.entries[lvl1_entry]
2385 .colors
2386 .iter()
2387 .all(|&c| c == 0x0000_2000));
2388 }
2389
2390 fn alloc_invariants(a: &ColorsAllocator, lens: &[u32]) {
2395 let mut prev_end = 0u32;
2396 for (e, &len) in lens.iter().enumerate() {
2397 let s = a.slot(e);
2398 assert_eq!(s.len, len, "slot {e} len");
2399 assert!(s.cap >= s.len, "slot {e} cap >= len");
2400 assert!(s.off >= prev_end, "slot {e} overlaps previous");
2402 assert!(s.off + s.cap <= a.cap_total(), "slot {e} past cap_total");
2403 prev_end = s.off + s.cap;
2404 }
2405 assert!(a.cap_total() >= prev_end, "tail headroom");
2406 }
2407
2408 #[test]
2409 fn allocator_new_lays_out_with_slack_and_headroom() {
2410 let lens = [10u32, 0, 64, 7];
2411 let a = ColorsAllocator::new(&lens);
2412 alloc_invariants(&a, &lens);
2413 assert!(a.slot(2).cap > 64);
2415 assert!(a.cap_total() > a.slot(3).off + a.slot(3).cap);
2417 }
2418
2419 #[test]
2420 fn allocator_place_in_place_when_within_cap() {
2421 let mut a = ColorsAllocator::new(&[10, 20]);
2422 let off0 = a.slot(0).off;
2423 let cap0 = a.slot(0).cap;
2424 assert_eq!(a.place(0, 5), Some(off0));
2426 assert_eq!(a.slot(0).len, 5);
2427 assert_eq!(a.slot(0).cap, cap0);
2428 assert_eq!(a.place(0, cap0), Some(off0));
2430 assert_eq!(a.slot(0).off, off0);
2431 assert!(a.free.is_empty(), "no relocation should free anything");
2432 }
2433
2434 #[test]
2435 fn allocator_place_relocates_to_tail_and_frees_old() {
2436 let mut a = ColorsAllocator::new(&[10, 20]);
2437 let old0 = (a.slot(0).off, a.slot(0).cap);
2438 let tail_before = a.tail;
2439 let new_len = a.slot(0).cap + 5;
2441 let off = a.place(0, new_len).expect("fits in headroom");
2442 assert_eq!(off, tail_before, "relocated to old tail");
2443 assert_eq!(a.slot(0).off, off);
2444 assert_eq!(a.slot(0).len, new_len);
2445 assert!(a.free.contains(&old0), "old slot freed");
2446 }
2447
2448 #[test]
2449 fn allocator_reuses_freed_block_first_fit() {
2450 let mut a = ColorsAllocator::new(&[10, 2]);
2453 let old0 = (a.slot(0).off, a.slot(0).cap);
2454 let _ = a.place(0, a.slot(0).cap + 5).unwrap();
2456 assert!(a.free.contains(&old0));
2457 let new1 = a.slot(1).cap + 1;
2460 assert!(new1 <= old0.1, "freed block big enough");
2461 let off = a.place(1, new1).expect("reuses freed block");
2462 assert_eq!(off, old0.0, "first-fit reused the freed slot offset");
2463 assert!(!a.free.contains(&old0), "freed block consumed");
2464 }
2465
2466 #[test]
2467 fn allocator_signals_grow_then_repack_restores() {
2468 let mut a = ColorsAllocator::new(&[8, 8]);
2469 let huge = a.cap_total() + 100;
2471 assert_eq!(a.place(0, huge), None, "overflow must signal grow");
2472 a.repack(&[huge, 8]);
2474 alloc_invariants(&a, &[huge, 8]);
2475 assert!(a.cap_total() > huge);
2476 assert_eq!(a.place(0, huge), Some(a.slot(0).off));
2478 }
2479
2480 #[test]
2487 fn allocator_carve_loop_keeps_live_windows_disjoint() {
2488 let mut a = ColorsAllocator::new(&[40, 12, 40]);
2489 let mut lens = [40u32, 12, 40];
2490 let walk = [13u32, 30, 60, 18, 9, 80, 80, 25, 200, 7];
2493 let mut grew = false;
2494 for &len in &walk {
2495 lens[1] = len;
2496 if a.place(1, len).is_none() {
2498 grew = true;
2499 a.repack(&lens);
2500 } else {
2501 assert_eq!(a.place(0, 40), Some(a.slot(0).off));
2503 assert_eq!(a.place(2, 40), Some(a.slot(2).off));
2504 }
2505 assert_eq!(a.slot(1).len, len);
2506
2507 let mut wins: Vec<(u32, u32)> =
2509 (0..3).map(|e| (a.slot(e).off, a.slot(e).len)).collect();
2510 wins.sort_by_key(|w| w.0);
2511 for pair in wins.windows(2) {
2512 let (o0, l0) = pair[0];
2513 let (o1, _) = pair[1];
2514 assert!(o0 + l0 <= o1, "live windows overlap: {pair:?}");
2515 }
2516 }
2517 assert!(grew, "the 200-word jump should have forced a repack");
2518 }
2519
2520 fn headless() -> Option<crate::HeadlessGpu> {
2523 match crate::HeadlessGpu::new_blocking(crate::GpuRendererSettings::default()) {
2524 Ok(h) => Some(h),
2525 Err(e) => {
2526 eprintln!("[skip] no GPU adapter reachable: {e}");
2527 None
2528 }
2529 }
2530 }
2531
2532 fn one_model_registry() -> (SpriteModelRegistry, u32) {
2533 let mut reg = SpriteModelRegistry::new();
2534 let id = reg.add(build_sprite_model(&kv6_unsorted()));
2535 (reg, id)
2536 }
2537
2538 fn inst(model_id: u32, pos: [f32; 3]) -> SpriteInstance {
2539 use roxlap_formats::sprite::Sprite;
2540 SpriteInstance::new(
2541 model_id,
2542 SpriteInstanceTransform::from_sprite(&Sprite::axis_aligned(kv6_unsorted(), pos)),
2543 )
2544 }
2545
2546 #[test]
2547 fn append_grows_count_and_capacity_pow2() {
2548 let Some(h) = headless() else { return };
2549 let (reg, m) = one_model_registry();
2550 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(m, [0.0; 3])]);
2551 assert_eq!(res.instance_count(), 1);
2552 assert_eq!(res.instance_capacity, 1);
2553
2554 let more: Vec<_> = (1..=4).map(|i| inst(m, [i as f32, 0.0, 0.0])).collect();
2556 let base = res.append_instances(&h.device, ®, &more);
2557 assert_eq!(base, 1, "first appended index follows the seed instance");
2558 assert_eq!(res.instance_count(), 5);
2559 assert_eq!(res.instance_capacity, 8, "power-of-two growth");
2560
2561 let base2 = res.append_instances(&h.device, ®, &[inst(m, [9.0, 0.0, 0.0])]);
2563 assert_eq!(base2, 5);
2564 assert_eq!(res.instance_count(), 6);
2565 assert_eq!(res.instance_capacity, 8, "fits existing capacity, no grow");
2566 }
2567
2568 #[test]
2569 fn append_empty_is_noop() {
2570 let Some(h) = headless() else { return };
2571 let (reg, m) = one_model_registry();
2572 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(m, [0.0; 3])]);
2573 let base = res.append_instances(&h.device, ®, &[]);
2574 assert_eq!(base, 1);
2575 assert_eq!(res.instance_count(), 1);
2576 assert_eq!(res.instance_capacity, 1);
2577 }
2578
2579 fn read_u32(h: &crate::HeadlessGpu, buf: &wgpu::Buffer, words: u64) -> Vec<u32> {
2581 let bytes = words * 4;
2582 let staging = h.device.create_buffer(&wgpu::BufferDescriptor {
2583 label: Some("readback"),
2584 size: bytes,
2585 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2586 mapped_at_creation: false,
2587 });
2588 let mut enc = h
2589 .device
2590 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
2591 enc.copy_buffer_to_buffer(buf, 0, &staging, 0, bytes);
2592 h.queue.submit(std::iter::once(enc.finish()));
2593 let slice = staging.slice(..);
2594 let (tx, rx) = std::sync::mpsc::channel();
2595 slice.map_async(wgpu::MapMode::Read, move |r| tx.send(r).unwrap());
2596 h.device.poll(wgpu::PollType::wait_indefinitely()).ok();
2597 rx.recv().unwrap().unwrap();
2598 let data = slice.get_mapped_range();
2599 let out = bytemuck::cast_slice::<u8, u32>(&data).to_vec();
2600 drop(data);
2601 staging.unmap();
2602 out
2603 }
2604
2605 fn kv6_other() -> Kv6 {
2608 let mk = |z, col| Voxel {
2609 col,
2610 z,
2611 vis: 0,
2612 dir: 0,
2613 };
2614 Kv6 {
2615 xsiz: 1,
2616 ysiz: 1,
2617 zsiz: 4,
2618 xpiv: 0.0,
2619 ypiv: 0.0,
2620 zpiv: 0.0,
2621 voxels: vec![mk(0, 0x11), mk(2, 0x22)],
2622 xlen: vec![2],
2623 ylen: vec![vec![2]],
2624 palette: None,
2625 }
2626 }
2627
2628 #[test]
2632 fn add_model_uploads_new_volume_incrementally() {
2633 let Some(h) = headless() else { return };
2634
2635 let mut reg = SpriteModelRegistry::new();
2637 let a = reg.add(build_sprite_model(&kv6_unsorted()));
2638 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(a, [0.0; 3])]);
2639 assert_eq!(res.chains.len(), 1);
2640 let entries_before = res.meta.len();
2641
2642 let b = reg.add(build_sprite_model(&kv6_other()));
2644 res.add_model(&h.device, &h.queue, ®, b);
2645 assert_eq!(res.chains.len(), 2);
2646 assert_eq!(res.meta.len(), entries_before + 1, "one new entry");
2647
2648 let occ = read_u32(&h, &res.occupancy, u64::from(res.occ_cap));
2652 let coloff = read_u32(&h, &res.color_offsets, u64::from(res.coloff_cap));
2653 let cols = read_u32(&h, &res.colors, u64::from(res.colors_alloc.cap_total()));
2654 for (e, m) in reg.entries.iter().enumerate() {
2655 let meta = res.meta[e];
2656 let oo = meta.occupancy_offset as usize;
2657 assert_eq!(
2658 &occ[oo..oo + m.occupancy.len()],
2659 &m.occupancy[..],
2660 "occ entry {e}"
2661 );
2662 let co = meta.color_offsets_offset as usize;
2663 assert_eq!(
2664 &coloff[co..co + m.color_offsets.len()],
2665 &m.color_offsets[..],
2666 "color_offsets entry {e}"
2667 );
2668 let cc = meta.colors_offset as usize;
2669 assert_eq!(
2670 &cols[cc..cc + m.colors.len()],
2671 &m.colors[..],
2672 "colors entry {e}"
2673 );
2674 }
2675
2676 let base = res.append_instances(&h.device, ®, &[inst(b, [5.0, 0.0, 0.0])]);
2678 assert_eq!(base, 1);
2679 assert_eq!(res.instance_count(), 2);
2680 }
2681
2682 #[test]
2686 fn add_model_survives_buffer_growth() {
2687 let Some(h) = headless() else { return };
2688 let mut reg = SpriteModelRegistry::new();
2689 let a = reg.add(build_sprite_model(&kv6_unsorted()));
2690 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(a, [0.0; 3])]);
2691 let occ_cap0 = res.occ_cap;
2692
2693 for _ in 0..40 {
2696 let id = reg.add(build_sprite_model(&kv6_other()));
2697 res.add_model(&h.device, &h.queue, ®, id);
2698 }
2699 assert_eq!(res.chains.len(), 41);
2700 assert!(res.occ_cap > occ_cap0, "occupancy buffer grew");
2701
2702 let occ = read_u32(&h, &res.occupancy, u64::from(res.occ_cap));
2703 let cols = read_u32(&h, &res.colors, u64::from(res.colors_alloc.cap_total()));
2704 for (e, m) in reg.entries.iter().enumerate() {
2705 let meta = res.meta[e];
2706 let oo = meta.occupancy_offset as usize;
2707 assert_eq!(
2708 &occ[oo..oo + m.occupancy.len()],
2709 &m.occupancy[..],
2710 "occ entry {e}"
2711 );
2712 let cc = meta.colors_offset as usize;
2713 assert_eq!(
2714 &cols[cc..cc + m.colors.len()],
2715 &m.colors[..],
2716 "colors entry {e}"
2717 );
2718 }
2719 }
2720
2721 #[test]
2728 fn voxel_clip_flipbook_set_instance_model() {
2729 use roxlap_formats::voxel_clip::{LoopMode, VoxelClip, VoxelFrame};
2730 let Some(h) = headless() else { return };
2731
2732 let dims = [1u32, 1, 4];
2735 let owpc = dims[2].div_ceil(32).max(1) as usize; let mk_frame = |zs: &[u32], cols: &[u32]| -> VoxelFrame {
2737 let mut occ = vec![0u32; owpc];
2738 for &z in zs {
2739 occ[(z >> 5) as usize] |= 1u32 << (z & 31);
2740 }
2741 VoxelFrame {
2742 occupancy: occ,
2743 colors: cols.to_vec(),
2744 color_offsets: vec![0, cols.len() as u32],
2745 }
2746 };
2747 let f0 = mk_frame(&[0], &[0x8011_2233]);
2748 let f1 = mk_frame(&[0, 1], &[0x8011_2233, 0x80AA_BBCC]);
2749 let clip = VoxelClip::from_frames(
2750 dims,
2751 [0.5, 0.5, 2.0],
2752 1.0,
2753 LoopMode::Loop,
2754 &[f0, f1],
2755 &[],
2756 33,
2757 0,
2758 );
2759 let decoded = clip.decode().expect("decode");
2760
2761 let mut reg = SpriteModelRegistry::new();
2763 let c0 = reg.add(sprite_model_from_clip_frame(&decoded, 0));
2764 let c1 = reg.add(sprite_model_from_clip_frame(&decoded, 1));
2765 assert_eq!(reg.model(c0).colors.len(), 1);
2766 assert_eq!(reg.model(c1).colors.len(), 2);
2767
2768 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(c0, [0.0, 0.0, 5.0])]);
2770 assert_eq!(res.cull[0].chain_id, c0);
2771
2772 res.set_instance_model(®, 0, c1);
2774 assert_eq!(res.cull[0].chain_id, c1);
2775 assert_eq!(res.cull[0].radius, reg.model(c1).bound_radius());
2776
2777 let f = test_frustum();
2780 let (visible, _, _) = res.cull_bin_upload(&h.device, &h.queue, &f, 64, 64, 16, 1.0);
2781 assert_eq!(visible, 1);
2782
2783 res.set_instance_model(®, 0, c0);
2785 assert_eq!(res.cull[0].chain_id, c0);
2786
2787 res.set_instance_model(®, 99, c1);
2789 assert_eq!(res.cull[0].chain_id, c0);
2790 }
2791
2792 fn test_frustum() -> ViewFrustum {
2793 ViewFrustum {
2794 pos: [0.0, 0.0, 0.0],
2795 right: [1.0, 0.0, 0.0],
2796 down: [0.0, 1.0, 0.0],
2797 forward: [0.0, 0.0, 1.0],
2798 half_w: 1.0,
2799 half_h: 1.0,
2800 far: 10_000.0,
2801 }
2802 }
2803
2804 #[test]
2805 fn remove_model_tombstones_frees_and_reuses() {
2806 let Some(h) = headless() else { return };
2807 let mut reg = SpriteModelRegistry::new();
2809 let a = reg.add(build_sprite_model(&kv6_unsorted()));
2810 let b = reg.add(build_sprite_model(&kv6_other()));
2811 let mut res = SpriteRegistryResident::upload(
2812 &h.device,
2813 ®,
2814 &[inst(a, [0.0; 3]), inst(b, [1.0, 0.0, 0.0])],
2815 );
2816 assert_eq!(res.live_model_count(), 2);
2817 assert_eq!(res.dead_model_count(), 0);
2818
2819 res.remove_model(b);
2821 assert_eq!(res.live_model_count(), 1);
2822 assert_eq!(res.dead_model_count(), 1);
2823 assert_eq!(res.dead.iter().filter(|&&d| d).count(), 1, "one entry dead");
2824 assert!(!res.colors_alloc.free.is_empty(), "B's colour slot freed");
2825
2826 let c = reg.add(build_sprite_model(&kv6_other()));
2828 res.add_model(&h.device, &h.queue, ®, c);
2829 assert_eq!(res.live_model_count(), 2);
2830
2831 let cols = read_u32(&h, &res.colors, u64::from(res.colors_alloc.cap_total()));
2833 for e in [a as usize, c as usize] {
2834 let m = ®.entries[e];
2835 let cc = res.meta[e].colors_offset as usize;
2836 assert_eq!(
2837 &cols[cc..cc + m.colors.len()],
2838 &m.colors[..],
2839 "colors entry {e}"
2840 );
2841 }
2842
2843 let f = test_frustum();
2845 let _ = res.cull_bin_upload(&h.device, &h.queue, &f, 64, 64, 16, 1.0);
2846 }
2847
2848 #[test]
2849 fn compact_reclaims_holes_keeps_ids_stable() {
2850 let Some(h) = headless() else { return };
2851 let mut reg = SpriteModelRegistry::new();
2852 let a = reg.add(build_sprite_model(&kv6_unsorted()));
2853 let b = reg.add(build_sprite_model(&kv6_other()));
2854 let c = reg.add(build_sprite_model(&kv6_other()));
2855 let mut res = SpriteRegistryResident::upload(
2856 &h.device,
2857 ®,
2858 &[inst(a, [0.0; 3]), inst(b, [1.0; 3]), inst(c, [2.0; 3])],
2859 );
2860 let occ_used_full = res.occ_used;
2861
2862 res.remove_model(b);
2864 res.compact(&h.device, &h.queue, ®);
2865
2866 let live_occ: u32 = [a, c]
2868 .iter()
2869 .map(|&e| reg.entries[e as usize].occupancy.len() as u32)
2870 .sum();
2871 assert_eq!(res.occ_used, live_occ);
2872 assert!(res.occ_used < occ_used_full, "compaction shrank occupancy");
2873 assert_eq!(res.meta[b as usize].occupancy_offset, 0);
2875 assert_eq!(res.live_model_count(), 2);
2876 assert_eq!(res.dead_model_count(), 1);
2877
2878 let occ = read_u32(&h, &res.occupancy, u64::from(res.occ_cap));
2880 let cols = read_u32(&h, &res.colors, u64::from(res.colors_alloc.cap_total()));
2881 for &e in &[a as usize, c as usize] {
2882 let m = ®.entries[e];
2883 let oo = res.meta[e].occupancy_offset as usize;
2884 assert_eq!(
2885 &occ[oo..oo + m.occupancy.len()],
2886 &m.occupancy[..],
2887 "occ {e}"
2888 );
2889 let cc = res.meta[e].colors_offset as usize;
2890 assert_eq!(&cols[cc..cc + m.colors.len()], &m.colors[..], "cols {e}");
2891 }
2892
2893 assert!(!res.chains[c as usize].is_empty());
2895 assert!(res.chains[b as usize].is_empty());
2896 }
2897
2898 #[test]
2899 fn remove_swap_semantics_and_capacity_retained() {
2900 let Some(h) = headless() else { return };
2901 let (reg, m) = one_model_registry();
2902 let seed: Vec<_> = (0..4).map(|i| inst(m, [i as f32, 0.0, 0.0])).collect();
2903 let mut res = SpriteRegistryResident::upload(&h.device, ®, &seed);
2904 assert_eq!(res.instance_count(), 4);
2905 let cap = res.instance_capacity;
2906
2907 assert_eq!(res.remove_instance(1), Some(3));
2909 assert_eq!(res.instance_count(), 3);
2910
2911 assert_eq!(res.remove_instance(2), None);
2913 assert_eq!(res.instance_count(), 2);
2914
2915 assert_eq!(res.remove_instance(99), None);
2917 assert_eq!(res.instance_count(), 2);
2918
2919 assert_eq!(res.instance_capacity, cap);
2921 }
2922}