Skip to main content

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;