From 4e43ab8306dfb8154d489f4ebc4fa261bac6e819 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 25 Sep 2026 15:00:04 +0800 Subject: [PATCH] Make Painted Multi-Material Slicing Deterministic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Painted (multi-material) models sliced to slightly different G-code on every run: ±1 µm wall coordinates and reordered islands. Hashing each stage of the segmentation across runs showed the projected painted lines and the per-layer Voronoi segmentation were stable; the raw top/bottom projections from slice_mesh_slabs() were not. Three causes, all thread-order dependent: - slice_slabs_make_lines() appends each slab's intersection lines from a parallel facet loop and never restored a canonical order, so the loop start vertices and polygon order from make_slab_loops() depended on scheduling. Sort every slab's lines with the same key slice_make_lines() already uses. - segmentation_top_and_bottom_layers() wrote a layer's shell projections into neighbouring layers' vectors from the parallel loop, relying on a parity double-buffer that assumes TBB ranges are exactly one group wide and aligned, which blocked_range does not guarantee; two threads could append to the same vector. Each source layer now records its projections in its own slot and they are gathered per target layer in source order. - The painted-line sort in post_process_painted_lines() was not a total order: projections of one span from facets of different colours tied on every key and the first one won the span. Colour and end points now break the tie. Three multi-threaded runs of each painted fixture now give one G-code; unpainted output is unchanged. --- src/libslic3r/MultiMaterialSegmentation.cpp | 93 ++++++++++++--------- src/libslic3r/TriangleMeshSlicer.cpp | 17 ++++ 2 files changed, 72 insertions(+), 38 deletions(-) diff --git a/src/libslic3r/MultiMaterialSegmentation.cpp b/src/libslic3r/MultiMaterialSegmentation.cpp index 6f80f7b759..464740768d 100644 --- a/src/libslic3r/MultiMaterialSegmentation.cpp +++ b/src/libslic3r/MultiMaterialSegmentation.cpp @@ -636,6 +636,8 @@ static std::vector> get_segments(const ColoredLines &p return segments; } + + static std::vector filter_painted_lines(const Line &line_to_process, const size_t start_idx, const size_t end_idx, const std::vector &painted_lines) { const int filter_eps_value = scale_(0.1f); @@ -688,15 +690,29 @@ static std::vector> post_process_painted_lines(const st if (painted_lines.empty()) return {}; + // The painted lines were appended by parallel workers, so their order is arbitrary. The sort must + // therefore be a total order: two projections of the same span from facets of different colours + // tie on every geometric key, and whichever sorts first wins the span in filter_painted_lines(). + // The colour and the end points break such ties so the result does not depend on scheduling. auto comp = [&contours](const PaintedLine &first, const PaintedLine &second) { - Point first_start_p = contours[first.contour_idx].segment_start(first.line_idx); - return first.contour_idx < second.contour_idx || - (first.contour_idx == second.contour_idx && - (first.line_idx < second.line_idx || - (first.line_idx == second.line_idx && - ((first.projected_line.a - first_start_p).cast().squaredNorm() < (second.projected_line.a - first_start_p).cast().squaredNorm() || - ((first.projected_line.a - first_start_p).cast().squaredNorm() == (second.projected_line.a - first_start_p).cast().squaredNorm() && - (first.projected_line.b - first.projected_line.a).cast().squaredNorm() < (second.projected_line.b - second.projected_line.a).cast().squaredNorm()))))); + if (first.contour_idx != second.contour_idx) + return first.contour_idx < second.contour_idx; + if (first.line_idx != second.line_idx) + return first.line_idx < second.line_idx; + const Point start_p = contours[first.contour_idx].segment_start(first.line_idx); + const double first_dist = (first.projected_line.a - start_p).cast().squaredNorm(); + const double second_dist = (second.projected_line.a - start_p).cast().squaredNorm(); + if (first_dist != second_dist) + return first_dist < second_dist; + const double first_len = (first.projected_line.b - first.projected_line.a).cast().squaredNorm(); + const double second_len = (second.projected_line.b - second.projected_line.a).cast().squaredNorm(); + if (first_len != second_len) + return first_len < second_len; + if (first.color != second.color) + return first.color < second.color; + if (first.projected_line.a != second.projected_line.a) + return first.projected_line.a < second.projected_line.a; + return first.projected_line.b < second.projected_line.b; }; std::sort(painted_lines.begin(), painted_lines.end(), comp); @@ -1200,15 +1216,12 @@ static inline std::vector> segmentation_top_and_bottom_l const size_t num_layers = input_expolygons.size(); const ConstLayerPtrsAdaptor layers = print_object.layers(); - // Maximum number of top / bottom layers accounts for maximum overlap of one thread group into a neighbor thread group. int max_top_layers = 0; int max_bottom_layers = 0; - int granularity = 1; for (size_t i = 0; i < print_object.num_printing_regions(); ++ i) { const PrintRegionConfig &config = print_object.printing_region(i).config(); max_top_layers = std::max(max_top_layers, config.top_shell_layers.value); max_bottom_layers = std::max(max_bottom_layers, config.bottom_shell_layers.value); - granularity = std::max(granularity, std::max(config.top_shell_layers.value, config.bottom_shell_layers.value) - 1); } // Project upwards pointing painted triangles over top surfaces, @@ -1327,14 +1340,16 @@ static inline std::vector> segmentation_top_and_bottom_l std::vector> triangles_by_color_bottom(num_facets_states); std::vector> triangles_by_color_top(num_facets_states); - triangles_by_color_bottom.assign(num_facets_states, std::vector(num_layers * 2)); - triangles_by_color_top.assign(num_facets_states, std::vector(num_layers * 2)); + triangles_by_color_bottom.assign(num_facets_states, std::vector(num_layers)); + triangles_by_color_top.assign(num_facets_states, std::vector(num_layers)); - // BBS: use shell_triangles_by_color_bottom & shell_triangles_by_color_top to save the top and bottom embedded layers's color information - std::vector> shell_triangles_by_color_bottom(num_facets_states); - std::vector> shell_triangles_by_color_top(num_facets_states); - shell_triangles_by_color_bottom.assign(num_facets_states, std::vector(num_layers * 2)); - shell_triangles_by_color_top.assign(num_facets_states, std::vector(num_layers * 2)); + // BBS: the painted top / bottom surfaces are also projected onto the shell layers below / above them. + // Each layer only writes the projections it produced, keyed by the layer they land on, so the + // parallel loop shares nothing; they are gathered per target layer afterwards, in source-layer + // order, which keeps the result independent of how the layers were scheduled. + using ShellProjections = std::vector>; // (target layer, projection) + std::vector> shell_triangles_by_color_bottom(num_facets_states, std::vector(num_layers)); + std::vector> shell_triangles_by_color_top(num_facets_states, std::vector(num_layers)); struct LayerColorStat { // Number of regions for a queried color. @@ -1378,11 +1393,9 @@ static inline std::vector> segmentation_top_and_bottom_l return out; }; - tbb::parallel_for(tbb::blocked_range(0, num_layers, granularity), [&granularity, &num_layers, &num_facets_states, &layer_color_stat, &top_raw, &triangles_by_color_top, - &throw_on_cancel_callback, &input_expolygons, &bottom_raw, &triangles_by_color_bottom, - &shell_triangles_by_color_top, &shell_triangles_by_color_bottom](const tbb::blocked_range &range) { - size_t group_idx = range.begin() / granularity; - size_t layer_idx_offset = (group_idx & 1) * num_layers; + tbb::parallel_for(tbb::blocked_range(0, num_layers), [&num_layers, &num_facets_states, &layer_color_stat, &top_raw, &triangles_by_color_top, + &throw_on_cancel_callback, &input_expolygons, &bottom_raw, &triangles_by_color_bottom, + &shell_triangles_by_color_top, &shell_triangles_by_color_bottom](const tbb::blocked_range &range) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx) { throw_on_cancel_callback(); @@ -1392,7 +1405,7 @@ static inline std::vector> segmentation_top_and_bottom_l // Clean up thin projections. They are not printable anyways. top_ex = opening_ex(top_ex, stat.small_region_threshold); if (! top_ex.empty()) { - append(triangles_by_color_top[color_idx][layer_idx + layer_idx_offset], top_ex); + append(triangles_by_color_top[color_idx][layer_idx], top_ex); float offset = 0.f; ExPolygons layer_slices_trimmed = input_expolygons[layer_idx]; for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx) { @@ -1403,7 +1416,7 @@ static inline std::vector> segmentation_top_and_bottom_l ExPolygons last = opening_ex(intersection_ex(top_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold); if (last.empty()) break; - append(shell_triangles_by_color_top[color_idx][last_idx + layer_idx_offset], std::move(last)); + shell_triangles_by_color_top[color_idx][layer_idx].emplace_back(size_t(last_idx), std::move(last)); } } } @@ -1412,7 +1425,7 @@ static inline std::vector> segmentation_top_and_bottom_l // Clean up thin projections. They are not printable anyways. bottom_ex = opening_ex(bottom_ex, stat.small_region_threshold); if (! bottom_ex.empty()) { - append(triangles_by_color_bottom[color_idx][layer_idx + layer_idx_offset], bottom_ex); + append(triangles_by_color_bottom[color_idx][layer_idx], bottom_ex); float offset = 0.f; ExPolygons layer_slices_trimmed = input_expolygons[layer_idx]; for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx) { @@ -1423,7 +1436,7 @@ static inline std::vector> segmentation_top_and_bottom_l ExPolygons last = opening_ex(intersection_ex(bottom_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold); if (last.empty()) break; - append(shell_triangles_by_color_bottom[color_idx][last_idx + layer_idx_offset], std::move(last)); + shell_triangles_by_color_bottom[color_idx][layer_idx].emplace_back(last_idx, std::move(last)); } } } @@ -1431,19 +1444,28 @@ static inline std::vector> segmentation_top_and_bottom_l } }); + // Gather the shell projections per target layer, walking the source layers in order. + std::vector> shell_top_by_layer(num_facets_states, std::vector(num_layers)); + std::vector> shell_bottom_by_layer(num_facets_states, std::vector(num_layers)); + for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx) + for (size_t layer_idx = 0; layer_idx < num_layers; ++layer_idx) { + for (auto &[target, projection] : shell_triangles_by_color_top[color_idx][layer_idx]) + append(shell_top_by_layer[color_idx][target], std::move(projection)); + for (auto &[target, projection] : shell_triangles_by_color_bottom[color_idx][layer_idx]) + append(shell_bottom_by_layer[color_idx][target], std::move(projection)); + } + std::vector> triangles_by_color_merged(num_facets_states); triangles_by_color_merged.assign(num_facets_states, std::vector(num_layers)); - tbb::parallel_for(tbb::blocked_range(0, num_layers), [&triangles_by_color_merged, &triangles_by_color_bottom, &triangles_by_color_top, &num_layers, &throw_on_cancel_callback, - &shell_triangles_by_color_top, &shell_triangles_by_color_bottom](const tbb::blocked_range &range) { + tbb::parallel_for(tbb::blocked_range(0, num_layers), [&triangles_by_color_merged, &triangles_by_color_bottom, &triangles_by_color_top, &throw_on_cancel_callback, + &shell_top_by_layer, &shell_bottom_by_layer](const tbb::blocked_range &range) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { throw_on_cancel_callback(); ExPolygons painted_exploys; for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx) { auto &self = triangles_by_color_merged[color_idx][layer_idx]; append(self, std::move(triangles_by_color_bottom[color_idx][layer_idx])); - append(self, std::move(triangles_by_color_bottom[color_idx][layer_idx + num_layers])); append(self, std::move(triangles_by_color_top[color_idx][layer_idx])); - append(self, std::move(triangles_by_color_top[color_idx][layer_idx + num_layers])); self = union_ex(self); append(painted_exploys, self); @@ -1455,13 +1477,8 @@ static inline std::vector> segmentation_top_and_bottom_l for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx) { auto &self = triangles_by_color_merged[color_idx][layer_idx]; - auto top_area = diff_ex(union_ex(shell_triangles_by_color_top[color_idx][layer_idx], - shell_triangles_by_color_top[color_idx][layer_idx + num_layers]), - painted_exploys); - - auto bottom_area = diff_ex(union_ex(shell_triangles_by_color_bottom[color_idx][layer_idx], - shell_triangles_by_color_bottom[color_idx][layer_idx + num_layers]), - painted_exploys); + auto top_area = diff_ex(union_ex(shell_top_by_layer[color_idx][layer_idx]), painted_exploys); + auto bottom_area = diff_ex(union_ex(shell_bottom_by_layer[color_idx][layer_idx]), painted_exploys); append(self, top_area); append(self, bottom_area); diff --git a/src/libslic3r/TriangleMeshSlicer.cpp b/src/libslic3r/TriangleMeshSlicer.cpp index 4ff18165cb..a08ea6b55d 100644 --- a/src/libslic3r/TriangleMeshSlicer.cpp +++ b/src/libslic3r/TriangleMeshSlicer.cpp @@ -1062,6 +1062,23 @@ inline std::pair slice_slabs_make_lines( } } ); + // As in slice_make_lines(): the facet loop is parallel, so the per-slab line order depends on + // thread scheduling, and make_slab_loops() derives loop order and start vertices from it. + // Sort canonically; edge_type and flags only break ties, std::sort being unstable. + auto sort_canonically = [](std::vector &lines_per_slab) { + tbb::parallel_for(tbb::blocked_range(0, lines_per_slab.size()), + [&lines_per_slab](const tbb::blocked_range &range) { + for (size_t i = range.begin(); i < range.end(); ++ i) + std::sort(lines_per_slab[i].begin(), lines_per_slab[i].end(), [](const IntersectionLine &l, const IntersectionLine &r) { + return std::make_tuple(l.edge_a_id, l.edge_b_id, l.a_id, l.b_id, l.a.x(), l.a.y(), l.b.x(), l.b.y(), l.edge_type, l.flags) < + std::make_tuple(r.edge_a_id, r.edge_b_id, r.a_id, r.b_id, r.a.x(), r.a.y(), r.b.x(), r.b.y(), r.edge_type, r.flags); + }); + }); + }; + for (SlabLines *slab_lines : { &lines_top, &lines_bottom }) { + sort_canonically(slab_lines->at_slice); + sort_canonically(slab_lines->between_slices); + } return out; }