Belt brim: fixes from review

Six issues found by reviewing the previous commit against belt-printer, two of
them release-blocking.

Data race (high).  Print::process() runs generate_support_material() for all
objects in a tbb::parallel_for, and make_belt_brim() runs at its tail, but
belt_brim_obstacles() read every OTHER object's support_layers() - which a
concurrent task may be inside clear_support_layers() deleting.  That is a
use-after-free, and even when it survives, the obstacle set depends on which
object finishes first.  Only this object's own supports are consulted now; they
are complete at that point.  Foreign objects still contribute their slices,
which are finished and immutable before the support phase.

Apron bands dropped (high), two separate causes.  An apron band prints below
its own object's first layer, but another object can already be printing at
that print_z, in which case process_layer() takes the ordinary path and never
emitted the band - the emission is now shared by both paths.  Separately, a
band whose print_z matched a support layer of the SAME object was overwritten
in the print-wide merge, which keeps one record per object per z and could not
detect the collision because LayerToPrint::layer() is null for a band.  The
per-object pairing loop is now a three-way merge over object, support and apron
streams, so each object contributes at most one record per z.

Multi-instance was far too strict (medium).  It refused belt brim for every
multi-instance object, killing plain brim width and inner brim too, and only
warned when a leading length was set.  Only movement ALONG the belt changes an
instance's belt-floor Z, so copies side by side ACROSS the belt share one set of
bands perfectly well; belt_brim_instances_compatible() now tests just that, and
the warning fires whenever the brim is actually suppressed.

Apron layer bookkeeping (medium).  Apron layers count toward m_layer_count and
advance m_layer_index, but emitted no Z/height tags, left m_last_layer_z,
m_max_layer_z and m_last_height stale - so the first object layer computed its
height against a pre-apron Z - and skipped before_layer_change_gcode and
layer_change_gcode entirely.  All of that now matches the ordinary path.

Obstacle cost (low).  belt_brim_obstacles() ran a full-plate union per band.
A bounding-box pre-filter drops non-overlapping objects before materialising any
polygon, and the union is skipped for trivial inputs.

Deliberately unchanged: every apron band still reports cooling layer_id 0.
CoolingBuffer uses it for the initial_layer_fan_speed override and the
close_fan_the_first_x_layers gate, and every band lies on the belt plane itself,
so it is all first-layer material by the only definition that means anything on
a belt.  Numbering the bands would ramp the fan up while still printing on the
belt.  Now documented at the assignment rather than left implicit.
This commit is contained in:
harrierpigeon
2026-08-06 01:08:44 -05:00
parent 55b4dca9bc
commit b1905ebc20
7 changed files with 282 additions and 42 deletions

View File

@@ -2125,22 +2125,20 @@ std::vector<GCode::LayerToPrint> GCode::collect_layers_to_print(const PrintObjec
std::vector<std::pair<double, double>> warning_ranges;
// Belt printers: the brim apron is stuck to the belt AHEAD of the part, which
// on a tilted belt means below the object's first layer. Those bands carry no
// object or support layer, so they are emitted first and handled by
// process_layer()'s brim-only branch. Already ordered lowest print_z first.
for (const BeltBrimBand &band : object.belt_brim_prologue()) {
LayerToPrint prologue_layer;
prologue_layer.belt_brim_band = &band;
prologue_layer.original_object = &object;
layers_to_print.push_back(prologue_layer);
}
// Pair the object layers with the support layers by z.
//
// Belt printers add a third stream: brim apron bands, which sit on the belt AHEAD of
// the part and so print below the object's first layer. They are merged here rather
// than pushed as standalone records, because a band's print_z can coincide with a
// support layer of this same object - and the print-wide merge downstream keeps only
// one record per object per z, so a standalone band would be silently overwritten.
size_t idx_object_layer = 0;
size_t idx_support_layer = 0;
size_t idx_brim_band = 0;
const auto &brim_bands = object.belt_brim_prologue(); // ordered by ascending print_z
const LayerToPrint* last_extrusion_layer = nullptr;
while (idx_object_layer < object.layers().size() || idx_support_layer < object.support_layers().size()) {
while (idx_object_layer < object.layers().size() || idx_support_layer < object.support_layers().size()
|| idx_brim_band < brim_bands.size()) {
LayerToPrint layer_to_print;
double print_z_min = std::numeric_limits<double>::max();
if (idx_object_layer < object.layers().size()) {
@@ -2153,6 +2151,11 @@ std::vector<GCode::LayerToPrint> GCode::collect_layers_to_print(const PrintObjec
print_z_min = std::min(print_z_min, layer_to_print.support_layer->print_z);
}
if (idx_brim_band < brim_bands.size()) {
layer_to_print.belt_brim_band = &brim_bands[idx_brim_band++];
print_z_min = std::min(print_z_min, layer_to_print.belt_brim_band->print_z);
}
if (layer_to_print.object_layer && layer_to_print.object_layer->print_z > print_z_min + EPSILON) {
layer_to_print.object_layer = nullptr;
--idx_object_layer;
@@ -2163,11 +2166,17 @@ std::vector<GCode::LayerToPrint> GCode::collect_layers_to_print(const PrintObjec
--idx_support_layer;
}
if (layer_to_print.belt_brim_band && layer_to_print.belt_brim_band->print_z > print_z_min + EPSILON) {
layer_to_print.belt_brim_band = nullptr;
--idx_brim_band;
}
layer_to_print.original_object = &object;
layers_to_print.push_back(layer_to_print);
bool has_extrusions = (layer_to_print.object_layer && layer_to_print.object_layer->has_extrusions())
|| (layer_to_print.support_layer && layer_to_print.support_layer->has_extrusions());
|| (layer_to_print.support_layer && layer_to_print.support_layer->has_extrusions())
|| (layer_to_print.belt_brim_band && ! layer_to_print.belt_brim_band->fills.empty());
// Check that there are extrusions on the very first layer. The case with empty
// first layer may result in skirt/brim in the air and maybe other issues.
@@ -5266,20 +5275,26 @@ LayerResult GCode::process_belt_brim_layer(
const bool last_layer,
const size_t single_object_instance_idx)
{
// layer_id 0: the apron precedes every object layer and nothing downstream
// indexes by it. spiral_vase_enable false: spiral vase is refused alongside
// belt brim in Print::validate(). cooling_buffer_flush true: an apron layer is
// a complete layer, and the default (object_layer || raft_layer || last_layer)
// would be false here, so fan and slowdown would never be applied to it.
// layer_id 0 is deliberate, not a placeholder. CoolingBuffer reads it for the
// initial_layer_fan_speed override and the close_fan_the_first_x_layers gate
// (CoolingBuffer.cpp), and every apron band is first-layer material by the only
// definition that means anything on a belt: it lies on the belt plane itself. Numbering
// the bands 1, 2, 3... would ramp the fan up while still printing on the belt.
// spiral_vase_enable false: spiral vase is refused alongside belt brim in
// Print::validate(). cooling_buffer_flush true: an apron layer is a complete layer, and
// the default (object_layer || raft_layer || last_layer) is false here, so fan and
// slowdown would otherwise never be applied to it.
LayerResult result { {}, 0, false, true };
if (layer_tools.extruders.empty())
// Nothing to extrude.
return result;
coordf_t print_z = 0.;
coordf_t height = 0.;
for (const LayerToPrint &ltp : layers)
if (ltp.belt_brim_band != nullptr) {
print_z = ltp.belt_brim_band->print_z;
height = ltp.belt_brim_band->height;
break;
}
@@ -5298,8 +5313,63 @@ LayerResult GCode::process_belt_brim_layer(
const unsigned int extruder_id = layer_tools.extruders.front();
if (m_writer->filament() == nullptr || m_writer->filament()->id() != extruder_id)
gcode += this->set_extruder(extruder_id, print_z);
// An apron band is a real printed layer: it is counted in m_layer_count, it advances
// m_layer_index through change_layer(), and the G-code viewer needs its Z/height tags.
// Keep the same caches and hooks the ordinary path maintains, or the first object layer
// would compute its height against a stale pre-apron Z and layer-change templates would
// skip these layers entirely.
{
char buf[64];
sprintf(buf, ";%s%g\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change).c_str(), print_z);
gcode += buf;
sprintf(buf, ";Z:%g\n", print_z);
gcode += buf;
const float band_height = float(height);
sprintf(buf, ";%s%g\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height).c_str(), band_height);
gcode += buf;
m_last_layer_z = float(print_z);
m_max_layer_z = std::max(m_max_layer_z, m_last_layer_z);
m_last_height = band_height;
}
if (! m_config.before_layer_change_gcode.value.empty()) {
DynamicConfig config;
config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index + 1));
config.set_key_value("layer_z", new ConfigOptionFloat(print_z));
config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z));
gcode += this->placeholder_parser_process("before_layer_change_gcode",
print.config().before_layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n";
}
gcode += this->change_layer(print_z);
if (! m_config.layer_change_gcode.value.empty()) {
DynamicConfig config;
config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index));
config.set_key_value("layer_z", new ConfigOptionFloat(print_z));
config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z));
gcode += this->placeholder_parser_process("layer_change_gcode",
print.config().layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n";
}
gcode += this->emit_belt_brim_bands(print, layers, single_object_instance_idx);
result.gcode = std::move(gcode);
return result;
}
// Emit every apron band carried by this set of layers.
//
// Shared by the brim-only branch above and the ordinary process_layer() path. Both need
// it: an apron band prints below its OWN object's first layer, but on a multi-object belt
// another object can already be printing at that print_z, in which case the layer has an
// object layer, takes the ordinary path, and the band would be silently dropped.
std::string GCode::emit_belt_brim_bands(const Print &print,
const std::vector<LayerToPrint> &layers,
const size_t single_object_instance_idx)
{
std::string gcode;
for (const LayerToPrint &ltp : layers) {
const BeltBrimBand *band = ltp.belt_brim_band;
if (band == nullptr || band->fills.empty() || ltp.original_object == nullptr)
@@ -5324,9 +5394,7 @@ LayerResult GCode::process_belt_brim_layer(
m_avoid_crossing_perimeters.disable_once();
}
}
result.gcode = std::move(gcode);
return result;
return gcode;
}
// Bedslinger model. The heavier the bed load, the lower the achievable Y acceleration for a given
@@ -5821,6 +5889,17 @@ LayerResult GCode::process_layer(
//BBS: set layer time fan speed after layer change gcode
gcode += ";_SET_FAN_SPEED_CHANGING_LAYER\n";
// Belt printers: an apron band prints below its own object's first layer, but with
// several objects on the belt another one can already be printing at this print_z.
// The layer then has an object layer and takes this ordinary path instead of the
// brim-only branch, so the band has to be emitted here or it would be dropped.
// Before any object extrusion at this Z, as the brim must go down first.
if (print.has_belt_brim()) {
const Vec2d saved_origin = m_origin;
gcode += this->emit_belt_brim_bands(print, layers, single_object_instance_idx);
this->set_origin(saved_origin);
}
//Calibration Layer-specific GCode
// ORCA-Belt: on belt printers the calibration object is counter-rotated to
// stand upright in slicing space on top of a support wedge, so its first