Compare commits

..
Author SHA1 Message Date
ExPikaPaka dbe342c978 Fix data races in the parallel slicing paths
Multi-material segmentation derived the output slot from range.begin() divided
by the granularity, which assumes tbb::blocked_range splits on grainsize
multiples. It bisects at midpoints, so two sub-ranges of one group get the same
slot and append into the same ExPolygons at once. With ten layers and a
granularity of two, the sub-ranges [2,3) and [3,5) collide. The granularity is
two or more whenever the shell layers are three or more, which is the default.
The loop now iterates groups, so the slot follows the element index and the
partitioner cannot cut a group in half.

CutSurface wrote std::vector<bool> elements from a parallel_for. The bits share
words across chunk boundaries, so flags were lost.

name_tbb_thread_pool_threads_set_locale() held every chunk of a parallel_for on
a condition variable until max_concurrency() of them were running. TBB does not
promise that, and there is no timeout, so the slicing thread could wait forever.
A task_scheduler_observer sets each worker up as it joins the arena, which needs
no simultaneity and covers workers the chunked version never reached.

TriangleSetSampling dereferenced upper_bound() without checking for end(), which
is reachable because the sum is a float and the keys are doubles.

TreeModelVolumes re-read getMaxCalculatedLayer() after releasing the lock, so the
layer difference could go negative and become a huge size_t. The two sibling
functions already carry the guard this adds.
2026-10-01 09:15:16 +02:00
24 changed files with 191 additions and 560 deletions
+1 -11
View File
@@ -151,12 +151,6 @@ elseif(APPLE)
# the post-install -add_rpath below. # the post-install -add_rpath below.
set(_python_ldflags "${_python_arch_flags} -Wl,-headerpad_max_install_names") set(_python_ldflags "${_python_arch_flags} -Wl,-headerpad_max_install_names")
# The macOS 27 SDK declares pipe2() and dup3() as available from macOS 27, so
# configure finds them and CPython 3.12 calls them without a runtime check.
# Below a macOS 27 deployment target they are weak-linked and resolve to NULL
# on older systems, where os.pipe() then segfaults -- in `make install`
# (compileall, ensurepip) and in the shipped app alike. Every configure below
# keeps the pipe()/dup2() fallbacks (python/cpython#153711).
if(IS_CROSS_COMPILE) if(IS_CROSS_COMPILE)
set(_python_build_tgt --build=${_python_build_arch}-apple-darwin --host=${_python_host_arch}-apple-darwin) set(_python_build_tgt --build=${_python_build_arch}-apple-darwin --host=${_python_host_arch}-apple-darwin)
set(_python_build_arch_flags "-arch ${_python_build_arch_flag} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") set(_python_build_arch_flags "-arch ${_python_build_arch_flag} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}")
@@ -180,8 +174,7 @@ elseif(APPLE)
--enable-shared \ --enable-shared \
--without-static-libpython \ --without-static-libpython \
--disable-test-modules \ --disable-test-modules \
--build=${_python_build_arch}-apple-darwin \ --build=${_python_build_arch}-apple-darwin && \
ac_cv_func_pipe2=no ac_cv_func_dup3=no && \
make -j${NPROC} python && \ make -j${NPROC} python && \
cd '<SOURCE_DIR>' && \ cd '<SOURCE_DIR>' && \
env \ env \
@@ -198,7 +191,6 @@ elseif(APPLE)
--without-static-libpython \ --without-static-libpython \
--with-openssl='${DESTDIR}' \ --with-openssl='${DESTDIR}' \
--disable-test-modules \ --disable-test-modules \
ac_cv_func_pipe2=no ac_cv_func_dup3=no \
${_python_build_tgt} \ ${_python_build_tgt} \
--with-build-python='${_python_build_python}' \ --with-build-python='${_python_build_python}' \
py_cv_module__tkinter=n/a" py_cv_module__tkinter=n/a"
@@ -221,8 +213,6 @@ elseif(APPLE)
--with-openssl=${DESTDIR} --with-openssl=${DESTDIR}
--disable-test-modules --disable-test-modules
${_python_build_tgt} ${_python_build_tgt}
ac_cv_func_pipe2=no
ac_cv_func_dup3=no
# Tcl/Tk 9.0 (e.g. from Homebrew) is incompatible with CPython 3.12's # Tcl/Tk 9.0 (e.g. from Homebrew) is incompatible with CPython 3.12's
# _tkinter; OrcaSlicer's embedded Python does not need tkinter anyway. # _tkinter; OrcaSlicer's embedded Python does not need tkinter anyway.
py_cv_module__tkinter=n/a py_cv_module__tkinter=n/a
+5 -36
View File
@@ -873,10 +873,10 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
CNumericLocalesSetter locales_setter; CNumericLocalesSetter locales_setter;
std::function<bool(const json&, const char,const char,const bool,std::string&)> parse_str_arr = [&parse_str_arr](const json& arr, const char single_sep,const char array_sep,const bool escape_string_style,std::string& value_str)->bool { std::function<bool(const json::const_iterator&, const char,const char,const bool,std::string&)> parse_str_arr = [&parse_str_arr](const json::const_iterator& it, const char single_sep,const char array_sep,const bool escape_string_style,std::string& value_str)->bool {
// must have consistent type name // must have consistent type name
std::string consistent_type; std::string consistent_type;
for (auto iter = arr.begin(); iter != arr.end(); ++iter) { for (auto iter = it.value().begin(); iter != it.value().end(); ++iter) {
if (consistent_type.empty()) if (consistent_type.empty())
consistent_type = iter.value().type_name(); consistent_type = iter.value().type_name();
else { else {
@@ -886,13 +886,13 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
} }
bool first = true; bool first = true;
for (auto iter = arr.begin(); iter != arr.end(); iter++) { for (auto iter = it.value().begin(); iter != it.value().end(); iter++) {
if (iter.value().is_array()) { if (iter.value().is_array()) {
if (!first) if (!first)
value_str += array_sep; value_str += array_sep;
else else
first = false; first = false;
bool success = parse_str_arr(iter.value(), single_sep, array_sep,escape_string_style, value_str); bool success = parse_str_arr(iter, single_sep, array_sep,escape_string_style, value_str);
if (!success) if (!success)
return false; return false;
} }
@@ -1038,39 +1038,8 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
} }
} }
// BambuStudio and its forks save a nozzle variant that matches the parent preset as "nil".
// An option that can't hold nil gets its default in that slot, and the slot is reported so
// the merge onto the parent keeps the parent's value. All slots nil means the key is not set.
const json *values = &it.value();
json values_with_defaults;
if (substitution_context.accept_nil && optdef && !optdef->nullable && optdef->default_value &&
(optdef->type == coFloats || optdef->type == coPercents || optdef->type == coFloatsOrPercents ||
optdef->type == coInts || optdef->type == coEnums || optdef->type == coBools)) {
auto is_nil = [](const json &v) { return v.is_string() && v.get<std::string>() == "nil"; };
std::vector<size_t> nil_slots;
for (size_t i = 0; i < values->size(); ++i)
if (is_nil((*values)[i]))
nil_slots.push_back(i);
if (!nil_slots.empty()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": " << file << ": " << it.key() << " is nil in "
<< nil_slots.size() << " of " << values->size()
<< " slots, read as not set (the parent preset's value, or the default)";
if (nil_slots.size() == values->size())
continue;
// create_default_option() gives enums their names, which vserialize() needs.
std::unique_ptr<ConfigOption> default_option(optdef->create_default_option());
const std::vector<std::string> defaults = static_cast<const ConfigOptionVectorBase*>(default_option.get())->vserialize();
values_with_defaults = *values;
const json first_value = *std::find_if_not(values->begin(), values->end(), is_nil);
for (size_t i : nil_slots)
values_with_defaults[i] = defaults.empty() ? first_value : json(defaults[i % defaults.size()]);
values = &values_with_defaults;
substitution_context.nil_slots[opt_key] = std::move(nil_slots);
}
}
// BBS: we only support 2 depth array // BBS: we only support 2 depth array
valid = parse_str_arr(*values, single_sep, array_sep,escape_string_type, value_str); valid = parse_str_arr(it, single_sep, array_sep,escape_string_type, value_str);
if (!valid) { if (!valid) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": parse " << file << " error, invalid json array for " << it.key(); BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": parse " << file << " error, invalid json array for " << it.key();
break; break;
-5
View File
@@ -267,11 +267,6 @@ struct ConfigSubstitutionContext
ForwardCompatibilitySubstitutionRule rule; ForwardCompatibilitySubstitutionRule rule;
ConfigSubstitutions substitutions; ConfigSubstitutions substitutions;
std::vector<std::string> unrecogized_keys; std::vector<std::string> unrecogized_keys;
// Read "nil" in an option that can't hold it as not set instead of failing. Set by callers that hand
// nil_slots to the merge onto the parent preset, or for which the option default is the right fallback.
bool accept_nil = false;
// Slots of options that can't hold nil but were "nil" in the file; they hold the option default.
std::map<std::string, std::vector<size_t>> nil_slots;
}; };
// A generic value of a configuration option. // A generic value of a configuration option.
+9 -2
View File
@@ -809,13 +809,20 @@ void priv::set_skip_for_out_of_aoi(std::vector<bool> &skip_indicies,
}); // END parallel for }); // END parallel for
// inspect all triangles, when it is out of bounding box // inspect all triangles, when it is out of bounding box
// NOTE: std::vector<bool> is bit packed, thus setting its items from multiple threads is a
// read-modify-write race on the shared words and silently loses flags. Collect the flags into
// a byte per triangle, where the chunks do not share memory, and merge them afterwards.
std::vector<unsigned char> skip_triangle(its.indices.size(), 0);
tbb::parallel_for(tbb::blocked_range<size_t>(0, its.indices.size()), tbb::parallel_for(tbb::blocked_range<size_t>(0, its.indices.size()),
[&its, &is_on_sides, &skip_indicies](const tbb::blocked_range<size_t> &range) { [&its, &is_on_sides, &skip_triangle](const tbb::blocked_range<size_t> &range) {
for (size_t i = range.begin(); i < range.end(); ++i) { for (size_t i = range.begin(); i < range.end(); ++i) {
if (is_all_on_one_side(its.indices[i], is_on_sides)) if (is_all_on_one_side(its.indices[i], is_on_sides))
skip_indicies[i] = true; skip_triangle[i] = 1;
} }
}); // END parallel for }); // END parallel for
for (size_t i = 0; i < skip_triangle.size(); ++i)
if (skip_triangle[i])
skip_indicies[i] = true;
} }
indexed_triangle_set Slic3r::its_mask(const indexed_triangle_set &its, indexed_triangle_set Slic3r::its_mask(const indexed_triangle_set &its,
+1 -10
View File
@@ -2696,8 +2696,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
} }
std::map<std::string, std::string> key_values; std::map<std::string, std::string> key_values;
std::string reason; std::string reason;
// No parent preset here: a nil slot keeps the option default.
config_substitutions.accept_nil = true;
int ret = config.load_from_json(dest_file, config_substitutions, true, key_values, reason); int ret = config.load_from_json(dest_file, config_substitutions, true, key_values, reason);
if (ret) { if (ret) {
add_error("Error load config from json:"+reason); add_error("Error load config from json:"+reason);
@@ -2738,13 +2736,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
//ConfigSubstitutions config_substitutions = config.load_from_ini(dest_file, Enable); //ConfigSubstitutions config_substitutions = config.load_from_ini(dest_file, Enable);
std::map<std::string, std::string> key_values; std::map<std::string, std::string> key_values;
std::string reason; std::string reason;
ConfigSubstitutionContext load_context(Enable); ConfigSubstitutions config_substitutions = use_json? config.load_from_json(dest_file, Enable, key_values, reason) : config.load_from_ini(dest_file, Enable);
load_context.accept_nil = true;
if (use_json)
config.load_from_json(dest_file, load_context, true, key_values, reason);
else
load_context.substitutions = config.load_from_ini(dest_file, Enable);
ConfigSubstitutions config_substitutions = std::move(load_context.substitutions);
if (!reason.empty()) { if (!reason.empty()) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", load project embedded config from %1% failed\n") % dest_file; BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", load project embedded config from %1% failed\n") % dest_file;
//skip this file //skip this file
@@ -2793,7 +2785,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
preset->is_project_embedded = true; preset->is_project_embedded = true;
preset->is_external = true; preset->is_external = true;
preset->is_dirty = false; preset->is_dirty = false;
preset->nil_slots = std::move(load_context.nil_slots);
std::string version_str = key_values[BBL_JSON_KEY_VERSION]; std::string version_str = key_values[BBL_JSON_KEY_VERSION];
boost::optional<Semver> version = Semver::parse(version_str); boost::optional<Semver> version = Semver::parse(version_str);
+56 -45
View File
@@ -1378,55 +1378,66 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
return out; return out;
}; };
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers, granularity), [&granularity, &num_layers, &num_facets_states, &layer_color_stat, &top_raw, &triangles_by_color_top, // The layers are processed in groups of "granularity" layers. A layer projects its shells up to "granularity"
&throw_on_cancel_callback, &input_expolygons, &bottom_raw, &triangles_by_color_bottom, // layers away, thus a group may write into the slots of its neighbor groups. The even and the odd groups
&shell_triangles_by_color_top, &shell_triangles_by_color_bottom](const tbb::blocked_range<size_t> &range) { // therefore write into two disjoint halves of the output vectors (the 2nd half is offset by num_layers) and
size_t group_idx = range.begin() / granularity; // both halves are merged below. The group index has to be derived from the layer index and not from the extent
size_t layer_idx_offset = (group_idx & 1) * num_layers; // of the TBB sub-range: tbb::blocked_range bisects at midpoints, thus a sub-range neither starts at a multiple
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { // of the grain size nor covers a whole group, and two sub-ranges of one group would append into a single
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx) { // ExPolygons concurrently. Iterating over the groups keeps every group on a single thread, in ascending order.
throw_on_cancel_callback(); const size_t num_groups = (num_layers + size_t(granularity) - 1) / size_t(granularity);
LayerColorStat stat = layer_color_stat(layer_idx, color_idx); tbb::parallel_for(tbb::blocked_range<size_t>(0, num_groups, 1), [&granularity, &num_layers, &num_facets_states, &layer_color_stat, &top_raw, &triangles_by_color_top,
if (std::vector<Polygons> &top = top_raw[color_idx]; ! top.empty() && ! top[layer_idx].empty()) &throw_on_cancel_callback, &input_expolygons, &bottom_raw, &triangles_by_color_bottom,
if (ExPolygons top_ex = union_ex(top[layer_idx]); ! top_ex.empty()) { &shell_triangles_by_color_top, &shell_triangles_by_color_bottom](const tbb::blocked_range<size_t> &range) {
// Clean up thin projections. They are not printable anyways. for (size_t group_idx = range.begin(); group_idx < range.end(); ++ group_idx) {
top_ex = opening_ex(top_ex, stat.small_region_threshold); const size_t layer_idx_offset = (group_idx & 1) * num_layers;
if (! top_ex.empty()) { const size_t layer_idx_begin = group_idx * size_t(granularity);
append(triangles_by_color_top[color_idx][layer_idx + layer_idx_offset], top_ex); const size_t layer_idx_end = std::min(num_layers, layer_idx_begin + size_t(granularity));
float offset = 0.f; for (size_t layer_idx = layer_idx_begin; layer_idx < layer_idx_end; ++ layer_idx) {
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx]; for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx) {
for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx) { throw_on_cancel_callback();
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line LayerColorStat stat = layer_color_stat(layer_idx, color_idx);
//offset -= stat.extrusion_width ; if (std::vector<Polygons> &top = top_raw[color_idx]; ! top.empty() && ! top[layer_idx].empty())
offset -= (stat.extrusion_spacing + stat.extrusion_width); if (ExPolygons top_ex = union_ex(top[layer_idx]); ! top_ex.empty()) {
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]); // Clean up thin projections. They are not printable anyways.
ExPolygons last = opening_ex(intersection_ex(top_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold); top_ex = opening_ex(top_ex, stat.small_region_threshold);
if (last.empty()) if (! top_ex.empty()) {
break; append(triangles_by_color_top[color_idx][layer_idx + layer_idx_offset], top_ex);
append(shell_triangles_by_color_top[color_idx][last_idx + layer_idx_offset], std::move(last)); float offset = 0.f;
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx) {
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
//offset -= stat.extrusion_width ;
offset -= (stat.extrusion_spacing + stat.extrusion_width);
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
ExPolygons last = opening_ex(intersection_ex(top_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
if (last.empty())
break;
append(shell_triangles_by_color_top[color_idx][last_idx + layer_idx_offset], std::move(last));
}
} }
} }
} if (std::vector<Polygons> &bottom = bottom_raw[color_idx]; ! bottom.empty() && ! bottom[layer_idx].empty())
if (std::vector<Polygons> &bottom = bottom_raw[color_idx]; ! bottom.empty() && ! bottom[layer_idx].empty()) if (ExPolygons bottom_ex = union_ex(bottom[layer_idx]); ! bottom_ex.empty()) {
if (ExPolygons bottom_ex = union_ex(bottom[layer_idx]); ! bottom_ex.empty()) { // Clean up thin projections. They are not printable anyways.
// Clean up thin projections. They are not printable anyways. bottom_ex = opening_ex(bottom_ex, stat.small_region_threshold);
bottom_ex = opening_ex(bottom_ex, stat.small_region_threshold); if (! bottom_ex.empty()) {
if (! bottom_ex.empty()) { append(triangles_by_color_bottom[color_idx][layer_idx + layer_idx_offset], bottom_ex);
append(triangles_by_color_bottom[color_idx][layer_idx + layer_idx_offset], bottom_ex); float offset = 0.f;
float offset = 0.f; ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx]; for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx) {
for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx) { //BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line //offset -= stat.extrusion_width;
//offset -= stat.extrusion_width; offset -= (stat.extrusion_spacing + stat.extrusion_width);
offset -= (stat.extrusion_spacing + stat.extrusion_width); layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]); ExPolygons last = opening_ex(intersection_ex(bottom_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
ExPolygons last = opening_ex(intersection_ex(bottom_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold); if (last.empty())
if (last.empty()) break;
break; append(shell_triangles_by_color_bottom[color_idx][last_idx + layer_idx_offset], std::move(last));
append(shell_triangles_by_color_bottom[color_idx][last_idx + layer_idx_offset], std::move(last)); }
} }
} }
} }
} }
} }
}); });
+5 -8
View File
@@ -1726,11 +1726,9 @@ PresetCollection::UserPresetLoad PresetCollection::resolve_user_preset(
//ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule); //ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule);
std::map<std::string, std::string> key_values; std::map<std::string, std::string> key_values;
std::string reason; std::string reason;
ConfigSubstitutionContext load_context(substitution_rule); ConfigSubstitutions config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason);
load_context.accept_nil = true; if (! config_substitutions.empty())
config.load_from_json(preset.file, load_context, true, key_values, reason); out.substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) });
if (! load_context.substitutions.empty())
out.substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(load_context.substitutions) });
if (!reason.empty()) { if (!reason.empty()) {
out.discard_file = true; out.discard_file = true;
out.errors.push_back((boost::format("parse config %1% failed") % preset.file).str()); out.errors.push_back((boost::format("parse config %1% failed") % preset.file).str());
@@ -1766,7 +1764,7 @@ PresetCollection::UserPresetLoad PresetCollection::resolve_user_preset(
preset.config = inherit_preset->config; preset.config = inherit_preset->config;
preset.filament_id = inherit_preset->filament_id; preset.filament_id = inherit_preset->filament_id;
extend_default_config_length(config, false, {}); extend_default_config_length(config, false, {});
preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2, &load_context.nil_slots); preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
} }
else { else {
auto inherits_config2 = dynamic_cast<ConfigOptionString *>(inherits_config); auto inherits_config2 = dynamic_cast<ConfigOptionString *>(inherits_config);
@@ -2139,8 +2137,7 @@ void PresetCollection::load_project_embedded_presets(std::vector<Preset*>& proje
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file; BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file;
continue; continue;
} }
preset->config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2, &preset->nil_slots); preset->config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
preset->nil_slots.clear();
//preset->config.apply(std::move(config)); //preset->config.apply(std::move(config));
Preset::normalize(preset->config); Preset::normalize(preset->config);
// Report configuration fields, which are misplaced into a wrong group. // Report configuration fields, which are misplaced into a wrong group.
-2
View File
@@ -269,8 +269,6 @@ public:
//BBS: add type for project-embedded //BBS: add type for project-embedded
bool is_project_embedded = false; bool is_project_embedded = false;
ConfigSubstitutions *loading_substitutions{nullptr}; ConfigSubstitutions *loading_substitutions{nullptr};
// Slots that were nil in the embedded preset's file, see ConfigSubstitutionContext::nil_slots.
std::map<std::string, std::vector<size_t>> nil_slots;
bool is_user() const { return ! this->is_default && ! this->is_system && ! this->is_project_embedded && ! this->is_from_bundle(); } bool is_user() const { return ! this->is_default && ! this->is_system && ! this->is_project_embedded && ! this->is_from_bundle(); }
bool can_overwrite() const { return ! this->is_default && ! this->is_system && ! this->is_from_bundle(); } bool can_overwrite() const { return ! this->is_default && ! this->is_system && ! this->is_from_bundle(); }
//bool is_user() const { return ! this->is_default && ! this->is_system; } //bool is_user() const { return ! this->is_default && ! this->is_system; }
+20 -83
View File
@@ -10,8 +10,6 @@
#include "GCode/Thumbnails.hpp" #include "GCode/Thumbnails.hpp"
#include <set> #include <set>
#include <boost/algorithm/string/case_conv.hpp> #include <boost/algorithm/string/case_conv.hpp>
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/join.hpp>
#include <boost/algorithm/string/replace.hpp> #include <boost/algorithm/string/replace.hpp>
#include <boost/algorithm/string/split.hpp> #include <boost/algorithm/string/split.hpp>
#include <boost/algorithm/string/trim.hpp> #include <boost/algorithm/string/trim.hpp>
@@ -9187,18 +9185,16 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
} else if (opt_key == "tree_support_wall_count" && value == "-1") { } else if (opt_key == "tree_support_wall_count" && value == "-1") {
value = "0"; value = "0";
} else if (opt_key == "different_settings_to_system") { } else if (opt_key == "different_settings_to_system") {
// Rename whole entries: a substring rename would also hit longer keys that contain the old name. std::string copy_value = value;
std::vector<std::string> entries; copy_value.erase(std::remove(copy_value.begin(), copy_value.end(), '\"'), copy_value.end()); // remove '"' in string
boost::split(entries, value, boost::is_any_of(";")); std::set<std::string> split_keys = SplitStringAndRemoveDuplicateElement(copy_value, ";");
for (std::string &entry : entries) { for (std::string split_key : split_keys) {
std::string key = entry, unused_value; std::string copy_key = split_key, copy_value = "";
key.erase(std::remove(key.begin(), key.end(), '\"'), key.end()); handle_legacy(copy_key, copy_value);
std::string new_key = key; if (copy_key != split_key) {
handle_legacy(new_key, unused_value); ReplaceString(value, split_key, copy_key);
if (new_key != key) }
ReplaceString(entry, key, new_key);
} }
value = boost::algorithm::join(entries, ";");
} else if (opt_key == "overhang_fan_threshold" && value == "5%") { } else if (opt_key == "overhang_fan_threshold" && value == "5%") {
value = "10%"; value = "10%";
} else if( opt_key == "wall_infill_order" ) { } else if( opt_key == "wall_infill_order" ) {
@@ -9240,13 +9236,6 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
else if (value == "0"){ else if (value == "0"){
value = "ensure_moderate"; value = "ensure_moderate";
} }
// BambuStudio's levels
else if (value == "enabled")
value = "ensure_all";
else if (value == "partial")
value = "ensure_moderate";
else if (value == "disabled")
value = "none";
} else if (opt_key == "rotate_solid_infill_direction") { } else if (opt_key == "rotate_solid_infill_direction") {
opt_key = "solid_infill_rotate_template"; opt_key = "solid_infill_rotate_template";
if (value == "1") { if (value == "1") {
@@ -9262,11 +9251,9 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
opt_key = "chamber_temperature"; opt_key = "chamber_temperature";
} else if (opt_key == "thumbnail_size") { } else if (opt_key == "thumbnail_size") {
opt_key = "thumbnails"; opt_key = "thumbnails";
} else if (opt_key == "top_one_wall_type") { } else if (opt_key == "top_one_wall_type" && value != "none") {
// BambuStudio's "topmost" (one wall on the topmost layer only) widens to every top surface.
opt_key = "only_one_wall_top"; opt_key = "only_one_wall_top";
if (!value.empty()) value = "1";
value = (value == "not apply" || value == "none") ? "0" : "1";
} else if (opt_key == "initial_layer_flow_ratio") { } else if (opt_key == "initial_layer_flow_ratio") {
opt_key = "bottom_solid_infill_flow_ratio"; opt_key = "bottom_solid_infill_flow_ratio";
} else if (opt_key == "ironing_direction") { } else if (opt_key == "ironing_direction") {
@@ -9284,20 +9271,6 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
opt_key == "ironing_pattern" || opt_key == "ironing_pattern" ||
opt_key == "support_ironing_pattern") && value == "zig-zag") { opt_key == "support_ironing_pattern") && value == "zig-zag") {
value = "rectilinear"; value = "rectilinear";
} else if (opt_key == "sparse_infill_pattern" && value == "2dlattice") {
// BambuStudio's name, and OrcaSlicer's before the lateral patterns were renamed
value = "lateral-lattice";
} else if (opt_key == "sparse_infill_pattern" && value == "2dhoneycomb") {
value = "lateral-honeycomb";
} else if (opt_key == "sparse_infill_lattice_angle_1" || opt_key == "lattice_angle_1") {
opt_key = "lateral_lattice_angle_1";
} else if (opt_key == "sparse_infill_lattice_angle_2" || opt_key == "lattice_angle_2") {
opt_key = "lateral_lattice_angle_2";
} else if (opt_key == "support_style" && value == "tree_organic") {
value = "organic";
} else if (opt_key == "raft_first_layer_expansion" && !value.empty() && value.front() == '-') {
// BambuStudio's auto: 2 mm for normal supports and rafts. Tree branches use tree_support_auto_brim here.
value = "2";
} else if (opt_key == "filament_map_mode") { } else if (opt_key == "filament_map_mode") {
if (value == "Auto") value = "Auto For Flush"; if (value == "Auto") value = "Auto For Flush";
} }
@@ -9326,11 +9299,12 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
} }
// Orca: Rename wipe tower ribs related options // Orca: Rename wipe tower ribs related options
else if (opt_key == "prime_tower_rib_wall") { else if (opt_key == "prime_tower_rib_wall") {
opt_key = "wipe_tower_wall_type"; if (value == "1") {
if (value == "1" || value == "true") opt_key = "wipe_tower_wall_type";
value = "rib"; value = "rib";
else if (!value.empty()) } else {
value = "rectangle"; opt_key = "";
}
} else if (opt_key == "prime_tower_extra_rib_length") { } else if (opt_key == "prime_tower_extra_rib_length") {
opt_key = "wipe_tower_extra_rib_length"; opt_key = "wipe_tower_extra_rib_length";
} else if (opt_key == "prime_tower_rib_width") { } else if (opt_key == "prime_tower_rib_width") {
@@ -9342,18 +9316,6 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
} else if (opt_key == "machine_switch_extruder_time") { } else if (opt_key == "machine_switch_extruder_time") {
opt_key = "machine_tool_change_time"; opt_key = "machine_tool_change_time";
} }
// BambuStudio names of OrcaSlicer options
else if (opt_key == "role_base_wipe_speed") {
opt_key = "role_based_wipe_speed";
} else if (opt_key == "no_slow_down_for_cooling_on_outwalls") {
opt_key = "dont_slow_down_outer_wall";
} else if (opt_key == "process_notes") {
opt_key = "notes";
} else if (opt_key == "prime_tower_max_speed") {
opt_key = "wipe_tower_max_purge_speed";
} else if (opt_key == "enable_support_ironing") {
opt_key = "support_ironing";
}
else if (opt_key == "wall_direction" && value == "auto") { else if (opt_key == "wall_direction" && value == "auto") {
value = "ccw"; value = "ccw";
} }
@@ -11596,8 +11558,7 @@ void DynamicPrintConfig::update_non_diff_values_to_base_config(DynamicPrintConfi
return; return;
} }
void DynamicPrintConfig::update_diff_values_to_child_config(DynamicPrintConfig& new_config, std::string extruder_id_name, std::string extruder_variant_name, std::set<std::string>& key_set1, std::set<std::string>& key_set2, void DynamicPrintConfig::update_diff_values_to_child_config(DynamicPrintConfig& new_config, std::string extruder_id_name, std::string extruder_variant_name, std::set<std::string>& key_set1, std::set<std::string>& key_set2)
const std::map<std::string, std::vector<size_t>>* nil_slots)
{ {
std::vector<int> cur_extruder_ids, target_extruder_ids, variant_index; std::vector<int> cur_extruder_ids, target_extruder_ids, variant_index;
std::vector<std::string> cur_extruder_variants, target_extruder_variants; std::vector<std::string> cur_extruder_variants, target_extruder_variants;
@@ -11658,13 +11619,6 @@ void DynamicPrintConfig::update_diff_values_to_child_config(DynamicPrintConfig&
if (opt_src && opt_target && (*opt_src != *opt_target)) { if (opt_src && opt_target && (*opt_src != *opt_target)) {
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" change key %1% from base_value %2% to child's value %3%") BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" change key %1% from base_value %2% to child's value %3%")
%opt %(opt_src->serialize()) %(opt_target->serialize()); %opt %(opt_src->serialize()) %(opt_target->serialize());
const std::vector<size_t> *unset_slots = nullptr;
if (nil_slots && opt_src->is_vector())
if (auto it = nil_slots->find(opt); it != nil_slots->end())
unset_slots = &it->second;
std::unique_ptr<ConfigOption> base_value(unset_slots ? opt_src->clone() : nullptr);
int stride = 1;
bool merged_by_variant = false;
if (opt_target->is_scalar() if (opt_target->is_scalar()
|| ((key_set1.find(opt) == key_set1.end()) && (key_set2.empty() || (key_set2.find(opt) == key_set2.end())))) { || ((key_set1.find(opt) == key_set1.end()) && (key_set2.empty() || (key_set2.find(opt) == key_set2.end())))) {
//nothing to do, keep the original one //nothing to do, keep the original one
@@ -11673,6 +11627,7 @@ void DynamicPrintConfig::update_diff_values_to_child_config(DynamicPrintConfig&
else { else {
ConfigOptionVectorBase* opt_vec_src = static_cast<ConfigOptionVectorBase*>(opt_src); ConfigOptionVectorBase* opt_vec_src = static_cast<ConfigOptionVectorBase*>(opt_src);
const ConfigOptionVectorBase* opt_vec_dest = static_cast<const ConfigOptionVectorBase*>(opt_target); const ConfigOptionVectorBase* opt_vec_dest = static_cast<const ConfigOptionVectorBase*>(opt_target);
int stride = 1;
if (key_set2.find(opt) != key_set2.end()) if (key_set2.find(opt) != key_set2.end())
stride = 2; stride = 2;
// set_only_diff() requires the base vector length to equal variant_index.size()*stride, where // set_only_diff() requires the base vector length to equal variant_index.size()*stride, where
@@ -11686,26 +11641,8 @@ void DynamicPrintConfig::update_diff_values_to_child_config(DynamicPrintConfig&
if (opt_vec_src->size() != variant_index.size() * size_t(stride)) { if (opt_vec_src->size() != variant_index.size() * size_t(stride)) {
opt_src->set(opt_target); opt_src->set(opt_target);
} }
else { else
opt_vec_src->set_only_diff(opt_vec_dest, variant_index, stride); opt_vec_src->set_only_diff(opt_vec_dest, variant_index, stride);
merged_by_variant = true;
}
}
// A slot that was nil in the child's file is not set there, so it keeps this config's value.
if (unset_slots) {
auto *merged = static_cast<ConfigOptionVectorBase*>(opt_src);
const size_t base_size = static_cast<const ConfigOptionVectorBase*>(base_value.get())->size();
for (size_t slot = 0; slot < merged->size() && slot < base_size; ++slot) {
size_t child_slot = slot;
if (merged_by_variant) {
const int child_variant = variant_index[slot / stride];
if (child_variant == -1)
continue;
child_slot = size_t(child_variant) * stride + slot % stride;
}
if (std::find(unset_slots->begin(), unset_slots->end(), child_slot) != unset_slots->end())
merged->set_at(base_value.get(), slot, slot);
}
} }
} }
} }
+1 -4
View File
@@ -859,10 +859,7 @@ public:
void update_non_diff_values_to_base_config(DynamicPrintConfig& new_config, const t_config_option_keys& keys, const std::set<std::string>& different_keys, std::string extruder_id_name, std::string extruder_variant_name, void update_non_diff_values_to_base_config(DynamicPrintConfig& new_config, const t_config_option_keys& keys, const std::set<std::string>& different_keys, std::string extruder_id_name, std::string extruder_variant_name,
std::set<std::string>& key_set1, std::set<std::string>& key_set2); std::set<std::string>& key_set1, std::set<std::string>& key_set2);
// nil_slots: per option, the slots of new_config that were nil in its file (see ConfigSubstitutionContext::nil_slots); void update_diff_values_to_child_config(DynamicPrintConfig& new_config, std::string extruder_id_name, std::string extruder_variant_name, std::set<std::string>& key_set1, std::set<std::string>& key_set2);
// those variants keep this config's value.
void update_diff_values_to_child_config(DynamicPrintConfig& new_config, std::string extruder_id_name, std::string extruder_variant_name, std::set<std::string>& key_set1, std::set<std::string>& key_set2,
const std::map<std::string, std::vector<size_t>>* nil_slots = nullptr);
int update_values_from_single_to_multi(DynamicPrintConfig& multi_config, std::set<std::string>& key_set, std::string id_name, std::string variant_name); int update_values_from_single_to_multi(DynamicPrintConfig& multi_config, std::set<std::string>& key_set, std::string id_name, std::string variant_name);
int update_values_from_multi_to_multi(DynamicPrintConfig& new_config, std::set<std::string>& key_set, std::string id_name, std::string variant_name, std::vector<std::string>& extruder_variants); int update_values_from_multi_to_multi(DynamicPrintConfig& new_config, std::set<std::string>& key_set, std::string id_name, std::string variant_name, std::vector<std::string>& extruder_variants);
+15 -1
View File
@@ -424,8 +424,16 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex
[this](size_t i, size_t j) { return m_layer_outlines[i].second.size() < m_layer_outlines[j].second.size(); }); [this](size_t i, size_t j) { return m_layer_outlines[i].second.size() < m_layer_outlines[j].second.size(); });
// Layer range for which the collisions will be calculated. // Layer range for which the collisions will be calculated.
// Another thread may have advanced getMaxCalculatedLayer() past max_layer_idx after this calculation
// was requested. Bail out in that case, otherwise the layer range would be negative and allocating
// it would throw std::length_error out of a parallel task.
const LayerIndex start_layer = 1 + m_collision_cache.getMaxCalculatedLayer(radius);
if (start_layer > max_layer_idx) {
BOOST_LOG_TRIVIAL(debug) << "Requested calculation for value already calculated ?";
return;
}
LayerPolygonCache data; LayerPolygonCache data;
data.allocate(m_collision_cache.getMaxCalculatedLayer(radius) + 1, max_layer_idx + 1); data.allocate(start_layer, max_layer_idx + 1);
const bool calculate_placable = m_support_rests_on_model && radius == 0; const bool calculate_placable = m_support_rests_on_model && radius == 0;
LayerPolygonCache data_placeable; LayerPolygonCache data_placeable;
@@ -804,6 +812,12 @@ void TreeModelVolumes::calculateWallRestrictions(const std::vector<RadiusLayerPa
const coord_t radius = keys[key_idx].first; const coord_t radius = keys[key_idx].first;
const LayerIndex max_required_layer = keys[key_idx].second; const LayerIndex max_required_layer = keys[key_idx].second;
const coord_t min_layer_bottom = std::max(1, m_wall_restrictions_cache.getMaxCalculatedLayer(radius)); const coord_t min_layer_bottom = std::max(1, m_wall_restrictions_cache.getMaxCalculatedLayer(radius));
if (min_layer_bottom > max_required_layer) {
// Another thread has calculated this range in the meantime. Continuing would make
// buffer_size negative and allocating it would throw std::length_error.
BOOST_LOG_TRIVIAL(debug) << "Requested calculation for value already calculated ?";
continue;
}
const size_t buffer_size = max_required_layer + 1 - min_layer_bottom; const size_t buffer_size = max_required_layer + 1 - min_layer_bottom;
std::vector<Polygons> data(buffer_size, Polygons{}); std::vector<Polygons> data(buffer_size, Polygons{});
std::vector<Polygons> data_min; std::vector<Polygons> data_min;
+59 -57
View File
@@ -12,6 +12,7 @@
#include <thread> #include <thread>
#include <tbb/parallel_for.h> #include <tbb/parallel_for.h>
#include <tbb/task_arena.h> #include <tbb/task_arena.h>
#include <tbb/task_scheduler_observer.h>
#include "Thread.hpp" #include "Thread.hpp"
#include "Utils.hpp" #include "Utils.hpp"
@@ -212,70 +213,71 @@ bool is_main_thread_active()
return get_main_thread_id() == boost::this_thread::get_id(); return get_main_thread_id() == boost::this_thread::get_id();
} }
// Spawn (n - 1) worker threads on Intel TBB thread pool and name them by an index and a system thread ID. // Name the current TBB worker thread and set its locale to "C", so that the G-code generator
// Also it sets locale of the worker threads to "C" for the G-code generator to produce "." as a decimal separator. // produces "." as a decimal separator. Called once per worker thread, before it runs its first task.
static void setup_tbb_worker_thread()
{
static std::atomic<size_t> s_worker_idx{ 0 };
std::ostringstream name;
name << "slic3r_tbb_" << (1 + s_worker_idx.fetch_add(1, std::memory_order_relaxed));
set_current_thread_name(name.str().c_str());
#ifdef _WIN32
_configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
std::setlocale(LC_ALL, "C");
#else
// We are leaking some memory here, because the newlocale() produced memory will never be released.
// This is not a problem though, as there will be a maximum one worker thread created per physical thread.
uselocale(newlocale(
#ifdef __APPLE__
LC_ALL_MASK
#else // some Unix / Linux / BSD
LC_ALL
#endif
, "C", nullptr));
#endif
}
// Sets up the TBB worker threads of the arena of the thread, which activated the observation.
// A worker sets itself up on entry to the arena, before it executes its first task, thus unlike a barrier
// inside a parallel_for, this does not depend on TBB running any number of tasks simultaneously.
class TBBWorkerThreadSetupObserver : public tbb::task_scheduler_observer
{
public:
TBBWorkerThreadSetupObserver() { this->observe(true); }
void on_scheduler_entry(bool is_worker) override
{
// Leave the external threads (the calling / UI thread) alone, their name and locale must not be modified here.
if (! is_worker)
return;
// A worker thread enters an arena many times, while its name and locale have to be set just once.
static thread_local bool initialized = false;
if (initialized)
return;
initialized = true;
setup_tbb_worker_thread();
}
};
// Name the threads of the Intel TBB thread pool by an index and set their locale to "C"
// for the G-code generator to produce "." as a decimal separator.
// Formerly all the worker threads were caught inside a single parallel_for, which was held on a condition
// variable barrier until max_concurrency() of its chunks were running. TBB guarantees no such simultaneity,
// thus the barrier was able to block the slicing threads indefinitely. The TBB scheduler observer below
// sets each worker up on its own, thus no two chunks have to run at the same time.
void name_tbb_thread_pool_threads_set_locale() void name_tbb_thread_pool_threads_set_locale()
{ {
static bool initialized = false;
if (initialized)
return;
initialized = true;
// see GH issue #5661 PrusaSlicer hangs on Linux when run with non standard task affinity
// TBB will respect the task affinity mask on Linux and spawn less threads than std::thread::hardware_concurrency().
// const size_t nthreads_hw = std::thread::hardware_concurrency();
const size_t nthreads_hw = tbb::this_task_arena::max_concurrency();
size_t nthreads = nthreads_hw;
#ifdef SLIC3R_PROFILE #ifdef SLIC3R_PROFILE
// Shiny profiler is not thread safe, thus disable parallelization. // Shiny profiler is not thread safe, thus disable parallelization.
disable_multi_threading(); disable_multi_threading();
nthreads = 1;
#endif #endif
size_t nthreads_running(0); // An observer is local to the arena of the thread which activates it, thus one observer is registered
std::condition_variable cv; // per calling thread. Being function local and thread local, it is also initialized exactly once per
std::mutex cv_m; // thread without a race. It is intentionally never destroyed, as it has to stay alive as long as the
auto master_thread_id = std::this_thread::get_id(); // TBB scheduler may notify it, which includes the shutdown of the process.
tbb::parallel_for( static thread_local tbb::task_scheduler_observer *observer = new TBBWorkerThreadSetupObserver();
tbb::blocked_range<size_t>(0, nthreads, 1), (void)observer;
[&nthreads_running, nthreads, &master_thread_id, &cv, &cv_m](const tbb::blocked_range<size_t> &range) {
assert(range.begin() + 1 == range.end());
if (std::unique_lock<std::mutex> lk(cv_m); ++nthreads_running == nthreads) {
lk.unlock();
// All threads are spinning.
// Wake them up.
cv.notify_all();
} else {
// Wait for the last thread to wake the others.
cv.wait(lk, [&nthreads_running, nthreads]{return nthreads_running == nthreads;});
}
auto thread_id = std::this_thread::get_id();
if (thread_id == master_thread_id) {
// The calling thread runs the 0'th task.
assert(range.begin() == 0);
} else {
assert(range.begin() > 0);
std::ostringstream name;
name << "slic3r_tbb_" << range.begin();
set_current_thread_name(name.str().c_str());
// Set locales of the worker thread to "C".
#ifdef _WIN32
_configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
std::setlocale(LC_ALL, "C");
#else
// We are leaking some memory here, because the newlocale() produced memory will never be released.
// This is not a problem though, as there will be a maximum one worker thread created per physical thread.
uselocale(newlocale(
#ifdef __APPLE__
LC_ALL_MASK
#else // some Unix / Linux / BSD
LC_ALL
#endif
, "C", nullptr));
#endif
}
});
} }
} }
+8 -1
View File
@@ -28,6 +28,10 @@ TriangleSetSamples sample_its_uniform_parallel(size_t samples_count, const index
area_sum_to_triangle_idx[area_sum] = t_idx; area_sum_to_triangle_idx[area_sum] = t_idx;
} }
if (area_sum_to_triangle_idx.empty())
// No triangle to sample from.
return {};
std::mt19937_64 mersenne_engine { 27644437 }; std::mt19937_64 mersenne_engine { 27644437 };
// random numbers on interval [0, 1) // random numbers on interval [0, 1)
std::uniform_real_distribution<double> fdistribution; std::uniform_real_distribution<double> fdistribution;
@@ -50,7 +54,10 @@ TriangleSetSamples sample_its_uniform_parallel(size_t samples_count, const index
tbb::blocked_range<size_t> r) { tbb::blocked_range<size_t> r) {
for (size_t s_idx = r.begin(); s_idx < r.end(); ++s_idx) { for (size_t s_idx = r.begin(); s_idx < r.end(); ++s_idx) {
double t_sample = random_samples[s_idx].x() * area_sum; double t_sample = random_samples[s_idx].x() * area_sum;
size_t t_idx = area_sum_to_triangle_idx.upper_bound(t_sample)->second; // The keys of area_sum_to_triangle_idx are accumulated areas in double precision, while area_sum
// is a float, thus t_sample may reach or exceed the largest key and upper_bound() may return end().
auto t_it = area_sum_to_triangle_idx.upper_bound(t_sample);
size_t t_idx = (t_it == area_sum_to_triangle_idx.end() ? std::prev(t_it) : t_it)->second;
double sq_u = std::sqrt(random_samples[s_idx].y()); double sq_u = std::sqrt(random_samples[s_idx].y());
double v = random_samples[s_idx].z(); double v = random_samples[s_idx].z();
+8 -23
View File
@@ -819,9 +819,7 @@ int OrcaCloudServiceAgent::user_logout(bool request)
} }
} }
// An explicit logout also wipes the backend the token storage option is not using, so a token clear_session();
// stranded by switching that option cannot sign the account back in later.
clear_session(/*all_backends=*/request);
return BAMBU_NETWORK_SUCCESS; return BAMBU_NETWORK_SUCCESS;
} }
@@ -1606,9 +1604,7 @@ void OrcaCloudServiceAgent::persist_user_secret(const std::string& secret)
} }
} }
if (stored) { (void) stored;
secret_stored = true;
}
} }
bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret) bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
@@ -1648,7 +1644,6 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
} }
if (integrity_ok && aes256gcm_decrypt(encoded_payload, key, plain) && !plain.empty()) { if (integrity_ok && aes256gcm_decrypt(encoded_payload, key, plain) && !plain.empty()) {
secret_stored = true;
out_secret = plain; out_secret = plain;
// Upgrade legacy payloads to signed format // Upgrade legacy payloads to signed format
if (payload.rfind("v2:", 0) != 0) { if (payload.rfind("v2:", 0) != 0) {
@@ -1666,7 +1661,6 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
if (store.Load(SECRET_STORE_SERVICE, username, secret) && secret.IsOk()) { if (store.Load(SECRET_STORE_SERVICE, username, secret) && secret.IsOk()) {
out_secret.assign(static_cast<const char*>(secret.GetData()), secret.GetSize()); out_secret.assign(static_cast<const char*>(secret.GetData()), secret.GetSize());
if (!out_secret.empty()) { if (!out_secret.empty()) {
secret_stored = true;
return true; return true;
} }
} }
@@ -1676,20 +1670,11 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
return false; return false;
} }
void OrcaCloudServiceAgent::clear_user_secret(bool all_backends) void OrcaCloudServiceAgent::clear_user_secret()
{ {
// Nothing this process loaded or saved: leave the store alone. Deleting would only cost a wxSecretStore store = wxSecretStore::GetDefault();
// keychain round trip (or a hang while the keychain is unresponsive) and could remove a if (store.IsOk()) {
// login another instance just saved. store.Delete(SECRET_STORE_SERVICE);
if (!secret_stored.exchange(false) && !all_backends) {
return;
}
if (all_backends || !m_use_encrypted_token_file) {
wxSecretStore store = wxSecretStore::GetDefault();
if (store.IsOk()) {
store.Delete(SECRET_STORE_SERVICE);
}
} }
compute_fallback_path(); compute_fallback_path();
@@ -2038,13 +2023,13 @@ bool OrcaCloudServiceAgent::set_user_session(const json& session_json, bool noti
return success; return success;
} }
void OrcaCloudServiceAgent::clear_session(bool all_backends) void OrcaCloudServiceAgent::clear_session()
{ {
{ {
std::lock_guard<std::mutex> lock(session_mutex); std::lock_guard<std::mutex> lock(session_mutex);
session = SessionInfo{}; session = SessionInfo{};
} }
clear_user_secret(all_backends); clear_user_secret();
} }
// ============================================================================ // ============================================================================
+2 -7
View File
@@ -326,7 +326,7 @@ public:
void persist_user_secret(const std::string& secret); void persist_user_secret(const std::string& secret);
bool load_user_secret(std::string& out_secret); bool load_user_secret(std::string& out_secret);
void clear_user_secret(bool all_backends = false); void clear_user_secret();
// Token refresh helpers // Token refresh helpers
bool refresh_if_expiring(std::chrono::seconds skew, const std::string& reason); bool refresh_if_expiring(std::chrono::seconds skew, const std::string& reason);
@@ -344,7 +344,7 @@ public:
bool persist = true); bool persist = true);
// Accepts either nested Orca cloud / GoTrue session JSON or flat WebView token JSON. // Accepts either nested Orca cloud / GoTrue session JSON or flat WebView token JSON.
bool set_user_session(const nlohmann::json& session_json, bool notify_login = true); bool set_user_session(const nlohmann::json& session_json, bool notify_login = true);
void clear_session(bool all_backends = false); void clear_session();
static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = ""); static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = "");
@@ -413,11 +413,6 @@ private:
// Member variables - auth state // Member variables - auth state
PkceBundle pkce_bundle; PkceBundle pkce_bundle;
std::string secret_fallback_path; std::string secret_fallback_path;
// Set once this process has read a secret from the store or written one. Unless the user logs
// out explicitly, clear_user_secret() only touches the store while it is set, so a logged-out
// instance (the GUI polls the login status every 2 s) makes no keychain calls and cannot wipe
// a login another instance saved.
std::atomic_bool secret_stored{false};
SessionHandler session_handler; SessionHandler session_handler;
OnLoginCompleteHandler on_login_complete_handler; OnLoginCompleteHandler on_login_complete_handler;
SessionInfo session; SessionInfo session;
-152
View File
@@ -508,158 +508,6 @@ TEST_CASE("load_from_json hands a preset's include list to the caller instead of
CHECK(config.opt_string("machine_end_gcode") == "M84"); CHECK(config.opt_string("machine_end_gcode") == "M84");
} }
TEST_CASE("handle_legacy maps BambuStudio keys and values to their OrcaSlicer equivalents", "[Config]") {
struct Row { std::string key, value, expected_key, expected_value; };
const Row row = GENERATE(values<Row>({
{"role_base_wipe_speed", "0", "role_based_wipe_speed", "0"},
{"no_slow_down_for_cooling_on_outwalls", "1", "dont_slow_down_outer_wall", "1"},
{"process_notes", "note", "notes", "note"},
{"prime_tower_max_speed", "70", "wipe_tower_max_purge_speed", "70"},
{"enable_support_ironing", "1", "support_ironing", "1"},
{"sparse_infill_lattice_angle_1", "-30", "lateral_lattice_angle_1", "-30"},
{"sparse_infill_lattice_angle_2", "20", "lateral_lattice_angle_2", "20"},
{"lattice_angle_1", "-30", "lateral_lattice_angle_1", "-30"},
{"lattice_angle_2", "20", "lateral_lattice_angle_2", "20"},
{"sparse_infill_pattern", "2dlattice", "sparse_infill_pattern", "lateral-lattice"},
{"sparse_infill_pattern", "2dhoneycomb", "sparse_infill_pattern", "lateral-honeycomb"},
{"support_style", "tree_organic", "support_style", "organic"},
{"raft_first_layer_expansion", "-1", "raft_first_layer_expansion", "2"},
{"raft_first_layer_expansion", "3.5", "raft_first_layer_expansion", "3.5"},
{"ensure_vertical_shell_thickness", "enabled", "ensure_vertical_shell_thickness", "ensure_all"},
{"ensure_vertical_shell_thickness", "partial", "ensure_vertical_shell_thickness", "ensure_moderate"},
{"ensure_vertical_shell_thickness", "disabled", "ensure_vertical_shell_thickness", "none"},
{"top_one_wall_type", "not apply", "only_one_wall_top", "0"},
{"top_one_wall_type", "all top", "only_one_wall_top", "1"},
{"top_one_wall_type", "topmost", "only_one_wall_top", "1"},
{"prime_tower_rib_wall", "1", "wipe_tower_wall_type", "rib"},
{"prime_tower_rib_wall", "0", "wipe_tower_wall_type", "rectangle"},
{"prime_tower_rib_wall", "true", "wipe_tower_wall_type", "rib"},
// A rename touches whole entries, not keys that contain the old name.
{"different_settings_to_system", "lattice_angle_1;sparse_infill_lattice_angle_1;wall_loops", "different_settings_to_system",
"lateral_lattice_angle_1;lateral_lattice_angle_1;wall_loops"},
// different_settings_to_system and JSON arrays pass the key alone
{"prime_tower_rib_wall", "", "wipe_tower_wall_type", ""},
{"top_one_wall_type", "", "only_one_wall_top", ""},
{"enable_height_slowdown", "1", "", "1"},
{"counter_coef_2", "0.025", "", "0.025"},
}));
t_config_option_key key = row.key;
std::string value = row.value;
PrintConfigDef::handle_legacy(key, value);
INFO(row.key << " = " << row.value);
CHECK(key == row.expected_key);
CHECK(value == row.expected_value);
}
TEST_CASE("A BambuStudio project config loads its renamed settings without substitutions", "[Config]") {
ScopedTemporaryFile tmp(".json");
{
boost::nowide::ofstream ofs(tmp.string());
ofs << R"({"role_base_wipe_speed":"0","no_slow_down_for_cooling_on_outwalls":["1","0"],)"
R"("process_notes":"note","prime_tower_max_speed":"70","enable_support_ironing":"1",)"
R"("sparse_infill_pattern":"2dlattice","sparse_infill_lattice_angle_1":"-30",)"
R"("support_style":"tree_organic","ensure_vertical_shell_thickness":"partial",)"
R"("top_one_wall_type":"not apply","prime_tower_rib_wall":"0","raft_first_layer_expansion":"-1",)"
R"("enable_height_slowdown":["1"]})";
}
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
ConfigSubstitutionContext substitutions(ForwardCompatibilitySubstitutionRule::Disable);
std::map<std::string, std::string> key_values;
std::string reason;
REQUIRE(config.load_from_json(tmp.string(), substitutions, true, key_values, reason) == 0);
CHECK(substitutions.substitutions.empty());
CHECK(substitutions.unrecogized_keys == std::vector<std::string>{"enable_height_slowdown"});
CHECK_FALSE(config.opt_bool("role_based_wipe_speed"));
CHECK(config.option<ConfigOptionBools>("dont_slow_down_outer_wall")->values == std::vector<unsigned char>{1, 0});
CHECK(config.opt_string("notes") == "note");
CHECK_THAT(config.opt_float("wipe_tower_max_purge_speed"), Catch::Matchers::WithinAbs(70., 1e-9));
CHECK(config.opt_bool("support_ironing"));
CHECK(config.opt_enum<InfillPattern>("sparse_infill_pattern") == ipLateralLattice);
CHECK_THAT(config.opt_float("lateral_lattice_angle_1"), Catch::Matchers::WithinAbs(-30., 1e-9));
CHECK(config.opt_enum<SupportMaterialStyle>("support_style") == smsTreeOrganic);
CHECK(config.opt_enum<EnsureVerticalShellThickness>("ensure_vertical_shell_thickness") == evstModerate);
CHECK_FALSE(config.opt_bool("only_one_wall_top"));
CHECK(config.opt_enum<WipeTowerWallType>("wipe_tower_wall_type") == wtwRectangle);
CHECK_THAT(config.opt_float("raft_first_layer_expansion"), Catch::Matchers::WithinAbs(2., 1e-9));
CHECK(config.validate().count("raft_first_layer_expansion") == 0);
}
TEST_CASE("load_from_json reads a BambuStudio nil slot as not set", "[Config]") {
ScopedTemporaryFile tmp(".json");
{
boost::nowide::ofstream ofs(tmp.string());
// Keys after retraction_length in file order must still load.
ofs << R"({"layer_height":"0.2","retraction_length":["0.8","nil"],"wall_loops":"3","z_hop":["nil","nil"],)"
R"("z_hop_types":["Spiral Lift","nil"]})";
}
std::map<std::string, std::string> key_values;
std::string reason;
SECTION("a loader that doesn't merge onto a parent still rejects nil") {
DynamicPrintConfig config;
ConfigSubstitutionContext context(ForwardCompatibilitySubstitutionRule::Enable);
config.load_from_json(tmp.string(), context, true, key_values, reason);
CHECK_FALSE(reason.empty());
}
SECTION("a loader that opts in reads it as not set") {
DynamicPrintConfig config;
ConfigSubstitutionContext context(ForwardCompatibilitySubstitutionRule::Enable);
context.accept_nil = true;
REQUIRE(config.load_from_json(tmp.string(), context, true, key_values, reason) == 0);
CHECK(reason.empty());
const auto *default_length = static_cast<const ConfigOptionFloats*>(print_config_def.get("retraction_length")->default_value.get());
const auto &retraction_length = config.option<ConfigOptionFloats>("retraction_length")->values;
REQUIRE(retraction_length.size() == 2);
CHECK_THAT(retraction_length[0], Catch::Matchers::WithinAbs(0.8, 1e-9));
CHECK_THAT(retraction_length[1], Catch::Matchers::WithinAbs(default_length->get_at(1), 1e-9));
std::unique_ptr<ConfigOption> default_types(print_config_def.get("z_hop_types")->create_default_option());
const std::vector<std::string> type_defaults = static_cast<const ConfigOptionVectorBase*>(default_types.get())->vserialize();
CHECK(static_cast<const ConfigOptionVectorBase*>(config.option("z_hop_types"))->vserialize() ==
std::vector<std::string>{"Spiral Lift", type_defaults[1 % type_defaults.size()]});
CHECK(context.nil_slots == std::map<std::string, std::vector<size_t>>{{"retraction_length", {1}}, {"z_hop_types", {1}}});
CHECK_FALSE(config.has("z_hop"));
CHECK(config.opt_int("wall_loops") == 3);
}
}
TEST_CASE("A nozzle variant that was nil keeps the parent preset's value", "[Config]") {
auto printer = [](std::vector<double> nozzle_diameter, std::vector<double> retraction_length, std::vector<double> max_speed_x) {
DynamicPrintConfig config;
config.set_key_value("printer_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive High Flow"}));
config.set_key_value("printer_extruder_id", new ConfigOptionInts({1, 1}));
// Not a per-variant key, so the merge copies it whole.
config.set_key_value("nozzle_diameter", new ConfigOptionFloats(nozzle_diameter));
config.set_key_value("retraction_length", new ConfigOptionFloats(retraction_length));
// Two values per variant: normal and silent mode.
config.set_key_value("machine_max_speed_x", new ConfigOptionFloats(max_speed_x));
return config;
};
auto check_values = [](const DynamicPrintConfig &config, const char *key, const std::vector<double> &expected) {
const std::vector<double> &values = config.option<ConfigOptionFloats>(key)->values;
INFO(key);
REQUIRE(values.size() == expected.size());
for (size_t i = 0; i < expected.size(); ++i)
CHECK_THAT(values[i], Catch::Matchers::WithinAbs(expected[i], 1e-9));
};
DynamicPrintConfig parent = printer({0.4, 0.6}, {0.6, 0.5}, {500, 200, 400, 100});
// The nil slots of the child hold the option default after loading.
DynamicPrintConfig child = printer({0.2, 0.4}, {0.8, 0.4}, {500, 200, 300, 90});
const std::map<std::string, std::vector<size_t>> nil_slots{{"nozzle_diameter", {1}}, {"retraction_length", {1}}, {"machine_max_speed_x", {3}}};
parent.update_diff_values_to_child_config(child, "printer_extruder_id", "printer_extruder_variant",
printer_options_with_variant_1, printer_options_with_variant_2, &nil_slots);
check_values(parent, "nozzle_diameter", {0.2, 0.6});
check_values(parent, "retraction_length", {0.8, 0.5});
// Only the silent-mode value of the second variant was nil.
check_values(parent, "machine_max_speed_x", {500, 200, 300, 100});
}
TEST_CASE("save_to_json writes the same document to a stream as to a file", "[Config]") { TEST_CASE("save_to_json writes the same document to a stream as to a file", "[Config]") {
DynamicPrintConfig config; DynamicPrintConfig config;
config.set_key_value("layer_height", new ConfigOptionFloat(0.2)); config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
-1
View File
@@ -13,7 +13,6 @@ add_executable(${_TEST_NAME}_tests
test_prebuild_queue.cpp test_prebuild_queue.cpp
test_staged_build.cpp test_staged_build.cpp
test_network_versions.cpp test_network_versions.cpp
test_orca_cloud_agent.cpp
test_action_source.cpp test_action_source.cpp
test_plugin_host_api.cpp test_plugin_host_api.cpp
# Exercise seam enums and predicates through the embedded Python host API. # Exercise seam enums and predicates through the embedded Python host API.
+1 -9
View File
@@ -6,7 +6,6 @@
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <memory.h> #include <memory.h>
#include <stdexcept> #include <stdexcept>
#include <string>
#include <pybind11/embed.h> #include <pybind11/embed.h>
#include <pybind11/pybind11.h> #include <pybind11/pybind11.h>
@@ -26,15 +25,8 @@ void ensure_python_initialized()
config.parse_argv = 0; config.parse_argv = 0;
const auto python_home = boost::dll::program_location().parent_path() / "python"; const auto python_home = boost::dll::program_location().parent_path() / "python";
#ifdef _WIN32
const auto stdlib = python_home / "Lib";
#else
const auto stdlib = python_home / "lib" /
("python" + std::to_string(PY_MAJOR_VERSION) + "." + std::to_string(PY_MINOR_VERSION));
#endif
// Only a real runtime: a stray python/ folder (packages a test left behind) is not a home. if (boost::filesystem::exists(python_home)) {
if (boost::filesystem::exists(stdlib / "encodings")) {
const std::string home = python_home.string(); const std::string home = python_home.string();
const PyStatus status = PyConfig_SetBytesString(&config, &config.home, home.c_str()); const PyStatus status = PyConfig_SetBytesString(&config, &config.home, home.c_str());
@@ -1,86 +0,0 @@
#include <catch2/catch_all.hpp>
#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>
#include <memory>
#include <string>
#include "slic3r/Utils/OrcaCloudServiceAgent.hpp"
#include "test_utils.hpp"
using namespace Slic3r;
namespace fs = boost::filesystem;
namespace {
// The encrypted token file is the one secret backend a test can observe without a system
// keychain. Every agent pointed at the same directory shares it, like separate app instances
// share the keychain entry.
std::unique_ptr<OrcaCloudServiceAgent> make_file_backed_agent(const fs::path& dir)
{
auto agent = std::make_unique<OrcaCloudServiceAgent>(dir.string());
agent->set_use_encrypted_token_file(true);
agent->set_config_dir(dir.string());
return agent;
}
fs::path secret_file(const fs::path& dir) { return dir / secret_constants::USER_SECRET_FILENAME; }
} // namespace
TEST_CASE("Logging out removes the secret this instance saved", "[OrcaCloudServiceAgent]")
{
ScopedTemporaryDir dir("orca-secret");
auto agent = make_file_backed_agent(dir.path());
agent->persist_user_secret("refresh-token");
REQUIRE(fs::exists(secret_file(dir.path())));
agent->user_logout(false);
CHECK_FALSE(fs::exists(secret_file(dir.path())));
}
TEST_CASE("Logging out removes a secret this instance loaded from the store", "[OrcaCloudServiceAgent]")
{
ScopedTemporaryDir dir("orca-secret");
make_file_backed_agent(dir.path())->persist_user_secret("refresh-token");
auto agent = make_file_backed_agent(dir.path());
std::string secret;
REQUIRE(agent->load_user_secret(secret));
CHECK(secret == "refresh-token");
agent->user_logout(false);
CHECK_FALSE(fs::exists(secret_file(dir.path())));
}
TEST_CASE("Logging out leaves a secret this instance never loaded or saved alone", "[OrcaCloudServiceAgent]")
{
ScopedTemporaryDir dir("orca-secret");
make_file_backed_agent(dir.path())->persist_user_secret("refresh-token");
// A logged-out instance is asked to log out on every login-status poll.
auto other = make_file_backed_agent(dir.path());
other->user_logout(false);
other->user_logout(false);
CHECK(fs::exists(secret_file(dir.path())));
std::string secret;
REQUIRE(make_file_backed_agent(dir.path())->load_user_secret(secret));
CHECK(secret == "refresh-token");
}
TEST_CASE("Logging out leaves a secret this instance could not read alone", "[OrcaCloudServiceAgent]")
{
ScopedTemporaryDir dir("orca-secret");
// Written under another encryption key, e.g. by another OS user sharing the data directory.
fs::ofstream(secret_file(dir.path())) << "v2:0000:not-a-payload-this-user-can-decrypt";
auto agent = make_file_backed_agent(dir.path());
std::string secret;
REQUIRE_FALSE(agent->load_user_secret(secret));
agent->user_logout(false);
CHECK(fs::exists(secret_file(dir.path())));
}
@@ -38,9 +38,6 @@ namespace {
// before this destructor's shutdown() runs. // before this destructor's shutdown() runs.
struct ScopedPluginManager struct ScopedPluginManager
{ {
// Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log,
// which would otherwise land in the working directory.
ScopedDataDir python_data_dir{"plugin-python"};
bool initialized = PluginManager::instance().initialize(); bool initialized = PluginManager::instance().initialize();
~ScopedPluginManager() ~ScopedPluginManager()
@@ -31,9 +31,6 @@ namespace {
// same as any other plugin. // same as any other plugin.
struct ScopedManagerShutdown struct ScopedManagerShutdown
{ {
// Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log,
// which would otherwise land in the working directory.
ScopedDataDir python_data_dir{"plugin-python"};
bool initialized = PluginManager::instance().initialize(); bool initialized = PluginManager::instance().initialize();
~ScopedManagerShutdown() ~ScopedManagerShutdown()
@@ -42,9 +42,6 @@ namespace {
// Declare this FIRST in a test so it is destroyed last. // Declare this FIRST in a test so it is destroyed last.
struct ScopedPluginManager struct ScopedPluginManager
{ {
// Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log,
// which would otherwise land in the working directory.
ScopedDataDir python_data_dir{"plugin-python"};
bool initialized = false; bool initialized = false;
ScopedPluginManager() { initialized = PluginManager::instance().initialize(); } ScopedPluginManager() { initialized = PluginManager::instance().initialize(); }
@@ -12,8 +12,6 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include "plugin_test_utils.hpp"
namespace py = pybind11; namespace py = pybind11;
using namespace Slic3r; using namespace Slic3r;
@@ -23,9 +21,6 @@ namespace {
// into Python unless PythonInterpreter::instance() reports initialized. // into Python unless PythonInterpreter::instance() reports initialized.
struct ScopedPluginManager struct ScopedPluginManager
{ {
// Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log,
// which would otherwise land in the working directory.
ScopedDataDir python_data_dir{"plugin-python"};
bool initialized = PluginManager::instance().initialize(); bool initialized = PluginManager::instance().initialize();
~ScopedPluginManager() ~ScopedPluginManager()
@@ -35,9 +35,6 @@ namespace {
struct ScopedPluginManager struct ScopedPluginManager
{ {
// Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log,
// which would otherwise land in the working directory.
ScopedDataDir python_data_dir{"plugin-python"};
bool initialized = false; bool initialized = false;
ScopedPluginManager() { initialized = PluginManager::instance().initialize(); } ScopedPluginManager() { initialized = PluginManager::instance().initialize(); }