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 meta: Vec<SpriteModelMeta>,
624 colors_alloc: ColorsAllocator,
628 occ_lens: Vec<u32>,
633 coloff_lens: Vec<u32>,
634}
635
636impl SpriteRegistryResident {
637 #[must_use]
642 pub fn upload(
643 device: &wgpu::Device,
644 registry: &SpriteModelRegistry,
645 instances: &[SpriteInstance],
646 ) -> Self {
647 let entry_lens: Vec<u32> = registry
652 .entries
653 .iter()
654 .map(|m| m.colors.len() as u32)
655 .collect();
656 let colors_alloc = ColorsAllocator::new(&entry_lens);
657 let cap_total = colors_alloc.cap_total();
658
659 let mut all_occ: Vec<u32> = Vec::new();
660 let mut all_offsets: Vec<u32> = Vec::new();
661 let mut all_colors: Vec<u32> = vec![0; cap_total as usize];
662 let mut all_dirs: Vec<u32> = vec![0; cap_total as usize];
663 let mut meta: Vec<SpriteModelMeta> = Vec::with_capacity(registry.entries.len());
664 let mut occ_lens: Vec<u32> = Vec::with_capacity(registry.entries.len());
665 let mut coloff_lens: Vec<u32> = Vec::with_capacity(registry.entries.len());
666
667 for (e, m) in registry.entries.iter().enumerate() {
669 let slot = colors_alloc.slot(e);
670 meta.push(SpriteModelMeta {
671 occupancy_offset: all_occ.len() as u32,
672 colors_offset: slot.off,
673 color_offsets_offset: all_offsets.len() as u32,
674 occ_words_per_col: m.occ_words_per_col,
675 dims: m.dims,
676 _pad0: 0,
677 pivot: m.pivot,
678 voxel_world_size: m.voxel_world_size,
679 });
680 occ_lens.push(m.occupancy.len() as u32);
681 coloff_lens.push(m.color_offsets.len() as u32);
682 all_occ.extend_from_slice(&m.occupancy);
683 all_offsets.extend_from_slice(&m.color_offsets);
684 let off = slot.off as usize;
685 all_colors[off..off + m.colors.len()].copy_from_slice(&m.colors);
686 all_dirs[off..off + m.dirs.len()].copy_from_slice(&m.dirs);
687 }
688
689 let cull: Vec<CullInstance> = instances
694 .iter()
695 .map(|i| CullInstance {
696 gpu: SpriteInstanceGpu {
697 inv_rot0: i.transform.inv_rot[0],
698 inv_rot1: i.transform.inv_rot[1],
699 inv_rot2: i.transform.inv_rot[2],
700 pos: i.transform.pos,
701 model_id: i.model_id, },
703 chain_id: i.model_id,
704 center: i.transform.pos,
705 radius: registry.model(i.model_id).bound_radius(),
706 colmul: identity_colmul(),
707 })
708 .collect();
709
710 let seed: Vec<SpriteInstanceGpu> = cull.iter().map(|c| c.gpu).collect();
713 let instances_buf = {
714 use wgpu::util::DeviceExt;
715 let one = [SpriteInstanceGpu::zeroed()];
716 let src: &[SpriteInstanceGpu] = if seed.is_empty() { &one } else { &seed };
717 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
718 label: Some("roxlap-gpu sprite_reg.instances"),
719 contents: bytemuck::cast_slice(src),
720 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
721 })
722 };
723
724 let tile_ranges = storage_dst_u32(device, "roxlap-gpu sprite_reg.tile_ranges", 1);
725 let tile_instances = storage_dst_u32(device, "roxlap-gpu sprite_reg.tile_instances", 1);
726 let colmul_cap = (cull.len() as u32).max(1) * 256 * 2;
729 let colmul = storage_dst_u32(device, "roxlap-gpu sprite_reg.colmul", colmul_cap);
730 Self {
731 occupancy: storage_dst_u32_cap(
732 device,
733 "roxlap-gpu sprite_reg.occupancy",
734 &all_occ,
735 all_occ.len() as u32,
736 ),
737 colors: storage_dst_u32_cap(
738 device,
739 "roxlap-gpu sprite_reg.colors",
740 &all_colors,
741 cap_total,
742 ),
743 dirs: storage_dst_u32_cap(device, "roxlap-gpu sprite_reg.dirs", &all_dirs, cap_total),
744 color_offsets: storage_dst_u32_cap(
745 device,
746 "roxlap-gpu sprite_reg.color_offsets",
747 &all_offsets,
748 all_offsets.len() as u32,
749 ),
750 model_meta: storage_dst_pod(device, "roxlap-gpu sprite_reg.model_meta", &meta),
751 instances: instances_buf,
752 instance_capacity: cull.len() as u32,
753 colmul,
754 colmul_cap,
755 tile_ranges,
756 tile_ranges_cap: 1,
757 tile_instances,
758 tile_instances_cap: 1,
759 cull,
760 chains: registry.chains.clone(),
761 meta,
762 colors_alloc,
763 occ_lens,
764 coloff_lens,
765 }
766 }
767
768 pub fn set_instance_colmul(&mut self, tables: &[[u64; 256]]) {
774 for (ci, t) in self.cull.iter_mut().zip(tables) {
775 ci.colmul.copy_from_slice(t);
776 }
777 }
778
779 pub fn update_transforms(&mut self, instances: &[SpriteInstance]) {
787 debug_assert_eq!(
788 instances.len(),
789 self.cull.len(),
790 "update_transforms instance count must match upload"
791 );
792 for (ci, inst) in self.cull.iter_mut().zip(instances) {
793 ci.gpu.inv_rot0 = inst.transform.inv_rot[0];
794 ci.gpu.inv_rot1 = inst.transform.inv_rot[1];
795 ci.gpu.inv_rot2 = inst.transform.inv_rot[2];
796 ci.gpu.pos = inst.transform.pos;
797 ci.center = inst.transform.pos;
799 }
800 }
801
802 pub fn update_model(
822 &mut self,
823 device: &wgpu::Device,
824 queue: &wgpu::Queue,
825 registry: &SpriteModelRegistry,
826 chain_id: u32,
827 ) {
828 let entries = self.chains[chain_id as usize].clone();
829 let mut grew = false;
830 for &e in &entries {
831 let e = e as usize;
832 let m = ®istry.entries[e];
833
834 debug_assert_eq!(
836 m.occupancy.len() as u32,
837 self.occ_lens[e],
838 "update_model: entry {e} occupancy length changed (dims grew?)"
839 );
840 debug_assert_eq!(
841 m.color_offsets.len() as u32,
842 self.coloff_lens[e],
843 "update_model: entry {e} color_offsets length changed (dims grew?)"
844 );
845 queue.write_buffer(
846 &self.occupancy,
847 u64::from(self.meta[e].occupancy_offset) * 4,
848 bytemuck::cast_slice(&m.occupancy),
849 );
850 queue.write_buffer(
851 &self.color_offsets,
852 u64::from(self.meta[e].color_offsets_offset) * 4,
853 bytemuck::cast_slice(&m.color_offsets),
854 );
855
856 let new_len = m.colors.len() as u32;
858 match self.colors_alloc.place(e, new_len) {
859 Some(off) => {
860 queue.write_buffer(
861 &self.colors,
862 u64::from(off) * 4,
863 bytemuck::cast_slice(&m.colors),
864 );
865 queue.write_buffer(
866 &self.dirs,
867 u64::from(off) * 4,
868 bytemuck::cast_slice(&m.dirs),
869 );
870 if self.meta[e].colors_offset != off {
871 self.meta[e].colors_offset = off;
873 queue.write_buffer(
874 &self.model_meta,
875 (e * std::mem::size_of::<SpriteModelMeta>()) as u64,
876 bytemuck::bytes_of(&self.meta[e]),
877 );
878 }
879 }
880 None => grew = true,
881 }
882 }
883
884 if grew {
887 self.grow_and_repack(device, queue, registry);
888 }
889 }
890
891 fn grow_and_repack(
898 &mut self,
899 device: &wgpu::Device,
900 queue: &wgpu::Queue,
901 registry: &SpriteModelRegistry,
902 ) {
903 let new_lens: Vec<u32> = registry
904 .entries
905 .iter()
906 .map(|m| m.colors.len() as u32)
907 .collect();
908 self.colors_alloc.repack(&new_lens);
909 let cap_total = self.colors_alloc.cap_total();
910
911 let mut all_colors = vec![0u32; cap_total as usize];
912 let mut all_dirs = vec![0u32; cap_total as usize];
913 for (e, m) in registry.entries.iter().enumerate() {
914 let off = self.colors_alloc.slot(e).off as usize;
915 all_colors[off..off + m.colors.len()].copy_from_slice(&m.colors);
916 all_dirs[off..off + m.dirs.len()].copy_from_slice(&m.dirs);
917 self.meta[e].colors_offset = off as u32;
918 }
919 self.colors = storage_dst_u32_cap(
920 device,
921 "roxlap-gpu sprite_reg.colors",
922 &all_colors,
923 cap_total,
924 );
925 self.dirs = storage_dst_u32_cap(device, "roxlap-gpu sprite_reg.dirs", &all_dirs, cap_total);
926 queue.write_buffer(&self.model_meta, 0, bytemuck::cast_slice(&self.meta));
928 eprintln!("roxlap-gpu: sprite registry colors/dirs grew + repacked to {cap_total} words");
929 }
930
931 #[allow(clippy::too_many_arguments)]
939 pub fn cull_bin_upload(
940 &mut self,
941 device: &wgpu::Device,
942 queue: &wgpu::Queue,
943 f: &ViewFrustum,
944 screen_w: u32,
945 screen_h: u32,
946 tile_size: u32,
947 lod_px: f32,
948 ) -> (u32, u32, u32) {
949 let tiles_x = screen_w.div_ceil(tile_size).max(1);
950 let tiles_y = screen_h.div_ceil(tile_size).max(1);
951 let n_tiles = (tiles_x * tiles_y) as usize;
952
953 let nw = (1.0 + f.half_w * f.half_w).sqrt();
954 let nh = (1.0 + f.half_h * f.half_h).sqrt();
955 let cx = screen_w as f32 * 0.5;
956 let cy = screen_h as f32 * 0.5;
957 let px_per_world = cx / f.half_w; let ts = tile_size as f32;
959 let tx_max = tiles_x as i32 - 1;
960 let ty_max = tiles_y as i32 - 1;
961
962 let mut visible: Vec<SpriteInstanceGpu> = Vec::with_capacity(self.cull.len());
963 let mut boxes: Vec<[i32; 4]> = Vec::with_capacity(self.cull.len());
965 let mut visible_colmul: Vec<u32> = Vec::with_capacity(self.cull.len() * 512);
969 let mut counts = vec![0u32; n_tiles];
970
971 for ci in &self.cull {
972 let rel = [
973 ci.center[0] - f.pos[0],
974 ci.center[1] - f.pos[1],
975 ci.center[2] - f.pos[2],
976 ];
977 let z = dot3(rel, f.forward);
978 let r = ci.radius;
979 if z + r < 0.0 || z - r > f.far {
980 continue; }
982 let x = dot3(rel, f.right);
983 if (x - f.half_w * z) > r * nw || (-x - f.half_w * z) > r * nw {
984 continue; }
986 let y = dot3(rel, f.down);
987 if (y - f.half_h * z) > r * nh || (-y - f.half_h * z) > r * nh {
988 continue; }
990
991 let (tx0, tx1, ty0, ty1) = if z > 1e-3 {
993 let sx = cx + (x / z) * px_per_world;
994 let sy = cy + (y / z) * px_per_world;
995 let sr = (r / z) * px_per_world;
996 (
997 (((sx - sr) / ts).floor() as i32).clamp(0, tx_max),
998 (((sx + sr) / ts).floor() as i32).clamp(0, tx_max),
999 (((sy - sr) / ts).floor() as i32).clamp(0, ty_max),
1000 (((sy + sr) / ts).floor() as i32).clamp(0, ty_max),
1001 )
1002 } else {
1003 (0, tx_max, 0, ty_max)
1005 };
1006 let chain = &self.chains[ci.chain_id as usize];
1013 let level = if z > 1e-3 && chain.len() > 1 {
1014 let voxel_px = px_per_world / z; ((lod_px / voxel_px).log2().ceil().max(0.0) as usize).min(chain.len() - 1)
1016 } else {
1017 0
1018 };
1019 let mut g = ci.gpu;
1020 g.model_id = chain[level];
1021 visible.push(g);
1022 boxes.push([tx0, tx1, ty0, ty1]);
1023 for &w in ci.colmul.iter() {
1024 visible_colmul.push((w & 0xffff_ffff) as u32);
1025 visible_colmul.push((w >> 32) as u32);
1026 }
1027 for ty in ty0..=ty1 {
1028 for tx in tx0..=tx1 {
1029 counts[(ty * tiles_x as i32 + tx) as usize] += 1;
1030 }
1031 }
1032 }
1033
1034 if visible.is_empty() {
1035 return (0, tiles_x, tiles_y);
1036 }
1037
1038 let mut tile_ranges = vec![0u32; n_tiles * 2];
1041 let mut running = 0u32;
1042 for t in 0..n_tiles {
1043 tile_ranges[2 * t] = running; tile_ranges[2 * t + 1] = counts[t]; running += counts[t];
1046 }
1047 let total = running as usize;
1048 let mut tile_instances = vec![0u32; total.max(1)];
1049 let mut cursor: Vec<u32> = (0..n_tiles).map(|t| tile_ranges[2 * t]).collect();
1050 for (vis_idx, b) in boxes.iter().enumerate() {
1051 for ty in b[2]..=b[3] {
1052 for tx in b[0]..=b[1] {
1053 let t = (ty * tiles_x as i32 + tx) as usize;
1054 tile_instances[cursor[t] as usize] = vis_idx as u32;
1055 cursor[t] += 1;
1056 }
1057 }
1058 }
1059
1060 queue.write_buffer(&self.instances, 0, bytemuck::cast_slice(&visible));
1064 let need_ranges = tile_ranges.len() as u32;
1065 if need_ranges > self.tile_ranges_cap {
1066 self.tile_ranges_cap = need_ranges.next_power_of_two();
1067 self.tile_ranges = storage_dst_u32(
1068 device,
1069 "roxlap-gpu sprite_reg.tile_ranges",
1070 self.tile_ranges_cap,
1071 );
1072 }
1073 let need_inst = tile_instances.len() as u32;
1074 if need_inst > self.tile_instances_cap {
1075 self.tile_instances_cap = need_inst.next_power_of_two();
1076 self.tile_instances = storage_dst_u32(
1077 device,
1078 "roxlap-gpu sprite_reg.tile_instances",
1079 self.tile_instances_cap,
1080 );
1081 }
1082 queue.write_buffer(&self.tile_ranges, 0, bytemuck::cast_slice(&tile_ranges));
1083 queue.write_buffer(
1084 &self.tile_instances,
1085 0,
1086 bytemuck::cast_slice(&tile_instances),
1087 );
1088 let need_colmul = visible_colmul.len() as u32;
1089 if need_colmul > self.colmul_cap {
1090 self.colmul_cap = need_colmul.next_power_of_two();
1091 self.colmul = storage_dst_u32(device, "roxlap-gpu sprite_reg.colmul", self.colmul_cap);
1092 }
1093 queue.write_buffer(&self.colmul, 0, bytemuck::cast_slice(&visible_colmul));
1094
1095 (visible.len() as u32, tiles_x, tiles_y)
1096 }
1097}
1098
1099#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1105struct ColorSlot {
1106 off: u32,
1107 cap: u32,
1108 len: u32,
1109}
1110
1111#[derive(Debug, Default)]
1118struct ColorsAllocator {
1119 slots: Vec<ColorSlot>,
1121 free: Vec<(u32, u32)>,
1123 tail: u32,
1125 cap_total: u32,
1127}
1128
1129fn slot_cap(len: u32) -> u32 {
1132 len + len / 4 + 16
1133}
1134
1135impl ColorsAllocator {
1136 fn new(entry_lens: &[u32]) -> Self {
1140 let mut a = Self::default();
1141 a.repack(entry_lens);
1142 a
1143 }
1144
1145 fn slot(&self, entry: usize) -> ColorSlot {
1146 self.slots[entry]
1147 }
1148
1149 fn cap_total(&self) -> u32 {
1150 self.cap_total
1151 }
1152
1153 fn repack(&mut self, new_lens: &[u32]) {
1157 self.free.clear();
1158 let mut off = 0u32;
1159 let mut slots = Vec::with_capacity(new_lens.len());
1160 for &len in new_lens {
1161 let cap = slot_cap(len);
1162 slots.push(ColorSlot { off, cap, len });
1163 off += cap;
1164 }
1165 self.slots = slots;
1166 self.tail = off;
1167 self.cap_total = off + off / 2 + 256;
1169 }
1170
1171 fn place(&mut self, entry: usize, new_len: u32) -> Option<u32> {
1176 let cur = self.slots[entry];
1177 if new_len <= cur.cap {
1178 self.slots[entry] = ColorSlot {
1179 len: new_len,
1180 ..cur
1181 };
1182 return Some(cur.off);
1183 }
1184 let old = (cur.off, cur.cap);
1185 if let Some(i) = self.free.iter().position(|&(_, c)| c >= new_len) {
1187 let (off, cap) = self.free.remove(i);
1188 self.free.push(old);
1189 self.slots[entry] = ColorSlot {
1190 off,
1191 cap,
1192 len: new_len,
1193 };
1194 return Some(off);
1195 }
1196 let want = slot_cap(new_len);
1198 if self.tail + want <= self.cap_total {
1199 let off = self.tail;
1200 self.tail += want;
1201 self.free.push(old);
1202 self.slots[entry] = ColorSlot {
1203 off,
1204 cap: want,
1205 len: new_len,
1206 };
1207 return Some(off);
1208 }
1209 None
1210 }
1211}
1212
1213#[allow(dead_code)]
1216fn storage_u32(device: &wgpu::Device, label: &str, data: &[u32]) -> wgpu::Buffer {
1217 use wgpu::util::DeviceExt;
1218 let bytes: &[u8] = if data.is_empty() {
1219 bytemuck::cast_slice(&[0u32])
1220 } else {
1221 bytemuck::cast_slice(data)
1222 };
1223 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1224 label: Some(label),
1225 contents: bytes,
1226 usage: wgpu::BufferUsages::STORAGE,
1227 })
1228}
1229
1230fn storage_dst_u32(device: &wgpu::Device, label: &str, cap: u32) -> wgpu::Buffer {
1233 device.create_buffer(&wgpu::BufferDescriptor {
1234 label: Some(label),
1235 size: u64::from(cap.max(1)) * 4,
1236 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1237 mapped_at_creation: false,
1238 })
1239}
1240
1241fn storage_dst_u32_cap(device: &wgpu::Device, label: &str, data: &[u32], cap: u32) -> wgpu::Buffer {
1249 let cap = cap.max(data.len() as u32).max(1);
1250 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1251 label: Some(label),
1252 size: u64::from(cap) * 4,
1253 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1254 mapped_at_creation: true,
1255 });
1256 if !data.is_empty() {
1257 buf.slice(..(data.len() as u64 * 4))
1258 .get_mapped_range_mut()
1259 .copy_from_slice(bytemuck::cast_slice(data));
1260 }
1261 buf.unmap();
1262 buf
1263}
1264
1265fn storage_dst_pod<T: Pod + Zeroable>(
1271 device: &wgpu::Device,
1272 label: &str,
1273 data: &[T],
1274) -> wgpu::Buffer {
1275 let one = [T::zeroed()];
1276 let src: &[T] = if data.is_empty() { &one } else { data };
1277 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1278 label: Some(label),
1279 size: std::mem::size_of_val(src) as u64,
1280 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1281 mapped_at_creation: true,
1282 });
1283 buf.slice(..)
1284 .get_mapped_range_mut()
1285 .copy_from_slice(bytemuck::cast_slice(src));
1286 buf.unmap();
1287 buf
1288}
1289
1290#[allow(dead_code)]
1293fn storage_pod<T: Pod + Zeroable>(device: &wgpu::Device, label: &str, data: &[T]) -> wgpu::Buffer {
1294 use wgpu::util::DeviceExt;
1295 let one = [T::zeroed()];
1296 let src: &[T] = if data.is_empty() { &one } else { data };
1297 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1298 label: Some(label),
1299 contents: bytemuck::cast_slice(src),
1300 usage: wgpu::BufferUsages::STORAGE,
1301 })
1302}
1303
1304#[cfg(test)]
1305mod tests {
1306 use super::*;
1307 use roxlap_formats::kv6::{Kv6, Voxel};
1308
1309 fn kv6_unsorted() -> Kv6 {
1312 let mk = |z, col| Voxel {
1313 col,
1314 z,
1315 vis: 0,
1316 dir: 0,
1317 };
1318 Kv6 {
1319 xsiz: 2,
1320 ysiz: 1,
1321 zsiz: 8,
1322 xpiv: 0.0,
1323 ypiv: 0.0,
1324 zpiv: 0.0,
1325 voxels: vec![mk(5, 0xAA), mk(1, 0xBB), mk(3, 0xCC)],
1326 xlen: vec![2, 1],
1327 ylen: vec![vec![2], vec![1]],
1328 palette: None,
1329 }
1330 }
1331
1332 #[test]
1333 fn occupancy_bits_set_at_voxel_z() {
1334 let m = build_sprite_model(&kv6_unsorted());
1335 assert_eq!(m.dims, [2, 1, 8]);
1336 assert_eq!(m.occ_words_per_col, 1); assert_eq!(m.occupancy[0], (1 << 1) | (1 << 5));
1339 assert_eq!(m.occupancy[1], 1 << 3);
1340 }
1341
1342 #[test]
1343 fn colors_are_ascending_z_for_rank_lookup() {
1344 let m = build_sprite_model(&kv6_unsorted());
1345 assert_eq!(m.color_offsets, vec![0, 2, 3]);
1347 assert_eq!(&m.colors, &[0xBB, 0xAA, 0xCC]);
1348 }
1349
1350 #[test]
1351 fn identity_basis_inverts_to_identity() {
1352 let inv = mat3_inverse([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
1353 assert_eq!(inv, [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
1354 }
1355
1356 #[test]
1357 fn fork_is_independent_of_parent() {
1358 let mut reg = SpriteModelRegistry::new();
1359 let base = reg.add(build_sprite_model(&kv6_unsorted()));
1360 let forked = reg.fork(base);
1361 assert_ne!(base, forked);
1362 reg.model_mut(forked).recolor(|_| 0x11);
1364 assert_eq!(®.model(base).colors, &[0xBB, 0xAA, 0xCC]);
1366 assert_eq!(®.model(forked).colors, &[0x11, 0x11, 0x11]);
1367 }
1368
1369 #[test]
1370 fn registry_gpu_structs_have_expected_sizes() {
1371 assert_eq!(std::mem::size_of::<SpriteModelMeta>(), 48);
1372 assert_eq!(std::mem::size_of::<SpriteInstanceGpu>(), 64);
1373 }
1374
1375 #[test]
1376 fn add_lod_builds_halving_mip_chain() {
1377 let mut reg = SpriteModelRegistry::new();
1378 let id = reg.add_lod(build_sprite_model(&kv6_unsorted()), 4);
1381 let m0 = reg.model(id);
1382 assert_eq!(m0.dims, [2, 1, 8]);
1383 assert!((m0.voxel_world_size - 1.0).abs() < 1e-6);
1384 }
1385
1386 fn kv6_from(xsiz: u32, ysiz: u32, zsiz: u32, voxels: &[(u32, u32, u16, u32)]) -> Kv6 {
1389 let mut ylen = vec![vec![0u16; ysiz as usize]; xsiz as usize];
1390 let mut flat = Vec::new();
1391 for x in 0..xsiz {
1392 for y in 0..ysiz {
1393 let mut col: Vec<(u16, u32)> = voxels
1394 .iter()
1395 .filter(|(vx, vy, _, _)| *vx == x && *vy == y)
1396 .map(|(_, _, z, c)| (*z, *c))
1397 .collect();
1398 col.sort_by_key(|(z, _)| *z);
1399 ylen[x as usize][y as usize] = col.len() as u16;
1400 for (z, c) in col {
1401 flat.push(Voxel {
1402 col: c,
1403 z,
1404 vis: 0,
1405 dir: 0,
1406 });
1407 }
1408 }
1409 }
1410 let xlen = ylen
1411 .iter()
1412 .map(|c| c.iter().map(|&v| u32::from(v)).sum())
1413 .collect();
1414 Kv6 {
1415 xsiz,
1416 ysiz,
1417 zsiz,
1418 xpiv: 0.0,
1419 ypiv: 0.0,
1420 zpiv: 0.0,
1421 voxels: flat,
1422 xlen,
1423 ylen,
1424 palette: None,
1425 }
1426 }
1427
1428 fn offsets_consistent(m: &SpriteModel) -> bool {
1429 let cols = (m.dims[0] * m.dims[1]) as usize;
1430 if m.color_offsets.len() != cols + 1 {
1431 return false;
1432 }
1433 for w in m.color_offsets.windows(2) {
1436 if w[1] < w[0] {
1437 return false;
1438 }
1439 }
1440 m.color_offsets[cols] as usize == m.colors.len()
1441 }
1442
1443 #[test]
1444 fn carve_two_layers_keeps_offsets_consistent() {
1445 let kv6 = kv6_from(
1448 3,
1449 2,
1450 8,
1451 &[
1452 (0, 0, 0, 0xA0),
1453 (0, 0, 1, 0xA1),
1454 (0, 0, 5, 0xA5),
1455 (1, 0, 1, 0xB1),
1456 (2, 1, 0, 0xC0),
1457 (2, 1, 3, 0xC3),
1458 ],
1459 );
1460 let mut m = build_sprite_model(&kv6);
1461 assert!(offsets_consistent(&m));
1462 for z in 0..2u32 {
1463 for y in 0..m.dims[1] {
1464 for x in 0..m.dims[0] {
1465 m.set_voxel(x, y, z, None);
1466 }
1467 }
1468 assert!(offsets_consistent(&m), "inconsistent after carving z={z}");
1469 let _ = m.downsample();
1471 }
1472 }
1473
1474 #[test]
1475 fn set_voxel_inserts_replaces_and_clears() {
1476 let mut m = build_sprite_model(&kv6_unsorted());
1478
1479 assert!(m.set_voxel(0, 0, 3, Some(0x55)));
1481 assert_eq!(m.occupancy[0], (1 << 1) | (1 << 3) | (1 << 5));
1482 assert_eq!(m.color_offsets, vec![0, 3, 4]);
1484 assert_eq!(&m.colors, &[0xBB, 0x55, 0xAA, 0xCC]);
1485
1486 assert!(m.set_voxel(0, 0, 3, Some(0x66)));
1488 assert_eq!(&m.colors, &[0xBB, 0x66, 0xAA, 0xCC]);
1489 assert_eq!(m.color_offsets, vec![0, 3, 4]);
1490
1491 assert!(m.set_voxel(0, 0, 1, None));
1493 assert_eq!(m.occupancy[0], (1 << 3) | (1 << 5));
1494 assert_eq!(m.color_offsets, vec![0, 2, 3]);
1495 assert_eq!(&m.colors, &[0x66, 0xAA, 0xCC]);
1496
1497 assert!(!m.set_voxel(0, 0, 2, None));
1499 assert!(!m.set_voxel(9, 0, 0, Some(1)));
1500 }
1501
1502 #[test]
1503 fn rebuild_lod_refreshes_coarse_levels_from_mip0() {
1504 let mut reg = SpriteModelRegistry::new();
1505 let id = reg.add_lod(build_sprite_model(&kv6_unsorted()), 3);
1506 reg.model_mut(id).recolor(|_| 0x0000_2000);
1508 reg.rebuild_lod(id);
1509 let lvl1_entry = reg.chains[id as usize][1] as usize;
1511 assert!(reg.entries[lvl1_entry]
1512 .colors
1513 .iter()
1514 .all(|&c| c == 0x0000_2000));
1515 }
1516
1517 fn alloc_invariants(a: &ColorsAllocator, lens: &[u32]) {
1522 let mut prev_end = 0u32;
1523 for (e, &len) in lens.iter().enumerate() {
1524 let s = a.slot(e);
1525 assert_eq!(s.len, len, "slot {e} len");
1526 assert!(s.cap >= s.len, "slot {e} cap >= len");
1527 assert!(s.off >= prev_end, "slot {e} overlaps previous");
1529 assert!(s.off + s.cap <= a.cap_total(), "slot {e} past cap_total");
1530 prev_end = s.off + s.cap;
1531 }
1532 assert!(a.cap_total() >= prev_end, "tail headroom");
1533 }
1534
1535 #[test]
1536 fn allocator_new_lays_out_with_slack_and_headroom() {
1537 let lens = [10u32, 0, 64, 7];
1538 let a = ColorsAllocator::new(&lens);
1539 alloc_invariants(&a, &lens);
1540 assert!(a.slot(2).cap > 64);
1542 assert!(a.cap_total() > a.slot(3).off + a.slot(3).cap);
1544 }
1545
1546 #[test]
1547 fn allocator_place_in_place_when_within_cap() {
1548 let mut a = ColorsAllocator::new(&[10, 20]);
1549 let off0 = a.slot(0).off;
1550 let cap0 = a.slot(0).cap;
1551 assert_eq!(a.place(0, 5), Some(off0));
1553 assert_eq!(a.slot(0).len, 5);
1554 assert_eq!(a.slot(0).cap, cap0);
1555 assert_eq!(a.place(0, cap0), Some(off0));
1557 assert_eq!(a.slot(0).off, off0);
1558 assert!(a.free.is_empty(), "no relocation should free anything");
1559 }
1560
1561 #[test]
1562 fn allocator_place_relocates_to_tail_and_frees_old() {
1563 let mut a = ColorsAllocator::new(&[10, 20]);
1564 let old0 = (a.slot(0).off, a.slot(0).cap);
1565 let tail_before = a.tail;
1566 let new_len = a.slot(0).cap + 5;
1568 let off = a.place(0, new_len).expect("fits in headroom");
1569 assert_eq!(off, tail_before, "relocated to old tail");
1570 assert_eq!(a.slot(0).off, off);
1571 assert_eq!(a.slot(0).len, new_len);
1572 assert!(a.free.contains(&old0), "old slot freed");
1573 }
1574
1575 #[test]
1576 fn allocator_reuses_freed_block_first_fit() {
1577 let mut a = ColorsAllocator::new(&[10, 2]);
1580 let old0 = (a.slot(0).off, a.slot(0).cap);
1581 let _ = a.place(0, a.slot(0).cap + 5).unwrap();
1583 assert!(a.free.contains(&old0));
1584 let new1 = a.slot(1).cap + 1;
1587 assert!(new1 <= old0.1, "freed block big enough");
1588 let off = a.place(1, new1).expect("reuses freed block");
1589 assert_eq!(off, old0.0, "first-fit reused the freed slot offset");
1590 assert!(!a.free.contains(&old0), "freed block consumed");
1591 }
1592
1593 #[test]
1594 fn allocator_signals_grow_then_repack_restores() {
1595 let mut a = ColorsAllocator::new(&[8, 8]);
1596 let huge = a.cap_total() + 100;
1598 assert_eq!(a.place(0, huge), None, "overflow must signal grow");
1599 a.repack(&[huge, 8]);
1601 alloc_invariants(&a, &[huge, 8]);
1602 assert!(a.cap_total() > huge);
1603 assert_eq!(a.place(0, huge), Some(a.slot(0).off));
1605 }
1606
1607 #[test]
1614 fn allocator_carve_loop_keeps_live_windows_disjoint() {
1615 let mut a = ColorsAllocator::new(&[40, 12, 40]);
1616 let mut lens = [40u32, 12, 40];
1617 let walk = [13u32, 30, 60, 18, 9, 80, 80, 25, 200, 7];
1620 let mut grew = false;
1621 for &len in &walk {
1622 lens[1] = len;
1623 if a.place(1, len).is_none() {
1625 grew = true;
1626 a.repack(&lens);
1627 } else {
1628 assert_eq!(a.place(0, 40), Some(a.slot(0).off));
1630 assert_eq!(a.place(2, 40), Some(a.slot(2).off));
1631 }
1632 assert_eq!(a.slot(1).len, len);
1633
1634 let mut wins: Vec<(u32, u32)> =
1636 (0..3).map(|e| (a.slot(e).off, a.slot(e).len)).collect();
1637 wins.sort_by_key(|w| w.0);
1638 for pair in wins.windows(2) {
1639 let (o0, l0) = pair[0];
1640 let (o1, _) = pair[1];
1641 assert!(o0 + l0 <= o1, "live windows overlap: {pair:?}");
1642 }
1643 }
1644 assert!(grew, "the 200-word jump should have forced a repack");
1645 }
1646}