mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-21 06:21:16 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c5049bc9a | ||
|
|
aecd76594f | ||
|
|
e9d1bb85b8 |
No files matched your search
@@ -9214,9 +9214,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string &path_d
|
||||
effective_height = static_cast<float>(m_sub_layer_height);
|
||||
}
|
||||
|
||||
// _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();
|
||||
// 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;
|
||||
|
||||
// set speed
|
||||
if (speed == -1) {
|
||||
|
||||
@@ -1882,23 +1882,6 @@ 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.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#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>
|
||||
@@ -664,6 +665,11 @@ 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();
|
||||
|
||||
@@ -603,6 +603,93 @@ 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
|
||||
|
||||
@@ -13,6 +13,15 @@ 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
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#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"
|
||||
@@ -90,28 +89,6 @@ 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]")
|
||||
|
||||
@@ -547,31 +547,6 @@ 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, {
|
||||
|
||||
Reference in new issue
Block a user