Skip to main content

manifold_rust/
cancel.rs

1// Copyright 2026 Lars Brubaker
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// cancel.rs — cooperative cancellation for the long-running kernel entry
16// points (boolean, CSG tree evaluation).
17//
18// Port of the *cancellation* half of the C++ `ExecutionContext` mechanism. The
19// progress-reporting half (donePhases / totalPhases / Progress()) is not
20// ported: src/progress.rs provides progress reporting natively instead,
21// reporting named phases plus an intra-phase fraction rather than the C++'s
22// whole-pipeline phase count. Both are threaded through the kernel the same
23// way — as an `Option<&_>` whose `None` path is the pre-existing code.
24//
25// What the C++ does (cpp-reference/manifold/src/execution_impl.h:81-114):
26//   - `ExecutionContext::Impl` holds a single `std::atomic<bool> cancel`,
27//     shared through a `shared_ptr` so copies of a context observe each other.
28//   - `ExecutionContext::Cancel()` stores `true` with `memory_order_relaxed`
29//     (execution_impl.cpp:91-93); cancel is advisory, so it needs no
30//     synchronisation with the surrounding data.
31//   - `IsCancelled(ctx)` (execution_impl.h:112-114) is the single canonical
32//     reader. It returns `false` for a null ctx, which is how the "no
33//     cancellation requested" path stays free: `ctx == nullptr` folds the
34//     atomic load out of the loop entirely.
35//   - Cancel is *sticky*: it is never reset, so once a context is cancelled
36//     every later operation through it short-circuits
37//     (execution_impl.cpp:30-33 explicitly preserves it across resets).
38//   - The observable result of an interrupted operation is an *empty* manifold
39//     whose status is `Manifold::Error::Cancelled` — the last enum value, added
40//     at the end of the list (manifold.h:124-140). See `ADVANCE_PHASE_OR_RETURN`
41//     (execution_impl.h:150-160) and `Boolean3::Result`'s `phase()` lambda
42//     (boolean_result.cpp:758-770), both of which do `MakeEmpty(Cancelled)`.
43//
44// The Rust mirror of `ExecutionContext::Impl*` is `Option<&CancelToken>`:
45// `None` is C++'s `nullptr` and costs nothing (no atomic is touched), `Some`
46// carries an `Arc<AtomicBool>` that any number of threads may share.
47//
48// Where the checks live (mirroring the C++ sites named above):
49//   csg_tree.rs           `to_leaf_node` (per stack step), `simple_boolean`
50//                         (entry), `batch_boolean` (per round), `batch_union`
51//                         (per chunk)          <- csg_tree.cpp:172/460/511/752
52//   boolean3.rs           `boolean_with_token` (entry), `Boolean3::new_with_token`
53//                         (four stage boundaries), plus intra-stage checks in
54//                         `intersect12` / `winding03`
55//                                              <- boolean3.cpp:380/437/456/472/
56//                                                 480/530/536/552/558
57//   boolean_result_       all eleven phase boundaries between the assembly
58//     assemble.rs         stages, including the final one after SortGeometry
59//                                              <- boolean_result.cpp:758-963
60//   face_op.rs            `face2tri_ct` entry plus per-face triangulation
61//                                              <- face_op.cpp:192/290
62//
63// The invariant those sites buy: **a cancelled token can never produce a
64// NoError result.** Every stage of the boolean pipeline is bracketed by a
65// check, so a cancel that lands inside a stage is always observed at the next
66// boundary and converted to `Error::Cancelled` before the value escapes. It is
67// not enough to check "often enough for good latency" — a missed *final* check
68// would report success for an operation the caller cancelled.
69//
70// Deviations from the C++, all in the "checks fewer places" direction, none
71// affecting the uncancelled result and none breaking the invariant above:
72//   - Progress reporting (donePhases/totalPhases/Progress) is not ported.
73//   - C++ threads ctx *into* `SortGeometry`, `ReorderHalfedges` and
74//     `SimplifyTopology`; we only bracket them. The cost is latency (one run of
75//     the trailing simplify + sort block), not a wrong status.
76//   - C++ also threads ctx into the non-Boolean entry points (`FromMeshGL`,
77//     `Smooth`, `LevelSet`, `Hull`, `Minkowski`, `Refine`). Here only the
78//     boolean / CSG pipeline is cancellable at all; those entry points ignore
79//     tokens rather than reporting a stale status, since they take none.
80
81use std::sync::atomic::{AtomicBool, Ordering};
82use std::sync::Arc;
83
84/// A cheaply cloneable, thread-safe cancellation flag.
85///
86/// Clones share one flag (the C++ `ExecutionContext` pimpl semantics), so a
87/// token handed to a worker thread can be cancelled from anywhere. Cancellation
88/// is **sticky**: there is no way to un-cancel a token, matching C++, where the
89/// flag is deliberately preserved across operation resets. Start a fresh token
90/// for work that should be allowed to complete.
91///
92/// # Example
93/// ```
94/// use manifold_rust::cancel::CancelToken;
95/// use manifold_rust::manifold::Manifold;
96/// use manifold_rust::linalg::Vec3;
97/// use manifold_rust::types::{Error, OpType};
98///
99/// let token = CancelToken::new();
100/// token.cancel();
101/// let a = Manifold::cube(Vec3::splat(1.0), true);
102/// let b = Manifold::cube(Vec3::splat(1.0), true);
103/// let result = a.boolean_with_token(&b, OpType::Add, Some(&token));
104/// assert_eq!(result.status(), Error::Cancelled);
105/// ```
106#[derive(Clone, Debug, Default)]
107pub struct CancelToken {
108    flag: Arc<AtomicBool>,
109}
110
111impl CancelToken {
112    /// A fresh, uncancelled token.
113    pub fn new() -> Self {
114        Self {
115            flag: Arc::new(AtomicBool::new(false)),
116        }
117    }
118
119    /// Request cancellation. Callable from any thread, including while another
120    /// thread is inside an operation holding a clone of this token.
121    ///
122    /// `Relaxed` matches C++ `cancel.store(true, std::memory_order_relaxed)`:
123    /// the flag is advisory and orders nothing else, and the readers only ever
124    /// use the value to decide whether to stop early.
125    pub fn cancel(&self) {
126        self.flag.store(true, Ordering::Relaxed);
127    }
128
129    /// Whether cancellation has been requested.
130    #[inline]
131    pub fn is_cancelled(&self) -> bool {
132        self.flag.load(Ordering::Relaxed)
133    }
134}
135
136/// Canonical reader, mirroring C++ `IsCancelled(ExecutionContext::Impl*)`.
137///
138/// `None` (C++'s `nullptr` ctx) returns `false` without touching an atomic, so
139/// the uncancellable path — every existing caller — reads exactly as it did
140/// before this module existed.
141#[inline]
142pub fn is_cancelled(token: Option<&CancelToken>) -> bool {
143    match token {
144        Some(t) => t.is_cancelled(),
145        None => false,
146    }
147}
148
149#[cfg(test)]
150#[path = "cancel_tests.rs"]
151mod tests;