Skip to content

Commit 9dd8ba1

Browse files
authored
perf(geometry): reuse mesh topology bookkeeping (#4006)
* perf(geometry): reuse mesh topology bookkeeping (#3988) * docs(perf): record mesh bookkeeping qualification verdict
1 parent ae886b4 commit 9dd8ba1

10 files changed

Lines changed: 527 additions & 91 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ifc-lite/wasm": patch
3+
---
4+
5+
Reuse per-mesh topology bookkeeping and compact filtered triangle indices in place while preserving geometry output, traversal order and retained buffer bounds.

rust/geometry/src/mesh.rs

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -491,17 +491,19 @@ impl Mesh {
491491
self.indices.clear();
492492
return;
493493
}
494-
let mut valid = Vec::with_capacity(self.indices.len());
495-
for chunk in self.indices.chunks(3) {
496-
if chunk.len() == 3
497-
&& (chunk[0] as usize) < vertex_count
494+
let original_len = self.indices.len();
495+
let mut kept = 0;
496+
for read in (0..original_len / 3 * 3).step_by(3) {
497+
let chunk = [self.indices[read], self.indices[read + 1], self.indices[read + 2]];
498+
if (chunk[0] as usize) < vertex_count
498499
&& (chunk[1] as usize) < vertex_count
499-
&& (chunk[2] as usize) < vertex_count
500-
{
501-
valid.extend_from_slice(chunk);
500+
&& (chunk[2] as usize) < vertex_count {
501+
if kept != read { self.indices[kept..kept + 3].copy_from_slice(&chunk); }
502+
kept += 3;
502503
}
503504
}
504-
self.indices = valid;
505+
self.indices.truncate(kept);
506+
self.indices.shrink_to(original_len);
505507
}
506508

507509
/// Drop triangles that collapsed into degenerate needles when the mesh was
@@ -554,14 +556,13 @@ impl Mesh {
554556
let dist = |a: [f64; 3], b: [f64; 3]| -> f64 {
555557
((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2) + (a[2] - b[2]).powi(2)).sqrt()
556558
};
557-
558-
let mut kept = Vec::with_capacity(self.indices.len());
559-
for tri in self.indices.chunks_exact(3) {
559+
// #3988: compact only after a drop; bound retained capacity as before.
560+
let original_len = self.indices.len();
561+
let mut kept = 0;
562+
for read in (0..original_len / 3 * 3).step_by(3) {
563+
let tri = [self.indices[read], self.indices[read + 1], self.indices[read + 2]];
560564
let (ia, ib, ic) = (tri[0], tri[1], tri[2]);
561-
// Drop any out-of-range triangle BEFORE the unchecked `bits()` closure
562-
// indexes positions[] (unlike `vert()` below, `bits()` has no bounds
563-
// check). Matches the drop-not-panic contract of vert()/validate_indices;
564-
// sibling drop_thin_triangles guards the same way.
565+
// Guard before bits() indexes positions; invalid triangles are dropped.
565566
if ia as usize >= vertex_count
566567
|| ib as usize >= vertex_count
567568
|| ic as usize >= vertex_count
@@ -582,17 +583,16 @@ impl Mesh {
582583
let e2 = dist(vc, va);
583584
let min_edge = e0.min(e1).min(e2);
584585
let max_edge = e0.max(e1).max(e2);
585-
// Catastrophic needle: a sliver whose longest edge dwarfs its
586-
// shortest by >1e5. min_edge==0 is already handled by the bit check
587-
// above, so a finite ratio here means near-but-not-identical f32.
586+
// Drop catastrophic needles; bit-identical zero edges were handled above.
588587
if min_edge > 0.0 && max_edge / min_edge > MAX_ASPECT {
589588
continue;
590589
}
591-
kept.extend_from_slice(tri);
590+
if kept != read { self.indices[kept..kept + 3].copy_from_slice(&tri); }
591+
kept += 3;
592592
}
593-
self.indices = kept;
593+
self.indices.truncate(kept);
594+
self.indices.shrink_to(original_len);
594595
}
595-
596596
/// Check if mesh is empty
597597
#[inline]
598598
pub fn is_empty(&self) -> bool {

rust/geometry/src/mesh_orient.rs

Lines changed: 26 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -33,38 +33,11 @@ use crate::Mesh;
3333
use rustc_hash::FxHashMap;
3434
use std::cell::RefCell;
3535

36-
/// Incident-triangle record for one undirected welded edge. A boundary edge has
37-
/// one incident triangle and a manifold edge exactly two, so the two triangle
38-
/// slots are stored INLINE — replacing the old per-edge heap `Vec<usize>`, which
39-
/// allocated ~1.5 tiny Vecs per triangle and dominated the allocator churn of
40-
/// this pass on mesh-heavy models. `count` is the TRUE incidence; a value > 2
41-
/// marks a non-manifold edge, which the propagation skips before ever reading
42-
/// the slots. Only the first two triangles are consulted, stored in ascending
43-
/// scan order (identical to the old `Vec` push order), so the BFS traversal —
44-
/// and therefore every flip decision — is byte-identical.
45-
#[derive(Clone, Copy, Default)]
46-
struct EdgeInc {
47-
tris: [usize; 2],
48-
count: u32,
49-
}
50-
51-
impl EdgeInc {
52-
#[inline]
53-
fn push(&mut self, t: usize) {
54-
if (self.count as usize) < 2 {
55-
self.tris[self.count as usize] = t;
56-
}
57-
self.count += 1;
58-
}
59-
60-
/// The incident triangles the propagation may consult (the first two, in
61-
/// push order). Only reached for `count` of 1 or 2 — the `count > 2` path
62-
/// `continue`s first — so this yields exactly what the old `Vec` iterated.
63-
#[inline]
64-
fn incident(&self) -> &[usize] {
65-
&self.tris[..(self.count as usize).min(2)]
66-
}
67-
}
36+
#[path = "mesh_orient_adjacency.rs"]
37+
mod adjacency;
38+
use adjacency::EdgeAdjacency;
39+
#[cfg(test)]
40+
use adjacency::EdgeInc;
6841

6942
/// Vertex weld grid scale (reciprocal of a 10 µm grid, i.e. positions are
7043
/// quantized to `round(v * WELD_SCALE)`): fine enough not to merge distinct
@@ -76,7 +49,7 @@ const WELD_SCALE: f64 = 1.0e5;
7649

7750
/// Per-worker reusable scratch for [`orient_mesh_outward`], cleared (never freed)
7851
/// between meshes. The pass runs once per assembled submesh (~109k times on a
79-
/// mesh-heavy model), so each fresh call's two `FxHashMap`s + six `Vec`s were
52+
/// mesh-heavy model), so each fresh call's two `FxHashMap`s + the traversal `Vec`s were
8053
/// ~4-6% of busy CPU on pure-brep/steel models; pooling makes it allocate-once,
8154
/// clear-many. BYTE-IDENTICAL: neither map is ever iterated — both are only
8255
/// `.entry()`-inserted (order fixed by the deterministic scan) and keyed-looked-up,
@@ -88,7 +61,7 @@ struct OrientScratch {
8861
vid_of: FxHashMap<(i64, i64, i64), u32>,
8962
vpos: Vec<[f64; 3]>,
9063
corner: Vec<u32>,
91-
edge_tris: FxHashMap<(u32, u32), EdgeInc>,
64+
edge_tris: EdgeAdjacency,
9265
flip: Vec<bool>,
9366
visited: Vec<bool>,
9467
comp: Vec<usize>,
@@ -221,9 +194,14 @@ pub fn orient_mesh_outward_verdict(mesh: &mut Mesh) -> OrientVerdict {
221194
corner.clear();
222195
corner.reserve(mesh.indices.len());
223196

224-
// Weld positions -> welded vertex id; record the welded vid of every corner.
197+
// #3988: indexed corners repeatedly use the SAME source vertex. Reuse its
198+
// welded id, keeping the original first-corner insertion order. Sparse meshes
199+
// use corner slots instead, so scratch never exceeds the old corner budget.
200+
let indexed = vertex_count < mesh.indices.len();
201+
if indexed { corner.resize(vertex_count, u32::MAX); }
225202
let q = |v: f32| (v as f64 * WELD_SCALE).round() as i64;
226203
for &idx in &mesh.indices {
204+
if indexed && corner[idx as usize] != u32::MAX { continue; }
227205
let b = idx as usize * 3;
228206
let key = (
229207
q(mesh.positions[b]),
@@ -239,22 +217,23 @@ pub fn orient_mesh_outward_verdict(mesh: &mut Mesh) -> OrientVerdict {
239217
]);
240218
id
241219
});
242-
corner.push(vid);
220+
if indexed { corner[idx as usize] = vid; } else { corner.push(vid); }
243221
}
244-
let tv = |t: usize| [corner[3 * t], corner[3 * t + 1], corner[3 * t + 2]];
222+
let tv = |t: usize| std::array::from_fn::<_, 3, _>(|k| {
223+
corner[if indexed { mesh.indices[3 * t + k] as usize } else { 3 * t + k }]
224+
});
245225

246226
// Undirected welded edge -> incident triangles. >2 incident ⇒ non-manifold.
247227
// A closed manifold has ~1.5 edges per triangle; reserve to skip rehashing.
248-
edge_tris.clear();
249-
edge_tris.reserve(ntri * 2);
228+
edge_tris.reset(ntri);
250229
for t in 0..ntri {
251230
let v = tv(t);
252-
for &(a, b) in &[(v[0], v[1]), (v[1], v[2]), (v[2], v[0])] {
231+
for (slot, &(a, b)) in [(v[0], v[1]), (v[1], v[2]), (v[2], v[0])].iter().enumerate() {
253232
if a == b {
254233
continue; // welded-degenerate edge
255234
}
256235
let key = if a < b { (a, b) } else { (b, a) };
257-
edge_tris.entry(key).or_default().push(t);
236+
edge_tris.push(key, t * 3 + slot);
258237
}
259238
}
260239

@@ -303,22 +282,16 @@ pub fn orient_mesh_outward_verdict(mesh: &mut Mesh) -> OrientVerdict {
303282
} else {
304283
[(v[0], v[1]), (v[1], v[2]), (v[2], v[0])]
305284
};
306-
for &(a, b) in &dirs {
285+
for (slot, &(a, b)) in dirs.iter().enumerate() {
307286
if a == b {
308287
continue;
309288
}
310289
let key = if a < b { (a, b) } else { (b, a) };
311-
let inc = &edge_tris[&key];
312-
if inc.count != 2 {
313-
closed = false; // boundary (1) or non-manifold (>2) edge
314-
}
315-
if inc.count > 2 {
316-
continue; // ambiguous — don't propagate across a non-manifold edge
317-
}
318-
for &nb in inc.incident() {
319-
if nb == t {
320-
continue;
321-
}
290+
let original_slot = if flip[t] { 2 - slot } else { slot };
291+
let (closed_edge, neighbors) = edge_tris.neighbors(key, t * 3 + original_slot);
292+
if !closed_edge { closed = false; }
293+
for nb in neighbors {
294+
if nb == usize::MAX || nb == t { continue; }
322295
// A consistent neighbour must traverse this edge as (b, a). Its
323296
// UNFLIPPED winding has (a, b) iff it must flip to do so.
324297
let nv = tv(nb);

0 commit comments

Comments
 (0)