roxlap_gpu/grid.rs
1//! GPU.4 — grid-of-chunks upload + storage layout.
2//!
3//! Concatenates every chunk of one `roxlap-scene::Grid` into a few
4//! flat storage buffers so a single compute dispatch can outer-DDA
5//! through chunk-space + inner-DDA into any chunk it hits.
6//!
7#![allow(
8 clippy::cast_sign_loss,
9 clippy::cast_lossless,
10 clippy::doc_markdown,
11 clippy::field_reassign_with_default
12)]
13
14//! Memory layout (post-bedrock-strip):
15//!
16//! * `occupancy[meta_idx]` — one chunk's 128 KiB occupancy slice
17//! starts at `meta_idx * vsid² * OCC_WORDS_PER_COLUMN` u32 words.
18//! Uniform per chunk (all chunks are vsid² × CHUNK_Z voxels).
19//! * `color_offsets[meta_idx]` — one chunk's `vsid² + 1` u32
20//! offsets start at `meta_idx * (vsid² + 1)` u32 words. Uniform
21//! per chunk.
22//! * `colors` — variable per chunk. Per-chunk base index lives in
23//! `chunk_colors_base[meta_idx]`.
24//! * `chunk_occupancy` — 1 bit per chunk position. Bit at
25//! `meta_idx` set iff that chunk has any textured voxels. The
26//! outer DDA uses this to skip empty chunks in one step.
27//!
28//! The `meta_idx` for a chunk at `(chx, chy, chz)` is its row-major
29//! offset within the grid's `chunks_dims` bounding box:
30//!
31//! ```text
32//! rel = chunk_idx - origin_chunk
33//! meta_idx = rel.x + rel.y * chunks_dims.x + rel.z * chunks_dims.x * chunks_dims.y
34//! ```
35
36use crate::decompress::{ChunkUpload, CHUNK_Z, OCC_WORDS_PER_COLUMN};
37
38/// CPU-side aggregation of a grid's chunks ready to upload. Host
39/// (e.g. `roxlap-scene-demo`) builds this by iterating its
40/// `roxlap-scene::Grid` and calling [`crate::decompress_chunk`] per
41/// materialised chunk.
42pub struct GridUpload {
43 /// Shared XY extent of every chunk in voxels. Matches
44 /// `roxlap-scene::CHUNK_SIZE_XY = 128`.
45 pub vsid: u32,
46 /// Lowest chunk index present in the grid `(min_chx, min_chy,
47 /// min_chz)`. The grid's bounding box runs from `origin_chunk`
48 /// to `origin_chunk + chunks_dims` exclusive.
49 pub origin_chunk: [i32; 3],
50 /// Chunk-count along each axis = `max - min + 1`.
51 pub chunks_dims: [u32; 3],
52 /// GPU.7 slot-pool dimensions for modular chunk indexing.
53 /// Every component MUST be a power of 2. A chunk at index
54 /// `(chx, chy, chz)` maps to slot
55 /// `(chx & (pool_dims.x - 1), chy & (pool_dims.y - 1),
56 /// chz & (pool_dims.z - 1))`. As long as
57 /// `pool_dims_axis ≥ active_range_along_axis`, no two
58 /// simultaneously-resident chunks collide. Set this larger than
59 /// `chunks_dims` only when streaming may install chunks at
60 /// indices outside the initial bbox.
61 pub pool_dims: [u32; 3],
62 /// `(chunk_idx, decompressed)` pairs. Chunks outside the
63 /// pool's collision-free active range are still accepted —
64 /// modular indexing will assign them slots; the caller is
65 /// responsible for avoiding collisions with other resident
66 /// chunks.
67 pub chunks: Vec<([i32; 3], ChunkUpload)>,
68}
69
70impl GridUpload {
71 #[must_use]
72 pub fn total_chunks(&self) -> u32 {
73 self.chunks_dims[0] * self.chunks_dims[1] * self.chunks_dims[2]
74 }
75
76 /// Default GPU.7 [`Self::pool_dims`] derived from
77 /// `chunks_dims` — each axis rounded up to the next power of 2.
78 /// Use this when the grid is static + slots map 1:1 to bbox
79 /// positions; for streaming grids, callers should pick a
80 /// larger pool that covers `2 × r_active_chunks + 1` along
81 /// each axis.
82 #[must_use]
83 pub fn default_pool_dims(chunks_dims: [u32; 3]) -> [u32; 3] {
84 [
85 ceil_pow2(chunks_dims[0]),
86 ceil_pow2(chunks_dims[1]),
87 ceil_pow2(chunks_dims[2]),
88 ]
89 }
90
91 /// Linear chunk index `(meta_idx)` for `(chx, chy, chz)` in the
92 /// grid's row-major bounding-box order. `None` if the index is
93 /// outside the grid.
94 #[must_use]
95 pub fn meta_idx_of(&self, chunk_idx: [i32; 3]) -> Option<u32> {
96 let dx = chunk_idx[0] - self.origin_chunk[0];
97 let dy = chunk_idx[1] - self.origin_chunk[1];
98 let dz = chunk_idx[2] - self.origin_chunk[2];
99 if dx < 0
100 || dy < 0
101 || dz < 0
102 || (dx as u32) >= self.chunks_dims[0]
103 || (dy as u32) >= self.chunks_dims[1]
104 || (dz as u32) >= self.chunks_dims[2]
105 {
106 return None;
107 }
108 Some(
109 (dx as u32)
110 + (dy as u32) * self.chunks_dims[0]
111 + (dz as u32) * self.chunks_dims[0] * self.chunks_dims[1],
112 )
113 }
114}
115
116/// Round `n` up to the nearest power of 2. `0` and `1` both return
117/// `1`. Used to derive a GPU.7 [`GridUpload::pool_dims`] from a
118/// non-pow2 `chunks_dims`.
119#[must_use]
120pub fn ceil_pow2(n: u32) -> u32 {
121 if n <= 1 {
122 return 1;
123 }
124 1u32 << (32 - (n - 1).leading_zeros())
125}
126
127/// Compute the smallest bounding box that contains every
128/// `(chunk_idx, _)` in `chunks`. Returns `None` if `chunks` is
129/// empty.
130#[must_use]
131pub fn bounding_box_of(chunks: impl IntoIterator<Item = [i32; 3]>) -> Option<([i32; 3], [u32; 3])> {
132 let mut min = [i32::MAX; 3];
133 let mut max = [i32::MIN; 3];
134 let mut any = false;
135 for idx in chunks {
136 for i in 0..3 {
137 if idx[i] < min[i] {
138 min[i] = idx[i];
139 }
140 if idx[i] > max[i] {
141 max[i] = idx[i];
142 }
143 }
144 any = true;
145 }
146 if !any {
147 return None;
148 }
149 #[allow(clippy::cast_sign_loss)]
150 let dims = [
151 (max[0] - min[0] + 1) as u32,
152 (max[1] - min[1] + 1) as u32,
153 (max[2] - min[2] + 1) as u32,
154 ];
155 Some((min, dims))
156}
157
158/// Number of u32 words a single chunk's per-chunk occupancy slice
159/// occupies in the concatenated grid occupancy buffer. Useful for
160/// host-side memory budgeting.
161#[must_use]
162pub fn occ_words_per_chunk(vsid: u32) -> u32 {
163 vsid * vsid * OCC_WORDS_PER_COLUMN
164}
165
166/// Z-extent of every chunk — re-export of the `CHUNK_Z` constant
167/// so hosts can budget without pulling `crate::decompress` in.
168pub const GRID_CHUNK_Z: u32 = CHUNK_Z;