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}