Open large designs faster: meshes, face names and Measure on all cores (#916)

Display meshes after opening, the face names read when a feature finds a face by name, and inspect.measure over all bodies are now worked out per body on plain scoped threads (solidcraft_kernel::for_each_on_threads / map_on_threads), in body order and with the same results; the browser build stays serial.
This commit is contained in:
bherbruck authored and GitHub committed 2026-10-13 19:49:04 -04:00
1 parent c333889fc0
commit 6c61442bb2
7 files changed
+110 -6

No files matched your search

+2
View File
@@ -431,6 +431,8 @@ fn cmd_bench_open(args: &[String]) -> Result<(), String> {
}
let t = std::time::Instant::now();
let st = s.world_state();
// As the desktop app prepares a design it opened.
st.warm_meshes(|| true);
let triangles: usize = st.bodies.iter().map(|b| b.mesh().triangles.len()).sum();
println!("{}", json!({"step": "mesh", "ms": t.elapsed().as_secs_f64() * 1000.0, "bodies": st.bodies.len(), "triangles": triangles}));
Ok(())
+11
View File
@@ -619,6 +619,17 @@ impl ModelState {
pub fn body(&self, name: &str) -> Option<&ModelBody> {
self.bodies.iter().find(|b| b.name == name)
}
/// Make every body's display mesh now, on all cores (bodies are meshed independently and
/// each keeps its own mesh, so the meshes are those `ModelBody::mesh` makes one by one).
/// Stops starting bodies once `go_on` says no.
pub fn warm_meshes(&self, go_on: impl Fn() -> bool + Sync) {
kernel::for_each_on_threads(&self.bodies, 16, |b| {
if go_on() {
let _ = b.mesh();
}
});
}
pub fn sketch(&self, feature: u64) -> Option<&SolvedSketch> {
self.sketches.iter().find(|s| s.feature == feature).map(|s| &**s)
}
+8
View File
@@ -260,6 +260,14 @@ impl EdgeHit {
/// answering to it (merged into it, or the named piece told apart by the faces around it); else
/// the piece of it nearest `p` (`split`).
pub fn locate_face(st: &ModelState, name: &str, p: Vec3) -> Option<FaceHit> {
// Every body's names are read below: work out the ones not known yet on all cores first
// (each body's names are its own, whichever thread works them out).
if legacy_name(name).is_none() {
let bodies: Vec<&ModelBody> = st.bodies.iter().filter(|b| !b.body.is_mesh()).collect();
solidcraft_kernel::for_each_on_threads(&bodies, 16, |b| {
let _ = naming::names(b);
});
}
locate_in(st.bodies.iter().enumerate().filter(|(_, b)| !b.body.is_mesh()), name, p)
}
+6 -1
View File
@@ -66,7 +66,12 @@ fn measure(s: &mut Session, p: &Value) -> Result<Value> {
let want = string_list(p, "bodies");
let st = s.world_state();
let selected: Vec<_> = st.bodies.iter().filter(|b| want.is_empty() || want.contains(&b.name)).collect();
let mut bodies: Vec<Value> = selected.iter().map(|b| measure_json(b)).collect();
// Each body is measured on its own: on all cores, in body order.
let mut bodies: Vec<Value> = solidcraft_kernel::map_on_threads(&selected, 16, |b| measure_json(b))
.into_iter()
.zip(&selected)
.map(|(m, b)| m.unwrap_or_else(|| json!({"name": b.name, "error": "measuring this body failed"})))
.collect();
// Mass from each body's material.
for (b, body) in bodies.iter_mut().zip(selected) {
if let Some(v) = b["volume_mm3"].as_f64() {
+1
View File
@@ -56,6 +56,7 @@ mod numbered;
mod offset;
mod ops;
mod par;
pub use par::{for_each_on_threads, map_on_threads};
#[cfg(test)]
mod pinch_tests;
mod plane_fit;
+79
View File
@@ -22,11 +22,90 @@ pub(crate) fn map<T: Sync, R: Send>(what: &'static str, items: &[T], parallel: b
items.iter().map(one).collect()
}
/// Run `f` on every item, spread over plain threads (one per core, at most `max_threads`),
/// each pulling the next item; on the calling thread in the browser build or when threads
/// cannot be started. Each call runs under [`crate::guard`] (a panic is that item's loss).
///
/// Plain threads rather than the worker pool: an item may wait on a lazily filled cell that
/// another item is filling, and a pool worker that picked up a second item while it waited
/// inside its own item's parallel work could wait on itself.
pub fn for_each_on_threads<T: Sync>(items: &[T], max_threads: usize, f: impl Fn(&T) + Sync) {
let _ = map_on_threads(items, max_threads, f);
}
/// `f` over `items` as [`for_each_on_threads`] runs it, results in item order (`None` where an
/// item panicked).
pub fn map_on_threads<T: Sync, R: Send>(items: &[T], max_threads: usize, f: impl Fn(&T) -> R + Sync) -> Vec<Option<R>> {
let one = |item: &T| guard("parallel item", || Ok(f(item))).ok();
#[cfg(not(target_arch = "wasm32"))]
{
use std::sync::atomic::{AtomicUsize, Ordering};
let threads = std::thread::available_parallelism().map(usize::from).unwrap_or(1).min(max_threads).min(items.len());
if threads > 1 {
let next = AtomicUsize::new(0);
let work = || {
let mut done = Vec::new();
loop {
let i = next.fetch_add(1, Ordering::Relaxed);
let Some(item) = items.get(i) else { break };
done.push((i, one(item)));
}
done
};
let mut out: Vec<Option<R>> = std::iter::repeat_with(|| None).take(items.len()).collect();
std::thread::scope(|scope| {
// A thread that cannot start leaves its share to the others.
let helpers: Vec<_> =
(1..threads).filter_map(|_| std::thread::Builder::new().name("solidcraft-each".into()).spawn_scoped(scope, work).ok()).collect();
let mut all = work();
for h in helpers {
// `work` never panics (every item runs under the guard).
if let Ok(done) = h.join() {
all.extend(done);
}
}
for (i, r) in all {
if let Some(slot) = out.get_mut(i) {
*slot = r;
}
}
});
return out;
}
}
let _ = max_threads;
items.iter().map(one).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::KernelError;
#[test]
fn each_item_once_on_threads() {
let items: Vec<u32> = (0..1000).collect();
let seen: Vec<std::sync::atomic::AtomicU32> = (0..1000).map(|_| std::sync::atomic::AtomicU32::new(0)).collect();
for_each_on_threads(&items, 8, |x| {
seen[*x as usize].fetch_add(1, std::sync::atomic::Ordering::Relaxed);
assert!(*x != 500, "a panicking item is that item's loss");
});
assert!(seen.iter().all(|s| s.load(std::sync::atomic::Ordering::Relaxed) == 1));
}
#[test]
fn map_on_threads_keeps_order() {
let items: Vec<u32> = (0..1000).collect();
let out = map_on_threads(&items, 8, |x| {
assert!(*x != 7, "a panicking item is that item's loss");
x * 3
});
assert_eq!(out.len(), 1000);
for (i, r) in out.iter().enumerate() {
assert_eq!(*r, (i != 7).then_some(3 * i as u32));
}
}
#[test]
fn keeps_order_and_errors() {
let items: Vec<u32> = (0..1000).collect();
+3 -5
View File
@@ -370,11 +370,9 @@ fn compute(mut session: Session, command: &str, params: &Value, thumbnail: bool,
let placement_ms = view_began.elapsed().as_secs_f64() * 1000.0;
let mesh_began = std::time::Instant::now();
// Fit, picking and the first viewport frame must not trigger cold tessellation on the UI.
for b in &world.bodies {
if progress.is_cancelled() {
return cancelled();
}
let _ = b.mesh();
world.warm_meshes(|| !progress.is_cancelled());
if progress.is_cancelled() {
return cancelled();
}
let tessellation_ms = mesh_began.elapsed().as_secs_f64() * 1000.0;
phase.store(2, Ordering::Relaxed);