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::sprite::Sprite;
28
29#[derive(Debug, Clone)]
31pub struct SpriteModel {
32 pub dims: [u32; 3],
34 pub occ_words_per_col: u32,
36 pub pivot: [f32; 3],
38 pub occupancy: Vec<u32>,
40 pub colors: Vec<u32>,
42 pub dirs: Vec<u32>,
47 pub color_offsets: Vec<u32>,
50 pub voxel_world_size: f32,
55}
56
57#[must_use]
65pub fn build_sprite_model(kv6: &Kv6) -> SpriteModel {
66 let (mx, my, mz) = (kv6.xsiz, kv6.ysiz, kv6.zsiz);
67 let occ_words_per_col = mz.div_ceil(32).max(1);
68 let cols = (mx * my) as usize;
69
70 let mut occupancy = vec![0u32; cols * occ_words_per_col as usize];
71 let mut color_offsets = vec![0u32; cols + 1];
72 let mut colors: Vec<u32> = Vec::with_capacity(kv6.voxels.len());
73 let mut dirs: Vec<u32> = Vec::with_capacity(kv6.voxels.len());
74
75 let mut buckets: Vec<Vec<(u16, u32, u8)>> = vec![Vec::new(); cols];
79 let mut voxel_iter = kv6.voxels.iter();
80 for x in 0..mx {
81 for y in 0..my {
82 let col = (x + y * mx) as usize;
83 let count = kv6.ylen[x as usize][y as usize];
84 for _ in 0..count {
85 let v = voxel_iter.next().expect("KV6 ylen / voxels.len mismatch");
86 buckets[col].push((v.z, v.col, v.dir));
87 }
88 }
89 }
90
91 for (col, bucket) in buckets.iter_mut().enumerate() {
96 color_offsets[col] = colors.len() as u32;
97 bucket.sort_by_key(|(z, _, _)| *z);
98 for &(z, col_rgba, dir) in bucket.iter() {
99 let z = u32::from(z);
100 let base = col * occ_words_per_col as usize + (z >> 5) as usize;
101 occupancy[base] |= 1u32 << (z & 31);
102 colors.push(col_rgba);
103 dirs.push(u32::from(dir));
104 }
105 }
106 color_offsets[cols] = colors.len() as u32;
107
108 SpriteModel {
109 dims: [mx, my, mz],
110 occ_words_per_col,
111 pivot: [kv6.xpiv, kv6.ypiv, kv6.zpiv],
112 occupancy,
113 color_offsets,
114 colors,
115 dirs,
116 voxel_world_size: 1.0,
117 }
118}
119
120#[repr(C)]
125#[derive(Clone, Copy, Pod, Zeroable, Debug)]
126pub struct SpriteInstanceTransform {
127 pub inv_rot: [[f32; 4]; 3],
130 pub pos: [f32; 3],
132 _pad: f32,
133}
134
135impl SpriteInstanceTransform {
136 #[must_use]
139 pub fn from_sprite(sprite: &Sprite) -> Self {
140 let inv = mat3_inverse([sprite.s, sprite.h, sprite.f]);
141 Self {
142 inv_rot: [
143 [inv[0][0], inv[0][1], inv[0][2], 0.0],
144 [inv[1][0], inv[1][1], inv[1][2], 0.0],
145 [inv[2][0], inv[2][1], inv[2][2], 0.0],
146 ],
147 pos: sprite.p,
148 _pad: 0.0,
149 }
150 }
151}
152
153#[derive(Debug, Clone, Default)]
161pub struct SpriteModelRegistry {
162 entries: Vec<SpriteModel>,
164 chains: Vec<Vec<u32>>,
166}
167
168impl SpriteModelRegistry {
169 #[must_use]
170 pub fn new() -> Self {
171 Self::default()
172 }
173
174 fn push_entry(&mut self, model: SpriteModel) -> u32 {
175 let id = self.entries.len() as u32;
176 self.entries.push(model);
177 id
178 }
179
180 pub fn add(&mut self, model: SpriteModel) -> u32 {
182 let e = self.push_entry(model);
183 let id = self.chains.len() as u32;
184 self.chains.push(vec![e]);
185 id
186 }
187
188 pub fn add_lod(&mut self, model: SpriteModel, max_levels: u32) -> u32 {
192 let mut levels = vec![self.push_entry(model.clone())];
193 let mut cur = model;
194 for _ in 1..max_levels.max(1) {
195 if cur.dims == [1, 1, 1] {
196 break;
197 }
198 cur = cur.downsample();
199 levels.push(self.push_entry(cur.clone()));
200 }
201 let id = self.chains.len() as u32;
202 self.chains.push(levels);
203 id
204 }
205
206 pub fn fork(&mut self, parent: u32) -> u32 {
214 let src = self.chains[parent as usize].clone();
215 let levels: Vec<u32> = src
216 .iter()
217 .map(|&e| {
218 let copy = self.entries[e as usize].clone();
219 self.push_entry(copy)
220 })
221 .collect();
222 let id = self.chains.len() as u32;
223 self.chains.push(levels);
224 id
225 }
226
227 #[must_use]
229 pub fn model(&self, id: u32) -> &SpriteModel {
230 &self.entries[self.chains[id as usize][0] as usize]
231 }
232
233 pub fn model_mut(&mut self, id: u32) -> &mut SpriteModel {
239 let e = self.chains[id as usize][0] as usize;
240 &mut self.entries[e]
241 }
242
243 pub fn recolor_chain(&mut self, id: u32, f: impl Fn(u32) -> u32 + Copy) {
246 for li in 0..self.chains[id as usize].len() {
247 let e = self.chains[id as usize][li] as usize;
248 self.entries[e].recolor(f);
249 }
250 }
251
252 pub fn rebuild_lod(&mut self, id: u32) {
257 let levels = self.chains[id as usize].clone();
258 if levels.len() <= 1 {
259 return;
260 }
261 let mut cur = self.entries[levels[0] as usize].clone();
262 for &e in &levels[1..] {
263 cur = cur.downsample();
264 self.entries[e as usize] = cur.clone();
265 }
266 }
267
268 #[must_use]
270 pub fn len(&self) -> usize {
271 self.chains.len()
272 }
273
274 #[must_use]
275 pub fn is_empty(&self) -> bool {
276 self.chains.is_empty()
277 }
278}
279
280impl SpriteModel {
281 pub fn recolor(&mut self, f: impl Fn(u32) -> u32) {
287 for c in &mut self.colors {
288 *c = f(*c);
289 }
290 }
291
292 pub fn set_voxel(&mut self, x: u32, y: u32, z: u32, color: Option<u32>) -> bool {
303 if x >= self.dims[0] || y >= self.dims[1] || z >= self.dims[2] {
304 return false;
305 }
306 let owpc = self.occ_words_per_col as usize;
307 let cols = (self.dims[0] * self.dims[1]) as usize;
308 let col = (x + y * self.dims[0]) as usize;
309 let base = col * owpc;
310 let zw = (z >> 5) as usize;
311 let zb = z & 31;
312
313 let mut rank = 0usize;
315 for w in 0..zw {
316 rank += self.occupancy[base + w].count_ones() as usize;
317 }
318 let below_mask = if zb > 0 { (1u32 << zb) - 1 } else { 0 };
319 rank += (self.occupancy[base + zw] & below_mask).count_ones() as usize;
320 let idx = self.color_offsets[col] as usize + rank;
321 let was_set = (self.occupancy[base + zw] >> zb) & 1 == 1;
322
323 if let Some(rgba) = color {
324 if was_set {
325 self.colors[idx] = rgba; } else {
327 self.occupancy[base + zw] |= 1u32 << zb;
328 self.colors.insert(idx, rgba);
329 self.dirs.insert(idx, 0);
332 for c in &mut self.color_offsets[col + 1..=cols] {
333 *c += 1;
334 }
335 }
336 true
337 } else {
338 if !was_set {
339 return false;
340 }
341 self.occupancy[base + zw] &= !(1u32 << zb);
342 self.colors.remove(idx);
343 self.dirs.remove(idx);
344 for c in &mut self.color_offsets[col + 1..=cols] {
345 *c -= 1;
346 }
347 true
348 }
349 }
350
351 #[must_use]
356 pub fn bound_radius(&self) -> f32 {
357 let mut r2 = 0.0_f32;
358 for &cx in &[0.0, self.dims[0] as f32] {
359 for &cy in &[0.0, self.dims[1] as f32] {
360 for &cz in &[0.0, self.dims[2] as f32] {
361 let d = [cx - self.pivot[0], cy - self.pivot[1], cz - self.pivot[2]];
362 r2 = r2.max(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
363 }
364 }
365 }
366 r2.sqrt()
367 }
368
369 #[must_use]
375 #[allow(clippy::manual_checked_ops)] pub fn downsample(&self) -> SpriteModel {
377 let [fx, fy, fz] = self.dims;
378 let fidx = |x: u32, y: u32, z: u32| (x + y * fx + z * fx * fy) as usize;
379
380 let mut solid = vec![false; (fx * fy * fz) as usize];
382 let mut fine = vec![0u32; (fx * fy * fz) as usize];
383 let mut fine_dir = vec![0u32; (fx * fy * fz) as usize];
384 for x in 0..fx {
385 for y in 0..fy {
386 let col = (x + y * fx) as usize;
387 let base = col * self.occ_words_per_col as usize;
388 let off = self.color_offsets[col] as usize;
389 let mut seen = 0usize;
390 for z in 0..fz {
391 let w = base + (z >> 5) as usize;
392 if (self.occupancy[w] >> (z & 31)) & 1 == 1 {
393 fine[fidx(x, y, z)] = self.colors[off + seen];
394 fine_dir[fidx(x, y, z)] = self.dirs[off + seen];
395 solid[fidx(x, y, z)] = true;
396 seen += 1;
397 }
398 }
399 }
400 }
401
402 let nx = fx.div_ceil(2).max(1);
403 let ny = fy.div_ceil(2).max(1);
404 let nz = fz.div_ceil(2).max(1);
405 let owpc = nz.div_ceil(32).max(1);
406 let cols = (nx * ny) as usize;
407 let mut occupancy = vec![0u32; cols * owpc as usize];
408 let mut color_offsets = vec![0u32; cols + 1];
409 let mut colors: Vec<u32> = Vec::new();
410 let mut dirs: Vec<u32> = Vec::new();
411
412 for cy in 0..ny {
415 for cx in 0..nx {
416 let ccol = (cx + cy * nx) as usize;
417 color_offsets[ccol] = colors.len() as u32;
418 for cz in 0..nz {
419 let (mut a, mut r, mut g, mut b, mut n) = (0u32, 0u32, 0u32, 0u32, 0u32);
420 let mut rep_dir = 0u32;
423 for dz in 0..2 {
424 for dy in 0..2 {
425 for dx in 0..2 {
426 let (x, y, z) = (2 * cx + dx, 2 * cy + dy, 2 * cz + dz);
427 if x < fx && y < fy && z < fz && solid[fidx(x, y, z)] {
428 let c = fine[fidx(x, y, z)];
429 if n == 0 {
430 rep_dir = fine_dir[fidx(x, y, z)];
431 }
432 a += (c >> 24) & 0xff;
433 r += (c >> 16) & 0xff;
434 g += (c >> 8) & 0xff;
435 b += c & 0xff;
436 n += 1;
437 }
438 }
439 }
440 }
441 if n > 0 {
442 let avg = ((a / n) << 24) | ((r / n) << 16) | ((g / n) << 8) | (b / n);
443 let base = ccol * owpc as usize + (cz >> 5) as usize;
444 occupancy[base] |= 1u32 << (cz & 31);
445 colors.push(avg);
446 dirs.push(rep_dir);
447 }
448 }
449 }
450 }
451 color_offsets[cols] = colors.len() as u32;
452
453 SpriteModel {
454 dims: [nx, ny, nz],
455 occ_words_per_col: owpc,
456 pivot: [
457 self.pivot[0] * 0.5,
458 self.pivot[1] * 0.5,
459 self.pivot[2] * 0.5,
460 ],
461 occupancy,
462 colors,
463 dirs,
464 color_offsets,
465 voxel_world_size: self.voxel_world_size * 2.0,
466 }
467 }
468}
469
470#[derive(Clone, Copy, Debug)]
475pub struct ViewFrustum {
476 pub pos: [f32; 3],
477 pub right: [f32; 3],
478 pub down: [f32; 3],
479 pub forward: [f32; 3],
480 pub half_w: f32,
481 pub half_h: f32,
482 pub far: f32,
483}
484
485#[derive(Clone)]
488struct CullInstance {
489 gpu: SpriteInstanceGpu,
492 chain_id: u32,
494 center: [f32; 3],
495 radius: f32,
496 colmul: Box<[u64; 256]>,
502}
503
504fn identity_colmul() -> Box<[u64; 256]> {
507 const LANE: u64 = 0x0100;
508 let w = LANE | (LANE << 16) | (LANE << 32) | (LANE << 48);
509 Box::new([w; 256])
510}
511
512fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
513 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
514}
515
516#[derive(Debug, Clone, Copy)]
518pub struct SpriteInstance {
519 pub model_id: u32,
520 pub transform: SpriteInstanceTransform,
521}
522
523#[repr(C)]
527#[derive(Clone, Copy, Pod, Zeroable, Debug)]
528struct SpriteModelMeta {
529 occupancy_offset: u32,
530 colors_offset: u32,
531 color_offsets_offset: u32,
532 occ_words_per_col: u32,
533 dims: [u32; 3],
534 _pad0: u32,
535 pivot: [f32; 3],
536 voxel_world_size: f32,
538}
539
540#[repr(C)]
543#[derive(Clone, Copy, Pod, Zeroable, Debug)]
544struct SpriteInstanceGpu {
545 inv_rot0: [f32; 4],
546 inv_rot1: [f32; 4],
547 inv_rot2: [f32; 4],
548 pos: [f32; 3],
549 model_id: u32,
550}
551
552#[must_use]
556fn mat3_inverse(cols: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
557 let [a, b, c] = cols; let cross = |u: [f32; 3], v: [f32; 3]| {
560 [
561 u[1] * v[2] - u[2] * v[1],
562 u[2] * v[0] - u[0] * v[2],
563 u[0] * v[1] - u[1] * v[0],
564 ]
565 };
566 let bc = cross(b, c);
567 let ca = cross(c, a);
568 let ab = cross(a, b);
569 let det = a[0] * bc[0] + a[1] * bc[1] + a[2] * bc[2];
570 let inv_det = if det.abs() < 1e-12 { 0.0 } else { 1.0 / det };
571 [
574 [bc[0] * inv_det, ca[0] * inv_det, ab[0] * inv_det],
575 [bc[1] * inv_det, ca[1] * inv_det, ab[1] * inv_det],
576 [bc[2] * inv_det, ca[2] * inv_det, ab[2] * inv_det],
577 ]
578}
579
580pub struct SpriteRegistryResident {
587 pub occupancy: wgpu::Buffer,
588 pub colors: wgpu::Buffer,
589 pub dirs: wgpu::Buffer,
593 pub color_offsets: wgpu::Buffer,
594 pub model_meta: wgpu::Buffer,
595 pub instances: wgpu::Buffer,
598 pub instance_capacity: u32,
599 pub colmul: wgpu::Buffer,
604 colmul_cap: u32,
605 pub tile_ranges: wgpu::Buffer,
608 tile_ranges_cap: u32,
609 pub tile_instances: wgpu::Buffer,
612 tile_instances_cap: u32,
613 cull: Vec<CullInstance>,
615 chains: Vec<Vec<u32>>,
619}
620
621impl SpriteRegistryResident {
622 #[must_use]
627 pub fn upload(
628 device: &wgpu::Device,
629 registry: &SpriteModelRegistry,
630 instances: &[SpriteInstance],
631 ) -> Self {
632 let mut all_occ: Vec<u32> = Vec::new();
633 let mut all_colors: Vec<u32> = Vec::new();
634 let mut all_dirs: Vec<u32> = Vec::new();
635 let mut all_offsets: Vec<u32> = Vec::new();
636 let mut meta: Vec<SpriteModelMeta> = Vec::with_capacity(registry.entries.len());
637
638 for m in ®istry.entries {
641 meta.push(SpriteModelMeta {
642 occupancy_offset: all_occ.len() as u32,
643 colors_offset: all_colors.len() as u32,
644 color_offsets_offset: all_offsets.len() as u32,
645 occ_words_per_col: m.occ_words_per_col,
646 dims: m.dims,
647 _pad0: 0,
648 pivot: m.pivot,
649 voxel_world_size: m.voxel_world_size,
650 });
651 all_occ.extend_from_slice(&m.occupancy);
652 all_colors.extend_from_slice(&m.colors);
653 all_dirs.extend_from_slice(&m.dirs);
654 all_offsets.extend_from_slice(&m.color_offsets);
655 }
656
657 let cull: Vec<CullInstance> = instances
662 .iter()
663 .map(|i| CullInstance {
664 gpu: SpriteInstanceGpu {
665 inv_rot0: i.transform.inv_rot[0],
666 inv_rot1: i.transform.inv_rot[1],
667 inv_rot2: i.transform.inv_rot[2],
668 pos: i.transform.pos,
669 model_id: i.model_id, },
671 chain_id: i.model_id,
672 center: i.transform.pos,
673 radius: registry.model(i.model_id).bound_radius(),
674 colmul: identity_colmul(),
675 })
676 .collect();
677
678 let seed: Vec<SpriteInstanceGpu> = cull.iter().map(|c| c.gpu).collect();
681 let instances_buf = {
682 use wgpu::util::DeviceExt;
683 let one = [SpriteInstanceGpu::zeroed()];
684 let src: &[SpriteInstanceGpu] = if seed.is_empty() { &one } else { &seed };
685 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
686 label: Some("roxlap-gpu sprite_reg.instances"),
687 contents: bytemuck::cast_slice(src),
688 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
689 })
690 };
691
692 let tile_ranges = storage_dst_u32(device, "roxlap-gpu sprite_reg.tile_ranges", 1);
693 let tile_instances = storage_dst_u32(device, "roxlap-gpu sprite_reg.tile_instances", 1);
694 let colmul_cap = (cull.len() as u32).max(1) * 256 * 2;
697 let colmul = storage_dst_u32(device, "roxlap-gpu sprite_reg.colmul", colmul_cap);
698 Self {
699 occupancy: storage_u32(device, "roxlap-gpu sprite_reg.occupancy", &all_occ),
700 colors: storage_u32(device, "roxlap-gpu sprite_reg.colors", &all_colors),
701 dirs: storage_u32(device, "roxlap-gpu sprite_reg.dirs", &all_dirs),
702 color_offsets: storage_u32(device, "roxlap-gpu sprite_reg.color_offsets", &all_offsets),
703 model_meta: storage_pod(device, "roxlap-gpu sprite_reg.model_meta", &meta),
704 instances: instances_buf,
705 instance_capacity: cull.len() as u32,
706 colmul,
707 colmul_cap,
708 tile_ranges,
709 tile_ranges_cap: 1,
710 tile_instances,
711 tile_instances_cap: 1,
712 cull,
713 chains: registry.chains.clone(),
714 }
715 }
716
717 pub fn set_instance_colmul(&mut self, tables: &[[u64; 256]]) {
723 for (ci, t) in self.cull.iter_mut().zip(tables) {
724 ci.colmul.copy_from_slice(t);
725 }
726 }
727
728 pub fn update_transforms(&mut self, instances: &[SpriteInstance]) {
736 debug_assert_eq!(
737 instances.len(),
738 self.cull.len(),
739 "update_transforms instance count must match upload"
740 );
741 for (ci, inst) in self.cull.iter_mut().zip(instances) {
742 ci.gpu.inv_rot0 = inst.transform.inv_rot[0];
743 ci.gpu.inv_rot1 = inst.transform.inv_rot[1];
744 ci.gpu.inv_rot2 = inst.transform.inv_rot[2];
745 ci.gpu.pos = inst.transform.pos;
746 ci.center = inst.transform.pos;
748 }
749 }
750
751 #[allow(clippy::too_many_arguments)]
759 pub fn cull_bin_upload(
760 &mut self,
761 device: &wgpu::Device,
762 queue: &wgpu::Queue,
763 f: &ViewFrustum,
764 screen_w: u32,
765 screen_h: u32,
766 tile_size: u32,
767 lod_px: f32,
768 ) -> (u32, u32, u32) {
769 let tiles_x = screen_w.div_ceil(tile_size).max(1);
770 let tiles_y = screen_h.div_ceil(tile_size).max(1);
771 let n_tiles = (tiles_x * tiles_y) as usize;
772
773 let nw = (1.0 + f.half_w * f.half_w).sqrt();
774 let nh = (1.0 + f.half_h * f.half_h).sqrt();
775 let cx = screen_w as f32 * 0.5;
776 let cy = screen_h as f32 * 0.5;
777 let px_per_world = cx / f.half_w; let ts = tile_size as f32;
779 let tx_max = tiles_x as i32 - 1;
780 let ty_max = tiles_y as i32 - 1;
781
782 let mut visible: Vec<SpriteInstanceGpu> = Vec::with_capacity(self.cull.len());
783 let mut boxes: Vec<[i32; 4]> = Vec::with_capacity(self.cull.len());
785 let mut visible_colmul: Vec<u32> = Vec::with_capacity(self.cull.len() * 512);
789 let mut counts = vec![0u32; n_tiles];
790
791 for ci in &self.cull {
792 let rel = [
793 ci.center[0] - f.pos[0],
794 ci.center[1] - f.pos[1],
795 ci.center[2] - f.pos[2],
796 ];
797 let z = dot3(rel, f.forward);
798 let r = ci.radius;
799 if z + r < 0.0 || z - r > f.far {
800 continue; }
802 let x = dot3(rel, f.right);
803 if (x - f.half_w * z) > r * nw || (-x - f.half_w * z) > r * nw {
804 continue; }
806 let y = dot3(rel, f.down);
807 if (y - f.half_h * z) > r * nh || (-y - f.half_h * z) > r * nh {
808 continue; }
810
811 let (tx0, tx1, ty0, ty1) = if z > 1e-3 {
813 let sx = cx + (x / z) * px_per_world;
814 let sy = cy + (y / z) * px_per_world;
815 let sr = (r / z) * px_per_world;
816 (
817 (((sx - sr) / ts).floor() as i32).clamp(0, tx_max),
818 (((sx + sr) / ts).floor() as i32).clamp(0, tx_max),
819 (((sy - sr) / ts).floor() as i32).clamp(0, ty_max),
820 (((sy + sr) / ts).floor() as i32).clamp(0, ty_max),
821 )
822 } else {
823 (0, tx_max, 0, ty_max)
825 };
826 let chain = &self.chains[ci.chain_id as usize];
833 let level = if z > 1e-3 && chain.len() > 1 {
834 let voxel_px = px_per_world / z; ((lod_px / voxel_px).log2().ceil().max(0.0) as usize).min(chain.len() - 1)
836 } else {
837 0
838 };
839 let mut g = ci.gpu;
840 g.model_id = chain[level];
841 visible.push(g);
842 boxes.push([tx0, tx1, ty0, ty1]);
843 for &w in ci.colmul.iter() {
844 visible_colmul.push((w & 0xffff_ffff) as u32);
845 visible_colmul.push((w >> 32) as u32);
846 }
847 for ty in ty0..=ty1 {
848 for tx in tx0..=tx1 {
849 counts[(ty * tiles_x as i32 + tx) as usize] += 1;
850 }
851 }
852 }
853
854 if visible.is_empty() {
855 return (0, tiles_x, tiles_y);
856 }
857
858 let mut tile_ranges = vec![0u32; n_tiles * 2];
861 let mut running = 0u32;
862 for t in 0..n_tiles {
863 tile_ranges[2 * t] = running; tile_ranges[2 * t + 1] = counts[t]; running += counts[t];
866 }
867 let total = running as usize;
868 let mut tile_instances = vec![0u32; total.max(1)];
869 let mut cursor: Vec<u32> = (0..n_tiles).map(|t| tile_ranges[2 * t]).collect();
870 for (vis_idx, b) in boxes.iter().enumerate() {
871 for ty in b[2]..=b[3] {
872 for tx in b[0]..=b[1] {
873 let t = (ty * tiles_x as i32 + tx) as usize;
874 tile_instances[cursor[t] as usize] = vis_idx as u32;
875 cursor[t] += 1;
876 }
877 }
878 }
879
880 queue.write_buffer(&self.instances, 0, bytemuck::cast_slice(&visible));
884 let need_ranges = tile_ranges.len() as u32;
885 if need_ranges > self.tile_ranges_cap {
886 self.tile_ranges_cap = need_ranges.next_power_of_two();
887 self.tile_ranges = storage_dst_u32(
888 device,
889 "roxlap-gpu sprite_reg.tile_ranges",
890 self.tile_ranges_cap,
891 );
892 }
893 let need_inst = tile_instances.len() as u32;
894 if need_inst > self.tile_instances_cap {
895 self.tile_instances_cap = need_inst.next_power_of_two();
896 self.tile_instances = storage_dst_u32(
897 device,
898 "roxlap-gpu sprite_reg.tile_instances",
899 self.tile_instances_cap,
900 );
901 }
902 queue.write_buffer(&self.tile_ranges, 0, bytemuck::cast_slice(&tile_ranges));
903 queue.write_buffer(
904 &self.tile_instances,
905 0,
906 bytemuck::cast_slice(&tile_instances),
907 );
908 let need_colmul = visible_colmul.len() as u32;
909 if need_colmul > self.colmul_cap {
910 self.colmul_cap = need_colmul.next_power_of_two();
911 self.colmul = storage_dst_u32(device, "roxlap-gpu sprite_reg.colmul", self.colmul_cap);
912 }
913 queue.write_buffer(&self.colmul, 0, bytemuck::cast_slice(&visible_colmul));
914
915 (visible.len() as u32, tiles_x, tiles_y)
916 }
917}
918
919fn storage_u32(device: &wgpu::Device, label: &str, data: &[u32]) -> wgpu::Buffer {
922 use wgpu::util::DeviceExt;
923 let bytes: &[u8] = if data.is_empty() {
924 bytemuck::cast_slice(&[0u32])
925 } else {
926 bytemuck::cast_slice(data)
927 };
928 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
929 label: Some(label),
930 contents: bytes,
931 usage: wgpu::BufferUsages::STORAGE,
932 })
933}
934
935fn storage_dst_u32(device: &wgpu::Device, label: &str, cap: u32) -> wgpu::Buffer {
938 device.create_buffer(&wgpu::BufferDescriptor {
939 label: Some(label),
940 size: u64::from(cap.max(1)) * 4,
941 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
942 mapped_at_creation: false,
943 })
944}
945
946fn storage_pod<T: Pod + Zeroable>(device: &wgpu::Device, label: &str, data: &[T]) -> wgpu::Buffer {
949 use wgpu::util::DeviceExt;
950 let one = [T::zeroed()];
951 let src: &[T] = if data.is_empty() { &one } else { data };
952 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
953 label: Some(label),
954 contents: bytemuck::cast_slice(src),
955 usage: wgpu::BufferUsages::STORAGE,
956 })
957}
958
959#[cfg(test)]
960mod tests {
961 use super::*;
962 use roxlap_formats::kv6::{Kv6, Voxel};
963
964 fn kv6_unsorted() -> Kv6 {
967 let mk = |z, col| Voxel {
968 col,
969 z,
970 vis: 0,
971 dir: 0,
972 };
973 Kv6 {
974 xsiz: 2,
975 ysiz: 1,
976 zsiz: 8,
977 xpiv: 0.0,
978 ypiv: 0.0,
979 zpiv: 0.0,
980 voxels: vec![mk(5, 0xAA), mk(1, 0xBB), mk(3, 0xCC)],
981 xlen: vec![2, 1],
982 ylen: vec![vec![2], vec![1]],
983 palette: None,
984 }
985 }
986
987 #[test]
988 fn occupancy_bits_set_at_voxel_z() {
989 let m = build_sprite_model(&kv6_unsorted());
990 assert_eq!(m.dims, [2, 1, 8]);
991 assert_eq!(m.occ_words_per_col, 1); assert_eq!(m.occupancy[0], (1 << 1) | (1 << 5));
994 assert_eq!(m.occupancy[1], 1 << 3);
995 }
996
997 #[test]
998 fn colors_are_ascending_z_for_rank_lookup() {
999 let m = build_sprite_model(&kv6_unsorted());
1000 assert_eq!(m.color_offsets, vec![0, 2, 3]);
1002 assert_eq!(&m.colors, &[0xBB, 0xAA, 0xCC]);
1003 }
1004
1005 #[test]
1006 fn identity_basis_inverts_to_identity() {
1007 let inv = mat3_inverse([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
1008 assert_eq!(inv, [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
1009 }
1010
1011 #[test]
1012 fn fork_is_independent_of_parent() {
1013 let mut reg = SpriteModelRegistry::new();
1014 let base = reg.add(build_sprite_model(&kv6_unsorted()));
1015 let forked = reg.fork(base);
1016 assert_ne!(base, forked);
1017 reg.model_mut(forked).recolor(|_| 0x11);
1019 assert_eq!(®.model(base).colors, &[0xBB, 0xAA, 0xCC]);
1021 assert_eq!(®.model(forked).colors, &[0x11, 0x11, 0x11]);
1022 }
1023
1024 #[test]
1025 fn registry_gpu_structs_have_expected_sizes() {
1026 assert_eq!(std::mem::size_of::<SpriteModelMeta>(), 48);
1027 assert_eq!(std::mem::size_of::<SpriteInstanceGpu>(), 64);
1028 }
1029
1030 #[test]
1031 fn add_lod_builds_halving_mip_chain() {
1032 let mut reg = SpriteModelRegistry::new();
1033 let id = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
1036 let m0 = reg.model(id);
1037 assert_eq!(m0.dims, [2, 1, 8]);
1038 assert!((m0.voxel_world_size - 1.0).abs() < 1e-6);
1039 }
1040
1041 fn kv6_from(xsiz: u32, ysiz: u32, zsiz: u32, voxels: &[(u32, u32, u16, u32)]) -> Kv6 {
1044 let mut ylen = vec![vec![0u16; ysiz as usize]; xsiz as usize];
1045 let mut flat = Vec::new();
1046 for x in 0..xsiz {
1047 for y in 0..ysiz {
1048 let mut col: Vec<(u16, u32)> = voxels
1049 .iter()
1050 .filter(|(vx, vy, _, _)| *vx == x && *vy == y)
1051 .map(|(_, _, z, c)| (*z, *c))
1052 .collect();
1053 col.sort_by_key(|(z, _)| *z);
1054 ylen[x as usize][y as usize] = col.len() as u16;
1055 for (z, c) in col {
1056 flat.push(Voxel {
1057 col: c,
1058 z,
1059 vis: 0,
1060 dir: 0,
1061 });
1062 }
1063 }
1064 }
1065 let xlen = ylen
1066 .iter()
1067 .map(|c| c.iter().map(|&v| u32::from(v)).sum())
1068 .collect();
1069 Kv6 {
1070 xsiz,
1071 ysiz,
1072 zsiz,
1073 xpiv: 0.0,
1074 ypiv: 0.0,
1075 zpiv: 0.0,
1076 voxels: flat,
1077 xlen,
1078 ylen,
1079 palette: None,
1080 }
1081 }
1082
1083 fn offsets_consistent(m: &SpriteModel) -> bool {
1084 let cols = (m.dims[0] * m.dims[1]) as usize;
1085 if m.color_offsets.len() != cols + 1 {
1086 return false;
1087 }
1088 for w in m.color_offsets.windows(2) {
1091 if w[1] < w[0] {
1092 return false;
1093 }
1094 }
1095 m.color_offsets[cols] as usize == m.colors.len()
1096 }
1097
1098 #[test]
1099 fn carve_two_layers_keeps_offsets_consistent() {
1100 let kv6 = kv6_from(
1103 3,
1104 2,
1105 8,
1106 &[
1107 (0, 0, 0, 0xA0),
1108 (0, 0, 1, 0xA1),
1109 (0, 0, 5, 0xA5),
1110 (1, 0, 1, 0xB1),
1111 (2, 1, 0, 0xC0),
1112 (2, 1, 3, 0xC3),
1113 ],
1114 );
1115 let mut m = build_sprite_model(&kv6);
1116 assert!(offsets_consistent(&m));
1117 for z in 0..2u32 {
1118 for y in 0..m.dims[1] {
1119 for x in 0..m.dims[0] {
1120 m.set_voxel(x, y, z, None);
1121 }
1122 }
1123 assert!(offsets_consistent(&m), "inconsistent after carving z={z}");
1124 let _ = m.downsample();
1126 }
1127 }
1128
1129 #[test]
1130 fn set_voxel_inserts_replaces_and_clears() {
1131 let mut m = build_sprite_model(&kv6_unsorted());
1133
1134 assert!(m.set_voxel(0, 0, 3, Some(0x55)));
1136 assert_eq!(m.occupancy[0], (1 << 1) | (1 << 3) | (1 << 5));
1137 assert_eq!(m.color_offsets, vec![0, 3, 4]);
1139 assert_eq!(&m.colors, &[0xBB, 0x55, 0xAA, 0xCC]);
1140
1141 assert!(m.set_voxel(0, 0, 3, Some(0x66)));
1143 assert_eq!(&m.colors, &[0xBB, 0x66, 0xAA, 0xCC]);
1144 assert_eq!(m.color_offsets, vec![0, 3, 4]);
1145
1146 assert!(m.set_voxel(0, 0, 1, None));
1148 assert_eq!(m.occupancy[0], (1 << 3) | (1 << 5));
1149 assert_eq!(m.color_offsets, vec![0, 2, 3]);
1150 assert_eq!(&m.colors, &[0x66, 0xAA, 0xCC]);
1151
1152 assert!(!m.set_voxel(0, 0, 2, None));
1154 assert!(!m.set_voxel(9, 0, 0, Some(1)));
1155 }
1156
1157 #[test]
1158 fn rebuild_lod_refreshes_coarse_levels_from_mip0() {
1159 let mut reg = SpriteModelRegistry::new();
1160 let id = reg.add_lod(build_sprite_model(&kv6_unsorted()), 3);
1161 reg.model_mut(id).recolor(|_| 0x0000_2000);
1163 reg.rebuild_lod(id);
1164 let lvl1_entry = reg.chains[id as usize][1] as usize;
1166 assert!(reg.entries[lvl1_entry]
1167 .colors
1168 .iter()
1169 .all(|&c| c == 0x0000_2000));
1170 }
1171}