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 _pad1: 0,
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}
791
792impl SpriteInstance {
793 #[must_use]
796 pub fn new(model_id: u32, transform: SpriteInstanceTransform) -> Self {
797 Self {
798 model_id,
799 transform,
800 material: 0,
801 alpha_mul: 255,
802 flags: 0,
803 }
804 }
805}
806
807#[repr(C)]
811#[derive(Clone, Copy, Pod, Zeroable, Debug)]
812struct SpriteModelMeta {
813 occupancy_offset: u32,
814 colors_offset: u32,
815 color_offsets_offset: u32,
816 occ_words_per_col: u32,
817 dims: [u32; 3],
818 has_vox_materials: u32,
821 pivot: [f32; 3],
822 voxel_world_size: f32,
824}
825
826#[repr(C)]
830#[derive(Clone, Copy, Pod, Zeroable, Debug)]
831struct SpriteInstanceGpu {
832 inv_rot0: [f32; 4],
833 inv_rot1: [f32; 4],
834 inv_rot2: [f32; 4],
835 pos: [f32; 3],
836 model_id: u32,
837 material: u32,
839 alpha_mul: f32,
841 flags: u32,
844 _pad1: u32,
845}
846
847#[must_use]
851fn mat3_inverse(cols: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
852 let [a, b, c] = cols; let cross = |u: [f32; 3], v: [f32; 3]| {
855 [
856 u[1] * v[2] - u[2] * v[1],
857 u[2] * v[0] - u[0] * v[2],
858 u[0] * v[1] - u[1] * v[0],
859 ]
860 };
861 let bc = cross(b, c);
862 let ca = cross(c, a);
863 let ab = cross(a, b);
864 let det = a[0] * bc[0] + a[1] * bc[1] + a[2] * bc[2];
865 let inv_det = if det.abs() < 1e-12 { 0.0 } else { 1.0 / det };
866 [
869 [bc[0] * inv_det, ca[0] * inv_det, ab[0] * inv_det],
870 [bc[1] * inv_det, ca[1] * inv_det, ab[1] * inv_det],
871 [bc[2] * inv_det, ca[2] * inv_det, ab[2] * inv_det],
872 ]
873}
874
875pub struct SpriteRegistryResident {
882 pub occupancy: wgpu::Buffer,
883 pub colors: wgpu::Buffer,
884 pub dirs: wgpu::Buffer,
888 pub materials_vox: wgpu::Buffer,
893 pub color_offsets: wgpu::Buffer,
894 pub model_meta: wgpu::Buffer,
895 pub instances: wgpu::Buffer,
898 pub instance_capacity: u32,
899 pub colmul: wgpu::Buffer,
904 colmul_cap: u32,
905 pub tile_ranges: wgpu::Buffer,
908 tile_ranges_cap: u32,
909 pub tile_instances: wgpu::Buffer,
912 tile_instances_cap: u32,
913 cull: Vec<CullInstance>,
915 chains: Vec<Vec<u32>>,
919 meta: Vec<SpriteModelMeta>,
924 colors_alloc: ColorsAllocator,
928 occ_lens: Vec<u32>,
933 coloff_lens: Vec<u32>,
934 occ_used: u32,
939 occ_cap: u32,
940 coloff_used: u32,
943 coloff_cap: u32,
944 meta_cap: u32,
947 dead: Vec<bool>,
953}
954
955#[derive(Clone, Copy)]
958enum ConcatBuf {
959 Occupancy,
960 ColorOffsets,
961}
962
963fn concat_data(m: &SpriteModel, which: ConcatBuf) -> &[u32] {
966 match which {
967 ConcatBuf::Occupancy => &m.occupancy,
968 ConcatBuf::ColorOffsets => &m.color_offsets,
969 }
970}
971
972impl SpriteRegistryResident {
973 #[must_use]
978 pub fn upload(
979 device: &wgpu::Device,
980 registry: &SpriteModelRegistry,
981 instances: &[SpriteInstance],
982 ) -> Self {
983 let entry_lens: Vec<u32> = registry
988 .entries
989 .iter()
990 .map(|m| m.colors.len() as u32)
991 .collect();
992 let colors_alloc = ColorsAllocator::new(&entry_lens);
993 let cap_total = colors_alloc.cap_total();
994
995 let mut all_occ: Vec<u32> = Vec::new();
996 let mut all_offsets: Vec<u32> = Vec::new();
997 let mut all_colors: Vec<u32> = vec![0; cap_total as usize];
998 let mut all_dirs: Vec<u32> = vec![0; cap_total as usize];
999 let mut all_materials: Vec<u32> = vec![0; cap_total as usize];
1000 let mut meta: Vec<SpriteModelMeta> = Vec::with_capacity(registry.entries.len());
1001 let mut occ_lens: Vec<u32> = Vec::with_capacity(registry.entries.len());
1002 let mut coloff_lens: Vec<u32> = Vec::with_capacity(registry.entries.len());
1003
1004 for (e, m) in registry.entries.iter().enumerate() {
1006 let slot = colors_alloc.slot(e);
1007 meta.push(SpriteModelMeta {
1008 occupancy_offset: all_occ.len() as u32,
1009 colors_offset: slot.off,
1010 color_offsets_offset: all_offsets.len() as u32,
1011 occ_words_per_col: m.occ_words_per_col,
1012 dims: m.dims,
1013 has_vox_materials: u32::from(!m.materials.is_empty()),
1014 pivot: m.pivot,
1015 voxel_world_size: m.voxel_world_size,
1016 });
1017 occ_lens.push(m.occupancy.len() as u32);
1018 coloff_lens.push(m.color_offsets.len() as u32);
1019 all_occ.extend_from_slice(&m.occupancy);
1020 all_offsets.extend_from_slice(&m.color_offsets);
1021 let off = slot.off as usize;
1022 all_colors[off..off + m.colors.len()].copy_from_slice(&m.colors);
1023 all_dirs[off..off + m.dirs.len()].copy_from_slice(&m.dirs);
1024 for (i, &mat) in m.materials.iter().enumerate() {
1025 all_materials[off + i] = u32::from(mat);
1026 }
1027 }
1028
1029 let cull: Vec<CullInstance> = instances.iter().map(|i| make_cull(registry, i)).collect();
1034
1035 let seed: Vec<SpriteInstanceGpu> = cull.iter().map(|c| c.gpu).collect();
1038 let instances_buf = {
1039 use wgpu::util::DeviceExt;
1040 let one = [SpriteInstanceGpu::zeroed()];
1041 let src: &[SpriteInstanceGpu] = if seed.is_empty() { &one } else { &seed };
1042 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1043 label: Some("roxlap-gpu sprite_reg.instances"),
1044 contents: bytemuck::cast_slice(src),
1045 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1046 })
1047 };
1048
1049 let tile_ranges = storage_dst_u32(device, "roxlap-gpu sprite_reg.tile_ranges", 1);
1050 let tile_instances = storage_dst_u32(device, "roxlap-gpu sprite_reg.tile_instances", 1);
1051 let colmul_cap = (cull.len() as u32).max(1) * 256 * 2;
1054 let colmul = storage_dst_u32(device, "roxlap-gpu sprite_reg.colmul", colmul_cap);
1055 Self {
1056 occupancy: storage_dst_u32_cap(
1057 device,
1058 "roxlap-gpu sprite_reg.occupancy",
1059 &all_occ,
1060 all_occ.len() as u32,
1061 ),
1062 colors: storage_dst_u32_cap(
1063 device,
1064 "roxlap-gpu sprite_reg.colors",
1065 &all_colors,
1066 cap_total,
1067 ),
1068 dirs: storage_dst_u32_cap(device, "roxlap-gpu sprite_reg.dirs", &all_dirs, cap_total),
1069 materials_vox: storage_dst_u32_cap(
1070 device,
1071 "roxlap-gpu sprite_reg.materials_vox",
1072 &all_materials,
1073 cap_total,
1074 ),
1075 color_offsets: storage_dst_u32_cap(
1076 device,
1077 "roxlap-gpu sprite_reg.color_offsets",
1078 &all_offsets,
1079 all_offsets.len() as u32,
1080 ),
1081 model_meta: storage_dst_pod(device, "roxlap-gpu sprite_reg.model_meta", &meta),
1082 instances: instances_buf,
1083 instance_capacity: cull.len() as u32,
1084 colmul,
1085 colmul_cap,
1086 tile_ranges,
1087 tile_ranges_cap: 1,
1088 tile_instances,
1089 tile_instances_cap: 1,
1090 cull,
1091 chains: registry.chains.clone(),
1092 occ_used: all_occ.len() as u32,
1093 occ_cap: all_occ.len() as u32,
1094 coloff_used: all_offsets.len() as u32,
1095 coloff_cap: all_offsets.len() as u32,
1096 meta_cap: meta.len() as u32,
1097 dead: vec![false; meta.len()],
1098 meta,
1099 colors_alloc,
1100 occ_lens,
1101 coloff_lens,
1102 }
1103 }
1104
1105 #[must_use]
1107 pub fn instance_count(&self) -> usize {
1108 self.cull.len()
1109 }
1110
1111 pub fn append_instances(
1133 &mut self,
1134 device: &wgpu::Device,
1135 registry: &SpriteModelRegistry,
1136 instances: &[SpriteInstance],
1137 ) -> u32 {
1138 let base = self.cull.len() as u32;
1139 if instances.is_empty() {
1140 return base;
1141 }
1142 for i in instances {
1143 debug_assert!(
1144 (i.model_id as usize) < self.chains.len(),
1145 "append_instances: model_id {} not resident (run upload to register new models)",
1146 i.model_id
1147 );
1148 self.cull.push(make_cull(registry, i));
1149 }
1150 let need = self.cull.len() as u32;
1151 if need > self.instance_capacity {
1152 self.instance_capacity = need.next_power_of_two();
1156 self.instances = instances_buffer(device, self.instance_capacity);
1157 }
1158 base
1159 }
1160
1161 pub fn remove_instance(&mut self, index: usize) -> Option<usize> {
1172 if index >= self.cull.len() {
1173 return None;
1174 }
1175 let last = self.cull.len() - 1;
1176 self.cull.swap_remove(index);
1177 (index != last).then_some(last)
1178 }
1179
1180 pub fn set_instance_colmul(&mut self, tables: &[[u64; 256]]) {
1186 for (ci, t) in self.cull.iter_mut().zip(tables) {
1187 ci.colmul.copy_from_slice(t);
1188 }
1189 }
1190
1191 pub fn update_transforms(&mut self, instances: &[SpriteInstance]) {
1199 debug_assert_eq!(
1200 instances.len(),
1201 self.cull.len(),
1202 "update_transforms instance count must match upload"
1203 );
1204 for (ci, inst) in self.cull.iter_mut().zip(instances) {
1205 ci.gpu.inv_rot0 = inst.transform.inv_rot[0];
1206 ci.gpu.inv_rot1 = inst.transform.inv_rot[1];
1207 ci.gpu.inv_rot2 = inst.transform.inv_rot[2];
1208 ci.gpu.pos = inst.transform.pos;
1209 ci.gpu.material = u32::from(inst.material);
1212 ci.gpu.alpha_mul = f32::from(inst.alpha_mul) / 255.0;
1213 ci.center = inst.transform.pos;
1215 }
1216 }
1217
1218 pub fn set_instance_model(
1233 &mut self,
1234 registry: &SpriteModelRegistry,
1235 idx: usize,
1236 chain_id: u32,
1237 ) {
1238 let Some(radius) = registry
1242 .model_checked(chain_id)
1243 .map(SpriteModel::bound_radius)
1244 else {
1245 return;
1246 };
1247 let Some(ci) = self.cull.get_mut(idx) else {
1248 return;
1249 };
1250 ci.chain_id = chain_id;
1251 ci.gpu.model_id = chain_id; ci.radius = radius;
1253 }
1254
1255 pub fn update_model(
1275 &mut self,
1276 device: &wgpu::Device,
1277 queue: &wgpu::Queue,
1278 registry: &SpriteModelRegistry,
1279 chain_id: u32,
1280 ) {
1281 let entries = self.chains[chain_id as usize].clone();
1282 let mut grew = false;
1283 for &e in &entries {
1284 let e = e as usize;
1285 let m = ®istry.entries[e];
1286
1287 debug_assert_eq!(
1289 m.occupancy.len() as u32,
1290 self.occ_lens[e],
1291 "update_model: entry {e} occupancy length changed (dims grew?)"
1292 );
1293 debug_assert_eq!(
1294 m.color_offsets.len() as u32,
1295 self.coloff_lens[e],
1296 "update_model: entry {e} color_offsets length changed (dims grew?)"
1297 );
1298 queue.write_buffer(
1299 &self.occupancy,
1300 u64::from(self.meta[e].occupancy_offset) * 4,
1301 bytemuck::cast_slice(&m.occupancy),
1302 );
1303 queue.write_buffer(
1304 &self.color_offsets,
1305 u64::from(self.meta[e].color_offsets_offset) * 4,
1306 bytemuck::cast_slice(&m.color_offsets),
1307 );
1308
1309 let new_len = m.colors.len() as u32;
1311 match self.colors_alloc.place(e, new_len) {
1312 Some(off) => {
1313 queue.write_buffer(
1314 &self.colors,
1315 u64::from(off) * 4,
1316 bytemuck::cast_slice(&m.colors),
1317 );
1318 queue.write_buffer(
1319 &self.dirs,
1320 u64::from(off) * 4,
1321 bytemuck::cast_slice(&m.dirs),
1322 );
1323 let mats: Vec<u32> = m.materials.iter().map(|&x| u32::from(x)).collect();
1324 queue.write_buffer(
1325 &self.materials_vox,
1326 u64::from(off) * 4,
1327 bytemuck::cast_slice(&mats),
1328 );
1329 if self.meta[e].colors_offset != off {
1330 self.meta[e].colors_offset = off;
1332 queue.write_buffer(
1333 &self.model_meta,
1334 (e * std::mem::size_of::<SpriteModelMeta>()) as u64,
1335 bytemuck::bytes_of(&self.meta[e]),
1336 );
1337 }
1338 }
1339 None => grew = true,
1340 }
1341 }
1342
1343 if grew {
1346 self.grow_and_repack(device, queue, registry);
1347 }
1348 }
1349
1350 fn grow_and_repack(
1357 &mut self,
1358 device: &wgpu::Device,
1359 queue: &wgpu::Queue,
1360 registry: &SpriteModelRegistry,
1361 ) {
1362 self.repack_colors_dirs(device, registry);
1363 queue.write_buffer(&self.model_meta, 0, bytemuck::cast_slice(&self.meta));
1365 }
1366
1367 fn repack_colors_dirs(&mut self, device: &wgpu::Device, registry: &SpriteModelRegistry) {
1375 let new_lens: Vec<u32> = registry
1378 .entries
1379 .iter()
1380 .enumerate()
1381 .map(|(e, m)| {
1382 if self.dead[e] {
1383 0
1384 } else {
1385 m.colors.len() as u32
1386 }
1387 })
1388 .collect();
1389 self.colors_alloc.repack(&new_lens);
1390 let cap_total = self.colors_alloc.cap_total();
1391
1392 let mut all_colors = vec![0u32; cap_total as usize];
1393 let mut all_dirs = vec![0u32; cap_total as usize];
1394 let mut all_materials = vec![0u32; cap_total as usize];
1395 for (e, m) in registry.entries.iter().enumerate() {
1396 if self.dead[e] {
1397 self.meta[e].colors_offset = 0;
1398 continue;
1399 }
1400 let off = self.colors_alloc.slot(e).off as usize;
1401 all_colors[off..off + m.colors.len()].copy_from_slice(&m.colors);
1402 all_dirs[off..off + m.dirs.len()].copy_from_slice(&m.dirs);
1403 for (i, &mat) in m.materials.iter().enumerate() {
1404 all_materials[off + i] = u32::from(mat);
1405 }
1406 self.meta[e].colors_offset = off as u32;
1407 }
1408 self.colors = storage_dst_u32_cap(
1409 device,
1410 "roxlap-gpu sprite_reg.colors",
1411 &all_colors,
1412 cap_total,
1413 );
1414 self.dirs = storage_dst_u32_cap(device, "roxlap-gpu sprite_reg.dirs", &all_dirs, cap_total);
1415 self.materials_vox = storage_dst_u32_cap(
1416 device,
1417 "roxlap-gpu sprite_reg.materials_vox",
1418 &all_materials,
1419 cap_total,
1420 );
1421 eprintln!(
1422 "roxlap-gpu: sprite registry colors/dirs/materials grew + repacked to {cap_total} words"
1423 );
1424 }
1425
1426 pub fn add_model(
1444 &mut self,
1445 device: &wgpu::Device,
1446 queue: &wgpu::Queue,
1447 registry: &SpriteModelRegistry,
1448 chain_id: u32,
1449 ) {
1450 let entries = registry.chains[chain_id as usize].clone();
1451 debug_assert_eq!(
1452 chain_id as usize,
1453 self.chains.len(),
1454 "add_model: chains must be appended in order"
1455 );
1456
1457 let mut need_colors_grow = false;
1461 for &e in &entries {
1462 let e = e as usize;
1463 debug_assert_eq!(
1464 e,
1465 self.meta.len(),
1466 "add_model: entries must be appended in order"
1467 );
1468 let m = ®istry.entries[e];
1469 let occ_off = self.occ_used;
1470 let coloff_off = self.coloff_used;
1471 self.occ_used += m.occupancy.len() as u32;
1472 self.coloff_used += m.color_offsets.len() as u32;
1473 let colors_off = match self.colors_alloc.push(m.colors.len() as u32) {
1474 Some(off) => off,
1475 None => {
1476 need_colors_grow = true;
1477 0 }
1479 };
1480 self.meta.push(SpriteModelMeta {
1481 occupancy_offset: occ_off,
1482 colors_offset: colors_off,
1483 color_offsets_offset: coloff_off,
1484 occ_words_per_col: m.occ_words_per_col,
1485 dims: m.dims,
1486 has_vox_materials: u32::from(!m.materials.is_empty()),
1487 pivot: m.pivot,
1488 voxel_world_size: m.voxel_world_size,
1489 });
1490 self.occ_lens.push(m.occupancy.len() as u32);
1491 self.coloff_lens.push(m.color_offsets.len() as u32);
1492 self.dead.push(false);
1493 }
1494 self.chains.push(entries.clone());
1495
1496 self.sync_concat(device, queue, registry, &entries, ConcatBuf::Occupancy);
1499 self.sync_concat(device, queue, registry, &entries, ConcatBuf::ColorOffsets);
1500
1501 if need_colors_grow {
1504 self.repack_colors_dirs(device, registry);
1505 } else {
1506 for &e in &entries {
1507 let e = e as usize;
1508 let m = ®istry.entries[e];
1509 let off = u64::from(self.meta[e].colors_offset) * 4;
1510 queue.write_buffer(&self.colors, off, bytemuck::cast_slice(&m.colors));
1511 queue.write_buffer(&self.dirs, off, bytemuck::cast_slice(&m.dirs));
1512 let mats: Vec<u32> = m.materials.iter().map(|&x| u32::from(x)).collect();
1513 queue.write_buffer(&self.materials_vox, off, bytemuck::cast_slice(&mats));
1514 }
1515 }
1516
1517 let count = self.meta.len() as u32;
1521 if count > self.meta_cap {
1522 self.meta_cap = grow_records(count);
1523 self.model_meta = storage_dst_pod_cap(
1524 device,
1525 "roxlap-gpu sprite_reg.model_meta",
1526 &self.meta,
1527 self.meta_cap,
1528 );
1529 } else {
1530 queue.write_buffer(&self.model_meta, 0, bytemuck::cast_slice(&self.meta));
1531 }
1532 }
1533
1534 fn sync_concat(
1540 &mut self,
1541 device: &wgpu::Device,
1542 queue: &wgpu::Queue,
1543 registry: &SpriteModelRegistry,
1544 new_entries: &[u32],
1545 which: ConcatBuf,
1546 ) {
1547 let (used, cap) = match which {
1548 ConcatBuf::Occupancy => (self.occ_used, self.occ_cap),
1549 ConcatBuf::ColorOffsets => (self.coloff_used, self.coloff_cap),
1550 };
1551 if used > cap {
1552 let new_cap = grow_words(used);
1553 let all: Vec<u32> = registry
1554 .entries
1555 .iter()
1556 .flat_map(|m| concat_data(m, which).iter().copied())
1557 .collect();
1558 let label = match which {
1559 ConcatBuf::Occupancy => "roxlap-gpu sprite_reg.occupancy",
1560 ConcatBuf::ColorOffsets => "roxlap-gpu sprite_reg.color_offsets",
1561 };
1562 let buf = storage_dst_u32_cap(device, label, &all, new_cap);
1563 match which {
1564 ConcatBuf::Occupancy => {
1565 self.occupancy = buf;
1566 self.occ_cap = new_cap;
1567 }
1568 ConcatBuf::ColorOffsets => {
1569 self.color_offsets = buf;
1570 self.coloff_cap = new_cap;
1571 }
1572 }
1573 } else {
1574 let target = match which {
1575 ConcatBuf::Occupancy => &self.occupancy,
1576 ConcatBuf::ColorOffsets => &self.color_offsets,
1577 };
1578 for &e in new_entries {
1579 let e = e as usize;
1580 let off = match which {
1581 ConcatBuf::Occupancy => self.meta[e].occupancy_offset,
1582 ConcatBuf::ColorOffsets => self.meta[e].color_offsets_offset,
1583 };
1584 queue.write_buffer(
1585 target,
1586 u64::from(off) * 4,
1587 bytemuck::cast_slice(concat_data(®istry.entries[e], which)),
1588 );
1589 }
1590 }
1591 }
1592
1593 #[must_use]
1598 pub fn dead_model_count(&self) -> usize {
1599 self.chains.iter().filter(|c| c.is_empty()).count()
1600 }
1601
1602 #[must_use]
1604 pub fn live_model_count(&self) -> usize {
1605 self.chains.iter().filter(|c| !c.is_empty()).count()
1606 }
1607
1608 pub fn remove_model(&mut self, chain_id: u32) {
1621 let Some(entries) = self.chains.get(chain_id as usize).cloned() else {
1622 return;
1623 };
1624 if entries.is_empty() {
1625 return; }
1627 for &e in &entries {
1628 let e = e as usize;
1629 self.dead[e] = true;
1630 self.colors_alloc.free(e);
1631 }
1632 self.chains[chain_id as usize] = Vec::new(); }
1634
1635 pub fn compact(
1645 &mut self,
1646 device: &wgpu::Device,
1647 queue: &wgpu::Queue,
1648 registry: &SpriteModelRegistry,
1649 ) {
1650 self.compact_concat(device, registry, ConcatBuf::Occupancy);
1653 self.compact_concat(device, registry, ConcatBuf::ColorOffsets);
1654 self.repack_colors_dirs(device, registry);
1656 queue.write_buffer(&self.model_meta, 0, bytemuck::cast_slice(&self.meta));
1659 }
1660
1661 fn compact_concat(
1666 &mut self,
1667 device: &wgpu::Device,
1668 registry: &SpriteModelRegistry,
1669 which: ConcatBuf,
1670 ) {
1671 let mut all: Vec<u32> = Vec::new();
1672 for e in 0..self.meta.len() {
1673 if self.dead[e] {
1674 match which {
1675 ConcatBuf::Occupancy => self.meta[e].occupancy_offset = 0,
1676 ConcatBuf::ColorOffsets => self.meta[e].color_offsets_offset = 0,
1677 }
1678 continue;
1679 }
1680 let off = all.len() as u32;
1681 match which {
1682 ConcatBuf::Occupancy => self.meta[e].occupancy_offset = off,
1683 ConcatBuf::ColorOffsets => self.meta[e].color_offsets_offset = off,
1684 }
1685 all.extend_from_slice(concat_data(®istry.entries[e], which));
1686 }
1687 let used = all.len() as u32;
1688 let cap = grow_words(used);
1689 let (label, buf) = match which {
1690 ConcatBuf::Occupancy => ("roxlap-gpu sprite_reg.occupancy", &mut self.occupancy),
1691 ConcatBuf::ColorOffsets => (
1692 "roxlap-gpu sprite_reg.color_offsets",
1693 &mut self.color_offsets,
1694 ),
1695 };
1696 *buf = storage_dst_u32_cap(device, label, &all, cap);
1697 match which {
1698 ConcatBuf::Occupancy => {
1699 self.occ_used = used;
1700 self.occ_cap = cap;
1701 }
1702 ConcatBuf::ColorOffsets => {
1703 self.coloff_used = used;
1704 self.coloff_cap = cap;
1705 }
1706 }
1707 }
1708
1709 #[allow(clippy::too_many_arguments)]
1717 pub fn cull_bin_upload(
1718 &mut self,
1719 device: &wgpu::Device,
1720 queue: &wgpu::Queue,
1721 f: &ViewFrustum,
1722 screen_w: u32,
1723 screen_h: u32,
1724 tile_size: u32,
1725 lod_px: f32,
1726 ) -> (u32, u32, u32) {
1727 let tiles_x = screen_w.div_ceil(tile_size).max(1);
1728 let tiles_y = screen_h.div_ceil(tile_size).max(1);
1729 let n_tiles = (tiles_x * tiles_y) as usize;
1730
1731 let nw = (1.0 + f.half_w * f.half_w).sqrt();
1732 let nh = (1.0 + f.half_h * f.half_h).sqrt();
1733 let cx = screen_w as f32 * 0.5;
1734 let cy = screen_h as f32 * 0.5;
1735 let px_per_world = cx / f.half_w; let ts = tile_size as f32;
1737 let tx_max = tiles_x as i32 - 1;
1738 let ty_max = tiles_y as i32 - 1;
1739
1740 let mut visible: Vec<SpriteInstanceGpu> = Vec::with_capacity(self.cull.len());
1741 let mut boxes: Vec<[i32; 4]> = Vec::with_capacity(self.cull.len());
1743 let mut visible_colmul: Vec<u32> = Vec::with_capacity(self.cull.len() * 512);
1747 let mut counts = vec![0u32; n_tiles];
1748
1749 for ci in &self.cull {
1750 if self.chains[ci.chain_id as usize].is_empty() {
1754 continue;
1755 }
1756 let rel = [
1757 ci.center[0] - f.pos[0],
1758 ci.center[1] - f.pos[1],
1759 ci.center[2] - f.pos[2],
1760 ];
1761 let z = dot3(rel, f.forward);
1762 let r = ci.radius;
1763 if z + r < 0.0 || z - r > f.far {
1764 continue; }
1766 let x = dot3(rel, f.right);
1767 if (x - f.half_w * z) > r * nw || (-x - f.half_w * z) > r * nw {
1768 continue; }
1770 let y = dot3(rel, f.down);
1771 if (y - f.half_h * z) > r * nh || (-y - f.half_h * z) > r * nh {
1772 continue; }
1774
1775 let (tx0, tx1, ty0, ty1) = if z > 1e-3 {
1777 let sx = cx + (x / z) * px_per_world;
1778 let sy = cy + (y / z) * px_per_world;
1779 let sr = (r / z) * px_per_world;
1780 (
1781 (((sx - sr) / ts).floor() as i32).clamp(0, tx_max),
1782 (((sx + sr) / ts).floor() as i32).clamp(0, tx_max),
1783 (((sy - sr) / ts).floor() as i32).clamp(0, ty_max),
1784 (((sy + sr) / ts).floor() as i32).clamp(0, ty_max),
1785 )
1786 } else {
1787 (0, tx_max, 0, ty_max)
1788 };
1789 let chain = &self.chains[ci.chain_id as usize];
1796 let level = if z > 1e-3 && chain.len() > 1 {
1797 let voxel_px = px_per_world / z; ((lod_px / voxel_px).log2().ceil().max(0.0) as usize).min(chain.len() - 1)
1799 } else {
1800 0
1801 };
1802 let mut g = ci.gpu;
1803 g.model_id = chain[level];
1804 visible.push(g);
1805 boxes.push([tx0, tx1, ty0, ty1]);
1806 for &w in ci.colmul.iter() {
1807 visible_colmul.push((w & 0xffff_ffff) as u32);
1808 visible_colmul.push((w >> 32) as u32);
1809 }
1810 for ty in ty0..=ty1 {
1811 for tx in tx0..=tx1 {
1812 counts[(ty * tiles_x as i32 + tx) as usize] += 1;
1813 }
1814 }
1815 }
1816
1817 if visible.is_empty() {
1818 return (0, tiles_x, tiles_y);
1819 }
1820
1821 let mut tile_ranges = vec![0u32; n_tiles * 2];
1824 let mut running = 0u32;
1825 for t in 0..n_tiles {
1826 tile_ranges[2 * t] = running; tile_ranges[2 * t + 1] = counts[t]; running += counts[t];
1829 }
1830 let total = running as usize;
1831 let mut tile_instances = vec![0u32; total.max(1)];
1832 let mut cursor: Vec<u32> = (0..n_tiles).map(|t| tile_ranges[2 * t]).collect();
1833 for (vis_idx, b) in boxes.iter().enumerate() {
1834 for ty in b[2]..=b[3] {
1835 for tx in b[0]..=b[1] {
1836 let t = (ty * tiles_x as i32 + tx) as usize;
1837 tile_instances[cursor[t] as usize] = vis_idx as u32;
1838 cursor[t] += 1;
1839 }
1840 }
1841 }
1842
1843 queue.write_buffer(&self.instances, 0, bytemuck::cast_slice(&visible));
1847 let need_ranges = tile_ranges.len() as u32;
1848 if need_ranges > self.tile_ranges_cap {
1849 self.tile_ranges_cap = need_ranges.next_power_of_two();
1850 self.tile_ranges = storage_dst_u32(
1851 device,
1852 "roxlap-gpu sprite_reg.tile_ranges",
1853 self.tile_ranges_cap,
1854 );
1855 }
1856 let need_inst = tile_instances.len() as u32;
1857 if need_inst > self.tile_instances_cap {
1858 self.tile_instances_cap = need_inst.next_power_of_two();
1859 self.tile_instances = storage_dst_u32(
1860 device,
1861 "roxlap-gpu sprite_reg.tile_instances",
1862 self.tile_instances_cap,
1863 );
1864 }
1865 queue.write_buffer(&self.tile_ranges, 0, bytemuck::cast_slice(&tile_ranges));
1866 queue.write_buffer(
1867 &self.tile_instances,
1868 0,
1869 bytemuck::cast_slice(&tile_instances),
1870 );
1871 let need_colmul = visible_colmul.len() as u32;
1872 if need_colmul > self.colmul_cap {
1873 self.colmul_cap = need_colmul.next_power_of_two();
1874 self.colmul = storage_dst_u32(device, "roxlap-gpu sprite_reg.colmul", self.colmul_cap);
1875 }
1876 queue.write_buffer(&self.colmul, 0, bytemuck::cast_slice(&visible_colmul));
1877
1878 (visible.len() as u32, tiles_x, tiles_y)
1879 }
1880}
1881
1882#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1888struct ColorSlot {
1889 off: u32,
1890 cap: u32,
1891 len: u32,
1892}
1893
1894#[derive(Debug, Default)]
1901struct ColorsAllocator {
1902 slots: Vec<ColorSlot>,
1904 free: Vec<(u32, u32)>,
1906 tail: u32,
1908 cap_total: u32,
1910}
1911
1912fn slot_cap(len: u32) -> u32 {
1915 len + len / 4 + 16
1916}
1917
1918fn grow_words(used: u32) -> u32 {
1922 used + used / 2 + 256
1923}
1924
1925fn grow_records(count: u32) -> u32 {
1927 count + count / 2 + 8
1928}
1929
1930impl ColorsAllocator {
1931 fn new(entry_lens: &[u32]) -> Self {
1935 let mut a = Self::default();
1936 a.repack(entry_lens);
1937 a
1938 }
1939
1940 fn slot(&self, entry: usize) -> ColorSlot {
1941 self.slots[entry]
1942 }
1943
1944 fn cap_total(&self) -> u32 {
1945 self.cap_total
1946 }
1947
1948 fn repack(&mut self, new_lens: &[u32]) {
1952 self.free.clear();
1953 let mut off = 0u32;
1954 let mut slots = Vec::with_capacity(new_lens.len());
1955 for &len in new_lens {
1956 let cap = if len == 0 { 0 } else { slot_cap(len) };
1959 slots.push(ColorSlot { off, cap, len });
1960 off += cap;
1961 }
1962 self.slots = slots;
1963 self.tail = off;
1964 self.cap_total = off + off / 2 + 256;
1966 }
1967
1968 fn place(&mut self, entry: usize, new_len: u32) -> Option<u32> {
1973 let cur = self.slots[entry];
1974 if new_len <= cur.cap {
1975 self.slots[entry] = ColorSlot {
1976 len: new_len,
1977 ..cur
1978 };
1979 return Some(cur.off);
1980 }
1981 let old = (cur.off, cur.cap);
1982 if let Some(i) = self.free.iter().position(|&(_, c)| c >= new_len) {
1984 let (off, cap) = self.free.remove(i);
1985 self.free.push(old);
1986 self.slots[entry] = ColorSlot {
1987 off,
1988 cap,
1989 len: new_len,
1990 };
1991 return Some(off);
1992 }
1993 let want = slot_cap(new_len);
1995 if self.tail + want <= self.cap_total {
1996 let off = self.tail;
1997 self.tail += want;
1998 self.free.push(old);
1999 self.slots[entry] = ColorSlot {
2000 off,
2001 cap: want,
2002 len: new_len,
2003 };
2004 return Some(off);
2005 }
2006 None
2007 }
2008
2009 fn push(&mut self, new_len: u32) -> Option<u32> {
2015 if let Some(i) = self.free.iter().position(|&(_, c)| c >= new_len) {
2016 let (off, cap) = self.free.remove(i);
2017 self.slots.push(ColorSlot {
2018 off,
2019 cap,
2020 len: new_len,
2021 });
2022 return Some(off);
2023 }
2024 let want = slot_cap(new_len);
2025 if self.tail + want <= self.cap_total {
2026 let off = self.tail;
2027 self.tail += want;
2028 self.slots.push(ColorSlot {
2029 off,
2030 cap: want,
2031 len: new_len,
2032 });
2033 return Some(off);
2034 }
2035 None
2036 }
2037
2038 fn free(&mut self, entry: usize) {
2043 let s = self.slots[entry];
2044 if s.cap > 0 {
2045 self.free.push((s.off, s.cap));
2046 }
2047 self.slots[entry] = ColorSlot {
2048 off: 0,
2049 cap: 0,
2050 len: 0,
2051 };
2052 }
2053}
2054
2055#[allow(dead_code)]
2058fn storage_u32(device: &wgpu::Device, label: &str, data: &[u32]) -> wgpu::Buffer {
2059 use wgpu::util::DeviceExt;
2060 let bytes: &[u8] = if data.is_empty() {
2061 bytemuck::cast_slice(&[0u32])
2062 } else {
2063 bytemuck::cast_slice(data)
2064 };
2065 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2066 label: Some(label),
2067 contents: bytes,
2068 usage: wgpu::BufferUsages::STORAGE,
2069 })
2070}
2071
2072fn storage_dst_u32(device: &wgpu::Device, label: &str, cap: u32) -> wgpu::Buffer {
2075 device.create_buffer(&wgpu::BufferDescriptor {
2076 label: Some(label),
2077 size: u64::from(cap.max(1)) * 4,
2078 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
2079 mapped_at_creation: false,
2080 })
2081}
2082
2083fn storage_dst_u32_cap(device: &wgpu::Device, label: &str, data: &[u32], cap: u32) -> wgpu::Buffer {
2091 let cap = cap.max(data.len() as u32).max(1);
2092 let buf = device.create_buffer(&wgpu::BufferDescriptor {
2093 label: Some(label),
2094 size: u64::from(cap) * 4,
2095 usage: wgpu::BufferUsages::STORAGE
2096 | wgpu::BufferUsages::COPY_DST
2097 | wgpu::BufferUsages::COPY_SRC,
2098 mapped_at_creation: true,
2099 });
2100 if !data.is_empty() {
2101 buf.slice(..(data.len() as u64 * 4))
2102 .get_mapped_range_mut()
2103 .copy_from_slice(bytemuck::cast_slice(data));
2104 }
2105 buf.unmap();
2106 buf
2107}
2108
2109fn storage_dst_pod<T: Pod + Zeroable>(
2115 device: &wgpu::Device,
2116 label: &str,
2117 data: &[T],
2118) -> wgpu::Buffer {
2119 let one = [T::zeroed()];
2120 let src: &[T] = if data.is_empty() { &one } else { data };
2121 let buf = device.create_buffer(&wgpu::BufferDescriptor {
2122 label: Some(label),
2123 size: std::mem::size_of_val(src) as u64,
2124 usage: wgpu::BufferUsages::STORAGE
2125 | wgpu::BufferUsages::COPY_DST
2126 | wgpu::BufferUsages::COPY_SRC,
2127 mapped_at_creation: true,
2128 });
2129 buf.slice(..)
2130 .get_mapped_range_mut()
2131 .copy_from_slice(bytemuck::cast_slice(src));
2132 buf.unmap();
2133 buf
2134}
2135
2136fn storage_dst_pod_cap<T: Pod + Zeroable>(
2141 device: &wgpu::Device,
2142 label: &str,
2143 data: &[T],
2144 cap: u32,
2145) -> wgpu::Buffer {
2146 let rec = std::mem::size_of::<T>() as u64;
2147 let cap = u64::from(cap.max(data.len() as u32).max(1));
2148 let buf = device.create_buffer(&wgpu::BufferDescriptor {
2149 label: Some(label),
2150 size: cap * rec,
2151 usage: wgpu::BufferUsages::STORAGE
2152 | wgpu::BufferUsages::COPY_DST
2153 | wgpu::BufferUsages::COPY_SRC,
2154 mapped_at_creation: true,
2155 });
2156 if !data.is_empty() {
2157 buf.slice(..(data.len() as u64 * rec))
2158 .get_mapped_range_mut()
2159 .copy_from_slice(bytemuck::cast_slice(data));
2160 }
2161 buf.unmap();
2162 buf
2163}
2164
2165#[allow(dead_code)]
2168fn storage_pod<T: Pod + Zeroable>(device: &wgpu::Device, label: &str, data: &[T]) -> wgpu::Buffer {
2169 use wgpu::util::DeviceExt;
2170 let one = [T::zeroed()];
2171 let src: &[T] = if data.is_empty() { &one } else { data };
2172 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2173 label: Some(label),
2174 contents: bytemuck::cast_slice(src),
2175 usage: wgpu::BufferUsages::STORAGE,
2176 })
2177}
2178
2179#[cfg(test)]
2180mod tests {
2181 use super::*;
2182 use roxlap_formats::kv6::{Kv6, Voxel};
2183
2184 fn kv6_unsorted() -> Kv6 {
2187 let mk = |z, col| Voxel {
2188 col,
2189 z,
2190 vis: 0,
2191 dir: 0,
2192 };
2193 Kv6 {
2194 xsiz: 2,
2195 ysiz: 1,
2196 zsiz: 8,
2197 xpiv: 0.0,
2198 ypiv: 0.0,
2199 zpiv: 0.0,
2200 voxels: vec![mk(5, 0xAA), mk(1, 0xBB), mk(3, 0xCC)],
2201 xlen: vec![2, 1],
2202 ylen: vec![vec![2], vec![1]],
2203 palette: None,
2204 }
2205 }
2206
2207 #[test]
2208 fn occupancy_bits_set_at_voxel_z() {
2209 let m = build_sprite_model(&kv6_unsorted());
2210 assert_eq!(m.dims, [2, 1, 8]);
2211 assert_eq!(m.occ_words_per_col, 1); assert_eq!(m.occupancy[0], (1 << 1) | (1 << 5));
2214 assert_eq!(m.occupancy[1], 1 << 3);
2215 }
2216
2217 #[test]
2218 fn colors_are_ascending_z_for_rank_lookup() {
2219 let m = build_sprite_model(&kv6_unsorted());
2220 assert_eq!(m.color_offsets, vec![0, 2, 3]);
2222 assert_eq!(&m.colors, &[0xBB, 0xAA, 0xCC]);
2223 }
2224
2225 #[test]
2226 fn identity_basis_inverts_to_identity() {
2227 let inv = mat3_inverse([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
2228 assert_eq!(inv, [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
2229 }
2230
2231 #[test]
2232 fn fork_is_independent_of_parent() {
2233 let mut reg = SpriteModelRegistry::new();
2234 let base = reg.add(build_sprite_model(&kv6_unsorted()));
2235 let forked = reg.fork(base);
2236 assert_ne!(base, forked);
2237 reg.model_mut(forked).recolor(|_| 0x11);
2239 assert_eq!(®.model(base).colors, &[0xBB, 0xAA, 0xCC]);
2241 assert_eq!(®.model(forked).colors, &[0x11, 0x11, 0x11]);
2242 }
2243
2244 #[test]
2245 fn remove_frees_chain_data_keeps_ids_stable() {
2246 let mut reg = SpriteModelRegistry::new();
2247 let a = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2248 let b = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2249 let len_before = reg.len();
2250 assert!(reg.is_live(a) && reg.is_live(b));
2251
2252 reg.remove(a);
2253 assert!(!reg.is_live(a));
2256 assert!(reg.is_live(b));
2258 assert_eq!(®.model(b).colors, &[0xBB, 0xAA, 0xCC]);
2259 assert_eq!(reg.len(), len_before);
2260
2261 let c = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2263 assert_eq!(c, len_before as u32);
2264 assert!(reg.is_live(c));
2265 assert_eq!(®.model(b).colors, &[0xBB, 0xAA, 0xCC]);
2267 }
2268
2269 #[test]
2270 fn model_checked_guards_out_of_range_and_tombstoned() {
2271 let mut reg = SpriteModelRegistry::new();
2274 let a = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2275 assert!(reg.model_checked(a).is_some());
2276 assert!(reg.model_checked(9999).is_none(), "out of range → None");
2277 reg.remove(a);
2278 assert!(reg.model_checked(a).is_none(), "tombstoned chain → None");
2279 }
2280
2281 #[test]
2282 fn remove_is_idempotent_and_bounds_safe() {
2283 let mut reg = SpriteModelRegistry::new();
2284 let a = reg.add(build_sprite_model(&kv6_unsorted()));
2285 reg.remove(a);
2286 reg.remove(a); reg.remove(999); assert!(!reg.is_live(a));
2289 assert!(!reg.is_live(999));
2290 }
2291
2292 #[test]
2293 fn registry_gpu_structs_have_expected_sizes() {
2294 assert_eq!(std::mem::size_of::<SpriteModelMeta>(), 48);
2295 assert_eq!(std::mem::size_of::<SpriteInstanceGpu>(), 80);
2298 }
2299
2300 #[test]
2301 fn add_lod_builds_halving_mip_chain() {
2302 let mut reg = SpriteModelRegistry::new();
2303 let id = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
2306 let m0 = reg.model(id);
2307 assert_eq!(m0.dims, [2, 1, 8]);
2308 assert!((m0.voxel_world_size - 1.0).abs() < 1e-6);
2309 }
2310
2311 fn kv6_from(xsiz: u32, ysiz: u32, zsiz: u32, voxels: &[(u32, u32, u16, u32)]) -> Kv6 {
2314 let mut ylen = vec![vec![0u16; ysiz as usize]; xsiz as usize];
2315 let mut flat = Vec::new();
2316 for x in 0..xsiz {
2317 for y in 0..ysiz {
2318 let mut col: Vec<(u16, u32)> = voxels
2319 .iter()
2320 .filter(|(vx, vy, _, _)| *vx == x && *vy == y)
2321 .map(|(_, _, z, c)| (*z, *c))
2322 .collect();
2323 col.sort_by_key(|(z, _)| *z);
2324 ylen[x as usize][y as usize] = col.len() as u16;
2325 for (z, c) in col {
2326 flat.push(Voxel {
2327 col: c,
2328 z,
2329 vis: 0,
2330 dir: 0,
2331 });
2332 }
2333 }
2334 }
2335 let xlen = ylen
2336 .iter()
2337 .map(|c| c.iter().map(|&v| u32::from(v)).sum())
2338 .collect();
2339 Kv6 {
2340 xsiz,
2341 ysiz,
2342 zsiz,
2343 xpiv: 0.0,
2344 ypiv: 0.0,
2345 zpiv: 0.0,
2346 voxels: flat,
2347 xlen,
2348 ylen,
2349 palette: None,
2350 }
2351 }
2352
2353 fn offsets_consistent(m: &SpriteModel) -> bool {
2354 let cols = (m.dims[0] * m.dims[1]) as usize;
2355 if m.color_offsets.len() != cols + 1 {
2356 return false;
2357 }
2358 for w in m.color_offsets.windows(2) {
2361 if w[1] < w[0] {
2362 return false;
2363 }
2364 }
2365 m.color_offsets[cols] as usize == m.colors.len()
2366 }
2367
2368 #[test]
2369 fn carve_two_layers_keeps_offsets_consistent() {
2370 let kv6 = kv6_from(
2373 3,
2374 2,
2375 8,
2376 &[
2377 (0, 0, 0, 0xA0),
2378 (0, 0, 1, 0xA1),
2379 (0, 0, 5, 0xA5),
2380 (1, 0, 1, 0xB1),
2381 (2, 1, 0, 0xC0),
2382 (2, 1, 3, 0xC3),
2383 ],
2384 );
2385 let mut m = build_sprite_model(&kv6);
2386 assert!(offsets_consistent(&m));
2387 for z in 0..2u32 {
2388 for y in 0..m.dims[1] {
2389 for x in 0..m.dims[0] {
2390 m.set_voxel(x, y, z, None);
2391 }
2392 }
2393 assert!(offsets_consistent(&m), "inconsistent after carving z={z}");
2394 let _ = m.downsample();
2396 }
2397 }
2398
2399 #[test]
2400 fn set_voxel_inserts_replaces_and_clears() {
2401 let mut m = build_sprite_model(&kv6_unsorted());
2403
2404 assert!(m.set_voxel(0, 0, 3, Some(0x55)));
2406 assert_eq!(m.occupancy[0], (1 << 1) | (1 << 3) | (1 << 5));
2407 assert_eq!(m.color_offsets, vec![0, 3, 4]);
2409 assert_eq!(&m.colors, &[0xBB, 0x55, 0xAA, 0xCC]);
2410
2411 assert!(m.set_voxel(0, 0, 3, Some(0x66)));
2413 assert_eq!(&m.colors, &[0xBB, 0x66, 0xAA, 0xCC]);
2414 assert_eq!(m.color_offsets, vec![0, 3, 4]);
2415
2416 assert!(m.set_voxel(0, 0, 1, None));
2418 assert_eq!(m.occupancy[0], (1 << 3) | (1 << 5));
2419 assert_eq!(m.color_offsets, vec![0, 2, 3]);
2420 assert_eq!(&m.colors, &[0x66, 0xAA, 0xCC]);
2421
2422 assert!(!m.set_voxel(0, 0, 2, None));
2424 assert!(!m.set_voxel(9, 0, 0, Some(1)));
2425 }
2426
2427 #[test]
2428 fn rebuild_lod_refreshes_coarse_levels_from_mip0() {
2429 let mut reg = SpriteModelRegistry::new();
2430 let id = reg.add_lod(build_sprite_model(&kv6_unsorted()), 3);
2431 reg.model_mut(id).recolor(|_| 0x0000_2000);
2433 reg.rebuild_lod(id);
2434 let lvl1_entry = reg.chains[id as usize][1] as usize;
2436 assert!(reg.entries[lvl1_entry]
2437 .colors
2438 .iter()
2439 .all(|&c| c == 0x0000_2000));
2440 }
2441
2442 fn alloc_invariants(a: &ColorsAllocator, lens: &[u32]) {
2447 let mut prev_end = 0u32;
2448 for (e, &len) in lens.iter().enumerate() {
2449 let s = a.slot(e);
2450 assert_eq!(s.len, len, "slot {e} len");
2451 assert!(s.cap >= s.len, "slot {e} cap >= len");
2452 assert!(s.off >= prev_end, "slot {e} overlaps previous");
2454 assert!(s.off + s.cap <= a.cap_total(), "slot {e} past cap_total");
2455 prev_end = s.off + s.cap;
2456 }
2457 assert!(a.cap_total() >= prev_end, "tail headroom");
2458 }
2459
2460 #[test]
2461 fn allocator_new_lays_out_with_slack_and_headroom() {
2462 let lens = [10u32, 0, 64, 7];
2463 let a = ColorsAllocator::new(&lens);
2464 alloc_invariants(&a, &lens);
2465 assert!(a.slot(2).cap > 64);
2467 assert!(a.cap_total() > a.slot(3).off + a.slot(3).cap);
2469 }
2470
2471 #[test]
2472 fn allocator_place_in_place_when_within_cap() {
2473 let mut a = ColorsAllocator::new(&[10, 20]);
2474 let off0 = a.slot(0).off;
2475 let cap0 = a.slot(0).cap;
2476 assert_eq!(a.place(0, 5), Some(off0));
2478 assert_eq!(a.slot(0).len, 5);
2479 assert_eq!(a.slot(0).cap, cap0);
2480 assert_eq!(a.place(0, cap0), Some(off0));
2482 assert_eq!(a.slot(0).off, off0);
2483 assert!(a.free.is_empty(), "no relocation should free anything");
2484 }
2485
2486 #[test]
2487 fn allocator_place_relocates_to_tail_and_frees_old() {
2488 let mut a = ColorsAllocator::new(&[10, 20]);
2489 let old0 = (a.slot(0).off, a.slot(0).cap);
2490 let tail_before = a.tail;
2491 let new_len = a.slot(0).cap + 5;
2493 let off = a.place(0, new_len).expect("fits in headroom");
2494 assert_eq!(off, tail_before, "relocated to old tail");
2495 assert_eq!(a.slot(0).off, off);
2496 assert_eq!(a.slot(0).len, new_len);
2497 assert!(a.free.contains(&old0), "old slot freed");
2498 }
2499
2500 #[test]
2501 fn allocator_reuses_freed_block_first_fit() {
2502 let mut a = ColorsAllocator::new(&[10, 2]);
2505 let old0 = (a.slot(0).off, a.slot(0).cap);
2506 let _ = a.place(0, a.slot(0).cap + 5).unwrap();
2508 assert!(a.free.contains(&old0));
2509 let new1 = a.slot(1).cap + 1;
2512 assert!(new1 <= old0.1, "freed block big enough");
2513 let off = a.place(1, new1).expect("reuses freed block");
2514 assert_eq!(off, old0.0, "first-fit reused the freed slot offset");
2515 assert!(!a.free.contains(&old0), "freed block consumed");
2516 }
2517
2518 #[test]
2519 fn allocator_signals_grow_then_repack_restores() {
2520 let mut a = ColorsAllocator::new(&[8, 8]);
2521 let huge = a.cap_total() + 100;
2523 assert_eq!(a.place(0, huge), None, "overflow must signal grow");
2524 a.repack(&[huge, 8]);
2526 alloc_invariants(&a, &[huge, 8]);
2527 assert!(a.cap_total() > huge);
2528 assert_eq!(a.place(0, huge), Some(a.slot(0).off));
2530 }
2531
2532 #[test]
2539 fn allocator_carve_loop_keeps_live_windows_disjoint() {
2540 let mut a = ColorsAllocator::new(&[40, 12, 40]);
2541 let mut lens = [40u32, 12, 40];
2542 let walk = [13u32, 30, 60, 18, 9, 80, 80, 25, 200, 7];
2545 let mut grew = false;
2546 for &len in &walk {
2547 lens[1] = len;
2548 if a.place(1, len).is_none() {
2550 grew = true;
2551 a.repack(&lens);
2552 } else {
2553 assert_eq!(a.place(0, 40), Some(a.slot(0).off));
2555 assert_eq!(a.place(2, 40), Some(a.slot(2).off));
2556 }
2557 assert_eq!(a.slot(1).len, len);
2558
2559 let mut wins: Vec<(u32, u32)> =
2561 (0..3).map(|e| (a.slot(e).off, a.slot(e).len)).collect();
2562 wins.sort_by_key(|w| w.0);
2563 for pair in wins.windows(2) {
2564 let (o0, l0) = pair[0];
2565 let (o1, _) = pair[1];
2566 assert!(o0 + l0 <= o1, "live windows overlap: {pair:?}");
2567 }
2568 }
2569 assert!(grew, "the 200-word jump should have forced a repack");
2570 }
2571
2572 fn headless() -> Option<crate::HeadlessGpu> {
2575 match crate::HeadlessGpu::new_blocking(crate::GpuRendererSettings::default()) {
2576 Ok(h) => Some(h),
2577 Err(e) => {
2578 eprintln!("[skip] no GPU adapter reachable: {e}");
2579 None
2580 }
2581 }
2582 }
2583
2584 fn one_model_registry() -> (SpriteModelRegistry, u32) {
2585 let mut reg = SpriteModelRegistry::new();
2586 let id = reg.add(build_sprite_model(&kv6_unsorted()));
2587 (reg, id)
2588 }
2589
2590 fn inst(model_id: u32, pos: [f32; 3]) -> SpriteInstance {
2591 use roxlap_formats::sprite::Sprite;
2592 SpriteInstance::new(
2593 model_id,
2594 SpriteInstanceTransform::from_sprite(&Sprite::axis_aligned(kv6_unsorted(), pos)),
2595 )
2596 }
2597
2598 #[test]
2599 fn append_grows_count_and_capacity_pow2() {
2600 let Some(h) = headless() else { return };
2601 let (reg, m) = one_model_registry();
2602 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(m, [0.0; 3])]);
2603 assert_eq!(res.instance_count(), 1);
2604 assert_eq!(res.instance_capacity, 1);
2605
2606 let more: Vec<_> = (1..=4).map(|i| inst(m, [i as f32, 0.0, 0.0])).collect();
2608 let base = res.append_instances(&h.device, ®, &more);
2609 assert_eq!(base, 1, "first appended index follows the seed instance");
2610 assert_eq!(res.instance_count(), 5);
2611 assert_eq!(res.instance_capacity, 8, "power-of-two growth");
2612
2613 let base2 = res.append_instances(&h.device, ®, &[inst(m, [9.0, 0.0, 0.0])]);
2615 assert_eq!(base2, 5);
2616 assert_eq!(res.instance_count(), 6);
2617 assert_eq!(res.instance_capacity, 8, "fits existing capacity, no grow");
2618 }
2619
2620 #[test]
2621 fn append_empty_is_noop() {
2622 let Some(h) = headless() else { return };
2623 let (reg, m) = one_model_registry();
2624 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(m, [0.0; 3])]);
2625 let base = res.append_instances(&h.device, ®, &[]);
2626 assert_eq!(base, 1);
2627 assert_eq!(res.instance_count(), 1);
2628 assert_eq!(res.instance_capacity, 1);
2629 }
2630
2631 fn read_u32(h: &crate::HeadlessGpu, buf: &wgpu::Buffer, words: u64) -> Vec<u32> {
2633 let bytes = words * 4;
2634 let staging = h.device.create_buffer(&wgpu::BufferDescriptor {
2635 label: Some("readback"),
2636 size: bytes,
2637 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2638 mapped_at_creation: false,
2639 });
2640 let mut enc = h
2641 .device
2642 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
2643 enc.copy_buffer_to_buffer(buf, 0, &staging, 0, bytes);
2644 h.queue.submit(std::iter::once(enc.finish()));
2645 let slice = staging.slice(..);
2646 let (tx, rx) = std::sync::mpsc::channel();
2647 slice.map_async(wgpu::MapMode::Read, move |r| tx.send(r).unwrap());
2648 h.device.poll(wgpu::PollType::wait_indefinitely()).ok();
2649 rx.recv().unwrap().unwrap();
2650 let data = slice.get_mapped_range();
2651 let out = bytemuck::cast_slice::<u8, u32>(&data).to_vec();
2652 drop(data);
2653 staging.unmap();
2654 out
2655 }
2656
2657 fn kv6_other() -> Kv6 {
2660 let mk = |z, col| Voxel {
2661 col,
2662 z,
2663 vis: 0,
2664 dir: 0,
2665 };
2666 Kv6 {
2667 xsiz: 1,
2668 ysiz: 1,
2669 zsiz: 4,
2670 xpiv: 0.0,
2671 ypiv: 0.0,
2672 zpiv: 0.0,
2673 voxels: vec![mk(0, 0x11), mk(2, 0x22)],
2674 xlen: vec![2],
2675 ylen: vec![vec![2]],
2676 palette: None,
2677 }
2678 }
2679
2680 #[test]
2684 fn add_model_uploads_new_volume_incrementally() {
2685 let Some(h) = headless() else { return };
2686
2687 let mut reg = SpriteModelRegistry::new();
2689 let a = reg.add(build_sprite_model(&kv6_unsorted()));
2690 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(a, [0.0; 3])]);
2691 assert_eq!(res.chains.len(), 1);
2692 let entries_before = res.meta.len();
2693
2694 let b = reg.add(build_sprite_model(&kv6_other()));
2696 res.add_model(&h.device, &h.queue, ®, b);
2697 assert_eq!(res.chains.len(), 2);
2698 assert_eq!(res.meta.len(), entries_before + 1, "one new entry");
2699
2700 let occ = read_u32(&h, &res.occupancy, u64::from(res.occ_cap));
2704 let coloff = read_u32(&h, &res.color_offsets, u64::from(res.coloff_cap));
2705 let cols = read_u32(&h, &res.colors, u64::from(res.colors_alloc.cap_total()));
2706 for (e, m) in reg.entries.iter().enumerate() {
2707 let meta = res.meta[e];
2708 let oo = meta.occupancy_offset as usize;
2709 assert_eq!(
2710 &occ[oo..oo + m.occupancy.len()],
2711 &m.occupancy[..],
2712 "occ entry {e}"
2713 );
2714 let co = meta.color_offsets_offset as usize;
2715 assert_eq!(
2716 &coloff[co..co + m.color_offsets.len()],
2717 &m.color_offsets[..],
2718 "color_offsets entry {e}"
2719 );
2720 let cc = meta.colors_offset as usize;
2721 assert_eq!(
2722 &cols[cc..cc + m.colors.len()],
2723 &m.colors[..],
2724 "colors entry {e}"
2725 );
2726 }
2727
2728 let base = res.append_instances(&h.device, ®, &[inst(b, [5.0, 0.0, 0.0])]);
2730 assert_eq!(base, 1);
2731 assert_eq!(res.instance_count(), 2);
2732 }
2733
2734 #[test]
2738 fn add_model_survives_buffer_growth() {
2739 let Some(h) = headless() else { return };
2740 let mut reg = SpriteModelRegistry::new();
2741 let a = reg.add(build_sprite_model(&kv6_unsorted()));
2742 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(a, [0.0; 3])]);
2743 let occ_cap0 = res.occ_cap;
2744
2745 for _ in 0..40 {
2748 let id = reg.add(build_sprite_model(&kv6_other()));
2749 res.add_model(&h.device, &h.queue, ®, id);
2750 }
2751 assert_eq!(res.chains.len(), 41);
2752 assert!(res.occ_cap > occ_cap0, "occupancy buffer grew");
2753
2754 let occ = read_u32(&h, &res.occupancy, u64::from(res.occ_cap));
2755 let cols = read_u32(&h, &res.colors, u64::from(res.colors_alloc.cap_total()));
2756 for (e, m) in reg.entries.iter().enumerate() {
2757 let meta = res.meta[e];
2758 let oo = meta.occupancy_offset as usize;
2759 assert_eq!(
2760 &occ[oo..oo + m.occupancy.len()],
2761 &m.occupancy[..],
2762 "occ entry {e}"
2763 );
2764 let cc = meta.colors_offset as usize;
2765 assert_eq!(
2766 &cols[cc..cc + m.colors.len()],
2767 &m.colors[..],
2768 "colors entry {e}"
2769 );
2770 }
2771 }
2772
2773 #[test]
2782 fn clip_frame_with_materials_classifies_by_color() {
2783 use roxlap_formats::voxel_clip::{LoopMode, VoxelClip, VoxelFrame};
2784
2785 let dims = [1u32, 1, 4];
2786 let owpc = dims[2].div_ceil(32).max(1) as usize; let glass = 0x80AA_BBCC;
2788 let stone = 0x8011_2233;
2789 let frame = VoxelFrame {
2790 occupancy: {
2791 let mut occ = vec![0u32; owpc];
2792 occ[0] |= (1 << 0) | (1 << 1);
2793 occ
2794 },
2795 colors: vec![stone, glass], color_offsets: vec![0, 2],
2797 };
2798 let clip = VoxelClip::from_frames(
2799 dims,
2800 [0.5, 0.5, 2.0],
2801 1.0,
2802 LoopMode::Loop,
2803 &[frame],
2804 &[],
2805 33,
2806 0,
2807 );
2808 let decoded = clip.decode().expect("decode");
2809
2810 let m = sprite_model_from_clip_frame_with_materials(&decoded, 0, &[(0x00AA_BBCC, 2)]);
2812 assert_eq!(
2813 m.materials.len(),
2814 m.colors.len(),
2815 "materials parallel to colors"
2816 );
2817 assert_eq!(
2819 m.materials,
2820 vec![0u8, 2u8],
2821 "stone opaque, glass material 2"
2822 );
2823
2824 let plain = sprite_model_from_clip_frame(&decoded, 0);
2826 let plain_mat = sprite_model_from_clip_frame_with_materials(&decoded, 0, &[]);
2827 assert!(plain.materials.is_empty());
2828 assert!(plain_mat.materials.is_empty());
2829 assert_eq!(plain.colors, plain_mat.colors);
2830 }
2831
2832 #[test]
2837 fn build_with_materials_classifies_by_color() {
2838 let glass = 0x80AA_BBCC;
2839 let stone = 0x8011_2233;
2840 let kv6 = kv6_from(1, 1, 4, &[(0, 0, 0, stone), (0, 0, 1, glass)]);
2842
2843 let m = build_sprite_model_with_materials(&kv6, &[(0x00AA_BBCC, 2)]);
2844 assert_eq!(
2845 m.materials.len(),
2846 m.colors.len(),
2847 "materials parallel to colors"
2848 );
2849 assert_eq!(
2850 m.materials,
2851 vec![0u8, 2u8],
2852 "stone opaque, glass material 2"
2853 );
2854
2855 let plain = build_sprite_model(&kv6);
2857 let plain_mat = build_sprite_model_with_materials(&kv6, &[]);
2858 assert!(plain.materials.is_empty());
2859 assert!(plain_mat.materials.is_empty());
2860 assert_eq!(plain.colors, plain_mat.colors);
2861 }
2862
2863 #[test]
2866 fn voxel_clip_flipbook_set_instance_model() {
2867 use roxlap_formats::voxel_clip::{LoopMode, VoxelClip, VoxelFrame};
2868 let Some(h) = headless() else { return };
2869
2870 let dims = [1u32, 1, 4];
2873 let owpc = dims[2].div_ceil(32).max(1) as usize; let mk_frame = |zs: &[u32], cols: &[u32]| -> VoxelFrame {
2875 let mut occ = vec![0u32; owpc];
2876 for &z in zs {
2877 occ[(z >> 5) as usize] |= 1u32 << (z & 31);
2878 }
2879 VoxelFrame {
2880 occupancy: occ,
2881 colors: cols.to_vec(),
2882 color_offsets: vec![0, cols.len() as u32],
2883 }
2884 };
2885 let f0 = mk_frame(&[0], &[0x8011_2233]);
2886 let f1 = mk_frame(&[0, 1], &[0x8011_2233, 0x80AA_BBCC]);
2887 let clip = VoxelClip::from_frames(
2888 dims,
2889 [0.5, 0.5, 2.0],
2890 1.0,
2891 LoopMode::Loop,
2892 &[f0, f1],
2893 &[],
2894 33,
2895 0,
2896 );
2897 let decoded = clip.decode().expect("decode");
2898
2899 let mut reg = SpriteModelRegistry::new();
2901 let c0 = reg.add(sprite_model_from_clip_frame(&decoded, 0));
2902 let c1 = reg.add(sprite_model_from_clip_frame(&decoded, 1));
2903 assert_eq!(reg.model(c0).colors.len(), 1);
2904 assert_eq!(reg.model(c1).colors.len(), 2);
2905
2906 let mut res = SpriteRegistryResident::upload(&h.device, ®, &[inst(c0, [0.0, 0.0, 5.0])]);
2908 assert_eq!(res.cull[0].chain_id, c0);
2909
2910 res.set_instance_model(®, 0, c1);
2912 assert_eq!(res.cull[0].chain_id, c1);
2913 assert_eq!(res.cull[0].radius, reg.model(c1).bound_radius());
2914
2915 let f = test_frustum();
2918 let (visible, _, _) = res.cull_bin_upload(&h.device, &h.queue, &f, 64, 64, 16, 1.0);
2919 assert_eq!(visible, 1);
2920
2921 res.set_instance_model(®, 0, c0);
2923 assert_eq!(res.cull[0].chain_id, c0);
2924
2925 res.set_instance_model(®, 99, c1);
2927 assert_eq!(res.cull[0].chain_id, c0);
2928 }
2929
2930 fn test_frustum() -> ViewFrustum {
2931 ViewFrustum {
2932 pos: [0.0, 0.0, 0.0],
2933 right: [1.0, 0.0, 0.0],
2934 down: [0.0, 1.0, 0.0],
2935 forward: [0.0, 0.0, 1.0],
2936 half_w: 1.0,
2937 half_h: 1.0,
2938 far: 10_000.0,
2939 }
2940 }
2941
2942 #[test]
2943 fn remove_model_tombstones_frees_and_reuses() {
2944 let Some(h) = headless() else { return };
2945 let mut reg = SpriteModelRegistry::new();
2947 let a = reg.add(build_sprite_model(&kv6_unsorted()));
2948 let b = reg.add(build_sprite_model(&kv6_other()));
2949 let mut res = SpriteRegistryResident::upload(
2950 &h.device,
2951 ®,
2952 &[inst(a, [0.0; 3]), inst(b, [1.0, 0.0, 0.0])],
2953 );
2954 assert_eq!(res.live_model_count(), 2);
2955 assert_eq!(res.dead_model_count(), 0);
2956
2957 res.remove_model(b);
2959 assert_eq!(res.live_model_count(), 1);
2960 assert_eq!(res.dead_model_count(), 1);
2961 assert_eq!(res.dead.iter().filter(|&&d| d).count(), 1, "one entry dead");
2962 assert!(!res.colors_alloc.free.is_empty(), "B's colour slot freed");
2963
2964 let c = reg.add(build_sprite_model(&kv6_other()));
2966 res.add_model(&h.device, &h.queue, ®, c);
2967 assert_eq!(res.live_model_count(), 2);
2968
2969 let cols = read_u32(&h, &res.colors, u64::from(res.colors_alloc.cap_total()));
2971 for e in [a as usize, c as usize] {
2972 let m = ®.entries[e];
2973 let cc = res.meta[e].colors_offset as usize;
2974 assert_eq!(
2975 &cols[cc..cc + m.colors.len()],
2976 &m.colors[..],
2977 "colors entry {e}"
2978 );
2979 }
2980
2981 let f = test_frustum();
2983 let _ = res.cull_bin_upload(&h.device, &h.queue, &f, 64, 64, 16, 1.0);
2984 }
2985
2986 #[test]
2987 fn compact_reclaims_holes_keeps_ids_stable() {
2988 let Some(h) = headless() else { return };
2989 let mut reg = SpriteModelRegistry::new();
2990 let a = reg.add(build_sprite_model(&kv6_unsorted()));
2991 let b = reg.add(build_sprite_model(&kv6_other()));
2992 let c = reg.add(build_sprite_model(&kv6_other()));
2993 let mut res = SpriteRegistryResident::upload(
2994 &h.device,
2995 ®,
2996 &[inst(a, [0.0; 3]), inst(b, [1.0; 3]), inst(c, [2.0; 3])],
2997 );
2998 let occ_used_full = res.occ_used;
2999
3000 res.remove_model(b);
3002 res.compact(&h.device, &h.queue, ®);
3003
3004 let live_occ: u32 = [a, c]
3006 .iter()
3007 .map(|&e| reg.entries[e as usize].occupancy.len() as u32)
3008 .sum();
3009 assert_eq!(res.occ_used, live_occ);
3010 assert!(res.occ_used < occ_used_full, "compaction shrank occupancy");
3011 assert_eq!(res.meta[b as usize].occupancy_offset, 0);
3013 assert_eq!(res.live_model_count(), 2);
3014 assert_eq!(res.dead_model_count(), 1);
3015
3016 let occ = read_u32(&h, &res.occupancy, u64::from(res.occ_cap));
3018 let cols = read_u32(&h, &res.colors, u64::from(res.colors_alloc.cap_total()));
3019 for &e in &[a as usize, c as usize] {
3020 let m = ®.entries[e];
3021 let oo = res.meta[e].occupancy_offset as usize;
3022 assert_eq!(
3023 &occ[oo..oo + m.occupancy.len()],
3024 &m.occupancy[..],
3025 "occ {e}"
3026 );
3027 let cc = res.meta[e].colors_offset as usize;
3028 assert_eq!(&cols[cc..cc + m.colors.len()], &m.colors[..], "cols {e}");
3029 }
3030
3031 assert!(!res.chains[c as usize].is_empty());
3033 assert!(res.chains[b as usize].is_empty());
3034 }
3035
3036 #[test]
3037 fn remove_swap_semantics_and_capacity_retained() {
3038 let Some(h) = headless() else { return };
3039 let (reg, m) = one_model_registry();
3040 let seed: Vec<_> = (0..4).map(|i| inst(m, [i as f32, 0.0, 0.0])).collect();
3041 let mut res = SpriteRegistryResident::upload(&h.device, ®, &seed);
3042 assert_eq!(res.instance_count(), 4);
3043 let cap = res.instance_capacity;
3044
3045 assert_eq!(res.remove_instance(1), Some(3));
3047 assert_eq!(res.instance_count(), 3);
3048
3049 assert_eq!(res.remove_instance(2), None);
3051 assert_eq!(res.instance_count(), 2);
3052
3053 assert_eq!(res.remove_instance(99), None);
3055 assert_eq!(res.instance_count(), 2);
3056
3057 assert_eq!(res.instance_capacity, cap);
3059 }
3060}