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 /// Capacity of the grid's bounding box in chunks
72 /// (`chunks_dims.x · y · z`) — an upper bound on `chunks.len()`,
73 /// which may be smaller when interior chunks are absent.
74 #[must_use]
75 pub fn total_chunks(&self) -> u32 {
76 self.chunks_dims[0] * self.chunks_dims[1] * self.chunks_dims[2]
77 }
78
79 /// Default GPU.7 [`Self::pool_dims`] derived from
80 /// `chunks_dims` — each axis rounded up to the next power of 2.
81 /// Use this when the grid is static + slots map 1:1 to bbox
82 /// positions; for streaming grids, callers should pick a
83 /// larger pool that covers `2 × r_active_chunks + 1` along
84 /// each axis.
85 #[must_use]
86 pub fn default_pool_dims(chunks_dims: [u32; 3]) -> [u32; 3] {
87 [
88 ceil_pow2(chunks_dims[0]),
89 ceil_pow2(chunks_dims[1]),
90 ceil_pow2(chunks_dims[2]),
91 ]
92 }
93
94 /// Linear chunk index `(meta_idx)` for `(chx, chy, chz)` in the
95 /// grid's row-major bounding-box order. `None` if the index is
96 /// outside the grid.
97 #[must_use]
98 pub fn meta_idx_of(&self, chunk_idx: [i32; 3]) -> Option<u32> {
99 let dx = chunk_idx[0] - self.origin_chunk[0];
100 let dy = chunk_idx[1] - self.origin_chunk[1];
101 let dz = chunk_idx[2] - self.origin_chunk[2];
102 if dx < 0
103 || dy < 0
104 || dz < 0
105 || (dx as u32) >= self.chunks_dims[0]
106 || (dy as u32) >= self.chunks_dims[1]
107 || (dz as u32) >= self.chunks_dims[2]
108 {
109 return None;
110 }
111 Some(
112 (dx as u32)
113 + (dy as u32) * self.chunks_dims[0]
114 + (dz as u32) * self.chunks_dims[0] * self.chunks_dims[1],
115 )
116 }
117}
118
119/// Round `n` up to the nearest power of 2. `0` and `1` both return
120/// `1`. Used to derive a GPU.7 [`GridUpload::pool_dims`] from a
121/// non-pow2 `chunks_dims`.
122#[must_use]
123pub fn ceil_pow2(n: u32) -> u32 {
124 if n <= 1 {
125 return 1;
126 }
127 1u32 << (32 - (n - 1).leading_zeros())
128}
129
130/// Compute the smallest bounding box that contains every
131/// `(chunk_idx, _)` in `chunks`. Returns `None` if `chunks` is
132/// empty.
133#[must_use]
134pub fn bounding_box_of(chunks: impl IntoIterator<Item = [i32; 3]>) -> Option<([i32; 3], [u32; 3])> {
135 let mut min = [i32::MAX; 3];
136 let mut max = [i32::MIN; 3];
137 let mut any = false;
138 for idx in chunks {
139 for i in 0..3 {
140 if idx[i] < min[i] {
141 min[i] = idx[i];
142 }
143 if idx[i] > max[i] {
144 max[i] = idx[i];
145 }
146 }
147 any = true;
148 }
149 if !any {
150 return None;
151 }
152 #[allow(clippy::cast_sign_loss)]
153 let dims = [
154 (max[0] - min[0] + 1) as u32,
155 (max[1] - min[1] + 1) as u32,
156 (max[2] - min[2] + 1) as u32,
157 ];
158 Some((min, dims))
159}
160
161/// Number of u32 words a single chunk's per-chunk occupancy slice
162/// occupies in the concatenated grid occupancy buffer. Useful for
163/// host-side memory budgeting.
164#[must_use]
165pub fn occ_words_per_chunk(vsid: u32) -> u32 {
166 vsid * vsid * OCC_WORDS_PER_COLUMN
167}
168
169/// Z-extent of every chunk — re-export of the `CHUNK_Z` constant
170/// so hosts can budget without pulling `crate::decompress` in.
171pub const GRID_CHUNK_Z: u32 = CHUNK_Z;