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:
@@ -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
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user