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 pub max_scale: f32,
274}
275
276impl SpriteInstanceTransform {
277 #[must_use]
281 pub fn from_sprite(sprite: &Sprite) -> Self {
282 let inv = mat3_inverse([sprite.s, sprite.h, sprite.f]);
283 let len = |c: [f32; 3]| (c[0] * c[0] + c[1] * c[1] + c[2] * c[2]).sqrt();
284 Self {
285 inv_rot: [
286 [inv[0][0], inv[0][1], inv[0][2], 0.0],
287 [inv[1][0], inv[1][1], inv[1][2], 0.0],
288 [inv[2][0], inv[2][1], inv[2][2], 0.0],
289 ],
290 pos: sprite.p,
291 max_scale: len(sprite.s).max(len(sprite.h)).max(len(sprite.f)),
292 }
293 }
294}
295
296#[derive(Debug, Clone, Default)]
304pub struct SpriteModelRegistry {
305 entries: Vec<SpriteModel>,
307 chains: Vec<Vec<u32>>,
309}
310
311impl SpriteModelRegistry {
312 #[must_use]
313 pub fn new() -> Self {
314 Self::default()
315 }
316
317 fn push_entry(&mut self, model: SpriteModel) -> u32 {
318 let id = self.entries.len() as u32;
319 self.entries.push(model);
320 id
321 }
322
323 pub fn add(&mut self, model: SpriteModel) -> u32 {
325 let e = self.push_entry(model);
326 let id = self.chains.len() as u32;
327 self.chains.push(vec![e]);
328 id
329 }
330
331 pub fn add_lod(&mut self, model: SpriteModel, max_levels: u32) -> u32 {
335 let mut levels = vec![self.push_entry(model.clone())];
336 let mut cur = model;
337 for _ in 1..max_levels.max(1) {
338 if cur.dims == [1, 1, 1] {
339 break;
340 }
341 cur = cur.downsample();
342 levels.push(self.push_entry(cur.clone()));
343 }
344 let id = self.chains.len() as u32;
345 self.chains.push(levels);
346 id
347 }
348
349 pub fn fork(&mut self, parent: u32) -> u32 {
357 let src = self.chains[parent as usize].clone();
358 let levels: Vec<u32> = src
359 .iter()
360 .map(|&e| {
361 let copy = self.entries[e as usize].clone();
362 self.push_entry(copy)
363 })
364 .collect();
365 let id = self.chains.len() as u32;
366 self.chains.push(levels);
367 id
368 }
369
370 #[must_use]
372 pub fn model(&self, id: u32) -> &SpriteModel {
373 &self.entries[self.chains[id as usize][0] as usize]
374 }
375
376 #[must_use]
380 pub fn model_checked(&self, id: u32) -> Option<&SpriteModel> {
381 let entry = *self.chains.get(id as usize)?.first()?;
382 self.entries.get(entry as usize)
383 }
384
385 pub fn model_mut(&mut self, id: u32) -> &mut SpriteModel {
391 let e = self.chains[id as usize][0] as usize;
392 &mut self.entries[e]
393 }
394
395 pub fn recolor_chain(&mut self, id: u32, f: impl Fn(u32) -> u32 + Copy) {
398 for li in 0..self.chains[id as usize].len() {
399 let e = self.chains[id as usize][li] as usize;
400 self.entries[e].recolor(f);
401 }
402 }
403
404 pub fn rebuild_lod(&mut self, id: u32) {
409 let levels = self.chains[id as usize].clone();
410 if levels.len() <= 1 {
411 return;
412 }
413 let mut cur = self.entries[levels[0] as usize].clone();
414 for &e in &levels[1..] {
415 cur = cur.downsample();
416 self.entries[e as usize] = cur.clone();
417 }
418 }
419
420 pub fn remove(&mut self, chain_id: u32) {
434 let Some(entries) = self.chains.get(chain_id as usize) else {
435 return;
436 };
437 let entries = entries.clone();
439 for e in entries {
440 self.entries[e as usize] = SpriteModel::empty();
441 }
442 self.chains[chain_id as usize] = Vec::new(); }
444
445 #[must_use]
448 pub fn is_live(&self, chain_id: u32) -> bool {
449 self.chains
450 .get(chain_id as usize)
451 .is_some_and(|c| !c.is_empty())
452 }
453
454 #[must_use]
458 pub fn len(&self) -> usize {
459 self.chains.len()
460 }
461
462 #[must_use]
463 pub fn is_empty(&self) -> bool {
464 self.chains.is_empty()
465 }
466}
467
468impl SpriteModel {
469 #[must_use]
476 pub fn empty() -> Self {
477 Self {
478 dims: [0, 0, 0],
479 occ_words_per_col: 1,
480 pivot: [0.0, 0.0, 0.0],
481 occupancy: Vec::new(),
482 colors: Vec::new(),
483 dirs: Vec::new(),
484 color_offsets: vec![0],
485 materials: Vec::new(),
486 voxel_world_size: 1.0,
487 }
488 }
489
490 pub fn recolor(&mut self, f: impl Fn(u32) -> u32) {
496 for c in &mut self.colors {
497 *c = f(*c);
498 }
499 }
500
501 pub fn set_voxel(&mut self, x: u32, y: u32, z: u32, color: Option<u32>) -> bool {
512 if x >= self.dims[0] || y >= self.dims[1] || z >= self.dims[2] {
513 return false;
514 }
515 let owpc = self.occ_words_per_col as usize;
516 let cols = (self.dims[0] * self.dims[1]) as usize;
517 let col = (x + y * self.dims[0]) as usize;
518 let base = col * owpc;
519 let zw = (z >> 5) as usize;
520 let zb = z & 31;
521
522 let mut rank = 0usize;
524 for w in 0..zw {
525 rank += self.occupancy[base + w].count_ones() as usize;
526 }
527 let below_mask = if zb > 0 { (1u32 << zb) - 1 } else { 0 };
528 rank += (self.occupancy[base + zw] & below_mask).count_ones() as usize;
529 let idx = self.color_offsets[col] as usize + rank;
530 let was_set = (self.occupancy[base + zw] >> zb) & 1 == 1;
531
532 if let Some(rgba) = color {
533 if was_set {
534 self.colors[idx] = rgba; } else {
536 self.occupancy[base + zw] |= 1u32 << zb;
537 self.colors.insert(idx, rgba);
538 self.dirs.insert(idx, 0);
541 if !self.materials.is_empty() {
542 self.materials.insert(idx, 0); }
544 for c in &mut self.color_offsets[col + 1..=cols] {
545 *c += 1;
546 }
547 }
548 true
549 } else {
550 if !was_set {
551 return false;
552 }
553 self.occupancy[base + zw] &= !(1u32 << zb);
554 self.colors.remove(idx);
555 self.dirs.remove(idx);
556 if !self.materials.is_empty() {
557 self.materials.remove(idx);
558 }
559 for c in &mut self.color_offsets[col + 1..=cols] {
560 *c -= 1;
561 }
562 true
563 }
564 }
565
566 #[must_use]
573 pub fn bound_radius(&self) -> f32 {
574 let mut r2 = 0.0_f32;
575 for &cx in &[0.0, self.dims[0] as f32] {
576 for &cy in &[0.0, self.dims[1] as f32] {
577 for &cz in &[0.0, self.dims[2] as f32] {
578 let d = [cx - self.pivot[0], cy - self.pivot[1], cz - self.pivot[2]];
579 r2 = r2.max(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
580 }
581 }
582 }
583 r2.sqrt()
584 }
585
586 #[must_use]
592 #[allow(clippy::manual_checked_ops)] pub fn downsample(&self) -> SpriteModel {
594 let [fx, fy, fz] = self.dims;
595 let fidx = |x: u32, y: u32, z: u32| (x + y * fx + z * fx * fy) as usize;
596
597 let has_mats = !self.materials.is_empty();
600 let mut solid = vec![false; (fx * fy * fz) as usize];
601 let mut fine = vec![0u32; (fx * fy * fz) as usize];
602 let mut fine_dir = vec![0u32; (fx * fy * fz) as usize];
603 let mut fine_mat = vec![0u8; (fx * fy * fz) as usize];
604 for x in 0..fx {
605 for y in 0..fy {
606 let col = (x + y * fx) as usize;
607 let base = col * self.occ_words_per_col as usize;
608 let off = self.color_offsets[col] as usize;
609 let mut seen = 0usize;
610 for z in 0..fz {
611 let w = base + (z >> 5) as usize;
612 if (self.occupancy[w] >> (z & 31)) & 1 == 1 {
613 fine[fidx(x, y, z)] = self.colors[off + seen];
614 fine_dir[fidx(x, y, z)] = self.dirs[off + seen];
615 if has_mats {
616 fine_mat[fidx(x, y, z)] = self.materials[off + seen];
617 }
618 solid[fidx(x, y, z)] = true;
619 seen += 1;
620 }
621 }
622 }
623 }
624
625 let nx = fx.div_ceil(2).max(1);
626 let ny = fy.div_ceil(2).max(1);
627 let nz = fz.div_ceil(2).max(1);
628 let owpc = nz.div_ceil(32).max(1);
629 let cols = (nx * ny) as usize;
630 let mut occupancy = vec![0u32; cols * owpc as usize];
631 let mut color_offsets = vec![0u32; cols + 1];
632 let mut colors: Vec<u32> = Vec::new();
633 let mut dirs: Vec<u32> = Vec::new();
634 let mut materials: Vec<u8> = Vec::new();
635
636 for cy in 0..ny {
639 for cx in 0..nx {
640 let ccol = (cx + cy * nx) as usize;
641 color_offsets[ccol] = colors.len() as u32;
642 for cz in 0..nz {
643 let (mut a, mut r, mut g, mut b, mut n) = (0u32, 0u32, 0u32, 0u32, 0u32);
644 let mut rep_dir = 0u32;
648 let mut rep_mat = 0u8;
649 for dz in 0..2 {
650 for dy in 0..2 {
651 for dx in 0..2 {
652 let (x, y, z) = (2 * cx + dx, 2 * cy + dy, 2 * cz + dz);
653 if x < fx && y < fy && z < fz && solid[fidx(x, y, z)] {
654 let c = fine[fidx(x, y, z)];
655 if n == 0 {
656 rep_dir = fine_dir[fidx(x, y, z)];
657 rep_mat = fine_mat[fidx(x, y, z)];
658 }
659 a += (c >> 24) & 0xff;
660 r += (c >> 16) & 0xff;
661 g += (c >> 8) & 0xff;
662 b += c & 0xff;
663 n += 1;
664 }
665 }
666 }
667 }
668 if n > 0 {
669 let avg = ((a / n) << 24) | ((r / n) << 16) | ((g / n) << 8) | (b / n);
670 let base = ccol * owpc as usize + (cz >> 5) as usize;
671 occupancy[base] |= 1u32 << (cz & 31);
672 colors.push(avg);
673 dirs.push(rep_dir);
674 if has_mats {
675 materials.push(rep_mat);
676 }
677 }
678 }
679 }
680 }
681 color_offsets[cols] = colors.len() as u32;
682
683 SpriteModel {
684 dims: [nx, ny, nz],
685 occ_words_per_col: owpc,
686 pivot: [
687 self.pivot[0] * 0.5,
688 self.pivot[1] * 0.5,
689 self.pivot[2] * 0.5,
690 ],
691 occupancy,
692 colors,
693 dirs,
694 color_offsets,
695 materials,
696 voxel_world_size: self.voxel_world_size * 2.0,
697 }
698 }
699}
700
701#[derive(Clone, Copy, Debug)]
706pub struct ViewFrustum {
707 pub pos: [f32; 3],
708 pub right: [f32; 3],
709 pub down: [f32; 3],
710 pub forward: [f32; 3],
711 pub half_w: f32,
712 pub half_h: f32,
713 pub far: f32,
714}
715
716#[derive(Clone)]
719struct CullInstance {
720 gpu: SpriteInstanceGpu,
723 chain_id: u32,
725 center: [f32; 3],
726 radius: f32,
730 model_radius: f32,
733 max_scale: f32,
737 colmul: Box<[u64; 256]>,
743}
744
745fn identity_colmul() -> Box<[u64; 256]> {
748 const LANE: u64 = 0x0100;
749 let w = LANE | (LANE << 16) | (LANE << 32) | (LANE << 48);
750 Box::new([w; 256])
751}
752
753fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
754 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
755}
756
757#[derive(Clone, Copy, PartialEq)]
764struct CullKey {
765 frustum: [u32; 15],
766 screen: [u32; 4],
767}
768
769impl CullKey {
770 fn new(f: &ViewFrustum, screen_w: u32, screen_h: u32, tile_size: u32, lod_px: f32) -> Self {
771 let b = |v: f32| v.to_bits();
772 Self {
773 frustum: [
774 b(f.pos[0]),
775 b(f.pos[1]),
776 b(f.pos[2]),
777 b(f.right[0]),
778 b(f.right[1]),
779 b(f.right[2]),
780 b(f.down[0]),
781 b(f.down[1]),
782 b(f.down[2]),
783 b(f.forward[0]),
784 b(f.forward[1]),
785 b(f.forward[2]),
786 b(f.half_w),
787 b(f.half_h),
788 b(f.far),
789 ],
790 screen: [screen_w, screen_h, tile_size, lod_px.to_bits()],
791 }
792 }
793}
794
795#[derive(Default)]
797struct CullScratch {
798 visible: Vec<SpriteInstanceGpu>,
799 boxes: Vec<[i32; 4]>,
800 colmul: Vec<u32>,
801 counts: Vec<u32>,
802 tile_ranges: Vec<u32>,
803 tile_instances: Vec<u32>,
804 cursor: Vec<u32>,
805}
806
807fn make_cull(registry: &SpriteModelRegistry, i: &SpriteInstance) -> CullInstance {
813 let model_radius = registry.model(i.model_id).bound_radius();
814 CullInstance {
815 gpu: SpriteInstanceGpu {
816 inv_rot0: i.transform.inv_rot[0],
817 inv_rot1: i.transform.inv_rot[1],
818 inv_rot2: i.transform.inv_rot[2],
819 pos: i.transform.pos,
820 model_id: i.model_id, material: u32::from(i.material),
822 alpha_mul: f32::from(i.alpha_mul) / 255.0,
823 flags: i.flags,
824 tint: i.tint,
825 },
826 chain_id: i.model_id,
827 center: i.transform.pos,
828 radius: model_radius * i.transform.max_scale,
829 model_radius,
830 max_scale: i.transform.max_scale,
831 colmul: identity_colmul(),
832 }
833}
834
835fn instances_buffer(device: &wgpu::Device, cap: u32) -> wgpu::Buffer {
840 device.create_buffer(&wgpu::BufferDescriptor {
841 label: Some("roxlap-gpu sprite_reg.instances"),
842 size: u64::from(cap.max(1)) * std::mem::size_of::<SpriteInstanceGpu>() as u64,
843 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
844 mapped_at_creation: false,
845 })
846}
847
848#[derive(Debug, Clone, Copy)]
850pub struct SpriteInstance {
851 pub model_id: u32,
852 pub transform: SpriteInstanceTransform,
853 pub material: u8,
857 pub alpha_mul: u8,
860 pub flags: u32,
864 pub tint: u32,
866}
867
868impl SpriteInstance {
869 #[must_use]
873 pub fn new(model_id: u32, transform: SpriteInstanceTransform) -> Self {
874 Self {
875 model_id,
876 transform,
877 material: 0,
878 alpha_mul: 255,
879 flags: 0,
880 tint: 0x00FF_FFFF,
881 }
882 }
883}
884
885#[repr(C)]
889#[derive(Clone, Copy, Pod, Zeroable, Debug)]
890struct SpriteModelMeta {
891 occupancy_offset: u32,
892 colors_offset: u32,
893 color_offsets_offset: u32,
894 occ_words_per_col: u32,
895 dims: [u32; 3],
896 has_vox_materials: u32,
899 pivot: [f32; 3],
900 voxel_world_size: f32,
902}
903
904#[repr(C)]
908#[derive(Clone, Copy, Pod, Zeroable, Debug)]
909struct SpriteInstanceGpu {
910 inv_rot0: [f32; 4],
911 inv_rot1: [f32; 4],
912 inv_rot2: [f32; 4],
913 pos: [f32; 3],
914 model_id: u32,
915 material: u32,
917 alpha_mul: f32,
919 flags: u32,
922 tint: u32,
924}
925
926#[must_use]
930fn mat3_inverse(cols: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
931 let [a, b, c] = cols; let cross = |u: [f32; 3], v: [f32; 3]| {
934 [
935 u[1] * v[2] - u[2] * v[1],
936 u[2] * v[0] - u[0] * v[2],
937 u[0] * v[1] - u[1] * v[0],
938 ]
939 };
940 let bc = cross(b, c);
941 let ca = cross(c, a);
942 let ab = cross(a, b);
943 let det = a[0] * bc[0] + a[1] * bc[1] + a[2] * bc[2];
944 let inv_det = if det.abs() < 1e-12 { 0.0 } else { 1.0 / det };
945 [
948 [bc[0] * inv_det, ca[0] * inv_det, ab[0] * inv_det],
949 [bc[1] * inv_det, ca[1] * inv_det, ab[1] * inv_det],
950 [bc[2] * inv_det, ca[2] * inv_det, ab[2] * inv_det],
951 ]
952}
953
954pub struct SpriteRegistryResident {
961 pub occupancy: wgpu::Buffer,
962 pub colors: wgpu::Buffer,
963 pub dirs: wgpu::Buffer,
967 pub materials_vox: wgpu::Buffer,
972 pub color_offsets: wgpu::Buffer,
973 pub model_meta: wgpu::Buffer,
974 pub instances: wgpu::Buffer,
977 pub instance_capacity: u32,
978 pub colmul: wgpu::Buffer,
983 colmul_cap: u32,
984 pub tile_ranges: wgpu::Buffer,
987 tile_ranges_cap: u32,
988 pub tile_instances: wgpu::Buffer,
991 tile_instances_cap: u32,
992 cull: Vec<CullInstance>,
994 chains: Vec<Vec<u32>>,
998 meta: Vec<SpriteModelMeta>,
1003 colors_alloc: ColorsAllocator,
1007 last_cull: Option<(CullKey, (u32, u32, u32))>,
1011 any_colmul: bool,
1016 colmul_identity: bool,
1019 scratch: CullScratch,
1021 occ_lens: Vec<u32>,
1026 coloff_lens: Vec<u32>,
1027 occ_used: u32,
1032 occ_cap: u32,
1033 coloff_used: u32,
1036 coloff_cap: u32,
1037 meta_cap: u32,
1040 dead: Vec<bool>,
1046}
1047
1048#[derive(Clone, Copy)]
1051enum ConcatBuf {
1052 Occupancy,
1053 ColorOffsets,
1054}
1055
1056fn concat_data(m: &SpriteModel, which: ConcatBuf) -> &[u32] {
1059 match which {
1060 ConcatBuf::Occupancy => &m.occupancy,
1061 ConcatBuf::ColorOffsets => &m.color_offsets,
1062 }
1063}
1064
1065impl SpriteRegistryResident {
1066 #[must_use]
1071 pub fn upload(
1072 device: &wgpu::Device,
1073 registry: &SpriteModelRegistry,
1074 instances: &[SpriteInstance],
1075 ) -> Self {
1076 let entry_lens: Vec<u32> = registry
1081 .entries
1082 .iter()
1083 .map(|m| m.colors.len() as u32)
1084 .collect();
1085 let colors_alloc = ColorsAllocator::new(&entry_lens);
1086 let cap_total = colors_alloc.cap_total();
1087
1088 let mut all_occ: Vec<u32> = Vec::new();
1089 let mut all_offsets: Vec<u32> = Vec::new();
1090 let mut all_colors: Vec<u32> = vec![0; cap_total as usize];
1091 let mut all_dirs: Vec<u32> = vec![0; cap_total as usize];
1092 let mut all_materials: Vec<u32> = vec![0; cap_total as usize];
1093 let mut meta: Vec<SpriteModelMeta> = Vec::with_capacity(registry.entries.len());
1094 let mut occ_lens: Vec<u32> = Vec::with_capacity(registry.entries.len());
1095 let mut coloff_lens: Vec<u32> = Vec::with_capacity(registry.entries.len());
1096
1097 for (e, m) in registry.entries.iter().enumerate() {
1099 let slot = colors_alloc.slot(e);
1100 meta.push(SpriteModelMeta {
1101 occupancy_offset: all_occ.len() as u32,
1102 colors_offset: slot.off,
1103 color_offsets_offset: all_offsets.len() as u32,
1104 occ_words_per_col: m.occ_words_per_col,
1105 dims: m.dims,
1106 has_vox_materials: u32::from(!m.materials.is_empty()),
1107 pivot: m.pivot,
1108 voxel_world_size: m.voxel_world_size,
1109 });
1110 occ_lens.push(m.occupancy.len() as u32);
1111 coloff_lens.push(m.color_offsets.len() as u32);
1112 all_occ.extend_from_slice(&m.occupancy);
1113 all_offsets.extend_from_slice(&m.color_offsets);
1114 let off = slot.off as usize;
1115 all_colors[off..off + m.colors.len()].copy_from_slice(&m.colors);
1116 all_dirs[off..off + m.dirs.len()].copy_from_slice(&m.dirs);
1117 for (i, &mat) in m.materials.iter().enumerate() {
1118 all_materials[off + i] = u32::from(mat);
1119 }
1120 }
1121
1122 let cull: Vec<CullInstance> = instances.iter().map(|i| make_cull(registry, i)).collect();
1127
1128 let seed: Vec<SpriteInstanceGpu> = cull.iter().map(|c| c.gpu).collect();
1131 let instances_buf = {
1132 use wgpu::util::DeviceExt;
1133 let one = [SpriteInstanceGpu::zeroed()];
1134 let src: &[SpriteInstanceGpu] = if seed.is_empty() { &one } else { &seed };
1135 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1136 label: Some("roxlap-gpu sprite_reg.instances"),
1137 contents: bytemuck::cast_slice(src),
1138 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1139 })
1140 };
1141
1142 let tile_ranges = storage_dst_u32(device, "roxlap-gpu sprite_reg.tile_ranges", 1);
1143 let tile_instances = storage_dst_u32(device, "roxlap-gpu sprite_reg.tile_instances", 1);
1144 let colmul_cap = (cull.len() as u32).max(1) * 256 * 2;
1147 let colmul = storage_dst_u32(device, "roxlap-gpu sprite_reg.colmul", colmul_cap);
1148 Self {
1149 occupancy: storage_dst_u32_cap(
1150 device,
1151 "roxlap-gpu sprite_reg.occupancy",
1152 &all_occ,
1153 all_occ.len() as u32,
1154 ),
1155 colors: storage_dst_u32_cap(
1156 device,
1157 "roxlap-gpu sprite_reg.colors",
1158 &all_colors,
1159 cap_total,
1160 ),
1161 dirs: storage_dst_u32_cap(device, "roxlap-gpu sprite_reg.dirs", &all_dirs, cap_total),
1162 materials_vox: storage_dst_u32_cap(
1163 device,
1164 "roxlap-gpu sprite_reg.materials_vox",
1165 &all_materials,
1166 cap_total,
1167 ),
1168 color_offsets: storage_dst_u32_cap(
1169 device,
1170 "roxlap-gpu sprite_reg.color_offsets",
1171 &all_offsets,
1172 all_offsets.len() as u32,
1173 ),
1174 model_meta: storage_dst_pod(device, "roxlap-gpu sprite_reg.model_meta", &meta),
1175 instances: instances_buf,
1176 instance_capacity: cull.len() as u32,
1177 colmul,
1178 colmul_cap,
1179 tile_ranges,
1180 tile_ranges_cap: 1,
1181 tile_instances,
1182 tile_instances_cap: 1,
1183 cull,
1184 chains: registry.chains.clone(),
1185 last_cull: None,
1186 any_colmul: false,
1187 colmul_identity: false,
1188 scratch: CullScratch::default(),
1189 occ_used: all_occ.len() as u32,
1190 occ_cap: all_occ.len() as u32,
1191 coloff_used: all_offsets.len() as u32,
1192 coloff_cap: all_offsets.len() as u32,
1193 meta_cap: meta.len() as u32,
1194 dead: vec![false; meta.len()],
1195 meta,
1196 colors_alloc,
1197 occ_lens,
1198 coloff_lens,
1199 }
1200 }
1201
1202 #[must_use]
1204 pub fn instance_count(&self) -> usize {
1205 self.cull.len()
1206 }
1207
1208 pub fn append_instances(
1230 &mut self,
1231 device: &wgpu::Device,
1232 registry: &SpriteModelRegistry,
1233 instances: &[SpriteInstance],
1234 ) -> u32 {
1235 let base = self.cull.len() as u32;
1236 if instances.is_empty() {
1237 return base;
1238 }
1239 self.last_cull = None; for i in instances {
1241 debug_assert!(
1242 (i.model_id as usize) < self.chains.len(),
1243 "append_instances: model_id {} not resident (run upload to register new models)",
1244 i.model_id
1245 );
1246 self.cull.push(make_cull(registry, i));
1247 }
1248 let need = self.cull.len() as u32;
1249 if need > self.instance_capacity {
1250 self.instance_capacity = need.next_power_of_two();
1254 self.instances = instances_buffer(device, self.instance_capacity);
1255 }
1256 base
1257 }
1258
1259 pub fn remove_instance(&mut self, index: usize) -> Option<usize> {
1270 if index >= self.cull.len() {
1271 return None;
1272 }
1273 self.last_cull = None; let last = self.cull.len() - 1;
1275 self.cull.swap_remove(index);
1276 (index != last).then_some(last)
1277 }
1278
1279 pub fn set_instance_colmul(&mut self, tables: &[[u64; 256]]) {
1285 self.any_colmul = true;
1288 self.last_cull = None;
1289 for (ci, t) in self.cull.iter_mut().zip(tables) {
1290 ci.colmul.copy_from_slice(t);
1291 }
1292 }
1293
1294 pub fn update_transforms(&mut self, instances: &[SpriteInstance]) {
1302 debug_assert_eq!(
1303 instances.len(),
1304 self.cull.len(),
1305 "update_transforms instance count must match upload"
1306 );
1307 self.last_cull = None; for (ci, inst) in self.cull.iter_mut().zip(instances) {
1309 ci.gpu.inv_rot0 = inst.transform.inv_rot[0];
1310 ci.gpu.inv_rot1 = inst.transform.inv_rot[1];
1311 ci.gpu.inv_rot2 = inst.transform.inv_rot[2];
1312 ci.gpu.pos = inst.transform.pos;
1313 ci.gpu.material = u32::from(inst.material);
1316 ci.gpu.alpha_mul = f32::from(inst.alpha_mul) / 255.0;
1317 ci.gpu.flags = inst.flags;
1320 ci.gpu.tint = inst.tint;
1321 ci.center = inst.transform.pos;
1325 ci.max_scale = inst.transform.max_scale;
1326 ci.radius = ci.model_radius * inst.transform.max_scale;
1327 }
1328 }
1329
1330 pub fn set_instance_model(
1345 &mut self,
1346 registry: &SpriteModelRegistry,
1347 idx: usize,
1348 chain_id: u32,
1349 ) {
1350 self.last_cull = None; let Some(model_radius) = registry
1355 .model_checked(chain_id)
1356 .map(SpriteModel::bound_radius)
1357 else {
1358 return;
1359 };
1360 let Some(ci) = self.cull.get_mut(idx) else {
1361 return;
1362 };
1363 ci.chain_id = chain_id;
1364 ci.gpu.model_id = chain_id; ci.model_radius = model_radius;
1366 ci.radius = model_radius * ci.max_scale;
1367 }
1368
1369 pub fn update_model(
1389 &mut self,
1390 device: &wgpu::Device,
1391 queue: &wgpu::Queue,
1392 registry: &SpriteModelRegistry,
1393 chain_id: u32,
1394 ) {
1395 self.last_cull = None; let entries = self.chains[chain_id as usize].clone();
1397 let mut grew = false;
1398 for &e in &entries {
1399 let e = e as usize;
1400 let m = ®istry.entries[e];
1401
1402 debug_assert_eq!(
1404 m.occupancy.len() as u32,
1405 self.occ_lens[e],
1406 "update_model: entry {e} occupancy length changed (dims grew?)"
1407 );
1408 debug_assert_eq!(
1409 m.color_offsets.len() as u32,
1410 self.coloff_lens[e],
1411 "update_model: entry {e} color_offsets length changed (dims grew?)"
1412 );
1413 queue.write_buffer(
1414 &self.occupancy,
1415 u64::from(self.meta[e].occupancy_offset) * 4,
1416 bytemuck::cast_slice(&m.occupancy),
1417 );
1418 queue.write_buffer(
1419 &self.color_offsets,
1420 u64::from(self.meta[e].color_offsets_offset) * 4,
1421 bytemuck::cast_slice(&m.color_offsets),
1422 );
1423
1424 let new_len = m.colors.len() as u32;
1426 match self.colors_alloc.place(e, new_len) {
1427 Some(off) => {
1428 queue.write_buffer(
1429 &self.colors,
1430 u64::from(off) * 4,
1431 bytemuck::cast_slice(&m.colors),
1432 );
1433 queue.write_buffer(
1434 &self.dirs,
1435 u64::from(off) * 4,
1436 bytemuck::cast_slice(&m.dirs),
1437 );
1438 let mats: Vec<u32> = m.materials.iter().map(|&x| u32::from(x)).collect();
1439 queue.write_buffer(
1440 &self.materials_vox,
1441 u64::from(off) * 4,
1442 bytemuck::cast_slice(&mats),
1443 );
1444 if self.meta[e].colors_offset != off {
1445 self.meta[e].colors_offset = off;
1447 queue.write_buffer(
1448 &self.model_meta,
1449 (e * std::mem::size_of::<SpriteModelMeta>()) as u64,
1450 bytemuck::bytes_of(&self.meta[e]),
1451 );
1452 }
1453 }
1454 None => grew = true,
1455 }
1456 }
1457
1458 if grew {
1461 self.grow_and_repack(device, queue, registry);
1462 }
1463 }
1464
1465 fn grow_and_repack(
1472 &mut self,
1473 device: &wgpu::Device,
1474 queue: &wgpu::Queue,
1475 registry: &SpriteModelRegistry,
1476 ) {
1477 self.repack_colors_dirs(device, registry);
1478 queue.write_buffer(&self.model_meta, 0, bytemuck::cast_slice(&self.meta));
1480 }
1481
1482 fn repack_colors_dirs(&mut self, device: &wgpu::Device, registry: &SpriteModelRegistry) {
1490 let new_lens: Vec<u32> = registry
1493 .entries
1494 .iter()
1495 .enumerate()
1496 .map(|(e, m)| {
1497 if self.dead[e] {
1498 0
1499 } else {
1500 m.colors.len() as u32
1501 }
1502 })
1503 .collect();
1504 self.colors_alloc.repack(&new_lens);
1505 let cap_total = self.colors_alloc.cap_total();
1506
1507 let mut all_colors = vec![0u32; cap_total as usize];
1508 let mut all_dirs = vec![0u32; cap_total as usize];
1509 let mut all_materials = vec![0u32; cap_total as usize];
1510 for (e, m) in registry.entries.iter().enumerate() {
1511 if self.dead[e] {
1512 self.meta[e].colors_offset = 0;
1513 continue;
1514 }
1515 let off = self.colors_alloc.slot(e).off as usize;
1516 all_colors[off..off + m.colors.len()].copy_from_slice(&m.colors);
1517 all_dirs[off..off + m.dirs.len()].copy_from_slice(&m.dirs);
1518 for (i, &mat) in m.materials.iter().enumerate() {
1519 all_materials[off + i] = u32::from(mat);
1520 }
1521 self.meta[e].colors_offset = off as u32;
1522 }
1523 self.colors = storage_dst_u32_cap(
1524 device,
1525 "roxlap-gpu sprite_reg.colors",
1526 &all_colors,
1527 cap_total,
1528 );
1529 self.dirs = storage_dst_u32_cap(device, "roxlap-gpu sprite_reg.dirs", &all_dirs, cap_total);
1530 self.materials_vox = storage_dst_u32_cap(
1531 device,
1532 "roxlap-gpu sprite_reg.materials_vox",
1533 &all_materials,
1534 cap_total,
1535 );
1536 eprintln!(
1537 "roxlap-gpu: sprite registry colors/dirs/materials grew + repacked to {cap_total} words"
1538 );
1539 }
1540
1541 pub fn add_model(
1559 &mut self,
1560 device: &wgpu::Device,
1561 queue: &wgpu::Queue,
1562 registry: &SpriteModelRegistry,
1563 chain_id: u32,
1564 ) {
1565 self.last_cull = None; let entries = registry.chains[chain_id as usize].clone();
1567 debug_assert_eq!(
1568 chain_id as usize,
1569 self.chains.len(),
1570 "add_model: chains must be appended in order"
1571 );
1572
1573 let mut need_colors_grow = false;
1577 for &e in &entries {
1578 let e = e as usize;
1579 debug_assert_eq!(
1580 e,
1581 self.meta.len(),
1582 "add_model: entries must be appended in order"
1583 );
1584 let m = ®istry.entries[e];
1585 let occ_off = self.occ_used;
1586 let coloff_off = self.coloff_used;
1587 self.occ_used += m.occupancy.len() as u32;
1588 self.coloff_used += m.color_offsets.len() as u32;
1589 let colors_off = match self.colors_alloc.push(m.colors.len() as u32) {
1590 Some(off) => off,
1591 None => {
1592 need_colors_grow = true;
1593 0 }
1595 };
1596 self.meta.push(SpriteModelMeta {
1597 occupancy_offset: occ_off,
1598 colors_offset: colors_off,
1599 color_offsets_offset: coloff_off,
1600 occ_words_per_col: m.occ_words_per_col,
1601 dims: m.dims,
1602 has_vox_materials: u32::from(!m.materials.is_empty()),
1603 pivot: m.pivot,
1604 voxel_world_size: m.voxel_world_size,
1605 });
1606 self.occ_lens.push(m.occupancy.len() as u32);
1607 self.coloff_lens.push(m.color_offsets.len() as u32);
1608 self.dead.push(false);
1609 }
1610 self.chains.push(entries.clone());
1611
1612 self.sync_concat(device, queue, registry, &entries, ConcatBuf::Occupancy);
1615 self.sync_concat(device, queue, registry, &entries, ConcatBuf::ColorOffsets);
1616
1617 if need_colors_grow {
1620 self.repack_colors_dirs(device, registry);
1621 } else {
1622 for &e in &entries {
1623 let e = e as usize;
1624 let m = ®istry.entries[e];
1625 let off = u64::from(self.meta[e].colors_offset) * 4;
1626 queue.write_buffer(&self.colors, off, bytemuck::cast_slice(&m.colors));
1627 queue.write_buffer(&self.dirs, off, bytemuck::cast_slice(&m.dirs));
1628 let mats: Vec<u32> = m.materials.iter().map(|&x| u32::from(x)).collect();
1629 queue.write_buffer(&self.materials_vox, off, bytemuck::cast_slice(&mats));
1630 }
1631 }
1632
1633 let count = self.meta.len() as u32;
1637 if count > self.meta_cap {
1638 self.meta_cap = grow_records(count);
1639 self.model_meta = storage_dst_pod_cap(
1640 device,
1641 "roxlap-gpu sprite_reg.model_meta",
1642 &self.meta,
1643 self.meta_cap,
1644 );
1645 } else {
1646 queue.write_buffer(&self.model_meta, 0, bytemuck::cast_slice(&self.meta));
1647 }
1648 }
1649
1650 fn sync_concat(
1656 &mut self,
1657 device: &wgpu::Device,
1658 queue: &wgpu::Queue,
1659 registry: &SpriteModelRegistry,
1660 new_entries: &[u32],
1661 which: ConcatBuf,
1662 ) {
1663 let (used, cap) = match which {
1664 ConcatBuf::Occupancy => (self.occ_used, self.occ_cap),
1665 ConcatBuf::ColorOffsets => (self.coloff_used, self.coloff_cap),
1666 };
1667 if used > cap {
1668 let new_cap = grow_words(used);
1669 let all: Vec<u32> = registry
1670 .entries
1671 .iter()
1672 .flat_map(|m| concat_data(m, which).iter().copied())
1673 .collect();
1674 let label = match which {
1675 ConcatBuf::Occupancy => "roxlap-gpu sprite_reg.occupancy",
1676 ConcatBuf::ColorOffsets => "roxlap-gpu sprite_reg.color_offsets",
1677 };
1678 let buf = storage_dst_u32_cap(device, label, &all, new_cap);
1679 match which {
1680 ConcatBuf::Occupancy => {
1681 self.occupancy = buf;
1682 self.occ_cap = new_cap;
1683 }
1684 ConcatBuf::ColorOffsets => {
1685 self.color_offsets = buf;
1686 self.coloff_cap = new_cap;
1687 }
1688 }
1689 } else {
1690 let target = match which {
1691 ConcatBuf::Occupancy => &self.occupancy,
1692 ConcatBuf::ColorOffsets => &self.color_offsets,
1693 };
1694 for &e in new_entries {
1695 let e = e as usize;
1696 let off = match which {
1697 ConcatBuf::Occupancy => self.meta[e].occupancy_offset,
1698 ConcatBuf::ColorOffsets => self.meta[e].color_offsets_offset,
1699 };
1700 queue.write_buffer(
1701 target,
1702 u64::from(off) * 4,
1703 bytemuck::cast_slice(concat_data(®istry.entries[e], which)),
1704 );
1705 }
1706 }
1707 }
1708
1709 #[must_use]
1714 pub fn dead_model_count(&self) -> usize {
1715 self.chains.iter().filter(|c| c.is_empty()).count()
1716 }
1717
1718 #[must_use]
1720 pub fn live_model_count(&self) -> usize {
1721 self.chains.iter().filter(|c| !c.is_empty()).count()
1722 }
1723
1724 pub fn remove_model(&mut self, chain_id: u32) {
1737 let Some(entries) = self.chains.get(chain_id as usize).cloned() else {
1738 return;
1739 };
1740 if entries.is_empty() {
1741 return; }
1743 self.last_cull = None; for &e in &entries {
1745 let e = e as usize;
1746 self.dead[e] = true;
1747 self.colors_alloc.free(e);
1748 }
1749 self.chains[chain_id as usize] = Vec::new(); }
1751
1752 pub fn compact(
1762 &mut self,
1763 device: &wgpu::Device,
1764 queue: &wgpu::Queue,
1765 registry: &SpriteModelRegistry,
1766 ) {
1767 self.last_cull = None; self.compact_concat(device, registry, ConcatBuf::Occupancy);
1771 self.compact_concat(device, registry, ConcatBuf::ColorOffsets);
1772 self.repack_colors_dirs(device, registry);
1774 queue.write_buffer(&self.model_meta, 0, bytemuck::cast_slice(&self.meta));
1777 }
1778
1779 fn compact_concat(
1784 &mut self,
1785 device: &wgpu::Device,
1786 registry: &SpriteModelRegistry,
1787 which: ConcatBuf,
1788 ) {
1789 let mut all: Vec<u32> = Vec::new();
1790 for e in 0..self.meta.len() {
1791 if self.dead[e] {
1792 match which {
1793 ConcatBuf::Occupancy => self.meta[e].occupancy_offset = 0,
1794 ConcatBuf::ColorOffsets => self.meta[e].color_offsets_offset = 0,
1795 }
1796 continue;
1797 }
1798 let off = all.len() as u32;
1799 match which {
1800 ConcatBuf::Occupancy => self.meta[e].occupancy_offset = off,
1801 ConcatBuf::ColorOffsets => self.meta[e].color_offsets_offset = off,
1802 }
1803 all.extend_from_slice(concat_data(®istry.entries[e], which));
1804 }
1805 let used = all.len() as u32;
1806 let cap = grow_words(used);
1807 let (label, buf) = match which {
1808 ConcatBuf::Occupancy => ("roxlap-gpu sprite_reg.occupancy", &mut self.occupancy),
1809 ConcatBuf::ColorOffsets => (
1810 "roxlap-gpu sprite_reg.color_offsets",
1811 &mut self.color_offsets,
1812 ),
1813 };
1814 *buf = storage_dst_u32_cap(device, label, &all, cap);
1815 match which {
1816 ConcatBuf::Occupancy => {
1817 self.occ_used = used;
1818 self.occ_cap = cap;
1819 }
1820 ConcatBuf::ColorOffsets => {
1821 self.coloff_used = used;
1822 self.coloff_cap = cap;
1823 }
1824 }
1825 }
1826
1827 #[allow(clippy::too_many_arguments)]
1835 pub fn cull_bin_upload(
1836 &mut self,
1837 device: &wgpu::Device,
1838 queue: &wgpu::Queue,
1839 f: &ViewFrustum,
1840 screen_w: u32,
1841 screen_h: u32,
1842 tile_size: u32,
1843 lod_px: f32,
1844 ) -> (u32, u32, u32) {
1845 let tiles_x = screen_w.div_ceil(tile_size).max(1);
1846 let tiles_y = screen_h.div_ceil(tile_size).max(1);
1847 let n_tiles = (tiles_x * tiles_y) as usize;
1848
1849 let key = CullKey::new(f, screen_w, screen_h, tile_size, lod_px);
1853 if let Some((k, res)) = self.last_cull {
1854 if k == key {
1855 return res;
1856 }
1857 }
1858
1859 let nw = (1.0 + f.half_w * f.half_w).sqrt();
1860 let nh = (1.0 + f.half_h * f.half_h).sqrt();
1861 let cx = screen_w as f32 * 0.5;
1862 let cy = screen_h as f32 * 0.5;
1863 let px_per_world = cx / f.half_w; let ts = tile_size as f32;
1865 let tx_max = tiles_x as i32 - 1;
1866 let ty_max = tiles_y as i32 - 1;
1867
1868 let scratch = &mut self.scratch;
1870 let visible = &mut scratch.visible;
1871 visible.clear();
1872 let boxes = &mut scratch.boxes;
1874 boxes.clear();
1875 let visible_colmul = &mut scratch.colmul;
1882 visible_colmul.clear();
1883 let counts = &mut scratch.counts;
1884 counts.clear();
1885 counts.resize(n_tiles, 0u32);
1886 let pack_colmul = self.any_colmul;
1887
1888 for ci in &self.cull {
1889 if self.chains[ci.chain_id as usize].is_empty() {
1893 continue;
1894 }
1895 let rel = [
1896 ci.center[0] - f.pos[0],
1897 ci.center[1] - f.pos[1],
1898 ci.center[2] - f.pos[2],
1899 ];
1900 let z = dot3(rel, f.forward);
1901 let r = ci.radius;
1902 if z + r < 0.0 || z - r > f.far {
1903 continue; }
1905 let x = dot3(rel, f.right);
1906 if (x - f.half_w * z) > r * nw || (-x - f.half_w * z) > r * nw {
1907 continue; }
1909 let y = dot3(rel, f.down);
1910 if (y - f.half_h * z) > r * nh || (-y - f.half_h * z) > r * nh {
1911 continue; }
1913
1914 let (tx0, tx1, ty0, ty1) = if z > 1e-3 {
1916 let sx = cx + (x / z) * px_per_world;
1917 let sy = cy + (y / z) * px_per_world;
1918 let sr = (r / z) * px_per_world;
1919 (
1920 (((sx - sr) / ts).floor() as i32).clamp(0, tx_max),
1921 (((sx + sr) / ts).floor() as i32).clamp(0, tx_max),
1922 (((sy - sr) / ts).floor() as i32).clamp(0, ty_max),
1923 (((sy + sr) / ts).floor() as i32).clamp(0, ty_max),
1924 )
1925 } else {
1926 (0, tx_max, 0, ty_max)
1927 };
1928 let chain = &self.chains[ci.chain_id as usize];
1935 let level = if z > 1e-3 && chain.len() > 1 {
1936 let voxel_px = px_per_world * ci.max_scale / z;
1940 ((lod_px / voxel_px).log2().ceil().max(0.0) as usize).min(chain.len() - 1)
1941 } else {
1942 0
1943 };
1944 let mut g = ci.gpu;
1945 g.model_id = chain[level];
1946 visible.push(g);
1947 boxes.push([tx0, tx1, ty0, ty1]);
1948 if pack_colmul {
1949 for &w in ci.colmul.iter() {
1950 visible_colmul.push((w & 0xffff_ffff) as u32);
1951 visible_colmul.push((w >> 32) as u32);
1952 }
1953 }
1954 for ty in ty0..=ty1 {
1955 for tx in tx0..=tx1 {
1956 counts[(ty * tiles_x as i32 + tx) as usize] += 1;
1957 }
1958 }
1959 }
1960
1961 if visible.is_empty() {
1962 let res = (0, tiles_x, tiles_y);
1963 self.last_cull = Some((key, res));
1964 return res;
1965 }
1966
1967 let tile_ranges = &mut scratch.tile_ranges;
1970 tile_ranges.clear();
1971 tile_ranges.resize(n_tiles * 2, 0u32);
1972 let mut running = 0u32;
1973 for t in 0..n_tiles {
1974 tile_ranges[2 * t] = running; tile_ranges[2 * t + 1] = counts[t]; running += counts[t];
1977 }
1978 let total = running as usize;
1979 let tile_instances = &mut scratch.tile_instances;
1980 tile_instances.clear();
1981 tile_instances.resize(total.max(1), 0u32);
1982 let cursor = &mut scratch.cursor;
1983 cursor.clear();
1984 cursor.extend((0..n_tiles).map(|t| tile_ranges[2 * t]));
1985 for (vis_idx, b) in boxes.iter().enumerate() {
1986 for ty in b[2]..=b[3] {
1987 for tx in b[0]..=b[1] {
1988 let t = (ty * tiles_x as i32 + tx) as usize;
1989 tile_instances[cursor[t] as usize] = vis_idx as u32;
1990 cursor[t] += 1;
1991 }
1992 }
1993 }
1994
1995 queue.write_buffer(&self.instances, 0, bytemuck::cast_slice(visible));
1999 let need_ranges = tile_ranges.len() as u32;
2000 if need_ranges > self.tile_ranges_cap {
2001 self.tile_ranges_cap = need_ranges.next_power_of_two();
2002 self.tile_ranges = storage_dst_u32(
2003 device,
2004 "roxlap-gpu sprite_reg.tile_ranges",
2005 self.tile_ranges_cap,
2006 );
2007 }
2008 let need_inst = tile_instances.len() as u32;
2009 if need_inst > self.tile_instances_cap {
2010 self.tile_instances_cap = need_inst.next_power_of_two();
2011 self.tile_instances = storage_dst_u32(
2012 device,
2013 "roxlap-gpu sprite_reg.tile_instances",
2014 self.tile_instances_cap,
2015 );
2016 }
2017 queue.write_buffer(&self.tile_ranges, 0, bytemuck::cast_slice(tile_ranges));
2018 queue.write_buffer(
2019 &self.tile_instances,
2020 0,
2021 bytemuck::cast_slice(tile_instances),
2022 );
2023 if pack_colmul {
2024 let need_colmul = visible_colmul.len() as u32;
2025 if need_colmul > self.colmul_cap {
2026 self.colmul_cap = need_colmul.next_power_of_two();
2027 self.colmul =
2028 storage_dst_u32(device, "roxlap-gpu sprite_reg.colmul", self.colmul_cap);
2029 self.colmul_identity = false;
2030 }
2031 queue.write_buffer(&self.colmul, 0, bytemuck::cast_slice(visible_colmul));
2032 } else {
2033 let need_colmul = visible.len() as u32 * 512;
2037 if need_colmul > self.colmul_cap {
2038 self.colmul_cap = need_colmul.next_power_of_two();
2039 self.colmul =
2040 storage_dst_u32(device, "roxlap-gpu sprite_reg.colmul", self.colmul_cap);
2041 self.colmul_identity = false;
2042 }
2043 if !self.colmul_identity {
2044 let w = identity_colmul()[0];
2045 let (lo, hi) = ((w & 0xffff_ffff) as u32, (w >> 32) as u32);
2046 let fill: Vec<u32> = (0..self.colmul_cap)
2047 .map(|i| if i & 1 == 0 { lo } else { hi })
2048 .collect();
2049 queue.write_buffer(&self.colmul, 0, bytemuck::cast_slice(&fill));
2050 self.colmul_identity = true;
2051 }
2052 }
2053
2054 let res = (visible.len() as u32, tiles_x, tiles_y);
2055 self.last_cull = Some((key, res));
2056 res
2057 }
2058}
2059
2060#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2066struct ColorSlot {
2067 off: u32,
2068 cap: u32,
2069 len: u32,
2070}
2071
2072#[derive(Debug, Default)]
2079struct ColorsAllocator {
2080 slots: Vec<ColorSlot>,
2082 free: Vec<(u32, u32)>,
2084 tail: u32,
2086 cap_total: u32,
2088}
2089
2090fn slot_cap(len: u32) -> u32 {
2093 len + len / 4 + 16
2094}
2095
2096fn grow_words(used: u32) -> u32 {
2100 used + used / 2 + 256
2101}
2102
2103fn grow_records(count: u32) -> u32 {
2105 count + count / 2 + 8
2106}
2107
2108impl ColorsAllocator {
2109 fn new(entry_lens: &[u32]) -> Self {
2113 let mut a = Self::default();
2114 a.repack(entry_lens);
2115 a
2116 }
2117
2118 fn slot(&self, entry: usize) -> ColorSlot {
2119 self.slots[entry]
2120 }
2121
2122 fn cap_total(&self) -> u32 {
2123 self.cap_total
2124 }
2125
2126 fn repack(&mut self, new_lens: &[u32]) {
2130 self.free.clear();
2131 let mut off = 0u32;
2132 let mut slots = Vec::with_capacity(new_lens.len());
2133 for &len in new_lens {
2134 let cap = if len == 0 { 0 } else { slot_cap(len) };
2137 slots.push(ColorSlot { off, cap, len });
2138 off += cap;
2139 }
2140 self.slots = slots;
2141 self.tail = off;
2142 self.cap_total = off + off / 2 + 256;
2144 }
2145
2146 fn place(&mut self, entry: usize, new_len: u32) -> Option<u32> {
2151 let cur = self.slots[entry];
2152 if new_len <= cur.cap {
2153 self.slots[entry] = ColorSlot {
2154 len: new_len,
2155 ..cur
2156 };
2157 return Some(cur.off);
2158 }
2159 let old = (cur.off, cur.cap);
2160 if let Some(i) = self.free.iter().position(|&(_, c)| c >= new_len) {
2162 let (off, cap) = self.free.remove(i);
2163 self.free.push(old);
2164 self.slots[entry] = ColorSlot {
2165 off,
2166 cap,
2167 len: new_len,
2168 };
2169 return Some(off);
2170 }
2171 let want = slot_cap(new_len);
2173 if self.tail + want <= self.cap_total {
2174 let off = self.tail;
2175 self.tail += want;
2176 self.free.push(old);
2177 self.slots[entry] = ColorSlot {
2178 off,
2179 cap: want,
2180 len: new_len,
2181 };
2182 return Some(off);
2183 }
2184 None
2185 }
2186
2187 fn push(&mut self, new_len: u32) -> Option<u32> {
2193 if let Some(i) = self.free.iter().position(|&(_, c)| c >= new_len) {
2194 let (off, cap) = self.free.remove(i);
2195 self.slots.push(ColorSlot {
2196 off,
2197 cap,
2198 len: new_len,
2199 });
2200 return Some(off);
2201 }
2202 let want = slot_cap(new_len);
2203 if self.tail + want <= self.cap_total {
2204 let off = self.tail;
2205 self.tail += want;
2206 self.slots.push(ColorSlot {
2207 off,
2208 cap: want,
2209 len: new_len,
2210 });
2211 return Some(off);
2212 }
2213 None
2214 }
2215
2216 fn free(&mut self, entry: usize) {
2221 let s = self.slots[entry];
2222 if s.cap > 0 {
2223 self.free.push((s.off, s.cap));
2224 }
2225 self.slots[entry] = ColorSlot {
2226 off: 0,
2227 cap: 0,
2228 len: 0,
2229 };
2230 }
2231}
2232
2233#[allow(dead_code)]
2236fn storage_u32(device: &wgpu::Device, label: &str, data: &[u32]) -> wgpu::Buffer {
2237 use wgpu::util::DeviceExt;
2238 let bytes: &[u8] = if data.is_empty() {
2239 bytemuck::cast_slice(&[0u32])
2240 } else {
2241 bytemuck::cast_slice(data)
2242 };
2243 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2244 label: Some(label),
2245 contents: bytes,
2246 usage: wgpu::BufferUsages::STORAGE,
2247 })
2248}
2249
2250fn storage_dst_u32(device: &wgpu::Device, label: &str, cap: u32) -> wgpu::Buffer {
2253 device.create_buffer(&wgpu::BufferDescriptor {
2254 label: Some(label),
2255 size: u64::from(cap.max(1)) * 4,
2256 usage: wgpu::BufferUsages::STORAGE
2259 | wgpu::BufferUsages::COPY_DST
2260 | wgpu::BufferUsages::COPY_SRC,
2261 mapped_at_creation: false,
2262 })
2263}
2264
2265fn storage_dst_u32_cap(device: &wgpu::Device, label: &str, data: &[u32], cap: u32) -> wgpu::Buffer {
2273 let cap = cap.max(data.len() as u32).max(1);
2274 let buf = device.create_buffer(&wgpu::BufferDescriptor {
2275 label: Some(label),
2276 size: u64::from(cap) * 4,
2277 usage: wgpu::BufferUsages::STORAGE
2278 | wgpu::BufferUsages::COPY_DST
2279 | wgpu::BufferUsages::COPY_SRC,
2280 mapped_at_creation: true,
2281 });
2282 if !data.is_empty() {
2283 buf.slice(..(data.len() as u64 * 4))
2284 .get_mapped_range_mut()
2285 .copy_from_slice(bytemuck::cast_slice(data));
2286 }
2287 buf.unmap();
2288 buf
2289}
2290
2291fn storage_dst_pod<T: Pod + Zeroable>(
2297 device: &wgpu::Device,
2298 label: &str,
2299 data: &[T],
2300) -> wgpu::Buffer {
2301 let one = [T::zeroed()];
2302 let src: &[T] = if data.is_empty() { &one } else { data };
2303 let buf = device.create_buffer(&wgpu::BufferDescriptor {
2304 label: Some(label),
2305 size: std::mem::size_of_val(src) as u64,
2306 usage: wgpu::BufferUsages::STORAGE
2307 | wgpu::BufferUsages::COPY_DST
2308 | wgpu::BufferUsages::COPY_SRC,
2309 mapped_at_creation: true,
2310 });
2311 buf.slice(..)
2312 .get_mapped_range_mut()
2313 .copy_from_slice(bytemuck::cast_slice(src));
2314 buf.unmap();
2315 buf
2316}
2317
2318fn storage_dst_pod_cap<T: Pod + Zeroable>(
2323 device: &wgpu::Device,
2324 label: &str,
2325 data: &[T],
2326 cap: u32,
2327) -> wgpu::Buffer {
2328 let rec = std::mem::size_of::<T>() as u64;
2329 let cap = u64::from(cap.max(data.len() as u32).max(1));
2330 let buf = device.create_buffer(&wgpu::BufferDescriptor {
2331 label: Some(label),
2332 size: cap * rec,
2333 usage: wgpu::BufferUsages::STORAGE
2334 | wgpu::BufferUsages::COPY_DST
2335 | wgpu::BufferUsages::COPY_SRC,
2336 mapped_at_creation: true,
2337 });
2338 if !data.is_empty() {
2339 buf.slice(..(data.len() as u64 * rec))
2340 .get_mapped_range_mut()
2341 .copy_from_slice(bytemuck::cast_slice(data));
2342 }
2343 buf.unmap();
2344 buf
2345}
2346
2347#[allow(dead_code)]
2350fn storage_pod<T: Pod + Zeroable>(device: &wgpu::Device, label: &str, data: &[T]) -> wgpu::Buffer {
2351 use wgpu::util::DeviceExt;
2352 let one = [T::zeroed()];
2353 let src: &[T] = if data.is_empty() { &one } else { data };
2354 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2355 label: Some(label),
2356 contents: bytemuck::cast_slice(src),
2357 usage: wgpu::BufferUsages::STORAGE,
2358 })
2359}
2360
2361#[cfg(test)]
2362mod tests {
2363 use super::*;
2364 use roxlap_formats::kv6::{Kv6, Voxel};
2365
2366 fn kv6_unsorted() -> Kv6 {
2369 let mk = |z, col| Voxel {
2370 col,
2371 z,
2372 vis: 0,
2373 dir: 0,
2374 };
2375 Kv6 {
2376 xsiz: 2,
2377 ysiz: 1,
2378 zsiz: 8,
2379 xpiv: 0.0,
2380 ypiv: 0.0,
2381 zpiv: 0.0,
2382 voxels: vec![mk(5, 0xAA), mk(1, 0xBB), mk(3, 0xCC)],
2383 xlen: vec![2, 1],
2384 ylen: vec![vec![2], vec![1]],
2385 palette: None,
2386 }
2387 }
2388
2389 #[test]
2390 fn occupancy_bits_set_at_voxel_z() {
2391 let m = build_sprite_model(&kv6_unsorted());
2392 assert_eq!(m.dims, [2, 1, 8]);
2393 assert_eq!(m.occ_words_per_col, 1); assert_eq!(m.occupancy[0], (1 << 1) | (1 << 5));
2396 assert_eq!(m.occupancy[1], 1 << 3);
2397 }
2398
2399 #[test]
2400 fn colors_are_ascending_z_for_rank_lookup() {
2401 let m = build_sprite_model(&kv6_unsorted());
2402 assert_eq!(m.color_offsets, vec![0, 2, 3]);
2404 assert_eq!(&m.colors, &[0xBB, 0xAA, 0xCC]);
2405 }
2406
2407 #[test]
2408 fn identity_basis_inverts_to_identity() {
2409 let inv = mat3_inverse([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
2410 assert_eq!(inv, [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
2411 }
2412
2413 #[test]
2414 fn fork_is_independent_of_parent() {
2415 let mut reg = SpriteModelRegistry::new();
2416 let base = reg.add(build_sprite_model(&kv6_unsorted()));
2417 let forked = reg.fork(base);
2418 assert_ne!(base, forked);
2419 reg.model_mut(forked).recolor(|_| 0x11);
2421 assert_eq!(®.model(base).colors, &[0xBB, 0xAA, 0xCC]);
2423 assert_eq!(®.model(forked).colors, &[0x11, 0x11, 0x11]);
2424 }
2425
2426 #[test]
2427 fn remove_frees_chain_data_keeps_ids_stable() {
2428 let mut reg = SpriteModelRegistry::new();
2429 let a = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2430 let b = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2431 let len_before = reg.len();
2432 assert!(reg.is_live(a) && reg.is_live(b));
2433
2434 reg.remove(a);
2435 assert!(!reg.is_live(a));
2438 assert!(reg.is_live(b));
2440 assert_eq!(®.model(b).colors, &[0xBB, 0xAA, 0xCC]);
2441 assert_eq!(reg.len(), len_before);
2442
2443 let c = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2445 assert_eq!(c, len_before as u32);
2446 assert!(reg.is_live(c));
2447 assert_eq!(®.model(b).colors, &[0xBB, 0xAA, 0xCC]);
2449 }
2450
2451 #[test]
2452 fn model_checked_guards_out_of_range_and_tombstoned() {
2453 let mut reg = SpriteModelRegistry::new();
2456 let a = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2457 assert!(reg.model_checked(a).is_some());
2458 assert!(reg.model_checked(9999).is_none(), "out of range → None");
2459 reg.remove(a);
2460 assert!(reg.model_checked(a).is_none(), "tombstoned chain → None");
2461 }
2462
2463 #[test]
2464 fn remove_is_idempotent_and_bounds_safe() {
2465 let mut reg = SpriteModelRegistry::new();
2466 let a = reg.add(build_sprite_model(&kv6_unsorted()));
2467 reg.remove(a);
2468 reg.remove(a); reg.remove(999); assert!(!reg.is_live(a));
2471 assert!(!reg.is_live(999));
2472 }
2473
2474 #[test]
2475 fn registry_gpu_structs_have_expected_sizes() {
2476 assert_eq!(std::mem::size_of::<SpriteModelMeta>(), 48);
2477 assert_eq!(std::mem::size_of::<SpriteInstanceGpu>(), 80);
2480 }
2481
2482 #[test]
2483 fn add_lod_builds_halving_mip_chain() {
2484 let mut reg = SpriteModelRegistry::new();
2485 let id = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2488 let m0 = reg.model(id);
2489 assert_eq!(m0.dims, [2, 1, 8]);
2490 assert!((m0.voxel_world_size - 1.0).abs() < 1e-6);
2491 }
2492
2493 fn kv6_from(xsiz: u32, ysiz: u32, zsiz: u32, voxels: &[(u32, u32, u16, u32)]) -> Kv6 {
2496 let mut ylen = vec![vec![0u16; ysiz as usize]; xsiz as usize];
2497 let mut flat = Vec::new();
2498 for x in 0..xsiz {
2499 for y in 0..ysiz {
2500 let mut col: Vec<(u16, u32)> = voxels
2501 .iter()
2502 .filter(|(vx, vy, _, _)| *vx == x && *vy == y)
2503 .map(|(_, _, z, c)| (*z, *c))
2504 .collect();
2505 col.sort_by_key(|(z, _)| *z);
2506 ylen[x as usize][y as usize] = col.len() as u16;
2507 for (z, c) in col {
2508 flat.push(Voxel {
2509 col: c,
2510 z,
2511 vis: 0,
2512 dir: 0,
2513 });
2514 }
2515 }
2516 }
2517 let xlen = ylen
2518 .iter()
2519 .map(|c| c.iter().map(|&v| u32::from(v)).sum())
2520 .collect();
2521 Kv6 {
2522 xsiz,
2523 ysiz,
2524 zsiz,
2525 xpiv: 0.0,
2526 ypiv: 0.0,
2527 zpiv: 0.0,
2528 voxels: flat,
2529 xlen,
2530 ylen,
2531 palette: None,
2532 }
2533 }
2534
2535 fn offsets_consistent(m: &SpriteModel) -> bool {
2536 let cols = (m.dims[0] * m.dims[1]) as usize;
2537 if m.color_offsets.len() != cols + 1 {
2538 return false;
2539 }
2540 for w in m.color_offsets.windows(2) {
2543 if w[1] < w[0] {
2544 return false;
2545 }
2546 }
2547 m.color_offsets[cols] as usize == m.colors.len()
2548 }
2549
2550 #[test]
2551 fn carve_two_layers_keeps_offsets_consistent() {
2552 let kv6 = kv6_from(
2555 3,
2556 2,
2557 8,
2558 &[
2559 (0, 0, 0, 0xA0),
2560 (0, 0, 1, 0xA1),
2561 (0, 0, 5, 0xA5),
2562 (1, 0, 1, 0xB1),
2563 (2, 1, 0, 0xC0),
2564 (2, 1, 3, 0xC3),
2565 ],
2566 );
2567 let mut m = build_sprite_model(&kv6);
2568 assert!(offsets_consistent(&m));
2569 for z in 0..2u32 {
2570 for y in 0..m.dims[1] {
2571 for x in 0..m.dims[0] {
2572 m.set_voxel(x, y, z, None);
2573 }
2574 }
2575 assert!(offsets_consistent(&m), "inconsistent after carving z={z}");
2576 let _ = m.downsample();
2578 }
2579 }
2580
2581 #[test]
2582 fn set_voxel_inserts_replaces_and_clears() {
2583 let mut m = build_sprite_model(&kv6_unsorted());
2585
2586 assert!(m.set_voxel(0, 0, 3, Some(0x55)));
2588 assert_eq!(m.occupancy[0], (1 << 1) | (1 << 3) | (1 << 5));
2589 assert_eq!(m.color_offsets, vec![0, 3, 4]);
2591 assert_eq!(&m.colors, &[0xBB, 0x55, 0xAA, 0xCC]);
2592
2593 assert!(m.set_voxel(0, 0, 3, Some(0x66)));
2595 assert_eq!(&m.colors, &[0xBB, 0x66, 0xAA, 0xCC]);
2596 assert_eq!(m.color_offsets, vec![0, 3, 4]);
2597
2598 assert!(m.set_voxel(0, 0, 1, None));
2600 assert_eq!(m.occupancy[0], (1 << 3) | (1 << 5));
2601 assert_eq!(m.color_offsets, vec![0, 2, 3]);
2602 assert_eq!(&m.colors, &[0x66, 0xAA, 0xCC]);
2603
2604 assert!(!m.set_voxel(0, 0, 2, None));
2606 assert!(!m.set_voxel(9, 0, 0, Some(1)));
2607 }
2608
2609 #[test]
2610 fn rebuild_lod_refreshes_coarse_levels_from_mip0() {
2611 let mut reg = SpriteModelRegistry::new();
2612 let id = reg.add_lod(build_sprite_model(&kv6_unsorted()), 3);
2613 reg.model_mut(id).recolor(|_| 0x0000_2000);
2615 reg.rebuild_lod(id);
2616 let lvl1_entry = reg.chains[id as usize][1] as usize;
2618 assert!(reg.entries[lvl1_entry]
2619 .colors
2620 .iter()
2621 .all(|&c| c == 0x0000_2000));
2622 }
2623
2624 fn alloc_invariants(a: &ColorsAllocator, lens: &[u32]) {
2629 let mut prev_end = 0u32;
2630 for (e, &len) in lens.iter().enumerate() {
2631 let s = a.slot(e);
2632 assert_eq!(s.len, len, "slot {e} len");
2633 assert!(s.cap >= s.len, "slot {e} cap >= len");
2634 assert!(s.off >= prev_end, "slot {e} overlaps previous");
2636 assert!(s.off + s.cap <= a.cap_total(), "slot {e} past cap_total");
2637 prev_end = s.off + s.cap;
2638 }
2639 assert!(a.cap_total() >= prev_end, "tail headroom");
2640 }
2641
2642 #[test]
2643 fn allocator_new_lays_out_with_slack_and_headroom() {
2644 let lens = [10u32, 0, 64, 7];
2645 let a = ColorsAllocator::new(&lens);
2646 alloc_invariants(&a, &lens);
2647 assert!(a.slot(2).cap > 64);
2649 assert!(a.cap_total() > a.slot(3).off + a.slot(3).cap);
2651 }
2652
2653 #[test]
2654 fn allocator_place_in_place_when_within_cap() {
2655 let mut a = ColorsAllocator::new(&[10, 20]);
2656 let off0 = a.slot(0).off;
2657 let cap0 = a.slot(0).cap;
2658 assert_eq!(a.place(0, 5), Some(off0));
2660 assert_eq!(a.slot(0).len, 5);
2661 assert_eq!(a.slot(0).cap, cap0);
2662 assert_eq!(a.place(0, cap0), Some(off0));
2664 assert_eq!(a.slot(0).off, off0);
2665 assert!(a.free.is_empty(), "no relocation should free anything");
2666 }
2667
2668 #[test]
2669 fn allocator_place_relocates_to_tail_and_frees_old() {
2670 let mut a = ColorsAllocator::new(&[10, 20]);
2671 let old0 = (a.slot(0).off, a.slot(0).cap);
2672 let tail_before = a.tail;
2673 let new_len = a.slot(0).cap + 5;
2675 let off = a.place(0, new_len).expect("fits in headroom");
2676 assert_eq!(off, tail_before, "relocated to old tail");
2677 assert_eq!(a.slot(0).off, off);
2678 assert_eq!(a.slot(0).len, new_len);
2679 assert!(a.free.contains(&old0), "old slot freed");
2680 }
2681
2682 #[test]
2683 fn allocator_reuses_freed_block_first_fit() {
2684 let mut a = ColorsAllocator::new(&[10, 2]);
2687 let old0 = (a.slot(0).off, a.slot(0).cap);
2688 let _ = a.place(0, a.slot(0).cap + 5).unwrap();
2690 assert!(a.free.contains(&old0));
2691 let new1 = a.slot(1).cap + 1;
2694 assert!(new1 <= old0.1, "freed block big enough");
2695 let off = a.place(1, new1).expect("reuses freed block");
2696 assert_eq!(off, old0.0, "first-fit reused the freed slot offset");
2697 assert!(!a.free.contains(&old0), "freed block consumed");
2698 }
2699
2700 #[test]
2701 fn allocator_signals_grow_then_repack_restores() {
2702 let mut a = ColorsAllocator::new(&[8, 8]);
2703 let huge = a.cap_total() + 100;
2705 assert_eq!(a.place(0, huge), None, "overflow must signal grow");
2706 a.repack(&[huge, 8]);
2708 alloc_invariants(&a, &[huge, 8]);
2709 assert!(a.cap_total() > huge);
2710 assert_eq!(a.place(0, huge), Some(a.slot(0).off));
2712 }
2713
2714 #[test]
2721 fn allocator_carve_loop_keeps_live_windows_disjoint() {
2722 let mut a = ColorsAllocator::new(&[40, 12, 40]);
2723 let mut lens = [40u32, 12, 40];
2724 let walk = [13u32, 30, 60, 18, 9, 80, 80, 25, 200, 7];
2727 let mut grew = false;
2728 for &len in &walk {
2729 lens[1] = len;
2730 if a.place(1, len).is_none() {
2732 grew = true;
2733 a.repack(&lens);
2734 } else {
2735 assert_eq!(a.place(0, 40), Some(a.slot(0).off));
2737 assert_eq!(a.place(2, 40), Some(a.slot(2).off));
2738 }
2739 assert_eq!(a.slot(1).len, len);
2740
2741 let mut wins: Vec<(u32, u32)> =
2743 (0..3).map(|e| (a.slot(e).off, a.slot(e).len)).collect();
2744 wins.sort_by_key(|w| w.0);
2745 for pair in wins.windows(2) {
2746 let (o0, l0) = pair[0];
2747 let (o1, _) = pair[1];
2748 assert!(o0 + l0 <= o1, "live windows overlap: {pair:?}");
2749 }
2750 }
2751 assert!(grew, "the 200-word jump should have forced a repack");
2752 }
2753
2754 fn headless() -> Option<crate::HeadlessGpu> {
2757 match crate::HeadlessGpu::new_blocking(crate::GpuRendererSettings::default()) {
2758 Ok(h) => Some(h),
2759 Err(e) => {
2760 eprintln!("[skip] no GPU adapter reachable: {e}");
2761 None
2762 }
2763 }
2764 }
2765
2766 fn one_model_registry() -> (SpriteModelRegistry, u32) {
2767 let mut reg = SpriteModelRegistry::new();
2768 let id = reg.add(build_sprite_model(&kv6_unsorted()));
2769 (reg, id)
2770 }
2771
2772 fn inst(model_id: u32, pos: [f32; 3]) -> SpriteInstance {
2773 use roxlap_formats::sprite::Sprite;
2774 SpriteInstance::new(
2775 model_id,
2776 SpriteInstanceTransform::from_sprite(&Sprite::axis_aligned(kv6_unsorted(), pos)),
2777 )
2778 }
2779
2780 #[test]
2785 fn scaled_basis_scales_cull_radius() {
2786 use roxlap_formats::sprite::Sprite;
2787
2788 let mut reg = SpriteModelRegistry::new();
2789 let chain = reg.add(build_sprite_model(&kv6_unsorted()));
2790 let model_r = reg.model(chain).bound_radius();
2791
2792 let scaled = |k: f32, pos: [f32; 3]| {
2793 let mut s = Sprite::axis_aligned(kv6_unsorted(), pos);
2794 for a in 0..3 {
2795 s.s[a] *= k;
2796 s.h[a] *= k;
2797 s.f[a] *= k;
2798 }
2799 SpriteInstanceTransform::from_sprite(&s)
2800 };
2801
2802 let unit = inst(chain, [0.0; 3]);
2804 assert_eq!(unit.transform.max_scale, 1.0);
2805 assert_eq!(make_cull(®, &unit).radius, model_r);
2806
2807 let xf2 = scaled(2.0, [0.0; 3]);
2809 assert!((xf2.max_scale - 2.0).abs() < 1e-6);
2810 let big = SpriteInstance::new(chain, xf2);
2811 assert!((make_cull(®, &big).radius - 2.0 * model_r).abs() < 1e-4);
2812
2813 let mut s = Sprite::axis_aligned(kv6_unsorted(), [0.0; 3]);
2815 for a in 0..3 {
2816 s.h[a] *= 3.0;
2817 s.f[a] *= 0.5;
2818 }
2819 let xf = SpriteInstanceTransform::from_sprite(&s);
2820 assert!((xf.max_scale - 3.0).abs() < 1e-6);
2821 }
2822
2823 #[test]
2824 fn append_grows_count_and_capacity_pow2() {
2825 let Some(h) = headless() else { return };
2826 let (reg, m) = one_model_registry();
2827 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(m, [0.0; 3])]);
2828 assert_eq!(res.instance_count(), 1);
2829 assert_eq!(res.instance_capacity, 1);
2830
2831 let more: Vec<_> = (1..=4).map(|i| inst(m, [i as f32, 0.0, 0.0])).collect();
2833 let base = res.append_instances(&h.device, ®, &more);
2834 assert_eq!(base, 1, "first appended index follows the seed instance");
2835 assert_eq!(res.instance_count(), 5);
2836 assert_eq!(res.instance_capacity, 8, "power-of-two growth");
2837
2838 let base2 = res.append_instances(&h.device, ®, &[inst(m, [9.0, 0.0, 0.0])]);
2840 assert_eq!(base2, 5);
2841 assert_eq!(res.instance_count(), 6);
2842 assert_eq!(res.instance_capacity, 8, "fits existing capacity, no grow");
2843 }
2844
2845 #[test]
2846 fn append_empty_is_noop() {
2847 let Some(h) = headless() else { return };
2848 let (reg, m) = one_model_registry();
2849 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(m, [0.0; 3])]);
2850 let base = res.append_instances(&h.device, ®, &[]);
2851 assert_eq!(base, 1);
2852 assert_eq!(res.instance_count(), 1);
2853 assert_eq!(res.instance_capacity, 1);
2854 }
2855
2856 fn read_u32(h: &crate::HeadlessGpu, buf: &wgpu::Buffer, words: u64) -> Vec<u32> {
2858 let bytes = words * 4;
2859 let staging = h.device.create_buffer(&wgpu::BufferDescriptor {
2860 label: Some("readback"),
2861 size: bytes,
2862 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2863 mapped_at_creation: false,
2864 });
2865 let mut enc = h
2866 .device
2867 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
2868 enc.copy_buffer_to_buffer(buf, 0, &staging, 0, bytes);
2869 h.queue.submit(std::iter::once(enc.finish()));
2870 let slice = staging.slice(..);
2871 let (tx, rx) = std::sync::mpsc::channel();
2872 slice.map_async(wgpu::MapMode::Read, move |r| tx.send(r).unwrap());
2873 h.device.poll(wgpu::PollType::wait_indefinitely()).ok();
2874 rx.recv().unwrap().unwrap();
2875 let data = slice.get_mapped_range();
2876 let out = bytemuck::cast_slice::<u8, u32>(&data).to_vec();
2877 drop(data);
2878 staging.unmap();
2879 out
2880 }
2881
2882 fn kv6_other() -> Kv6 {
2885 let mk = |z, col| Voxel {
2886 col,
2887 z,
2888 vis: 0,
2889 dir: 0,
2890 };
2891 Kv6 {
2892 xsiz: 1,
2893 ysiz: 1,
2894 zsiz: 4,
2895 xpiv: 0.0,
2896 ypiv: 0.0,
2897 zpiv: 0.0,
2898 voxels: vec![mk(0, 0x11), mk(2, 0x22)],
2899 xlen: vec![2],
2900 ylen: vec![vec![2]],
2901 palette: None,
2902 }
2903 }
2904
2905 #[test]
2909 fn add_model_uploads_new_volume_incrementally() {
2910 let Some(h) = headless() else { return };
2911
2912 let mut reg = SpriteModelRegistry::new();
2914 let a = reg.add(build_sprite_model(&kv6_unsorted()));
2915 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(a, [0.0; 3])]);
2916 assert_eq!(res.chains.len(), 1);
2917 let entries_before = res.meta.len();
2918
2919 let b = reg.add(build_sprite_model(&kv6_other()));
2921 res.add_model(&h.device, &h.queue, ®, b);
2922 assert_eq!(res.chains.len(), 2);
2923 assert_eq!(res.meta.len(), entries_before + 1, "one new entry");
2924
2925 let occ = read_u32(&h, &res.occupancy, u64::from(res.occ_cap));
2929 let coloff = read_u32(&h, &res.color_offsets, u64::from(res.coloff_cap));
2930 let cols = read_u32(&h, &res.colors, u64::from(res.colors_alloc.cap_total()));
2931 for (e, m) in reg.entries.iter().enumerate() {
2932 let meta = res.meta[e];
2933 let oo = meta.occupancy_offset as usize;
2934 assert_eq!(
2935 &occ[oo..oo + m.occupancy.len()],
2936 &m.occupancy[..],
2937 "occ entry {e}"
2938 );
2939 let co = meta.color_offsets_offset as usize;
2940 assert_eq!(
2941 &coloff[co..co + m.color_offsets.len()],
2942 &m.color_offsets[..],
2943 "color_offsets entry {e}"
2944 );
2945 let cc = meta.colors_offset as usize;
2946 assert_eq!(
2947 &cols[cc..cc + m.colors.len()],
2948 &m.colors[..],
2949 "colors entry {e}"
2950 );
2951 }
2952
2953 let base = res.append_instances(&h.device, ®, &[inst(b, [5.0, 0.0, 0.0])]);
2955 assert_eq!(base, 1);
2956 assert_eq!(res.instance_count(), 2);
2957 }
2958
2959 #[test]
2963 fn add_model_survives_buffer_growth() {
2964 let Some(h) = headless() else { return };
2965 let mut reg = SpriteModelRegistry::new();
2966 let a = reg.add(build_sprite_model(&kv6_unsorted()));
2967 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(a, [0.0; 3])]);
2968 let occ_cap0 = res.occ_cap;
2969
2970 for _ in 0..40 {
2973 let id = reg.add(build_sprite_model(&kv6_other()));
2974 res.add_model(&h.device, &h.queue, ®, id);
2975 }
2976 assert_eq!(res.chains.len(), 41);
2977 assert!(res.occ_cap > occ_cap0, "occupancy buffer grew");
2978
2979 let occ = read_u32(&h, &res.occupancy, u64::from(res.occ_cap));
2980 let cols = read_u32(&h, &res.colors, u64::from(res.colors_alloc.cap_total()));
2981 for (e, m) in reg.entries.iter().enumerate() {
2982 let meta = res.meta[e];
2983 let oo = meta.occupancy_offset as usize;
2984 assert_eq!(
2985 &occ[oo..oo + m.occupancy.len()],
2986 &m.occupancy[..],
2987 "occ entry {e}"
2988 );
2989 let cc = meta.colors_offset as usize;
2990 assert_eq!(
2991 &cols[cc..cc + m.colors.len()],
2992 &m.colors[..],
2993 "colors entry {e}"
2994 );
2995 }
2996 }
2997
2998 #[test]
3007 fn clip_frame_with_materials_classifies_by_color() {
3008 use roxlap_formats::voxel_clip::{LoopMode, VoxelClip, VoxelFrame};
3009
3010 let dims = [1u32, 1, 4];
3011 let owpc = dims[2].div_ceil(32).max(1) as usize; let glass = 0x80AA_BBCC;
3013 let stone = 0x8011_2233;
3014 let frame = VoxelFrame {
3015 occupancy: {
3016 let mut occ = vec![0u32; owpc];
3017 occ[0] |= (1 << 0) | (1 << 1);
3018 occ
3019 },
3020 colors: vec![stone, glass], color_offsets: vec![0, 2],
3022 };
3023 let clip = VoxelClip::from_frames(
3024 dims,
3025 [0.5, 0.5, 2.0],
3026 1.0,
3027 LoopMode::Loop,
3028 &[frame],
3029 &[],
3030 33,
3031 0,
3032 );
3033 let decoded = clip.decode().expect("decode");
3034
3035 let m = sprite_model_from_clip_frame_with_materials(&decoded, 0, &[(0x00AA_BBCC, 2)]);
3037 assert_eq!(
3038 m.materials.len(),
3039 m.colors.len(),
3040 "materials parallel to colors"
3041 );
3042 assert_eq!(
3044 m.materials,
3045 vec![0u8, 2u8],
3046 "stone opaque, glass material 2"
3047 );
3048
3049 let plain = sprite_model_from_clip_frame(&decoded, 0);
3051 let plain_mat = sprite_model_from_clip_frame_with_materials(&decoded, 0, &[]);
3052 assert!(plain.materials.is_empty());
3053 assert!(plain_mat.materials.is_empty());
3054 assert_eq!(plain.colors, plain_mat.colors);
3055 }
3056
3057 #[test]
3062 fn build_with_materials_classifies_by_color() {
3063 let glass = 0x80AA_BBCC;
3064 let stone = 0x8011_2233;
3065 let kv6 = kv6_from(1, 1, 4, &[(0, 0, 0, stone), (0, 0, 1, glass)]);
3067
3068 let m = build_sprite_model_with_materials(&kv6, &[(0x00AA_BBCC, 2)]);
3069 assert_eq!(
3070 m.materials.len(),
3071 m.colors.len(),
3072 "materials parallel to colors"
3073 );
3074 assert_eq!(
3075 m.materials,
3076 vec![0u8, 2u8],
3077 "stone opaque, glass material 2"
3078 );
3079
3080 let plain = build_sprite_model(&kv6);
3082 let plain_mat = build_sprite_model_with_materials(&kv6, &[]);
3083 assert!(plain.materials.is_empty());
3084 assert!(plain_mat.materials.is_empty());
3085 assert_eq!(plain.colors, plain_mat.colors);
3086 }
3087
3088 #[test]
3091 fn voxel_clip_flipbook_set_instance_model() {
3092 use roxlap_formats::voxel_clip::{LoopMode, VoxelClip, VoxelFrame};
3093 let Some(h) = headless() else { return };
3094
3095 let dims = [1u32, 1, 4];
3098 let owpc = dims[2].div_ceil(32).max(1) as usize; let mk_frame = |zs: &[u32], cols: &[u32]| -> VoxelFrame {
3100 let mut occ = vec![0u32; owpc];
3101 for &z in zs {
3102 occ[(z >> 5) as usize] |= 1u32 << (z & 31);
3103 }
3104 VoxelFrame {
3105 occupancy: occ,
3106 colors: cols.to_vec(),
3107 color_offsets: vec![0, cols.len() as u32],
3108 }
3109 };
3110 let f0 = mk_frame(&[0], &[0x8011_2233]);
3111 let f1 = mk_frame(&[0, 1], &[0x8011_2233, 0x80AA_BBCC]);
3112 let clip = VoxelClip::from_frames(
3113 dims,
3114 [0.5, 0.5, 2.0],
3115 1.0,
3116 LoopMode::Loop,
3117 &[f0, f1],
3118 &[],
3119 33,
3120 0,
3121 );
3122 let decoded = clip.decode().expect("decode");
3123
3124 let mut reg = SpriteModelRegistry::new();
3126 let c0 = reg.add(sprite_model_from_clip_frame(&decoded, 0));
3127 let c1 = reg.add(sprite_model_from_clip_frame(&decoded, 1));
3128 assert_eq!(reg.model(c0).colors.len(), 1);
3129 assert_eq!(reg.model(c1).colors.len(), 2);
3130
3131 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(c0, [0.0, 0.0, 5.0])]);
3133 assert_eq!(res.cull[0].chain_id, c0);
3134
3135 res.set_instance_model(®, 0, c1);
3137 assert_eq!(res.cull[0].chain_id, c1);
3138 assert_eq!(res.cull[0].radius, reg.model(c1).bound_radius());
3139
3140 let f = test_frustum();
3143 let (visible, _, _) = res.cull_bin_upload(&h.device, &h.queue, &f, 64, 64, 16, 1.0);
3144 assert_eq!(visible, 1);
3145
3146 res.set_instance_model(®, 0, c0);
3148 assert_eq!(res.cull[0].chain_id, c0);
3149
3150 res.set_instance_model(®, 99, c1);
3152 assert_eq!(res.cull[0].chain_id, c0);
3153 }
3154
3155 fn test_frustum() -> ViewFrustum {
3156 ViewFrustum {
3157 pos: [0.0, 0.0, 0.0],
3158 right: [1.0, 0.0, 0.0],
3159 down: [0.0, 1.0, 0.0],
3160 forward: [0.0, 0.0, 1.0],
3161 half_w: 1.0,
3162 half_h: 1.0,
3163 far: 10_000.0,
3164 }
3165 }
3166
3167 #[test]
3168 fn remove_model_tombstones_frees_and_reuses() {
3169 let Some(h) = headless() else { return };
3170 let mut reg = SpriteModelRegistry::new();
3172 let a = reg.add(build_sprite_model(&kv6_unsorted()));
3173 let b = reg.add(build_sprite_model(&kv6_other()));
3174 let mut res = SpriteRegistryResident::upload(
3175 &h.device,
3176 ®,
3177 &[inst(a, [0.0; 3]), inst(b, [1.0, 0.0, 0.0])],
3178 );
3179 assert_eq!(res.live_model_count(), 2);
3180 assert_eq!(res.dead_model_count(), 0);
3181
3182 res.remove_model(b);
3184 assert_eq!(res.live_model_count(), 1);
3185 assert_eq!(res.dead_model_count(), 1);
3186 assert_eq!(res.dead.iter().filter(|&&d| d).count(), 1, "one entry dead");
3187 assert!(!res.colors_alloc.free.is_empty(), "B's colour slot freed");
3188
3189 let c = reg.add(build_sprite_model(&kv6_other()));
3191 res.add_model(&h.device, &h.queue, ®, c);
3192 assert_eq!(res.live_model_count(), 2);
3193
3194 let cols = read_u32(&h, &res.colors, u64::from(res.colors_alloc.cap_total()));
3196 for e in [a as usize, c as usize] {
3197 let m = ®.entries[e];
3198 let cc = res.meta[e].colors_offset as usize;
3199 assert_eq!(
3200 &cols[cc..cc + m.colors.len()],
3201 &m.colors[..],
3202 "colors entry {e}"
3203 );
3204 }
3205
3206 let f = test_frustum();
3208 let _ = res.cull_bin_upload(&h.device, &h.queue, &f, 64, 64, 16, 1.0);
3209 }
3210
3211 #[test]
3212 fn compact_reclaims_holes_keeps_ids_stable() {
3213 let Some(h) = headless() else { return };
3214 let mut reg = SpriteModelRegistry::new();
3215 let a = reg.add(build_sprite_model(&kv6_unsorted()));
3216 let b = reg.add(build_sprite_model(&kv6_other()));
3217 let c = reg.add(build_sprite_model(&kv6_other()));
3218 let mut res = SpriteRegistryResident::upload(
3219 &h.device,
3220 ®,
3221 &[inst(a, [0.0; 3]), inst(b, [1.0; 3]), inst(c, [2.0; 3])],
3222 );
3223 let occ_used_full = res.occ_used;
3224
3225 res.remove_model(b);
3227 res.compact(&h.device, &h.queue, ®);
3228
3229 let live_occ: u32 = [a, c]
3231 .iter()
3232 .map(|&e| reg.entries[e as usize].occupancy.len() as u32)
3233 .sum();
3234 assert_eq!(res.occ_used, live_occ);
3235 assert!(res.occ_used < occ_used_full, "compaction shrank occupancy");
3236 assert_eq!(res.meta[b as usize].occupancy_offset, 0);
3238 assert_eq!(res.live_model_count(), 2);
3239 assert_eq!(res.dead_model_count(), 1);
3240
3241 let occ = read_u32(&h, &res.occupancy, u64::from(res.occ_cap));
3243 let cols = read_u32(&h, &res.colors, u64::from(res.colors_alloc.cap_total()));
3244 for &e in &[a as usize, c as usize] {
3245 let m = ®.entries[e];
3246 let oo = res.meta[e].occupancy_offset as usize;
3247 assert_eq!(
3248 &occ[oo..oo + m.occupancy.len()],
3249 &m.occupancy[..],
3250 "occ {e}"
3251 );
3252 let cc = res.meta[e].colors_offset as usize;
3253 assert_eq!(&cols[cc..cc + m.colors.len()], &m.colors[..], "cols {e}");
3254 }
3255
3256 assert!(!res.chains[c as usize].is_empty());
3258 assert!(res.chains[b as usize].is_empty());
3259 }
3260
3261 #[test]
3262 fn remove_swap_semantics_and_capacity_retained() {
3263 let Some(h) = headless() else { return };
3264 let (reg, m) = one_model_registry();
3265 let seed: Vec<_> = (0..4).map(|i| inst(m, [i as f32, 0.0, 0.0])).collect();
3266 let mut res = SpriteRegistryResident::upload(&h.device, ®, &seed);
3267 assert_eq!(res.instance_count(), 4);
3268 let cap = res.instance_capacity;
3269
3270 assert_eq!(res.remove_instance(1), Some(3));
3272 assert_eq!(res.instance_count(), 3);
3273
3274 assert_eq!(res.remove_instance(2), None);
3276 assert_eq!(res.instance_count(), 2);
3277
3278 assert_eq!(res.remove_instance(99), None);
3280 assert_eq!(res.instance_count(), 2);
3281
3282 assert_eq!(res.instance_capacity, cap);
3284 }
3285}