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;