Skip to content

Pairs phase at 1.1-1.2x Box3D, the wide separating axis test, #158 - #159

Merged
nicolas-maman merged 4 commits into
mainfrom
perf/float-tree-wide-sat
Oct 9, 2026
Merged

nicolas-maman merged 4 commits into
mainfrom
perf/float-tree-wide-sat

Conversation

@nicolas-maman

Copy link
Copy Markdown
Collaborator

The pairs phase brought to 1.1-1.2x Box3D e77352c (it was 1.45-1.6x), a wide separating axis test, and a bug from #157. The golden trace is unchanged bit for bit.

The broad phase (#151):

  • The dynamic tree's boxes and the world's fat boxes are single precision, rounded outward from double, as the reference's large-world build keeps them (math.aabb_round_out, its b3RoundDownFloat and b3RoundUpFloat). That gives 32-byte nodes.
  • The insertion costs and the rays are measured on the widened boxes, which is exact. Storage, refit and overlaps are float.
  • A tree query compares the double box against the widened nodes, so a query pays no rounding at all.
  • The rounding is the bit step nextafterf takes, inline in the native header and branch-free. It is checked against libm's at zero, the denormals and the largest float.
  • Measured alone, the float boxes did not move the pairs phase. The profile showed why: two thirds of the walk was the pair set's probe, a cache miss per candidate. So candidates are now batched 32 at a time with every hash slot prefetched before any probe, as the reference does (b3FlushCandidatePairs). The walk also reads a leaf's shape index from the node rather than the proxy array.
  • Pairs phase, ms per step: convex pile 1.82 to 1.44 (Box3D 1.24), junkyard 2.88 to 2.21 (2.00), washer 2.65 to 2.21 (1.83), spinner 0.403 to 0.353 (0.284).

The separating axis test (#150, in part):

  • Every hull carries single-precision rows of its vertices and normals, padded to eight lanes, and its edges' cosines. The HULL_VERSION changed, and hull_bytes_valid checks the new sections.
  • Support searches run eight vertices a pass. A hull of 16 vertices or fewer is searched one vertex at a time in double, where the lanes' reduction cost more than it saved.
  • Edge candidates are culled with the stored cosines and then by the best faces' probe points (b3FilterEdgeCandidates). Edge pairs run eight a pass, with the axis and separation in lanes.
  • Every winner is measured again in double.
  • SAT runs, µs: cylinder/cylinder 4.73 to 2.58 (Box3D 1.41), complex/cylinder 2.77 to 2.02 (1.06), complex/complex 1.76 to 1.60 (0.95).
  • Hull SAT: e77352c's wide separating axis (convex_manifold_wide.inl); the convex pile's collide is 1.38x #150 stays open for the rest: the face dots, the transform of B and the reduction, done in lanes. The reduction waits on aether#2603, filed here: std.lanes has no bit reinterpretation for the reference's embedded-index minimum.

A bug fixed (#158): hull_bytes_valid (from #157) bounded half-edges by MAX_HULL_EDGES, half what the builder allows. A compound holding a hull of more than 64 edges failed its byte round trip. It is fixed, and the round trip is now tested on a 240-half-edge cylinder.

Whole step, ours before and after, ms per step:

  • convex pile 11.84 to 11.26, junkyard 28.3 to 25.0, washer 17.6 to 16.7, spinner 4.31 to 4.08, sleep 2.40 to 2.33;
  • trees25 1.589 to 1.587, trees100 unchanged, trees50 +2.8%.

The trees scenes pay for rounding the fat boxes of fast shapes, which leave them every step. Details are in bench/RESULTS.md, "The pairs phase and the wide separating axis test".

Also:

  • scripts/profile.sh passes ae cflags whole, since 0.795's include directories come as -idirafter <dir> pairs.
  • Tests:
    • centimetre boxes at 1e7, each held by its leaf and found by every query that touches them;
    • the rounding's properties and its agreement with libm;
    • the broad phase's brute force over the stored boxes;
    • the big-hull byte round trip.

Local gate on Aether 0.795.0: all 34 suites pass with no warnings, and the golden trace matches bit for bit (1,256 steps). Pins: Aether 0.795.0 and aeb v0.325, the latest releases.

Closes #151. Closes #158. Refs #150, #121.

…h prefetch

The dynamic tree's nodes and the world's fat boxes are single precision,
narrowed outward from double (math.aabb_round_out, b3RoundDownFloat and
b3RoundUpFloat): 32-byte nodes, the reference's. The insertion costs and
the rays stay in double on the widened boxes; storage, refit, overlaps
and queries are float. The golden trace is unchanged.

The pairs phase's cost was the pair set's cache misses, not the box
bytes: candidates are batched 32 at a time with every hash slot
prefetched before any probe (b3FlushCandidatePairs), and the walk reads a
leaf's shape index from the node instead of the proxy array.

scripts/profile.sh passes ae cflags whole (0.795's -idirafter pairs).
Every hull carries f32 rows of its vertices and normals, padded to eight
lanes with vertex 0 and zeros, and its edges' cosines, filled where its
hash is stamped (HULL_VERSION 0x4A4C9588; hull_bytes_valid checks them).

The separating axis test reads them as the reference's wide test does:
support searches eight vertices a pass (the lowest index wins a tie; the
reference's embedded-index trick waits on aether#2603), edge candidates
culled by the inscribed spheres with the stored cosines and again by a
probe point (b3FilterEdgeCandidates), edge pairs eight a pass with the
axis and separation in lanes. Every winner is measured again in double,
and the golden trace is unchanged.

hull_bytes_valid bounded half-edges by MAX_HULL_EDGES, half what the
builder allows: a compound of big hulls failed its byte round trip
(#158). Fixed, with the round trip tested on a 240-half-edge cylinder.
The narrowing of a coordinate to the float at or below (or above) it is
the bit step nextafterf takes, inline in the native header and without
branches, checked against libm's at zero, the denormals and the largest
float. A tree query compares the double box against each node's float
box widened, which is exact (a node holds every double box under it), so
a query pays no rounding at all. Small hulls (16 vertices or fewer) are
searched one vertex at a time in double, where the lanes' reduction cost
more than it saved.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant