Make Tree-Support Deterministic (#15565)

* Make tree support deterministic without giving up its parallelism

* Break equal-distance ties in the tree support MST by coordinates

* test: cover the determinism this PR fixes

The MST unit tests here cover the tie-break, but the drop_nodes rework
has no test.

Adds two cases to the tree support suite. The thread-scheduling one
slices five configs twice each and compares the support point sequence,
which is what the node ordering moves. The MST tie one pins the branch
diameter and line width that carry Prim's equal-distance ties into the
toolpaths.

slice_with_tree_support takes an optional config list so the second case
can add the tree parameters it needs, and the double-slice comparison is
shared rather than written twice.

Both fail on main without this PR. The first passes from 60d1ceb580, the
second from e148865dd6.

---------

Co-authored-by: raistlin7447 <kris.austin@gmail.com>
This commit is contained in:
HanifKoh
2026-09-09 12:33:42 +08:00
committed by GitHub
co-authored by raistlin7447
parent 0f5891f25d
commit 4deadc9dce
6 changed files with 223 additions and 33 deletions
+67 -2
View File
@@ -1,5 +1,7 @@
#include <catch2/catch_all.hpp>
#include <algorithm>
#include "libslic3r/Layer.hpp"
#include "libslic3r/TriangleMesh.hpp"
@@ -33,10 +35,13 @@ TriangleMesh scaled(TestMesh id, float scale)
return mesh;
}
// `extra` is applied last, so a caller can add or override any key.
void slice_with_tree_support(const TriangleMesh &mesh, Slic3r::Print &print, const char *style,
int threshold_angle = 30, int build_plate_only = 0, int raft_layers = 0)
int threshold_angle = 30, int build_plate_only = 0, int raft_layers = 0,
std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> extra = {})
{
Slic3r::Test::init_and_process_print({ mesh }, print, {
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "enable_support", 1 },
{ "support_type", "tree(auto)" },
{ "support_style", style },
@@ -45,6 +50,8 @@ void slice_with_tree_support(const TriangleMesh &mesh, Slic3r::Print &print, con
{ "raft_layers", raft_layers },
{ "layer_height", 0.2 },
});
config.set_deserialize_strict(extra);
Slic3r::Test::init_and_process_print({ mesh }, print, config);
}
Points support_points(const Slic3r::Print &print)
@@ -63,6 +70,32 @@ size_t support_point_count(const TriangleMesh &mesh, const char *style, int thre
return support_points(print).size();
}
// Index of the first differing point, or the common length when they match. An index keeps a
// failure readable; comparing the vectors themselves dumps thousands of points.
size_t first_difference(const Points &a, const Points &b)
{
const size_t common = std::min(a.size(), b.size());
for (size_t i = 0; i < common; ++i)
if (a[i] != b[i])
return i;
return common;
}
// Slice `mesh` twice and require an identical support point sequence. Point counts and total
// length are order insensitive, so the sequence is what a reordering shows up in.
void sliced_twice_matches(const TriangleMesh &mesh, int build_plate_only, const char *style = "tree_slim",
std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> extra = {})
{
Slic3r::Print first_print, second_print;
slice_with_tree_support(mesh, first_print, style, 30, build_plate_only, 0, extra);
slice_with_tree_support(mesh, second_print, style, 30, build_plate_only, 0, extra);
const Points first = support_points(first_print);
const Points second = support_points(second_print);
REQUIRE(first.size() > 1000); // without support the comparison below passes vacuously
REQUIRE(second.size() == first.size());
REQUIRE(first_difference(first, second) == first.size());
}
} // namespace
TEST_CASE("Tree support is generated for an overhang and not for a plain cube", "[TreeSupport]")
@@ -123,3 +156,35 @@ TEST_CASE("A raft is still generated under tree support", "[TreeSupport]")
// The raft goes under the object.
REQUIRE(rafted_object->layers().front()->print_z > unrafted_object->layers().front()->print_z);
}
// drop_nodes() decides the node merges and spawns the next layer's nodes in parallel. Every one of
// those decisions has to be applied in a fixed order, or the same model gives different branches on
// each slice.
TEST_CASE("Tree support toolpaths do not depend on thread scheduling", "[TreeSupport][Regression]")
{
// Scaled up so that a layer holds enough nodes for the parallel range to be split. At stock
// size it stays in one chunk and the order never varies.
SECTION("overhang") { sliced_twice_matches(scaled(TestMesh::overhang, 2.f), 0); }
SECTION("bridge with hole") { sliced_twice_matches(scaled(TestMesh::bridge_with_hole, 3.f), 0); }
// Dropping every branch that cannot reach the bed leaves the survivors dense enough that the
// neighbour merge fires in bulk.
SECTION("on the build plate") { sliced_twice_matches(scaled(TestMesh::overhang, 4.f), 1); }
// Branches resting on the model are what put nodes in a part group other than 0, which is the
// only way to reach the prune in the second pass. tree_hybrid additionally builds polygon
// nodes, so it is the only style that exercises the overhang merge.
SECTION("resting on the model") { sliced_twice_matches(two_tier_mesh(), 0); }
SECTION("hybrid on the model") { sliced_twice_matches(two_tier_mesh(), 0, "tree_hybrid"); }
}
// Prim breaks equal-distance ties by heap address. A 1 mm branch diameter puts neighbours close
// enough to tie, and an explicit line width pins max_move_dist, so the moved tie winner reaches
// the support toolpaths.
TEST_CASE("Tree support toolpaths do not depend on the MST tie order", "[TreeSupport][Regression]")
{
sliced_twice_matches(two_tier_mesh(), 0, "tree_hybrid", {
{ "tree_support_branch_diameter", 1.0 },
{ "tree_support_branch_distance", 5.0 },
{ "tree_support_branch_angle", 40 },
{ "support_line_width", 0.4 },
});
}
+1
View File
@@ -29,6 +29,7 @@ add_executable(${_TEST_NAME}_tests
test_polygon.cpp
test_mutable_polygon.cpp
test_mutable_priority_queue.cpp
test_minimum_spanning_tree.cpp
test_nozzle_volume_type.cpp
test_step.cpp
test_stl.cpp
@@ -0,0 +1,66 @@
#include <catch2/catch_all.hpp>
#include <algorithm>
#include "libslic3r/MinimumSpanningTree.hpp"
#include "libslic3r/Point.hpp"
using namespace Slic3r;
// A 5x5 lattice: at every step of Prim's algorithm several candidates sit at the same
// distance from the tree, so the tie-break decides the tree's shape.
static std::vector<Point> lattice()
{
std::vector<Point> vertices;
for (int y = 0; y < 5; ++y)
for (int x = 0; x < 5; ++x)
vertices.emplace_back(Point::new_scale(x, y));
return vertices;
}
static std::vector<Point> sorted_neighbours(const MinimumSpanningTree &mst, const Point &vertex)
{
std::vector<Point> neighbours = mst.adjacent_nodes(vertex);
std::sort(neighbours.begin(), neighbours.end());
return neighbours;
}
TEST_CASE("Minimum spanning tree connects every vertex", "[MinimumSpanningTree]")
{
const std::vector<Point> vertices = lattice();
const MinimumSpanningTree mst(vertices);
REQUIRE(mst.vertices().size() == vertices.size());
size_t adjacency_entries = 0;
for (const Point &vertex : vertices) {
const std::vector<Point> neighbours = mst.adjacent_nodes(vertex);
REQUIRE(! neighbours.empty());
adjacency_entries += neighbours.size();
}
// A tree on n vertices has n - 1 edges, each listed from both ends.
REQUIRE(adjacency_entries == 2 * (vertices.size() - 1));
}
TEST_CASE("Minimum spanning tree does not depend on the order of the non-root vertices", "[MinimumSpanningTree][Regression]")
{
const std::vector<Point> vertices = lattice();
const MinimumSpanningTree reference(vertices);
// The root stays first: Prim's tree legitimately depends on where it starts.
// Every other order of the remaining vertices must give the same tree.
std::vector<std::vector<Point>> orders;
orders.emplace_back(vertices);
std::reverse(orders.back().begin() + 1, orders.back().end());
for (size_t shift = 1; shift + 1 < vertices.size(); ++shift) {
orders.emplace_back(vertices);
std::rotate(orders.back().begin() + 1, orders.back().begin() + 1 + shift, orders.back().end());
}
for (const std::vector<Point> &order : orders) {
const MinimumSpanningTree mst(order);
for (const Point &vertex : vertices) {
INFO("vertex " << vertex.x() << "," << vertex.y());
REQUIRE(sorted_neighbours(mst, vertex) == sorted_neighbours(reference, vertex));
}
}
}