Skip to main content

runmat_runtime/builtins/plotting/core/
perf.rs

1//! Plotting performance knobs and level-of-detail helpers.
2
3use once_cell::sync::OnceCell;
4use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
5
6const DEFAULT_SCATTER_TARGET_POINTS: u32 = 250_000;
7const MIN_SCATTER_TARGET_POINTS: u32 = 16_384;
8const DEFAULT_SURFACE_VERTEX_BUDGET: u64 = 400_000;
9const MIN_SURFACE_VERTEX_BUDGET: u64 = 65_536;
10const SCATTER_EXTENT_REFERENCE: f32 = 250.0;
11const SURFACE_EXTENT_REFERENCE: f32 = 500.0;
12
13static SCATTER_TARGET_POINTS: AtomicU32 = AtomicU32::new(DEFAULT_SCATTER_TARGET_POINTS);
14static SURFACE_VERTEX_BUDGET: AtomicU64 = AtomicU64::new(DEFAULT_SURFACE_VERTEX_BUDGET);
15static SCATTER_ENV_INIT: OnceCell<()> = OnceCell::new();
16static SURFACE_ENV_INIT: OnceCell<()> = OnceCell::new();
17
18/// Returns the target number of scatter points we aim to draw per dispatch
19/// before enabling compute-side decimation. The value can be overridden via
20/// the `RUNMAT_PLOT_SCATTER_TARGET` environment variable or by calling
21/// [`set_scatter_target_points`].
22pub(crate) fn scatter_target_points() -> u32 {
23    ensure_scatter_env();
24    SCATTER_TARGET_POINTS.load(Ordering::Relaxed)
25}
26
27/// Override the scatter point target at runtime (e.g., via CLI flags or
28/// TypeScript bindings). Values below the minimum threshold are clamped.
29pub fn set_scatter_target_points(value: u32) {
30    let clamped = value.max(MIN_SCATTER_TARGET_POINTS);
31    SCATTER_TARGET_POINTS.store(clamped, Ordering::Relaxed);
32}
33
34/// Returns the maximum number of surface vertices we attempt to pack on the GPU
35/// before enabling LOD. Override via `RUNMAT_PLOT_SURFACE_VERTEX_BUDGET` or
36/// [`set_surface_vertex_budget`].
37pub(crate) fn surface_vertex_budget() -> u64 {
38    ensure_surface_env();
39    SURFACE_VERTEX_BUDGET.load(Ordering::Relaxed)
40}
41
42/// Override the surface vertex budget at runtime.
43pub fn set_surface_vertex_budget(value: u64) {
44    let clamped = value.max(MIN_SURFACE_VERTEX_BUDGET);
45    SURFACE_VERTEX_BUDGET.store(clamped, Ordering::Relaxed);
46}
47
48fn ensure_scatter_env() {
49    SCATTER_ENV_INIT.get_or_init(|| {
50        if let Some(value) = read_env_u32("RUNMAT_PLOT_SCATTER_TARGET") {
51            set_scatter_target_points(value);
52        }
53    });
54}
55
56fn ensure_surface_env() {
57    SURFACE_ENV_INIT.get_or_init(|| {
58        if let Some(value) = read_env_u64("RUNMAT_PLOT_SURFACE_VERTEX_BUDGET") {
59            set_surface_vertex_budget(value);
60        }
61    });
62}
63
64fn read_env_u32(key: &str) -> Option<u32> {
65    std::env::var(key).ok().and_then(|value| value.parse().ok())
66}
67
68fn read_env_u64(key: &str) -> Option<u64> {
69    std::env::var(key).ok().and_then(|value| value.parse().ok())
70}
71
72#[derive(Debug, Clone, Copy)]
73pub(crate) struct SurfaceLod {
74    pub stride_x: u32,
75    pub stride_y: u32,
76    pub lod_x_len: u32,
77    pub lod_y_len: u32,
78}
79
80impl SurfaceLod {
81    pub fn vertex_count(&self) -> usize {
82        (self.lod_x_len as usize) * (self.lod_y_len as usize)
83    }
84}
85
86fn adjust_for_extent<T>(base: T, extent_hint: f32, reference: f32) -> T
87where
88    T: num_traits::NumCast + Copy,
89{
90    if !extent_hint.is_finite() || extent_hint <= 0.0 {
91        return base;
92    }
93    let reference = reference.max(1.0);
94    let ratio = (reference / extent_hint).clamp(0.25, 4.0);
95    let adjusted = num_traits::cast::<_, f64>(base).unwrap_or(0.0) * ratio as f64;
96    num_traits::cast(adjusted).unwrap_or(base)
97}
98
99/// Compute an approximate level-of-detail strategy for a surface grid so that
100/// the generated vertex count stays below the configured budget. `extent_hint`
101/// should represent the planar diagonal of the surface (e.g. sqrt(dx^2+dy^2))
102/// so that zoomed-in views retain more detail than zoomed-out ones.
103pub(crate) fn compute_surface_lod(x_len: usize, y_len: usize, extent_hint: f32) -> SurfaceLod {
104    let x_len = x_len.max(1);
105    let y_len = y_len.max(1);
106    let x_u32 = x_len as u32;
107    let y_u32 = y_len as u32;
108    let total_vertices = (x_len as u64) * (y_len as u64);
109    let mut budget = surface_vertex_budget().max(MIN_SURFACE_VERTEX_BUDGET);
110    if extent_hint.is_finite() && extent_hint > 0.0 {
111        let adjusted =
112            adjust_for_extent::<u64>(budget, extent_hint, SURFACE_EXTENT_REFERENCE).max(1);
113        budget = adjusted.max(MIN_SURFACE_VERTEX_BUDGET);
114    }
115
116    if total_vertices <= budget {
117        return SurfaceLod {
118            stride_x: 1,
119            stride_y: 1,
120            lod_x_len: x_u32,
121            lod_y_len: y_u32,
122        };
123    }
124
125    let stride_guess = ((total_vertices as f64 / budget as f64).sqrt().ceil() as u32).max(2);
126    let mut stride_x = stride_guess.min(x_u32);
127    let mut stride_y = stride_guess.min(y_u32);
128    let mut lod_x_len = ceil_div(x_u32, stride_x);
129    let mut lod_y_len = ceil_div(y_u32, stride_y);
130
131    for _ in 0..32 {
132        if (lod_x_len as u64) * (lod_y_len as u64) <= budget {
133            break;
134        }
135        if lod_x_len >= lod_y_len && stride_x < x_u32 {
136            stride_x = stride_x.saturating_add(1).min(x_u32);
137            lod_x_len = ceil_div(x_u32, stride_x);
138        } else if stride_y < y_u32 {
139            stride_y = stride_y.saturating_add(1).min(y_u32);
140            lod_y_len = ceil_div(y_u32, stride_y);
141        } else {
142            break;
143        }
144    }
145
146    SurfaceLod {
147        stride_x: stride_x.max(1),
148        stride_y: stride_y.max(1),
149        lod_x_len: lod_x_len.max(1),
150        lod_y_len: lod_y_len.max(1),
151    }
152}
153
154fn ceil_div(len: u32, stride: u32) -> u32 {
155    if stride == 0 {
156        return len;
157    }
158    len.div_ceil(stride)
159}
160
161/// Compute the level-of-detail stride for scatter3 given the number of points
162/// and an extent hint (diagonal of the plot bounds). Larger plots will target
163/// fewer points, while zoomed-in plots retain more detail.
164pub(crate) fn scatter3_lod_stride(point_count: u32, extent_hint: f32) -> u32 {
165    let base = scatter_target_points();
166    let adjusted = adjust_for_extent::<u32>(base, extent_hint, SCATTER_EXTENT_REFERENCE)
167        .max(MIN_SCATTER_TARGET_POINTS);
168    if point_count <= adjusted {
169        1
170    } else {
171        point_count.div_ceil(adjusted)
172    }
173}
174
175#[cfg(test)]
176pub(crate) mod tests {
177    use super::*;
178
179    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
180    #[test]
181    fn scatter_target_env_override() {
182        std::env::set_var("RUNMAT_PLOT_SCATTER_TARGET", "300000");
183        // Clear cached cell by recreating process-local OnceCell via CHAIN?
184        // We cannot reset OnceCell, so just assert helper respects defaults via
185        // direct read function.
186        assert_eq!(read_env_u64("RUNMAT_PLOT_SCATTER_TARGET").unwrap(), 300_000);
187    }
188
189    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
190    #[test]
191    fn surface_lod_identity_when_small() {
192        let lod = compute_surface_lod(32, 64, 10.0);
193        assert_eq!(lod.stride_x, 1);
194        assert_eq!(lod.stride_y, 1);
195        assert_eq!(lod.lod_x_len, 32);
196        assert_eq!(lod.lod_y_len, 64);
197    }
198
199    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
200    #[test]
201    fn surface_lod_downsamples_large_grid() {
202        let lod = compute_surface_lod(4096, 4096, 10_000.0);
203        assert!(lod.stride_x > 1);
204        assert!(lod.stride_y > 1);
205        assert!((lod.lod_x_len as u64) * (lod.lod_y_len as u64) <= surface_vertex_budget());
206    }
207
208    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
209    #[test]
210    fn scatter3_stride_scales_with_extent() {
211        set_scatter_target_points(100_000);
212        let dense = scatter3_lod_stride(1_000_000, 50.0);
213        let sparse = scatter3_lod_stride(1_000_000, 5_000.0);
214        assert!(dense < sparse, "{dense} vs {sparse}");
215    }
216}