From c67b54b39db106a9f5652a522a65b0d61ed0bba0 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Sat, 3 Oct 2026 09:50:36 -0500 Subject: [PATCH] perf: speed up G-code export by 4-17% via parallel overhang precompute (#16050) Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> --- src/libslic3r/AABBTreeLines.hpp | 2 +- src/libslic3r/ExtrusionEntityCollection.hpp | 17 ++ src/libslic3r/GCode.cpp | 189 ++++++++++++------- src/libslic3r/GCode.hpp | 4 + src/libslic3r/GCode/ExtrusionProcessor.hpp | 150 ++++++++++++--- tests/fff_print/test_extrusion_processor.cpp | 184 ++++++++++++++++++ 6 files changed, 453 insertions(+), 93 deletions(-) diff --git a/src/libslic3r/AABBTreeLines.hpp b/src/libslic3r/AABBTreeLines.hpp index de0c88aeb1..957627ab9b 100644 --- a/src/libslic3r/AABBTreeLines.hpp +++ b/src/libslic3r/AABBTreeLines.hpp @@ -358,7 +358,7 @@ namespace AABBTreeLines { return dist; } - std::vector all_lines_in_radius(const Vec& point, Floating radius) + std::vector all_lines_in_radius(const Vec& point, Floating radius) const { return AABBTreeLines::all_lines_in_radius(this->lines, this->tree, point.template cast(), radius * radius); } diff --git a/src/libslic3r/ExtrusionEntityCollection.hpp b/src/libslic3r/ExtrusionEntityCollection.hpp index 664ad1ddcd..f79ae22aba 100644 --- a/src/libslic3r/ExtrusionEntityCollection.hpp +++ b/src/libslic3r/ExtrusionEntityCollection.hpp @@ -175,6 +175,23 @@ private: bool is_reverse{true}; }; +// Calls `f` for every ExtrusionPath in `entity`, descending into collections, loops and multi-paths. +template void for_each_extrusion_path(const ExtrusionEntity &entity, F &&f) +{ + if (auto *collection = dynamic_cast(&entity)) { + for (const ExtrusionEntity *child : collection->entities) + for_each_extrusion_path(*child, f); + } else if (auto *loop = dynamic_cast(&entity)) { + for (const ExtrusionPath &path : loop->paths) + f(path); + } else if (auto *multi_path = dynamic_cast(&entity)) { + for (const ExtrusionPath &path : multi_path->paths) + f(path); + } else if (auto *path = dynamic_cast(&entity)) { + f(*path); + } +} + } // namespace Slic3r #endif diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 2b0f254b43..7aed4b808b 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -4404,6 +4404,75 @@ size_t GCode::get_nozzle_config_index(int filament_id) const return get_extruder_id(filament_id); } +namespace { +struct PrecomputedLayer +{ + size_t index{size_t(-1)}; // size_t(-1) for the empty layer after the last + std::vector overhang_layers; +}; +} // namespace + +template static bool any_enabled(const BoolsOption &option) +{ + return std::any_of(option.values.begin(), option.values.end(), [](unsigned char enabled) { return enabled != 0; }); +} + +// Whether process_layer() prepares the overhang estimator for `layer`. +template +static bool prepares_overhang_estimator(const Layer &layer, bool overhang_fan, OverhangSpeed overhang_speed) +{ + const LayerRegionPtrs ®ions = layer.regions(); + return std::any_of(regions.begin(), regions.end(), [overhang_fan, &overhang_speed](const LayerRegion *region) { + return region->has_extrusions() && (overhang_fan || overhang_speed(*region)); + }); +} + +std::vector precompute_overhang_layers(const std::vector &layers, bool overhang_fan) +{ + // Any filament may print the layer, so a region's overhang speed counts if it is enabled for any. + auto overhang_speed = [](const LayerRegion ®ion) { return any_enabled(region.region().config().enable_overhang_speed); }; + std::vector out; + for (const GCode::LayerToPrint &layer : layers) + if (layer.object_layer != nullptr && layer.object_layer->lower_layer != nullptr && + prepares_overhang_estimator(*layer.object_layer, overhang_fan, overhang_speed)) { + const LayerRegionPtrs ®ions = layer.object_layer->regions(); + const bool curled_lines = std::any_of(regions.begin(), regions.end(), [](const LayerRegion *region) { + return any_enabled(region->region().config().slowdown_for_curled_perimeters); + }); + out.push_back(precompute_overhang_layer(layer.original_object, *layer.object_layer, curled_lines)); + } + return out; +} + +// Hands out the index of each layer to process_layers(), then computes the layers' overhang data in parallel. +template +static auto precomputed_layers_source(size_t &next_index, size_t layer_count, bool nop_layer, bool overhang_fan, LayersAt layers_at) +{ + return tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, + [&next_index, layer_count, nop_layer](tbb::flow_control &fc) -> PrecomputedLayer { + if (next_index < layer_count) + return {next_index++}; + // The pressure equalizer returns one layer back, so it gets an empty layer after the last. + if (next_index == layer_count + (nop_layer ? 1 : 0)) + fc.stop(); + else + ++next_index; + return {}; + }) & + tbb::make_filter(slic3r_tbb_filtermode::parallel, + [layers_at, overhang_fan](PrecomputedLayer layer) -> PrecomputedLayer { + if (layer.index != size_t(-1)) + layer.overhang_layers = precompute_overhang_layers(layers_at(layer.index), overhang_fan); + return layer; + }); +} + +// Whether the overhang fan can switch on for any filament. +static bool overhang_fan_enabled(const PrintConfig &config, bool cooling_markers) +{ + return cooling_markers && any_enabled(config.enable_overhang_bridge_fan); +} + // Process all layers of all objects (non-sequential mode) with a parallel pipeline: // Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser // and export G-code into file. @@ -4416,29 +4485,23 @@ void GCode::process_layers( { // The pipeline is variable: The vase mode filter is optional. size_t layer_to_print_idx = 0; - const auto generator = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [this, &print, &tool_ordering, &print_object_instances_ordering, &layers_to_print, &layer_to_print_idx](tbb::flow_control& fc) -> LayerResult { - if (layer_to_print_idx >= layers_to_print.size()) { - if (layer_to_print_idx == layers_to_print.size() + (m_pressure_equalizer ? 1 : 0)) { - fc.stop(); - return {}; - } else { - // Pressure equalizer need insert empty input. Because it returns one layer back. - // Insert NOP (no operation) layer; - ++layer_to_print_idx; - return LayerResult::make_nop_layer_result(); - } - } else { - const std::pair>& layer = layers_to_print[layer_to_print_idx++]; - const LayerTools& layer_tools = tool_ordering.tools_for_layer(layer.first); - print.set_status(80, Slic3r::format(_(L("Generating G-code: layer %1%")), std::to_string(layer_to_print_idx))); - if (m_wipe_tower && layer_tools.has_wipe_tower) - m_wipe_tower->next_layer(); - //BBS - check_placeholder_parser_failed(); - print.throw_if_canceled(); - return this->process_layer(print, layer.second, layer_tools, &layer == &layers_to_print.back(), &print_object_instances_ordering, tool_ordering.get_most_used_extruder(), size_t(-1)); - } + const auto source = precomputed_layers_source(layer_to_print_idx, layers_to_print.size(), m_pressure_equalizer != nullptr, + overhang_fan_enabled(print.config(), m_enable_cooling_markers), + [&layers_to_print](size_t index) -> const std::vector & { return layers_to_print[index].second; }); + const auto generator = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, + [this, &print, &tool_ordering, &print_object_instances_ordering, &layers_to_print](PrecomputedLayer precomputed) -> LayerResult { + if (precomputed.index == size_t(-1)) + return LayerResult::make_nop_layer_result(); + const std::pair>& layer = layers_to_print[precomputed.index]; + const LayerTools& layer_tools = tool_ordering.tools_for_layer(layer.first); + print.set_status(80, Slic3r::format(_(L("Generating G-code: layer %1%")), std::to_string(precomputed.index + 1))); + if (m_wipe_tower && layer_tools.has_wipe_tower) + m_wipe_tower->next_layer(); + //BBS + check_placeholder_parser_failed(); + print.throw_if_canceled(); + m_extrusion_quality_estimator.set_precomputed_layers(std::move(precomputed.overhang_layers)); + return this->process_layer(print, layer.second, layer_tools, &layer == &layers_to_print.back(), &print_object_instances_ordering, tool_ordering.get_most_used_extruder(), size_t(-1)); }); if (m_spiral_vase) { float nozzle_diameter = EXTRUDER_CONFIG(nozzle_diameter); @@ -4496,13 +4559,15 @@ void GCode::process_layers( // The pipeline elements are joined using const references, thus no copying is performed. if (m_spiral_vase && m_pressure_equalizer) - tbb::parallel_pipeline(12, generator & spiral_mode & pressure_equalizer & cooling & fan_mover & output); + tbb::parallel_pipeline(12, source & generator & spiral_mode & pressure_equalizer & cooling & fan_mover & output); else if (m_spiral_vase) - tbb::parallel_pipeline(12, generator & spiral_mode & cooling & fan_mover & output); + tbb::parallel_pipeline(12, source & generator & spiral_mode & cooling & fan_mover & output); else if (m_pressure_equalizer) - tbb::parallel_pipeline(12, generator & pressure_equalizer & cooling & fan_mover & pa_processor_filter & output); + tbb::parallel_pipeline(12, source & generator & pressure_equalizer & cooling & fan_mover & pa_processor_filter & output); else - tbb::parallel_pipeline(12, generator & cooling & fan_mover & pa_processor_filter & output); + tbb::parallel_pipeline(12, source & generator & cooling & fan_mover & pa_processor_filter & output); + // The estimator's precomputed data points into this print's layers. + m_extrusion_quality_estimator.set_precomputed_layers({}); } @@ -4520,26 +4585,20 @@ void GCode::process_layers( { // The pipeline is variable: The vase mode filter is optional. size_t layer_to_print_idx = 0; - const auto generator = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [this, &print, &tool_ordering, &layers_to_print, &layer_to_print_idx, single_object_idx, prime_extruder](tbb::flow_control& fc) -> LayerResult { - if (layer_to_print_idx >= layers_to_print.size()) { - if (layer_to_print_idx == layers_to_print.size() + (m_pressure_equalizer ? 1 : 0)) { - fc.stop(); - return {}; - } else { - // Pressure equalizer need insert empty input. Because it returns one layer back. - // Insert NOP (no operation) layer; - ++layer_to_print_idx; - return LayerResult::make_nop_layer_result(); - } - } else { - LayerToPrint &layer = layers_to_print[layer_to_print_idx ++]; - print.set_status(80, Slic3r::format(_(L("Generating G-code: layer %1%")), std::to_string(layer_to_print_idx))); - //BBS - check_placeholder_parser_failed(); - print.throw_if_canceled(); - return this->process_layer(print, { std::move(layer) }, tool_ordering.tools_for_layer(layer.print_z()), &layer == &layers_to_print.back(), nullptr, tool_ordering.get_most_used_extruder(), single_object_idx, prime_extruder); - } + const auto source = precomputed_layers_source(layer_to_print_idx, layers_to_print.size(), m_pressure_equalizer != nullptr, + overhang_fan_enabled(print.config(), m_enable_cooling_markers), + [&layers_to_print](size_t index) { return std::vector{layers_to_print[index]}; }); + const auto generator = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, + [this, &print, &tool_ordering, &layers_to_print, single_object_idx, prime_extruder](PrecomputedLayer precomputed) -> LayerResult { + if (precomputed.index == size_t(-1)) + return LayerResult::make_nop_layer_result(); + LayerToPrint &layer = layers_to_print[precomputed.index]; + print.set_status(80, Slic3r::format(_(L("Generating G-code: layer %1%")), std::to_string(precomputed.index + 1))); + //BBS + check_placeholder_parser_failed(); + print.throw_if_canceled(); + m_extrusion_quality_estimator.set_precomputed_layers(std::move(precomputed.overhang_layers)); + return this->process_layer(print, { std::move(layer) }, tool_ordering.tools_for_layer(layer.print_z()), &layer == &layers_to_print.back(), nullptr, tool_ordering.get_most_used_extruder(), single_object_idx, prime_extruder); }); if (m_spiral_vase) { float nozzle_diameter = EXTRUDER_CONFIG(nozzle_diameter); @@ -4594,13 +4653,15 @@ void GCode::process_layers( // The pipeline elements are joined using const references, thus no copying is performed. if (m_spiral_vase && m_pressure_equalizer) - tbb::parallel_pipeline(12, generator & spiral_mode & pressure_equalizer & cooling & fan_mover & output); + tbb::parallel_pipeline(12, source & generator & spiral_mode & pressure_equalizer & cooling & fan_mover & output); else if (m_spiral_vase) - tbb::parallel_pipeline(12, generator & spiral_mode & cooling & fan_mover & output); + tbb::parallel_pipeline(12, source & generator & spiral_mode & cooling & fan_mover & output); else if (m_pressure_equalizer) - tbb::parallel_pipeline(12, generator & pressure_equalizer & cooling & fan_mover & pa_processor_filter & output); + tbb::parallel_pipeline(12, source & generator & pressure_equalizer & cooling & fan_mover & pa_processor_filter & output); else - tbb::parallel_pipeline(12, generator & cooling & fan_mover & pa_processor_filter & output); + tbb::parallel_pipeline(12, source & generator & cooling & fan_mover & pa_processor_filter & output); + // The estimator's precomputed data points into this print's layers. + m_extrusion_quality_estimator.set_precomputed_layers({}); } std::string GCode::placeholder_parser_process(const std::string &name, const std::string &templ, unsigned int current_filament_id, const DynamicConfig *config_override) @@ -5960,25 +6021,13 @@ LayerResult GCode::process_layer( return next_extruder; }; - for (const auto &layer_to_print : layers) { - if (layer_to_print.object_layer) { - const auto& regions = layer_to_print.object_layer->regions(); - const bool has_extrusions = std::any_of(regions.begin(), regions.end(), [](const LayerRegion* r) { - return r->has_extrusions(); - }); - const bool enable_overhang_speed = std::any_of(regions.begin(), regions.end(), [this](const LayerRegion* r) { - return r->has_extrusions() && r->region().config().enable_overhang_speed.get_at(get_nozzle_config_index(m_writer.filament()->id())); - }); - const bool enable_overhang_fan = m_enable_cooling_markers && has_extrusions && - std::any_of(m_config.enable_overhang_bridge_fan.values.begin(), - m_config.enable_overhang_bridge_fan.values.end(), - [](unsigned char value) { return value != 0; }); - if (enable_overhang_speed || enable_overhang_fan) { - m_extrusion_quality_estimator.prepare_for_new_layer(layer_to_print.original_object, - layer_to_print.object_layer); - } - } - } + const bool overhang_fan = overhang_fan_enabled(m_config, m_enable_cooling_markers); + auto overhang_speed = [this](const LayerRegion ®ion) { + return bool(region.region().config().enable_overhang_speed.get_at(get_nozzle_config_index(m_writer.filament()->id()))); + }; + for (const auto &layer_to_print : layers) + if (layer_to_print.object_layer && prepares_overhang_estimator(*layer_to_print.object_layer, overhang_fan, overhang_speed)) + m_extrusion_quality_estimator.prepare_for_new_layer(layer_to_print.original_object, layer_to_print.object_layer); // Group extrusions by an extruder, then by an object, an island and a region. std::map> by_extruder; diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 0172fcf8c0..6b050a4739 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -864,6 +864,10 @@ private: std::vector sort_object_instances_by_model_order(const Print& print, bool init_order = false); +// The overhang data ExtrusionQualityEstimator needs for the object layers in `layers`, computed ahead of the generator; +// `overhang_fan` says whether the overhang fan can switch on for any filament. +std::vector precompute_overhang_layers(const std::vector &layers, bool overhang_fan); + } #endif diff --git a/src/libslic3r/GCode/ExtrusionProcessor.hpp b/src/libslic3r/GCode/ExtrusionProcessor.hpp index 7cc886bfe9..6abf09fe14 100644 --- a/src/libslic3r/GCode/ExtrusionProcessor.hpp +++ b/src/libslic3r/GCode/ExtrusionProcessor.hpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -38,7 +39,14 @@ template struct ExtendedPoint float curvature; }; -template +// A KNOWN_DISTANCES functor that knows no distances, so every input point is queried. +struct NoKnownDistances +{ + template const double *operator()(const P &) const { return nullptr; } +}; + +template std::vector> estimate_points_properties(const POINTS& input_points, const AABBTreeLines::LinesDistancer& unscaled_prev_layer, float flow_width, @@ -49,7 +57,9 @@ std::vector> estimate_points_properties(const POINTS& const std::function& distance_to_speed = {}, // Overlap (1 - distance / flow_width) at or below which the overhang // fan switches on; negative when the fan does not depend on overlap. - float fan_overlap_threshold = -1.0f) + float fan_overlap_threshold = -1.0f, + // Returns an input point's signed distance if already known, else nullptr. + const KNOWN_DISTANCES& known_distance = KNOWN_DISTANCES{}) { bool looped = input_points.front() == input_points.back(); std::function get_prev_index = [](size_t idx, size_t count) { @@ -94,21 +104,26 @@ std::vector> estimate_points_properties(const POINTS& float boundary_offset = PREV_LAYER_BOUNDARY_OFFSET ? 0.5 * flow_width : 0.0f; auto maybe_unscale = [](const P& p) -> Vec { return SCALED_INPUT ? unscaled(p) : p.template cast(); }; + using Distance = typename AABBTreeLines::LinesDistancer::Floating; + auto input_distance = [&unscaled_prev_layer, &known_distance](const P &input, const Vec &position) -> Distance { + if (const double *known = known_distance(input)) + return Distance(*known); + auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra( + position.template cast()); + return distance; + }; + std::vector> points; points.reserve(input_points.size() * (ADD_INTERSECTIONS ? 1.5 : 1)); { ExtendedPoint start_point{maybe_unscale(input_points.front())}; - auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra( - start_point.position.template cast()); - start_point.distance = distance + boundary_offset; + start_point.distance = input_distance(input_points.front(), start_point.position) + boundary_offset; points.push_back(start_point); } for (size_t i = 1; i < input_points.size(); i++) { ExtendedPoint next_point{maybe_unscale(input_points[i])}; - auto [distance, nearest_line, - x] = unscaled_prev_layer.template distance_from_lines_extra(next_point.position.template cast()); - next_point.distance = distance + boundary_offset; + next_point.distance = input_distance(input_points[i], next_point.position) + boundary_offset; // Intersection handling if (ADD_INTERSECTIONS && @@ -349,6 +364,9 @@ std::vector> estimate_points_properties(const POINTS& points = std::move(new_points); } + if constexpr (!CURVATURE) + return points; + // Curvature calculation float accumulated_distance = 0; std::vector distances_for_curvature(points.size()); @@ -417,6 +435,49 @@ std::vector> estimate_points_properties(const POINTS& return points; } +struct Point3Hash +{ + size_t operator()(const Vec3crd &pt) const noexcept { return size_t(((89 * 31 + int64_t(pt.x())) * 31 + pt.y()) * 31 + pt.z()); } +}; + +// The trees of the layer below an object layer, and the signed distances from the layer's perimeter and bridge +// vertices to that layer's outline, computed for ExtrusionQualityEstimator ahead of the G-code generator. +struct PrecomputedOverhangLayer +{ + const PrintObject *object{nullptr}; + const Layer *layer{nullptr}; + std::shared_ptr> lower_boundaries; + std::shared_ptr> lower_curled_lines; + std::unordered_map distances; +}; + +// `layer` must have a layer below it; leave out `curled_lines` only when no region of `layer` slows down for curled +// perimeters. +inline PrecomputedOverhangLayer precompute_overhang_layer(const PrintObject *object, const Layer &layer, bool curled_lines = true) +{ + PrecomputedOverhangLayer out{object, &layer, + std::make_shared>(to_unscaled_linesf3(layer.lower_layer->lslices)), + curled_lines ? std::make_shared>(layer.lower_layer->curled_lines) : + nullptr, + {}}; + const AABBTreeLines::LinesDistancer &lower = *out.lower_boundaries; + auto add_path = [&out, &lower](const ExtrusionPath &path) { + if (!is_bridge(path.role()) && !is_perimeter(path.role())) + return; + for (const Point3 &point : path.polyline.points) + if (auto [it, inserted] = out.distances.try_emplace(point, 0.); inserted) { + const Eigen::Matrix position = unscaled(point); + auto [distance, nearest_line, x] = lower.distance_from_lines_extra(position.cast()); + it->second = distance; + } + }; + for (const LayerRegion *region : layer.regions()) { + for_each_extrusion_path(region->perimeters, add_path); + for_each_extrusion_path(region->fills, add_path); + } + return out; +} + struct ProcessedPoint { Point3 p; @@ -426,23 +487,53 @@ struct ProcessedPoint class ExtrusionQualityEstimator { - std::unordered_map> prev_layer_boundaries; - std::unordered_map> next_layer_boundaries; - std::unordered_map> prev_curled_extrusions; - std::unordered_map> next_curled_extrusions; - const PrintObject *current_object; + using Boundaries = AABBTreeLines::LinesDistancer; + using CurledLines = AABBTreeLines::LinesDistancer; + std::unordered_map> prev_layer_boundaries; + std::unordered_map> prev_curled_extrusions; + // The layers the trees above are built from, and the layers prepared last. + std::unordered_map prev_layer_sources; + std::unordered_map last_prepared_layers; + std::vector precomputed_layers; + const PrintObject *current_object; + + const PrecomputedOverhangLayer *precomputed_for(const PrintObject *object) const + { + auto it = std::find_if(precomputed_layers.begin(), precomputed_layers.end(), + [object](const PrecomputedOverhangLayer &layer) { return layer.object == object; }); + return it == precomputed_layers.end() ? nullptr : &*it; + } + + template static const T &or_empty(const std::shared_ptr &tree) + { + static const T empty; + return tree ? *tree : empty; + } public: void set_current_object(const PrintObject *object) { current_object = object; } + // Takes the data computed ahead for the layer about to be generated, replacing the previous layer's. + void set_precomputed_layers(std::vector &&layers) { precomputed_layers = std::move(layers); } + + // Measures the layer against the layer prepared before it. void prepare_for_new_layer(const PrintObject * obj, const Layer *layer) { if (layer == nullptr) return; const PrintObject *object = obj; - prev_layer_boundaries[object] = next_layer_boundaries[object]; - next_layer_boundaries[object] = AABBTreeLines::LinesDistancer{to_unscaled_linesf3(layer->lslices)}; - prev_curled_extrusions[object] = next_curled_extrusions[object]; - next_curled_extrusions[object] = AABBTreeLines::LinesDistancer{layer->curled_lines}; + const Layer *prev = std::exchange(last_prepared_layers[object], layer); + prev_layer_sources[object] = prev; + const PrecomputedOverhangLayer *precomputed = precomputed_for(object); + if (prev == nullptr) { + prev_layer_boundaries[object] = nullptr; + prev_curled_extrusions[object] = nullptr; + } else if (precomputed != nullptr && precomputed->layer == layer && layer->lower_layer == prev) { + prev_layer_boundaries[object] = precomputed->lower_boundaries; + prev_curled_extrusions[object] = precomputed->lower_curled_lines; + } else { + prev_layer_boundaries[object] = std::make_shared(to_unscaled_linesf3(prev->lslices)); + prev_curled_extrusions[object] = std::make_shared(prev->curled_lines); + } } std::vector estimate_extrusion_quality(const ExtrusionPath &path, @@ -526,9 +617,24 @@ public: return std::min(calculate_speed(distance), original_speed); }; + // Precomputed distances hold only if they were measured against the layer prev_layer_boundaries is built from. + const std::unordered_map *known = nullptr; + if (const PrecomputedOverhangLayer *precomputed = precomputed_for(current_object); + precomputed != nullptr && precomputed->layer->lower_layer == prev_layer_sources[current_object]) + known = &precomputed->distances; + const Boundaries &prev_boundaries = or_empty(prev_layer_boundaries[current_object]); + const CurledLines &prev_curled = or_empty(prev_curled_extrusions[current_object]); + auto known_distance = [known](const Point3 &point) -> const double * { + if (known == nullptr) + return nullptr; + auto it = known->find(point); + return it == known->end() ? nullptr : &it->second; + }; + std::vector> extended_points = - estimate_points_properties(path.polyline.points, prev_layer_boundaries[current_object], path.width, -1, - smallest_distance_with_lower_speed, effective_speed, fan_overlap_threshold); + estimate_points_properties(path.polyline.points, prev_boundaries, path.width, -1, + smallest_distance_with_lower_speed, effective_speed, fan_overlap_threshold, + known_distance); const auto width_inv = 1.0f / path.width; std::vector processed_points; processed_points.reserve(extended_points.size()); @@ -542,7 +648,7 @@ public: const double dist_limit = 10.0 * path.width; { Vec3d middle = 0.5 * (curr.position + next.position); - auto line_indices = prev_curled_extrusions[current_object].all_lines_in_radius(Point::new_scale(middle), + auto line_indices = prev_curled.all_lines_in_radius(Point::new_scale(middle), scale_(dist_limit)); if (!line_indices.empty()) { double len = (next.position - curr.position).norm(); @@ -563,7 +669,7 @@ public: double projected_lengths_sum = 0; for (size_t idx : line_indices) { - const CurledLine& line = prev_curled_extrusions[current_object].get_line(idx); + const CurledLine& line = prev_curled.get_line(idx); Lines inside = intersection_ln({{line.a, line.b}}, {box_of_influence}); if (inside.empty()) continue; @@ -576,7 +682,7 @@ public: } for (size_t idx : line_indices) { - const CurledLine &line = prev_curled_extrusions[current_object].get_line(idx); + const CurledLine &line = prev_curled.get_line(idx); float distance_from_curled = unscaled(line_alg::distance_to(line, Point::new_scale(middle))); float dist = path.width * (1.0 - (distance_from_curled / dist_limit)) * (1.0 - (distance_from_curled / dist_limit)) * diff --git a/tests/fff_print/test_extrusion_processor.cpp b/tests/fff_print/test_extrusion_processor.cpp index a3499a71b5..1f55c1b5c1 100644 --- a/tests/fff_print/test_extrusion_processor.cpp +++ b/tests/fff_print/test_extrusion_processor.cpp @@ -7,6 +7,7 @@ #include #include #include "libslic3r/AABBTreeLines.hpp" +#include "libslic3r/GCode.hpp" #include "libslic3r/GCode/ExtrusionProcessor.hpp" #include "libslic3r/GCodeReader.hpp" #include "libslic3r/TriangleMesh.hpp" @@ -15,6 +16,8 @@ #include #include +#include +#include #include #include "libslic3r/Line.hpp" #include "libslic3r/Point.hpp" @@ -538,6 +541,187 @@ TEST_CASE("A wall is left whole where neither its speed nor its cooling changes" REQUIRE(points.size() == 3); } +namespace { + +// The caged overhang box, sliced, and a layer on its slope. +struct SlicedCage +{ + Print print; + Model model; + const PrintObject *object{nullptr}; + const Layer *layer{nullptr}; + + explicit SlicedCage(const DynamicPrintConfig &config = caged_overhang_config("classic")) + { + init_print(std::vector{caged_overhang_mesh()}, print, model, config, nullptr, false); + print.process(); + object = print.objects().front(); + layer = object->get_layer(int(std::lround((caged_slope_z_min + caged_slope_z_max) / 2. / caged_layer_height))); + } +}; + +using Walls = std::vector>; + +// Estimates every wall of `layer` against whatever layer `estimator` was last prepared with before it. +Walls estimate_walls(ExtrusionQualityEstimator &estimator, const PrintObject *object, const Layer &layer) +{ + const ConfigOptionPercents overlaps({90, 75, 50, 25, 13, 0}); + const ConfigOptionFloatsOrPercents speeds({FloatOrPercent{100, true}, FloatOrPercent{50, true}, FloatOrPercent{30, true}, + FloatOrPercent{20, true}, FloatOrPercent{10, true}, FloatOrPercent{5, true}}); + Walls walls; + estimator.set_current_object(object); + for (const LayerRegion *region : layer.regions()) + for_each_extrusion_path(region->perimeters, [&](const ExtrusionPath &path) { + if (is_perimeter(path.role())) + walls.push_back(estimator.estimate_extrusion_quality(path, overlaps, speeds, caged_outer_wall_speed, caged_outer_wall_speed, + true, 0.5f)); + }); + return walls; +} + +uint32_t float_bits(float value) +{ + uint32_t bits; + std::memcpy(&bits, &value, sizeof(bits)); + return bits; +} + +bool same_point(const ProcessedPoint &a, const ProcessedPoint &b) +{ + return a.p == b.p && float_bits(a.speed) == float_bits(b.speed) && float_bits(a.overlap) == float_bits(b.overlap); +} + +// Requires the walls to match point for point, bit for bit. +void check_identical(const Walls &actual, const Walls &expected) +{ + REQUIRE(actual.size() == expected.size()); + for (size_t wall = 0; wall < actual.size(); ++wall) { + INFO("wall " << wall); + REQUIRE(actual[wall].size() == expected[wall].size()); + for (size_t i = 0; i < actual[wall].size(); ++i) { + const ProcessedPoint &a = actual[wall][i]; + const ProcessedPoint &e = expected[wall][i]; + INFO("point " << i << ": speed " << a.speed << " vs " << e.speed << ", overlap " << a.overlap << " vs " << e.overlap); + CHECK(a.p == e.p); + CHECK(float_bits(a.speed) == float_bits(e.speed)); + CHECK(float_bits(a.overlap) == float_bits(e.overlap)); + } + } +} + +bool any_difference(const Walls &a, const Walls &b) +{ + return !std::equal(a.begin(), a.end(), b.begin(), b.end(), [](const std::vector &wa, const std::vector &wb) { + return std::equal(wa.begin(), wa.end(), wb.begin(), wb.end(), same_point); + }); +} + +bool any_slowed(const Walls &walls) +{ + return std::any_of(walls.begin(), walls.end(), [](const std::vector &wall) { + return std::any_of(wall.begin(), wall.end(), [](const ProcessedPoint &point) { return point.speed < caged_outer_wall_speed; }); + }); +} + +} // namespace + +TEST_CASE("Overhang data computed ahead of the generator gives the same wall speeds", "[ExtrusionProcessor]") +{ + const SlicedCage cage; + REQUIRE(cage.layer->lower_layer != nullptr); + + ExtrusionQualityEstimator queried; + queried.prepare_for_new_layer(cage.object, cage.layer->lower_layer); + queried.prepare_for_new_layer(cage.object, cage.layer); + const Walls expected = estimate_walls(queried, cage.object, *cage.layer); + REQUIRE(any_slowed(expected)); + + ExtrusionQualityEstimator precomputed; + precomputed.prepare_for_new_layer(cage.object, cage.layer->lower_layer); + precomputed.set_precomputed_layers({precompute_overhang_layer(cage.object, *cage.layer)}); + precomputed.prepare_for_new_layer(cage.object, cage.layer); + check_identical(estimate_walls(precomputed, cage.object, *cage.layer), expected); +} + +TEST_CASE("Overhang distances measured against another layer than the previous one are not used", "[ExtrusionProcessor]") +{ + const SlicedCage cage; + const Layer *two_below = cage.layer->lower_layer->lower_layer; + REQUIRE(two_below != nullptr); + + ExtrusionQualityEstimator queried; + queried.prepare_for_new_layer(cage.object, two_below); + queried.prepare_for_new_layer(cage.object, cage.layer); + const Walls expected = estimate_walls(queried, cage.object, *cage.layer); + ExtrusionQualityEstimator one_below; + one_below.prepare_for_new_layer(cage.object, cage.layer->lower_layer); + one_below.prepare_for_new_layer(cage.object, cage.layer); + REQUIRE(any_difference(estimate_walls(one_below, cage.object, *cage.layer), expected)); + + ExtrusionQualityEstimator precomputed; + precomputed.prepare_for_new_layer(cage.object, two_below); + precomputed.set_precomputed_layers({precompute_overhang_layer(cage.object, *cage.layer)}); + precomputed.prepare_for_new_layer(cage.object, cage.layer); + check_identical(estimate_walls(precomputed, cage.object, *cage.layer), expected); +} + +TEST_CASE("Overhang data computed for another layer is not used", "[ExtrusionProcessor]") +{ + const SlicedCage cage; + const Layer *one_below = cage.layer->lower_layer; + REQUIRE(one_below != nullptr); + REQUIRE(one_below->lower_layer != nullptr); + + ExtrusionQualityEstimator queried; + queried.prepare_for_new_layer(cage.object, one_below); + queried.prepare_for_new_layer(cage.object, cage.layer); + const Walls expected = estimate_walls(queried, cage.object, *cage.layer); + ExtrusionQualityEstimator two_below; + two_below.prepare_for_new_layer(cage.object, one_below->lower_layer); + two_below.prepare_for_new_layer(cage.object, cage.layer); + REQUIRE(any_difference(estimate_walls(two_below, cage.object, *cage.layer), expected)); + + ExtrusionQualityEstimator precomputed; + precomputed.set_precomputed_layers({precompute_overhang_layer(cage.object, *one_below)}); + precomputed.prepare_for_new_layer(cage.object, one_below); + precomputed.prepare_for_new_layer(cage.object, cage.layer); + check_identical(estimate_walls(precomputed, cage.object, *cage.layer), expected); +} + +TEST_CASE("Precomputed overhang data has the curled-line tree exactly when a region slows down for curled perimeters", "[ExtrusionProcessor]") +{ + const bool slowdown = GENERATE(false, true); + DynamicPrintConfig config = caged_overhang_config("classic"); + config.set_deserialize_strict("slowdown_for_curled_perimeters", slowdown ? "1" : "0"); + const SlicedCage cage(config); + + GCode::LayerToPrint layer; + layer.object_layer = cage.layer; + layer.original_object = cage.object; + const std::vector precomputed = precompute_overhang_layers({layer}, false); + REQUIRE(precomputed.size() == 1); + CHECK((precomputed.front().lower_curled_lines != nullptr) == slowdown); +} + +TEST_CASE("Caged external overhangs are slowed when printed by object or through the pressure equalizer", "[ExtrusionProcessor]") +{ + const auto [key, value] = GENERATE(table({ + {"print_sequence", "by object"}, + {"max_volumetric_extrusion_rate_slope", "10"}, + })); + INFO(key << " = " << value); + DynamicPrintConfig config = caged_overhang_config("classic"); + config.set_deserialize_strict(key, value); + Print print; + Model model; + init_print(std::vector{caged_overhang_mesh()}, print, model, config, nullptr, false); + + const std::vector feed_rates = caged_slope_feed_rates(gcode(print)); + info_feed_rates("caged slope", feed_rates); + REQUIRE_FALSE(feed_rates.empty()); + REQUIRE(*std::max_element(feed_rates.begin(), feed_rates.end()) < caged_slow_speed * MM_PER_MIN); +} + TEST_CASE("Benchmark caged overhang interior sampling", "[ExtrusionProcessor][!benchmark]"){ const char* wall_generator = GENERATE("classic", "arachne");