1use std::fmt;
2use std::os::raw::c_void;
3use std::panic::{AssertUnwindSafe, catch_unwind};
4use std::ptr;
5use std::sync::{Condvar, Mutex};
6
7use maplibre_native_ffi_core as maplibre_core;
8use maplibre_native_ffi_sys as sys;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12#[non_exhaustive]
13pub struct CanonicalTileId {
14 pub z: u32,
15 pub x: u32,
16 pub y: u32,
17}
18
19impl CanonicalTileId {
20 pub const fn new(z: u32, x: u32, y: u32) -> Self {
21 Self { z, x, y }
22 }
23
24 pub(crate) fn from_native(raw: sys::mln_canonical_tile_id) -> Self {
25 Self {
26 z: raw.z,
27 x: raw.x,
28 y: raw.y,
29 }
30 }
31
32 pub(crate) fn to_native(self) -> sys::mln_canonical_tile_id {
33 sys::mln_canonical_tile_id {
34 z: self.z,
35 x: self.x,
36 y: self.y,
37 }
38 }
39}
40
41type TileCallback = dyn Fn(CanonicalTileId) + Send + Sync + 'static;
42
43#[non_exhaustive]
49pub struct CustomGeometrySourceOptions {
50 fetch_tile: Box<TileCallback>,
51 cancel_tile: Option<Box<TileCallback>>,
52 pub min_zoom: Option<f64>,
54 pub max_zoom: Option<f64>,
56 pub tolerance: Option<f64>,
58 pub tile_size: Option<u32>,
60 pub buffer: Option<u32>,
62 pub clip: Option<bool>,
64 pub wrap: Option<bool>,
66}
67
68impl fmt::Debug for CustomGeometrySourceOptions {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 f.debug_struct("CustomGeometrySourceOptions")
71 .field("has_cancel_tile", &self.cancel_tile.is_some())
72 .field("min_zoom", &self.min_zoom)
73 .field("max_zoom", &self.max_zoom)
74 .field("tolerance", &self.tolerance)
75 .field("tile_size", &self.tile_size)
76 .field("buffer", &self.buffer)
77 .field("clip", &self.clip)
78 .field("wrap", &self.wrap)
79 .finish_non_exhaustive()
80 }
81}
82
83impl CustomGeometrySourceOptions {
84 pub fn new<F>(fetch_tile: F) -> Self
85 where
86 F: Fn(CanonicalTileId) + Send + Sync + 'static,
87 {
88 Self {
89 fetch_tile: Box::new(fetch_tile),
90 cancel_tile: None,
91 min_zoom: None,
92 max_zoom: None,
93 tolerance: None,
94 tile_size: None,
95 buffer: None,
96 clip: None,
97 wrap: None,
98 }
99 }
100
101 pub fn with_cancel_tile<F>(mut self, cancel_tile: F) -> Self
102 where
103 F: Fn(CanonicalTileId) + Send + Sync + 'static,
104 {
105 self.cancel_tile = Some(Box::new(cancel_tile));
106 self
107 }
108}
109
110#[derive(Debug, Default)]
111struct CallbackLifecycle {
112 active: usize,
113 closing: bool,
114 closed: bool,
115}
116
117pub(crate) struct CustomGeometrySourceState {
120 fetch_tile: Box<TileCallback>,
121 cancel_tile: Option<Box<TileCallback>>,
122 min_zoom: Option<f64>,
123 max_zoom: Option<f64>,
124 tolerance: Option<f64>,
125 tile_size: Option<u32>,
126 buffer: Option<u32>,
127 clip: Option<bool>,
128 wrap: Option<bool>,
129 lifecycle: Mutex<CallbackLifecycle>,
130 idle: Condvar,
131}
132
133impl fmt::Debug for CustomGeometrySourceState {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 f.debug_struct("CustomGeometrySourceState")
136 .finish_non_exhaustive()
137 }
138}
139
140impl CustomGeometrySourceState {
141 pub(crate) fn new(options: CustomGeometrySourceOptions) -> Box<Self> {
142 Box::new(Self {
143 fetch_tile: options.fetch_tile,
144 cancel_tile: options.cancel_tile,
145 min_zoom: options.min_zoom,
146 max_zoom: options.max_zoom,
147 tolerance: options.tolerance,
148 tile_size: options.tile_size,
149 buffer: options.buffer,
150 clip: options.clip,
151 wrap: options.wrap,
152 lifecycle: Mutex::new(CallbackLifecycle::default()),
153 idle: Condvar::new(),
154 })
155 }
156
157 pub(crate) fn descriptor(&self) -> sys::mln_custom_geometry_source_options {
158 maplibre_core::style::custom_geometry_source_options_to_native(
159 maplibre_core::style::CustomGeometrySourceDescriptorFields {
160 fetch_tile: Some(fetch_tile_trampoline),
161 cancel_tile: self
162 .cancel_tile
163 .as_ref()
164 .map(|_| cancel_tile_trampoline as _),
165 release_user_data: Some(release_trampoline),
166 user_data: ptr::from_ref(self).cast_mut().cast::<c_void>(),
167 min_zoom: self.min_zoom,
168 max_zoom: self.max_zoom,
169 tolerance: self.tolerance,
170 tile_size: self.tile_size,
171 buffer: self.buffer,
172 clip: self.clip,
173 wrap: self.wrap,
174 },
175 )
176 }
177
178 pub(crate) fn close(&self) {
179 let mut lifecycle = self
180 .lifecycle
181 .lock()
182 .unwrap_or_else(|poisoned| poisoned.into_inner());
183 if lifecycle.closed {
184 return;
185 }
186 lifecycle.closing = true;
187 while lifecycle.active != 0 {
188 lifecycle = self
189 .idle
190 .wait(lifecycle)
191 .unwrap_or_else(|poisoned| poisoned.into_inner());
192 }
193 lifecycle.closed = true;
194 }
195
196 fn invoke_fetch(&self, tile_id: CanonicalTileId) {
197 let Some(_guard) = self.enter_callback() else {
198 return;
199 };
200 let _ = catch_unwind(AssertUnwindSafe(|| (self.fetch_tile)(tile_id)));
201 }
202
203 fn invoke_cancel(&self, tile_id: CanonicalTileId) {
204 let Some(_guard) = self.enter_callback() else {
205 return;
206 };
207 if let Some(cancel_tile) = &self.cancel_tile {
208 let _ = catch_unwind(AssertUnwindSafe(|| cancel_tile(tile_id)));
209 }
210 }
211
212 fn enter_callback(&self) -> Option<CallbackGuard<'_>> {
213 let mut lifecycle = self
214 .lifecycle
215 .lock()
216 .unwrap_or_else(|poisoned| poisoned.into_inner());
217 if lifecycle.closing || lifecycle.closed {
218 return None;
219 }
220 lifecycle.active += 1;
221 Some(CallbackGuard { state: self })
222 }
223
224 fn exit_callback(&self) {
225 let mut lifecycle = self
226 .lifecycle
227 .lock()
228 .unwrap_or_else(|poisoned| poisoned.into_inner());
229 lifecycle.active -= 1;
230 if lifecycle.active == 0 {
231 self.idle.notify_all();
232 }
233 }
234}
235
236impl Drop for CustomGeometrySourceState {
237 fn drop(&mut self) {
238 self.close();
239 }
240}
241
242struct CallbackGuard<'a> {
243 state: &'a CustomGeometrySourceState,
244}
245
246impl Drop for CallbackGuard<'_> {
247 fn drop(&mut self) {
248 self.state.exit_callback();
249 }
250}
251
252unsafe extern "C" fn fetch_tile_trampoline(
253 user_data: *mut c_void,
254 tile_id: sys::mln_canonical_tile_id,
255) {
256 let Some(state) = ptr::NonNull::new(user_data.cast::<CustomGeometrySourceState>()) else {
257 return;
258 };
259 unsafe { state.as_ref() }.invoke_fetch(CanonicalTileId::from_native(tile_id));
262}
263
264unsafe extern "C" fn release_trampoline(user_data: *mut c_void) {
265 let Some(state) = ptr::NonNull::new(user_data.cast::<CustomGeometrySourceState>()) else {
266 return;
267 };
268 let state = unsafe { Box::from_raw(state.as_ptr()) };
273 let _ = catch_unwind(AssertUnwindSafe(move || drop(state)));
274}
275
276unsafe extern "C" fn cancel_tile_trampoline(
277 user_data: *mut c_void,
278 tile_id: sys::mln_canonical_tile_id,
279) {
280 let Some(state) = ptr::NonNull::new(user_data.cast::<CustomGeometrySourceState>()) else {
281 return;
282 };
283 unsafe { state.as_ref() }.invoke_cancel(CanonicalTileId::from_native(tile_id));
286}
287
288#[cfg(test)]
289mod tests {
290 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
291 use std::sync::{Arc, Condvar, Mutex};
292 use std::time::Duration;
293
294 use super::*;
295
296 fn tile(z: u32, x: u32, y: u32) -> sys::mln_canonical_tile_id {
297 CanonicalTileId::new(z, x, y).to_native()
298 }
299
300 #[test]
301 fn custom_geometry_callbacks_invoke_fetch_and_cancel_with_copied_tile_id() {
303 let fetched = Arc::new(Mutex::new(Vec::new()));
304 let cancelled = Arc::new(Mutex::new(Vec::new()));
305 let fetched_callback = Arc::clone(&fetched);
306 let cancelled_callback = Arc::clone(&cancelled);
307 let state = CustomGeometrySourceState::new(
308 CustomGeometrySourceOptions::new(move |tile_id| {
309 fetched_callback.lock().unwrap().push(tile_id);
310 })
311 .with_cancel_tile(move |tile_id| {
312 cancelled_callback.lock().unwrap().push(tile_id);
313 }),
314 );
315 let descriptor = state.descriptor();
316
317 unsafe {
318 descriptor.fetch_tile.unwrap()(descriptor.user_data, tile(1, 2, 3));
319 descriptor.cancel_tile.unwrap()(descriptor.user_data, tile(4, 5, 6));
320 }
321
322 assert_eq!(
323 fetched.lock().unwrap().as_slice(),
324 &[CanonicalTileId::new(1, 2, 3)]
325 );
326 assert_eq!(
327 cancelled.lock().unwrap().as_slice(),
328 &[CanonicalTileId::new(4, 5, 6)]
329 );
330 }
331
332 #[test]
333 fn custom_geometry_callbacks_contain_panics() {
335 let cancel_called = Arc::new(AtomicBool::new(false));
336 let cancel_called_callback = Arc::clone(&cancel_called);
337 let state = CustomGeometrySourceState::new(
338 CustomGeometrySourceOptions::new(|_| panic!("fetch panic")).with_cancel_tile(
339 move |_| {
340 cancel_called_callback.store(true, Ordering::SeqCst);
341 panic!("cancel panic");
342 },
343 ),
344 );
345 let descriptor = state.descriptor();
346
347 unsafe {
348 descriptor.fetch_tile.unwrap()(descriptor.user_data, tile(0, 0, 0));
349 descriptor.cancel_tile.unwrap()(descriptor.user_data, tile(0, 0, 0));
350 }
351
352 assert!(cancel_called.load(Ordering::SeqCst));
353 }
354
355 #[test]
356 fn custom_geometry_state_release_waits_for_active_upcalls() {
358 let entered = Arc::new((Mutex::new(false), Condvar::new()));
359 let release = Arc::new((Mutex::new(false), Condvar::new()));
360 let closed = Arc::new(AtomicBool::new(false));
361 let close_attempts = Arc::new(AtomicUsize::new(0));
362 let entered_callback = Arc::clone(&entered);
363 let release_callback = Arc::clone(&release);
364 let state = CustomGeometrySourceState::new(CustomGeometrySourceOptions::new(move |_| {
365 let (entered_lock, entered_cvar) = &*entered_callback;
366 *entered_lock.lock().unwrap() = true;
367 entered_cvar.notify_all();
368
369 let (release_lock, release_cvar) = &*release_callback;
370 let released = release_lock.lock().unwrap();
371 let (_released, timeout) = release_cvar
372 .wait_timeout_while(released, Duration::from_secs(5), |released| !*released)
373 .unwrap();
374 assert!(!timeout.timed_out());
375 }));
376 let descriptor = state.descriptor();
377 let callback = descriptor.fetch_tile.unwrap();
378 let user_data = descriptor.user_data as usize;
379
380 std::thread::scope(|scope| {
381 scope.spawn(move || unsafe {
382 callback(user_data as *mut c_void, tile(1, 1, 1));
383 });
384 let (entered_lock, entered_cvar) = &*entered;
385 let entered_guard = entered_lock.lock().unwrap();
386 let (_entered_guard, timeout) = entered_cvar
387 .wait_timeout_while(entered_guard, Duration::from_secs(5), |entered| !*entered)
388 .unwrap();
389 assert!(!timeout.timed_out());
390
391 let closed_for_thread = Arc::clone(&closed);
392 let close_attempts_for_thread = Arc::clone(&close_attempts);
393 let state_ref = &*state;
394 scope.spawn(move || {
395 close_attempts_for_thread.fetch_add(1, Ordering::SeqCst);
396 state_ref.close();
397 closed_for_thread.store(true, Ordering::SeqCst);
398 });
399
400 std::thread::sleep(Duration::from_millis(50));
401 assert_eq!(close_attempts.load(Ordering::SeqCst), 1);
402 assert!(!closed.load(Ordering::SeqCst));
403 let (release_lock, release_cvar) = &*release;
404 *release_lock.lock().unwrap() = true;
405 release_cvar.notify_all();
406 });
407
408 assert!(closed.load(Ordering::SeqCst));
409 unsafe {
410 descriptor.fetch_tile.unwrap()(descriptor.user_data, tile(9, 9, 9));
411 }
412 }
413}