STEP import: a part's later instances in a window wait for its first, not end the window

A window of planned solids ended at the second instance of a part not yet
read, so an assembly that places a part several times in a row converted
in windows of one or two solids, each waiting for its slowest read before
the next began (Poppy Humanoid: 14 windows of 1 to 30 reads on 8 workers).
The window now always takes MAX_WINDOW solids; an instance of a part an
earlier job of the window reads is placed after that job, from the
orientation it found, as the next window would have placed it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 08:24:41 -04:00
committed by Irakli Kourtis
parent 025344dee2
commit 25642b6d26
3 changed files with 47 additions and 23 deletions
+42 -18
View File
@@ -764,27 +764,24 @@ impl Reader {
let first = self.converted;
// The next solids, across instances and parts, converted together: parts still to
// orient are shared out between the workers. A part that comes back within the window
// waits for the next one, which places the orientation its first instance found.
let mut last = first + 1;
if !single {
let mut fresh: HashSet<u64> = self.plan.get(first).map(|p| p.item).into_iter().collect();
while let Some(p) = self.plan.get(last)
&& last - first < MAX_WINDOW
{
if !self.oriented.contains_key(&p.item) && !fresh.insert(p.item) {
break;
}
last += 1;
}
}
// is placed once its first instance is read, from the orientation that instance found.
let last = if single { first + 1 } else { (first + MAX_WINDOW).min(self.plan.len()) };
self.converted = last;
let ex = self.ex.clone();
// Each planned solid of the window, with its job (`None`: a later instance of a part an
// earlier job of the window reads, placed after it).
let mut sources = Vec::new();
let mut jobs = Vec::new();
let mut reading = HashSet::new();
for i in first..last {
self.made.push(None);
let Some(p) = self.plan.get(i) else { continue };
let (item, units) = (p.item, p.units);
let placement = self.batches.get(p.batch).map_or(IDENTITY, |b| b.m);
if reading.contains(&item) {
sources.push((i, item, placement, None));
continue;
}
if let Some(again) = self.unreadable.get(&item) {
if let Some(msg) = again.clone() {
let label = self.label(item);
@@ -794,15 +791,42 @@ impl Reader {
}
let source = match self.oriented.get(&item) {
Some(part) => parallel::Source::Oriented(part.clone()),
None => parallel::Source::Item { item, units },
None => {
reading.insert(item);
parallel::Source::Item { item, units }
}
};
let placement = self.batches.get(p.batch).map_or(IDENTITY, |b| b.m);
sources.push((i, item));
sources.push((i, item, placement, Some(jobs.len())));
jobs.push(parallel::Job { source, placement, check_mesh: !self.unmeshed_checked.contains(&item) });
}
// Reading and orienting new parts is shared out between the workers.
let prepared = parallel::bodies(&ex, &jobs, self.workers);
for ((i, item), made) in sources.into_iter().zip(prepared) {
let mut prepared: Vec<Option<_>> = parallel::bodies(&ex, &jobs, self.workers).into_iter().map(Some).collect();
for (i, item, placement, job) in sources {
let made = match job {
Some(j) => match prepared.get_mut(j).and_then(Option::take) {
Some(made) => made,
None => continue,
},
// A later instance of a part read in this window: as it would be in the next
// window, after its first instance.
None => {
if let Some(again) = self.unreadable.get(&item) {
if let Some(msg) = again.clone() {
let label = self.label(item);
self.warn(format!("{label}: not imported: {msg}"));
}
continue;
}
let source = match self.oriented.get(&item) {
Some(part) => parallel::Source::Oriented(part.clone()),
None => match self.plan.get(i) {
Some(p) => parallel::Source::Item { item, units: p.units },
None => continue,
},
};
parallel::prepare(&ex, &parallel::Job { source, placement, check_mesh: !self.unmeshed_checked.contains(&item) })
}
};
let Some(e) = ex.get(item) else { continue };
let Some(batch) = self.plan.get(i).and_then(|p| self.batches.get(p.batch)) else { continue };
let (m, path, product, instance) = (batch.m, batch.path.clone(), batch.product.clone(), batch.instance.clone());
+1 -1
View File
@@ -70,7 +70,7 @@ pub(super) fn workers() -> usize {
}
}
fn prepare(ex: &Exchange, job: &Job) -> Prepared {
pub(super) fn prepare(ex: &Exchange, job: &Job) -> Prepared {
let _span = solidcraft_profile::span("import", "step.body");
let (part, read) = match &job.source {
Source::Oriented(part) => (part.clone(), None),
+4 -4
View File
@@ -772,9 +772,9 @@ fn instanced_parts_are_oriented_once() {
}
/// Distinct parts are oriented together, not one occurrence at a time: an assembly placing
/// two parts in turn converts in a few steps of many solids (the first finds both parts'
/// orientations side by side on the workers, the next places every other instance), where
/// converting instance by instance took one step each.
/// two parts in turn converts in one step (both parts' orientations found side by side on the
/// workers, every other instance placed after its part's first), where converting instance by
/// instance took one step each, and stopping the step at a part's second instance two.
#[test]
fn distinct_parts_convert_together_across_instances() {
let n = 24;
@@ -784,7 +784,7 @@ fn distinct_parts_convert_together_across_instances() {
while reading.step() {
steps += 1;
}
assert_eq!(steps, 2, "both parts in the first step, every other instance in the second");
assert_eq!(steps, 1, "both parts and every other instance in one step");
let threaded = reading.finish().unwrap();
assert_eq!(threaded.bodies.len(), n as usize);
assert!(threaded.warnings.is_empty(), "{:?}", threaded.warnings);