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 MAX_SCENE_GRIDS: u32 = 16;
90
91pub const COLORS_PER_CHUNK_WORDS: u32 = 65536;
107
108pub const MAX_OCC_PAGES: usize = 4;
123
124#[derive(Debug, Clone, Copy)]
130pub struct GridRuntimeTransform {
131 pub grid_origin_world: [f64; 3],
135 pub world_to_grid_rotation: [[f32; 3]; 3],
137}
138
139impl Default for GridRuntimeTransform {
140 fn default() -> Self {
141 Self {
142 grid_origin_world: [0.0, 0.0, 0.0],
143 world_to_grid_rotation: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
144 }
145 }
146}
147
148pub struct SceneUpload {
152 pub grids: Vec<GridUpload>,
153}
154
155impl SceneUpload {
156 #[must_use]
157 pub fn grid_count(&self) -> u32 {
158 u32::try_from(self.grids.len()).unwrap_or(u32::MAX)
159 }
160}
161
162#[repr(C)]
171#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug)]
172pub struct GridStaticMeta {
173 pub occupancy_offset: u32,
175 pub color_offsets_offset: u32,
176 pub colors_offset: u32,
177 pub chunk_colors_base_offset: u32,
178 pub chunk_occupancy_offset: u32,
179 pub slot_chunk_idx_offset: u32,
183 pub vsid: u32,
184 pub total_slots: u32,
185 pub pool_dims: [u32; 3],
186 pub _pad0: u32,
187 pub occ_words_per_slot: u32,
191 pub offsets_words_per_slot: u32,
193 pub mip_count: u32,
195 pub _pad1: u32,
196 pub mip_occ_rel: [u32; MAX_GPU_MIPS],
199 pub mip_coff_rel: [u32; MAX_GPU_MIPS],
202}
203
204pub const SLOT_EMPTY_SENTINEL: [i32; 3] = [i32::MIN, i32::MIN, i32::MIN];
209
210pub struct GpuSceneResident {
212 pub grid_count: u32,
213 pub occupancy_pages: Vec<wgpu::Buffer>,
223 pub occupancy_page_words: u32,
225 pub occupancy_num_pages: u32,
227 pub all_color_offsets: wgpu::Buffer,
228 pub all_colors: wgpu::Buffer,
229 pub all_chunk_colors_base: wgpu::Buffer,
230 pub all_chunk_occupancy: wgpu::Buffer,
231 pub all_slot_chunk_idx: wgpu::Buffer,
235 pub grid_static_meta: wgpu::Buffer,
236 pub total_bytes: u64,
237 pub static_meta: Vec<GridStaticMeta>,
239 pub(crate) chunk_occupancy_shadow: Vec<Vec<u32>>,
244 pub(crate) slot_chunk_idx_shadow: Vec<Vec<[i32; 4]>>,
249}
250
251impl GpuSceneResident {
252 pub fn upload(device: &wgpu::Device, info: &SceneUpload) -> Self {
259 let grid_count = info.grid_count();
260 assert!(
261 grid_count <= MAX_SCENE_GRIDS,
262 "GpuSceneResident: scene has {grid_count} grids, shader supports {MAX_SCENE_GRIDS}",
263 );
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 meta = GridStaticMeta {
379 occupancy_offset: u32::try_from(all_occupancy.len()).expect("fits"),
380 color_offsets_offset: u32::try_from(all_color_offsets.len()).expect("fits"),
381 colors_offset: u32::try_from(all_colors.len()).expect("fits"),
382 chunk_colors_base_offset: u32::try_from(all_chunk_colors_base.len()).expect("fits"),
383 chunk_occupancy_offset: u32::try_from(all_chunk_occupancy.len()).expect("fits"),
384 slot_chunk_idx_offset,
385 vsid,
386 total_slots: total_slots as u32,
387 pool_dims: grid.pool_dims,
388 _pad0: 0,
389 occ_words_per_slot: layout.occ_words_per_slot,
390 offsets_words_per_slot: layout.offsets_words_per_slot,
391 mip_count: layout.mip_count,
392 _pad1: 0,
393 mip_occ_rel: layout.mip_occ_rel,
394 mip_coff_rel: layout.mip_coff_rel,
395 };
396
397 chunk_occupancy_shadow.push(grid_chunk_occupancy.clone());
398 slot_chunk_idx_shadow.push(grid_slot_chunk_idx.clone());
399
400 all_occupancy.extend_from_slice(&grid_occupancy);
401 all_color_offsets.extend_from_slice(&grid_color_offsets);
402 all_colors.extend_from_slice(&grid_colors);
403 all_chunk_colors_base.extend_from_slice(&grid_chunk_colors_base);
404 all_chunk_occupancy.extend_from_slice(&grid_chunk_occupancy);
405 for entry in &grid_slot_chunk_idx {
406 all_slot_chunk_idx.extend_from_slice(entry);
407 }
408 static_meta.push(meta);
409 }
410
411 if all_occupancy.is_empty() {
414 all_occupancy.push(0);
415 }
416 if all_color_offsets.is_empty() {
417 all_color_offsets.push(0);
418 }
419 if all_colors.is_empty() {
420 all_colors.push(0);
421 }
422 if all_chunk_colors_base.is_empty() {
423 all_chunk_colors_base.push(0);
424 }
425 if all_chunk_occupancy.is_empty() {
426 all_chunk_occupancy.push(0);
427 }
428 if all_slot_chunk_idx.is_empty() {
429 all_slot_chunk_idx.extend_from_slice(&[0; 4]);
432 }
433 if static_meta.is_empty() {
434 static_meta.push(GridStaticMeta::zeroed());
435 }
436
437 let occupancy_bytes = (all_occupancy.len() * 4) as u64;
438 let color_offsets_bytes = (all_color_offsets.len() * 4) as u64;
439 let colors_bytes = (all_colors.len() * 4) as u64;
440 let chunk_colors_base_bytes = (all_chunk_colors_base.len() * 4) as u64;
441 let chunk_occupancy_bytes = (all_chunk_occupancy.len() * 4) as u64;
442 let slot_chunk_idx_bytes = (all_slot_chunk_idx.len() * 4) as u64;
443 let static_meta_bytes = (static_meta.len() * std::mem::size_of::<GridStaticMeta>()) as u64;
444 let total_bytes = occupancy_bytes
445 + color_offsets_bytes
446 + colors_bytes
447 + chunk_colors_base_bytes
448 + chunk_occupancy_bytes
449 + slot_chunk_idx_bytes
450 + static_meta_bytes;
451
452 let slot_align_words = info
460 .grids
461 .iter()
462 .map(|g| u64::from(MipLayout::for_vsid(g.vsid).occ_words_per_slot))
463 .max()
464 .unwrap_or(1)
465 .max(1);
466 let (occupancy_pages, occupancy_page_words, occupancy_num_pages) =
467 split_occupancy_pages(device, &all_occupancy, slot_align_words);
468 let all_color_offsets =
469 create_storage(device, "roxlap-gpu scene.color_offsets", &all_color_offsets);
470 let all_colors = create_storage(device, "roxlap-gpu scene.colors", &all_colors);
471 let all_chunk_colors_base = create_storage(
472 device,
473 "roxlap-gpu scene.chunk_colors_base",
474 &all_chunk_colors_base,
475 );
476 let all_chunk_occupancy = create_storage(
477 device,
478 "roxlap-gpu scene.chunk_occupancy",
479 &all_chunk_occupancy,
480 );
481 let all_slot_chunk_idx_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
483 label: Some("roxlap-gpu scene.slot_chunk_idx"),
484 contents: bytemuck::cast_slice(&all_slot_chunk_idx),
485 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
486 });
487 let grid_static_meta = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
488 label: Some("roxlap-gpu scene.grid_static_meta"),
489 contents: bytemuck::cast_slice(&static_meta),
490 usage: wgpu::BufferUsages::STORAGE,
491 });
492
493 Self {
494 grid_count,
495 occupancy_pages,
496 occupancy_page_words,
497 occupancy_num_pages,
498 all_color_offsets,
499 all_colors,
500 all_chunk_colors_base,
501 all_chunk_occupancy,
502 all_slot_chunk_idx: all_slot_chunk_idx_buf,
503 grid_static_meta,
504 total_bytes,
505 static_meta,
506 chunk_occupancy_shadow,
507 slot_chunk_idx_shadow,
508 }
509 }
510
511 pub fn resident_bytes(&self) -> u64 {
512 self.total_bytes
513 }
514
515 pub fn refresh_chunk(
523 &mut self,
524 queue: &wgpu::Queue,
525 scene_idx: usize,
526 chunk_idx: [i32; 3],
527 chunk: &ChunkUpload,
528 ) -> RefreshOutcome {
529 let Some(meta) = self.static_meta.get(scene_idx).copied() else {
530 return RefreshOutcome::SceneIdxOob;
531 };
532 let slot_idx = modular_slot_idx(chunk_idx, meta.pool_dims);
533
534 let layout = MipLayout::for_vsid(meta.vsid);
537 let occ_words_per_slot = layout.occ_words_per_slot as usize;
538 let offsets_words_per_slot = layout.offsets_words_per_slot as usize;
539 let colors_stride = COLORS_PER_CHUNK_WORDS as usize;
540
541 assert_eq!(
542 chunk.mips.len() as u32,
543 layout.mip_count,
544 "refresh_chunk: mip count mismatch (chunk {} vs grid {})",
545 chunk.mips.len(),
546 layout.mip_count,
547 );
548
549 let slot_occ_base = meta.occupancy_offset as usize + slot_idx * occ_words_per_slot;
554 let page_words = self.occupancy_page_words as usize;
555 let page = slot_occ_base / page_words;
556 let slot_local_word = slot_occ_base % page_words;
557 debug_assert!(
558 slot_local_word + occ_words_per_slot <= page_words,
559 "occupancy slot straddles a page boundary — page size not slot-aligned",
560 );
561 let off_slot_base = meta.color_offsets_offset as usize + slot_idx * offsets_words_per_slot;
562 let col_slot_base = meta.colors_offset as usize + slot_idx * colors_stride;
563
564 let mut outcome = RefreshOutcome::Ok;
565 let mut color_cursor = 0usize;
566 for (m, mip) in chunk.mips.iter().enumerate() {
567 let local = slot_local_word + layout.mip_occ_rel[m] as usize;
569 queue.write_buffer(
570 &self.occupancy_pages[page],
571 (local * 4) as u64,
572 bytemuck::cast_slice(&mip.occupancy),
573 );
574 queue.write_buffer(
575 &self.occupancy_pages[page],
576 ((local + mip.occupancy.len()) * 4) as u64,
577 bytemuck::cast_slice(&mip.solid_occupancy),
578 );
579 let coff = off_slot_base + layout.mip_coff_rel[m] as usize;
581 queue.write_buffer(
582 &self.all_color_offsets,
583 (coff * 4) as u64,
584 bytemuck::cast_slice(&mip.color_offsets),
585 );
586 let remaining = colors_stride.saturating_sub(color_cursor);
588 let n = mip.colors.len().min(remaining);
589 if n < mip.colors.len() {
590 eprintln!(
591 "roxlap-gpu refresh_chunk: scene_idx={scene_idx} chunk_idx={chunk_idx:?} \
592 mip {m} colours overflow stride {colors_stride}; truncating",
593 );
594 outcome = RefreshOutcome::ColorsTruncated;
595 }
596 if n > 0 {
597 queue.write_buffer(
598 &self.all_colors,
599 ((col_slot_base + color_cursor) * 4) as u64,
600 bytemuck::cast_slice(&mip.colors[..n]),
601 );
602 }
603 color_cursor += n;
604 }
605
606 self.set_chunk_occupancy_bit(
608 queue,
609 scene_idx,
610 &meta,
611 slot_idx,
612 !chunk.mips[0].colors.is_empty(),
613 );
614
615 self.set_slot_chunk_idx(queue, scene_idx, &meta, slot_idx, chunk_idx);
617
618 outcome
619 }
620
621 pub fn evict_chunk(
629 &mut self,
630 queue: &wgpu::Queue,
631 scene_idx: usize,
632 chunk_idx: [i32; 3],
633 ) -> bool {
634 let Some(meta) = self.static_meta.get(scene_idx).copied() else {
635 return false;
636 };
637 let slot_idx = modular_slot_idx(chunk_idx, meta.pool_dims);
638 let shadow_entry = self.slot_chunk_idx_shadow[scene_idx][slot_idx];
642 if shadow_entry[0] != chunk_idx[0]
643 || shadow_entry[1] != chunk_idx[1]
644 || shadow_entry[2] != chunk_idx[2]
645 {
646 return true;
647 }
648 self.set_chunk_occupancy_bit(queue, scene_idx, &meta, slot_idx, false);
649 self.set_slot_chunk_idx(queue, scene_idx, &meta, slot_idx, SLOT_EMPTY_SENTINEL);
650 true
651 }
652
653 fn set_chunk_occupancy_bit(
654 &mut self,
655 queue: &wgpu::Queue,
656 scene_idx: usize,
657 meta: &GridStaticMeta,
658 slot_idx: usize,
659 new_bit: bool,
660 ) {
661 let word_idx = slot_idx >> 5;
662 let bit = slot_idx & 31;
663 let shadow = &mut self.chunk_occupancy_shadow[scene_idx][word_idx];
664 let was_bit = (*shadow >> bit) & 1 == 1;
665 if new_bit == was_bit {
666 return;
667 }
668 if new_bit {
669 *shadow |= 1u32 << bit;
670 } else {
671 *shadow &= !(1u32 << bit);
672 }
673 let global_word_idx = meta.chunk_occupancy_offset as usize + word_idx;
674 queue.write_buffer(
675 &self.all_chunk_occupancy,
676 (global_word_idx * 4) as u64,
677 bytemuck::bytes_of(shadow),
678 );
679 }
680
681 fn set_slot_chunk_idx(
682 &mut self,
683 queue: &wgpu::Queue,
684 scene_idx: usize,
685 meta: &GridStaticMeta,
686 slot_idx: usize,
687 chunk_idx: [i32; 3],
688 ) {
689 let entry = [chunk_idx[0], chunk_idx[1], chunk_idx[2], 0];
690 self.slot_chunk_idx_shadow[scene_idx][slot_idx] = entry;
691 let global_word_idx = meta.slot_chunk_idx_offset as usize + slot_idx * 4;
692 queue.write_buffer(
693 &self.all_slot_chunk_idx,
694 (global_word_idx * 4) as u64,
695 bytemuck::cast_slice(&entry),
696 );
697 }
698}
699
700#[must_use]
705pub fn modular_slot_idx(chunk_idx: [i32; 3], pool_dims: [u32; 3]) -> usize {
706 let mask_x = (pool_dims[0] - 1) as i32;
707 let mask_y = (pool_dims[1] - 1) as i32;
708 let mask_z = (pool_dims[2] - 1) as i32;
709 let sx = (chunk_idx[0] & mask_x) as usize;
710 let sy = (chunk_idx[1] & mask_y) as usize;
711 let sz = (chunk_idx[2] & mask_z) as usize;
712 sx + sy * (pool_dims[0] as usize) + sz * (pool_dims[0] as usize) * (pool_dims[1] as usize)
713}
714
715#[derive(Debug, Clone, Copy, PartialEq, Eq)]
719pub enum RefreshOutcome {
720 Ok,
721 ColorsTruncated,
725 ChunkOutOfBbox,
728 SceneIdxOob,
730}
731
732fn create_storage(device: &wgpu::Device, label: &str, data: &[u32]) -> wgpu::Buffer {
733 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
736 label: Some(label),
737 contents: bytemuck::cast_slice(data),
738 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
739 })
740}
741
742fn split_occupancy_pages(
753 device: &wgpu::Device,
754 words: &[u32],
755 slot_align_words: u64,
756) -> (Vec<wgpu::Buffer>, u32, u32) {
757 let total_words = words.len() as u64;
758 let limit_words = u64::from(device.limits().max_storage_buffer_binding_size) / 4;
759 let page_slots = (limit_words / slot_align_words).max(1);
761 let mut page_words = page_slots.saturating_mul(slot_align_words);
762 page_words = page_words.min(total_words.max(1));
765 let num_pages = total_words.div_ceil(page_words);
766 assert!(
767 num_pages as usize <= MAX_OCC_PAGES,
768 "occupancy needs {num_pages} pages (>{MAX_OCC_PAGES}) at this device's \
769 {limit_words}-word binding limit; shrink the streaming pool or raise MAX_OCC_PAGES",
770 );
771
772 let mut pages: Vec<wgpu::Buffer> = Vec::with_capacity(MAX_OCC_PAGES);
773 let page_words_usize = page_words as usize;
774 for p in 0..num_pages as usize {
775 let start = p * page_words_usize;
776 let end = ((p + 1) * page_words_usize).min(words.len());
777 pages.push(create_storage(
778 device,
779 &format!("roxlap-gpu scene.occupancy.page{p}"),
780 &words[start..end],
781 ));
782 }
783 while pages.len() < MAX_OCC_PAGES {
785 pages.push(create_storage(
786 device,
787 "roxlap-gpu scene.occupancy.page_dummy",
788 &[0u32],
789 ));
790 }
791 (
792 pages,
793 u32::try_from(page_words).expect("page_words fits u32"),
794 num_pages as u32,
795 )
796}
797
798#[cfg(test)]
799mod tests {
800 use super::*;
801
802 #[test]
803 fn grid_static_meta_matches_wgsl_std430_size() {
804 assert_eq!(std::mem::size_of::<GridStaticMeta>(), 112);
811 assert_eq!(std::mem::align_of::<GridStaticMeta>(), 4);
812 }
813
814 #[test]
815 fn mip_layout_offsets_accumulate() {
816 let l = MipLayout::for_vsid(128);
819 assert_eq!(l.mip_count, 6);
820 assert_eq!(l.mip_occ_rel[0], 0);
821 assert_eq!(l.mip_coff_rel[0], 0);
822
823 let mut occ = 0u32;
826 let mut coff = 0u32;
827 for m in 0..6u32 {
828 assert_eq!(l.mip_occ_rel[m as usize], occ, "occ rel mip {m}");
829 assert_eq!(l.mip_coff_rel[m as usize], coff, "coff rel mip {m}");
830 let v = 128u32 >> m;
831 occ += 2 * v * v * occ_words_per_column_for_mip(m);
832 coff += v * v + 1;
833 }
834 assert_eq!(l.occ_words_per_slot, occ);
835 assert_eq!(l.offsets_words_per_slot, coff);
836
837 assert_eq!(l.mip_occ_rel[1], 2 * 128 * 128 * 8);
840 assert!(l.occ_words_per_slot < 2 * 128 * 128 * 8 * 5 / 4);
843 }
844}