runmat_runtime/builtins/plotting/core/
perf.rs1use 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
18pub(crate) fn scatter_target_points() -> u32 {
22 SCATTER_TARGET_POINTS.load(Ordering::Relaxed)
23}
24
25pub 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
32pub(crate) fn surface_vertex_budget() -> u64 {
36 SURFACE_VERTEX_BUDGET.load(Ordering::Relaxed)
37}
38
39pub 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
81pub(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
143pub(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}