Thread ExecutionContext into Smooth via ExecutionContext::Smooth (#1734)

* Thread ExecutionContext into Manifold::Smooth via ExecutionContext::Smooth

Adds the static factories `ExecutionContext::Smooth(MeshGL)` and
`Smooth(MeshGL64)`, mirroring the FromMeshGL pattern from #1723. The
ingest phases (`kPhasesPerFromMesh = 7`) plus seven new tangent-creation
phases in `CreateTangents` (`kPhasesPerSmooth = 14` total) check cancel
and publish progress at each boundary.

Implementation reuses the existing scaffolding:
- A templated `MakeSmoothImpl` helper in `impl.h` replaces the
  anonymous-namespace `SmoothImpl` in `constructors.cpp` so both the
  non-ctx `Manifold::Smooth` and the ctx-aware `ExecutionContext::Smooth`
  share it. Entry-time cancel wins; past the entry gate, validation
  errors from the inner MeshGL ctor short-circuit before
  CreateTangents, keeping phase counters honest.
- `Manifold::Impl::CreateTangents(std::vector<Smoothness>)` gains an
  optional `ExecutionContext::Impl* ctx = nullptr` parameter and seven
  `ADVANCE_PHASE_OR_RETURN(ctx)` call sites. The two parallel
  `for_each_n` loops pass `ctx` to the existing ctx-aware overload in
  `parallel.h` for per-chunk cancel granularity, no wrapper needed.
- `LinearizeFlatTangents` and `DistributeTangents` are not ctx-aware
  themselves -- the boundary belongs at their call site in
  CreateTangents, since both are also called from the unrelated
  `CreateTangents(int normalIdx)` path.

Test coverage in `context_test.cpp` mirrors the ten
ExecutionContextFromMeshGL tests plus one Smooth-specific case
(`CancelWithSharpenedEdges`) asserting `NumTri() == 0` on Cancelled --
the faceID-restoration loop in `MakeSmoothImpl` is a natural no-op
after `MakeEmpty`.

* Trim comments per manifold-style-review

- MakeSmoothImpl docstring matches the two-line FromMeshGL ctor
  precedent at impl.h:279-280.
- Drop the Phase 4 sub-loop note from kPhasesPerSmooth; the
  bump-in-lockstep rule is the load-bearing invariant.
- Shorten the inline validation-error and faceID-restoration comments.

* Collapse FromMeshGL/Smooth ctx test clusters into one TYPED_TEST_SUITE

Addresses @elalish's PR #1734 review comment about generic test generation.
The two factories share 10 near-identical tests; a `TYPED_TEST_SUITE`
parameterized by `FromMeshGLTraits` / `SmoothTraits` removes ~150 lines
of duplication while keeping per-factory phase counts and names.

The Smooth-specific `CancelWithSharpenedEdges` stays as a separate
`TEST(ExecutionContextSmoothExtras, ...)` since no other factory takes a
`sharpenedEdges` argument. A compile-time `kIsStaticFactoryTraits`
contract check guards against future traits with missing members.

* Trim Smooth ctx tests to unique paths

Reverts the TYPED_TEST_SUITE indirection from 6bde95ed; per review, cut
the per-factory test surface instead. The shared counter/reuse/validation
machinery is covered by the full FromMeshGL cluster, so Smooth keeps only
its unique paths: happy path, parity with the plain factory, the
entry-time cancel gate in MakeSmoothImpl, concurrent cancel through the
extra tangent phases, and sharpenedEdges.
This commit is contained in:
Zach Loafman
2026-05-29 21:29:56 +12:00
committed by GitHub
parent f77233c520
commit 9f01818d63
7 changed files with 182 additions and 38 deletions
+9
View File
@@ -268,6 +268,15 @@ class ExecutionContext {
Manifold FromMeshGL(const MeshGL& mesh);
Manifold FromMeshGL(const MeshGL64& mesh);
/// Eager ctx-aware `Manifold::Smooth(MeshGL[64])`. The ingest phases
/// plus the tangent-creation phases check cancel and credit
/// `Progress()` between phases. Same cancel-vs-validation precedence
/// as `FromMeshGL`.
Manifold Smooth(const MeshGL& mesh,
const std::vector<Smoothness>& sharpenedEdges = {});
Manifold Smooth(const MeshGL64& mesh,
const std::vector<Smoothness>& sharpenedEdges = {});
/// @internal Opaque implementation. Defined in src/execution_impl.h;
/// accessible only to internal code that includes that header.
struct Impl;
+2 -34
View File
@@ -19,38 +19,6 @@
#include "manifold/polygon.h"
#include "parallel.h"
namespace {
using namespace manifold;
template <typename P, typename I>
std::shared_ptr<Manifold::Impl> SmoothImpl(
const MeshGLP<P, I>& meshGL,
const std::vector<Smoothness>& sharpenedEdges) {
DEBUG_ASSERT(meshGL.halfedgeTangent.empty(), std::runtime_error,
"when supplying tangents, the normal constructor should be used "
"rather than Smooth().");
MeshGLP<P, I> meshTmp = meshGL;
meshTmp.faceID.resize(meshGL.NumTri());
std::iota(meshTmp.faceID.begin(), meshTmp.faceID.end(), 0);
std::shared_ptr<Manifold::Impl> impl =
std::make_shared<Manifold::Impl>(meshTmp);
impl->CreateTangents(impl->UpdateSharpenedEdges(sharpenedEdges));
// Restore the original faceID
const size_t numTri = impl->NumTri();
for (size_t i = 0; i < numTri; ++i) {
if (meshGL.faceID.size() == numTri) {
impl->meshRelation_.triRef[i].faceID =
meshGL.faceID[impl->meshRelation_.triRef[i].faceID];
} else {
impl->meshRelation_.triRef[i].faceID = -1;
}
}
return impl;
}
} // namespace
namespace manifold {
/**
* Constructs a smooth version of the input mesh by creating tangents; this
@@ -82,7 +50,7 @@ namespace manifold {
*/
Manifold Manifold::Smooth(const MeshGL& meshGL,
const std::vector<Smoothness>& sharpenedEdges) {
return Manifold(SmoothImpl(meshGL, sharpenedEdges));
return Manifold(MakeSmoothImpl(meshGL, sharpenedEdges));
}
/**
@@ -115,7 +83,7 @@ Manifold Manifold::Smooth(const MeshGL& meshGL,
*/
Manifold Manifold::Smooth(const MeshGL64& meshGL64,
const std::vector<Smoothness>& sharpenedEdges) {
return Manifold(SmoothImpl(meshGL64, sharpenedEdges));
return Manifold(MakeSmoothImpl(meshGL64, sharpenedEdges));
}
/**
+12
View File
@@ -72,4 +72,16 @@ Manifold ExecutionContext::FromMeshGL(const MeshGL64& mesh) {
std::make_shared<Manifold::Impl>(mesh, impl_.get()));
}
Manifold ExecutionContext::Smooth(
const MeshGL& mesh, const std::vector<Smoothness>& sharpenedEdges) {
ResetForStaticFactory(impl_.get(), kPhasesPerSmooth);
return Manifold::FromImpl(MakeSmoothImpl(mesh, sharpenedEdges, impl_.get()));
}
Manifold ExecutionContext::Smooth(
const MeshGL64& mesh, const std::vector<Smoothness>& sharpenedEdges) {
ResetForStaticFactory(impl_.get(), kPhasesPerSmooth);
return Manifold::FromImpl(MakeSmoothImpl(mesh, sharpenedEdges, impl_.get()));
}
} // namespace manifold
+10
View File
@@ -38,6 +38,16 @@ constexpr int kPhasesPerBoolean = 11;
*/
constexpr int kPhasesPerFromMesh = 7;
/** @ingroup Private
*
* Heavy phases in `ExecutionContext::Smooth(MeshGL[64])`: the
* `kPhasesPerFromMesh` ingest phases followed by 7 tangent-creation
* phases in `Manifold::Impl::CreateTangents(sharpenedEdges, ctx)`.
* Bump in lockstep with `ADVANCE_PHASE_OR_RETURN(ctx)` sites in that
* function.
*/
constexpr int kPhasesPerSmooth = kPhasesPerFromMesh + 7;
/** @ingroup Private
*
* Pimpl for ExecutionContext. `cancel` is private; use `IsCancelled(ctx)`
+41 -1
View File
@@ -231,7 +231,8 @@ struct Manifold::Impl {
void LinearizeFlatTangents();
void DistributeTangents(const Vec<bool>& fixedHalfedges);
void CreateTangents(int normalIdx);
void CreateTangents(std::vector<Smoothness>);
void CreateTangents(std::vector<Smoothness>,
ExecutionContext::Impl* ctx = nullptr);
void Refine(std::function<int(vec3, vec4, vec4)>, bool = false,
ExecutionContext::Impl* ctx = nullptr);
@@ -677,4 +678,43 @@ inline MeshGLP<Precision, I> GetMeshGLImpl(const manifold::Manifold::Impl& impl,
}
return out;
}
// Entry-time cancel wins over empty/malformed input; past this gate,
// validation errors win over races.
template <typename P, typename I>
std::shared_ptr<Manifold::Impl> MakeSmoothImpl(
const MeshGLP<P, I>& meshGL, const std::vector<Smoothness>& sharpenedEdges,
ExecutionContext::Impl* ctx = nullptr) {
if (IsCancelled(ctx)) {
auto impl = std::make_shared<Manifold::Impl>();
impl->MakeEmpty(Manifold::Error::Cancelled);
return impl;
}
DEBUG_ASSERT(meshGL.halfedgeTangent.empty(), std::runtime_error,
"when supplying tangents, the normal constructor should be used "
"rather than Smooth().");
MeshGLP<P, I> meshTmp = meshGL;
meshTmp.faceID.resize(meshGL.NumTri());
std::iota(meshTmp.faceID.begin(), meshTmp.faceID.end(), 0);
std::shared_ptr<Manifold::Impl> impl =
std::make_shared<Manifold::Impl>(meshTmp, ctx);
// Skip tangent creation if ingest failed; phase counters must not
// credit smoothing phases that never ran.
if (impl->status_ != Manifold::Error::NoError) return impl;
impl->CreateTangents(impl->UpdateSharpenedEdges(sharpenedEdges), ctx);
// NumTri() is 0 after MakeEmpty, so this loop is a no-op on cancel.
const size_t numTri = impl->NumTri();
for (size_t i = 0; i < numTri; ++i) {
if (meshGL.faceID.size() == numTri) {
impl->meshRelation_.triRef[i].faceID =
meshGL.faceID[impl->meshRelation_.triRef[i].faceID];
} else {
impl->meshRelation_.triRef[i].faceID = -1;
}
}
return impl;
}
} // namespace manifold
+12 -3
View File
@@ -933,7 +933,8 @@ void Manifold::Impl::CreateTangents(int normalIdx) {
* curvature there, while the tangents of the sharp edges themselves are aligned
* for continuity.
*/
void Manifold::Impl::CreateTangents(std::vector<Smoothness> sharpenedEdges) {
void Manifold::Impl::CreateTangents(std::vector<Smoothness> sharpenedEdges,
ExecutionContext::Impl* ctx) {
ZoneScoped;
const int numHalfedge = halfedge_.size();
halfedgeTangent_.clear();
@@ -949,8 +950,9 @@ void Manifold::Impl::CreateTangents(std::vector<Smoothness> sharpenedEdges) {
vertNormal[v] = faceNormal_[vertFlatFace[v]];
}
}
ADVANCE_PHASE_OR_RETURN(ctx);
for_each_n(autoPolicy(numHalfedge, 1e4), countAt(0), numHalfedge,
for_each_n(autoPolicy(numHalfedge, 1e4), countAt(0), numHalfedge, ctx,
[&tangent, &vertNormal, this](const int edgeIdx) {
tangent[edgeIdx] =
IsInsideQuad(edgeIdx)
@@ -960,6 +962,7 @@ void Manifold::Impl::CreateTangents(std::vector<Smoothness> sharpenedEdges) {
});
halfedgeTangent_ = std::move(tangent);
ADVANCE_PHASE_OR_RETURN(ctx);
// Add sharpened edges around faces, just on the face side.
for (size_t tri = 0; tri < NumTri(); ++tri) {
@@ -972,6 +975,7 @@ void Manifold::Impl::CreateTangents(std::vector<Smoothness> sharpenedEdges) {
}
}
}
ADVANCE_PHASE_OR_RETURN(ctx);
using Pair = std::pair<Smoothness, Smoothness>;
// Fill in missing pairs with default smoothness = 1.
@@ -997,10 +1001,11 @@ void Manifold::Impl::CreateTangents(std::vector<Smoothness> sharpenedEdges) {
vertTangents[halfedge_.Start(edge.second.halfedge)].push_back(
{edge.second, edge.first});
}
ADVANCE_PHASE_OR_RETURN(ctx);
const int numVert = NumVert();
for_each_n(
autoPolicy(numVert, 1e4), countAt(0), numVert,
autoPolicy(numVert, 1e4), countAt(0), numVert, ctx,
[this, &vertTangents, &fixedHalfedge, &vertHalfedge,
&triIsFlatFace](int v) {
auto it = vertTangents.find(v);
@@ -1059,9 +1064,13 @@ void Manifold::Impl::CreateTangents(std::vector<Smoothness> sharpenedEdges) {
});
}
});
ADVANCE_PHASE_OR_RETURN(ctx);
LinearizeFlatTangents();
ADVANCE_PHASE_OR_RETURN(ctx);
DistributeTangents(fixedHalfedge);
ADVANCE_PHASE_OR_RETURN(ctx);
}
bool Manifold::Impl::ValidTangents() const {
+96
View File
@@ -911,3 +911,99 @@ TEST(ExecutionContextFromMeshGL, StickyCancelAcrossCalls) {
EXPECT_EQ(ctx.FromMeshGL(TetGL()).Status(), Manifold::Error::Cancelled);
EXPECT_EQ(ctx.FromMeshGL(TetGL()).Status(), Manifold::Error::Cancelled);
}
// Smooth: ctx-aware analog of `Manifold::Smooth(MeshGL[64])`. The shared
// counter/reuse/validation machinery is the same as FromMeshGL and is covered
// by that cluster above, so here we only test Smooth's unique paths: the happy
// path, parity with the plain factory, the entry-time cancel gate in
// MakeSmoothImpl (distinct from the shared Impl-ctor gate), concurrent cancel
// through the extra tangent-creation phases, and the sharpenedEdges argument.
TEST(ExecutionContextSmooth, HappyPath) {
ExecutionContext ctx;
Manifold tet = ctx.Smooth(TetGL());
EXPECT_FALSE(tet.IsEmpty());
EXPECT_EQ(tet.Status(), Manifold::Error::NoError);
EXPECT_EQ(ctx.impl_->totalPhases.load(), kPhasesPerSmooth);
EXPECT_EQ(ctx.impl_->donePhases.load(), kPhasesPerSmooth);
EXPECT_DOUBLE_EQ(ctx.Progress(), 1.0);
}
// Equivalent geometry to the plain factory.
TEST(ExecutionContextSmooth, MatchesFactory) {
MeshGL mesh = Manifold::Sphere(1.0, 32).GetMeshGL();
ExecutionContext ctx;
Manifold viaCtx = ctx.Smooth(mesh);
Manifold viaFactory = Manifold::Smooth(mesh);
EXPECT_EQ(viaCtx.Status(), viaFactory.Status());
EXPECT_EQ(viaCtx.NumTri(), viaFactory.NumTri());
EXPECT_EQ(viaCtx.NumVert(), viaFactory.NumVert());
}
// Pre-cancel hits Smooth's own entry gate in MakeSmoothImpl and returns
// before ingest - distinct from the shared Impl-ctor gate FromMeshGL covers.
TEST(ExecutionContextSmooth, CancelBeforeIngest) {
ExecutionContext ctx;
ctx.Cancel();
Manifold m = ctx.Smooth(TetGL());
EXPECT_EQ(m.Status(), Manifold::Error::Cancelled);
}
// Concurrent cancel mid-smooth. Adaptive backoff guarantees we observe
// at least one Cancelled outcome; each Cancelled hit pins donePhases <
// totalPhases.
#if MANIFOLD_PAR == 1
TEST(ExecutionContextSmooth, CancelConcurrent) {
MeshGL mesh = Manifold::Sphere(1.0, 512).GetMeshGL(); // ~524k tris
int cancelledHits = 0;
auto sleep = std::chrono::microseconds(100);
for (int attempt = 0; attempt < 12 && cancelledHits == 0; ++attempt) {
ExecutionContext ctx;
std::atomic<Manifold::Error> result{Manifold::Error::NoError};
std::thread evalThread([&] { result.store(ctx.Smooth(mesh).Status()); });
std::this_thread::sleep_for(sleep);
ctx.Cancel();
evalThread.join();
EXPECT_TRUE(result.load() == Manifold::Error::Cancelled ||
result.load() == Manifold::Error::NoError);
if (result.load() == Manifold::Error::Cancelled) {
++cancelledHits;
EXPECT_LT(ctx.impl_->donePhases.load(), ctx.impl_->totalPhases.load());
EXPECT_LT(ctx.Progress(), 1.0);
}
sleep *= 2;
}
EXPECT_GT(cancelledHits, 0);
}
#endif // MANIFOLD_PAR == 1
// Smooth-specific: cancel mid-flight with a non-empty sharpenedEdges
// vector. Asserts NumTri() == 0 on Cancelled to lock in that the
// faceID-restoration loop in MakeSmoothImpl is a natural no-op after
// MakeEmpty.
#if MANIFOLD_PAR == 1
TEST(ExecutionContextSmooth, CancelWithSharpenedEdges) {
MeshGL mesh = Manifold::Sphere(1.0, 512).GetMeshGL();
std::vector<Smoothness> sharpenedEdges = {{0, 0.0}, {3, 0.5}, {6, 0.0}};
int cancelledHits = 0;
auto sleep = std::chrono::microseconds(100);
for (int attempt = 0; attempt < 12 && cancelledHits == 0; ++attempt) {
ExecutionContext ctx;
std::atomic<Manifold::Error> result{Manifold::Error::NoError};
std::atomic<size_t> numTri{0};
std::thread evalThread([&] {
Manifold m = ctx.Smooth(mesh, sharpenedEdges);
result.store(m.Status());
numTri.store(m.NumTri());
});
std::this_thread::sleep_for(sleep);
ctx.Cancel();
evalThread.join();
if (result.load() == Manifold::Error::Cancelled) {
++cancelledHits;
EXPECT_EQ(numTri.load(), 0u);
}
sleep *= 2;
}
EXPECT_GT(cancelledHits, 0);
}
#endif // MANIFOLD_PAR == 1