Skip to main content

runmat_runtime/builtins/plotting/core/
perf.rs

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