From 626350269737627e341b56dfbe2ad86971c9dc59 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 13 Oct 2026 00:15:07 +0800 Subject: [PATCH] Free a sharing object's own layers only when it takes over the shared ones --- src/libslic3r/Print.cpp | 14 +++-- src/libslic3r/Support/TreeSupport.cpp | 9 ++-- tests/fff_print/test_print.cpp | 76 +++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index b636345d60..e15d9b18cd 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -2971,11 +2971,6 @@ void Print::auto_assign_extruders(ModelObject* model_object) const void PrintObject::set_shared_object(PrintObject *object) { - // Orca: from now on m_layers / m_support_layers only alias the shared object's layers, so release the - // ones this object still owns (it may have sliced itself before it became shareable again). - // Both are no-ops once m_shared_object is set, so this cannot free layers owned by another object. - clear_support_layers(); - clear_layers(); m_shared_object = object; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": this=%1%, found shared object from %2%")%this%m_shared_object; } @@ -2996,6 +2991,15 @@ void PrintObject::clear_shared_object() void PrintObject::copy_layers_from_shared_object() { if (m_shared_object) { + // Orca: Print::process() calls this once per slice for each sharing object, after clear_shared_object() has + // emptied any earlier alias, so the vectors hold only layers this object sliced itself before it started sharing + // (or nothing). Free them here, as clear_support_layers() / clear_layers() would; those skip deletion while + // m_shared_object is set. The belt-only data they also free is always empty here, because belt printers never + // share layers. + for (SupportLayer *layer : m_support_layers) + delete layer; + for (Layer *layer : m_layers) + delete layer; m_layers.clear(); m_support_layers.clear(); diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 41668e012c..5187e73a25 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -1976,8 +1976,8 @@ coordf_t TreeSupport::get_radius(const SupportNode* node) return node->radius; } -// Orca: these are hit up to several times per node per layer in drop_nodes(), so hand out a -// reference into the TreeSupportData cache instead of copying the ExPolygons out of it. +// Orca: drop_nodes() calls these several times per node per layer, so they return a reference into the +// TreeSupportData cache. The cache only ever inserts, so its entries never move while nodes drop in parallel. const ExPolygons& TreeSupport::get_avoidance(coordf_t radius, size_t obj_layer_nr) { return m_ts_data->get_avoidance(radius, obj_layer_nr); @@ -2805,9 +2805,10 @@ void TreeSupport::draw_circles() #endif // SUPPORT_TREE_DEBUG_TO_SVG SupportLayerPtrs& ts_layers = m_object->support_layers(); - // Orca: the vector owns its layers, so the dropped ones have to be deleted, not just unlinked. + // Orca: the vector owns its layers, so the dropped ones are deleted before they are erased. // std::stable_partition (unlike std::remove_if) leaves exactly the dropped layers in the tail. - auto iter = std::stable_partition(ts_layers.begin(), ts_layers.end(), [](SupportLayer* ts_layer) { return ts_layer->height >= EPSILON; }); + // Only heights below EPSILON are dropped; written as !(height < EPSILON), a NaN height is kept. + auto iter = std::stable_partition(ts_layers.begin(), ts_layers.end(), [](SupportLayer* ts_layer) { return !(ts_layer->height < EPSILON); }); for (auto it = iter; it != ts_layers.end(); ++it) delete *it; ts_layers.erase(iter, ts_layers.end()); diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 58f8d12e39..4b76de4b55 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -43,6 +43,7 @@ #include "libslic3r/GCode/GCodeProcessor.hpp" #include "libslic3r/Exception.hpp" #include "libslic3r/LifecycleEvents.hpp" +#include "libslic3r/Utils.hpp" #include "test_helpers.hpp" #include "test_utils.hpp" @@ -156,6 +157,81 @@ SCENARIO("Changing the number of solid shell layers does not make all surfaces i } } +// Two copies of one object share a single slice. When a copy that has sliced itself becomes identical to an +// earlier object, it keeps its own layers while that object slices and only then takes the shared layers over. +TEST_CASE("An object that starts sharing another object's slices keeps its own layers until it takes them over", "[Print][Regression]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "raft_layers", 1 }, // each object gets support layers of its own + { "sparse_infill_density", "20%" }, // object_a's override below differs from this + }); + // Activates the slicing-pipeline hook installed for the second slice below. + config.set_key_value("slicing_pipeline_plugin", new ConfigOptionStrings({"probe"})); + + Print print; + Model model; + init_print({cube(20)}, print, model, config); + ModelObject *object_a = model.objects.front(); + // add_object() gives the copy extruder 1, and objects only share slices when their configs match. + object_a->config.set_key_value("extruder", new ConfigOptionInt(1)); + ModelObject *object_b = model.add_object(*object_a); // shares object_a's mesh + for (ModelInstance *instance : object_b->instances) + instance->set_offset(instance->get_offset() + Vec3d(40., 0., 0.)); + // A per-object override makes the objects differ, so each slices itself. + object_a->config.set_key_value("sparse_infill_density", new ConfigOptionPercent(40)); + + print.apply(model, config); + print.process(); + REQUIRE(print.objects().size() == 2); + // The non-const accessors hand out the layer vectors themselves, which the checks below compare. + PrintObject *print_b = print.get_object(1); + REQUIRE(print_b->model_object()->id() == object_b->id()); // Print slices its own copy of the model + REQUIRE(print_b->get_shared_object() == nullptr); + const LayerPtrs own_layers = print_b->layers(); + const SupportLayerPtrs own_support_layers = print_b->support_layers(); + REQUIRE(!own_layers.empty()); + REQUIRE(!own_support_layers.empty()); + + // Without the override the objects are identical again: print_a redoes the steps the setting invalidated, + // and print_b shares it. + object_a->config.erase("sparse_infill_density"); + print.apply(model, config); + REQUIRE(print.objects().size() == 2); + PrintObject *print_a = print.get_object(0); + + // Erasing the override invalidates print_a's posPrepareInfill and the steps after it. Its hook is the first one + // print_a fires, and it runs after print_b starts sharing but before print_b takes the shared layers over. + bool hook_fired = false; + bool shared_while_a_runs = false; + bool kept_layers = false; + bool kept_support_layers = false; + { + Print::set_slicing_pipeline_hook_fn([&](Print &, const PrintObject *object, SlicingPipelineStepPlugin) { + if (object != print_a || hook_fired) + return; + hook_fired = true; + shared_while_a_runs = print_b->get_shared_object() == print_a; + kept_layers = print_b->layers() == own_layers; + kept_support_layers = print_b->support_layers() == own_support_layers; + }); + const ScopeGuard reset_hook([] { Print::set_slicing_pipeline_hook_fn(nullptr); }); + print.process(); + } + + REQUIRE(hook_fired); + CHECK(print.objects()[1] == print_b); + CHECK(shared_while_a_runs); + CHECK(kept_layers); + CHECK(kept_support_layers); + CHECK(print_b->get_shared_object() == print_a); + REQUIRE(!print_a->layers().empty()); + CHECK(print_b->layers() == print_a->layers()); + CHECK(print_b->support_layers() == print_a->support_layers()); +} + // --------------------------------------------------------------------------- // Print::validate() warning collection //