From eb30ea1eb8d510175bfd3d97e489e5de458732a8 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Fri, 2 Oct 2026 17:34:47 -0500 Subject: [PATCH] perf: speed up G-code export by 3-9% via cached config lookups (#16028) --- src/libslic3r/GCode.cpp | 58 +++++++++-------- src/libslic3r/GCode.hpp | 20 ++++++ src/libslic3r/Print.cpp | 12 ++-- src/libslic3r/Print.hpp | 11 +++- src/libslic3r/PrintApply.cpp | 2 +- tests/fff_print/test_gcodewriter.cpp | 62 +++++++++++++++++++ .../test_toolordering_nozzle_group.cpp | 16 +++++ 7 files changed, 148 insertions(+), 33 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 4c02cae7b4..25addff79d 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -4334,9 +4334,9 @@ size_t GCode::get_extruder_id(unsigned int filament_id) const size_t GCode::get_filament_config_index(int filament_id) const { - if (m_print) { - return m_print->get_filament_config_indx(filament_id, m_cur_layer_idx); - } + if (m_print) + return m_filament_index_cache.get(filament_id, m_cur_layer_idx, m_print->config_index_generation(), + [&] { return m_print->get_filament_config_indx(filament_id, m_cur_layer_idx); }); // Orca: without a Print the filament-indexed arrays are unexpanded, so the // filament id itself is the only meaningful column. return filament_id; @@ -4350,9 +4350,9 @@ size_t GCode::get_filament_config_index(int filament_id, size_t layer_id) const size_t GCode::get_nozzle_config_index(int filament_id) const { - if (m_print) { - return m_print->get_nozzle_config_index(filament_id, m_cur_layer_idx); - } + if (m_print) + return m_nozzle_index_cache.get(filament_id, m_cur_layer_idx, m_print->config_index_generation(), + [&] { return m_print->get_nozzle_config_index(filament_id, m_cur_layer_idx); }); // Orca: same reasoning; degenerate to the filament's extruder column. return get_extruder_id(filament_id); } @@ -8059,28 +8059,36 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, unsigned int acceleration_i = 0; double jerk = 0; // adjust acceleration - if (NOZZLE_CONFIG(default_acceleration) > 0) { + const size_t nozzle = get_nozzle_config_index(m_writer.filament()->id()); + if (m_config.default_acceleration.get_at(nozzle) > 0) { + const ExtrusionRole role = path.role(); + const double bridge_acceleration = is_bridge(role) ? + m_config.bridge_acceleration.get_at(nozzle).get_abs_value(m_config.outer_wall_acceleration.get_at(nozzle)) : 0.; + const double sparse_infill_acceleration = role == erInternalInfill ? + m_config.sparse_infill_acceleration.get_at(nozzle).get_abs_value(m_config.default_acceleration.get_at(nozzle)) : 0.; + const double internal_solid_infill_acceleration = role == erSolidInfill ? + m_config.internal_solid_infill_acceleration.get_at(nozzle).get_abs_value(m_config.default_acceleration.get_at(nozzle)) : 0.; double acceleration; - if (this->on_first_layer() && NOZZLE_CONFIG(initial_layer_acceleration) > 0) { - acceleration = NOZZLE_CONFIG(initial_layer_acceleration); + if (this->on_first_layer() && m_config.initial_layer_acceleration.get_at(nozzle) > 0) { + acceleration = m_config.initial_layer_acceleration.get_at(nozzle); #if 0 } else if (this->object_layer_over_raft() && m_config.first_layer_acceleration_over_raft.value > 0) { acceleration = m_config.first_layer_acceleration_over_raft.value; #endif - } else if (m_config.get_abs_value_at("bridge_acceleration", get_nozzle_config_index(m_writer.filament()->id())) > 0 && is_bridge(path.role())) { - acceleration = m_config.get_abs_value_at("bridge_acceleration", get_nozzle_config_index(m_writer.filament()->id())); - } else if (m_config.get_abs_value_at("sparse_infill_acceleration", get_nozzle_config_index(m_writer.filament()->id())) > 0 && (path.role() == erInternalInfill)) { - acceleration = m_config.get_abs_value_at("sparse_infill_acceleration", get_nozzle_config_index(m_writer.filament()->id())); - } else if (m_config.get_abs_value_at("internal_solid_infill_acceleration", get_nozzle_config_index(m_writer.filament()->id())) > 0 && (path.role() == erSolidInfill)) { - acceleration = m_config.get_abs_value_at("internal_solid_infill_acceleration", get_nozzle_config_index(m_writer.filament()->id())); - } else if (NOZZLE_CONFIG(outer_wall_acceleration) > 0 && is_external_perimeter(path.role())) { - acceleration = NOZZLE_CONFIG(outer_wall_acceleration); - } else if (NOZZLE_CONFIG(inner_wall_acceleration) > 0 && is_internal_perimeter(path.role())) { - acceleration = NOZZLE_CONFIG(inner_wall_acceleration); - } else if (NOZZLE_CONFIG(top_surface_acceleration) > 0 && is_top_surface(path.role())) { - acceleration = NOZZLE_CONFIG(top_surface_acceleration); + } else if (bridge_acceleration > 0) { + acceleration = bridge_acceleration; + } else if (sparse_infill_acceleration > 0) { + acceleration = sparse_infill_acceleration; + } else if (internal_solid_infill_acceleration > 0) { + acceleration = internal_solid_infill_acceleration; + } else if (m_config.outer_wall_acceleration.get_at(nozzle) > 0 && is_external_perimeter(role)) { + acceleration = m_config.outer_wall_acceleration.get_at(nozzle); + } else if (m_config.inner_wall_acceleration.get_at(nozzle) > 0 && is_internal_perimeter(role)) { + acceleration = m_config.inner_wall_acceleration.get_at(nozzle); + } else if (m_config.top_surface_acceleration.get_at(nozzle) > 0 && is_top_surface(role)) { + acceleration = m_config.top_surface_acceleration.get_at(nozzle); } else { - acceleration = NOZZLE_CONFIG(default_acceleration); + acceleration = m_config.default_acceleration.get_at(nozzle); } acceleration_i = (unsigned int)floor(acceleration + 0.5); } @@ -8181,7 +8189,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, speed = std::min(speed, m_config.scarf_joint_speed.get_abs_value(speed)); } } else if(path.role() == erInternalBridgeInfill) { - speed = m_config.get_abs_value_at("internal_bridge_speed", get_nozzle_config_index(m_writer.filament()->id())); + speed = m_config.internal_bridge_speed.get_at(nozzle).get_abs_value(m_config.bridge_speed.get_at(nozzle)); } else if (path.role() == erOverhangPerimeter || path.role() == erSupportTransition || path.role() == erBridgeInfill) { speed = NOZZLE_CONFIG(bridge_speed); } else if (path.role() == erInternalInfill) { @@ -8193,7 +8201,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } else if (path.role() == erIroning) { const size_t filament_idx = get_filament_config_index(m_writer.filament()->id()); speed = m_config.filament_ironing_speed.is_nil(filament_idx) - ? m_config.get_abs_value("ironing_speed") + ? m_config.ironing_speed.value : m_config.filament_ironing_speed.get_at(filament_idx); } else if (path.role() == erBottomSurface) { speed = NOZZLE_CONFIG(initial_layer_infill_speed); @@ -8254,7 +8262,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } // Override skirt speed if set if (path.role() == erSkirt) { - const double skirt_speed = m_config.get_abs_value("skirt_speed"); + const double skirt_speed = m_config.skirt_speed.value; if (skirt_speed > 0.0) speed = skirt_speed; } diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 29a8ae5a34..7567bc6e0d 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -301,6 +301,24 @@ public: size_t get_filament_config_index(int filament_id, size_t layer_id) const; size_t get_nozzle_config_index(int filament_id) const; + // Holds the last slot a resolver returned without locking, so only the G-code generator may + // call the resolvers. + struct ConfigIndexCache + { + bool valid{false}; + int filament_id{0}; + size_t layer_idx{0}; + size_t generation{0}; + size_t index{0}; + + template size_t get(int filament, size_t layer, size_t gen, Lookup &&lookup) + { + if (!valid || filament_id != filament || layer_idx != layer || generation != gen) + *this = {true, filament, layer, gen, size_t(lookup())}; + return index; + } + }; + // Object and support extrusions of the same PrintObject at the same print_z. // public, so that it could be accessed by free helper functions from GCode.cpp struct LayerToPrint @@ -784,6 +802,8 @@ private: // Object layer id of the layer being generated; keys the per-filament config-slot // resolvers. Distinct from m_layer_index (an export progress counter starting at -1). size_t m_cur_layer_idx{0}; + mutable ConfigIndexCache m_filament_index_cache; + mutable ConfigIndexCache m_nozzle_index_cache; std::set m_initial_layer_extruders; std::vector> m_sorted_layer_filaments; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 6d60874c18..c3d157e1e6 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -4298,10 +4298,11 @@ Polygons Print::get_extruder_shared_printable_polygon() const return shared_printable_polys; } -// Narrow the stored grouping result to the layer-aware type the slicing pipeline uses. -std::shared_ptr Print::get_layered_nozzle_group_result() const +void Print::set_nozzle_group_result(std::shared_ptr result) { - return std::dynamic_pointer_cast(m_nozzle_group_result); + m_nozzle_group_result = std::move(result); + m_layered_nozzle_group_result = std::dynamic_pointer_cast(m_nozzle_group_result); + ++m_config_index_generation; } // Dynamic (per-layer selector) regroup predicate. @@ -4337,6 +4338,7 @@ int Print::get_filament_config_indx(int filament_id, int layer_id, bool use_cach void Print::update_filament_self_index_cache() { m_missing_nozzle_group_logged.clear(); // reset the per-slice get_config_index log dedupe + ++m_config_index_generation; std::vector values; if (m_full_print_config.has("filament_self_index")) { @@ -4376,7 +4378,7 @@ int Print::get_nozzle_config_index(int filament_id, int layer_id) int Print::get_config_index(int filament_id, int layer_id, const std::vector &variant_list, const std::vector& self_index_list, FilamentIndexMap *index_map) { - auto group_result = get_layered_nozzle_group_result(); + const MultiNozzleUtils::LayeredNozzleGroupResult *group_result = m_layered_nozzle_group_result.get(); // Orca: defensive — when no grouping producer has published a result yet, fall back to the // static identity: one filament-variant column per filament. if (!group_result) @@ -4411,7 +4413,7 @@ int Print::get_config_index(int filament_id, int layer_id, const std::vector &variant_list, const std::vector& self_index_list, PrintIndexMap &index_map) { - auto group_result = get_layered_nozzle_group_result(); + const MultiNozzleUtils::LayeredNozzleGroupResult *group_result = m_layered_nozzle_group_result.get(); // Orca: same static fallback as the filament overload; the slot degenerates to the filament's // extruder column (filament_map is 1 based, get_extruder_id guards the filament id range). if (!group_result) diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index f185962b1e..13562b4355 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -1073,9 +1073,9 @@ public: // Logical (extruder, nozzle) grouping result produced by ToolOrdering during reorder. // Consumed by GCode via get_layered_nozzle_group_result()->get_nozzle_id(filament, layer) etc. - void set_nozzle_group_result(std::shared_ptr result) { m_nozzle_group_result = result; } + void set_nozzle_group_result(std::shared_ptr result); std::shared_ptr get_nozzle_group_result() const { return m_nozzle_group_result; } - std::shared_ptr get_layered_nozzle_group_result() const; + std::shared_ptr get_layered_nozzle_group_result() const { return m_layered_nozzle_group_result; } // True only when the project opts into the per-layer filament selector // (enable_filament_dynamic_map) in auto-for-flush mode on a multi-extruder machine. Gates the @@ -1226,6 +1226,9 @@ public: // pipeline's cooling stage, which runs concurrently with the generator stage filling it. int get_filament_config_indx(int filament_id, int layer_id, bool use_cache = true); int get_nozzle_config_index(int filament_id, int layer_id); + // Changes with the grouping result and the filament maps, so a caller may reuse a resolved slot + // until it changes. + size_t config_index_generation() const { return m_config_index_generation; } // Orca: Implement prusa's filament shrink compensation approach // Returns if all used filaments have same shrinkage compensations. @@ -1352,6 +1355,9 @@ private: // Logical (extruder, nozzle) grouping result, set by ToolOrdering during reorder. std::shared_ptr m_nozzle_group_result; + // m_nozzle_group_result narrowed to the layer-aware type; only set_nozzle_group_result() assigns + // either. + std::shared_ptr m_layered_nozzle_group_result; // Sequential (by-object) selector plans, keyed by object; see sequential_dynamic_orderings(). // Rebuilt (or cleared) on every process(). @@ -1361,6 +1367,7 @@ private: FilamentIndexMap m_filament_index_map; // Used to cache printer and process parameter information PrintIndexMap m_nozzle_index_map; + size_t m_config_index_generation{0}; // Orca: filament ids already reported as missing a nozzle-group entry this slice. get_config_index() // falls back per-filament/per-layer in the g-code hot path, so this dedupes its log to once per // filament instead of flooding thousands of identical error lines. Cleared with the caches each slice. diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 21e2251b65..9b794d2787 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1309,7 +1309,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ // Reproduce that exact expansion here so an unchanged config diffs empty — the expanded // keys invalidate the wipe tower / g-code export, and the placeholder parser aliases // the full config — instead of trimming back to one slot per filament. - auto group_result = std::dynamic_pointer_cast(this->get_nozzle_group_result()); + auto group_result = this->get_layered_nozzle_group_result(); std::unordered_map> filament_variant_uses; if (group_result && group_result->is_support_dynamic_nozzle_map() && collect_filament_variant_uses(*group_result, m_ori_full_print_config, filament_variant_uses)) diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index f371fcb7f4..d3a0722fc1 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -4,18 +4,21 @@ #include #include #include +#include #include #include #include #include "nlohmann/json.hpp" +#include "libslic3r/GCodeReader.hpp" #include "libslic3r/GCodeWriter.hpp" #include "libslic3r/GCode.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/Print.hpp" #include "libslic3r/ModelArrange.hpp" +#include #include #include "test_helpers.hpp" @@ -97,6 +100,22 @@ SCENARIO("Origin manipulation", "[GCodeWriter]") { } } +TEST_CASE("A cached config slot is looked up again whenever its key changes", "[GCodeWriter]") +{ + GCode::ConfigIndexCache cache; + int lookups = 0; + auto slot = [&](int filament, size_t layer, size_t generation) { + return cache.get(filament, layer, generation, [&] { ++lookups; return filament * 100 + int(layer) * 10 + int(generation); }); + }; + REQUIRE(slot(1, 2, 3) == 123); + REQUIRE(slot(1, 2, 3) == 123); + REQUIRE(lookups == 1); + REQUIRE(slot(4, 2, 3) == 423); + REQUIRE(slot(4, 5, 3) == 453); + REQUIRE(slot(4, 5, 6) == 456); + REQUIRE(lookups == 4); +} + // Verify that emit_machine_limits_to_gcode emits the correct max value across // used extruders (regression for commit b4ee665: "Emit max value of machine // limit among used extruders"). @@ -872,3 +891,46 @@ TEST_CASE("Custom G-code motion limits are restored before generated moves", "[G REQUIRE(gcode.find("M204 S6000 ; adjust acceleration", custom_gcode_pos) != std::string::npos); REQUIRE(gcode.find("M205 X8 Y8 ; adjust jerk", custom_gcode_pos) != std::string::npos); } + +TEST_CASE("Percent accelerations resolve against the option they are a percentage of", "[GCodeWriter]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "gcode_flavor", "marlin" }, + { "machine_max_acceleration_extruding", "20000,20000" }, + { "default_acceleration", "4000" }, + { "initial_layer_acceleration", "0" }, + { "outer_wall_acceleration", "3000" }, + { "bridge_acceleration", "50%" }, + { "sparse_infill_acceleration", "25%" }, + { "internal_solid_infill_acceleration", "60%" }, + { "sparse_infill_density", "20%" }, + }); + // get_abs_value_at() resolves each percentage through the ratio_over in the config definitions. + const std::map expected = { + { "Bridge", int(config.get_abs_value_at("bridge_acceleration", 0)) }, + { "Sparse infill", int(config.get_abs_value_at("sparse_infill_acceleration", 0)) }, + { "Internal solid infill", int(config.get_abs_value_at("internal_solid_infill_acceleration", 0)) }, + }; + REQUIRE(expected.at("Bridge") == 1500); + REQUIRE(expected.at("Sparse infill") == 1000); + REQUIRE(expected.at("Internal solid infill") == 2400); + + std::map> accelerations_by_role; + std::string role; + int acceleration = 0; + GCodeReader reader; + reader.parse_buffer(Slic3r::Test::slice({ TestMesh::bridge }, config), [&](GCodeReader &self, const GCodeReader::GCodeLine &line) { + float value; + if (boost::starts_with(line.raw(), ";TYPE:")) + role = line.raw().substr(6); + else if (line.cmd_is("M204") && line.has_value('S', value)) + acceleration = int(value); + else if (line.extruding(self) && line.dist_XY(self) > 0) + accelerations_by_role[role].insert(acceleration); + }); + for (const auto &[role_name, value] : expected) { + INFO(role_name); + REQUIRE(accelerations_by_role[role_name] == std::set{ value }); + } +} diff --git a/tests/libslic3r/test_toolordering_nozzle_group.cpp b/tests/libslic3r/test_toolordering_nozzle_group.cpp index 4074a80995..837b66287a 100644 --- a/tests/libslic3r/test_toolordering_nozzle_group.cpp +++ b/tests/libslic3r/test_toolordering_nozzle_group.cpp @@ -510,6 +510,22 @@ TEST_CASE("Print config-index resolvers pick per-filament Hybrid slots", "[Print } } +TEST_CASE("Regrouping or rewriting the filament maps changes the config-index generation", "[Print][H2C]") +{ + Model model; + model.add_object("cube", "", make_cube(20, 20, 20))->add_instance(); + Print print; + print.apply(model, DynamicPrintConfig::full_print_config()); + + size_t generation = print.config_index_generation(); + print.set_nozzle_group_result(nullptr); + REQUIRE(print.config_index_generation() != generation); + + generation = print.config_index_generation(); + print.update_filament_maps_to_config({1}, {(int) nvtStandard}, {0}); + REQUIRE(print.config_index_generation() != generation); +} + TEST_CASE("Re-applying an unchanged config after slicing keeps the result valid", "[Print][H2C]") { // apply() rebuilds m_config.filament_map_2 to the real per-filament slot map, while the