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 sprite_model_from_voxel_frame_with_materials(frame, dirs, dims, pivot, voxel_world_size, &[])
175}
176
177#[must_use]
185pub fn sprite_model_from_voxel_frame_with_materials(
186 frame: &VoxelFrame,
187 dirs: &[u32],
188 dims: [u32; 3],
189 pivot: [f32; 3],
190 voxel_world_size: f32,
191 material_map: &[(u32, u8)],
192) -> SpriteModel {
193 let occ_words_per_col = dims[2].div_ceil(32).max(1);
194 let cols = (dims[0] * dims[1]) as usize;
195 debug_assert_eq!(frame.occupancy.len(), cols * occ_words_per_col as usize);
196 debug_assert_eq!(frame.color_offsets.len(), cols + 1);
197 debug_assert_eq!(dirs.len(), frame.colors.len());
198 let materials: Vec<u8> = if material_map.is_empty() {
201 Vec::new()
202 } else {
203 frame
204 .colors
205 .iter()
206 .map(|&c| material_for_color(material_map, c))
207 .collect()
208 };
209 SpriteModel {
210 dims,
211 occ_words_per_col,
212 pivot,
213 occupancy: frame.occupancy.clone(),
214 colors: frame.colors.clone(),
215 dirs: dirs.to_vec(),
216 color_offsets: frame.color_offsets.clone(),
217 materials,
218 voxel_world_size,
219 }
220}
221
222#[must_use]
228pub fn sprite_model_from_clip_frame(clip: &DecodedClip, frame: usize) -> SpriteModel {
229 sprite_model_from_clip_frame_with_materials(clip, frame, &[])
230}
231
232#[must_use]
239pub fn sprite_model_from_clip_frame_with_materials(
240 clip: &DecodedClip,
241 frame: usize,
242 material_map: &[(u32, u8)],
243) -> SpriteModel {
244 sprite_model_from_voxel_frame_with_materials(
245 &clip.frames[frame],
246 &clip.dirs[frame],
247 clip.dims,
248 clip.pivot,
249 clip.voxel_world_size,
250 material_map,
251 )
252}
253
254#[repr(C)]
259#[derive(Clone, Copy, Pod, Zeroable, Debug)]
260pub struct SpriteInstanceTransform {
261 pub inv_rot: [[f32; 4]; 3],
264 pub pos: [f32; 3],
266 _pad: f32,
267}
268
269impl SpriteInstanceTransform {
270 #[must_use]
273 pub fn from_sprite(sprite: &Sprite) -> Self {
274 let inv = mat3_inverse([sprite.s, sprite.h, sprite.f]);
275 Self {
276 inv_rot: [
277 [inv[0][0], inv[0][1], inv[0][2], 0.0],
278 [inv[1][0], inv[1][1], inv[1][2], 0.0],
279 [inv[2][0], inv[2][1], inv[2][2], 0.0],
280 ],
281 pos: sprite.p,
282 _pad: 0.0,
283 }
284 }
285}
286
287#[derive(Debug, Clone, Default)]
295pub struct SpriteModelRegistry {
296 entries: Vec<SpriteModel>,
298 chains: Vec<Vec<u32>>,
300}
301
302impl SpriteModelRegistry {
303 #[must_use]
304 pub fn new() -> Self {
305 Self::default()
306 }
307
308 fn push_entry(&mut self, model: SpriteModel) -> u32 {
309 let id = self.entries.len() as u32;
310 self.entries.push(model);
311 id
312 }
313
314 pub fn add(&mut self, model: SpriteModel) -> u32 {
316 let e = self.push_entry(model);
317 let id = self.chains.len() as u32;
318 self.chains.push(vec![e]);
319 id
320 }
321
322 pub fn add_lod(&mut self, model: SpriteModel, max_levels: u32) -> u32 {
326 let mut levels = vec![self.push_entry(model.clone())];
327 let mut cur = model;
328 for _ in 1..max_levels.max(1) {
329 if cur.dims == [1, 1, 1] {
330 break;
331 }
332 cur = cur.downsample();
333 levels.push(self.push_entry(cur.clone()));
334 }
335 let id = self.chains.len() as u32;
336 self.chains.push(levels);
337 id
338 }
339
340 pub fn fork(&mut self, parent: u32) -> u32 {
348 let src = self.chains[parent as usize].clone();
349 let levels: Vec<u32> = src
350 .iter()
351 .map(|&e| {
352 let copy = self.entries[e as usize].clone();
353 self.push_entry(copy)
354 })
355 .collect();
356 let id = self.chains.len() as u32;
357 self.chains.push(levels);
358 id
359 }
360
361 #[must_use]
363 pub fn model(&self, id: u32) -> &SpriteModel {
364 &self.entries[self.chains[id as usize][0] as usize]
365 }
366
367 #[must_use]
371 pub fn model_checked(&self, id: u32) -> Option<&SpriteModel> {
372 let entry = *self.chains.get(id as usize)?.first()?;
373 self.entries.get(entry as usize)
374 }
375
376 pub fn model_mut(&mut self, id: u32) -> &mut SpriteModel {
382 let e = self.chains[id as usize][0] as usize;
383 &mut self.entries[e]
384 }
385
386 pub fn recolor_chain(&mut self, id: u32, f: impl Fn(u32) -> u32 + Copy) {
389 for li in 0..self.chains[id as usize].len() {
390 let e = self.chains[id as usize][li] as usize;
391 self.entries[e].recolor(f);
392 }
393 }
394
395 pub fn rebuild_lod(&mut self, id: u32) {
400 let levels = self.chains[id as usize].clone();
401 if levels.len() <= 1 {
402 return;
403 }
404 let mut cur = self.entries[levels[0] as usize].clone();
405 for &e in &levels[1..] {
406 cur = cur.downsample();
407 self.entries[e as usize] = cur.clone();
408 }
409 }
410
411 pub fn remove(&mut self, chain_id: u32) {
425 let Some(entries) = self.chains.get(chain_id as usize) else {
426 return;
427 };
428 let entries = entries.clone();
430 for e in entries {
431 self.entries[e as usize] = SpriteModel::empty();
432 }
433 self.chains[chain_id as usize] = Vec::new(); }
435
436 #[must_use]
439 pub fn is_live(&self, chain_id: u32) -> bool {
440 self.chains
441 .get(chain_id as usize)
442 .is_some_and(|c| !c.is_empty())
443 }
444
445 #[must_use]
449 pub fn len(&self) -> usize {
450 self.chains.len()
451 }
452
453 #[must_use]
454 pub fn is_empty(&self) -> bool {
455 self.chains.is_empty()
456 }
457}
458
459impl SpriteModel {
460 #[must_use]
467 pub fn empty() -> Self {
468 Self {
469 dims: [0, 0, 0],
470 occ_words_per_col: 1,
471 pivot: [0.0, 0.0, 0.0],
472 occupancy: Vec::new(),
473 colors: Vec::new(),
474 dirs: Vec::new(),
475 color_offsets: vec![0],
476 materials: Vec::new(),
477 voxel_world_size: 1.0,
478 }
479 }
480
481 pub fn recolor(&mut self, f: impl Fn(u32) -> u32) {
487 for c in &mut self.colors {
488 *c = f(*c);
489 }
490 }
491
492 pub fn set_voxel(&mut self, x: u32, y: u32, z: u32, color: Option<u32>) -> bool {
503 if x >= self.dims[0] || y >= self.dims[1] || z >= self.dims[2] {
504 return false;
505 }
506 let owpc = self.occ_words_per_col as usize;
507 let cols = (self.dims[0] * self.dims[1]) as usize;
508 let col = (x + y * self.dims[0]) as usize;
509 let base = col * owpc;
510 let zw = (z >> 5) as usize;
511 let zb = z & 31;
512
513 let mut rank = 0usize;
515 for w in 0..zw {
516 rank += self.occupancy[base + w].count_ones() as usize;
517 }
518 let below_mask = if zb > 0 { (1u32 << zb) - 1 } else { 0 };
519 rank += (self.occupancy[base + zw] & below_mask).count_ones() as usize;
520 let idx = self.color_offsets[col] as usize + rank;
521 let was_set = (self.occupancy[base + zw] >> zb) & 1 == 1;
522
523 if let Some(rgba) = color {
524 if was_set {
525 self.colors[idx] = rgba; } else {
527 self.occupancy[base + zw] |= 1u32 << zb;
528 self.colors.insert(idx, rgba);
529 self.dirs.insert(idx, 0);
532 if !self.materials.is_empty() {
533 self.materials.insert(idx, 0); }
535 for c in &mut self.color_offsets[col + 1..=cols] {
536 *c += 1;
537 }
538 }
539 true
540 } else {
541 if !was_set {
542 return false;
543 }
544 self.occupancy[base + zw] &= !(1u32 << zb);
545 self.colors.remove(idx);
546 self.dirs.remove(idx);
547 if !self.materials.is_empty() {
548 self.materials.remove(idx);
549 }
550 for c in &mut self.color_offsets[col + 1..=cols] {
551 *c -= 1;
552 }
553 true
554 }
555 }
556
557 #[must_use]
562 pub fn bound_radius(&self) -> f32 {
563 let mut r2 = 0.0_f32;
564 for &cx in &[0.0, self.dims[0] as f32] {
565 for &cy in &[0.0, self.dims[1] as f32] {
566 for &cz in &[0.0, self.dims[2] as f32] {
567 let d = [cx - self.pivot[0], cy - self.pivot[1], cz - self.pivot[2]];
568 r2 = r2.max(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
569 }
570 }
571 }
572 r2.sqrt()
573 }
574
575 #[must_use]
581 #[allow(clippy::manual_checked_ops)] pub fn downsample(&self) -> SpriteModel {
583 let [fx, fy, fz] = self.dims;
584 let fidx = |x: u32, y: u32, z: u32| (x + y * fx + z * fx * fy) as usize;
585
586 let has_mats = !self.materials.is_empty();
589 let mut solid = vec![false; (fx * fy * fz) as usize];
590 let mut fine = vec![0u32; (fx * fy * fz) as usize];
591 let mut fine_dir = vec![0u32; (fx * fy * fz) as usize];
592 let mut fine_mat = vec![0u8; (fx * fy * fz) as usize];
593 for x in 0..fx {
594 for y in 0..fy {
595 let col = (x + y * fx) as usize;
596 let base = col * self.occ_words_per_col as usize;
597 let off = self.color_offsets[col] as usize;
598 let mut seen = 0usize;
599 for z in 0..fz {
600 let w = base + (z >> 5) as usize;
601 if (self.occupancy[w] >> (z & 31)) & 1 == 1 {
602 fine[fidx(x, y, z)] = self.colors[off + seen];
603 fine_dir[fidx(x, y, z)] = self.dirs[off + seen];
604 if has_mats {
605 fine_mat[fidx(x, y, z)] = self.materials[off + seen];
606 }
607 solid[fidx(x, y, z)] = true;
608 seen += 1;
609 }
610 }
611 }
612 }
613
614 let nx = fx.div_ceil(2).max(1);
615 let ny = fy.div_ceil(2).max(1);
616 let nz = fz.div_ceil(2).max(1);
617 let owpc = nz.div_ceil(32).max(1);
618 let cols = (nx * ny) as usize;
619 let mut occupancy = vec![0u32; cols * owpc as usize];
620 let mut color_offsets = vec![0u32; cols + 1];
621 let mut colors: Vec<u32> = Vec::new();
622 let mut dirs: Vec<u32> = Vec::new();
623 let mut materials: Vec<u8> = Vec::new();
624
625 for cy in 0..ny {
628 for cx in 0..nx {
629 let ccol = (cx + cy * nx) as usize;
630 color_offsets[ccol] = colors.len() as u32;
631 for cz in 0..nz {
632 let (mut a, mut r, mut g, mut b, mut n) = (0u32, 0u32, 0u32, 0u32, 0u32);
633 let mut rep_dir = 0u32;
637 let mut rep_mat = 0u8;
638 for dz in 0..2 {
639 for dy in 0..2 {
640 for dx in 0..2 {
641 let (x, y, z) = (2 * cx + dx, 2 * cy + dy, 2 * cz + dz);
642 if x < fx && y < fy && z < fz && solid[fidx(x, y, z)] {
643 let c = fine[fidx(x, y, z)];
644 if n == 0 {
645 rep_dir = fine_dir[fidx(x, y, z)];
646 rep_mat = fine_mat[fidx(x, y, z)];
647 }
648 a += (c >> 24) & 0xff;
649 r += (c >> 16) & 0xff;
650 g += (c >> 8) & 0xff;
651 b += c & 0xff;
652 n += 1;
653 }
654 }
655 }
656 }
657 if n > 0 {
658 let avg = ((a / n) << 24) | ((r / n) << 16) | ((g / n) << 8) | (b / n);
659 let base = ccol * owpc as usize + (cz >> 5) as usize;
660 occupancy[base] |= 1u32 << (cz & 31);
661 colors.push(avg);
662 dirs.push(rep_dir);
663 if has_mats {
664 materials.push(rep_mat);
665 }
666 }
667 }
668 }
669 }
670 color_offsets[cols] = colors.len() as u32;
671
672 SpriteModel {
673 dims: [nx, ny, nz],
674 occ_words_per_col: owpc,
675 pivot: [
676 self.pivot[0] * 0.5,
677 self.pivot[1] * 0.5,
678 self.pivot[2] * 0.5,
679 ],
680 occupancy,
681 colors,
682 dirs,
683 color_offsets,
684 materials,
685 voxel_world_size: self.voxel_world_size * 2.0,
686 }
687 }
688}
689
690#[derive(Clone, Copy, Debug)]
695pub struct ViewFrustum {
696 pub pos: [f32; 3],
697 pub right: [f32; 3],
698 pub down: [f32; 3],
699 pub forward: [f32; 3],
700 pub half_w: f32,
701 pub half_h: f32,
702 pub far: f32,
703}
704
705#[derive(Clone)]
708struct CullInstance {
709 gpu: SpriteInstanceGpu,
712 chain_id: u32,
714 center: [f32; 3],
715 radius: f32,
716 colmul: Box<[u64; 256]>,
722}
723
724fn identity_colmul() -> Box<[u64; 256]> {
727 const LANE: u64 = 0x0100;
728 let w = LANE | (LANE << 16) | (LANE << 32) | (LANE << 48);
729 Box::new([w; 256])
730}
731
732fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
733 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
734}
735
736fn make_cull(registry: &SpriteModelRegistry, i: &SpriteInstance) -> CullInstance {
742 CullInstance {
743 gpu: SpriteInstanceGpu {
744 inv_rot0: i.transform.inv_rot[0],
745 inv_rot1: i.transform.inv_rot[1],
746 inv_rot2: i.transform.inv_rot[2],
747 pos: i.transform.pos,
748 model_id: i.model_id, material: u32::from(i.material),
750 alpha_mul: f32::from(i.alpha_mul) / 255.0,
751 flags: i.flags,
752 tint: i.tint,
753 },
754 chain_id: i.model_id,
755 center: i.transform.pos,
756 radius: registry.model(i.model_id).bound_radius(),
757 colmul: identity_colmul(),
758 }
759}
760
761fn instances_buffer(device: &wgpu::Device, cap: u32) -> wgpu::Buffer {
766 device.create_buffer(&wgpu::BufferDescriptor {
767 label: Some("roxlap-gpu sprite_reg.instances"),
768 size: u64::from(cap.max(1)) * std::mem::size_of::<SpriteInstanceGpu>() as u64,
769 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
770 mapped_at_creation: false,
771 })
772}
773
774#[derive(Debug, Clone, Copy)]
776pub struct SpriteInstance {
777 pub model_id: u32,
778 pub transform: SpriteInstanceTransform,
779 pub material: u8,
783 pub alpha_mul: u8,
786 pub flags: u32,
790 pub tint: u32,
792}
793
794impl SpriteInstance {
795 #[must_use]
799 pub fn new(model_id: u32, transform: SpriteInstanceTransform) -> Self {
800 Self {
801 model_id,
802 transform,
803 material: 0,
804 alpha_mul: 255,
805 flags: 0,
806 tint: 0x00FF_FFFF,
807 }
808 }
809}
810
811#[repr(C)]
815#[derive(Clone, Copy, Pod, Zeroable, Debug)]
816struct SpriteModelMeta {
817 occupancy_offset: u32,
818 colors_offset: u32,
819 color_offsets_offset: u32,
820 occ_words_per_col: u32,
821 dims: [u32; 3],
822 has_vox_materials: u32,
825 pivot: [f32; 3],
826 voxel_world_size: f32,
828}
829
830#[repr(C)]
834#[derive(Clone, Copy, Pod, Zeroable, Debug)]
835struct SpriteInstanceGpu {
836 inv_rot0: [f32; 4],
837 inv_rot1: [f32; 4],
838 inv_rot2: [f32; 4],
839 pos: [f32; 3],
840 model_id: u32,
841 material: u32,
843 alpha_mul: f32,
845 flags: u32,
848 tint: u32,
850}
851
852#[must_use]
856fn mat3_inverse(cols: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
857 let [a, b, c] = cols; let cross = |u: [f32; 3], v: [f32; 3]| {
860 [
861 u[1] * v[2] - u[2] * v[1],
862 u[2] * v[0] - u[0] * v[2],
863 u[0] * v[1] - u[1] * v[0],
864 ]
865 };
866 let bc = cross(b, c);
867 let ca = cross(c, a);
868 let ab = cross(a, b);
869 let det = a[0] * bc[0] + a[1] * bc[1] + a[2] * bc[2];
870 let inv_det = if det.abs() < 1e-12 { 0.0 } else { 1.0 / det };
871 [
874 [bc[0] * inv_det, ca[0] * inv_det, ab[0] * inv_det],
875 [bc[1] * inv_det, ca[1] * inv_det, ab[1] * inv_det],
876 [bc[2] * inv_det, ca[2] * inv_det, ab[2] * inv_det],
877 ]
878}
879
880pub struct SpriteRegistryResident {
887 pub occupancy: wgpu::Buffer,
888 pub colors: wgpu::Buffer,
889 pub dirs: wgpu::Buffer,
893 pub materials_vox: wgpu::Buffer,
898 pub color_offsets: wgpu::Buffer,
899 pub model_meta: wgpu::Buffer,
900 pub instances: wgpu::Buffer,
903 pub instance_capacity: u32,
904 pub colmul: wgpu::Buffer,
909 colmul_cap: u32,
910 pub tile_ranges: wgpu::Buffer,
913 tile_ranges_cap: u32,
914 pub tile_instances: wgpu::Buffer,
917 tile_instances_cap: u32,
918 cull: Vec<CullInstance>,
920 chains: Vec<Vec<u32>>,
924 meta: Vec<SpriteModelMeta>,
929 colors_alloc: ColorsAllocator,
933 occ_lens: Vec<u32>,
938 coloff_lens: Vec<u32>,
939 occ_used: u32,
944 occ_cap: u32,
945 coloff_used: u32,
948 coloff_cap: u32,
949 meta_cap: u32,
952 dead: Vec<bool>,
958}
959
960#[derive(Clone, Copy)]
963enum ConcatBuf {
964 Occupancy,
965 ColorOffsets,
966}
967
968fn concat_data(m: &SpriteModel, which: ConcatBuf) -> &[u32] {
971 match which {
972 ConcatBuf::Occupancy => &m.occupancy,
973 ConcatBuf::ColorOffsets => &m.color_offsets,
974 }
975}
976
977impl SpriteRegistryResident {
978 #[must_use]
983 pub fn upload(
984 device: &wgpu::Device,
985 registry: &SpriteModelRegistry,
986 instances: &[SpriteInstance],
987 ) -> Self {
988 let entry_lens: Vec<u32> = registry
993 .entries
994 .iter()
995 .map(|m| m.colors.len() as u32)
996 .collect();
997 let colors_alloc = ColorsAllocator::new(&entry_lens);
998 let cap_total = colors_alloc.cap_total();
999
1000 let mut all_occ: Vec<u32> = Vec::new();
1001 let mut all_offsets: Vec<u32> = Vec::new();
1002 let mut all_colors: Vec<u32> = vec![0; cap_total as usize];
1003 let mut all_dirs: Vec<u32> = vec![0; cap_total as usize];
1004 let mut all_materials: Vec<u32> = vec![0; cap_total as usize];
1005 let mut meta: Vec<SpriteModelMeta> = Vec::with_capacity(registry.entries.len());
1006 let mut occ_lens: Vec<u32> = Vec::with_capacity(registry.entries.len());
1007 let mut coloff_lens: Vec<u32> = Vec::with_capacity(registry.entries.len());
1008
1009 for (e, m) in registry.entries.iter().enumerate() {
1011 let slot = colors_alloc.slot(e);
1012 meta.push(SpriteModelMeta {
1013 occupancy_offset: all_occ.len() as u32,
1014 colors_offset: slot.off,
1015 color_offsets_offset: all_offsets.len() as u32,
1016 occ_words_per_col: m.occ_words_per_col,
1017 dims: m.dims,
1018 has_vox_materials: u32::from(!m.materials.is_empty()),
1019 pivot: m.pivot,
1020 voxel_world_size: m.voxel_world_size,
1021 });
1022 occ_lens.push(m.occupancy.len() as u32);
1023 coloff_lens.push(m.color_offsets.len() as u32);
1024 all_occ.extend_from_slice(&m.occupancy);
1025 all_offsets.extend_from_slice(&m.color_offsets);
1026 let off = slot.off as usize;
1027 all_colors[off..off + m.colors.len()].copy_from_slice(&m.colors);
1028 all_dirs[off..off + m.dirs.len()].copy_from_slice(&m.dirs);
1029 for (i, &mat) in m.materials.iter().enumerate() {
1030 all_materials[off + i] = u32::from(mat);
1031 }
1032 }
1033
1034 let cull: Vec<CullInstance> = instances.iter().map(|i| make_cull(registry, i)).collect();
1039
1040 let seed: Vec<SpriteInstanceGpu> = cull.iter().map(|c| c.gpu).collect();
1043 let instances_buf = {
1044 use wgpu::util::DeviceExt;
1045 let one = [SpriteInstanceGpu::zeroed()];
1046 let src: &[SpriteInstanceGpu] = if seed.is_empty() { &one } else { &seed };
1047 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1048 label: Some("roxlap-gpu sprite_reg.instances"),
1049 contents: bytemuck::cast_slice(src),
1050 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1051 })
1052 };
1053
1054 let tile_ranges = storage_dst_u32(device, "roxlap-gpu sprite_reg.tile_ranges", 1);
1055 let tile_instances = storage_dst_u32(device, "roxlap-gpu sprite_reg.tile_instances", 1);
1056 let colmul_cap = (cull.len() as u32).max(1) * 256 * 2;
1059 let colmul = storage_dst_u32(device, "roxlap-gpu sprite_reg.colmul", colmul_cap);
1060 Self {
1061 occupancy: storage_dst_u32_cap(
1062 device,
1063 "roxlap-gpu sprite_reg.occupancy",
1064 &all_occ,
1065 all_occ.len() as u32,
1066 ),
1067 colors: storage_dst_u32_cap(
1068 device,
1069 "roxlap-gpu sprite_reg.colors",
1070 &all_colors,
1071 cap_total,
1072 ),
1073 dirs: storage_dst_u32_cap(device, "roxlap-gpu sprite_reg.dirs", &all_dirs, cap_total),
1074 materials_vox: storage_dst_u32_cap(
1075 device,
1076 "roxlap-gpu sprite_reg.materials_vox",
1077 &all_materials,
1078 cap_total,
1079 ),
1080 color_offsets: storage_dst_u32_cap(
1081 device,
1082 "roxlap-gpu sprite_reg.color_offsets",
1083 &all_offsets,
1084 all_offsets.len() as u32,
1085 ),
1086 model_meta: storage_dst_pod(device, "roxlap-gpu sprite_reg.model_meta", &meta),
1087 instances: instances_buf,
1088 instance_capacity: cull.len() as u32,
1089 colmul,
1090 colmul_cap,
1091 tile_ranges,
1092 tile_ranges_cap: 1,
1093 tile_instances,
1094 tile_instances_cap: 1,
1095 cull,
1096 chains: registry.chains.clone(),
1097 occ_used: all_occ.len() as u32,
1098 occ_cap: all_occ.len() as u32,
1099 coloff_used: all_offsets.len() as u32,
1100 coloff_cap: all_offsets.len() as u32,
1101 meta_cap: meta.len() as u32,
1102 dead: vec![false; meta.len()],
1103 meta,
1104 colors_alloc,
1105 occ_lens,
1106 coloff_lens,
1107 }
1108 }
1109
1110 #[must_use]
1112 pub fn instance_count(&self) -> usize {
1113 self.cull.len()
1114 }
1115
1116 pub fn append_instances(
1138 &mut self,
1139 device: &wgpu::Device,
1140 registry: &SpriteModelRegistry,
1141 instances: &[SpriteInstance],
1142 ) -> u32 {
1143 let base = self.cull.len() as u32;
1144 if instances.is_empty() {
1145 return base;
1146 }
1147 for i in instances {
1148 debug_assert!(
1149 (i.model_id as usize) < self.chains.len(),
1150 "append_instances: model_id {} not resident (run upload to register new models)",
1151 i.model_id
1152 );
1153 self.cull.push(make_cull(registry, i));
1154 }
1155 let need = self.cull.len() as u32;
1156 if need > self.instance_capacity {
1157 self.instance_capacity = need.next_power_of_two();
1161 self.instances = instances_buffer(device, self.instance_capacity);
1162 }
1163 base
1164 }
1165
1166 pub fn remove_instance(&mut self, index: usize) -> Option<usize> {
1177 if index >= self.cull.len() {
1178 return None;
1179 }
1180 let last = self.cull.len() - 1;
1181 self.cull.swap_remove(index);
1182 (index != last).then_some(last)
1183 }
1184
1185 pub fn set_instance_colmul(&mut self, tables: &[[u64; 256]]) {
1191 for (ci, t) in self.cull.iter_mut().zip(tables) {
1192 ci.colmul.copy_from_slice(t);
1193 }
1194 }
1195
1196 pub fn update_transforms(&mut self, instances: &[SpriteInstance]) {
1204 debug_assert_eq!(
1205 instances.len(),
1206 self.cull.len(),
1207 "update_transforms instance count must match upload"
1208 );
1209 for (ci, inst) in self.cull.iter_mut().zip(instances) {
1210 ci.gpu.inv_rot0 = inst.transform.inv_rot[0];
1211 ci.gpu.inv_rot1 = inst.transform.inv_rot[1];
1212 ci.gpu.inv_rot2 = inst.transform.inv_rot[2];
1213 ci.gpu.pos = inst.transform.pos;
1214 ci.gpu.material = u32::from(inst.material);
1217 ci.gpu.alpha_mul = f32::from(inst.alpha_mul) / 255.0;
1218 ci.gpu.flags = inst.flags;
1221 ci.gpu.tint = inst.tint;
1222 ci.center = inst.transform.pos;
1224 }
1225 }
1226
1227 pub fn set_instance_model(
1242 &mut self,
1243 registry: &SpriteModelRegistry,
1244 idx: usize,
1245 chain_id: u32,
1246 ) {
1247 let Some(radius) = registry
1251 .model_checked(chain_id)
1252 .map(SpriteModel::bound_radius)
1253 else {
1254 return;
1255 };
1256 let Some(ci) = self.cull.get_mut(idx) else {
1257 return;
1258 };
1259 ci.chain_id = chain_id;
1260 ci.gpu.model_id = chain_id; ci.radius = radius;
1262 }
1263
1264 pub fn update_model(
1284 &mut self,
1285 device: &wgpu::Device,
1286 queue: &wgpu::Queue,
1287 registry: &SpriteModelRegistry,
1288 chain_id: u32,
1289 ) {
1290 let entries = self.chains[chain_id as usize].clone();
1291 let mut grew = false;
1292 for &e in &entries {
1293 let e = e as usize;
1294 let m = ®istry.entries[e];
1295
1296 debug_assert_eq!(
1298 m.occupancy.len() as u32,
1299 self.occ_lens[e],
1300 "update_model: entry {e} occupancy length changed (dims grew?)"
1301 );
1302 debug_assert_eq!(
1303 m.color_offsets.len() as u32,
1304 self.coloff_lens[e],
1305 "update_model: entry {e} color_offsets length changed (dims grew?)"
1306 );
1307 queue.write_buffer(
1308 &self.occupancy,
1309 u64::from(self.meta[e].occupancy_offset) * 4,
1310 bytemuck::cast_slice(&m.occupancy),
1311 );
1312 queue.write_buffer(
1313 &self.color_offsets,
1314 u64::from(self.meta[e].color_offsets_offset) * 4,
1315 bytemuck::cast_slice(&m.color_offsets),
1316 );
1317
1318 let new_len = m.colors.len() as u32;
1320 match self.colors_alloc.place(e, new_len) {
1321 Some(off) => {
1322 queue.write_buffer(
1323 &self.colors,
1324 u64::from(off) * 4,
1325 bytemuck::cast_slice(&m.colors),
1326 );
1327 queue.write_buffer(
1328 &self.dirs,
1329 u64::from(off) * 4,
1330 bytemuck::cast_slice(&m.dirs),
1331 );
1332 let mats: Vec<u32> = m.materials.iter().map(|&x| u32::from(x)).collect();
1333 queue.write_buffer(
1334 &self.materials_vox,
1335 u64::from(off) * 4,
1336 bytemuck::cast_slice(&mats),
1337 );
1338 if self.meta[e].colors_offset != off {
1339 self.meta[e].colors_offset = off;
1341 queue.write_buffer(
1342 &self.model_meta,
1343 (e * std::mem::size_of::<SpriteModelMeta>()) as u64,
1344 bytemuck::bytes_of(&self.meta[e]),
1345 );
1346 }
1347 }
1348 None => grew = true,
1349 }
1350 }
1351
1352 if grew {
1355 self.grow_and_repack(device, queue, registry);
1356 }
1357 }
1358
1359 fn grow_and_repack(
1366 &mut self,
1367 device: &wgpu::Device,
1368 queue: &wgpu::Queue,
1369 registry: &SpriteModelRegistry,
1370 ) {
1371 self.repack_colors_dirs(device, registry);
1372 queue.write_buffer(&self.model_meta, 0, bytemuck::cast_slice(&self.meta));
1374 }
1375
1376 fn repack_colors_dirs(&mut self, device: &wgpu::Device, registry: &SpriteModelRegistry) {
1384 let new_lens: Vec<u32> = registry
1387 .entries
1388 .iter()
1389 .enumerate()
1390 .map(|(e, m)| {
1391 if self.dead[e] {
1392 0
1393 } else {
1394 m.colors.len() as u32
1395 }
1396 })
1397 .collect();
1398 self.colors_alloc.repack(&new_lens);
1399 let cap_total = self.colors_alloc.cap_total();
1400
1401 let mut all_colors = vec![0u32; cap_total as usize];
1402 let mut all_dirs = vec![0u32; cap_total as usize];
1403 let mut all_materials = vec![0u32; cap_total as usize];
1404 for (e, m) in registry.entries.iter().enumerate() {
1405 if self.dead[e] {
1406 self.meta[e].colors_offset = 0;
1407 continue;
1408 }
1409 let off = self.colors_alloc.slot(e).off as usize;
1410 all_colors[off..off + m.colors.len()].copy_from_slice(&m.colors);
1411 all_dirs[off..off + m.dirs.len()].copy_from_slice(&m.dirs);
1412 for (i, &mat) in m.materials.iter().enumerate() {
1413 all_materials[off + i] = u32::from(mat);
1414 }
1415 self.meta[e].colors_offset = off as u32;
1416 }
1417 self.colors = storage_dst_u32_cap(
1418 device,
1419 "roxlap-gpu sprite_reg.colors",
1420 &all_colors,
1421 cap_total,
1422 );
1423 self.dirs = storage_dst_u32_cap(device, "roxlap-gpu sprite_reg.dirs", &all_dirs, cap_total);
1424 self.materials_vox = storage_dst_u32_cap(
1425 device,
1426 "roxlap-gpu sprite_reg.materials_vox",
1427 &all_materials,
1428 cap_total,
1429 );
1430 eprintln!(
1431 "roxlap-gpu: sprite registry colors/dirs/materials grew + repacked to {cap_total} words"
1432 );
1433 }
1434
1435 pub fn add_model(
1453 &mut self,
1454 device: &wgpu::Device,
1455 queue: &wgpu::Queue,
1456 registry: &SpriteModelRegistry,
1457 chain_id: u32,
1458 ) {
1459 let entries = registry.chains[chain_id as usize].clone();
1460 debug_assert_eq!(
1461 chain_id as usize,
1462 self.chains.len(),
1463 "add_model: chains must be appended in order"
1464 );
1465
1466 let mut need_colors_grow = false;
1470 for &e in &entries {
1471 let e = e as usize;
1472 debug_assert_eq!(
1473 e,
1474 self.meta.len(),
1475 "add_model: entries must be appended in order"
1476 );
1477 let m = ®istry.entries[e];
1478 let occ_off = self.occ_used;
1479 let coloff_off = self.coloff_used;
1480 self.occ_used += m.occupancy.len() as u32;
1481 self.coloff_used += m.color_offsets.len() as u32;
1482 let colors_off = match self.colors_alloc.push(m.colors.len() as u32) {
1483 Some(off) => off,
1484 None => {
1485 need_colors_grow = true;
1486 0 }
1488 };
1489 self.meta.push(SpriteModelMeta {
1490 occupancy_offset: occ_off,
1491 colors_offset: colors_off,
1492 color_offsets_offset: coloff_off,
1493 occ_words_per_col: m.occ_words_per_col,
1494 dims: m.dims,
1495 has_vox_materials: u32::from(!m.materials.is_empty()),
1496 pivot: m.pivot,
1497 voxel_world_size: m.voxel_world_size,
1498 });
1499 self.occ_lens.push(m.occupancy.len() as u32);
1500 self.coloff_lens.push(m.color_offsets.len() as u32);
1501 self.dead.push(false);
1502 }
1503 self.chains.push(entries.clone());
1504
1505 self.sync_concat(device, queue, registry, &entries, ConcatBuf::Occupancy);
1508 self.sync_concat(device, queue, registry, &entries, ConcatBuf::ColorOffsets);
1509
1510 if need_colors_grow {
1513 self.repack_colors_dirs(device, registry);
1514 } else {
1515 for &e in &entries {
1516 let e = e as usize;
1517 let m = ®istry.entries[e];
1518 let off = u64::from(self.meta[e].colors_offset) * 4;
1519 queue.write_buffer(&self.colors, off, bytemuck::cast_slice(&m.colors));
1520 queue.write_buffer(&self.dirs, off, bytemuck::cast_slice(&m.dirs));
1521 let mats: Vec<u32> = m.materials.iter().map(|&x| u32::from(x)).collect();
1522 queue.write_buffer(&self.materials_vox, off, bytemuck::cast_slice(&mats));
1523 }
1524 }
1525
1526 let count = self.meta.len() as u32;
1530 if count > self.meta_cap {
1531 self.meta_cap = grow_records(count);
1532 self.model_meta = storage_dst_pod_cap(
1533 device,
1534 "roxlap-gpu sprite_reg.model_meta",
1535 &self.meta,
1536 self.meta_cap,
1537 );
1538 } else {
1539 queue.write_buffer(&self.model_meta, 0, bytemuck::cast_slice(&self.meta));
1540 }
1541 }
1542
1543 fn sync_concat(
1549 &mut self,
1550 device: &wgpu::Device,
1551 queue: &wgpu::Queue,
1552 registry: &SpriteModelRegistry,
1553 new_entries: &[u32],
1554 which: ConcatBuf,
1555 ) {
1556 let (used, cap) = match which {
1557 ConcatBuf::Occupancy => (self.occ_used, self.occ_cap),
1558 ConcatBuf::ColorOffsets => (self.coloff_used, self.coloff_cap),
1559 };
1560 if used > cap {
1561 let new_cap = grow_words(used);
1562 let all: Vec<u32> = registry
1563 .entries
1564 .iter()
1565 .flat_map(|m| concat_data(m, which).iter().copied())
1566 .collect();
1567 let label = match which {
1568 ConcatBuf::Occupancy => "roxlap-gpu sprite_reg.occupancy",
1569 ConcatBuf::ColorOffsets => "roxlap-gpu sprite_reg.color_offsets",
1570 };
1571 let buf = storage_dst_u32_cap(device, label, &all, new_cap);
1572 match which {
1573 ConcatBuf::Occupancy => {
1574 self.occupancy = buf;
1575 self.occ_cap = new_cap;
1576 }
1577 ConcatBuf::ColorOffsets => {
1578 self.color_offsets = buf;
1579 self.coloff_cap = new_cap;
1580 }
1581 }
1582 } else {
1583 let target = match which {
1584 ConcatBuf::Occupancy => &self.occupancy,
1585 ConcatBuf::ColorOffsets => &self.color_offsets,
1586 };
1587 for &e in new_entries {
1588 let e = e as usize;
1589 let off = match which {
1590 ConcatBuf::Occupancy => self.meta[e].occupancy_offset,
1591 ConcatBuf::ColorOffsets => self.meta[e].color_offsets_offset,
1592 };
1593 queue.write_buffer(
1594 target,
1595 u64::from(off) * 4,
1596 bytemuck::cast_slice(concat_data(®istry.entries[e], which)),
1597 );
1598 }
1599 }
1600 }
1601
1602 #[must_use]
1607 pub fn dead_model_count(&self) -> usize {
1608 self.chains.iter().filter(|c| c.is_empty()).count()
1609 }
1610
1611 #[must_use]
1613 pub fn live_model_count(&self) -> usize {
1614 self.chains.iter().filter(|c| !c.is_empty()).count()
1615 }
1616
1617 pub fn remove_model(&mut self, chain_id: u32) {
1630 let Some(entries) = self.chains.get(chain_id as usize).cloned() else {
1631 return;
1632 };
1633 if entries.is_empty() {
1634 return; }
1636 for &e in &entries {
1637 let e = e as usize;
1638 self.dead[e] = true;
1639 self.colors_alloc.free(e);
1640 }
1641 self.chains[chain_id as usize] = Vec::new(); }
1643
1644 pub fn compact(
1654 &mut self,
1655 device: &wgpu::Device,
1656 queue: &wgpu::Queue,
1657 registry: &SpriteModelRegistry,
1658 ) {
1659 self.compact_concat(device, registry, ConcatBuf::Occupancy);
1662 self.compact_concat(device, registry, ConcatBuf::ColorOffsets);
1663 self.repack_colors_dirs(device, registry);
1665 queue.write_buffer(&self.model_meta, 0, bytemuck::cast_slice(&self.meta));
1668 }
1669
1670 fn compact_concat(
1675 &mut self,
1676 device: &wgpu::Device,
1677 registry: &SpriteModelRegistry,
1678 which: ConcatBuf,
1679 ) {
1680 let mut all: Vec<u32> = Vec::new();
1681 for e in 0..self.meta.len() {
1682 if self.dead[e] {
1683 match which {
1684 ConcatBuf::Occupancy => self.meta[e].occupancy_offset = 0,
1685 ConcatBuf::ColorOffsets => self.meta[e].color_offsets_offset = 0,
1686 }
1687 continue;
1688 }
1689 let off = all.len() as u32;
1690 match which {
1691 ConcatBuf::Occupancy => self.meta[e].occupancy_offset = off,
1692 ConcatBuf::ColorOffsets => self.meta[e].color_offsets_offset = off,
1693 }
1694 all.extend_from_slice(concat_data(®istry.entries[e], which));
1695 }
1696 let used = all.len() as u32;
1697 let cap = grow_words(used);
1698 let (label, buf) = match which {
1699 ConcatBuf::Occupancy => ("roxlap-gpu sprite_reg.occupancy", &mut self.occupancy),
1700 ConcatBuf::ColorOffsets => (
1701 "roxlap-gpu sprite_reg.color_offsets",
1702 &mut self.color_offsets,
1703 ),
1704 };
1705 *buf = storage_dst_u32_cap(device, label, &all, cap);
1706 match which {
1707 ConcatBuf::Occupancy => {
1708 self.occ_used = used;
1709 self.occ_cap = cap;
1710 }
1711 ConcatBuf::ColorOffsets => {
1712 self.coloff_used = used;
1713 self.coloff_cap = cap;
1714 }
1715 }
1716 }
1717
1718 #[allow(clippy::too_many_arguments)]
1726 pub fn cull_bin_upload(
1727 &mut self,
1728 device: &wgpu::Device,
1729 queue: &wgpu::Queue,
1730 f: &ViewFrustum,
1731 screen_w: u32,
1732 screen_h: u32,
1733 tile_size: u32,
1734 lod_px: f32,
1735 ) -> (u32, u32, u32) {
1736 let tiles_x = screen_w.div_ceil(tile_size).max(1);
1737 let tiles_y = screen_h.div_ceil(tile_size).max(1);
1738 let n_tiles = (tiles_x * tiles_y) as usize;
1739
1740 let nw = (1.0 + f.half_w * f.half_w).sqrt();
1741 let nh = (1.0 + f.half_h * f.half_h).sqrt();
1742 let cx = screen_w as f32 * 0.5;
1743 let cy = screen_h as f32 * 0.5;
1744 let px_per_world = cx / f.half_w; let ts = tile_size as f32;
1746 let tx_max = tiles_x as i32 - 1;
1747 let ty_max = tiles_y as i32 - 1;
1748
1749 let mut visible: Vec<SpriteInstanceGpu> = Vec::with_capacity(self.cull.len());
1750 let mut boxes: Vec<[i32; 4]> = Vec::with_capacity(self.cull.len());
1752 let mut visible_colmul: Vec<u32> = Vec::with_capacity(self.cull.len() * 512);
1756 let mut counts = vec![0u32; n_tiles];
1757
1758 for ci in &self.cull {
1759 if self.chains[ci.chain_id as usize].is_empty() {
1763 continue;
1764 }
1765 let rel = [
1766 ci.center[0] - f.pos[0],
1767 ci.center[1] - f.pos[1],
1768 ci.center[2] - f.pos[2],
1769 ];
1770 let z = dot3(rel, f.forward);
1771 let r = ci.radius;
1772 if z + r < 0.0 || z - r > f.far {
1773 continue; }
1775 let x = dot3(rel, f.right);
1776 if (x - f.half_w * z) > r * nw || (-x - f.half_w * z) > r * nw {
1777 continue; }
1779 let y = dot3(rel, f.down);
1780 if (y - f.half_h * z) > r * nh || (-y - f.half_h * z) > r * nh {
1781 continue; }
1783
1784 let (tx0, tx1, ty0, ty1) = if z > 1e-3 {
1786 let sx = cx + (x / z) * px_per_world;
1787 let sy = cy + (y / z) * px_per_world;
1788 let sr = (r / z) * px_per_world;
1789 (
1790 (((sx - sr) / ts).floor() as i32).clamp(0, tx_max),
1791 (((sx + sr) / ts).floor() as i32).clamp(0, tx_max),
1792 (((sy - sr) / ts).floor() as i32).clamp(0, ty_max),
1793 (((sy + sr) / ts).floor() as i32).clamp(0, ty_max),
1794 )
1795 } else {
1796 (0, tx_max, 0, ty_max)
1797 };
1798 let chain = &self.chains[ci.chain_id as usize];
1805 let level = if z > 1e-3 && chain.len() > 1 {
1806 let voxel_px = px_per_world / z; ((lod_px / voxel_px).log2().ceil().max(0.0) as usize).min(chain.len() - 1)
1808 } else {
1809 0
1810 };
1811 let mut g = ci.gpu;
1812 g.model_id = chain[level];
1813 visible.push(g);
1814 boxes.push([tx0, tx1, ty0, ty1]);
1815 for &w in ci.colmul.iter() {
1816 visible_colmul.push((w & 0xffff_ffff) as u32);
1817 visible_colmul.push((w >> 32) as u32);
1818 }
1819 for ty in ty0..=ty1 {
1820 for tx in tx0..=tx1 {
1821 counts[(ty * tiles_x as i32 + tx) as usize] += 1;
1822 }
1823 }
1824 }
1825
1826 if visible.is_empty() {
1827 return (0, tiles_x, tiles_y);
1828 }
1829
1830 let mut tile_ranges = vec![0u32; n_tiles * 2];
1833 let mut running = 0u32;
1834 for t in 0..n_tiles {
1835 tile_ranges[2 * t] = running; tile_ranges[2 * t + 1] = counts[t]; running += counts[t];
1838 }
1839 let total = running as usize;
1840 let mut tile_instances = vec![0u32; total.max(1)];
1841 let mut cursor: Vec<u32> = (0..n_tiles).map(|t| tile_ranges[2 * t]).collect();
1842 for (vis_idx, b) in boxes.iter().enumerate() {
1843 for ty in b[2]..=b[3] {
1844 for tx in b[0]..=b[1] {
1845 let t = (ty * tiles_x as i32 + tx) as usize;
1846 tile_instances[cursor[t] as usize] = vis_idx as u32;
1847 cursor[t] += 1;
1848 }
1849 }
1850 }
1851
1852 queue.write_buffer(&self.instances, 0, bytemuck::cast_slice(&visible));
1856 let need_ranges = tile_ranges.len() as u32;
1857 if need_ranges > self.tile_ranges_cap {
1858 self.tile_ranges_cap = need_ranges.next_power_of_two();
1859 self.tile_ranges = storage_dst_u32(
1860 device,
1861 "roxlap-gpu sprite_reg.tile_ranges",
1862 self.tile_ranges_cap,
1863 );
1864 }
1865 let need_inst = tile_instances.len() as u32;
1866 if need_inst > self.tile_instances_cap {
1867 self.tile_instances_cap = need_inst.next_power_of_two();
1868 self.tile_instances = storage_dst_u32(
1869 device,
1870 "roxlap-gpu sprite_reg.tile_instances",
1871 self.tile_instances_cap,
1872 );
1873 }
1874 queue.write_buffer(&self.tile_ranges, 0, bytemuck::cast_slice(&tile_ranges));
1875 queue.write_buffer(
1876 &self.tile_instances,
1877 0,
1878 bytemuck::cast_slice(&tile_instances),
1879 );
1880 let need_colmul = visible_colmul.len() as u32;
1881 if need_colmul > self.colmul_cap {
1882 self.colmul_cap = need_colmul.next_power_of_two();
1883 self.colmul = storage_dst_u32(device, "roxlap-gpu sprite_reg.colmul", self.colmul_cap);
1884 }
1885 queue.write_buffer(&self.colmul, 0, bytemuck::cast_slice(&visible_colmul));
1886
1887 (visible.len() as u32, tiles_x, tiles_y)
1888 }
1889}
1890
1891#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1897struct ColorSlot {
1898 off: u32,
1899 cap: u32,
1900 len: u32,
1901}
1902
1903#[derive(Debug, Default)]
1910struct ColorsAllocator {
1911 slots: Vec<ColorSlot>,
1913 free: Vec<(u32, u32)>,
1915 tail: u32,
1917 cap_total: u32,
1919}
1920
1921fn slot_cap(len: u32) -> u32 {
1924 len + len / 4 + 16
1925}
1926
1927fn grow_words(used: u32) -> u32 {
1931 used + used / 2 + 256
1932}
1933
1934fn grow_records(count: u32) -> u32 {
1936 count + count / 2 + 8
1937}
1938
1939impl ColorsAllocator {
1940 fn new(entry_lens: &[u32]) -> Self {
1944 let mut a = Self::default();
1945 a.repack(entry_lens);
1946 a
1947 }
1948
1949 fn slot(&self, entry: usize) -> ColorSlot {
1950 self.slots[entry]
1951 }
1952
1953 fn cap_total(&self) -> u32 {
1954 self.cap_total
1955 }
1956
1957 fn repack(&mut self, new_lens: &[u32]) {
1961 self.free.clear();
1962 let mut off = 0u32;
1963 let mut slots = Vec::with_capacity(new_lens.len());
1964 for &len in new_lens {
1965 let cap = if len == 0 { 0 } else { slot_cap(len) };
1968 slots.push(ColorSlot { off, cap, len });
1969 off += cap;
1970 }
1971 self.slots = slots;
1972 self.tail = off;
1973 self.cap_total = off + off / 2 + 256;
1975 }
1976
1977 fn place(&mut self, entry: usize, new_len: u32) -> Option<u32> {
1982 let cur = self.slots[entry];
1983 if new_len <= cur.cap {
1984 self.slots[entry] = ColorSlot {
1985 len: new_len,
1986 ..cur
1987 };
1988 return Some(cur.off);
1989 }
1990 let old = (cur.off, cur.cap);
1991 if let Some(i) = self.free.iter().position(|&(_, c)| c >= new_len) {
1993 let (off, cap) = self.free.remove(i);
1994 self.free.push(old);
1995 self.slots[entry] = ColorSlot {
1996 off,
1997 cap,
1998 len: new_len,
1999 };
2000 return Some(off);
2001 }
2002 let want = slot_cap(new_len);
2004 if self.tail + want <= self.cap_total {
2005 let off = self.tail;
2006 self.tail += want;
2007 self.free.push(old);
2008 self.slots[entry] = ColorSlot {
2009 off,
2010 cap: want,
2011 len: new_len,
2012 };
2013 return Some(off);
2014 }
2015 None
2016 }
2017
2018 fn push(&mut self, new_len: u32) -> Option<u32> {
2024 if let Some(i) = self.free.iter().position(|&(_, c)| c >= new_len) {
2025 let (off, cap) = self.free.remove(i);
2026 self.slots.push(ColorSlot {
2027 off,
2028 cap,
2029 len: new_len,
2030 });
2031 return Some(off);
2032 }
2033 let want = slot_cap(new_len);
2034 if self.tail + want <= self.cap_total {
2035 let off = self.tail;
2036 self.tail += want;
2037 self.slots.push(ColorSlot {
2038 off,
2039 cap: want,
2040 len: new_len,
2041 });
2042 return Some(off);
2043 }
2044 None
2045 }
2046
2047 fn free(&mut self, entry: usize) {
2052 let s = self.slots[entry];
2053 if s.cap > 0 {
2054 self.free.push((s.off, s.cap));
2055 }
2056 self.slots[entry] = ColorSlot {
2057 off: 0,
2058 cap: 0,
2059 len: 0,
2060 };
2061 }
2062}
2063
2064#[allow(dead_code)]
2067fn storage_u32(device: &wgpu::Device, label: &str, data: &[u32]) -> wgpu::Buffer {
2068 use wgpu::util::DeviceExt;
2069 let bytes: &[u8] = if data.is_empty() {
2070 bytemuck::cast_slice(&[0u32])
2071 } else {
2072 bytemuck::cast_slice(data)
2073 };
2074 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2075 label: Some(label),
2076 contents: bytes,
2077 usage: wgpu::BufferUsages::STORAGE,
2078 })
2079}
2080
2081fn storage_dst_u32(device: &wgpu::Device, label: &str, cap: u32) -> wgpu::Buffer {
2084 device.create_buffer(&wgpu::BufferDescriptor {
2085 label: Some(label),
2086 size: u64::from(cap.max(1)) * 4,
2087 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
2088 mapped_at_creation: false,
2089 })
2090}
2091
2092fn storage_dst_u32_cap(device: &wgpu::Device, label: &str, data: &[u32], cap: u32) -> wgpu::Buffer {
2100 let cap = cap.max(data.len() as u32).max(1);
2101 let buf = device.create_buffer(&wgpu::BufferDescriptor {
2102 label: Some(label),
2103 size: u64::from(cap) * 4,
2104 usage: wgpu::BufferUsages::STORAGE
2105 | wgpu::BufferUsages::COPY_DST
2106 | wgpu::BufferUsages::COPY_SRC,
2107 mapped_at_creation: true,
2108 });
2109 if !data.is_empty() {
2110 buf.slice(..(data.len() as u64 * 4))
2111 .get_mapped_range_mut()
2112 .copy_from_slice(bytemuck::cast_slice(data));
2113 }
2114 buf.unmap();
2115 buf
2116}
2117
2118fn storage_dst_pod<T: Pod + Zeroable>(
2124 device: &wgpu::Device,
2125 label: &str,
2126 data: &[T],
2127) -> wgpu::Buffer {
2128 let one = [T::zeroed()];
2129 let src: &[T] = if data.is_empty() { &one } else { data };
2130 let buf = device.create_buffer(&wgpu::BufferDescriptor {
2131 label: Some(label),
2132 size: std::mem::size_of_val(src) as u64,
2133 usage: wgpu::BufferUsages::STORAGE
2134 | wgpu::BufferUsages::COPY_DST
2135 | wgpu::BufferUsages::COPY_SRC,
2136 mapped_at_creation: true,
2137 });
2138 buf.slice(..)
2139 .get_mapped_range_mut()
2140 .copy_from_slice(bytemuck::cast_slice(src));
2141 buf.unmap();
2142 buf
2143}
2144
2145fn storage_dst_pod_cap<T: Pod + Zeroable>(
2150 device: &wgpu::Device,
2151 label: &str,
2152 data: &[T],
2153 cap: u32,
2154) -> wgpu::Buffer {
2155 let rec = std::mem::size_of::<T>() as u64;
2156 let cap = u64::from(cap.max(data.len() as u32).max(1));
2157 let buf = device.create_buffer(&wgpu::BufferDescriptor {
2158 label: Some(label),
2159 size: cap * rec,
2160 usage: wgpu::BufferUsages::STORAGE
2161 | wgpu::BufferUsages::COPY_DST
2162 | wgpu::BufferUsages::COPY_SRC,
2163 mapped_at_creation: true,
2164 });
2165 if !data.is_empty() {
2166 buf.slice(..(data.len() as u64 * rec))
2167 .get_mapped_range_mut()
2168 .copy_from_slice(bytemuck::cast_slice(data));
2169 }
2170 buf.unmap();
2171 buf
2172}
2173
2174#[allow(dead_code)]
2177fn storage_pod<T: Pod + Zeroable>(device: &wgpu::Device, label: &str, data: &[T]) -> wgpu::Buffer {
2178 use wgpu::util::DeviceExt;
2179 let one = [T::zeroed()];
2180 let src: &[T] = if data.is_empty() { &one } else { data };
2181 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2182 label: Some(label),
2183 contents: bytemuck::cast_slice(src),
2184 usage: wgpu::BufferUsages::STORAGE,
2185 })
2186}
2187
2188#[cfg(test)]
2189mod tests {
2190 use super::*;
2191 use roxlap_formats::kv6::{Kv6, Voxel};
2192
2193 fn kv6_unsorted() -> Kv6 {
2196 let mk = |z, col| Voxel {
2197 col,
2198 z,
2199 vis: 0,
2200 dir: 0,
2201 };
2202 Kv6 {
2203 xsiz: 2,
2204 ysiz: 1,
2205 zsiz: 8,
2206 xpiv: 0.0,
2207 ypiv: 0.0,
2208 zpiv: 0.0,
2209 voxels: vec![mk(5, 0xAA), mk(1, 0xBB), mk(3, 0xCC)],
2210 xlen: vec![2, 1],
2211 ylen: vec![vec![2], vec![1]],
2212 palette: None,
2213 }
2214 }
2215
2216 #[test]
2217 fn occupancy_bits_set_at_voxel_z() {
2218 let m = build_sprite_model(&kv6_unsorted());
2219 assert_eq!(m.dims, [2, 1, 8]);
2220 assert_eq!(m.occ_words_per_col, 1); assert_eq!(m.occupancy[0], (1 << 1) | (1 << 5));
2223 assert_eq!(m.occupancy[1], 1 << 3);
2224 }
2225
2226 #[test]
2227 fn colors_are_ascending_z_for_rank_lookup() {
2228 let m = build_sprite_model(&kv6_unsorted());
2229 assert_eq!(m.color_offsets, vec![0, 2, 3]);
2231 assert_eq!(&m.colors, &[0xBB, 0xAA, 0xCC]);
2232 }
2233
2234 #[test]
2235 fn identity_basis_inverts_to_identity() {
2236 let inv = mat3_inverse([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
2237 assert_eq!(inv, [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
2238 }
2239
2240 #[test]
2241 fn fork_is_independent_of_parent() {
2242 let mut reg = SpriteModelRegistry::new();
2243 let base = reg.add(build_sprite_model(&kv6_unsorted()));
2244 let forked = reg.fork(base);
2245 assert_ne!(base, forked);
2246 reg.model_mut(forked).recolor(|_| 0x11);
2248 assert_eq!(®.model(base).colors, &[0xBB, 0xAA, 0xCC]);
2250 assert_eq!(®.model(forked).colors, &[0x11, 0x11, 0x11]);
2251 }
2252
2253 #[test]
2254 fn remove_frees_chain_data_keeps_ids_stable() {
2255 let mut reg = SpriteModelRegistry::new();
2256 let a = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2257 let b = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2258 let len_before = reg.len();
2259 assert!(reg.is_live(a) && reg.is_live(b));
2260
2261 reg.remove(a);
2262 assert!(!reg.is_live(a));
2265 assert!(reg.is_live(b));
2267 assert_eq!(®.model(b).colors, &[0xBB, 0xAA, 0xCC]);
2268 assert_eq!(reg.len(), len_before);
2269
2270 let c = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2272 assert_eq!(c, len_before as u32);
2273 assert!(reg.is_live(c));
2274 assert_eq!(®.model(b).colors, &[0xBB, 0xAA, 0xCC]);
2276 }
2277
2278 #[test]
2279 fn model_checked_guards_out_of_range_and_tombstoned() {
2280 let mut reg = SpriteModelRegistry::new();
2283 let a = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2284 assert!(reg.model_checked(a).is_some());
2285 assert!(reg.model_checked(9999).is_none(), "out of range → None");
2286 reg.remove(a);
2287 assert!(reg.model_checked(a).is_none(), "tombstoned chain → None");
2288 }
2289
2290 #[test]
2291 fn remove_is_idempotent_and_bounds_safe() {
2292 let mut reg = SpriteModelRegistry::new();
2293 let a = reg.add(build_sprite_model(&kv6_unsorted()));
2294 reg.remove(a);
2295 reg.remove(a); reg.remove(999); assert!(!reg.is_live(a));
2298 assert!(!reg.is_live(999));
2299 }
2300
2301 #[test]
2302 fn registry_gpu_structs_have_expected_sizes() {
2303 assert_eq!(std::mem::size_of::<SpriteModelMeta>(), 48);
2304 assert_eq!(std::mem::size_of::<SpriteInstanceGpu>(), 80);
2307 }
2308
2309 #[test]
2310 fn add_lod_builds_halving_mip_chain() {
2311 let mut reg = SpriteModelRegistry::new();
2312 let id = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2315 let m0 = reg.model(id);
2316 assert_eq!(m0.dims, [2, 1, 8]);
2317 assert!((m0.voxel_world_size - 1.0).abs() < 1e-6);
2318 }
2319
2320 fn kv6_from(xsiz: u32, ysiz: u32, zsiz: u32, voxels: &[(u32, u32, u16, u32)]) -> Kv6 {
2323 let mut ylen = vec![vec![0u16; ysiz as usize]; xsiz as usize];
2324 let mut flat = Vec::new();
2325 for x in 0..xsiz {
2326 for y in 0..ysiz {
2327 let mut col: Vec<(u16, u32)> = voxels
2328 .iter()
2329 .filter(|(vx, vy, _, _)| *vx == x && *vy == y)
2330 .map(|(_, _, z, c)| (*z, *c))
2331 .collect();
2332 col.sort_by_key(|(z, _)| *z);
2333 ylen[x as usize][y as usize] = col.len() as u16;
2334 for (z, c) in col {
2335 flat.push(Voxel {
2336 col: c,
2337 z,
2338 vis: 0,
2339 dir: 0,
2340 });
2341 }
2342 }
2343 }
2344 let xlen = ylen
2345 .iter()
2346 .map(|c| c.iter().map(|&v| u32::from(v)).sum())
2347 .collect();
2348 Kv6 {
2349 xsiz,
2350 ysiz,
2351 zsiz,
2352 xpiv: 0.0,
2353 ypiv: 0.0,
2354 zpiv: 0.0,
2355 voxels: flat,
2356 xlen,
2357 ylen,
2358 palette: None,
2359 }
2360 }
2361
2362 fn offsets_consistent(m: &SpriteModel) -> bool {
2363 let cols = (m.dims[0] * m.dims[1]) as usize;
2364 if m.color_offsets.len() != cols + 1 {
2365 return false;
2366 }
2367 for w in m.color_offsets.windows(2) {
2370 if w[1] < w[0] {
2371 return false;
2372 }
2373 }
2374 m.color_offsets[cols] as usize == m.colors.len()
2375 }
2376
2377 #[test]
2378 fn carve_two_layers_keeps_offsets_consistent() {
2379 let kv6 = kv6_from(
2382 3,
2383 2,
2384 8,
2385 &[
2386 (0, 0, 0, 0xA0),
2387 (0, 0, 1, 0xA1),
2388 (0, 0, 5, 0xA5),
2389 (1, 0, 1, 0xB1),
2390 (2, 1, 0, 0xC0),
2391 (2, 1, 3, 0xC3),
2392 ],
2393 );
2394 let mut m = build_sprite_model(&kv6);
2395 assert!(offsets_consistent(&m));
2396 for z in 0..2u32 {
2397 for y in 0..m.dims[1] {
2398 for x in 0..m.dims[0] {
2399 m.set_voxel(x, y, z, None);
2400 }
2401 }
2402 assert!(offsets_consistent(&m), "inconsistent after carving z={z}");
2403 let _ = m.downsample();
2405 }
2406 }
2407
2408 #[test]
2409 fn set_voxel_inserts_replaces_and_clears() {
2410 let mut m = build_sprite_model(&kv6_unsorted());
2412
2413 assert!(m.set_voxel(0, 0, 3, Some(0x55)));
2415 assert_eq!(m.occupancy[0], (1 << 1) | (1 << 3) | (1 << 5));
2416 assert_eq!(m.color_offsets, vec![0, 3, 4]);
2418 assert_eq!(&m.colors, &[0xBB, 0x55, 0xAA, 0xCC]);
2419
2420 assert!(m.set_voxel(0, 0, 3, Some(0x66)));
2422 assert_eq!(&m.colors, &[0xBB, 0x66, 0xAA, 0xCC]);
2423 assert_eq!(m.color_offsets, vec![0, 3, 4]);
2424
2425 assert!(m.set_voxel(0, 0, 1, None));
2427 assert_eq!(m.occupancy[0], (1 << 3) | (1 << 5));
2428 assert_eq!(m.color_offsets, vec![0, 2, 3]);
2429 assert_eq!(&m.colors, &[0x66, 0xAA, 0xCC]);
2430
2431 assert!(!m.set_voxel(0, 0, 2, None));
2433 assert!(!m.set_voxel(9, 0, 0, Some(1)));
2434 }
2435
2436 #[test]
2437 fn rebuild_lod_refreshes_coarse_levels_from_mip0() {
2438 let mut reg = SpriteModelRegistry::new();
2439 let id = reg.add_lod(build_sprite_model(&kv6_unsorted()), 3);
2440 reg.model_mut(id).recolor(|_| 0x0000_2000);
2442 reg.rebuild_lod(id);
2443 let lvl1_entry = reg.chains[id as usize][1] as usize;
2445 assert!(reg.entries[lvl1_entry]
2446 .colors
2447 .iter()
2448 .all(|&c| c == 0x0000_2000));
2449 }
2450
2451 fn alloc_invariants(a: &ColorsAllocator, lens: &[u32]) {
2456 let mut prev_end = 0u32;
2457 for (e, &len) in lens.iter().enumerate() {
2458 let s = a.slot(e);
2459 assert_eq!(s.len, len, "slot {e} len");
2460 assert!(s.cap >= s.len, "slot {e} cap >= len");
2461 assert!(s.off >= prev_end, "slot {e} overlaps previous");
2463 assert!(s.off + s.cap <= a.cap_total(), "slot {e} past cap_total");
2464 prev_end = s.off + s.cap;
2465 }
2466 assert!(a.cap_total() >= prev_end, "tail headroom");
2467 }
2468
2469 #[test]
2470 fn allocator_new_lays_out_with_slack_and_headroom() {
2471 let lens = [10u32, 0, 64, 7];
2472 let a = ColorsAllocator::new(&lens);
2473 alloc_invariants(&a, &lens);
2474 assert!(a.slot(2).cap > 64);
2476 assert!(a.cap_total() > a.slot(3).off + a.slot(3).cap);
2478 }
2479
2480 #[test]
2481 fn allocator_place_in_place_when_within_cap() {
2482 let mut a = ColorsAllocator::new(&[10, 20]);
2483 let off0 = a.slot(0).off;
2484 let cap0 = a.slot(0).cap;
2485 assert_eq!(a.place(0, 5), Some(off0));
2487 assert_eq!(a.slot(0).len, 5);
2488 assert_eq!(a.slot(0).cap, cap0);
2489 assert_eq!(a.place(0, cap0), Some(off0));
2491 assert_eq!(a.slot(0).off, off0);
2492 assert!(a.free.is_empty(), "no relocation should free anything");
2493 }
2494
2495 #[test]
2496 fn allocator_place_relocates_to_tail_and_frees_old() {
2497 let mut a = ColorsAllocator::new(&[10, 20]);
2498 let old0 = (a.slot(0).off, a.slot(0).cap);
2499 let tail_before = a.tail;
2500 let new_len = a.slot(0).cap + 5;
2502 let off = a.place(0, new_len).expect("fits in headroom");
2503 assert_eq!(off, tail_before, "relocated to old tail");
2504 assert_eq!(a.slot(0).off, off);
2505 assert_eq!(a.slot(0).len, new_len);
2506 assert!(a.free.contains(&old0), "old slot freed");
2507 }
2508
2509 #[test]
2510 fn allocator_reuses_freed_block_first_fit() {
2511 let mut a = ColorsAllocator::new(&[10, 2]);
2514 let old0 = (a.slot(0).off, a.slot(0).cap);
2515 let _ = a.place(0, a.slot(0).cap + 5).unwrap();
2517 assert!(a.free.contains(&old0));
2518 let new1 = a.slot(1).cap + 1;
2521 assert!(new1 <= old0.1, "freed block big enough");
2522 let off = a.place(1, new1).expect("reuses freed block");
2523 assert_eq!(off, old0.0, "first-fit reused the freed slot offset");
2524 assert!(!a.free.contains(&old0), "freed block consumed");
2525 }
2526
2527 #[test]
2528 fn allocator_signals_grow_then_repack_restores() {
2529 let mut a = ColorsAllocator::new(&[8, 8]);
2530 let huge = a.cap_total() + 100;
2532 assert_eq!(a.place(0, huge), None, "overflow must signal grow");
2533 a.repack(&[huge, 8]);
2535 alloc_invariants(&a, &[huge, 8]);
2536 assert!(a.cap_total() > huge);
2537 assert_eq!(a.place(0, huge), Some(a.slot(0).off));
2539 }
2540
2541 #[test]
2548 fn allocator_carve_loop_keeps_live_windows_disjoint() {
2549 let mut a = ColorsAllocator::new(&[40, 12, 40]);
2550 let mut lens = [40u32, 12, 40];
2551 let walk = [13u32, 30, 60, 18, 9, 80, 80, 25, 200, 7];
2554 let mut grew = false;
2555 for &len in &walk {
2556 lens[1] = len;
2557 if a.place(1, len).is_none() {
2559 grew = true;
2560 a.repack(&lens);
2561 } else {
2562 assert_eq!(a.place(0, 40), Some(a.slot(0).off));
2564 assert_eq!(a.place(2, 40), Some(a.slot(2).off));
2565 }
2566 assert_eq!(a.slot(1).len, len);
2567
2568 let mut wins: Vec<(u32, u32)> =
2570 (0..3).map(|e| (a.slot(e).off, a.slot(e).len)).collect();
2571 wins.sort_by_key(|w| w.0);
2572 for pair in wins.windows(2) {
2573 let (o0, l0) = pair[0];
2574 let (o1, _) = pair[1];
2575 assert!(o0 + l0 <= o1, "live windows overlap: {pair:?}");
2576 }
2577 }
2578 assert!(grew, "the 200-word jump should have forced a repack");
2579 }
2580
2581 fn headless() -> Option<crate::HeadlessGpu> {
2584 match crate::HeadlessGpu::new_blocking(crate::GpuRendererSettings::default()) {
2585 Ok(h) => Some(h),
2586 Err(e) => {
2587 eprintln!("[skip] no GPU adapter reachable: {e}");
2588 None
2589 }
2590 }
2591 }
2592
2593 fn one_model_registry() -> (SpriteModelRegistry, u32) {
2594 let mut reg = SpriteModelRegistry::new();
2595 let id = reg.add(build_sprite_model(&kv6_unsorted()));
2596 (reg, id)
2597 }
2598
2599 fn inst(model_id: u32, pos: [f32; 3]) -> SpriteInstance {
2600 use roxlap_formats::sprite::Sprite;
2601 SpriteInstance::new(
2602 model_id,
2603 SpriteInstanceTransform::from_sprite(&Sprite::axis_aligned(kv6_unsorted(), pos)),
2604 )
2605 }
2606
2607 #[test]
2608 fn append_grows_count_and_capacity_pow2() {
2609 let Some(h) = headless() else { return };
2610 let (reg, m) = one_model_registry();
2611 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(m, [0.0; 3])]);
2612 assert_eq!(res.instance_count(), 1);
2613 assert_eq!(res.instance_capacity, 1);
2614
2615 let more: Vec<_> = (1..=4).map(|i| inst(m, [i as f32, 0.0, 0.0])).collect();
2617 let base = res.append_instances(&h.device, ®, &more);
2618 assert_eq!(base, 1, "first appended index follows the seed instance");
2619 assert_eq!(res.instance_count(), 5);
2620 assert_eq!(res.instance_capacity, 8, "power-of-two growth");
2621
2622 let base2 = res.append_instances(&h.device, ®, &[inst(m, [9.0, 0.0, 0.0])]);
2624 assert_eq!(base2, 5);
2625 assert_eq!(res.instance_count(), 6);
2626 assert_eq!(res.instance_capacity, 8, "fits existing capacity, no grow");
2627 }
2628
2629 #[test]
2630 fn append_empty_is_noop() {
2631 let Some(h) = headless() else { return };
2632 let (reg, m) = one_model_registry();
2633 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(m, [0.0; 3])]);
2634 let base = res.append_instances(&h.device, ®, &[]);
2635 assert_eq!(base, 1);
2636 assert_eq!(res.instance_count(), 1);
2637 assert_eq!(res.instance_capacity, 1);
2638 }
2639
2640 fn read_u32(h: &crate::HeadlessGpu, buf: &wgpu::Buffer, words: u64) -> Vec<u32> {
2642 let bytes = words * 4;
2643 let staging = h.device.create_buffer(&wgpu::BufferDescriptor {
2644 label: Some("readback"),
2645 size: bytes,
2646 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2647 mapped_at_creation: false,
2648 });
2649 let mut enc = h
2650 .device
2651 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
2652 enc.copy_buffer_to_buffer(buf, 0, &staging, 0, bytes);
2653 h.queue.submit(std::iter::once(enc.finish()));
2654 let slice = staging.slice(..);
2655 let (tx, rx) = std::sync::mpsc::channel();
2656 slice.map_async(wgpu::MapMode::Read, move |r| tx.send(r).unwrap());
2657 h.device.poll(wgpu::PollType::wait_indefinitely()).ok();
2658 rx.recv().unwrap().unwrap();
2659 let data = slice.get_mapped_range();
2660 let out = bytemuck::cast_slice::<u8, u32>(&data).to_vec();
2661 drop(data);
2662 staging.unmap();
2663 out
2664 }
2665
2666 fn kv6_other() -> Kv6 {
2669 let mk = |z, col| Voxel {
2670 col,
2671 z,
2672 vis: 0,
2673 dir: 0,
2674 };
2675 Kv6 {
2676 xsiz: 1,
2677 ysiz: 1,
2678 zsiz: 4,
2679 xpiv: 0.0,
2680 ypiv: 0.0,
2681 zpiv: 0.0,
2682 voxels: vec![mk(0, 0x11), mk(2, 0x22)],
2683 xlen: vec![2],
2684 ylen: vec![vec![2]],
2685 palette: None,
2686 }
2687 }
2688
2689 #[test]
2693 fn add_model_uploads_new_volume_incrementally() {
2694 let Some(h) = headless() else { return };
2695
2696 let mut reg = SpriteModelRegistry::new();
2698 let a = reg.add(build_sprite_model(&kv6_unsorted()));
2699 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(a, [0.0; 3])]);
2700 assert_eq!(res.chains.len(), 1);
2701 let entries_before = res.meta.len();
2702
2703 let b = reg.add(build_sprite_model(&kv6_other()));
2705 res.add_model(&h.device, &h.queue, ®, b);
2706 assert_eq!(res.chains.len(), 2);
2707 assert_eq!(res.meta.len(), entries_before + 1, "one new entry");
2708
2709 let occ = read_u32(&h, &res.occupancy, u64::from(res.occ_cap));
2713 let coloff = read_u32(&h, &res.color_offsets, u64::from(res.coloff_cap));
2714 let cols = read_u32(&h, &res.colors, u64::from(res.colors_alloc.cap_total()));
2715 for (e, m) in reg.entries.iter().enumerate() {
2716 let meta = res.meta[e];
2717 let oo = meta.occupancy_offset as usize;
2718 assert_eq!(
2719 &occ[oo..oo + m.occupancy.len()],
2720 &m.occupancy[..],
2721 "occ entry {e}"
2722 );
2723 let co = meta.color_offsets_offset as usize;
2724 assert_eq!(
2725 &coloff[co..co + m.color_offsets.len()],
2726 &m.color_offsets[..],
2727 "color_offsets entry {e}"
2728 );
2729 let cc = meta.colors_offset as usize;
2730 assert_eq!(
2731 &cols[cc..cc + m.colors.len()],
2732 &m.colors[..],
2733 "colors entry {e}"
2734 );
2735 }
2736
2737 let base = res.append_instances(&h.device, ®, &[inst(b, [5.0, 0.0, 0.0])]);
2739 assert_eq!(base, 1);
2740 assert_eq!(res.instance_count(), 2);
2741 }
2742
2743 #[test]
2747 fn add_model_survives_buffer_growth() {
2748 let Some(h) = headless() else { return };
2749 let mut reg = SpriteModelRegistry::new();
2750 let a = reg.add(build_sprite_model(&kv6_unsorted()));
2751 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(a, [0.0; 3])]);
2752 let occ_cap0 = res.occ_cap;
2753
2754 for _ in 0..40 {
2757 let id = reg.add(build_sprite_model(&kv6_other()));
2758 res.add_model(&h.device, &h.queue, ®, id);
2759 }
2760 assert_eq!(res.chains.len(), 41);
2761 assert!(res.occ_cap > occ_cap0, "occupancy buffer grew");
2762
2763 let occ = read_u32(&h, &res.occupancy, u64::from(res.occ_cap));
2764 let cols = read_u32(&h, &res.colors, u64::from(res.colors_alloc.cap_total()));
2765 for (e, m) in reg.entries.iter().enumerate() {
2766 let meta = res.meta[e];
2767 let oo = meta.occupancy_offset as usize;
2768 assert_eq!(
2769 &occ[oo..oo + m.occupancy.len()],
2770 &m.occupancy[..],
2771 "occ entry {e}"
2772 );
2773 let cc = meta.colors_offset as usize;
2774 assert_eq!(
2775 &cols[cc..cc + m.colors.len()],
2776 &m.colors[..],
2777 "colors entry {e}"
2778 );
2779 }
2780 }
2781
2782 #[test]
2791 fn clip_frame_with_materials_classifies_by_color() {
2792 use roxlap_formats::voxel_clip::{LoopMode, VoxelClip, VoxelFrame};
2793
2794 let dims = [1u32, 1, 4];
2795 let owpc = dims[2].div_ceil(32).max(1) as usize; let glass = 0x80AA_BBCC;
2797 let stone = 0x8011_2233;
2798 let frame = VoxelFrame {
2799 occupancy: {
2800 let mut occ = vec![0u32; owpc];
2801 occ[0] |= (1 << 0) | (1 << 1);
2802 occ
2803 },
2804 colors: vec![stone, glass], color_offsets: vec![0, 2],
2806 };
2807 let clip = VoxelClip::from_frames(
2808 dims,
2809 [0.5, 0.5, 2.0],
2810 1.0,
2811 LoopMode::Loop,
2812 &[frame],
2813 &[],
2814 33,
2815 0,
2816 );
2817 let decoded = clip.decode().expect("decode");
2818
2819 let m = sprite_model_from_clip_frame_with_materials(&decoded, 0, &[(0x00AA_BBCC, 2)]);
2821 assert_eq!(
2822 m.materials.len(),
2823 m.colors.len(),
2824 "materials parallel to colors"
2825 );
2826 assert_eq!(
2828 m.materials,
2829 vec![0u8, 2u8],
2830 "stone opaque, glass material 2"
2831 );
2832
2833 let plain = sprite_model_from_clip_frame(&decoded, 0);
2835 let plain_mat = sprite_model_from_clip_frame_with_materials(&decoded, 0, &[]);
2836 assert!(plain.materials.is_empty());
2837 assert!(plain_mat.materials.is_empty());
2838 assert_eq!(plain.colors, plain_mat.colors);
2839 }
2840
2841 #[test]
2846 fn build_with_materials_classifies_by_color() {
2847 let glass = 0x80AA_BBCC;
2848 let stone = 0x8011_2233;
2849 let kv6 = kv6_from(1, 1, 4, &[(0, 0, 0, stone), (0, 0, 1, glass)]);
2851
2852 let m = build_sprite_model_with_materials(&kv6, &[(0x00AA_BBCC, 2)]);
2853 assert_eq!(
2854 m.materials.len(),
2855 m.colors.len(),
2856 "materials parallel to colors"
2857 );
2858 assert_eq!(
2859 m.materials,
2860 vec![0u8, 2u8],
2861 "stone opaque, glass material 2"
2862 );
2863
2864 let plain = build_sprite_model(&kv6);
2866 let plain_mat = build_sprite_model_with_materials(&kv6, &[]);
2867 assert!(plain.materials.is_empty());
2868 assert!(plain_mat.materials.is_empty());
2869 assert_eq!(plain.colors, plain_mat.colors);
2870 }
2871
2872 #[test]
2875 fn voxel_clip_flipbook_set_instance_model() {
2876 use roxlap_formats::voxel_clip::{LoopMode, VoxelClip, VoxelFrame};
2877 let Some(h) = headless() else { return };
2878
2879 let dims = [1u32, 1, 4];
2882 let owpc = dims[2].div_ceil(32).max(1) as usize; let mk_frame = |zs: &[u32], cols: &[u32]| -> VoxelFrame {
2884 let mut occ = vec![0u32; owpc];
2885 for &z in zs {
2886 occ[(z >> 5) as usize] |= 1u32 << (z & 31);
2887 }
2888 VoxelFrame {
2889 occupancy: occ,
2890 colors: cols.to_vec(),
2891 color_offsets: vec![0, cols.len() as u32],
2892 }
2893 };
2894 let f0 = mk_frame(&[0], &[0x8011_2233]);
2895 let f1 = mk_frame(&[0, 1], &[0x8011_2233, 0x80AA_BBCC]);
2896 let clip = VoxelClip::from_frames(
2897 dims,
2898 [0.5, 0.5, 2.0],
2899 1.0,
2900 LoopMode::Loop,
2901 &[f0, f1],
2902 &[],
2903 33,
2904 0,
2905 );
2906 let decoded = clip.decode().expect("decode");
2907
2908 let mut reg = SpriteModelRegistry::new();
2910 let c0 = reg.add(sprite_model_from_clip_frame(&decoded, 0));
2911 let c1 = reg.add(sprite_model_from_clip_frame(&decoded, 1));
2912 assert_eq!(reg.model(c0).colors.len(), 1);
2913 assert_eq!(reg.model(c1).colors.len(), 2);
2914
2915 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(c0, [0.0, 0.0, 5.0])]);
2917 assert_eq!(res.cull[0].chain_id, c0);
2918
2919 res.set_instance_model(®, 0, c1);
2921 assert_eq!(res.cull[0].chain_id, c1);
2922 assert_eq!(res.cull[0].radius, reg.model(c1).bound_radius());
2923
2924 let f = test_frustum();
2927 let (visible, _, _) = res.cull_bin_upload(&h.device, &h.queue, &f, 64, 64, 16, 1.0);
2928 assert_eq!(visible, 1);
2929
2930 res.set_instance_model(®, 0, c0);
2932 assert_eq!(res.cull[0].chain_id, c0);
2933
2934 res.set_instance_model(®, 99, c1);
2936 assert_eq!(res.cull[0].chain_id, c0);
2937 }
2938
2939 fn test_frustum() -> ViewFrustum {
2940 ViewFrustum {
2941 pos: [0.0, 0.0, 0.0],
2942 right: [1.0, 0.0, 0.0],
2943 down: [0.0, 1.0, 0.0],
2944 forward: [0.0, 0.0, 1.0],
2945 half_w: 1.0,
2946 half_h: 1.0,
2947 far: 10_000.0,
2948 }
2949 }
2950
2951 #[test]
2952 fn remove_model_tombstones_frees_and_reuses() {
2953 let Some(h) = headless() else { return };
2954 let mut reg = SpriteModelRegistry::new();
2956 let a = reg.add(build_sprite_model(&kv6_unsorted()));
2957 let b = reg.add(build_sprite_model(&kv6_other()));
2958 let mut res = SpriteRegistryResident::upload(
2959 &h.device,
2960 ®,
2961 &[inst(a, [0.0; 3]), inst(b, [1.0, 0.0, 0.0])],
2962 );
2963 assert_eq!(res.live_model_count(), 2);
2964 assert_eq!(res.dead_model_count(), 0);
2965
2966 res.remove_model(b);
2968 assert_eq!(res.live_model_count(), 1);
2969 assert_eq!(res.dead_model_count(), 1);
2970 assert_eq!(res.dead.iter().filter(|&&d| d).count(), 1, "one entry dead");
2971 assert!(!res.colors_alloc.free.is_empty(), "B's colour slot freed");
2972
2973 let c = reg.add(build_sprite_model(&kv6_other()));
2975 res.add_model(&h.device, &h.queue, ®, c);
2976 assert_eq!(res.live_model_count(), 2);
2977
2978 let cols = read_u32(&h, &res.colors, u64::from(res.colors_alloc.cap_total()));
2980 for e in [a as usize, c as usize] {
2981 let m = ®.entries[e];
2982 let cc = res.meta[e].colors_offset as usize;
2983 assert_eq!(
2984 &cols[cc..cc + m.colors.len()],
2985 &m.colors[..],
2986 "colors entry {e}"
2987 );
2988 }
2989
2990 let f = test_frustum();
2992 let _ = res.cull_bin_upload(&h.device, &h.queue, &f, 64, 64, 16, 1.0);
2993 }
2994
2995 #[test]
2996 fn compact_reclaims_holes_keeps_ids_stable() {
2997 let Some(h) = headless() else { return };
2998 let mut reg = SpriteModelRegistry::new();
2999 let a = reg.add(build_sprite_model(&kv6_unsorted()));
3000 let b = reg.add(build_sprite_model(&kv6_other()));
3001 let c = reg.add(build_sprite_model(&kv6_other()));
3002 let mut res = SpriteRegistryResident::upload(
3003 &h.device,
3004 ®,
3005 &[inst(a, [0.0; 3]), inst(b, [1.0; 3]), inst(c, [2.0; 3])],
3006 );
3007 let occ_used_full = res.occ_used;
3008
3009 res.remove_model(b);
3011 res.compact(&h.device, &h.queue, ®);
3012
3013 let live_occ: u32 = [a, c]
3015 .iter()
3016 .map(|&e| reg.entries[e as usize].occupancy.len() as u32)
3017 .sum();
3018 assert_eq!(res.occ_used, live_occ);
3019 assert!(res.occ_used < occ_used_full, "compaction shrank occupancy");
3020 assert_eq!(res.meta[b as usize].occupancy_offset, 0);
3022 assert_eq!(res.live_model_count(), 2);
3023 assert_eq!(res.dead_model_count(), 1);
3024
3025 let occ = read_u32(&h, &res.occupancy, u64::from(res.occ_cap));
3027 let cols = read_u32(&h, &res.colors, u64::from(res.colors_alloc.cap_total()));
3028 for &e in &[a as usize, c as usize] {
3029 let m = ®.entries[e];
3030 let oo = res.meta[e].occupancy_offset as usize;
3031 assert_eq!(
3032 &occ[oo..oo + m.occupancy.len()],
3033 &m.occupancy[..],
3034 "occ {e}"
3035 );
3036 let cc = res.meta[e].colors_offset as usize;
3037 assert_eq!(&cols[cc..cc + m.colors.len()], &m.colors[..], "cols {e}");
3038 }
3039
3040 assert!(!res.chains[c as usize].is_empty());
3042 assert!(res.chains[b as usize].is_empty());
3043 }
3044
3045 #[test]
3046 fn remove_swap_semantics_and_capacity_retained() {
3047 let Some(h) = headless() else { return };
3048 let (reg, m) = one_model_registry();
3049 let seed: Vec<_> = (0..4).map(|i| inst(m, [i as f32, 0.0, 0.0])).collect();
3050 let mut res = SpriteRegistryResident::upload(&h.device, ®, &seed);
3051 assert_eq!(res.instance_count(), 4);
3052 let cap = res.instance_capacity;
3053
3054 assert_eq!(res.remove_instance(1), Some(3));
3056 assert_eq!(res.instance_count(), 3);
3057
3058 assert_eq!(res.remove_instance(2), None);
3060 assert_eq!(res.instance_count(), 2);
3061
3062 assert_eq!(res.remove_instance(99), None);
3064 assert_eq!(res.instance_count(), 2);
3065
3066 assert_eq!(res.instance_capacity, cap);
3068 }
3069}