Skip to content

Add SIMD batch support to the distance, barrier, and friction functions - #251

Merged
zfergus merged 35 commits into
mainfrom
feature/simd-distance
Sep 6, 2026
Merged

Add SIMD batch support to the distance, barrier, and friction functions#251
zfergus merged 35 commits into
mainfrom
feature/simd-distance

Conversation

@zfergus

@zfergus zfergus commented Sep 6, 2026

Copy link
Copy Markdown
Member

Description

Makes an xsimd batch a valid scalar type throughout the geometry layer, so a caller holding a structure-of-arrays layout evaluates SimdBatch<T>::size independent collisions per call instead of one. Builds on the scalar templating of #249 and covers the distance, barrier, tangent-basis, closest-point, normal, mollifier, friction, adhesion, and dihedral-angle functions.

The mechanism is two new headers plus a rewrite of every value-dependent branch:

  • ipc/utils/simd.hpp (new) — specializes Eigen::NumTraits for xsimd::batch so Eigen::Vector3<ipc::SimdBatch<double>> is a valid argument, and supplies the scalar/batch bridges the kernels need: scalar_of_t, all_of, select, select_lazy, literal<T>, infinity<T>, and ipc::normalized.
  • ipc/math/scalar_math.hpp (new) — ipc::abs, atan2, fma, log, and sqrt resolve through a block-scope using std::…, so ADL reaches the xsimd:: overloads a qualified std::sqrt cannot find. It also holds the free ipc::sqr/ipc::cubic that replace four separate definitions, and splits the scalar helpers out of math.hpp so consumers stop pulling in Eigen and TinyAD for them.
  • select_lazy cascades replace the if/else in the barriers, the friction and edge-edge mollifiers, smooth-μ, adhesion, and the tangent basis' reference-axis pick. A scalar still evaluates only the case it lands in; a batch evaluates every case and blends per lane, so one batch may carry lanes on either side of a threshold.
  • Friction, adhesion, and dihedral angle are now templated on T — the last hardcoded doubles in the geometry layer, which is what ruled out a batch friction or adhesion path. smooth_friction_mollifier.cpp and adhesion.cpp are gone; both headers are header-only templates now, following edge_edge_mollifier.hpp.

Impact:

  • A batched caller gets 0.97–2.07× with SimdBatch<double> and 1.80–4.32× with SimdBatch<float> on NEON, rising to 1.01–3.09× and 2.09–7.36× on AVX2; see Performance.
  • Consumers are now compiled with the library's SIMD flags — the one change that reaches a downstream build unasked (see Breaking changes).
  • The scalar API is unchanged in shape and in output: the double instantiations keep their original early-return form.

API changes

  • ipc::SimdBatch<T> aliases xsimd::batch<T, xsimd::default_arch>. Passing Eigen::Vector3<ipc::SimdBatch<double>> to a distance function evaluates one independent problem per lane.
  • Instantiated for SimdBatch<float> and SimdBatch<double>: the values/gradients/Hessians of the point-point, point-line, point-edge, line-line, edge-edge, and point-triangle distances; the barrier functions and classes (barrier, ClampedLogBarrier, ClampedLogSqBarrier, CubicBarrier, TwoStageBarrier); the tangent bases and their Jacobians; the closest-point Jacobians/Hessians; the unnormalized-normal Jacobians/Hessians; the signed-distance Hessians; the triangle-area gradient; the edge-edge mollifier family; and the dihedral angle with its gradient and Hessian.
  • edge_edge_closest_point and point_triangle_closest_point solve their 2×2 symmetric positive-definite system with Cramer's rule instead of A.ldlt().solve(). Pivoting requires branching on matrix values, which breaks vectorization since different lanes would need different control flow. Accuracy is limited by the Gram matrix's conditioning rather than the algorithm, so the branchless form wins. On a singular matrix it returns zero branchlessly instead of dividing by it, matching Eigen's LDLT pseudo-inverse behavior.
  • The normalized normals (point_line_normal, triangle_normal, line_line_normal) use ipc::normalized instead of Eigen's normalized(), whose zero-length guard is an if a batch cannot answer with one bool. That was the only thing blocking the values and gradients of the point_line, line_line, and point_plane signed distances, which route through those normals.
  • The relative-velocity functions were already batch-compatible; they are now covered by tests.
  • AUTO and the *_distance_type predicates are not available for batch scalars and throw std::invalid_argument. This is semantic, not an omission: the distance type is a per-lane property, but the predicates return a single enum, so two lanes cannot report different closest features. Resolve the distance types scalar-side and group problems by type before batching, which is the natural structure-of-arrays layout anyway.

🐛 Fixed in passing

  • TwoStageBarrier had no d <= 0 guard, so its log returned NaN for d < 0 and its first derivative flipped sign, reporting an attractive force at penetration. It now matches the other log barriers: +inf for the value, 0 for the derivatives.

💥 Breaking changes

  • xsimd and SIMD_CXX_FLAGS are linked/applied PUBLIC rather than PRIVATE. xsimd::default_arch is selected from each translation unit's own compiler flags: with the flags PRIVATE, this library's TUs resolve it to i8mm+neon64 while a consumer's resolve to arm64+neon — a genuinely different type, so the instantiations here would not match what a caller names and the link fails. The cost is that consumers are now compiled with the detected SIMD flags (typically -march=native), making their binaries non-portable; disable IPC_TOOLKIT_WITH_SIMD if that is unwanted.
  • Mixed-precision friction and adhesion calls no longer deduce. As with the distance functions in Template the per-collision geometry functions on scalar and dimension #249, every argument must share one scalar type: smooth_mu(float_y, 0.5, 0.3, 0.001) must become smooth_mu(float_y, 0.5f, 0.3f, 0.001f).
  • The anisotropic-friction helpers (anisotropic_mu_eff_f, anisotropic_x_from_tau_aniso, anisotropic_mu_eff_from_tau_aniso) stay double-only. Their branches test the material rather than a per-problem speed, so there is nothing per-lane to blend yet.
  • eigen_ext.hpp no longer leaks a global using std::abs under EIGEN_DONT_VECTORIZE. Callers relying on unqualified abs at namespace scope must qualify it; the affected tests in this repo are updated.

Performance

Per-collision barrier potential — value, gradient, and Hessian — evaluated with five scalar types on the same collision set across the assembly benchmark scenes, so every variant does identical work. The distance type is resolved scalar-side and passed in, which a batch requires; the edge-edge mollifier is excluded because no batch caller applies it yet. "Hessian (compute only)" sums the entries instead of storing them, separating the arithmetic from the 144 stores per collision.

Apple M3 Pro — NEON, 128-bit (2 lanes per double, 4 per float)

simd_barrier_potential_all_scenes_parallel
  • SimdBatch<double> runs 0.97–2.07× and SimdBatch<float> 1.80–4.32× over the scalar double loop, across all six scenes and all four quantities.
  • The batch types win on every scene; the single cell below parity is the n-body stored Hessian at 0.97×.

Intel Core Ultra 9 285K — AVX2+FMA (4 lanes per double, 8 per float)

simd_barrier_potential_all_scenes_parallel
  • Doubling the lane count pays off: SimdBatch<double> reaches 1.01–3.09× and SimdBatch<float> 2.09–7.36×, against 2.07× and 4.32× on NEON.
  • The gain is not uniform, because the wider machine hits the memory wall sooner — see below.

What the Hessian columns show. Speedup falls off exactly where the stores dominate. On the two largest scenes on AVX2, SimdBatch<double>'s stored Hessian collapses to parity — 1.01× on rod-twist and 1.02× on puffer-ball — while the compute-only Hessian on the same data holds 1.71× and 1.82×. Each collision writes 144 doubles, and that traffic does not shrink with lane width; 24 threads saturate the bus before the arithmetic is the limit. NEON, with half the lanes and fewer threads, never reaches that point: 1.37× stored against 1.51× compute-only on puffer-ball.

float needs rescaling to be usable. Raw float overflows the generated line-line and point-plane Hessians at scene scale (edge lengths ~1e-4), and does so identically for the scalar and batch float paths — a precision property, not a SIMD one. The plotted float timings are from the rescaled inputs, which re-center each stencil on its centroid and divide by d̂ before conversion, folding the scale back into κ; the instructions are the same either way, only the packing differs.

Accuracy. SimdBatch<double> reproduces the scalar path: on puffer-ball's 512k collisions the value agrees to 6.4e-16 relative and the gradient to 1.8e-16 median. The 9.0e-11 gradient max is the near-parallel edge-edge configurations where the derivative is ill-conditioned and the two paths contract multiply-adds differently.

New dependencies

None. xsimd was already fetched via CPM by #207; this PR only widens its link scope to PUBLIC.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Enhancement (non-breaking change which improves existing functionality)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

  • New SIMD tests — 27 cases / 15,233 assertions across test_simd_{distance,signed_distance,edge_edge_mollifier,barrier,friction,adhesion,geometry,closest_point,tangent_basis,relative_velocity,utils}.cpp, sharing the helpers in tests/src/tests/simd_utils.hpp. Each packs independent problems one per lane and checks every lane against the scalar path, sweeping lane offsets so every branch of a select_lazy cascade is exercised even when a batch holds fewer lanes than there are cases. Run with ipc_toolkit_tests "[simd] ~[!benchmark]".
  • Full existing suiteipc_toolkit_tests "~[!benchmark]" on a Debug build: 320 cases / 3,510,369 assertions pass, 5 skipped (the spatial-hash and STQ cases that skip themselves in Debug). No CUDA on this host, so IPC_TOOLKIT_WITH_CUDA=OFF.
  • Benchmarkipc_toolkit_tests "[simd_barrier_potential]", which checks every scalar variant against the double path, and the double path against the library, before reporting any timing. Run on both machines plotted above:
    • Apple M3 Pro (5P+6E), 11 threads, median of 5 runs, Release -O3, Apple clang 21.0.0, arm64 NEON 128-bit.
    • Intel Core Ultra 9 285K, 24 threads, median of 7 runs, Release -O3, GCC 11.5, -march=native (AVX2+FMA).
  • Not covered: no CUDA build (no toolkit on either host).

Test Configuration:

  • OS and Version: macOS 26.6.2 (Apple M3 Pro) and Linux (Intel Core Ultra 9 285K)
  • Compiler and Version: Apple clang 21.0.0 and GCC 11.5, C++17, -march=native; Debug for the unit tests, Release for the benchmark

Checklist

  • I have followed the project style guide
  • My code follows the clang-format style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

zfergus and others added 17 commits September 2, 2026 21:13
Specialize `Eigen::NumTraits` for `xsimd::batch` and add `ipc::SimdBatch<T>`
in the new `ipc/utils/simd.hpp`, so `Eigen::Vector3<ipc::SimdBatch<double>>`
is a valid argument to the distance functions. A caller holding a
structure-of-arrays layout can then evaluate one independent problem per SIMD
lane per call. The scalar templating this builds on is what makes it possible;
the values, gradients, and Hessians of point-point, point-line, point-edge,
line-line, edge-edge, and point-triangle are instantiated for
`SimdBatch<float>` and `SimdBatch<double>`.

The generated `autogen` kernels needed instantiating for the batch types too,
since they are where the derivatives actually land.

Verified lane-by-lane against the scalar path: the values agree to one ulp,
and every gradient and Hessian entry agrees to 1e-9 relative to the magnitude
of the result. The derivatives need the looser, norm-scaled bound because they
are ill-conditioned for near-parallel edges -- a random configuration
routinely produces entries of order 1e6 whose small differences are
cancellations of large terms -- while the batch and scalar paths contract
multiply-adds differently.

AUTO and the `*_distance_type` predicates are unavailable for batch scalars
and throw. This is semantic, not an omission: the distance type is a per-lane
property, but the predicates return a single enum, so two lanes cannot report
different closest features. Resolve the distance types scalar-side and group
problems by type before batching, which is the natural SoA layout anyway.

[Breaking] `xsimd` and `SIMD_CXX_FLAGS` become PUBLIC rather than PRIVATE.
`xsimd::default_arch` is selected from each translation unit's own compiler
flags: with the flags PRIVATE, this library's TUs resolve it to `i8mm+neon64`
while a consumer's resolve it to `arm64+neon`, a genuinely different type, so
the instantiations here would not match what a caller names. The cost is that
consumers are now compiled with the detected SIMD flags (typically
`-march=native`), which makes their binaries non-portable; disable
IPC_TOOLKIT_WITH_SIMD if that is unwanted. Moving the edge-edge and
point-triangle kernel definitions into a `.tpp` so callers instantiate in
their own TU would avoid this, at the cost of a larger refactor.

A caveat on the payoff: on an Apple M-series (NEON, 2 lanes per double) a
batched point-line sweep measured 1.06-1.13x over the scalar loop, and
1.13-1.18x with float at 4 lanes -- well short of the lane count, because the
compiler already auto-vectorizes a loop over independent problems and these
kernels are largely memory-bound. Wider ISAs may do better; that is unmeasured.
Only `SimdBatch<double>` is covered by the tests; the `float` instantiations
compile and link but are not numerically verified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`sqr` was defined four times across the library: as `Math<T>::sqr` in
math.hpp and as three separate anonymous-namespace copies, two of which
were `double`-only and so unusable from templated code.

- Add free `ipc::sqr` and `ipc::cubic` to a new `math/scalar_math.hpp`.
- `Math<T>::sqr`/`cubic` forward to them, so the public API is unchanged
  and there is a single implementation.
- Drop the duplicates in normal_collisions.cpp and
  voxel_size_heuristic.cpp.

Split math.hpp along its dependencies so consumers that only want the
scalar helpers do not pay for the rest:

- `math/scalar_math.hpp` -- no includes.
- `math/heaviside_type.hpp` -- `HeavisideType`, `OrientationTypes`.
- `math/math.hpp` -- the parts needing eigen_ext, and math.tpp which
  pulls in the autodiff scalars. It still includes both new headers, so
  existing `#include <ipc/math/math.hpp>` keeps working.

normal_collisions.cpp and voxel_size_heuristic.cpp no longer pull in
TinyAD as a result (verified with -H).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Instantiate the remaining scalar-templated geometry for `SimdBatch<float>`
and `SimdBatch<double>`: the barrier functions and classes, the tangent
bases and their Jacobians, the closest-point Jacobians/Hessians, the
unnormalized-normal Jacobians/Hessians, the signed-distance Hessians, and
the triangle-area gradient.

Three things blocked this and are fixed here:

- Qualified `std::sqrt` cannot find `xsimd::sqrt`, which is only reachable
  by ADL. `ipc::sqrt` does the lookup in one place via a block-scope
  `using std::sqrt`, so it also covers the autodiff scalars.
- Eigen's `normalized()` guards a zero-length vector with an `if`, which a
  batch cannot answer with one bool. `ipc::normalized` applies that rule
  per-lane and defers to Eigen for scalars.
- Functions that pick a case from the values themselves (a barrier
  clamping at dhat, the 3D point-point tangent basis choosing a reference
  axis) have no single answer for a batch. `select_lazy` expresses these
  as one first-match-wins cascade: a scalar evaluates only the case it
  lands in, while a batch evaluates every case and blends per-lane. Each
  barrier is now written once rather than as separate scalar and batch
  bodies.

The `*_distance_type` predicates remain scalar-only by design -- they
return an enum, which has no per-lane blend.

Also fixes TwoStageBarrier at penetration. It had no `d <= 0` guard, so
its log returned NaN for d < 0 and its first derivative flipped sign,
reporting an attractive force. It now matches the other log barriers:
+inf for the value, 0 for the derivatives.

Tests cover the batch/scalar agreement lane-wise, sweeping lane offsets so
every branch is exercised even when a batch holds fewer lanes than there
are cases, plus the barrier penetration and stage-boundary conventions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Time the per-collision barrier value, gradient, and Hessian with double,
float, SimdBatch<double>, and SimdBatch<float> on the assembly benchmark
scenes, so the four are compared on identical work. Collisions are grouped
by kind and distance type and packed one per lane; a single templated loop
evaluates the chain d -> f(d) -> derivatives for every scalar type, and the
library path is timed alongside as a reference. A compute-only Hessian
column sums the entries instead of storing them, separating the arithmetic
from the 144 stores per collision, which are memory-bound once threads
share the bus.

Every variant is checked against the double path, and the double path
against the library on the unmollified collisions. Two caveats the report
states explicitly: the edge-edge mollifier has no batch implementation and
is excluded, and raw float overflows the generated line-line and point-plane
Hessians at scene scale (edge lengths ~1e-4), so "rescaled" float variants
re-center each stencil and divide by dhat before conversion, folding the
scale back into kappa.

On puffer-ball (512k collisions, AVX2, single-threaded) SimdBatch<double> is
3.1x faster than the scalar loop on the gradient and 2.2x on the Hessian
without stores; SimdBatch<float> 7.1x and 4.5x.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The normalized normals (`point_line_normal`, `triangle_normal`,
`line_line_normal`) called Eigen's `normalized()`, whose zero-length guard is
an `if` on a value that a batch cannot answer with one bool. They now use
`ipc::normalized`, which applies that rule per-lane, as the tangent bases
already do.

That was the only thing blocking the values and gradients of the `point_line`,
`line_line`, and `point_plane` signed distances, which route through those
normals; previously only their Hessians were instantiated for batch scalars.
All of these are header-only templates, so no new instantiations were needed.

`edge_length_gradient` asserted `(e1 - e0).norm() != 0`, which likewise has no
single answer for a batch. That assert is now scalar-only, matching how
`tangent_basis.hpp` handles the same pattern.

The relative-velocity functions turned out to already be batch-compatible;
they are noted in the release notes and covered by tests separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ions

Six SIMD test files each carried a private copy of the same lane-packing and
lane-comparison helpers. They now share `tests/simd_utils.hpp`, which owns
`Batch`, `L`, `Lanes`, `Points<dim>`, `pack`, `random_points`, `lane_cases`,
`approx`, `check_lanes`, `check_scalar_lanes`, and `check_swept_lanes`. The
four pre-existing files shrank by 37-81 lines each.

Three changes beyond a straight extraction:

- `Lanes` is a `std::array<double, Batch::size>` aligned to the architecture's
  requirement, so packing uses `load_aligned`/`store_aligned` rather than
  staging through an unaligned buffer. A `static_assert` checks that the type
  actually meets the alignment its load claims.
- Comparisons go through `Catch::Approx` instead of a hand-rolled `close`.
  `epsilon(0)` is not optional there: `Approx` ors its margin test with an
  epsilon test whose default is float-grade (~1e-5 relative), which would
  silently pass a wrong double. `Approx` also compares without subtracting, so
  it already gives the sign-respecting infinity matching the barriers need.
- The barrier's region sweep and the mollifier's threshold sweep were the same
  loop, and are now one `check_swept_lanes`.

New coverage for the batch functions that had none: the normals and the signed
distances, the triangle area and edge length, and the relative velocities,
which were already batch-compatible but untested. The relative velocities are
held to the value tolerance rather than the derivative one, since they are
linear combinations with no cancellation of large terms.

`test_simd_utils.cpp` pins down the shared helper's own contract, six files now
depending on it: the epsilon leak above, infinity matching by sign, and NaN
never passing. It caught `tol * scale` going infinite when the expected value
is `+inf` -- the case the barrier tests rely on -- which would have made those
lanes accept anything.

The signed-distance values needed a looser bound than the unsigned ones, and
structurally so: a signed distance is `normal.dot(p - x0)` with the normal a
normalized cross product, so the cross cancels and the sqrt and division that
follow amplify the two paths' differing multiply-add contraction. The reason is
recorded at the call site rather than hidden in a per-file constant.
The packed buffers were plain `std::vector<R>`, guaranteeing only
`alignof(R)`, so the batch loads and stores had to be unaligned. They now use
`xsimd::default_allocator<R>`, which falls back to `std::allocator` on an
architecture with no alignment requirement.

Every batch access is a whole number of lanes into one of these buffers, and
one lane's width times the lane count is the architecture's alignment, so an
aligned base makes all of them aligned. This matters here rather than in the
tests: an unaligned load can cost throughput on some instruction sets, which
is the very thing the benchmark measures.

The traits assert `xsimd::is_aligned<A>` on every load and store instead of
leaving that reasoning as a comment. An aligned load off a misaligned address
happens to work on NEON and faults on AVX, so it cannot be validated by
running on one machine; the assert checks it on every access in a debug build,
including after any future change to the packing layout.

Also drops the note that a batch cannot evaluate the edge-edge mollifier,
which stopped being true when the mollifier gained batch instantiations. The
benchmark still does not apply it, so that the four variants stay on the same
arithmetic the original numbers were taken with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Replace `A.ldlt().solve()` with a branchless closed form, since pivoting
  branches on matrix values and cannot vectorize
- Add `scalar_of_t` and `all_of` to `ipc/utils/simd.hpp`, fixing the residual
  tolerance for batches and keeping its assert alive per-lane

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
These were the last hardcoded-`double` potentials, which is what ruled out a
batch-capable friction or adhesion path.

- Move the friction mollifier, smooth-mu, and adhesion functions into
  header-only templates, deleting `smooth_friction_mollifier.cpp` and
  `adhesion.cpp`
- Rewrite their piecewise branches as `select_lazy` cascades, so one batch may
  carry lanes on either side of a threshold. Several inactive branches divide
  by `y`, which a batch evaluates even where `y == 0`; the per-lane blend
  discards the infinity rather than propagating a NaN
- Split `dihedral_angle` and its derivatives into `ipc::detail` kernels
  instantiated for `float`, `double`, and both batch types, behind front ends
  that deduce `T`
- Add `ipc::abs` and `ipc::atan2`. `Math<T>::abs` picks the sign with a
  ternary, which asks a batch for one `bool` its lanes may disagree on
- Keep the anisotropic friction helpers `double`-only: their branches test the
  material rather than a per-problem speed, so there is nothing per-lane

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Return `decltype(auto)` from `ipc::abs` so it stops hijacking Eigen's
  array `abs` under `using namespace ipc`
- Zero the 2x2 closest-point solve on a singular Gram matrix instead of
  dividing by zero, and scale its residual assert to the system's
  magnitude instead of an absolute bound
- Add `ipc::literal<T>` and use it wherever a fractional constant would
  narrow for a `SimdBatch<float>`
- Collapse the generic `std::` math forwarders into one variadic macro
  over a single name list
- Delegate `Math<T>::abs` to `ipc::abs` instead of a batch-incompatible
  ternary
- Fix the point-edge distance test's inverted transition-point guard
  and add the degenerate-edge guard its finite-difference comparison
  needs
- Factor the dihedral-angle Jacobian assembly out of the gradient and
  Hessian, consolidate the SIMD test helpers, and correct several
  stale or misleading comments
- Decouple config.hpp.in from <Eigen/Core> by mirroring the
  StorageOptions values it needs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Fix single bracket array initializer in tests
Suppress -Warray-bounds only on GCC 16+, the one compiler that trips it.
In a translation unit instantiating tbb::enumerable_thread_specific for
more than one type, GCC 16 speculatively devirtualizes the type-erased
construct callback in combine() to an override for the wrong type and
reports the placement-new into the smaller ets_element as out of bounds.
GCC <= 15 and Clang are clean across all 182 translation units, so they
keep the warning instead of losing it project-wide.

Drop -Wmaybe-uninitialized from the non-GNU branch: it is GCC-only, so
check_cxx_compiler_flag filters it out on Clang while the -Wno- form
disables it on GCC. It was enabled nowhere.

Remove <type_traits> and the EIGEN_USING_STD(abs) workaround from
eigen_ext.hpp; neither the header nor its .tpp uses either one, and the
latter injected a using-declaration into the global namespace from a
public header.
@zfergus
zfergus marked this pull request as draft September 6, 2026 01:53
- Add MeshFEMSparse, which is on by default but was missing
- Drop nlohmann/json and Tracy, which are off by default
- Dash the edges of optional dependencies and note it in the legend
- Correct several node comments that named the wrong target
@codecov

codecov Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.80119% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 96.70%. Comparing base (af52a2e) to head (93d09f7).

Files with missing lines Patch % Lines
src/ipc/geometry/angle.cpp 98.27% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #251      +/-   ##
==========================================
+ Coverage   96.41%   96.70%   +0.29%     
==========================================
  Files         179      191      +12     
  Lines       16808    17291     +483     
  Branches      962      928      -34     
==========================================
+ Hits        16205    16721     +516     
+ Misses        603      570      -33     
Flag Coverage Δ
unittests 96.70% <99.80%> (+0.29%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@zfergus
zfergus marked this pull request as ready for review September 6, 2026 03:00
@zfergus
zfergus requested a lite review from Copilot September 6, 2026 03:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A debug-only assertion in solve_spd_2x2 uses || on a potentially per-lane SIMD mask, which is unsafe for batch types and should be replaced with a per-lane OR to ensure correct compilation/behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Extends IPC Toolkit’s geometry-layer scalar templating to support xsimd::batch (ipc::SimdBatch<T>) as a valid “scalar” throughout distance, barrier, tangent-basis, closest-point, friction, adhesion, and dihedral-angle codepaths, enabling structure-of-arrays callers to evaluate one independent problem per SIMD lane.

Changes:

  • Add SIMD scalar infrastructure (ipc/utils/simd.hpp) and ADL-friendly scalar math wrappers (ipc/math/scalar_math.hpp), then refactor value-dependent branches to select_lazy.
  • Template/instantiate the remaining geometry-layer hardcoded-double paths (friction/adhesion/dihedral angle) and add SIMD-focused unit tests + a SIMD barrier benchmark.
  • Update build/docs to reflect that SIMD flags and xsimd are now applied/linked PUBLIC when IPC_TOOLKIT_WITH_SIMD=ON.
File summaries
File Description
tests/src/tests/utils/test_simd_utils.cpp Adds unit tests pinning SIMD test helper semantics (Approx behavior, lane packing).
tests/src/tests/utils/CMakeLists.txt Registers test_simd_utils.cpp.
tests/src/tests/tangent/test_simd_tangent_basis.cpp Adds lane-wise SIMD vs scalar tangent-basis tests (including per-lane reference-axis blend).
tests/src/tests/tangent/test_simd_relative_velocity.cpp Adds SIMD coverage for relative velocity values/Jacobians/dx_dbeta in 2D/3D.
tests/src/tests/tangent/test_simd_closest_point.cpp Adds SIMD closest-point tests including near-parallel edge conditioning.
tests/src/tests/tangent/test_closest_point.cpp Tightens closest-point scalar assertions (zero epsilon, tiny margin).
tests/src/tests/tangent/CMakeLists.txt Registers tangent SIMD test files.
tests/src/tests/simd_utils.hpp New shared SIMD test utilities (lane packing, comparisons, tolerances).
tests/src/tests/potential/CMakeLists.txt Registers SIMD barrier potential benchmark source.
tests/src/tests/geometry/test_simd_geometry.cpp Adds SIMD tests for normals, normalization, area, and dihedral angle (value/grad/Hess).
tests/src/tests/geometry/CMakeLists.txt Registers test_simd_geometry.cpp.
tests/src/tests/friction/test_simd_friction.cpp Adds SIMD tests for friction mollifier and smooth-μ families across thresholds.
tests/src/tests/friction/CMakeLists.txt Registers test_simd_friction.cpp.
tests/src/tests/distance/test_simd_signed_distance.cpp Adds SIMD tests for signed distances (value/grad/Hess) with adjusted tolerance.
tests/src/tests/distance/test_simd_edge_edge_mollifier.cpp Adds SIMD tests for edge-edge mollifier + per-lane threshold blending.
tests/src/tests/distance/test_simd_distance.cpp Adds SIMD tests for core distances plus explicit rejection of AUTO.
tests/src/tests/distance/test_point_edge.cpp Makes finite-difference derivative checks robust by skipping degenerate edges and fixing branch condition.
tests/src/tests/distance/CMakeLists.txt Registers distance SIMD test files.
tests/src/tests/CMakeLists.txt Adds simd_utils.hpp to test target sources.
tests/src/tests/broad_phase/test_lbvh.cpp Removes unused iostream include.
tests/src/tests/barrier/test_simd_barrier.cpp Adds SIMD barrier tests over all piecewise regions.
tests/src/tests/barrier/test_barrier.cpp Adds regression tests for penetration convention + stage boundaries; cleans up abs usage.
tests/src/tests/barrier/CMakeLists.txt Registers SIMD barrier tests.
tests/src/tests/adhesion/test_simd_adhesion.cpp Adds SIMD tests for normal/tangential adhesion and smooth-μ adhesion variants.
tests/src/tests/adhesion/CMakeLists.txt Registers test_simd_adhesion.cpp.
src/ipc/utils/simd.hpp New SIMD glue: SimdBatch, NumTraits specialization, select_lazy, literal, all_of, normalized, etc.
src/ipc/utils/eigen_ext.hpp Removes conditional EIGEN_USING_STD(abs) workaround.
src/ipc/utils/CMakeLists.txt Registers new simd.hpp.
src/ipc/tangent/tangent_basis.hpp Switches to ipc::normalized and adds per-lane selection for SIMD reference-axis choice.
src/ipc/tangent/closest_point.hpp Replaces LDLT solve with branchless 2×2 solver for SIMD compatibility.
src/ipc/tangent/closest_point.cpp Instantiates closest-point autogen for SIMD batch types.
src/ipc/smooth_contact/common.hpp Qualifies abs as std::abs in parameter validation.
src/ipc/math/scalar_math.hpp New ADL-friendly wrappers for abs/atan2/fma/log/sqrt + shared sqr/cubic.
src/ipc/math/math.tpp Uses Math<T>::abs consistently.
src/ipc/math/math.hpp Splits heaviside types + scalar helpers out; routes Math<T> helpers through ipc:: wrappers.
src/ipc/math/heaviside_type.hpp New header extracting HeavisideType and OrientationTypes.
src/ipc/math/CMakeLists.txt Registers new math headers.
src/ipc/geometry/normal.hpp Switches to ipc::normalized for batch-safe normalization.
src/ipc/geometry/normal.cpp Uses ipc::sqrt and instantiates for SIMD batch types.
src/ipc/geometry/area.hpp Avoids scalar-only degeneracy assert when templated on SIMD batch.
src/ipc/geometry/area.cpp Uses ipc::sqrt and instantiates for SIMD batch types.
src/ipc/geometry/angle.hpp Converts dihedral-angle API to templated front ends calling detail kernels.
src/ipc/friction/smooth_mu.hpp Makes smooth-μ family templated and SIMD-safe via select_lazy.
src/ipc/friction/smooth_mu.cpp Removes scalar smooth-μ implementations (now header-only templates); keeps anisotropic helpers double-only.
src/ipc/friction/smooth_friction_mollifier.hpp Makes friction mollifier functions templated and SIMD-safe via select_lazy.
src/ipc/friction/smooth_friction_mollifier.cpp Deletes old scalar implementations (now header-only templates).
src/ipc/friction/CMakeLists.txt Stops compiling deleted friction mollifier .cpp.
src/ipc/distance/signed/point_plane.cpp Uses fixed-size reshaped(fix<…>) and instantiates Hessian for SIMD batch types.
src/ipc/distance/signed/point_line.cpp Uses fixed-size reshaped(fix<…>), guards scalar-only asserts, instantiates for SIMD batch types.
src/ipc/distance/signed/line_line.cpp Uses fixed-size reshaped(fix<…>) and instantiates for SIMD batch types.
src/ipc/distance/point_triangle.cpp Instantiates point-triangle distance family for SIMD batch types (explicit type only).
src/ipc/distance/point_plane.cpp Instantiates point-plane autogen for SIMD batch types.
src/ipc/distance/point_line.cpp Instantiates point-line autogen for SIMD batch types.
src/ipc/distance/point_edge.cpp Instantiates point-edge Hessian kernels for SIMD batch types.
src/ipc/distance/line_line.cpp Instantiates line-line autogen for SIMD batch types.
src/ipc/distance/edge_edge.cpp Instantiates edge-edge distance family for SIMD batch types.
src/ipc/distance/edge_edge_mollifier.hpp Refactors mollifier branches to select_lazy, uses ipc::fma, adds scalar fast-path guards.
src/ipc/distance/edge_edge_mollifier.cpp Adds scalar fast-path guards + instantiates mollifier templates/autogen for SIMD batch types.
src/ipc/config.hpp.in Removes Eigen include by mirroring storage option constants and substituting layout token directly.
src/ipc/collisions/normal/normal_collisions.cpp Reuses shared ipc::sqr helper by including scalar_math.hpp.
src/ipc/broad_phase/voxel_size_heuristic.cpp Reuses shared ipc::sqr helper by including scalar_math.hpp.
src/ipc/barrier/barrier.hpp Updates TwoStageBarrier documentation to include d <= 0 convention.
src/ipc/barrier/barrier.cpp Refactors barriers to select_lazy, adds SIMD instantiations, and uses ipc::log/sqr/infinity.
src/ipc/adhesion/CMakeLists.txt Stops compiling deleted adhesion .cpp.
src/ipc/adhesion/adhesion.cpp Deletes old scalar adhesion implementation (now header-only templates).
python/src/geometry/angle.cpp Binds Python angle APIs to detail::…<double> template instantiations.
python/src/friction/smooth_mu.cpp Binds Python smooth-μ APIs to templated <double> instantiations.
python/src/friction/smooth_friction_mollifier.cpp Binds Python friction mollifier APIs to templated <double> instantiations.
python/src/adhesion/adhesion.cpp Binds Python adhesion APIs to templated <double> instantiations.
docs/source/about/release_notes.rst Documents new templating/SIMD support and the PUBLIC SIMD flag/link behavior.
docs/source/about/dependencies.rst Adds warning about PUBLIC SIMD flags and binary portability implications.
docs/source/_static/graphviz/dependencies.dot Updates dependency graph legend and indicates optional deps via dashed edges and xsimd scope.
CMakeLists.txt Makes SIMD compile options and xsimd linkage PUBLIC when SIMD is enabled.
cmake/recipes/meshfem_sparse.cmake Switches generated export header creation to file(CONFIGURE CONTENT ...).
cmake/ipc_toolkit/ipc_toolkit_warnings.cmake Adds targeted warning suppressions for specific GCC diagnostics.
CLAUDE.md Adds benchmark/test-running guidance (notably excluding [!benchmark] when filtering [simd]).
.gitignore Ignores __cmake_systeminformation.
Review details
  • Files reviewed: 80/82 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/ipc/tangent/closest_point.hpp
The nearly-parallel sweep stopped at 1e-3, so the singular branch of
solve_spd_2x2 was never taken per-lane. Slide the parallel pair along
its shared direction so the zeroed lane leaves a residual of order
‖b‖; neutering the det <= 0 guard now trips the assert.
Random segments are unconstrained in direction and offset, so the closest
points routinely land far off both ends and the coordinates amplify the
inputs. On an 8-lane runner one draw drifted 1.2e-14 relative, just over
VALUE_TOL, while the same test passes at 2 lanes. The sibling
near-parallel test already uses this bound for the same quantity.
The --coverage compile options were PRIVATE, so only the library's own
translation units were instrumented. Much of the library is header-only
templates instantiated in the caller's TU, so a header exercised solely by
tests recorded no coverage and read as untested: adhesion.hpp at 34.6%
despite scalar and batch tests covering every function in it.

The link options were already PUBLIC, and the workflow strips '*tests/*'
from the report, which only has an effect if tests are instrumented.
A single "*" does not cross a "/", so "tests/*" only matched files
sitting directly in tests/ and everything under tests/src/tests/ was
reported. Confirmed with codecov's validator: the old pattern compiles to
(?s:tests/[^/]*)\Z, the new one to (?s:tests/.*)\Z.
The closest-point Hessians had no test at all: the existing cases
finite-difference each Jacobian against the value, then stop. Check each
Hessian against a finite difference of the corresponding Jacobian, in the
same cases.

The front ends also have a runtime branch on size() for argument types that
do not carry their dimension. Every test passed a fixed-size vector, so only
the if constexpr arms ran. Passing the same points as a VectorMax3d takes
the fallback; both reach the same kernel, so the results must be identical.

Takes tangent_basis.hpp from 73.3% to 100% and closest_point.hpp from 65.4%
to 100%.
Coverage flags on ipc_toolkit go back to PRIVATE; the test target sets the
same flags on itself. Instrumentation still reaches the tests, which it must
because a template is only compiled where it is instantiated, but consumers
of ipc::toolkit no longer inherit --coverage.
The naive a00*a11 - a01*a10 cancels catastrophically as the edges approach
parallel, which is where the closest-point solve is used. Recovering the
dropped rounding error with fma makes the determinant good to a couple of
ulp however badly the products cancel.

Measured against the exact determinant of the same double inputs, the
relative error at 1e-4 rad drops from 2.5e-9 to 4.5e-17, and the full solve
improves by the same factor: the determinant, not the numerators, was the
dominant error for this geometry. Well-conditioned angles are unchanged.

Degrades to the naive expression, rather than misbehaving, where xsimd has
no hardware fma; NEON and FMA3 both provide a fused one.
The probe needs -mavx2 -mfma and <immintrin.h>, so it can only ever answer
for x86. On AArch64 it failed while the chip does have hardware FMA, which
reads like a missing capability. Only the log line changes; FMA_FOUND and
FMA_FLAGS are untouched, and an empty FMA_FLAGS stays correct on AArch64
because fmadd and vfmaq_* are mandatory there with no flag to opt into.
The old note said accuracy was unchanged, which was measured before the
determinant used Kahan's fused form. Measured against the exact solution of
the same double inputs, the closed form now beats LDLT by up to 5.6e7x near
parallel, because LDLT's second pivot suffers the same cancellation the
naive determinant did.
abs, atan2, fma, log and sqrt carry the most collision-prone names in
C++.
At ipc scope they join the overload set of anyone writing
'using namespace ipc;', where ipc::abs was already found to hijack
Eigen's
array abs. They are mechanism, not API: each exists only so a template
can
reach the xsimd and autodiff overloads by ADL.

Following Eigen::numext, they now live in their own namespace. Not
ipc::detail, since the kernels live there and would pick these up by
ordinary lookup ahead of ADL, reproducing the same hijack one level
down.

sqr, cubic and MOLLIFIER_THRESHOLD_EPS stay at ipc scope; they collide
with
nothing and Math<T>::sqr forwards to them as public API.
@zfergus
zfergus merged commit b778f64 into main Sep 6, 2026
21 checks passed
@zfergus
zfergus deleted the feature/simd-distance branch September 6, 2026 16:37
zfergus added a commit that referenced this pull request Sep 7, 2026
Main's SIMD batch support (#251) instantiates the distance, geometry, tangent,
and barrier templates for xsimd batches. Sixteen of those files are shared
device sources here, and a batch has no device-callable operations, so nvcc
would have compiled host-only calls into device code for every one of them.

Rather than split each file, we let a shared device source be compiled twice
under CUDA and split the instantiations between the passes:

- config.hpp gains IPC_TOOLKIT_INSTANTIATE_DEVICE_SCALARS and
  IPC_TOOLKIT_INSTANTIATE_HOST_SCALARS, both 1 when CUDA is off.
- ipc_toolkit_target_shared_device_sources() now adds the .cpp alongside its
  generated .cu wrapper, defining one switch per pass, so float and double are
  emitted by nvcc and the autodiff and batch scalars by the host compiler.
  Every symbol is still emitted exactly once.

This replaces the edge_edge/point_triangle _impl.hpp split from the previous
merge, so both files go back to main's layout.

Two of main's changes subsume workarounds this branch was carrying:

- solve_spd_2x2() replaces Eigen's pivoting LDLT with Cramer's rule for the
  same reason we needed a device path, so the __CUDA_ARCH__ branches in
  closest_point.hpp are gone. That also drops a stale IPC_TOOLKIT_HOST_DEVICE
  on a constexpr variable, which is not valid CUDA.
- scalar_math.hpp routes the std math functions through the global namespace
  under nvcc, which is what our unqualified fma and log calls were doing by
  hand. Its forwarders, sqr, cubic, and the scalar helpers in simd.hpp are
  annotated, since device code reaches them through the barrier functions.

Kept from this branch where main's version is not device-safe: the Maps
standing in for .reshaped() in the signed-distance contractions, and the
explicit column scatter in place of Eigen::all index slicing in angle.cpp.

SIMD compile flags stay PUBLIC, as main made them, but remain restricted to
C++ sources so nvcc does not read them as input files.
@zfergus zfergus added this to the v2.0.0 milestone Sep 8, 2026
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.

2 participants