Compare commits

..
Author SHA1 Message Date
Hanif Koh 9fe75c776d Refuse a Filament Flow Ratio of 0 Instead of Writing NaN Extrusion
A used filament with a flow ratio of 0 sliced in the GUI and wrote
E-92233720368547.758 on every extrusion move: GCode::_extrude multiplied
the flow by the ratio and divided by it again, giving 0 * 0 / 0 = NaN.
The CLI already rejected the value through the config check, but
Print::validate never looked at it.

Print::validate now stops the slice with an error naming the filament
when a used filament's flow ratio is 0 or nil, and _extrude converts the
flow to extrusion length with the bare filament cross-section, so the
ratio is applied once without a divide.
2026-10-21 14:19:17 +08:00
7 changed files with 68 additions and 106 deletions

No files matched your search

+3 -4
View File
@@ -9214,10 +9214,9 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string &path_d
effective_height = static_cast<float>(m_sub_layer_height);
}
// Effective extrusion length per distance unit = (filament_flow_ratio/cross_section) * mm3_per_mm / print flow ratio
// m_writer.extruder()->e_per_mm3() below is (filament flow ratio / cross-sectional area)
double e_per_mm = m_writer.filament()->e_per_mm3() * _mm3_per_mm;
e_per_mm /= filament_flow_ratio;
// _mm3_per_mm already includes the filament flow ratio, so convert it with the bare cross-section
// rather than e_per_mm3(), which would apply the ratio a second time.
double e_per_mm = _mm3_per_mm / m_writer.filament()->filament_crossection();
// set speed
if (speed == -1) {
+17
View File
@@ -1882,6 +1882,23 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
if (extruders.empty())
return { L("No extrusions under current settings.") };
// A flow ratio of 0 would extrude nothing, and a nil one would write NaN extrusion lengths.
{
const std::vector<double> &flow_ratios = m_config.filament_flow_ratio.values;
const std::vector<int> &self_index = m_config.filament_self_index.values;
for (size_t column = 0; column < flow_ratios.size(); ++column) {
// filament_self_index maps a column to its filament once the filament arrays hold one column per variant
const unsigned int filament_id = self_index.size() == flow_ratios.size() ? self_index[column] - 1 : column;
if (flow_ratios[column] > 0. || !std::binary_search(extruders.begin(), extruders.end(), filament_id))
continue;
StringObjectException err;
// TRN %1% is the filament number as shown in the filament list, starting from 1.
err.string = Slic3r::format(L("The flow ratio of filament %1% must be greater than 0."), filament_id + 1);
err.opt_key = "filament_flow_ratio";
return err;
}
}
// IDEX/IQEX: block multi-color in non-primary parallel modes when the active
// configuration can't physically support it (no within-gantry toolchange path
// or MMU lane sharing). See imex_multicolor_block_reason() for the full rule.
-6
View File
@@ -2,7 +2,6 @@
#include "GeneratedConfig.hpp"
#include "libslic3r/Utils.hpp"
#include "PluginAuditManager.hpp"
#include "PythonPluginBridge.hpp"
#include <atomic>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
@@ -665,11 +664,6 @@ bool PythonInterpreter::initialize()
// background-thread exceptions) to <data_dir>/log/python_*.log.
install_python_stderr_redirect();
// Attribute plugin-spawned Python threads to the plugin/capability that created them.
// Must run while the GIL is still held here, after the audit hook is in place and
// before the GIL is released below.
install_python_thread_audit_shim();
// Release the GIL so other threads can acquire it via PyGILState_Ensure.
// Without this, calls from background threads will block trying to acquire the GIL.
m_main_thread_state = PyEval_SaveThread();
-87
View File
@@ -603,93 +603,6 @@ request aborts the plugin load.)pbdoc");
}
void install_python_thread_audit_shim()
{
// Runs with the GIL held from PythonInterpreter::initialize(). CPython's own thread bootstrap
// calls the target with no knowledge of the audit context that created it: the calling
// thread is the only place the plugin key and capability are still available. Capture them
// here, then re-establish the same ScopedPluginAuditContext inside the spawned thread before
// the plugin body runs. The identity lives only in this C++ closure -- no Python-visible
// setter is added.
try {
py::object thread_module = py::module_::import("_thread");
const int installed = PyObject_HasAttrString(thread_module.ptr(), "_orca_thread_audit_shim_installed");
if (installed == 1) {
BOOST_LOG_TRIVIAL(debug) << "[AUDIT] Python thread attribution shim already installed";
return;
}
if (installed < 0)
PyErr_Clear();
py::object original = py::reinterpret_steal<py::object>(
PyObject_GetAttrString(thread_module.ptr(), "start_new_thread"));
if (!original || !PyCallable_Check(original.ptr())) {
PyErr_Clear();
BOOST_LOG_TRIVIAL(error) << "[AUDIT] _thread.start_new_thread is missing or not callable; "
"plugin threads will run without audit attribution";
return;
}
py::object shim = py::cpp_function(
[original](py::object function, py::object args, py::object kwargs) -> py::object {
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
if (plugin_key.empty()) {
// Not called from a plugin: preserve the stock implementation untouched.
return kwargs.is_none() ? original(function, args) : original(function, args, kwargs);
}
const std::string capability = PluginAuditManager::instance().current_capability();
py::object guarded = py::cpp_function(
[function, plugin_key, capability](py::args call_args, py::kwargs call_kwargs) -> py::object {
ScopedPluginAuditContext audit_scope(plugin_key, capability);
PyObject* result = PyObject_Call(function.ptr(), call_args.ptr(), call_kwargs.ptr());
if (result == nullptr)
throw py::error_already_set();
return py::reinterpret_steal<py::object>(result);
});
return kwargs.is_none() ? original(guarded, args) : original(guarded, args, kwargs);
},
py::arg("function"), py::arg("args"), py::arg("kwargs") = py::none());
if (PyObject_SetAttrString(thread_module.ptr(), "start_new_thread", shim.ptr()) < 0) {
PyErr_Clear();
BOOST_LOG_TRIVIAL(error)
<< "[AUDIT] Failed to install Python thread attribution shim on _thread.start_new_thread";
return;
}
if (PyObject_SetAttrString(thread_module.ptr(), "_orca_thread_audit_shim_installed", Py_True) < 0)
PyErr_Clear();
// threading.py caches the function at import time (as `_start_new_thread` in 3.12, and as
// `start_new_thread` in older versions). Patch the cached reference too when the module is
// already loaded; otherwise its Thread.start() would keep calling the unwrapped original.
py::object sys_modules = py::module_::import("sys").attr("modules");
PyObject* threading_module = PyDict_GetItemString(sys_modules.ptr(), "threading");
if (threading_module && threading_module != Py_None) {
const char* cached_names[] = {"_start_new_thread", "start_new_thread"};
for (const char* cached_name : cached_names) {
const int has_cached_name = PyObject_HasAttrString(threading_module, cached_name);
if (has_cached_name < 0) {
PyErr_Clear();
continue;
}
if (has_cached_name != 1)
continue;
if (PyObject_SetAttrString(threading_module, cached_name, shim.ptr()) < 0) {
PyErr_Clear();
BOOST_LOG_TRIVIAL(warning) << "[AUDIT] Failed to update threading." << cached_name
<< " with the Python thread attribution shim";
}
}
}
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Python thread attribution shim installed";
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install Python thread attribution shim: " << ex.what();
}
}
} // namespace Slic3r
#ifdef ORCA_PYTHON_STUBGEN_MODULE
-9
View File
@@ -13,15 +13,6 @@ namespace Slic3r { class PluginCapabilityInterface; }
namespace Slic3r {
// Install the thread-attribution shim: wraps `_thread.start_new_thread` (and the reference
// cached by an already-imported `threading` module) so a thread spawned from inside a plugin
// call runs with that call's plugin key/capability audit scope. The captured identity is held
// in a private C++ wrapper; nothing is exposed to Python that could set it.
//
// Call with the GIL held, after PluginAuditManager::install_hook() and before the interpreter
// releases the GIL. Safe to call more than once per interpreter.
void install_python_thread_audit_shim();
// One materialized capability returned from finalize_plugin_capture: the C++ instance
// and its resolved get_name() (cached while the GIL was held).
struct CapturedCapability
+23
View File
@@ -4,6 +4,7 @@
#include <catch2/catch_message.hpp>
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/ModelArrange.hpp"
#include "libslic3r/Print.hpp"
@@ -89,6 +90,28 @@ TEST_CASE("The exclude object tolerance defaults to the former fixed 0.1 mm", "[
Catch::Matchers::WithinAbs(0.1, 1e-9));
}
TEST_CASE("The filament flow ratio scales the extruded length once", "[GCode]")
{
// Retraction off, so every positive relative E move is extrusion and scales with the ratio.
auto extruded_length = [](double flow_ratio) {
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({ { "use_relative_e_distances", "1" }, { "retraction_length", "0" }, { "wipe", "0" } });
config.set_key_value("filament_flow_ratio", new ConfigOptionFloatsNullable{ flow_ratio });
double total = 0.;
GCodeReader reader;
reader.parse_buffer(Test::slice({ Test::cube(20) }, config),
[&](GCodeReader &, const GCodeReader::GCodeLine &line) {
if ((line.cmd_is("G1") || line.cmd_is("G2") || line.cmd_is("G3")) && line.has_e() && line.e() > 0.f)
total += line.e();
});
return total;
};
const double full = extruded_length(1.);
REQUIRE(full > 0.);
CHECK_THAT(extruded_length(0.9) / full, Catch::Matchers::WithinRel(0.9, 1e-3));
}
// Firmwares read the compatibility header lines (Anycubic's GoKlipper heats the bed with
// first_layer_bed_temperature), so they must describe the filament the plate prints, not filament 1.
TEST_CASE("The G-code header names the first layer temperatures of the filament the plate prints", "[GCode]")
+25
View File
@@ -547,6 +547,31 @@ TEST_CASE("Print::validate tolerates a null warnings pointer", "[Print][validate
CHECK(err.string.empty());
}
TEST_CASE("Print::validate refuses a used filament with a flow ratio of 0", "[Print][validate][Regression]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_key_value("filament_flow_ratio", new ConfigOptionFloatsNullable{ 0. });
std::vector<StringObjectException> warnings;
StringObjectException err = validate_cubes(config, warnings);
CHECK_FALSE(err.string.empty());
CHECK_FALSE(err.is_warning);
CHECK(err.opt_key == "filament_flow_ratio");
}
TEST_CASE("Print::validate ignores a flow ratio of 0 on an unused filament", "[Print][validate]")
{
// The cube prints with filament 1, so filament 2's ratio is never used.
DynamicPrintConfig config = multifilament_config(2, { { "filament_self_index", "1,2" } });
config.set_key_value("filament_flow_ratio", new ConfigOptionFloatsNullable{ 1., 0. });
std::vector<StringObjectException> warnings;
StringObjectException err = validate_cubes(config, warnings);
CHECK(err.string.empty());
}
TEST_CASE("Purge tower selection keeps ordinary printers on the classic path", "[Print][PurgeTower][Regression]")
{
DynamicPrintConfig config = multifilament_config(2, {