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    #[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;