Skip to content

getBadFacesAfterCut: parallel walks, independent of the walk order - #7239

Draft
Grantim wants to merge 1 commit into
masterfrom
agent/mlib-bad-faces-parallel
Draft

Grantim wants to merge 1 commit into
masterfrom
agent/mlib-bad-faces-parallel

Conversation

@Grantim

@Grantim Grantim commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

What

getBadFacesAfterCut finds the faces of the cut whose holes are not valid loops, for example where contours cross inside a face. It walks the hole loop on the left of every path edge whose face was removed: of the path edge, or of its sym if the face was only on its right.

  • Every path edge walks its loop in parallel (tbb::parallel_for over all path edges, grain 256). Before, one serial walk per loop, skipped when the other side of the path edge had been walked.
  • A loop is bad if it passes an edge both ways: e0.sym(), or a new edge (contour piece, chord) met twice. Before, only e0.sym() counted. A loop that ran another edge both ways was missed when the walk started from an edge whose two sides are in different loops. For example, two contours whose chords cross in a face, where one chord crosses two others, gave one bad face out of two. Only new edges can bound one face's hole on both sides in valid loops. This also affects plain cuts: such faces are reported as bad instead of being filled with overlapping triangles.
  • Every path edge whose walk is bad sets its own old face. Before, only the first path edge of a loop did, so the result depended on the walk order.
  • Per thread (tbb::enumerable_thread_specific):
    • a bit set of the new edges of the loops it found good: their other path edges need no walk;
    • 16-bit walk numbers of the new undirected edges, to find a new edge met twice; they are cleared when the number wraps;
    • a FaceBitSet of the faces it set, made at its first one: its path edges with such a face need no walk. The sets are merged at the end, so nothing is written from two threads.

The same check exists in its serial form in #7231; whichever of the two merges second takes this version.

Tests

  • MRTest passes in Release and Debug (604).
  • Boolean regression set against master 29f1678: no regressions; 3093 of 3097 Release rows and 1259 of 1262 Debug rows are bit-identical. The changed rows are selfBoolean errors that count more nested-intersection faces: Quantized_20 612 → 821 (Release only, its Debug run is skipped), 12_rnd_spheres 67 → 70, 40_rnd_spheres 563 → 576, dental|self.ab 4 → 5. All boolean rows, plain and forced, are bit-identical.
  • Contour regression set: bit-identical to master in Release and Debug.
  • Every cutMesh call of the regression set (5435) was checked against a plain reference, where every path edge walks its whole loop with no skips: the same faces in 3 runs each. So did a build with the walk numbers cut to 8 bits, which wrap every 255 walks.

Benchmark

Alternating rounds against master 29f1678. Every MRTest was opted out of Windows power throttling, and no run was dropped as disturbed.

Plain boolean, 5 rounds:

case operation master, ms this PR, ms
dental DifferenceAB 20.05 19.17 -4.4%
dental Intersection 18.38 17.99 -2.1%
dental Union 19.48 19.40 -0.4%
dental Union, B rotated 19.31 19.26 -0.3%
nef DifferenceAB 110.56 108.84 -1.6%
nef Intersection 109.45 108.79 -0.6%
nef Union 127.56 126.67 -0.7%
nef Union, B rotated 129.47 128.53 -0.7%
nef-copy-noise Union 189.85 187.33 -1.3%
3366 spheres, DifferenceAB, 2000 rotations AVG, ms MEDIAN, ms MIN, ms MAX, ms
master 11.438 11.292 2.493 23.994
this PR 11.444 11.314 2.409 21.508

Forced boolean (forceCut), 3 rounds:

case operation master, ms this PR, ms
dental DifferenceAB 58.88 57.31 -2.7%
dental Intersection 57.94 55.64 -4.0%
dental Union 60.78 56.70 -6.7%
dental Union, B rotated 58.26 58.36 +0.2%
nef DifferenceAB 433.29 425.89 -1.7%
nef Intersection 425.64 427.15 +0.4%
nef Union 441.01 445.50 +1.0%
nef Union, B rotated 446.39 444.09 -0.5%
nef-copy-noise Union 567.52 561.62 -1.0%
forced 3366 spheres AVG, ms MEDIAN, ms MIN, ms MAX, ms
master 19.537 19.302 3.533 33.071
this PR 19.391 19.190 3.489 34.355

The function alone: inputs captured from real cuts, median of 30 rounds, time over master's getBadFacesAfterCut:

case 1 thread all threads, workers awake all threads, workers asleep (2 ms pause before each call)
3366 spheres, plain and forced 1.06-1.08 0.27-0.49 0.81-1.57
a cube (12 triangles) and a 100k-vertex sphere 1.00-1.08 0.62 2.6-3.1
dental, nef 0.86-0.89 0.30-0.34 1.10-1.86
nef-copy-noise 0.97-0.99 0.23 0.37
selfBoolean cuts with bad faces 1.03-1.18 0.23-0.41 0.76-1.53
  • One thread: it walks the loops on both sides of the path edges and checks every new edge, so it does a little more work than master. The single-CPU profile of 200 plain spheres booleans (400 cuts) shows getBadFacesAfterCut at 0.096-0.102 s on master and 0.103-0.110 s here; on the big cases, 0.023-0.025 s vs 0.020-0.024 s.
  • Sleeping workers: starting the parallel walks costs about 0.2 ms, more than they save on all but the biggest cuts. In the boolean the workers are awake from the phases before (the A/B above).
  • Memory per thread: 2 bytes per new undirected edge and 2 bits per new edge. Master's version takes 1 bit per edge, once.

Generated with Claude Code

…pendent of the walk order

Every path edge with a removed face walks its hole loop in parallel, unless its thread already found that loop good
or already set its face. A walk meeting e0.sym() or a new edge twice sets the old face of its own path edge; new edges
met twice are found by per-thread 16-bit walk numbers.

Before, one serial walk per loop, skipped when the other side of the path edge was walked, found a loop bad only if it
came back along the reverse of its start edge: a loop running another edge both ways was missed when walked from an
edge whose two sides are in different loops. The selfBoolean of four test inputs now reports more faces with nested
intersections; all boolean results are the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant