Large STEP imports: no more multi-minute hang, shared STEP text, cancel and progress (#925)

This commit is contained in:
bherbruck authored and GitHub committed 2026-10-14 05:01:12 -04:00
1 parent dfbf7dd6a8
commit 2f91338d3d
18 files changed
+525 -45

No files matched your search

+12 -4
View File
@@ -474,6 +474,8 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
};
let phase = r.phase.load(Ordering::Relaxed);
let (done, total) = r.progress.features();
// Long work inside one feature (a large STEP file) has progress of its own.
let work = r.progress.work().filter(|(_, d, t)| phase == 0 && d < t);
let screen = ctx.content_rect();
// Leave the title bar to the window. The resize edges are a foreground layer above this.
let cover = egui::Rect::from_min_max(egui::pos2(screen.left(), screen.top() + crate::toolbar::APP_BAR_HEIGHT), screen.max);
@@ -538,16 +540,22 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
ui.label(if cancelled {
"Waiting for the previous operation to stop...".to_string()
} else {
match phase {
1 => "Preparing the view...".into(),
2 => "Preparing the thumbnail...".into(),
match (phase, work) {
(_, Some(("reading", d, t))) => format!("Reading the file... {}%", d * 100 / t.max(1)),
(_, Some((_, d, t))) => format!("Converting solid {} of {t}", d + 1),
(1, _) => "Preparing the view...".into(),
(2, _) => "Preparing the thumbnail...".into(),
_ if total > 0 => format!("Rebuilding feature {} of {total}", (done + 1).min(total)),
_ => "Reading the file...".into(),
}
});
});
// Always there, so the card keeps its height as the phases change.
let fraction = if cancelled || total == 0 {
let fraction = if cancelled {
0.0
} else if let Some((_, d, t)) = work {
d as f32 / t.max(1) as f32
} else if total == 0 {
0.0
} else if phase == 0 {
done as f32 / total as f32
+59 -16
View File
@@ -179,9 +179,13 @@ pub struct AccessMap {
pub scale: f32,
}
/// Vertices (or line instances) on the GPU, in as many buffers as the device's buffer size
/// limit needs: a large import's triangles can exceed one buffer (#883).
struct Batch {
buffer: wgpu::Buffer,
count: u32,
/// Each buffer and its vertex (instance) count.
parts: Vec<(wgpu::Buffer, u32)>,
/// Bytes per buffer (all but the last are full).
chunk: usize,
}
/// A chunk of planned bodies on the GPU: their evaluated mesh and a colour per vertex.
@@ -289,8 +293,8 @@ impl Batches {
let faces = mesh.vertex_face.get(part.vertices.start as usize..part.vertices.end as usize).unwrap_or_default();
bytes.extend(faces.iter().flat_map(|f| b.face_colors.get(*f as usize).copied().unwrap_or(last)));
}
if let Some(colors) = upload(device, "sc_planned_colors", &bytes, 4) {
out.push(PlannedDraw { mesh, colors: colors.buffer });
if let Some((colors, _)) = upload(device, "sc_planned_colors", &bytes, 4).and_then(|c| c.parts.into_iter().next()) {
out.push(PlannedDraw { mesh, colors });
}
}
out
@@ -663,13 +667,27 @@ fn upload(device: &wgpu::Device, label: &str, bytes: &[u8], stride: usize) -> Op
}
fn upload_with(device: &wgpu::Device, label: &str, bytes: &[u8], stride: usize, usage: wgpu::BufferUsages) -> Option<Batch> {
if bytes.len() < stride {
if bytes.len() < stride || stride == 0 {
return None;
}
Some(Batch {
buffer: device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(label), contents: bytes, usage }),
count: u32::try_from(bytes.len() / stride).unwrap_or(0),
})
let chunk = buffer_chunk(stride, device.limits().max_buffer_size);
let parts = bytes
.chunks(chunk)
.filter(|part| part.len() >= stride)
.map(|part| {
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(label), contents: part, usage });
(buffer, u32::try_from(part.len() / stride).unwrap_or(0))
})
.collect();
Some(Batch { parts, chunk })
}
/// Bytes per GPU buffer for vertices of `stride` bytes: within the device's limit and a
/// whole number of triangles and line pairs, so no primitive straddles two buffers.
fn buffer_chunk(stride: usize, max_buffer_size: u64) -> usize {
let unit = stride.max(1) * 6;
let max = usize::try_from(max_buffer_size).unwrap_or(usize::MAX).min(1 << 30);
(max / unit).max(1) * unit
}
/// Triangles (3 vertices of `TRI_SIZE` bytes each, position first) ordered far to near along
@@ -691,7 +709,10 @@ impl Batches {
let (Some(g), false) = (&self.glass, self.glass_cpu.is_empty()) else { return };
let turned = self.glass_sorted_for.is_none_or(|s| s[0] * back[0] + s[1] * back[1] + s[2] * back[2] < 0.9994);
if turned {
queue.write_buffer(&g.buffer, 0, &sort_back_to_front(&self.glass_cpu, back));
let sorted = sort_back_to_front(&self.glass_cpu, back);
for ((buffer, _), bytes) in g.parts.iter().zip(sorted.chunks(g.chunk.max(1))) {
queue.write_buffer(buffer, 0, bytes);
}
self.glass_sorted_for = Some(back);
}
}
@@ -796,15 +817,19 @@ impl egui_wgpu::CallbackTrait for ViewportCallback {
let tris = |pass: &mut wgpu::RenderPass<'static>, p: &wgpu::RenderPipeline, b: &Option<Batch>| {
if let Some(b) = b {
pass.set_pipeline(p);
pass.set_vertex_buffer(0, b.buffer.slice(..));
pass.draw(0..b.count, 0..1);
for (buffer, count) in &b.parts {
pass.set_vertex_buffer(0, buffer.slice(..));
pass.draw(0..*count, 0..1);
}
}
};
let lines = |pass: &mut wgpu::RenderPass<'static>, p: &wgpu::RenderPipeline, b: &Option<Batch>| {
if let Some(b) = b {
pass.set_pipeline(p);
pass.set_vertex_buffer(0, b.buffer.slice(..));
pass.draw(0..6, 0..b.count);
for (buffer, count) in &b.parts {
pass.set_vertex_buffer(0, buffer.slice(..));
pass.draw(0..6, 0..*count);
}
}
};
let (m, sk, h, pv) = (&res.model, &res.sketch, &res.highlight, &res.preview);
@@ -848,8 +873,10 @@ impl egui_wgpu::CallbackTrait for ViewportCallback {
if let Some((_, bind)) = res.textures.get(id) {
pass.set_pipeline(&res.image);
pass.set_bind_group(1, bind, &[]);
pass.set_vertex_buffer(0, q.buffer.slice(..));
pass.draw(0..q.count, 0..1);
for (buffer, count) in &q.parts {
pass.set_vertex_buffer(0, buffer.slice(..));
pass.draw(0..*count, 0..1);
}
}
}
tris(pass, &res.ghost, &pv.ghost);
@@ -1079,6 +1106,22 @@ mod tests {
mod glass_tests {
use super::*;
/// A large import's triangles once went into one buffer past the device's limit (524 MB
/// against 256 MB) and the app crashed (#883): they are split into whole triangles.
#[test]
fn large_scenes_are_split_into_buffers_within_the_device_limit() {
let limit = 256u64 << 20;
for stride in [TRI_SIZE, LINE_SIZE, 4] {
let chunk = buffer_chunk(stride, limit);
assert!(chunk as u64 <= limit && chunk > limit as usize / 2);
assert_eq!(chunk % (stride * 3), 0, "whole triangles");
assert_eq!(chunk % (stride * 2), 0, "whole line pairs");
}
let scene = 524_434_680usize;
assert_eq!(scene.div_ceil(buffer_chunk(TRI_SIZE, limit)), 2);
assert!(buffer_chunk(TRI_SIZE, 1) >= TRI_SIZE * 6, "a tiny limit still holds one unit");
}
#[test]
fn glass_is_drawn_far_to_near() {
// Two triangles at z = 5 and z = -5, given near first; looking down from +Z.
@@ -14,7 +14,8 @@ fn placed_step_body_uses_imported_opacity_until_a_design_look_overrides_it() {
.unwrap();
let mut document = doc::Document::default();
let component = document.add_component(Some("Imported part"), 0).unwrap();
let feature = document.add_feature(doc::FeatureKind::Import { file: "glass.step".into(), step, components: Vec::new() }, None).unwrap();
let feature =
document.add_feature(doc::FeatureKind::Import { file: "glass.step".into(), step: step.into(), components: Vec::new() }, None).unwrap();
document.features.iter_mut().find(|f| f.id == feature).unwrap().component = component;
let mut session = Session::new(document);
let source = session.model.state().bodies[0].name.clone();