Within-phase cancel via Cancellable<F> wrapper (#1674)
* Cancellable primitive: IsCancelled boundary + ctx-aware for_each Privatize ExecutionContext::Impl::cancel; IsCancelled(ctx) is the canonical reader, friended back. parallel.h gains for_each / for_each_n overloads that check IsCancelled(ctx) once per chunk (parallel) or once at start (sequential). nullptr ctx folds out at the call site, so non-cancellable callers cost nothing. No callers of the new overloads yet. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Thread ctx through CSG; post-loop IsCancelled guards; tests Convert cancellable parallel loops in boolean / sort / edge_op to the ctx-aware overloads, pairing each with a post-loop IsCancelled early return so partial output can't feed unconditional downstream consumers. Add invariant docs on Boolean3 / Intersect12_ for the contract the guards point at. Tests cover within-phase cancel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * parallel.h: periodic IsCancelled check on the seq for_each branch Replace `std::for_each` on the sequential branch with a manual loop that calls `IsCancelled(ctx)` every kSeqCancelChunk = 1024 elements, so cancel latency is bounded under MANIFOLD_PAR=OFF and on the small- input branch under PAR=ON. nullptr ctx still falls through to plain `std::for_each` — non-cancellable callers pay nothing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
e73c0781ac
commit
3ce9622b85
+38
-26
@@ -353,9 +353,11 @@ struct Kernel12Recorder {
|
||||
};
|
||||
|
||||
template <bool expandP, bool forward>
|
||||
Intersections Intersect12_(const Manifold::Impl& inP,
|
||||
const Manifold::Impl& inQ) {
|
||||
Intersections Intersect12_(const Manifold::Impl& inP, const Manifold::Impl& inQ,
|
||||
ExecutionContext::Impl* ctx) {
|
||||
ZoneScoped;
|
||||
// Invariant: every ctx-passing parallel op is followed by IsCancelled to
|
||||
// keep partial output from feeding unconditional downstream consumers.
|
||||
// a: 1 (edge), b: 2 (face)
|
||||
const Manifold::Impl& a = forward ? inP : inQ;
|
||||
const Manifold::Impl& b = forward ? inQ : inP;
|
||||
@@ -371,7 +373,8 @@ Intersections Intersect12_(const Manifold::Impl& inP,
|
||||
a.vertPos_[a.halfedge_[i].endVert])
|
||||
: Box();
|
||||
};
|
||||
b.collider_.Collisions<false>(recorder, f, a.halfedge_.size());
|
||||
b.collider_.Collisions<false>(recorder, f, a.halfedge_.size(), true, ctx);
|
||||
if (IsCancelled(ctx)) return Intersections{};
|
||||
|
||||
Intersections result = recorder.get();
|
||||
auto& p1q2 = result.p1q2;
|
||||
@@ -393,16 +396,17 @@ Intersections Intersect12_(const Manifold::Impl& inP,
|
||||
|
||||
template <bool forward>
|
||||
Intersections Intersect12(const Manifold::Impl& inP, const Manifold::Impl& inQ,
|
||||
bool expandP) {
|
||||
bool expandP, ExecutionContext::Impl* ctx) {
|
||||
if (expandP)
|
||||
return Intersect12_<true, forward>(inP, inQ);
|
||||
return Intersect12_<true, forward>(inP, inQ, ctx);
|
||||
else
|
||||
return Intersect12_<false, forward>(inP, inQ);
|
||||
return Intersect12_<false, forward>(inP, inQ, ctx);
|
||||
}
|
||||
|
||||
template <bool expandP, bool forward>
|
||||
Vec<int> Winding03_(const Manifold::Impl& inP, const Manifold::Impl& inQ,
|
||||
const VecView<std::array<int, 2>> p1q2) {
|
||||
const VecView<std::array<int, 2>> p1q2,
|
||||
ExecutionContext::Impl* ctx) {
|
||||
ZoneScoped;
|
||||
// a: 0 (vert), b: 2 (face)
|
||||
const Manifold::Impl& a = forward ? inP : inQ;
|
||||
@@ -410,9 +414,11 @@ Vec<int> Winding03_(const Manifold::Impl& inP, const Manifold::Impl& inQ,
|
||||
Vec<int> brokenHalfedges;
|
||||
int index = forward ? 0 : 1;
|
||||
|
||||
// Invariant: every ctx-passing parallel op is followed by IsCancelled to
|
||||
// keep partial output from feeding unconditional downstream consumers.
|
||||
DisjointSets uA(a.vertPos_.size());
|
||||
for_each(autoPolicy(a.halfedge_.size()), countAt(0),
|
||||
countAt(a.halfedge_.size()), [&](int edge) {
|
||||
countAt(a.halfedge_.size()), ctx, [&](int edge) {
|
||||
const Halfedge& he = a.halfedge_[edge];
|
||||
if (!he.IsForward()) return;
|
||||
// check if the edge is broken
|
||||
@@ -424,6 +430,7 @@ Vec<int> Winding03_(const Manifold::Impl& inP, const Manifold::Impl& inQ,
|
||||
if (it == p1q2.end() || (*it)[index] != edge)
|
||||
uA.unite(he.startVert, he.endVert);
|
||||
});
|
||||
if (IsCancelled(ctx)) return Vec<int>{};
|
||||
|
||||
// find components, the hope is the number of components should be small
|
||||
std::unordered_set<int> components;
|
||||
@@ -431,7 +438,7 @@ Vec<int> Winding03_(const Manifold::Impl& inP, const Manifold::Impl& inQ,
|
||||
if (a.vertPos_.size() > 1e5) {
|
||||
tbb::combinable<std::unordered_set<int>> componentsShared;
|
||||
for_each(autoPolicy(a.vertPos_.size()), countAt(0),
|
||||
countAt(a.vertPos_.size()),
|
||||
countAt(a.vertPos_.size()), ctx,
|
||||
[&](int v) { componentsShared.local().insert(uA.find(v)); });
|
||||
componentsShared.combine_each([&](const std::unordered_set<int>& data) {
|
||||
components.insert(data.begin(), data.end());
|
||||
@@ -442,6 +449,7 @@ Vec<int> Winding03_(const Manifold::Impl& inP, const Manifold::Impl& inQ,
|
||||
for (size_t v = 0; v < a.vertPos_.size(); v++)
|
||||
components.insert(uA.find(v));
|
||||
}
|
||||
if (IsCancelled(ctx)) return Vec<int>{};
|
||||
Vec<int> verts;
|
||||
verts.reserve(components.size());
|
||||
for (int c : components) verts.push_back(c);
|
||||
@@ -456,24 +464,27 @@ Vec<int> Winding03_(const Manifold::Impl& inP, const Manifold::Impl& inQ,
|
||||
};
|
||||
auto recorder = MakeSimpleRecorder(recorderf);
|
||||
auto f = [&](int i) { return a.vertPos_[verts[i]]; };
|
||||
b.collider_.Collisions<false>(recorder, f, verts.size());
|
||||
b.collider_.Collisions<false>(recorder, f, verts.size(), true, ctx);
|
||||
if (IsCancelled(ctx)) return Vec<int>{};
|
||||
// flood fill
|
||||
for_each(autoPolicy(w03.size()), countAt(0), countAt(w03.size()),
|
||||
for_each(autoPolicy(w03.size()), countAt(0), countAt(w03.size()), ctx,
|
||||
[&](size_t i) {
|
||||
size_t root = uA.find(i);
|
||||
if (root == i) return;
|
||||
w03[i] = w03[root];
|
||||
});
|
||||
if (IsCancelled(ctx)) return Vec<int>{};
|
||||
return w03;
|
||||
}
|
||||
|
||||
template <bool forward>
|
||||
Vec<int> Winding03(const Manifold::Impl& inP, const Manifold::Impl& inQ,
|
||||
const VecView<std::array<int, 2>> p1q2, bool expandP) {
|
||||
const VecView<std::array<int, 2>> p1q2, bool expandP,
|
||||
ExecutionContext::Impl* ctx) {
|
||||
if (expandP)
|
||||
return Winding03_<true, forward>(inP, inQ, p1q2);
|
||||
return Winding03_<true, forward>(inP, inQ, p1q2, ctx);
|
||||
else
|
||||
return Winding03_<false, forward>(inP, inQ, p1q2);
|
||||
return Winding03_<false, forward>(inP, inQ, p1q2, ctx);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -498,10 +509,9 @@ Boolean3::Boolean3(const Manifold::Impl& inP, const Manifold::Impl& inQ,
|
||||
return;
|
||||
}
|
||||
|
||||
auto cancelled = [&] {
|
||||
return ctx_ && ctx_->cancel.load(std::memory_order_relaxed);
|
||||
};
|
||||
|
||||
// Phase-boundary fast-path: skip launching the next phase if cancel fired
|
||||
// between phases. The per-phase invariant is enforced inside the called
|
||||
// functions (Intersect12_, Winding03_).
|
||||
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
|
||||
Timer intersections;
|
||||
intersections.Start();
|
||||
@@ -513,14 +523,14 @@ Boolean3::Boolean3(const Manifold::Impl& inP, const Manifold::Impl& inQ,
|
||||
// Build up the intersection of the edges and triangles, keeping only those
|
||||
// that intersect, and record the direction the edge is passing through the
|
||||
// triangle.
|
||||
if (cancelled()) return;
|
||||
xv12_ = Intersect12<true>(inP, inQ, expandP_);
|
||||
if (IsCancelled(ctx_)) return;
|
||||
xv12_ = Intersect12<true>(inP, inQ, expandP_, ctx_);
|
||||
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
|
||||
intersect12P.Stop();
|
||||
intersect12Q.Start();
|
||||
#endif
|
||||
if (cancelled()) return;
|
||||
xv21_ = Intersect12<false>(inP, inQ, expandP_);
|
||||
if (IsCancelled(ctx_)) return;
|
||||
xv21_ = Intersect12<false>(inP, inQ, expandP_, ctx_);
|
||||
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
|
||||
intersect12Q.Stop();
|
||||
#endif
|
||||
@@ -535,14 +545,16 @@ Boolean3::Boolean3(const Manifold::Impl& inP, const Manifold::Impl& inQ,
|
||||
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
|
||||
winding03P.Start();
|
||||
#endif
|
||||
if (cancelled()) return;
|
||||
w03_ = Winding03<true>(inP, inQ, xv12_.p1q2, expandP_);
|
||||
if (IsCancelled(ctx_)) return;
|
||||
w03_ = Winding03<true>(inP, inQ, xv12_.p1q2, expandP_, ctx_);
|
||||
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
|
||||
winding03P.Stop();
|
||||
winding03Q.Start();
|
||||
#endif
|
||||
if (cancelled()) return;
|
||||
w30_ = Winding03<false>(inP, inQ, xv21_.p1q2, expandP_);
|
||||
if (IsCancelled(ctx_)) return;
|
||||
w30_ = Winding03<false>(inP, inQ, xv21_.p1q2, expandP_, ctx_);
|
||||
// No trailing check: Winding03_ already returns empty on cancel and
|
||||
// Boolean3::Result re-checks on entry.
|
||||
|
||||
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
|
||||
winding03Q.Stop();
|
||||
|
||||
+57
-39
@@ -107,8 +107,11 @@ std::tuple<Vec<int>, Vec<int>> SizeOutput(
|
||||
Manifold::Impl& outR, const Manifold::Impl& inP, const Manifold::Impl& inQ,
|
||||
const Vec<int>& i03, const Vec<int>& i30, const Vec<int>& i12,
|
||||
const Vec<int>& i21, const Vec<std::array<int, 2>>& p1q2,
|
||||
const Vec<std::array<int, 2>>& p2q1, bool invertQ) {
|
||||
const Vec<std::array<int, 2>>& p2q1, bool invertQ,
|
||||
ExecutionContext::Impl* ctx) {
|
||||
ZoneScoped;
|
||||
// Invariant: every ctx-passing parallel op is followed by IsCancelled to
|
||||
// keep partial output from feeding unconditional downstream consumers.
|
||||
Vec<int> sidesPerFacePQ(inP.NumTri() + inQ.NumTri(), 0);
|
||||
// note: numFaceR <= facePQ2R.size() = sidesPerFacePQ.size() + 1
|
||||
|
||||
@@ -116,26 +119,27 @@ std::tuple<Vec<int>, Vec<int>> SizeOutput(
|
||||
auto sidesPerFaceQ = sidesPerFacePQ.view(inP.NumTri(), inQ.NumTri());
|
||||
|
||||
auto policy = autoPolicy(inP.halfedge_.size());
|
||||
for_each(policy, countAt(0_uz), countAt(inP.halfedge_.size() / 3),
|
||||
for_each(policy, countAt(0_uz), countAt(inP.halfedge_.size() / 3), ctx,
|
||||
CountVerts({inP.halfedge_, sidesPerFaceP, i03}));
|
||||
for_each(policy, countAt(0_uz), countAt(inQ.halfedge_.size() / 3),
|
||||
for_each(policy, countAt(0_uz), countAt(inQ.halfedge_.size() / 3), ctx,
|
||||
CountVerts({inQ.halfedge_, sidesPerFaceQ, i30}));
|
||||
|
||||
if (i12.size() >= 1e5) {
|
||||
for_each_n(ExecutionPolicy::Par, countAt(0), i12.size(),
|
||||
for_each_n(ExecutionPolicy::Par, countAt(0), i12.size(), ctx,
|
||||
CountNewVerts<false, true>(
|
||||
{sidesPerFaceP, sidesPerFaceQ, i12, p1q2, inP.halfedge_}));
|
||||
for_each_n(ExecutionPolicy::Par, countAt(0), i21.size(),
|
||||
for_each_n(ExecutionPolicy::Par, countAt(0), i21.size(), ctx,
|
||||
CountNewVerts<true, true>(
|
||||
{sidesPerFaceQ, sidesPerFaceP, i21, p2q1, inQ.halfedge_}));
|
||||
} else {
|
||||
for_each_n(ExecutionPolicy::Seq, countAt(0), i12.size(),
|
||||
for_each_n(ExecutionPolicy::Seq, countAt(0), i12.size(), ctx,
|
||||
CountNewVerts<false, false>(
|
||||
{sidesPerFaceP, sidesPerFaceQ, i12, p1q2, inP.halfedge_}));
|
||||
for_each_n(ExecutionPolicy::Seq, countAt(0), i21.size(),
|
||||
for_each_n(ExecutionPolicy::Seq, countAt(0), i21.size(), ctx,
|
||||
CountNewVerts<true, false>(
|
||||
{sidesPerFaceQ, sidesPerFaceP, i21, p2q1, inQ.halfedge_}));
|
||||
}
|
||||
if (IsCancelled(ctx)) return std::make_tuple(Vec<int>{}, Vec<int>{});
|
||||
|
||||
Vec<int> facePQ2R(inP.NumTri() + inQ.NumTri() + 1, 0);
|
||||
auto keepFace = TransformIterator(sidesPerFacePQ.begin(),
|
||||
@@ -206,8 +210,10 @@ void AddNewEdgeVerts(
|
||||
concurrent_map<std::pair<int, int>, std::vector<EdgePos>>& edgesNew,
|
||||
const Vec<std::array<int, 2>>& p1q2, const Vec<int>& i12,
|
||||
const Vec<int>& v12R, const VecView<Halfedge>& halfedgeP, bool forward,
|
||||
size_t offset) {
|
||||
size_t offset, ExecutionContext::Impl* ctx) {
|
||||
ZoneScoped;
|
||||
// Invariant: every ctx-passing parallel op is followed by IsCancelled to
|
||||
// keep partial output from feeding unconditional downstream consumers.
|
||||
// For each edge of P that intersects a face of Q (p1q2), add this vertex to
|
||||
// P's corresponding edge vector and to the two new edges, which are
|
||||
// intersections between the face of Q and the two faces of P attached to the
|
||||
@@ -261,12 +267,14 @@ void AddNewEdgeVerts(
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<size_t>(0_uz, p1q2.size(), 32),
|
||||
[&](const tbb::blocked_range<size_t>& range) {
|
||||
if (IsCancelled(ctx)) return;
|
||||
for (size_t i = range.begin(); i != range.end(); i++) processFun(i);
|
||||
},
|
||||
ap);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (IsCancelled(ctx)) return;
|
||||
auto processFun =
|
||||
std::bind(process, [](size_t) {}, [](size_t) {}, std::placeholders::_1);
|
||||
for (size_t i = 0; i < p1q2.size(); ++i) processFun(i);
|
||||
@@ -473,10 +481,12 @@ void AppendWholeEdges(Manifold::Impl& outR, Vec<int>& facePtrR,
|
||||
Vec<TriRef>& halfedgeRef, const Manifold::Impl& inP,
|
||||
const Vec<char> wholeHalfedgeP, const Vec<int>& i03,
|
||||
const Vec<int>& vP2R, VecView<const int> faceP2R,
|
||||
bool forward) {
|
||||
bool forward, ExecutionContext::Impl* ctx) {
|
||||
ZoneScoped;
|
||||
// Invariant: every ctx-passing parallel op is followed by IsCancelled to
|
||||
// keep partial output from feeding unconditional downstream consumers.
|
||||
for_each_n(
|
||||
autoPolicy(inP.halfedge_.size()), countAt(0), inP.halfedge_.size(),
|
||||
autoPolicy(inP.halfedge_.size()), countAt(0), inP.halfedge_.size(), ctx,
|
||||
DuplicateHalfedges({outR.halfedge_, halfedgeRef, facePtrR, wholeHalfedgeP,
|
||||
inP.halfedge_, i03, vP2R, faceP2R, forward}));
|
||||
}
|
||||
@@ -495,12 +505,16 @@ struct MapTriRef {
|
||||
};
|
||||
|
||||
void UpdateReference(Manifold::Impl& outR, const Manifold::Impl& inP,
|
||||
const Manifold::Impl& inQ, bool invertQ) {
|
||||
const Manifold::Impl& inQ, bool invertQ,
|
||||
ExecutionContext::Impl* ctx) {
|
||||
// Invariant: every ctx-passing parallel op is followed by IsCancelled to
|
||||
// keep partial output from feeding unconditional downstream consumers.
|
||||
const int offsetQ = Manifold::Impl::meshIDCounter_;
|
||||
for_each_n(
|
||||
autoPolicy(outR.NumTri(), 1e5), outR.meshRelation_.triRef.begin(),
|
||||
outR.NumTri(),
|
||||
outR.NumTri(), ctx,
|
||||
MapTriRef({inP.meshRelation_.triRef, inQ.meshRelation_.triRef, offsetQ}));
|
||||
if (IsCancelled(ctx)) return;
|
||||
|
||||
for (const auto& pair : inP.meshRelation_.meshIDtransform) {
|
||||
outR.meshRelation_.meshIDtransform[pair.first] = pair.second;
|
||||
@@ -544,8 +558,10 @@ struct Barycentric {
|
||||
};
|
||||
|
||||
void CreateProperties(Manifold::Impl& outR, const Manifold::Impl& inP,
|
||||
const Manifold::Impl& inQ) {
|
||||
const Manifold::Impl& inQ, ExecutionContext::Impl* ctx) {
|
||||
ZoneScoped;
|
||||
// Invariant: every ctx-passing parallel op is followed by IsCancelled to
|
||||
// keep partial output from feeding unconditional downstream consumers.
|
||||
const int numPropP = inP.NumProp();
|
||||
const int numPropQ = inQ.NumProp();
|
||||
const int numProp = std::max(numPropP, numPropQ);
|
||||
@@ -554,10 +570,11 @@ void CreateProperties(Manifold::Impl& outR, const Manifold::Impl& inP,
|
||||
|
||||
const int numTri = outR.NumTri();
|
||||
Vec<vec3> bary(outR.halfedge_.size());
|
||||
for_each_n(autoPolicy(numTri, 1e4), countAt(0), numTri,
|
||||
for_each_n(autoPolicy(numTri, 1e4), countAt(0), numTri, ctx,
|
||||
Barycentric({bary, outR.meshRelation_.triRef, inP.vertPos_,
|
||||
inQ.vertPos_, outR.vertPos_, inP.halfedge_,
|
||||
inQ.halfedge_, outR.halfedge_, outR.epsilon_}));
|
||||
if (IsCancelled(ctx)) return;
|
||||
|
||||
using Entry = std::pair<ivec3, int>;
|
||||
int idMissProp = outR.NumVert();
|
||||
@@ -687,7 +704,7 @@ Manifold::Impl Boolean3::Result(OpType op) const {
|
||||
}
|
||||
|
||||
auto cancelled = [&]() -> std::optional<Manifold::Impl> {
|
||||
if (ctx_ && ctx_->cancel.load(std::memory_order_relaxed)) {
|
||||
if (IsCancelled(ctx_)) {
|
||||
auto impl = Manifold::Impl();
|
||||
impl.status_ = Manifold::Error::Cancelled;
|
||||
return impl;
|
||||
@@ -695,10 +712,8 @@ Manifold::Impl Boolean3::Result(OpType op) const {
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
// Catches both ctor-detected cancel (ctx->cancel is sticky) and cancel
|
||||
// fired between ctor and Result. The assemble phase (through
|
||||
// AppendWholeEdges) is O(N) in intersection count and has no internal
|
||||
// cancel checkpoint until Face2Tri.
|
||||
// Invariant: every ctx-passing parallel op is followed by IsCancelled to
|
||||
// keep partial output from feeding unconditional downstream consumers.
|
||||
if (auto c = cancelled()) return *c;
|
||||
|
||||
if (!valid) {
|
||||
@@ -761,15 +776,16 @@ Manifold::Impl Boolean3::Result(OpType op) const {
|
||||
outR.vertPos_.resize_nofill(numVertR);
|
||||
// Add vertices, duplicating for inclusion numbers not in [-1, 1].
|
||||
// Retained vertices from P and Q:
|
||||
for_each_n(autoPolicy(inP_.NumVert(), 1e4), countAt(0), inP_.NumVert(),
|
||||
for_each_n(autoPolicy(inP_.NumVert(), 1e4), countAt(0), inP_.NumVert(), ctx_,
|
||||
DuplicateVerts({outR.vertPos_, i03, vP2R, inP_.vertPos_}));
|
||||
for_each_n(autoPolicy(inQ_.NumVert(), 1e4), countAt(0), inQ_.NumVert(),
|
||||
for_each_n(autoPolicy(inQ_.NumVert(), 1e4), countAt(0), inQ_.NumVert(), ctx_,
|
||||
DuplicateVerts({outR.vertPos_, i30, vQ2R, inQ_.vertPos_}));
|
||||
// New vertices created from intersections:
|
||||
for_each_n(autoPolicy(i12.size(), 1e4), countAt(0), i12.size(),
|
||||
for_each_n(autoPolicy(i12.size(), 1e4), countAt(0), i12.size(), ctx_,
|
||||
DuplicateVerts({outR.vertPos_, i12, v12R, xv12_.v12}));
|
||||
for_each_n(autoPolicy(i21.size(), 1e4), countAt(0), i21.size(),
|
||||
for_each_n(autoPolicy(i21.size(), 1e4), countAt(0), i21.size(), ctx_,
|
||||
DuplicateVerts({outR.vertPos_, i21, v21R, xv21_.v12}));
|
||||
if (auto c = cancelled()) return *c;
|
||||
|
||||
PRINT(nPv << " verts from inP");
|
||||
PRINT(nQv << " verts from inQ");
|
||||
@@ -788,9 +804,10 @@ Manifold::Impl Boolean3::Result(OpType op) const {
|
||||
concurrent_map<std::pair<int, int>, std::vector<EdgePos>> edgesNew;
|
||||
|
||||
AddNewEdgeVerts(edgesP, edgesNew, xv12_.p1q2, i12, v12R, inP_.halfedge_, true,
|
||||
0);
|
||||
0, ctx_);
|
||||
AddNewEdgeVerts(edgesQ, edgesNew, xv21_.p1q2, i21, v21R, inQ_.halfedge_,
|
||||
false, xv12_.p1q2.size());
|
||||
false, xv12_.p1q2.size(), ctx_);
|
||||
if (auto c = cancelled()) return *c;
|
||||
|
||||
v12R.clear();
|
||||
v21R.clear();
|
||||
@@ -798,8 +815,10 @@ Manifold::Impl Boolean3::Result(OpType op) const {
|
||||
// Level 4
|
||||
Vec<int> faceEdge;
|
||||
Vec<int> facePQ2R;
|
||||
std::tie(faceEdge, facePQ2R) = SizeOutput(
|
||||
outR, inP_, inQ_, i03, i30, i12, i21, xv12_.p1q2, xv21_.p1q2, invertQ);
|
||||
std::tie(faceEdge, facePQ2R) =
|
||||
SizeOutput(outR, inP_, inQ_, i03, i30, i12, i21, xv12_.p1q2, xv21_.p1q2,
|
||||
invertQ, ctx_);
|
||||
if (auto c = cancelled()) return *c;
|
||||
|
||||
i12.clear();
|
||||
i21.clear();
|
||||
@@ -828,9 +847,10 @@ Manifold::Impl Boolean3::Result(OpType op) const {
|
||||
edgesNew.clear();
|
||||
|
||||
AppendWholeEdges(outR, facePtrR, halfedgeRef, inP_, wholeHalfedgeP, i03, vP2R,
|
||||
facePQ2R.cview(0, inP_.NumTri()), true);
|
||||
facePQ2R.cview(0, inP_.NumTri()), true, ctx_);
|
||||
AppendWholeEdges(outR, facePtrR, halfedgeRef, inQ_, wholeHalfedgeQ, i30, vQ2R,
|
||||
facePQ2R.cview(inP_.NumTri(), inQ_.NumTri()), false);
|
||||
facePQ2R.cview(inP_.NumTri(), inQ_.NumTri()), false, ctx_);
|
||||
if (auto c = cancelled()) return *c;
|
||||
|
||||
wholeHalfedgeP.clear();
|
||||
wholeHalfedgeQ.clear();
|
||||
@@ -847,14 +867,13 @@ Manifold::Impl Boolean3::Result(OpType op) const {
|
||||
triangulate.Start();
|
||||
#endif
|
||||
|
||||
if (auto c = cancelled()) return *c;
|
||||
|
||||
// Level 6
|
||||
outR.Face2Tri(faceEdge, halfedgeRef);
|
||||
halfedgeRef.clear();
|
||||
faceEdge.clear();
|
||||
|
||||
outR.ReorderHalfedges();
|
||||
outR.ReorderHalfedges(ctx_);
|
||||
if (auto c = cancelled()) return *c;
|
||||
|
||||
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
|
||||
triangulate.Stop();
|
||||
@@ -862,15 +881,15 @@ Manifold::Impl Boolean3::Result(OpType op) const {
|
||||
simplify.Start();
|
||||
#endif
|
||||
|
||||
if (auto c = cancelled()) return *c;
|
||||
|
||||
if (ManifoldParams().intermediateChecks)
|
||||
DEBUG_ASSERT(outR.IsManifold(), logicErr,
|
||||
"triangulated mesh is not manifold!");
|
||||
|
||||
CreateProperties(outR, inP_, inQ_);
|
||||
CreateProperties(outR, inP_, inQ_, ctx_);
|
||||
if (auto c = cancelled()) return *c;
|
||||
|
||||
UpdateReference(outR, inP_, inQ_, invertQ);
|
||||
UpdateReference(outR, inP_, inQ_, invertQ, ctx_);
|
||||
if (auto c = cancelled()) return *c;
|
||||
|
||||
outR.SimplifyTopology(nPv + nQv);
|
||||
outR.RemoveUnreferencedVerts();
|
||||
@@ -885,11 +904,10 @@ Manifold::Impl Boolean3::Result(OpType op) const {
|
||||
sort.Start();
|
||||
#endif
|
||||
|
||||
if (auto c = cancelled()) return *c;
|
||||
|
||||
outR.CalculateBBox();
|
||||
outR.SortGeometry();
|
||||
outR.SortGeometry(ctx_);
|
||||
outR.IncrementMeshIDs();
|
||||
if (auto c = cancelled()) return *c;
|
||||
|
||||
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
|
||||
sort.Stop();
|
||||
|
||||
+7
-4
@@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
#pragma once
|
||||
#include "execution_impl.h"
|
||||
#include "manifold/common.h"
|
||||
#include "parallel.h"
|
||||
#include "utils.h"
|
||||
@@ -312,13 +313,14 @@ class Collider {
|
||||
}
|
||||
|
||||
template <const bool selfCollision = false, typename F, typename Recorder>
|
||||
void Collisions(Recorder& recorder, F f, int n, bool parallel = true) const {
|
||||
void Collisions(Recorder& recorder, F f, int n, bool parallel = true,
|
||||
ExecutionContext::Impl* ctx = nullptr) const {
|
||||
ZoneScoped;
|
||||
using collider_internal::FindCollision;
|
||||
if (internalChildren_.empty()) return;
|
||||
for_each_n(parallel ? autoPolicy(n, collider_internal::kSequentialThreshold)
|
||||
: ExecutionPolicy::Seq,
|
||||
countAt(0), n,
|
||||
countAt(0), n, ctx,
|
||||
FindCollision<decltype(f), selfCollision, Recorder>{
|
||||
f, nodeBBox_, internalChildren_, recorder});
|
||||
}
|
||||
@@ -334,9 +336,10 @@ class Collider {
|
||||
// If thread local storage is not needed, use SimpleRecorder.
|
||||
template <const bool selfCollision = false, typename T, typename Recorder>
|
||||
void Collisions(Recorder& recorder, const VecView<const T>& queriesIn,
|
||||
bool parallel = true) const {
|
||||
bool parallel = true,
|
||||
ExecutionContext::Impl* ctx = nullptr) const {
|
||||
auto f = [queriesIn](const int i) { return queriesIn[i]; };
|
||||
Collisions<selfCollision>(recorder, f, queriesIn.size(), parallel);
|
||||
Collisions<selfCollision>(recorder, f, queriesIn.size(), parallel, ctx);
|
||||
}
|
||||
|
||||
static uint32_t MortonCode(vec3 position, Box bBox) {
|
||||
|
||||
+4
-7
@@ -157,8 +157,7 @@ std::shared_ptr<CsgLeafNode> ErrorLeaf(Manifold::Error err) {
|
||||
std::shared_ptr<CsgLeafNode> SimpleBoolean(const Manifold::Impl& a,
|
||||
const Manifold::Impl& b, OpType op,
|
||||
ExecutionContext::Impl* ctx) {
|
||||
if (ctx && ctx->cancel.load(std::memory_order_relaxed))
|
||||
return ErrorLeaf(Manifold::Error::Cancelled);
|
||||
if (IsCancelled(ctx)) return ErrorLeaf(Manifold::Error::Cancelled);
|
||||
#ifdef MANIFOLD_DEBUG
|
||||
auto dump = [&]() {
|
||||
dump_lock.lock();
|
||||
@@ -433,8 +432,7 @@ std::shared_ptr<CsgLeafNode> BatchBoolean(
|
||||
for (int i = 0; i < 4; i++) parallelSerial.push_back(0);
|
||||
#endif
|
||||
while (heapNodes.size() > 1) {
|
||||
if (ctx && ctx->cancel.load(std::memory_order_relaxed))
|
||||
return ErrorLeaf(Manifold::Error::Cancelled);
|
||||
if (IsCancelled(ctx)) return ErrorLeaf(Manifold::Error::Cancelled);
|
||||
for (size_t i = 0; i < 4 && heapNodes.size() > 1; i++) {
|
||||
std::pop_heap(heapNodes.begin(), heapNodes.end(), cmpFn);
|
||||
auto a = std::move(heapNodes.back());
|
||||
@@ -485,8 +483,7 @@ std::shared_ptr<CsgLeafNode> BatchUnion(
|
||||
DEBUG_ASSERT(!children.empty(), logicErr,
|
||||
"BatchUnion should not have empty children");
|
||||
while (children.size() > 1) {
|
||||
if (ctx && ctx->cancel.load(std::memory_order_relaxed))
|
||||
return ErrorLeaf(Manifold::Error::Cancelled);
|
||||
if (IsCancelled(ctx)) return ErrorLeaf(Manifold::Error::Cancelled);
|
||||
const size_t start = (children.size() > kMaxUnionSize)
|
||||
? (children.size() - kMaxUnionSize)
|
||||
: 0;
|
||||
@@ -719,7 +716,7 @@ std::shared_ptr<CsgLeafNode> CsgOpNode::ToLeafNode(
|
||||
// destination->push_back(node->cache_->Transform(transform));
|
||||
// }
|
||||
while (!stack.empty()) {
|
||||
if (ctx && ctx->cancel.load(std::memory_order_relaxed)) {
|
||||
if (IsCancelled(ctx)) {
|
||||
// Poison every op_node currently on the stack, not just `this`.
|
||||
// Sub-ops may have had their impl_ partially mutated during finalize
|
||||
// (children replaced with an intermediate result); leaving cache_
|
||||
|
||||
@@ -29,9 +29,7 @@ void ExecutionContext::Cancel() {
|
||||
impl_->cancel.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool ExecutionContext::Cancelled() const {
|
||||
return impl_->cancel.load(std::memory_order_relaxed);
|
||||
}
|
||||
bool ExecutionContext::Cancelled() const { return IsCancelled(impl_.get()); }
|
||||
|
||||
double ExecutionContext::Progress() const {
|
||||
const int total = impl_->totalBooleans.load(std::memory_order_relaxed);
|
||||
|
||||
+27
-3
@@ -19,16 +19,40 @@
|
||||
|
||||
namespace manifold {
|
||||
|
||||
inline bool IsCancelled(ExecutionContext::Impl* ctx);
|
||||
|
||||
/** @ingroup Private
|
||||
*
|
||||
* Pimpl for ExecutionContext. Holds the atomic state observed/mutated by
|
||||
* the CSG evaluation machinery. Internal code accesses this via
|
||||
* ctx.impl_->field directly.
|
||||
* Pimpl for ExecutionContext. `cancel` is private; use `IsCancelled(ctx)`
|
||||
* to read it -- this is the canonical reader, enforced by the type system.
|
||||
* `totalBooleans` and `doneBooleans` are public for progress reporting and
|
||||
* test introspection.
|
||||
*/
|
||||
struct ExecutionContext::Impl {
|
||||
public:
|
||||
std::atomic<int> totalBooleans{0};
|
||||
std::atomic<int> doneBooleans{0};
|
||||
|
||||
private:
|
||||
std::atomic<bool> cancel{false};
|
||||
|
||||
friend bool IsCancelled(Impl*);
|
||||
friend class manifold::ExecutionContext;
|
||||
};
|
||||
|
||||
/** @ingroup Private
|
||||
*
|
||||
* Canonical reader for the cancel flag. The only path to observe cancel
|
||||
* state from internal code -- `Impl::cancel` is private and friended only
|
||||
* to this function and `ExecutionContext` (for its public `Cancel`/`Cancelled`
|
||||
* members). The ctx-aware overloads in `parallel.h` go through here.
|
||||
*
|
||||
* Returns false if `ctx` is nullptr (no-cancellation calls), otherwise
|
||||
* loads `cancel` with `memory_order_relaxed` (cancel is advisory; we
|
||||
* don't need synchronization with other operations).
|
||||
*/
|
||||
inline bool IsCancelled(ExecutionContext::Impl* ctx) {
|
||||
return ctx && ctx->cancel.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
} // namespace manifold
|
||||
|
||||
+14
-9
@@ -123,15 +123,20 @@ struct Manifold::Impl {
|
||||
std::vector<RayHit> RayCast(vec3 origin, vec3 endpoint) const;
|
||||
|
||||
// sort.cpp
|
||||
void SortGeometry();
|
||||
void SortVerts();
|
||||
void ReindexVerts(const Vec<int>& vertNew2Old, size_t numOldVert);
|
||||
void CompactProps();
|
||||
void GetFaceBoxMorton(Vec<Box>& faceBox, Vec<uint32_t>& faceMorton) const;
|
||||
void SortFaces(Vec<Box>& faceBox, Vec<uint32_t>& faceMorton);
|
||||
void GatherFaces(const Vec<int>& faceNew2Old);
|
||||
void GatherFaces(const Impl& old, const Vec<int>& faceNew2Old);
|
||||
void ReorderHalfedges();
|
||||
void SortGeometry(ExecutionContext::Impl* ctx = nullptr);
|
||||
void SortVerts(ExecutionContext::Impl* ctx = nullptr);
|
||||
void ReindexVerts(const Vec<int>& vertNew2Old, size_t numOldVert,
|
||||
ExecutionContext::Impl* ctx = nullptr);
|
||||
void CompactProps(ExecutionContext::Impl* ctx = nullptr);
|
||||
void GetFaceBoxMorton(Vec<Box>& faceBox, Vec<uint32_t>& faceMorton,
|
||||
ExecutionContext::Impl* ctx = nullptr) const;
|
||||
void SortFaces(Vec<Box>& faceBox, Vec<uint32_t>& faceMorton,
|
||||
ExecutionContext::Impl* ctx = nullptr);
|
||||
void GatherFaces(const Vec<int>& faceNew2Old,
|
||||
ExecutionContext::Impl* ctx = nullptr);
|
||||
void GatherFaces(const Impl& old, const Vec<int>& faceNew2Old,
|
||||
ExecutionContext::Impl* ctx = nullptr);
|
||||
void ReorderHalfedges(ExecutionContext::Impl* ctx = nullptr);
|
||||
|
||||
// face_op.cpp
|
||||
void Face2Tri(const Vec<int>& faceEdge, const Vec<TriRef>& halfedgeRef,
|
||||
|
||||
+47
-7
@@ -17,6 +17,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "execution_impl.h"
|
||||
#include "iters.h"
|
||||
#if (MANIFOLD_PAR == 1)
|
||||
#include <tbb/combinable.h>
|
||||
@@ -387,9 +388,18 @@ struct SortFunctor<
|
||||
|
||||
#endif
|
||||
|
||||
// Applies the function `f` to each element in the range `[first, last)`
|
||||
// Applies the function `f` to each element in the range `[first, last)`,
|
||||
// optionally checking `ctx` for cancellation periodically. `ctx` may be
|
||||
// nullptr — the cancel branch is null-short-circuited and folds out at
|
||||
// the call site, so non-cancellable callers pay nothing.
|
||||
//
|
||||
// Cancel granularity: once per parallel chunk, and once per kSeqCancelChunk
|
||||
// elements on the sequential branch. Only safe when "skip the rest of the
|
||||
// range" produces a result the caller will discard via a post-loop
|
||||
// `IsCancelled` check.
|
||||
template <typename Iter, typename F>
|
||||
void for_each(ExecutionPolicy policy, Iter first, Iter last, F f) {
|
||||
void for_each(ExecutionPolicy policy, Iter first, Iter last,
|
||||
ExecutionContext::Impl* ctx, F f) {
|
||||
static_assert(std::is_convertible_v<
|
||||
typename std::iterator_traits<Iter>::iterator_category,
|
||||
std::random_access_iterator_tag>,
|
||||
@@ -399,7 +409,8 @@ void for_each(ExecutionPolicy policy, Iter first, Iter last, F f) {
|
||||
if (policy == ExecutionPolicy::Par) {
|
||||
tbb::this_task_arena::isolate([&]() {
|
||||
tbb::parallel_for(tbb::blocked_range<Iter>(first, last),
|
||||
[&f](const tbb::blocked_range<Iter>& range) {
|
||||
[&f, ctx](const tbb::blocked_range<Iter>& range) {
|
||||
if (IsCancelled(ctx)) return;
|
||||
for (Iter i = range.begin(); i != range.end(); i++)
|
||||
f(*i);
|
||||
});
|
||||
@@ -407,17 +418,46 @@ void for_each(ExecutionPolicy policy, Iter first, Iter last, F f) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
std::for_each(first, last, f);
|
||||
// Sequential branch: check once at start, then every kSeqCancelChunk
|
||||
// elements. Bounded latency for MANIFOLD_PAR=OFF builds and for the
|
||||
// small-input branch under PAR=ON.
|
||||
constexpr size_t kSeqCancelChunk = 1024;
|
||||
if (IsCancelled(ctx)) return;
|
||||
if (ctx == nullptr) {
|
||||
std::for_each(first, last, f);
|
||||
return;
|
||||
}
|
||||
size_t since_check = 0;
|
||||
for (Iter i = first; i != last; ++i) {
|
||||
if (++since_check == kSeqCancelChunk) {
|
||||
if (IsCancelled(ctx)) return;
|
||||
since_check = 0;
|
||||
}
|
||||
f(*i);
|
||||
}
|
||||
}
|
||||
|
||||
// Applies the function `f` to each element in the range `[first, last)`
|
||||
// Non-cancellable shim. Threads `nullptr` through the ctx-aware impl.
|
||||
template <typename Iter, typename F>
|
||||
void for_each_n(ExecutionPolicy policy, Iter first, size_t n, F f) {
|
||||
void for_each(ExecutionPolicy policy, Iter first, Iter last, F f) {
|
||||
for_each(policy, first, last, nullptr, f);
|
||||
}
|
||||
|
||||
// for_each over [first, first + n).
|
||||
template <typename Iter, typename F>
|
||||
void for_each_n(ExecutionPolicy policy, Iter first, size_t n,
|
||||
ExecutionContext::Impl* ctx, F f) {
|
||||
static_assert(std::is_convertible_v<
|
||||
typename std::iterator_traits<Iter>::iterator_category,
|
||||
std::random_access_iterator_tag>,
|
||||
"You can only parallelize RandomAccessIterator.");
|
||||
for_each(policy, first, first + n, f);
|
||||
for_each(policy, first, first + n, ctx, f);
|
||||
}
|
||||
|
||||
// Non-cancellable shim.
|
||||
template <typename Iter, typename F>
|
||||
void for_each_n(ExecutionPolicy policy, Iter first, size_t n, F f) {
|
||||
for_each_n(policy, first, n, nullptr, f);
|
||||
}
|
||||
|
||||
// Reduce the range `[first, last)` using a binary operation `f` with an initial
|
||||
|
||||
+72
-30
@@ -217,25 +217,32 @@ namespace manifold {
|
||||
* rest of the internal data structures. This function also removes the verts
|
||||
* and halfedges flagged for removal (NaN verts and -1 halfedges).
|
||||
*/
|
||||
void Manifold::Impl::SortGeometry() {
|
||||
void Manifold::Impl::SortGeometry(ExecutionContext::Impl* ctx) {
|
||||
if (halfedge_.size() == 0) {
|
||||
collider_ = {};
|
||||
return;
|
||||
}
|
||||
|
||||
// Invariant: every ctx-passing parallel op is followed by IsCancelled to
|
||||
// keep partial output from feeding unconditional downstream consumers.
|
||||
|
||||
halfedge_.MakeUnique();
|
||||
SortVerts();
|
||||
SortVerts(ctx);
|
||||
if (IsCancelled(ctx)) return;
|
||||
Vec<Box> faceBox;
|
||||
Vec<uint32_t> faceMorton;
|
||||
GetFaceBoxMorton(faceBox, faceMorton);
|
||||
SortFaces(faceBox, faceMorton);
|
||||
GetFaceBoxMorton(faceBox, faceMorton, ctx);
|
||||
if (IsCancelled(ctx)) return;
|
||||
SortFaces(faceBox, faceMorton, ctx);
|
||||
if (IsCancelled(ctx)) return;
|
||||
if (halfedge_.size() == 0) {
|
||||
collider_ = {};
|
||||
return;
|
||||
}
|
||||
collider_ = Collider(faceBox, faceMorton);
|
||||
bBox_ = collider_.GetBoundingBox();
|
||||
CompactProps();
|
||||
CompactProps(ctx);
|
||||
if (IsCancelled(ctx)) return;
|
||||
|
||||
DEBUG_ASSERT(halfedge_.size() % 6 == 0, topologyErr,
|
||||
"Not an even number of faces after sorting faces!");
|
||||
@@ -280,14 +287,18 @@ void Manifold::Impl::SortGeometry() {
|
||||
/**
|
||||
* Sorts the vertices according to their Morton code.
|
||||
*/
|
||||
void Manifold::Impl::SortVerts() {
|
||||
void Manifold::Impl::SortVerts(ExecutionContext::Impl* ctx) {
|
||||
ZoneScoped;
|
||||
// Invariant: every ctx-passing parallel op is followed by IsCancelled to
|
||||
// keep partial output from feeding unconditional downstream consumers.
|
||||
const auto numVert = NumVert();
|
||||
Vec<uint32_t> vertMorton(numVert);
|
||||
auto policy = autoPolicy(numVert, 1e5);
|
||||
for_each_n(policy, countAt(0), numVert, [this, &vertMorton](const int vert) {
|
||||
vertMorton[vert] = MortonCode(vertPos_[vert], bBox_);
|
||||
});
|
||||
for_each_n(policy, countAt(0), numVert, ctx,
|
||||
[this, &vertMorton](const int vert) {
|
||||
vertMorton[vert] = MortonCode(vertPos_[vert], bBox_);
|
||||
});
|
||||
if (IsCancelled(ctx)) return;
|
||||
|
||||
Vec<int> vertNew2Old(numVert);
|
||||
sequence(vertNew2Old.begin(), vertNew2Old.end());
|
||||
@@ -297,7 +308,8 @@ void Manifold::Impl::SortVerts() {
|
||||
return vertMorton[a] < vertMorton[b];
|
||||
});
|
||||
|
||||
ReindexVerts(vertNew2Old, numVert);
|
||||
ReindexVerts(vertNew2Old, numVert, ctx);
|
||||
if (IsCancelled(ctx)) return;
|
||||
|
||||
// Verts were flagged for removal with NaNs and assigned kNoCode to sort
|
||||
// them to the end, which allows them to be removed.
|
||||
@@ -322,13 +334,16 @@ void Manifold::Impl::SortVerts() {
|
||||
* also given.
|
||||
*/
|
||||
void Manifold::Impl::ReindexVerts(const Vec<int>& vertNew2Old,
|
||||
size_t oldNumVert) {
|
||||
size_t oldNumVert,
|
||||
ExecutionContext::Impl* ctx) {
|
||||
ZoneScoped;
|
||||
// Invariant: every ctx-passing parallel op is followed by IsCancelled to
|
||||
// keep partial output from feeding unconditional downstream consumers.
|
||||
Vec<int> vertOld2New(oldNumVert);
|
||||
scatter(countAt(0), countAt(static_cast<int>(NumVert())), vertNew2Old.begin(),
|
||||
vertOld2New.begin());
|
||||
const bool hasProp = NumProp() > 0;
|
||||
for_each(autoPolicy(oldNumVert, 1e5), halfedge_.begin(), halfedge_.end(),
|
||||
for_each(autoPolicy(oldNumVert, 1e5), halfedge_.begin(), halfedge_.end(), ctx,
|
||||
[&vertOld2New, hasProp](Halfedge& edge) {
|
||||
if (edge.startVert < 0) return;
|
||||
edge.startVert = vertOld2New[edge.startVert];
|
||||
@@ -337,24 +352,29 @@ void Manifold::Impl::ReindexVerts(const Vec<int>& vertNew2Old,
|
||||
edge.propVert = edge.startVert;
|
||||
}
|
||||
});
|
||||
if (IsCancelled(ctx)) return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes unreferenced property verts and reindexes propVerts.
|
||||
*/
|
||||
void Manifold::Impl::CompactProps() {
|
||||
void Manifold::Impl::CompactProps(ExecutionContext::Impl* ctx) {
|
||||
ZoneScoped;
|
||||
if (numProp_ == 0) return;
|
||||
// Invariant: every ctx-passing parallel op is followed by IsCancelled to
|
||||
// keep partial output from feeding unconditional downstream consumers.
|
||||
|
||||
const int numProp = NumProp();
|
||||
const auto numVerts = properties_.size() / numProp;
|
||||
Vec<int> keep(numVerts, 0);
|
||||
auto policy = autoPolicy(numVerts, 1e5);
|
||||
|
||||
for_each(policy, halfedge_.cbegin(), halfedge_.cend(), [&keep](Halfedge h) {
|
||||
reinterpret_cast<std::atomic<int>*>(&keep[h.propVert])
|
||||
->store(1, std::memory_order_relaxed);
|
||||
});
|
||||
for_each(policy, halfedge_.cbegin(), halfedge_.cend(), ctx,
|
||||
[&keep](Halfedge h) {
|
||||
reinterpret_cast<std::atomic<int>*>(&keep[h.propVert])
|
||||
->store(1, std::memory_order_relaxed);
|
||||
});
|
||||
if (IsCancelled(ctx)) return;
|
||||
Vec<int> propOld2New(numVerts + 1, 0);
|
||||
inclusive_scan(keep.begin(), keep.end(), propOld2New.begin() + 1);
|
||||
|
||||
@@ -363,7 +383,7 @@ void Manifold::Impl::CompactProps() {
|
||||
auto& properties = properties_;
|
||||
properties.resize_nofill(numProp * numVertsNew);
|
||||
for_each_n(
|
||||
policy, countAt(0), numVerts,
|
||||
policy, countAt(0), numVerts, ctx,
|
||||
[&properties, &oldProp, &propOld2New, &keep, &numProp](const int oldIdx) {
|
||||
if (keep[oldIdx] == 0) return;
|
||||
for (int p = 0; p < numProp; ++p) {
|
||||
@@ -371,10 +391,12 @@ void Manifold::Impl::CompactProps() {
|
||||
oldProp[oldIdx * numProp + p];
|
||||
}
|
||||
});
|
||||
for_each(policy, halfedge_.begin(), halfedge_.end(),
|
||||
if (IsCancelled(ctx)) return;
|
||||
for_each(policy, halfedge_.begin(), halfedge_.end(), ctx,
|
||||
[&propOld2New](Halfedge& edge) {
|
||||
edge.propVert = propOld2New[edge.propVert];
|
||||
});
|
||||
if (IsCancelled(ctx)) return;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -383,12 +405,15 @@ void Manifold::Impl::CompactProps() {
|
||||
* the bounding box.
|
||||
*/
|
||||
void Manifold::Impl::GetFaceBoxMorton(Vec<Box>& faceBox,
|
||||
Vec<uint32_t>& faceMorton) const {
|
||||
Vec<uint32_t>& faceMorton,
|
||||
ExecutionContext::Impl* ctx) const {
|
||||
ZoneScoped;
|
||||
// Invariant: every ctx-passing parallel op is followed by IsCancelled to
|
||||
// keep partial output from feeding unconditional downstream consumers.
|
||||
// faceBox should be initialized
|
||||
faceBox.resize(NumTri(), Box());
|
||||
faceMorton.resize_nofill(NumTri());
|
||||
for_each_n(autoPolicy(NumTri(), 1e5), countAt(0), NumTri(),
|
||||
for_each_n(autoPolicy(NumTri(), 1e5), countAt(0), NumTri(), ctx,
|
||||
[this, &faceBox, &faceMorton](const int face) {
|
||||
// Removed tris are marked by all halfedges having pairedHalfedge
|
||||
// = -1, and this will sort them to the end (the Morton code only
|
||||
@@ -409,14 +434,18 @@ void Manifold::Impl::GetFaceBoxMorton(Vec<Box>& faceBox,
|
||||
|
||||
faceMorton[face] = MortonCode(center, bBox_);
|
||||
});
|
||||
if (IsCancelled(ctx)) return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the faces of this manifold according to their input Morton code. The
|
||||
* bounding box and Morton code arrays are also sorted accordingly.
|
||||
*/
|
||||
void Manifold::Impl::SortFaces(Vec<Box>& faceBox, Vec<uint32_t>& faceMorton) {
|
||||
void Manifold::Impl::SortFaces(Vec<Box>& faceBox, Vec<uint32_t>& faceMorton,
|
||||
ExecutionContext::Impl* ctx) {
|
||||
ZoneScoped;
|
||||
// Invariant: every ctx-passing parallel op is followed by IsCancelled to
|
||||
// keep partial output from feeding unconditional downstream consumers.
|
||||
Vec<int> faceNew2Old(NumTri());
|
||||
sequence(faceNew2Old.begin(), faceNew2Old.end());
|
||||
|
||||
@@ -437,7 +466,8 @@ void Manifold::Impl::SortFaces(Vec<Box>& faceBox, Vec<uint32_t>& faceMorton) {
|
||||
|
||||
Permute(faceMorton, faceNew2Old);
|
||||
Permute(faceBox, faceNew2Old);
|
||||
GatherFaces(faceNew2Old);
|
||||
GatherFaces(faceNew2Old, ctx);
|
||||
if (IsCancelled(ctx)) return;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -445,8 +475,11 @@ void Manifold::Impl::SortFaces(Vec<Box>& faceBox, Vec<uint32_t>& faceMorton) {
|
||||
* another manifold, given by oldHalfedge. Input faceNew2Old defines the old
|
||||
* faces to gather into this.
|
||||
*/
|
||||
void Manifold::Impl::GatherFaces(const Vec<int>& faceNew2Old) {
|
||||
void Manifold::Impl::GatherFaces(const Vec<int>& faceNew2Old,
|
||||
ExecutionContext::Impl* ctx) {
|
||||
ZoneScoped;
|
||||
// Invariant: every ctx-passing parallel op is followed by IsCancelled to
|
||||
// keep partial output from feeding unconditional downstream consumers.
|
||||
const auto numTri = faceNew2Old.size();
|
||||
if (meshRelation_.triRef.size() == NumTri())
|
||||
Permute(meshRelation_.triRef, faceNew2Old);
|
||||
@@ -462,13 +495,17 @@ void Manifold::Impl::GatherFaces(const Vec<int>& faceNew2Old) {
|
||||
halfedge_.resize_nofill(3 * numTri);
|
||||
if (oldHalfedgeTangent.size() != 0)
|
||||
halfedgeTangent_.resize_nofill(3 * numTri);
|
||||
for_each_n(policy, countAt(0), numTri,
|
||||
for_each_n(policy, countAt(0), numTri, ctx,
|
||||
ReindexFace({halfedge_, halfedgeTangent_, oldHalfedge,
|
||||
oldHalfedgeTangent, faceNew2Old, faceOld2New}));
|
||||
if (IsCancelled(ctx)) return;
|
||||
}
|
||||
|
||||
void Manifold::Impl::GatherFaces(const Impl& old, const Vec<int>& faceNew2Old) {
|
||||
void Manifold::Impl::GatherFaces(const Impl& old, const Vec<int>& faceNew2Old,
|
||||
ExecutionContext::Impl* ctx) {
|
||||
ZoneScoped;
|
||||
// Invariant: every ctx-passing parallel op is followed by IsCancelled to
|
||||
// keep partial output from feeding unconditional downstream consumers.
|
||||
const auto numTri = faceNew2Old.size();
|
||||
|
||||
meshRelation_.triRef.resize_nofill(numTri);
|
||||
@@ -497,20 +534,23 @@ void Manifold::Impl::GatherFaces(const Impl& old, const Vec<int>& faceNew2Old) {
|
||||
halfedge_.resize_nofill(3 * numTri);
|
||||
if (old.halfedgeTangent_.size() != 0)
|
||||
halfedgeTangent_.resize_nofill(3 * numTri);
|
||||
for_each_n(autoPolicy(numTri, 1e5), countAt(0), numTri,
|
||||
for_each_n(autoPolicy(numTri, 1e5), countAt(0), numTri, ctx,
|
||||
ReindexFace({halfedge_, halfedgeTangent_, old.halfedge_,
|
||||
old.halfedgeTangent_, faceNew2Old, faceOld2New}));
|
||||
if (IsCancelled(ctx)) return;
|
||||
}
|
||||
|
||||
void Manifold::Impl::ReorderHalfedges() {
|
||||
void Manifold::Impl::ReorderHalfedges(ExecutionContext::Impl* ctx) {
|
||||
ZoneScoped;
|
||||
// Invariant: every ctx-passing parallel op is followed by IsCancelled to
|
||||
// keep partial output from feeding unconditional downstream consumers.
|
||||
// halfedges in the same face are added in non-deterministic order, so we have
|
||||
// to reorder them for determinism
|
||||
|
||||
// step 1: reorder within the same face, such that the halfedge with the
|
||||
// smallest starting vertex is placed first
|
||||
for_each(autoPolicy(halfedge_.size() / 3), countAt(0),
|
||||
countAt(halfedge_.size() / 3), [this](size_t tri) {
|
||||
countAt(halfedge_.size() / 3), ctx, [this](size_t tri) {
|
||||
std::array<Halfedge, 3> face = {halfedge_[tri * 3],
|
||||
halfedge_[tri * 3 + 1],
|
||||
halfedge_[tri * 3 + 2]};
|
||||
@@ -521,9 +561,10 @@ void Manifold::Impl::ReorderHalfedges() {
|
||||
for (int i : {0, 1, 2})
|
||||
halfedge_[tri * 3 + i] = face[(index + i) % 3];
|
||||
});
|
||||
if (IsCancelled(ctx)) return;
|
||||
// step 2: fix paired halfedge
|
||||
for_each(autoPolicy(halfedge_.size() / 3), countAt(0),
|
||||
countAt(halfedge_.size() / 3), [this](size_t tri) {
|
||||
countAt(halfedge_.size() / 3), ctx, [this](size_t tri) {
|
||||
for (int i : {0, 1, 2}) {
|
||||
Halfedge& curr = halfedge_[tri * 3 + i];
|
||||
if (curr.startVert < 0) return;
|
||||
@@ -535,6 +576,7 @@ void Manifold::Impl::ReorderHalfedges() {
|
||||
curr.pairedHalfedge = oppositeFace * 3 + index;
|
||||
}
|
||||
});
|
||||
if (IsCancelled(ctx)) return;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include "../src/execution_impl.h"
|
||||
#include "../src/parallel.h"
|
||||
#include "../src/vec.h"
|
||||
#ifdef MANIFOLD_CROSS_SECTION
|
||||
#include "manifold/cross_section.h"
|
||||
#endif
|
||||
@@ -1530,6 +1531,50 @@ TEST(Manifold, ExecutionContextCancelMidBoolean) {
|
||||
}
|
||||
#endif // MANIFOLD_PAR == 1
|
||||
|
||||
// Direct test of the ctx-aware `for_each` overload: cancel-already-set
|
||||
// must skip the entire range; cancel-unset must run every element. This
|
||||
// is the mechanism test; the integration test that cancel works
|
||||
// end-to-end on a real boolean is ExecutionContextCancelMidBoolean.
|
||||
TEST(Manifold, ForEachCtxSkipsWhenCancelled) {
|
||||
ExecutionContext ctx;
|
||||
ctx.Cancel();
|
||||
std::atomic<int> calls{0};
|
||||
Vec<int> range(100);
|
||||
sequence(range.begin(), range.end());
|
||||
for_each(ExecutionPolicy::Seq, range.begin(), range.end(), ctx.impl_.get(),
|
||||
[&calls](int) { calls.fetch_add(1, std::memory_order_relaxed); });
|
||||
EXPECT_EQ(calls.load(), 0);
|
||||
}
|
||||
|
||||
// Nullptr ctx: must not crash and must invoke the functor for every element.
|
||||
TEST(Manifold, ForEachCtxNullCtxPassesThrough) {
|
||||
std::atomic<int> calls{0};
|
||||
Vec<int> range(10);
|
||||
sequence(range.begin(), range.end());
|
||||
for_each(ExecutionPolicy::Seq, range.begin(), range.end(),
|
||||
static_cast<ExecutionContext::Impl*>(nullptr),
|
||||
[&calls](int) { calls.fetch_add(1, std::memory_order_relaxed); });
|
||||
EXPECT_EQ(calls.load(), 10);
|
||||
}
|
||||
|
||||
// Cancel set mid-iteration must be observed within bounded latency
|
||||
// (kSeqCancelChunk = 1024 in parallel.h). Functor cancels after 100
|
||||
// calls; total calls must be < N + 1024 (= 100 + at most one full chunk).
|
||||
TEST(Manifold, ForEachCtxCancelMidSeqLoop) {
|
||||
ExecutionContext ctx;
|
||||
std::atomic<int> calls{0};
|
||||
constexpr int N = 10000;
|
||||
Vec<int> range(N);
|
||||
sequence(range.begin(), range.end());
|
||||
for_each(ExecutionPolicy::Seq, range.begin(), range.end(), ctx.impl_.get(),
|
||||
[&calls, &ctx](int) {
|
||||
const int n = calls.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
if (n == 100) ctx.Cancel();
|
||||
});
|
||||
EXPECT_GE(calls.load(), 100);
|
||||
EXPECT_LT(calls.load(), 100 + 1024);
|
||||
}
|
||||
|
||||
// Status() and Status(ctx) should return identical results when no cancel.
|
||||
TEST(Manifold, ExecutionContextMatchesPlainStatus) {
|
||||
Manifold u = (Manifold::Cube(vec3(1), true) + Manifold::Sphere(1.0, 32)) -
|
||||
|
||||
Reference in New Issue
Block a user