Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
39d62f5
Consolidate git submodules under Submodules/; add nanoflann, picoflan…
rmrsk Jul 11, 2026
d1124ff
Add Benchmark/ suite: PointCloudBVH vs picoflann/nanoflann, TriMeshSD…
rmrsk Jul 11, 2026
1d7bf72
Benchmark/MeshSDF: build fcpw with Enoki and add TriangleMeshDistance…
rmrsk Jul 11, 2026
e1c355d
Relocate benchmark submodules under Benchmark/, soften framing, add c…
claude Jul 15, 2026
2ddfc6c
Add PointCloudHashGrid and a sphere-surface distribution to the NN be…
claude Jul 15, 2026
dc03101
Prototype eps-approximate pruning in PointCloudBVH and benchmark it
claude Jul 15, 2026
5ec6e96
Revert "Prototype eps-approximate pruning in PointCloudBVH and benchm…
claude Jul 15, 2026
8fca504
Cap the default BVH branching ratio at 8 for float on AVX-512F (cache…
claude Jul 16, 2026
410f689
Document the float K=8 branching-ratio finding in the benchmark README
claude Jul 16, 2026
dcad7e1
Add an experimental packet-traversal allNearestNeighbors prototype
claude Jul 16, 2026
844a849
Revert "Add an experimental packet-traversal allNearestNeighbors prot…
claude Jul 16, 2026
02e1077
Revert "Document the float K=8 branching-ratio finding in the benchma…
claude Jul 16, 2026
823ad54
Revert "Cap the default BVH branching ratio at 8 for float on AVX-512…
claude Jul 16, 2026
a3eaa65
Add kd3 to the NearestNeighbor benchmark comparison
claude Jul 16, 2026
6546de5
Correct the kd3 caveat: its 2x query claim is single-threaded, not Op…
claude Jul 16, 2026
470dc18
Add a kd3 (float) row showing kd3's native SoA/SIMD best case
claude Jul 16, 2026
db8de23
Make disabled EBGEOMETRY_EXPECT truly vanish (unevaluated sizeof)
claude Jul 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -430,8 +430,39 @@ jobs:
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
ASAN_OPTIONS: detect_leaks=1

Benchmark-Compile:
needs: [Formatting, Codespell, Reuse, Doxygen-check]
name: Benchmarks (compile-only, ${{ matrix.directory }})
runs-on: ubuntu-latest
timeout-minutes: 20
# The benchmarks link against external comparison libraries with their own build
# systems, so they are not part of the normal test build. This job only *compiles*
# them (a smoke test against bit-rot as the library evolves) -- it never runs or
# times them, so no mesh data (common-3d-test-models) is needed and results are
# never treated as measurements. See Benchmark/README.md.
strategy:
fail-fast: false
matrix:
include:
- directory: Benchmark/NearestNeighbor
submodules: Benchmark/picoflann Benchmark/nanoflann Benchmark/kd3
- directory: Benchmark/MeshSDF
submodules: Benchmark/fcpw Benchmark/TriangleMeshDistance
steps:
- uses: actions/checkout@v7
- name: Fetch comparison-library submodules
run: |
git submodule update --init --depth 1 ${{ matrix.submodules }}
if [ "${{ matrix.directory }}" = "Benchmark/MeshSDF" ]; then
# fcpw's Eigen (nested submodule); Enoki is vendored inside fcpw, GPU slang-rhi is skipped.
git -C Benchmark/fcpw submodule update --init --depth 1 deps/eigen
fi
- name: Compile benchmark (build only, do not run)
working-directory: ${{ matrix.directory }}
run: make

CI-passed:
needs: [Formatting, Codespell, Reuse, Doxygen-check, Linux-GNU, Linux-Intel, Examples-GNUMake, Examples-CMake, Examples-FloatPrecision, Build-documentation, Unit-Tests, Release-Test, Sanitizers]
needs: [Formatting, Codespell, Reuse, Doxygen-check, Linux-GNU, Linux-Intel, Examples-GNUMake, Examples-CMake, Examples-FloatPrecision, Build-documentation, Unit-Tests, Release-Test, Sanitizers, Benchmark-Compile]
runs-on: ubuntu-latest
steps:
- name: Do nothing
Expand Down
19 changes: 19 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,22 @@
path = common-3d-test-models
url = https://github.com/alecjacobson/common-3d-test-models
shallow = true
[submodule "Benchmark/nanoflann"]
path = Benchmark/nanoflann
url = https://github.com/jlblancoc/nanoflann.git
shallow = true
[submodule "Benchmark/picoflann"]
path = Benchmark/picoflann
url = https://github.com/rmsalinas/picoflann.git
shallow = true
[submodule "Benchmark/fcpw"]
path = Benchmark/fcpw
url = https://github.com/rohan-sawhney/fcpw.git
shallow = true
[submodule "Benchmark/TriangleMeshDistance"]
path = Benchmark/TriangleMeshDistance
url = https://github.com/InteractiveComputerGraphics/TriangleMeshDistance
[submodule "Benchmark/kd3"]
path = Benchmark/kd3
url = https://github.com/KaruroChori/kd3
shallow = true
32 changes: 32 additions & 0 deletions Benchmark/MeshSDF/GNUmakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Benchmark: EBGeometry TriMeshSDF vs fcpw vs TriangleMeshDistance (closest-point on a triangle mesh).
# Requires the mesh submodule, fcpw (Enoki is vendored inside fcpw; Eigen is a nested submodule), and
# TriangleMeshDistance:
# git submodule update --init Benchmark/fcpw common-3d-test-models Benchmark/TriangleMeshDistance
# git -C Benchmark/fcpw submodule update --init deps/eigen # Eigen only (skip GPU slang-rhi)
# fcpw is built with its Enoki CPU vectorization: FCPW_SIMD_WIDTH matches the ISA (4=SSE, 8=AVX2,
# 16=AVX-512); adjust it below to your machine. EBGEOMETRY_HOME defaults to the repo root.

EBGEOMETRY_HOME ?= ../..
CXX ?= g++
CXXFLAGS ?= -std=c++17 -O3 -march=native
FCPW_SIMD_WIDTH ?= 8

INCLUDES := -I$(EBGEOMETRY_HOME) \
-I$(EBGEOMETRY_HOME)/Benchmark/fcpw/include \
-I$(EBGEOMETRY_HOME)/Benchmark/fcpw/deps/eigen \
-I$(EBGEOMETRY_HOME)/Benchmark/fcpw/deps/enoki/include \
-I$(EBGEOMETRY_HOME)/Benchmark/TriangleMeshDistance/TriangleMeshDistance/include

DEFINES := -DFCPW_USE_ENOKI -DFCPW_SIMD_WIDTH=$(FCPW_SIMD_WIDTH)

TARGET := MeshSDF.ex

$(TARGET): main.cpp
$(CXX) $(CXXFLAGS) $(DEFINES) $(INCLUDES) $< -o $@

.PHONY: run clean
run: $(TARGET)
./$(TARGET)

clean:
$(RM) $(TARGET)
182 changes: 182 additions & 0 deletions Benchmark/MeshSDF/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// SPDX-FileCopyrightText: 2026 Robert Marskar <robert.marskar@sintef.no>
//
// SPDX-License-Identifier: GPL-3.0-or-later

// Benchmark: EBGeometry TriMeshSDF vs fcpw (https://github.com/rohan-sawhney/fcpw) vs
// TriangleMeshDistance (https://github.com/InteractiveComputerGraphics/TriangleMeshDistance) on
// closest-point queries over a triangle mesh -- the task all three are built for (an acceleration
// structure over triangles, answering "closest point on the surface").
//
// The mesh is parsed once (shared, untimed) from the common-3d-test-models submodule. Each library
// then builds its own structure over the same triangles (timed) and answers the same random
// closest-point queries (timed). Results are cross-checked against TriMeshSDF's unsigned distance.
//
// Precision / vectorization caveats (each library on its own intended fast path):
// - TriMeshSDF -- float, SIMD-vectorized (this build's native ISA).
// - fcpw -- float; built with its Enoki CPU vectorization (FCPW_USE_ENOKI, vectorized BVH).
// fcpw returns an unsigned closest point; TriMeshSDF additionally computes a sign.
// - TriangleMeshDistance -- double, scalar (header-only, no SIMD). A signed-distance library like
// TriMeshSDF; runs its queries in double, so it is not a same-precision comparison.

#include <cmath>
#include <cstddef>
#include <cstdio>
#include <random>
#include <string>
#include <vector>

#include <EBGeometry.hpp>

#include <fcpw/fcpw.h>
#include <tmd/TriangleMeshDistance.h>

using T = float; // fcpw is float
using Vec3 = EBGeometry::Vec3T<T>;
using Meta = EBGeometry::DCEL::DefaultMetaData;

// SIMD-optimal branching factor / SoA triangle width for T (what readIntoTriangleBVH would pick).
constexpr std::size_t K = EBGeometry::BVH::DefaultBranchingRatio<T>();
constexpr std::size_t W = EBGeometry::TriangleSoA::DefaultWidth<T>();
using SDF = EBGeometry::TriMeshSDF<T, Meta, K, W>;

int
main(int argc, char** argv)
{
const std::string objFile = (argc > 1) ? argv[1] : "../../common-3d-test-models/data/armadillo.obj";
constexpr std::size_t numQueries = 100000;
constexpr std::size_t sampleSize = 500;
constexpr std::size_t maxLeafGroups = 4;

// Parse the mesh once -- shared preamble, not part of either library's timed build.
const auto tris = EBGeometry::Parser::readIntoTriangles<T, Meta>(objFile);
const std::size_t nTri = tris.size();

std::printf("MeshSDF closest-point: EBGeometry TriMeshSDF vs fcpw\n");
std::printf(" Mesh = %s (%zu triangles, float)\n Queries = %zu\n\n", objFile.c_str(), nTri, numQueries);

EBGeometry::SimpleTimer timer;

// Random query points in the mesh bounding box, expanded to 1.5x so queries sit inside and around.
Vec3 lo = +Vec3::max();
Vec3 hi = -Vec3::max();
for (const auto& tri : tris) {
for (const auto& p : tri->getVertexPositions()) {
lo = min(lo, p);
hi = max(hi, p);
}
}
const Vec3 center = T(0.5) * (lo + hi);
const Vec3 half = T(0.75) * (hi - lo);
std::mt19937 rng(12345u);
std::uniform_real_distribution<T> u(T(-1), T(1));
std::vector<Vec3> queries(numQueries);
for (auto& q : queries) {
q = center + Vec3(u(rng) * half[0], u(rng) * half[1], u(rng) * half[2]);
}

// ── EBGeometry TriMeshSDF ──
timer.start();
const SDF sdf(tris, EBGeometry::BVH::Build::SAH, maxLeafGroups);
timer.stop();
const double ebBuildMs = 1.0e3 * timer.seconds();

std::vector<T> ebDist(numQueries);
timer.start();
for (std::size_t i = 0; i < numQueries; i++) {
ebDist[i] = std::abs(sdf.signedDistance(queries[i]));
}
timer.stop();
const double ebQueryUs = 1.0e6 * timer.seconds() / double(numQueries);

// ── fcpw ──
// fcpw needs vertices + triangle indices. Use an unwelded soup (3 vertices per triangle); closest
// point on triangles is unaffected by vertex sharing. Building this soup is fcpw's setup, not parse.
std::vector<fcpw::Vector<3>> V;
std::vector<fcpw::Vector3i> F;
V.reserve(3 * nTri);
F.reserve(nTri);
timer.start();
for (const auto& tri : tris) {
const auto& p = tri->getVertexPositions();
const int base = static_cast<int>(V.size());
for (int k = 0; k < 3; k++) {
V.emplace_back(fcpw::Vector<3>(p[k][0], p[k][1], p[k][2]));
}
F.emplace_back(fcpw::Vector3i(base, base + 1, base + 2));
}
fcpw::Scene<3> scene;
scene.setObjectCount(1);
scene.setObjectVertices(V, 0);
scene.setObjectTriangles(F, 0);
scene.build(fcpw::AggregateType::Bvh_SurfaceArea, true /* vectorize (Enoki MBVH) */);
timer.stop();
const double fcpwBuildMs = 1.0e3 * timer.seconds();

std::vector<T> fcpwDist(numQueries);
timer.start();
for (std::size_t i = 0; i < numQueries; i++) {
fcpw::Interaction<3> it;
scene.findClosestPoint(fcpw::Vector<3>(queries[i][0], queries[i][1], queries[i][2]), it);
fcpwDist[i] = it.d;
}
timer.stop();
const double fcpwQueryUs = 1.0e6 * timer.seconds() / double(numQueries);

// ── TriangleMeshDistance (double, scalar, header-only) ──
// Same unwelded triangle soup, in double. Building the arrays + the structure is its timed setup,
// mirroring how fcpw's soup construction is folded into fcpw's build above.
std::vector<double> tmdVertices;
std::vector<int> tmdTriangles;
tmdVertices.reserve(9 * nTri);
tmdTriangles.reserve(3 * nTri);
timer.start();
for (const auto& tri : tris) {
const auto& p = tri->getVertexPositions();
const int base = static_cast<int>(tmdVertices.size() / 3);
for (int k = 0; k < 3; k++) {
tmdVertices.push_back(double(p[k][0]));
tmdVertices.push_back(double(p[k][1]));
tmdVertices.push_back(double(p[k][2]));
}
tmdTriangles.push_back(base);
tmdTriangles.push_back(base + 1);
tmdTriangles.push_back(base + 2);
}
const tmd::TriangleMeshDistance tmdMesh(
tmdVertices.data(), tmdVertices.size() / 3, tmdTriangles.data(), tmdTriangles.size() / 3);
timer.stop();
const double tmdBuildMs = 1.0e3 * timer.seconds();

std::vector<T> tmdDist(numQueries);
timer.start();
for (std::size_t i = 0; i < numQueries; i++) {
const tmd::Result r =
tmdMesh.signed_distance({double(queries[i][0]), double(queries[i][1]), double(queries[i][2])});
tmdDist[i] = T(std::abs(r.distance));
}
timer.stop();
const double tmdQueryUs = 1.0e6 * timer.seconds() / double(numQueries);

// Cross-check on a spread sample: unsigned closest-surface distance must agree across all three.
std::size_t badFcpw = 0;
std::size_t badTmd = 0;
for (std::size_t s = 0; s < sampleSize; s++) {
const std::size_t i = s * (numQueries / sampleSize);
if (std::abs(ebDist[i] - fcpwDist[i]) > T(1.0e-3) * std::max(fcpwDist[i], T(1))) {
badFcpw++;
}
if (std::abs(ebDist[i] - tmdDist[i]) > T(1.0e-3) * std::max(tmdDist[i], T(1))) {
badTmd++;
}
}

std::printf(" %-22s build %7.1f ms query %7.3f us/query\n", "TriMeshSDF", ebBuildMs, ebQueryUs);
std::printf(" %-22s build %7.1f ms query %7.3f us/query\n", "fcpw (Enoki)", fcpwBuildMs, fcpwQueryUs);
std::printf(" %-22s build %7.1f ms query %7.3f us/query\n", "TriangleMeshDistance", tmdBuildMs, tmdQueryUs);
std::printf(" cross-check vs TriMeshSDF: fcpw %zu/%zu, TriangleMeshDistance %zu/%zu sample mismatches\n",
badFcpw,
sampleSize,
badTmd,
sampleSize);
return 0;
}
27 changes: 27 additions & 0 deletions Benchmark/NearestNeighbor/GNUmakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Benchmark: PointCloudBVH vs picoflann vs nanoflann vs kd3 (all-nearest-neighbor).
# Requires the Benchmark/{picoflann,nanoflann,kd3} submodules:
# git submodule update --init Benchmark/picoflann Benchmark/nanoflann Benchmark/kd3
# kd3 needs C++23 (std::expected/std::span), so the whole benchmark is built with -std=c++23; it is
# compiled WITHOUT -fopenmp so kd3 (like the others) runs single-threaded, for a fair comparison.
# EBGEOMETRY_HOME defaults to the repo root (two levels up).

EBGEOMETRY_HOME ?= ../..
CXX ?= g++
CXXFLAGS ?= -std=c++23 -O3 -march=native

INCLUDES := -I$(EBGEOMETRY_HOME) \
-I$(EBGEOMETRY_HOME)/Benchmark/picoflann \
-I$(EBGEOMETRY_HOME)/Benchmark/nanoflann/include \
-I$(EBGEOMETRY_HOME)/Benchmark/kd3/include

TARGET := NearestNeighbor.ex

$(TARGET): main.cpp
$(CXX) $(CXXFLAGS) $(INCLUDES) $< -o $@

.PHONY: run clean
run: $(TARGET)
./$(TARGET)

clean:
$(RM) $(TARGET)
Loading
Loading