1#![allow(
16 clippy::cast_sign_loss,
17 clippy::cast_lossless,
18 clippy::cast_possible_truncation,
19 clippy::cast_possible_wrap,
20 clippy::doc_markdown,
21 clippy::missing_panics_doc,
22 clippy::needless_range_loop,
23 clippy::pub_underscore_fields
24)]
25
26use bytemuck::Zeroable;
27use wgpu::util::DeviceExt;
28
29use crate::decompress::{gpu_mip_count, occ_words_per_column_for_mip, ChunkUpload};
30use crate::grid::GridUpload;
31
32pub const MAX_GPU_MIPS: usize = 6;
37
38#[derive(Debug, Clone, Copy)]
46pub struct MipLayout {
47 pub mip_count: u32,
48 pub occ_words_per_slot: u32,
49 pub offsets_words_per_slot: u32,
50 pub mip_occ_rel: [u32; MAX_GPU_MIPS],
52 pub mip_coff_rel: [u32; MAX_GPU_MIPS],
54}
55
56impl MipLayout {
57 #[must_use]
58 pub fn for_vsid(vsid: u32) -> Self {
59 let mip_count = gpu_mip_count(vsid);
60 let mut mip_occ_rel = [0u32; MAX_GPU_MIPS];
61 let mut mip_coff_rel = [0u32; MAX_GPU_MIPS];
62 let mut occ_acc = 0u32;
63 let mut coff_acc = 0u32;
64 for m in 0..mip_count {
65 mip_occ_rel[m as usize] = occ_acc;
66 mip_coff_rel[m as usize] = coff_acc;
67 let vsid_m = vsid >> m;
68 let cols = vsid_m * vsid_m;
69 occ_acc += 2 * cols * occ_words_per_column_for_mip(m);
73 coff_acc += cols + 1;
74 }
75 Self {
76 mip_count,
77 occ_words_per_slot: occ_acc,
78 offsets_words_per_slot: coff_acc,
79 mip_occ_rel,
80 mip_coff_rel,
81 }
82 }
83}
84
85pub const COLORS_PER_CHUNK_WORDS: u32 = 65536;
101
102pub const MAX_OCC_PAGES: usize = 4;
117
118#[derive(Debug, Clone, Copy)]
124pub struct GridRuntimeTransform {
125 pub grid_origin_world: [f64; 3],
129 pub world_to_grid_rotation: [[f32; 3]; 3],
131}
132
133impl Default for GridRuntimeTransform {
134 fn default() -> Self {
135 Self {
136 grid_origin_world: [0.0, 0.0, 0.0],
137 world_to_grid_rotation: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
138 }
139 }
140}
141
142pub struct SceneUpload {
146 pub grids: Vec<GridUpload>,
147}
148
149impl SceneUpload {
150 #[must_use]
151 pub fn grid_count(&self) -> u32 {
152 u32::try_from(self.grids.len()).unwrap_or(u32::MAX)
153 }
154}
155
156#[repr(C)]
165#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug)]
166pub struct GridStaticMeta {
167 pub occupancy_offset: u32,
169 pub color_offsets_offset: u32,
170 pub colors_offset: u32,
171 pub chunk_colors_base_offset: u32,
172 pub chunk_occupancy_offset: u32,
173 pub slot_chunk_idx_offset: u32,
177 pub vsid: u32,
178 pub total_slots: u32,
179 pub pool_dims: [u32; 3],
180 pub _pad0: u32,
181 pub occ_words_per_slot: u32,
185 pub offsets_words_per_slot: u32,
187 pub mip_count: u32,
189 pub _pad1: u32,
190 pub mip_occ_rel: [u32; MAX_GPU_MIPS],
193 pub mip_coff_rel: [u32; MAX_GPU_MIPS],
196 pub aabb_min: [i32; 3],
204 pub _pad2: i32,
205 pub aabb_max: [i32; 3],
206 pub _pad3: i32,
207}
208
209pub const SLOT_EMPTY_SENTINEL: [i32; 3] = [i32::MIN, i32::MIN, i32::MIN];
214
215pub struct GpuSceneResident {
217 pub grid_count: u32,
218 pub occupancy_pages: Vec<wgpu::Buffer>,
228 pub occupancy_page_words: u32,
230 pub occupancy_num_pages: u32,
232 pub all_color_offsets: wgpu::Buffer,
233 pub all_colors: wgpu::Buffer,
234 pub all_chunk_colors_base: wgpu::Buffer,
235 pub all_chunk_occupancy: wgpu::Buffer,
236 pub all_slot_chunk_idx: wgpu::Buffer,
240 pub grid_static_meta: wgpu::Buffer,
241 pub total_bytes: u64,
242 pub static_meta: Vec<GridStaticMeta>,
244 pub(crate) chunk_occupancy_shadow: Vec<Vec<u32>>,
249 pub(crate) slot_chunk_idx_shadow: Vec<Vec<[i32; 4]>>,
254}
255
256impl GpuSceneResident {
257 pub fn upload(device: &wgpu::Device, info: &SceneUpload) -> Self {
263 let grid_count = info.grid_count();
264
265 let mut all_occupancy: Vec<u32> = Vec::new();
266 let mut all_color_offsets: Vec<u32> = Vec::new();
267 let mut all_colors: Vec<u32> = Vec::new();
268 let mut all_chunk_colors_base: Vec<u32> = Vec::new();
269 let mut all_chunk_occupancy: Vec<u32> = Vec::new();
270 let mut all_slot_chunk_idx: Vec<i32> = Vec::new();
271 let mut static_meta: Vec<GridStaticMeta> = Vec::with_capacity(info.grids.len());
272 let mut chunk_occupancy_shadow: Vec<Vec<u32>> = Vec::with_capacity(info.grids.len());
273 let mut slot_chunk_idx_shadow: Vec<Vec<[i32; 4]>> = Vec::with_capacity(info.grids.len());
274
275 for grid in &info.grids {
276 let vsid = grid.vsid;
277 let layout = MipLayout::for_vsid(vsid);
279 let occ_words_per_slot = layout.occ_words_per_slot as usize;
280 let offsets_words_per_slot = layout.offsets_words_per_slot as usize;
281 let colors_stride = COLORS_PER_CHUNK_WORDS as usize;
282
283 assert!(
287 grid.pool_dims[0].is_power_of_two()
288 && grid.pool_dims[1].is_power_of_two()
289 && grid.pool_dims[2].is_power_of_two(),
290 "scene grid: pool_dims {:?} must all be powers of 2",
291 grid.pool_dims,
292 );
293 let pool_x = grid.pool_dims[0] as usize;
294 let pool_y = grid.pool_dims[1] as usize;
295 let pool_z = grid.pool_dims[2] as usize;
296 let total_slots = pool_x * pool_y * pool_z;
297
298 let mut grid_occupancy = vec![0u32; total_slots * occ_words_per_slot];
299 let mut grid_color_offsets = vec![0u32; total_slots * offsets_words_per_slot];
300 let mut grid_colors = vec![0u32; total_slots * colors_stride];
301 let mut grid_chunk_colors_base = vec![0u32; total_slots];
302 for i in 0..total_slots {
303 grid_chunk_colors_base[i] = (i * colors_stride) as u32;
304 }
305 let mut grid_chunk_occupancy = vec![0u32; total_slots.div_ceil(32)];
306 let mut grid_slot_chunk_idx: Vec<[i32; 4]> = Vec::with_capacity(total_slots);
311 for _ in 0..total_slots {
312 grid_slot_chunk_idx.push([
313 SLOT_EMPTY_SENTINEL[0],
314 SLOT_EMPTY_SENTINEL[1],
315 SLOT_EMPTY_SENTINEL[2],
316 0,
317 ]);
318 }
319
320 let mask_x = (grid.pool_dims[0] - 1) as i32;
321 let mask_y = (grid.pool_dims[1] - 1) as i32;
322 let mask_z = (grid.pool_dims[2] - 1) as i32;
323 let chunks_per_layer = pool_x * pool_y;
324
325 for (chunk_idx, chunk) in &grid.chunks {
326 assert_eq!(chunk.vsid, vsid, "scene grid: chunk vsid mismatch");
327 let sx = (chunk_idx[0] & mask_x) as usize;
328 let sy = (chunk_idx[1] & mask_y) as usize;
329 let sz = (chunk_idx[2] & mask_z) as usize;
330 let slot_idx = sx + sy * pool_x + sz * chunks_per_layer;
331
332 let occ_start = slot_idx * occ_words_per_slot;
340 let off_start = slot_idx * offsets_words_per_slot;
341 let col_start = slot_idx * colors_stride;
342 let mut color_cursor = 0usize;
343 for (m, mip) in chunk.mips.iter().enumerate() {
344 let occ_dst = occ_start + layout.mip_occ_rel[m] as usize;
345 grid_occupancy[occ_dst..occ_dst + mip.occupancy.len()]
346 .copy_from_slice(&mip.occupancy);
347 let solid_dst = occ_dst + mip.occupancy.len();
349 grid_occupancy[solid_dst..solid_dst + mip.solid_occupancy.len()]
350 .copy_from_slice(&mip.solid_occupancy);
351 let coff_dst = off_start + layout.mip_coff_rel[m] as usize;
352 grid_color_offsets[coff_dst..coff_dst + mip.color_offsets.len()]
353 .copy_from_slice(&mip.color_offsets);
354
355 let remaining = colors_stride.saturating_sub(color_cursor);
356 let n = mip.colors.len().min(remaining);
357 if n < mip.colors.len() {
358 eprintln!(
359 "roxlap-gpu SceneUpload: scene grid chunk {chunk_idx:?} mip {m} \
360 colours overflow COLORS_PER_CHUNK_WORDS ({colors_stride}); \
361 truncating",
362 );
363 }
364 grid_colors[col_start + color_cursor..col_start + color_cursor + n]
365 .copy_from_slice(&mip.colors[..n]);
366 color_cursor += n;
367 }
368
369 if !chunk.mips[0].colors.is_empty() {
370 grid_chunk_occupancy[slot_idx >> 5] |= 1u32 << (slot_idx & 31);
371 }
372 grid_slot_chunk_idx[slot_idx] = [chunk_idx[0], chunk_idx[1], chunk_idx[2], 0];
373 }
374
375 let slot_chunk_idx_offset = u32::try_from(all_slot_chunk_idx.len()).expect("fits");
378 let (aabb_min, aabb_max) = aabb_of_slots(&grid_slot_chunk_idx);
380 let meta = GridStaticMeta {
381 occupancy_offset: u32::try_from(all_occupancy.len()).expect("fits"),
382 color_offsets_offset: u32::try_from(all_color_offsets.len()).expect("fits"),
383 colors_offset: u32::try_from(all_colors.len()).expect("fits"),
384 chunk_colors_base_offset: u32::try_from(all_chunk_colors_base.len()).expect("fits"),
385 chunk_occupancy_offset: u32::try_from(all_chunk_occupancy.len()).expect("fits"),
386 slot_chunk_idx_offset,
387 vsid,
388 total_slots: total_slots as u32,
389 pool_dims: grid.pool_dims,
390 _pad0: 0,
391 occ_words_per_slot: layout.occ_words_per_slot,
392 offsets_words_per_slot: layout.offsets_words_per_slot,
393 mip_count: layout.mip_count,
394 _pad1: 0,
395 mip_occ_rel: layout.mip_occ_rel,
396 mip_coff_rel: layout.mip_coff_rel,
397 aabb_min,
398 _pad2: 0,
399 aabb_max,
400 _pad3: 0,
401 };
402
403 chunk_occupancy_shadow.push(grid_chunk_occupancy.clone());
404 slot_chunk_idx_shadow.push(grid_slot_chunk_idx.clone());
405
406 all_occupancy.extend_from_slice(&grid_occupancy);
407 all_color_offsets.extend_from_slice(&grid_color_offsets);
408 all_colors.extend_from_slice(&grid_colors);
409 all_chunk_colors_base.extend_from_slice(&grid_chunk_colors_base);
410 all_chunk_occupancy.extend_from_slice(&grid_chunk_occupancy);
411 for entry in &grid_slot_chunk_idx {
412 all_slot_chunk_idx.extend_from_slice(entry);
413 }
414 static_meta.push(meta);
415 }
416
417 if all_occupancy.is_empty() {
420 all_occupancy.push(0);
421 }
422 if all_color_offsets.is_empty() {
423 all_color_offsets.push(0);
424 }
425 if all_colors.is_empty() {
426 all_colors.push(0);
427 }
428 if all_chunk_colors_base.is_empty() {
429 all_chunk_colors_base.push(0);
430 }
431 if all_chunk_occupancy.is_empty() {
432 all_chunk_occupancy.push(0);
433 }
434 if all_slot_chunk_idx.is_empty() {
435 all_slot_chunk_idx.extend_from_slice(&[0; 4]);
438 }
439 if static_meta.is_empty() {
440 static_meta.push(GridStaticMeta::zeroed());
441 }
442
443 let occupancy_bytes = (all_occupancy.len() * 4) as u64;
444 let color_offsets_bytes = (all_color_offsets.len() * 4) as u64;
445 let colors_bytes = (all_colors.len() * 4) as u64;
446 let chunk_colors_base_bytes = (all_chunk_colors_base.len() * 4) as u64;
447 let chunk_occupancy_bytes = (all_chunk_occupancy.len() * 4) as u64;
448 let slot_chunk_idx_bytes = (all_slot_chunk_idx.len() * 4) as u64;
449 let static_meta_bytes = (static_meta.len() * std::mem::size_of::<GridStaticMeta>()) as u64;
450 let total_bytes = occupancy_bytes
451 + color_offsets_bytes
452 + colors_bytes
453 + chunk_colors_base_bytes
454 + chunk_occupancy_bytes
455 + slot_chunk_idx_bytes
456 + static_meta_bytes;
457
458 let slot_align_words = info
466 .grids
467 .iter()
468 .map(|g| u64::from(MipLayout::for_vsid(g.vsid).occ_words_per_slot))
469 .max()
470 .unwrap_or(1)
471 .max(1);
472 let (occupancy_pages, occupancy_page_words, occupancy_num_pages) =
473 split_occupancy_pages(device, &all_occupancy, slot_align_words);
474 let all_color_offsets =
475 create_storage(device, "roxlap-gpu scene.color_offsets", &all_color_offsets);
476 let all_colors = create_storage(device, "roxlap-gpu scene.colors", &all_colors);
477 let all_chunk_colors_base = create_storage(
478 device,
479 "roxlap-gpu scene.chunk_colors_base",
480 &all_chunk_colors_base,
481 );
482 let all_chunk_occupancy = create_storage(
483 device,
484 "roxlap-gpu scene.chunk_occupancy",
485 &all_chunk_occupancy,
486 );
487 let all_slot_chunk_idx_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
489 label: Some("roxlap-gpu scene.slot_chunk_idx"),
490 contents: bytemuck::cast_slice(&all_slot_chunk_idx),
491 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
492 });
493 let grid_static_meta = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
494 label: Some("roxlap-gpu scene.grid_static_meta"),
495 contents: bytemuck::cast_slice(&static_meta),
496 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
499 });
500
501 Self {
502 grid_count,
503 occupancy_pages,
504 occupancy_page_words,
505 occupancy_num_pages,
506 all_color_offsets,
507 all_colors,
508 all_chunk_colors_base,
509 all_chunk_occupancy,
510 all_slot_chunk_idx: all_slot_chunk_idx_buf,
511 grid_static_meta,
512 total_bytes,
513 static_meta,
514 chunk_occupancy_shadow,
515 slot_chunk_idx_shadow,
516 }
517 }
518
519 pub fn resident_bytes(&self) -> u64 {
520 self.total_bytes
521 }
522
523 pub fn refresh_chunk(
531 &mut self,
532 queue: &wgpu::Queue,
533 scene_idx: usize,
534 chunk_idx: [i32; 3],
535 chunk: &ChunkUpload,
536 ) -> RefreshOutcome {
537 let Some(meta) = self.static_meta.get(scene_idx).copied() else {
538 return RefreshOutcome::SceneIdxOob;
539 };
540 let slot_idx = modular_slot_idx(chunk_idx, meta.pool_dims);
541
542 let layout = MipLayout::for_vsid(meta.vsid);
545 let occ_words_per_slot = layout.occ_words_per_slot as usize;
546 let offsets_words_per_slot = layout.offsets_words_per_slot as usize;
547 let colors_stride = COLORS_PER_CHUNK_WORDS as usize;
548
549 assert_eq!(
550 chunk.mips.len() as u32,
551 layout.mip_count,
552 "refresh_chunk: mip count mismatch (chunk {} vs grid {})",
553 chunk.mips.len(),
554 layout.mip_count,
555 );
556
557 let slot_occ_base = meta.occupancy_offset as usize + slot_idx * occ_words_per_slot;
562 let page_words = self.occupancy_page_words as usize;
563 let page = slot_occ_base / page_words;
564 let slot_local_word = slot_occ_base % page_words;
565 debug_assert!(
566 slot_local_word + occ_words_per_slot <= page_words,
567 "occupancy slot straddles a page boundary — page size not slot-aligned",
568 );
569 let off_slot_base = meta.color_offsets_offset as usize + slot_idx * offsets_words_per_slot;
570 let col_slot_base = meta.colors_offset as usize + slot_idx * colors_stride;
571
572 let mut outcome = RefreshOutcome::Ok;
573 let mut color_cursor = 0usize;
574 for (m, mip) in chunk.mips.iter().enumerate() {
575 let local = slot_local_word + layout.mip_occ_rel[m] as usize;
577 queue.write_buffer(
578 &self.occupancy_pages[page],
579 (local * 4) as u64,
580 bytemuck::cast_slice(&mip.occupancy),
581 );
582 queue.write_buffer(
583 &self.occupancy_pages[page],
584 ((local + mip.occupancy.len()) * 4) as u64,
585 bytemuck::cast_slice(&mip.solid_occupancy),
586 );
587 let coff = off_slot_base + layout.mip_coff_rel[m] as usize;
589 queue.write_buffer(
590 &self.all_color_offsets,
591 (coff * 4) as u64,
592 bytemuck::cast_slice(&mip.color_offsets),
593 );
594 let remaining = colors_stride.saturating_sub(color_cursor);
596 let n = mip.colors.len().min(remaining);
597 if n < mip.colors.len() {
598 eprintln!(
599 "roxlap-gpu refresh_chunk: scene_idx={scene_idx} chunk_idx={chunk_idx:?} \
600 mip {m} colours overflow stride {colors_stride}; truncating",
601 );
602 outcome = RefreshOutcome::ColorsTruncated;
603 }
604 if n > 0 {
605 queue.write_buffer(
606 &self.all_colors,
607 ((col_slot_base + color_cursor) * 4) as u64,
608 bytemuck::cast_slice(&mip.colors[..n]),
609 );
610 }
611 color_cursor += n;
612 }
613
614 self.set_chunk_occupancy_bit(
616 queue,
617 scene_idx,
618 &meta,
619 slot_idx,
620 !chunk.mips[0].colors.is_empty(),
621 );
622
623 self.set_slot_chunk_idx(queue, scene_idx, &meta, slot_idx, chunk_idx);
625
626 self.sync_aabb(queue, scene_idx);
628
629 outcome
630 }
631
632 pub fn evict_chunk(
640 &mut self,
641 queue: &wgpu::Queue,
642 scene_idx: usize,
643 chunk_idx: [i32; 3],
644 ) -> bool {
645 let Some(meta) = self.static_meta.get(scene_idx).copied() else {
646 return false;
647 };
648 let slot_idx = modular_slot_idx(chunk_idx, meta.pool_dims);
649 let shadow_entry = self.slot_chunk_idx_shadow[scene_idx][slot_idx];
653 if shadow_entry[0] != chunk_idx[0]
654 || shadow_entry[1] != chunk_idx[1]
655 || shadow_entry[2] != chunk_idx[2]
656 {
657 return true;
658 }
659 self.set_chunk_occupancy_bit(queue, scene_idx, &meta, slot_idx, false);
660 self.set_slot_chunk_idx(queue, scene_idx, &meta, slot_idx, SLOT_EMPTY_SENTINEL);
661 self.sync_aabb(queue, scene_idx);
663 true
664 }
665
666 fn set_chunk_occupancy_bit(
667 &mut self,
668 queue: &wgpu::Queue,
669 scene_idx: usize,
670 meta: &GridStaticMeta,
671 slot_idx: usize,
672 new_bit: bool,
673 ) {
674 let word_idx = slot_idx >> 5;
675 let bit = slot_idx & 31;
676 let shadow = &mut self.chunk_occupancy_shadow[scene_idx][word_idx];
677 let was_bit = (*shadow >> bit) & 1 == 1;
678 if new_bit == was_bit {
679 return;
680 }
681 if new_bit {
682 *shadow |= 1u32 << bit;
683 } else {
684 *shadow &= !(1u32 << bit);
685 }
686 let global_word_idx = meta.chunk_occupancy_offset as usize + word_idx;
687 queue.write_buffer(
688 &self.all_chunk_occupancy,
689 (global_word_idx * 4) as u64,
690 bytemuck::bytes_of(shadow),
691 );
692 }
693
694 fn set_slot_chunk_idx(
695 &mut self,
696 queue: &wgpu::Queue,
697 scene_idx: usize,
698 meta: &GridStaticMeta,
699 slot_idx: usize,
700 chunk_idx: [i32; 3],
701 ) {
702 let entry = [chunk_idx[0], chunk_idx[1], chunk_idx[2], 0];
703 self.slot_chunk_idx_shadow[scene_idx][slot_idx] = entry;
704 let global_word_idx = meta.slot_chunk_idx_offset as usize + slot_idx * 4;
705 queue.write_buffer(
706 &self.all_slot_chunk_idx,
707 (global_word_idx * 4) as u64,
708 bytemuck::cast_slice(&entry),
709 );
710 }
711
712 fn sync_aabb(&mut self, queue: &wgpu::Queue, scene_idx: usize) {
720 let (aabb_min, aabb_max) = aabb_of_slots(&self.slot_chunk_idx_shadow[scene_idx]);
721 let meta = &mut self.static_meta[scene_idx];
722 if meta.aabb_min == aabb_min && meta.aabb_max == aabb_max {
723 return;
724 }
725 meta.aabb_min = aabb_min;
726 meta.aabb_max = aabb_max;
727 let off = (scene_idx * std::mem::size_of::<GridStaticMeta>()) as u64;
728 queue.write_buffer(&self.grid_static_meta, off, bytemuck::bytes_of(meta));
729 }
730}
731
732fn aabb_of_slots(slots: &[[i32; 4]]) -> ([i32; 3], [i32; 3]) {
738 let mut min = [i32::MAX; 3];
739 let mut max = [i32::MIN; 3];
740 for e in slots {
741 if e[0] == SLOT_EMPTY_SENTINEL[0]
742 && e[1] == SLOT_EMPTY_SENTINEL[1]
743 && e[2] == SLOT_EMPTY_SENTINEL[2]
744 {
745 continue;
746 }
747 for k in 0..3 {
748 if e[k] < min[k] {
749 min[k] = e[k];
750 }
751 if e[k] > max[k] {
752 max[k] = e[k];
753 }
754 }
755 }
756 (min, max)
757}
758
759#[must_use]
764pub fn modular_slot_idx(chunk_idx: [i32; 3], pool_dims: [u32; 3]) -> usize {
765 let mask_x = (pool_dims[0] - 1) as i32;
766 let mask_y = (pool_dims[1] - 1) as i32;
767 let mask_z = (pool_dims[2] - 1) as i32;
768 let sx = (chunk_idx[0] & mask_x) as usize;
769 let sy = (chunk_idx[1] & mask_y) as usize;
770 let sz = (chunk_idx[2] & mask_z) as usize;
771 sx + sy * (pool_dims[0] as usize) + sz * (pool_dims[0] as usize) * (pool_dims[1] as usize)
772}
773
774#[derive(Debug, Clone, Copy, PartialEq, Eq)]
778pub enum RefreshOutcome {
779 Ok,
780 ColorsTruncated,
784 ChunkOutOfBbox,
787 SceneIdxOob,
789}
790
791fn create_storage(device: &wgpu::Device, label: &str, data: &[u32]) -> wgpu::Buffer {
792 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
795 label: Some(label),
796 contents: bytemuck::cast_slice(data),
797 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
798 })
799}
800
801fn split_occupancy_pages(
812 device: &wgpu::Device,
813 words: &[u32],
814 slot_align_words: u64,
815) -> (Vec<wgpu::Buffer>, u32, u32) {
816 let total_words = words.len() as u64;
817 let limit_words = device.limits().max_storage_buffer_binding_size / 4;
819 let page_slots = (limit_words / slot_align_words).max(1);
821 let mut page_words = page_slots.saturating_mul(slot_align_words);
822 page_words = page_words.min(total_words.max(1));
825 let num_pages = total_words.div_ceil(page_words);
826 assert!(
827 num_pages as usize <= MAX_OCC_PAGES,
828 "occupancy needs {num_pages} pages (>{MAX_OCC_PAGES}) at this device's \
829 {limit_words}-word binding limit; shrink the streaming pool or raise MAX_OCC_PAGES",
830 );
831
832 let mut pages: Vec<wgpu::Buffer> = Vec::with_capacity(MAX_OCC_PAGES);
833 let page_words_usize = page_words as usize;
834 for p in 0..num_pages as usize {
835 let start = p * page_words_usize;
836 let end = ((p + 1) * page_words_usize).min(words.len());
837 pages.push(create_storage(
838 device,
839 &format!("roxlap-gpu scene.occupancy.page{p}"),
840 &words[start..end],
841 ));
842 }
843 while pages.len() < MAX_OCC_PAGES {
845 pages.push(create_storage(
846 device,
847 "roxlap-gpu scene.occupancy.page_dummy",
848 &[0u32],
849 ));
850 }
851 (
852 pages,
853 u32::try_from(page_words).expect("page_words fits u32"),
854 num_pages as u32,
855 )
856}
857
858#[cfg(test)]
859mod tests {
860 use super::*;
861
862 #[test]
863 fn grid_static_meta_matches_wgsl_std430_size() {
864 assert_eq!(std::mem::size_of::<GridStaticMeta>(), 144);
871 assert_eq!(std::mem::align_of::<GridStaticMeta>(), 4);
872 }
873
874 #[test]
875 fn mip_layout_offsets_accumulate() {
876 let l = MipLayout::for_vsid(128);
879 assert_eq!(l.mip_count, 6);
880 assert_eq!(l.mip_occ_rel[0], 0);
881 assert_eq!(l.mip_coff_rel[0], 0);
882
883 let mut occ = 0u32;
886 let mut coff = 0u32;
887 for m in 0..6u32 {
888 assert_eq!(l.mip_occ_rel[m as usize], occ, "occ rel mip {m}");
889 assert_eq!(l.mip_coff_rel[m as usize], coff, "coff rel mip {m}");
890 let v = 128u32 >> m;
891 occ += 2 * v * v * occ_words_per_column_for_mip(m);
892 coff += v * v + 1;
893 }
894 assert_eq!(l.occ_words_per_slot, occ);
895 assert_eq!(l.offsets_words_per_slot, coff);
896
897 assert_eq!(l.mip_occ_rel[1], 2 * 128 * 128 * 8);
900 assert!(l.occ_words_per_slot < 2 * 128 * 128 * 8 * 5 / 4);
903 }
904}