Skip to main content

rlx_core/render/scenes/warp_mesh/
mesh.rs

1//! The mesh grid: its bounds, the per-vertex coordinate every program is
2//! evaluated against, and the CPU-side state a frame's vertex buffer is
3//! assembled from.
4//!
5//! No `wgpu` beyond the [`Vertex`](super::shaders::Vertex) it fills — this is
6//! the arithmetic half of the scene, and it is the half the renderer also calls
7//! (it sizes its per-vertex scratch off [`vertex_count`] and evaluates a
8//! preset's bindings at [`vertex_position`]).
9
10// Hot-path panic-denial pragma (Plan 0002 Phase 2; render/ is scanned by the
11// hygiene guard).
12#![deny(
13    clippy::unwrap_used,
14    clippy::expect_used,
15    clippy::indexing_slicing,
16    clippy::panic,
17    clippy::unreachable
18)]
19
20// A continuation of one module split across five files, so it needs the names
21// `warp_mesh/mod.rs` has in scope.
22use super::*;
23
24/// The smallest grid a `[mesh]` table may name, in cells. Below two the mesh is
25/// a single quad and the per-vertex program has no interior to interpolate.
26pub const MIN_MESH: u32 = 2;
27
28/// The largest grid **any** tier may name, in cells — the `.milk` format's own
29/// ceiling (`meshx <= 128`, `meshy <= 96`), so a converted preset's requested
30/// grid is always representable.
31pub const MAX_MESH: (u32, u32) = (128, 96);
32
33/// The grid a `[mesh]` table's absent keys mean. Coarse enough to be free on any
34/// machine and fine enough that a `rad`-driven program reads as a curve rather
35/// than as facets.
36pub const DEFAULT_MESH: (u32, u32) = (32, 24);
37
38/// Clamp a preset's requested grid into what `tier` will carry.
39///
40/// **The one place the tier clamp happens.** Two consumers need the same answer
41/// — the scene, which builds the vertex and index buffers, and the renderer,
42/// which sizes the per-vertex evaluation scratch — and if they disagreed the
43/// renderer would hand the scene a series of the wrong length every frame. Pure,
44/// so both can call it and a test can hold them to the same value.
45pub fn clamp_grid(requested: (u32, u32), tier: &crate::render::TierConfig) -> (u32, u32) {
46    (
47        requested
48            .0
49            .clamp(MIN_MESH, tier.mesh_grid.0.clamp(MIN_MESH, MAX_MESH.0)),
50        requested
51            .1
52            .clamp(MIN_MESH, tier.mesh_grid.1.clamp(MIN_MESH, MAX_MESH.1)),
53    )
54}
55
56/// How many vertices a grid of `mesh` cells has. One more than the cell count on
57/// each axis — the fencepost the whole per-vertex path is sized by.
58pub fn vertex_count(mesh: (u32, u32)) -> usize {
59    (mesh.0 as usize + 1) * (mesh.1 as usize + 1)
60}
61
62/// The `(x, y, rad, ang)` a vertex's `[per_vertex]` bindings are evaluated
63/// against, for the vertex at column `col`, row `row` of a `mesh` grid, on a
64/// render target of aspect `aspect`.
65///
66/// `x` and `y` are the vertex's uv in `0..=1`, with `y = 0` at the **top** —
67/// texture space, the space every sampler in this file addresses.
68///
69/// `rad` is the distance from the mesh centre and `ang` the angle there, both in
70/// the **aspect-corrected** space of the render target (ADR-0037): `rad` reaches
71/// `1.0` at the middle of the top and bottom edges on any display, and further
72/// than that at the sides of a wide one. So a program written as
73/// `zoom = 1 + rad * 0.2` makes a circular figure on a 16:9 monitor and the same
74/// circular figure on a 5:4 one, which is the property the ADR exists for and the
75/// reason this takes `aspect` rather than deriving one from `mesh`.
76///
77/// `ang` is in `0..tau`, measured counter-clockwise from the +x axis in *screen*
78/// terms (y is flipped on the way in, so a positive angle turns the way an author
79/// looking at the screen expects).
80pub fn vertex_position(col: u32, row: u32, mesh: (u32, u32), aspect: f32) -> (f32, f32, f32, f32) {
81    let x = col as f32 / mesh.0.max(1) as f32;
82    let y = row as f32 / mesh.1.max(1) as f32;
83    // Aspect correction on the *x* axis, so one unit of `rad` is one half-height
84    // whatever the target's shape. A non-finite or non-positive aspect degrades
85    // to square rather than poisoning every vertex with a NaN.
86    let aspect = if aspect.is_finite() && aspect > 0.0 {
87        aspect
88    } else {
89        1.0
90    };
91    let px = (x - 0.5) * 2.0 * aspect;
92    // Flip y so +y is up, which is what makes `ang` read the way it looks.
93    let py = (0.5 - y) * 2.0;
94    let rad = (px * px + py * py).sqrt();
95    let mut ang = py.atan2(px);
96    if ang < 0.0 {
97        ang += std::f32::consts::TAU;
98    }
99    (x, y, rad, ang)
100}
101
102/// The per-frame values the CPU assembles a vertex buffer from.
103pub(super) struct MeshState {
104    /// The clamped grid this state is sized for.
105    pub(super) mesh: (u32, u32),
106    /// One value per vertex for each of the nine outputs. Only the entries whose
107    /// `bound` flag is set this frame are read; the rest fall back to the scalar
108    /// param, which is what makes a `[per_vertex]` binding an override.
109    pub(super) values: [Vec<f32>; OUTPUTS],
110    pub(super) bound: [bool; OUTPUTS],
111    /// The assembled vertex buffer, resized only when the grid changes.
112    pub(super) vertices: Vec<Vertex>,
113}
114
115impl MeshState {
116    pub(super) fn new(mesh: (u32, u32)) -> Self {
117        let n = vertex_count(mesh);
118        Self {
119            mesh,
120            values: std::array::from_fn(|_| vec![0.0; n]),
121            bound: [false; OUTPUTS],
122            vertices: vec![
123                Vertex {
124                    clip: [0.0; 2],
125                    t0: [0.0; 4],
126                    t1: [0.0; 4],
127                    t2: [0.0; 4],
128                };
129                n
130            ],
131        }
132    }
133
134    /// Resize to `mesh` if it differs — off the hot path (a preset switch), so
135    /// the allocation here is not a per-frame one.
136    pub(super) fn resize(&mut self, mesh: (u32, u32)) {
137        if self.mesh == mesh {
138            return;
139        }
140        *self = Self::new(mesh);
141    }
142
143    /// Fill `out` with this frame's vertices. `scalars` supplies the fallback for
144    /// every output with no `[per_vertex]` binding this frame.
145    pub(super) fn assemble(&mut self, scalars: &[f32; OUTPUTS]) {
146        let (mx, my) = self.mesh;
147        let mut v = 0usize;
148        for row in 0..=my {
149            for col in 0..=mx {
150                let clip = [
151                    (col as f32 / mx.max(1) as f32) * 2.0 - 1.0,
152                    1.0 - (row as f32 / my.max(1) as f32) * 2.0,
153                ];
154                let mut out = [0.0f32; OUTPUTS];
155                for (i, slot) in out.iter_mut().enumerate() {
156                    *slot = match (self.bound.get(i), self.values.get(i)) {
157                        (Some(true), Some(series)) => series.get(v).copied().unwrap_or(0.0),
158                        _ => scalars.get(i).copied().unwrap_or(0.0),
159                    };
160                }
161                if let Some(slot) = self.vertices.get_mut(v) {
162                    *slot = Vertex {
163                        clip,
164                        t0: [out[0], out[1], out[2], out[3]],
165                        t1: [out[4], out[5], out[6], out[7]],
166                        t2: [out[8], 0.0, 0.0, 0.0],
167                    };
168                }
169                v += 1;
170            }
171        }
172    }
173}
174
175/// The triangle indices for a `mesh` grid, two triangles per cell.
176pub(super) fn build_indices(mesh: (u32, u32)) -> Vec<u32> {
177    let (mx, my) = mesh;
178    let stride = mx + 1;
179    let mut out = Vec::with_capacity((mx as usize) * (my as usize) * 6);
180    for row in 0..my {
181        for col in 0..mx {
182            let a = row * stride + col;
183            let b = a + 1;
184            let c = a + stride;
185            let d = c + 1;
186            out.extend_from_slice(&[a, c, b, b, c, d]);
187        }
188    }
189    out
190}