Skip to content

Commit 2adcb66

Browse files
zmerlynnclaude
andcommitted
Add ExecutionContext for boolean progress + cancellation
Progress towards #971. Introduces ExecutionContext for observing progress and requesting cancellation of long-running boolean evaluations via a new Status(ExecutionContext&) overload; no other public API is affected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 97df55b commit 2adcb66

14 files changed

Lines changed: 522 additions & 24 deletions

File tree

bindings/c/conv.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ ManifoldError to_c(manifold::Manifold::Error error) {
116116
case Manifold::Error::InvalidTangents:
117117
e = MANIFOLD_INVALID_TANGENTS;
118118
break;
119+
case Manifold::Error::Cancelled:
120+
e = MANIFOLD_CANCELLED;
121+
break;
119122
};
120123
return e;
121124
}

bindings/c/include/manifold/types.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ typedef enum ManifoldError {
119119
MANIFOLD_INVALID_CONSTRUCTION,
120120
MANIFOLD_RESULT_TOO_LARGE,
121121
MANIFOLD_INVALID_TANGENTS,
122+
MANIFOLD_CANCELLED,
122123
} ManifoldError;
123124

124125
typedef enum ManifoldFillRule {

bindings/python/manifold3d.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,8 @@ NB_MODULE(manifold3d, m) {
731731
.value("FaceIDWrongLength", Manifold::Error::FaceIDWrongLength)
732732
.value("InvalidConstruction", Manifold::Error::InvalidConstruction)
733733
.value("ResultTooLarge", Manifold::Error::ResultTooLarge)
734-
.value("InvalidTangents", Manifold::Error::InvalidTangents);
734+
.value("InvalidTangents", Manifold::Error::InvalidTangents)
735+
.value("Cancelled", Manifold::Error::Cancelled);
735736

736737
nb::enum_<CrossSection::FillRule>(m, "FillRule")
737738
.value("EvenOdd", CrossSection::FillRule::EvenOdd,

bindings/wasm/bindings.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ EMSCRIPTEN_BINDINGS(whatever) {
8383
.value("FaceIDWrongLength", Manifold::Error::FaceIDWrongLength)
8484
.value("InvalidConstruction", Manifold::Error::InvalidConstruction)
8585
.value("ResultTooLarge", Manifold::Error::ResultTooLarge)
86-
.value("InvalidTangents", Manifold::Error::InvalidTangents);
86+
.value("InvalidTangents", Manifold::Error::InvalidTangents)
87+
.value("Cancelled", Manifold::Error::Cancelled);
8788

8889
enum_<CrossSection::FillRule>("fillrule")
8990
.value("EvenOdd", CrossSection::FillRule::EvenOdd)

bindings/wasm/manifold-global-types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,4 @@ export type ErrorStatus = 'NoError'|'NonFiniteVertex'|'NotManifold'|
120120
'VertexOutOfBounds'|'PropertiesWrongLength'|'MissingPositionProperties'|
121121
'MergeVectorsDifferentLengths'|'MergeIndexOutOfBounds'|
122122
'TransformWrongLength'|'RunIndexWrongLength'|'FaceIDWrongLength'|
123-
'InvalidConstruction'|'ResultTooLarge'|'InvalidTangents';
123+
'InvalidConstruction'|'ResultTooLarge'|'InvalidTangents'|'Cancelled';

include/manifold/common.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#pragma once
1616
#include <cmath>
1717
#include <limits>
18+
#include <memory>
1819
#include <vector>
1920

2021
#ifdef MANIFOLD_DEBUG
@@ -170,6 +171,55 @@ struct RayHit {
170171
vec3 normal = vec3(0.0);
171172
};
172173

174+
/**
175+
* @brief Observe and control a long-running Manifold evaluation.
176+
*
177+
* Pass to Manifold::Status(ctx) to observe progress and optionally request
178+
* cancellation of the evaluation. Safe to read/write from any thread.
179+
*
180+
* Copyable and movable: copies share the same underlying state via a
181+
* shared_ptr, so one thread can evaluate while another holds a copy and
182+
* observes Progress() or calls Cancel(). Use a separate context per
183+
* evaluation; passing the same context (or a copy of it) to two
184+
* concurrent Status(ctx) calls produces meaningless progress values
185+
* because both calls reset and mutate the same counters.
186+
*
187+
* Cancellation is permanent for a Manifold: once requested and detected,
188+
* the Manifold's status becomes Error::Cancelled and stays Cancelled. To
189+
* retry, construct a new Manifold. A context, however, is reusable: each
190+
* Status(ctx) call resets the progress counters, but it does NOT clear
191+
* the cancel flag — once Cancel() has been called on a context, every
192+
* subsequent evaluation with that context (or any copy of it) will
193+
* short-circuit to Error::Cancelled. Construct a fresh context to make a
194+
* new evaluation cancellable independently.
195+
*
196+
* Cancellation granularity is currently per-boolean-operation; a single
197+
* large boolean may run to completion before the flag is checked again.
198+
* This may improve in future versions.
199+
*/
200+
class ExecutionContext {
201+
public:
202+
ExecutionContext();
203+
~ExecutionContext();
204+
ExecutionContext(const ExecutionContext&);
205+
ExecutionContext(ExecutionContext&&) noexcept;
206+
ExecutionContext& operator=(const ExecutionContext&);
207+
ExecutionContext& operator=(ExecutionContext&&) noexcept;
208+
209+
/// Request cancellation. Can be called from any thread. Idempotent.
210+
void Cancel();
211+
/// Has cancellation been requested?
212+
bool Cancelled() const;
213+
/// Normalized progress in [0, 1]. Returns 0 before any work has been
214+
/// scheduled. Monotonically increases during evaluation.
215+
double Progress() const;
216+
217+
/// @internal Opaque implementation. Defined in src/execution_impl.h;
218+
/// accessible only to internal code that includes that header.
219+
struct Impl;
220+
std::shared_ptr<Impl> impl_;
221+
};
222+
173223
/**
174224
* @brief Axis-aligned 3D box, primarily for bounding.
175225
*/

include/manifold/manifold.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,13 +371,15 @@ class Manifold {
371371
InvalidConstruction,
372372
ResultTooLarge,
373373
InvalidTangents,
374+
Cancelled,
374375
};
375376

376377
/** @name Information
377378
* Details of the manifold
378379
*/
379380
///@{
380381
Error Status() const;
382+
Error Status(ExecutionContext& ctx) const;
381383
bool IsEmpty() const;
382384
size_t NumVert() const;
383385
size_t NumEdge() const;
@@ -539,7 +541,7 @@ class Manifold {
539541
mutable std::shared_ptr<CsgNode> pNode_;
540542

541543
std::shared_ptr<CsgNode> LoadPNode() const;
542-
CsgLeafNode& GetCsgLeafNode() const;
544+
CsgLeafNode& GetCsgLeafNode(ExecutionContext::Impl* ctx = nullptr) const;
543545
};
544546
/** @} */
545547

@@ -582,6 +584,8 @@ inline std::string ToString(const Manifold::Error& error) {
582584
return "Result Too Large";
583585
case Manifold::Error::InvalidTangents:
584586
return "Invalid Tangents";
587+
case Manifold::Error::Cancelled:
588+
return "Cancelled";
585589
default:
586590
return "Unknown Error";
587591
};

src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ set(
1919
constructors.cpp
2020
csg_tree.cpp
2121
edge_op.cpp
22+
execution_impl.cpp
2223
face_op.cpp
2324
impl.cpp
2425
manifold.cpp
@@ -40,6 +41,7 @@ set(
4041
boolean3.h
4142
collider.h
4243
csg_tree.h
44+
execution_impl.h
4345
hashtable.h
4446
impl.h
4547
iters.h

src/csg_tree.cpp

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
#include "boolean3.h"
2222
#include "csg_tree.h"
23+
#include "execution_impl.h"
2324
#include "impl.h"
2425
#include "mesh_fixes.h"
2526
#include "parallel.h"
@@ -96,7 +97,8 @@ std::shared_ptr<const Manifold::Impl> CsgLeafNode::GetImpl() const {
9697
return pImpl_;
9798
}
9899

99-
std::shared_ptr<CsgLeafNode> CsgLeafNode::ToLeafNode() const {
100+
std::shared_ptr<CsgLeafNode> CsgLeafNode::ToLeafNode(
101+
ExecutionContext::Impl*) const {
100102
return std::make_shared<CsgLeafNode>(*this);
101103
}
102104

@@ -139,8 +141,19 @@ std::shared_ptr<CsgLeafNode> ImplToLeaf(Manifold::Impl&& impl) {
139141
std::make_shared<Manifold::Impl>(std::move(impl)));
140142
}
141143

144+
// Build a leaf with the given error status — used to short-circuit boolean
145+
// evaluation on cancellation.
146+
std::shared_ptr<CsgLeafNode> ErrorLeaf(Manifold::Error err) {
147+
Manifold::Impl impl;
148+
impl.status_ = err;
149+
return ImplToLeaf(std::move(impl));
150+
}
151+
142152
std::shared_ptr<CsgLeafNode> SimpleBoolean(const Manifold::Impl& a,
143-
const Manifold::Impl& b, OpType op) {
153+
const Manifold::Impl& b, OpType op,
154+
ExecutionContext::Impl* ctx) {
155+
if (ctx && ctx->cancel.load(std::memory_order_relaxed))
156+
return ErrorLeaf(Manifold::Error::Cancelled);
144157
#ifdef MANIFOLD_DEBUG
145158
auto dump = [&]() {
146159
dump_lock.lock();
@@ -170,6 +183,7 @@ std::shared_ptr<CsgLeafNode> SimpleBoolean(const Manifold::Impl& a,
170183
dump_lock.unlock();
171184
throw logicErr("self intersection detected");
172185
}
186+
if (ctx) ctx->doneBooleans.fetch_add(1, std::memory_order_relaxed);
173187
return ImplToLeaf(std::move(impl));
174188
} catch (logicErr& err) {
175189
dump();
@@ -179,7 +193,9 @@ std::shared_ptr<CsgLeafNode> SimpleBoolean(const Manifold::Impl& a,
179193
throw err;
180194
}
181195
#else
182-
return ImplToLeaf(Boolean3(a, b, op).Result(op));
196+
auto leaf = ImplToLeaf(Boolean3(a, b, op).Result(op));
197+
if (ctx) ctx->doneBooleans.fetch_add(1, std::memory_order_relaxed);
198+
return leaf;
183199
#endif
184200
}
185201

@@ -374,7 +390,8 @@ std::shared_ptr<CsgLeafNode> CsgLeafNode::Compose(
374390
* operation. Only supports union and intersection.
375391
*/
376392
std::shared_ptr<CsgLeafNode> BatchBoolean(
377-
OpType operation, std::vector<std::shared_ptr<CsgLeafNode>>& results) {
393+
OpType operation, std::vector<std::shared_ptr<CsgLeafNode>>& results,
394+
ExecutionContext::Impl* ctx) {
378395
ZoneScoped;
379396
DEBUG_ASSERT(operation != OpType::Subtract, logicErr,
380397
"BatchBoolean doesn't support Difference.");
@@ -383,7 +400,7 @@ std::shared_ptr<CsgLeafNode> BatchBoolean(
383400
if (results.size() == 1) return results.front();
384401
if (results.size() == 2)
385402
return SimpleBoolean(*results[0]->GetImpl(), *results[1]->GetImpl(),
386-
operation);
403+
operation, ctx);
387404
// apply boolean operations starting from smaller meshes
388405
// the assumption is that boolean operations on smaller meshes is faster,
389406
// due to less data being copied and processed
@@ -397,6 +414,8 @@ std::shared_ptr<CsgLeafNode> BatchBoolean(
397414
for (int i = 0; i < 4; i++) parallelTmp.push_back(nullptr);
398415
#endif
399416
while (results.size() > 1) {
417+
if (ctx && ctx->cancel.load(std::memory_order_relaxed))
418+
return ErrorLeaf(Manifold::Error::Cancelled);
400419
for (size_t i = 0; i < 4 && results.size() > 1; i++) {
401420
std::pop_heap(results.begin(), results.end(), cmpFn);
402421
auto a = std::move(results.back());
@@ -406,10 +425,11 @@ std::shared_ptr<CsgLeafNode> BatchBoolean(
406425
results.pop_back();
407426
#if MANIFOLD_PAR == 1
408427
group.run([&, i, a, b]() {
409-
parallelTmp[i] = SimpleBoolean(*a->GetImpl(), *b->GetImpl(), operation);
428+
parallelTmp[i] =
429+
SimpleBoolean(*a->GetImpl(), *b->GetImpl(), operation, ctx);
410430
});
411431
#else
412-
auto result = SimpleBoolean(*a->GetImpl(), *b->GetImpl(), operation);
432+
auto result = SimpleBoolean(*a->GetImpl(), *b->GetImpl(), operation, ctx);
413433
tmp.push_back(result);
414434
#endif
415435
}
@@ -432,7 +452,8 @@ std::shared_ptr<CsgLeafNode> BatchBoolean(
432452
* possible.
433453
*/
434454
std::shared_ptr<CsgLeafNode> BatchUnion(
435-
std::vector<std::shared_ptr<CsgLeafNode>>& children) {
455+
std::vector<std::shared_ptr<CsgLeafNode>>& children,
456+
ExecutionContext::Impl* ctx) {
436457
ZoneScoped;
437458
// INVARIANT: children_ is a vector of leaf nodes
438459
// this kMaxUnionSize is a heuristic to avoid the pairwise disjoint check
@@ -443,6 +464,8 @@ std::shared_ptr<CsgLeafNode> BatchUnion(
443464
DEBUG_ASSERT(!children.empty(), logicErr,
444465
"BatchUnion should not have empty children");
445466
while (children.size() > 1) {
467+
if (ctx && ctx->cancel.load(std::memory_order_relaxed))
468+
return ErrorLeaf(Manifold::Error::Cancelled);
446469
const size_t start = (children.size() > kMaxUnionSize)
447470
? (children.size() - kMaxUnionSize)
448471
: 0;
@@ -478,11 +501,16 @@ std::shared_ptr<CsgLeafNode> BatchUnion(
478501
tmp.push_back(children[start + j]);
479502
}
480503
impls.push_back(CsgLeafNode::Compose(tmp));
504+
// Compose absorbs set.size() leaves into 1 leaf, which is
505+
// set.size() - 1 leaf-reductions toward the final result.
506+
if (ctx)
507+
ctx->doneBooleans.fetch_add(static_cast<int>(set.size() - 1),
508+
std::memory_order_relaxed);
481509
}
482510
}
483511

484512
children.erase(children.begin() + start, children.end());
485-
children.push_back(BatchBoolean(OpType::Add, impls));
513+
children.push_back(BatchBoolean(OpType::Add, impls, ctx));
486514
// move it to the front as we process from the back, and the newly added
487515
// child should be quite complicated
488516
std::swap(children.front(), children.back());
@@ -563,7 +591,8 @@ struct CsgStackFrame {
563591
op_node(op_node) {}
564592
};
565593

566-
std::shared_ptr<CsgLeafNode> CsgOpNode::ToLeafNode() const {
594+
std::shared_ptr<CsgLeafNode> CsgOpNode::ToLeafNode(
595+
ExecutionContext::Impl* ctx) const {
567596
ZoneScoped;
568597
if (cache_ != nullptr) return cache_;
569598

@@ -666,31 +695,37 @@ std::shared_ptr<CsgLeafNode> CsgOpNode::ToLeafNode() const {
666695
// destination->push_back(node->cache_->Transform(transform));
667696
// }
668697
while (!stack.empty()) {
698+
if (ctx && ctx->cancel.load(std::memory_order_relaxed)) {
699+
cache_ = ErrorLeaf(Manifold::Error::Cancelled);
700+
return cache_;
701+
}
669702
std::shared_ptr<CsgStackFrame> frame = stack.back();
670703
auto impl = frame->op_node->impl_.GetGuard();
671704
if (frame->finalize) {
672705
if (!frame->op_node->cache_) {
673706
switch (frame->op_node->op_) {
674707
case OpType::Add:
675-
*impl = {BatchUnion(frame->positive_children)};
708+
*impl = {BatchUnion(frame->positive_children, ctx)};
676709
break;
677710
case OpType::Intersect: {
678-
*impl = {BatchBoolean(OpType::Intersect, frame->positive_children)};
711+
*impl = {
712+
BatchBoolean(OpType::Intersect, frame->positive_children, ctx)};
679713
break;
680714
};
681715
case OpType::Subtract:
682716
if (frame->positive_children.empty()) {
683717
// nothing to subtract from, so the result is empty.
684718
*impl = {std::make_shared<CsgLeafNode>()};
685719
} else {
686-
auto positive = BatchUnion(frame->positive_children);
720+
auto positive = BatchUnion(frame->positive_children, ctx);
687721
if (frame->negative_children.empty()) {
688722
// nothing to subtract, result equal to the LHS.
689723
*impl = {frame->positive_children[0]};
690724
} else {
691-
auto negative = BatchUnion(frame->negative_children);
725+
auto negative = BatchUnion(frame->negative_children, ctx);
692726
*impl = {SimpleBoolean(*positive->GetImpl(),
693-
*negative->GetImpl(), OpType::Subtract)};
727+
*negative->GetImpl(), OpType::Subtract,
728+
ctx)};
694729
}
695730
}
696731
break;
@@ -761,4 +796,14 @@ CsgNodeType CsgOpNode::GetNodeType() const {
761796
return CsgNodeType::Leaf;
762797
}
763798

799+
size_t CsgOpNode::NumLeaves() const {
800+
// An already-evaluated CsgOpNode counts as a single leaf for the purposes
801+
// of estimating remaining boolean work.
802+
if (cache_ != nullptr) return 1;
803+
auto impl = impl_.GetGuard();
804+
size_t total = 0;
805+
for (const auto& child : *impl) total += child->NumLeaves();
806+
return total;
807+
}
808+
764809
} // namespace manifold

0 commit comments

Comments
 (0)