Skip to main content

manifold_csg/
execution.rs

1//! Cooperative cancellation + progress observation for long-running boolean evaluations.
2//!
3//! Manifold operations are lazy: building a CSG tree is cheap, and the
4//! actual evaluation happens when you query results. Attach an
5//! [`ExecutionContext`] with [`Manifold::with_context`](crate::Manifold::with_context),
6//! then call an eager operation that consumes it, such as
7//! [`Manifold::status`](crate::Manifold::status) or `refine*`. An
8//! [`ExecutionContext`] lets you observe an in-flight evaluation from another
9//! thread and ask it to stop early.
10//!
11//! Cancellation is **sticky** (once cancelled, stays cancelled) and granular
12//! per-boolean (the upstream kernel checks the cancel flag at boolean
13//! boundaries; it doesn't interrupt a single boolean mid-flight). Progress
14//! is reported as a fraction in `[0.0, 1.0]`.
15//!
16//! The C API documents the underlying `ExecutionContext` as safe to read
17//! and write from any thread, so [`ExecutionContext`] is `Send` + `Sync`
18//! and can be wrapped in [`Arc`](std::sync::Arc) to share between the
19//! evaluator thread and a controller/observer thread.
20//!
21//! ```no_run
22//! use std::sync::Arc;
23//! use std::thread;
24//! use std::time::Duration;
25//! use manifold_csg::{ExecutionContext, Manifold};
26//!
27//! let ctx = Arc::new(ExecutionContext::new());
28//! let cancel = Arc::clone(&ctx);
29//!
30//! // Cancel the evaluation if it takes longer than 100ms.
31//! thread::spawn(move || {
32//!     thread::sleep(Duration::from_millis(100));
33//!     cancel.cancel();
34//! });
35//!
36//! let result = Manifold::cube(1.0, 1.0, 1.0, true);
37//! let status = result.with_context(&ctx).status();
38//! // `status` will be `Ok(())` for trivial work that finishes before cancel
39//! // fires; for a heavy boolean tree it would surface cancellation as an error.
40//! # let _ = status;
41//! ```
42//!
43//! Available since manifold3d's post-v3.4.1 master.
44
45use manifold_csg_sys::{
46    ManifoldExecutionContext, manifold_alloc_execution_context, manifold_delete_execution_context,
47    manifold_execution_context, manifold_execution_context_cancel,
48    manifold_execution_context_cancelled, manifold_execution_context_progress,
49};
50
51/// Observes progress and allows cooperative cancellation of long-running
52/// boolean evaluations. See the [module docs](self) for usage.
53pub struct ExecutionContext {
54    ptr: *mut ManifoldExecutionContext,
55}
56
57// SAFETY: The C API explicitly documents `ExecutionContext` as safe to
58// read/write from any thread; the upstream C++ implementation
59// synchronizes the cancel flag and progress counter internally.
60unsafe impl Send for ExecutionContext {}
61// SAFETY: Same justification as `Send` — all accessors (`cancel`,
62// `cancelled`, `progress`) are documented thread-safe at the C boundary.
63unsafe impl Sync for ExecutionContext {}
64
65impl ExecutionContext {
66    /// Create a fresh, un-cancelled context with zero progress.
67    #[must_use]
68    pub fn new() -> Self {
69        // SAFETY: alloc returns a valid handle; the constructor takes that
70        // raw memory and constructs an ExecutionContext into it (returning
71        // the same pointer).
72        let mem = unsafe { manifold_alloc_execution_context() };
73        // SAFETY: mem is a valid, freshly-allocated handle.
74        let ptr = unsafe { manifold_execution_context(mem) };
75        Self { ptr }
76    }
77
78    /// Request cancellation of any in-flight evaluation observing this
79    /// context. Sticky: once called, [`is_cancelled`](Self::is_cancelled)
80    /// returns `true` for the rest of the context's lifetime.
81    pub fn cancel(&self) {
82        // SAFETY: self.ptr is a valid handle for the lifetime of self;
83        // upstream documents thread-safe access.
84        unsafe { manifold_execution_context_cancel(self.ptr) };
85    }
86
87    /// Returns `true` if [`cancel`](Self::cancel) has been called.
88    #[must_use]
89    pub fn is_cancelled(&self) -> bool {
90        // SAFETY: self.ptr is a valid handle; upstream documents thread-safe access.
91        unsafe { manifold_execution_context_cancelled(self.ptr) != 0 }
92    }
93
94    /// Progress of an in-flight evaluation as a fraction in `[0.0, 1.0]`.
95    /// Meaningful only after this context has been attached to a manifold
96    /// via [`Manifold::with_context`](crate::Manifold::with_context) and an
97    /// eager op has started. For an unattached context, upstream reads
98    /// `1.0` ("nothing in flight, complete").
99    #[must_use]
100    pub fn progress(&self) -> f64 {
101        // SAFETY: self.ptr is a valid handle; upstream documents thread-safe access.
102        unsafe { manifold_execution_context_progress(self.ptr) }
103    }
104
105    /// Raw pointer for FFI calls that take an `ExecutionContext`. Crate-local
106    /// so the safe wrapper retains exclusive control of the lifetime.
107    pub(crate) fn as_ptr(&self) -> *mut ManifoldExecutionContext {
108        self.ptr
109    }
110}
111
112impl Default for ExecutionContext {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118impl Drop for ExecutionContext {
119    fn drop(&mut self) {
120        if !self.ptr.is_null() {
121            // SAFETY: ptr was returned by alloc + construct; not freed since.
122            unsafe { manifold_delete_execution_context(self.ptr) };
123            self.ptr = std::ptr::null_mut();
124        }
125    }
126}