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}