From 7d2a3214a2d6b2b4c0bb15ff5e70553fe9f83fce Mon Sep 17 00:00:00 2001 From: Robert Marskar Date: Sat, 11 Jul 2026 19:19:46 +0200 Subject: [PATCH 1/3] Consolidate git submodules under Submodules/; add nanoflann, picoflann, fcpw Toward a benchmark suite (issue #109), gather all submodules under a single Submodules/ directory: - Move common-3d-test-models -> Submodules/common-3d-test-models, updating every relative mesh path in the Examples (MeshSDF, CSGUnion) and Integrations (AMReX MeshSDF/PaintEB) main.cpp defaults, Examples/CMakeLists.txt, and the docs (ObtainingEBGeometry/ExampleMeshSDF/ExampleCSGUnion.rst, READMEs) + the REUSE.toml note. URLs and the submodule's own name are left untouched. Verified MeshSDF and CSGUnion still load the mesh at the new path. - Add the benchmark comparison libraries as submodules: Submodules/nanoflann, Submodules/picoflann, Submodules/fcpw (all shallow). fcpw's Eigen dependency is its own nested submodule (deps/eigen). reuse lint compliant (submodule contents carry their own licensing). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HS5LFQihKoMrE6nbPaQW9R --- .gitmodules | 14 +++++++++++++- Docs/Sphinx/source/ExampleCSGUnion.rst | 2 +- Docs/Sphinx/source/ExampleMeshSDF.rst | 2 +- Docs/Sphinx/source/ObtainingEBGeometry.rst | 6 +++--- Examples/CMakeLists.txt | 2 +- Examples/CSGUnion/README.md | 2 +- Examples/CSGUnion/main.cpp | 2 +- Examples/MeshSDF/README.md | 2 +- Examples/MeshSDF/main.cpp | 4 ++-- Examples/README.md | 2 +- Integrations/AMReX/MeshSDF/main.cpp | 2 +- Integrations/AMReX/PaintEB/main.cpp | 2 +- REUSE.toml | 2 +- .../common-3d-test-models | 0 Submodules/fcpw | 1 + Submodules/nanoflann | 1 + Submodules/picoflann | 1 + 17 files changed, 31 insertions(+), 16 deletions(-) rename common-3d-test-models => Submodules/common-3d-test-models (100%) create mode 160000 Submodules/fcpw create mode 160000 Submodules/nanoflann create mode 160000 Submodules/picoflann diff --git a/.gitmodules b/.gitmodules index 1915074c..ed5eb559 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,16 @@ [submodule "common-3d-test-models"] - path = common-3d-test-models + path = Submodules/common-3d-test-models url = https://github.com/alecjacobson/common-3d-test-models shallow = true +[submodule "Submodules/nanoflann"] + path = Submodules/nanoflann + url = https://github.com/jlblancoc/nanoflann.git + shallow = true +[submodule "Submodules/picoflann"] + path = Submodules/picoflann + url = https://github.com/rmsalinas/picoflann.git + shallow = true +[submodule "Submodules/fcpw"] + path = Submodules/fcpw + url = https://github.com/rohan-sawhney/fcpw.git + shallow = true diff --git a/Docs/Sphinx/source/ExampleCSGUnion.rst b/Docs/Sphinx/source/ExampleCSGUnion.rst index 483e589b..12ab8cc4 100644 --- a/Docs/Sphinx/source/ExampleCSGUnion.rst +++ b/Docs/Sphinx/source/ExampleCSGUnion.rst @@ -16,4 +16,4 @@ for how to compile it with CMake, GNU Make, or a direct compiler invocation. cd Examples/CSGUnion ./CSGUnion.ex # defaults to cow.obj - ./CSGUnion.ex ../../common-3d-test-models/data/cow.obj + ./CSGUnion.ex ../../Submodules/common-3d-test-models/data/cow.obj diff --git a/Docs/Sphinx/source/ExampleMeshSDF.rst b/Docs/Sphinx/source/ExampleMeshSDF.rst index 59fb5af4..1f0863df 100644 --- a/Docs/Sphinx/source/ExampleMeshSDF.rst +++ b/Docs/Sphinx/source/ExampleMeshSDF.rst @@ -23,7 +23,7 @@ for how to compile it with CMake, GNU Make, or a direct compiler invocation. cd Examples/MeshSDF ./MeshSDF.ex # defaults to armadillo.obj - ./MeshSDF.ex ../../common-3d-test-models/data/cow.obj # or pick another mesh + ./MeshSDF.ex ../../Submodules/common-3d-test-models/data/cow.obj # or pick another mesh With no argument the example loads ``armadillo.obj`` from the ``common-3d-test-models`` submodule, so make sure it is checked out first (see :ref:`Sec:Cloning`). diff --git a/Docs/Sphinx/source/ObtainingEBGeometry.rst b/Docs/Sphinx/source/ObtainingEBGeometry.rst index 10217425..342960b5 100644 --- a/Docs/Sphinx/source/ObtainingEBGeometry.rst +++ b/Docs/Sphinx/source/ObtainingEBGeometry.rst @@ -13,7 +13,7 @@ Clone the repository from `GitHub `_: The core library is header-only and completely self-contained once cloned. However, the ready-to-run examples in :file:`Examples/` read surface meshes from the `common-3d-test-models `_ collection, -which is bundled as a git submodule (``common-3d-test-models/``) at the repository root. +which is bundled as a git submodule (``Submodules/common-3d-test-models/``) at the repository root. If you intend to run the bundled examples, clone with the submodule in one step instead: .. code-block:: bash @@ -26,13 +26,13 @@ If you already cloned without ``--recurse-submodules``, fetch the submodule afte git submodule update --init --recursive -The meshes are then available as ``.obj`` files under ``common-3d-test-models/data/``. +The meshes are then available as ``.obj`` files under ``Submodules/common-3d-test-models/data/``. Some mesh-based examples take a mesh path on the command line, resolved relative to the run directory (each example is run from its own source folder), for example: .. code-block:: bash - ./a.out ../../common-3d-test-models/data/armadillo.obj + ./a.out ../../Submodules/common-3d-test-models/data/armadillo.obj Running an example with no argument falls back to a default mesh from the submodule, so the submodule must be checked out for the examples to run. diff --git a/Examples/CMakeLists.txt b/Examples/CMakeLists.txt index 6defb562..86549f74 100644 --- a/Examples/CMakeLists.txt +++ b/Examples/CMakeLists.txt @@ -5,7 +5,7 @@ # and layer on the repo-side concerns: optional sanitizers and test registration. # # The binary is run with its source directory as the working directory so that -# relative mesh paths (e.g. "../../common-3d-test-models/data/*.obj") +# relative mesh paths (e.g. "../../Submodules/common-3d-test-models/data/*.obj") # in each example resolve correctly. function(ebgeometry_add_example name) add_subdirectory(${name}) diff --git a/Examples/CSGUnion/README.md b/Examples/CSGUnion/README.md index c5bcc62f..05ea1262 100644 --- a/Examples/CSGUnion/README.md +++ b/Examples/CSGUnion/README.md @@ -63,7 +63,7 @@ Running Run from this directory so the default mesh path resolves: ./CSGUnion.ex - ./CSGUnion.ex ../../common-3d-test-models/data/cow.obj + ./CSGUnion.ex ../../Submodules/common-3d-test-models/data/cow.obj With no argument the example loads `cow.obj` from the `common-3d-test-models` submodule, so make sure it is checked out diff --git a/Examples/CSGUnion/main.cpp b/Examples/CSGUnion/main.cpp index e7590371..15c299db 100644 --- a/Examples/CSGUnion/main.cpp +++ b/Examples/CSGUnion/main.cpp @@ -40,7 +40,7 @@ main(int argc, char* argv[]) // Mesh to merge with the sphere. Pass a path on the command line, or fall back to an OBJ file from // the common-3d-test-models submodule (path is relative to this example's source folder, where the // executable is run). See the "Building and using" docs for how to fetch the submodule. - std::string file = "../../common-3d-test-models/data/cow.obj"; + std::string file = "../../Submodules/common-3d-test-models/data/cow.obj"; if (argc >= 2) { file = std::string(argv[1]); } diff --git a/Examples/MeshSDF/README.md b/Examples/MeshSDF/README.md index c81d9b40..c60123ab 100644 --- a/Examples/MeshSDF/README.md +++ b/Examples/MeshSDF/README.md @@ -68,7 +68,7 @@ Running Run from this directory so the default mesh path resolves: ./MeshSDF.ex - ./MeshSDF.ex ../../common-3d-test-models/data/cow.obj + ./MeshSDF.ex ../../Submodules/common-3d-test-models/data/cow.obj With no argument the example loads `armadillo.obj` from the `common-3d-test-models` submodule, so make sure it is checked out diff --git a/Examples/MeshSDF/main.cpp b/Examples/MeshSDF/main.cpp index 874497b8..1380c2c4 100644 --- a/Examples/MeshSDF/main.cpp +++ b/Examples/MeshSDF/main.cpp @@ -31,9 +31,9 @@ int main(int argc, char* argv[]) { // Path to a surface mesh (STL/PLY/VTK/OBJ). Pass one on the command line, e.g. - // ./a.out ../../common-3d-test-models/data/cow.obj + // ./a.out ../../Submodules/common-3d-test-models/data/cow.obj // Paths are resolved relative to the run directory (this example's source folder when run via ctest). - std::string file = "../../common-3d-test-models/data/armadillo.obj"; + std::string file = "../../Submodules/common-3d-test-models/data/armadillo.obj"; if (argc == 2) { file = std::string(argv[1]); diff --git a/Examples/README.md b/Examples/README.md index bc454aa6..e7903cfd 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -17,5 +17,5 @@ Examples that couple EBGeometry to a third-party application code (AMReX, Chombo [`Integrations/`](../Integrations/README.md) at the repository root instead. The mesh files used by the examples come from the [common-3d-test-models](https://github.com/alecjacobson/common-3d-test-models) -git submodule (`common-3d-test-models/`) at the repository root. Clone the repository with `--recurse-submodules` (or run +git submodule (`Submodules/common-3d-test-models/`) at the repository root. Clone the repository with `--recurse-submodules` (or run `git submodule update --init --recursive`); see the "Building and using" documentation for details. diff --git a/Integrations/AMReX/MeshSDF/main.cpp b/Integrations/AMReX/MeshSDF/main.cpp index a46146d5..46a89b50 100644 --- a/Integrations/AMReX/MeshSDF/main.cpp +++ b/Integrations/AMReX/MeshSDF/main.cpp @@ -96,7 +96,7 @@ main(int argc, char* argv[]) // Mesh file (STL/PLY/VTK/OBJ). Override with 'filename=' in the inputs file. The default is an OBJ // from the common-3d-test-models submodule (see the "Building and using" docs for how to fetch it); the // path is relative to this example's source folder, where the executable is run. - filename = "../../common-3d-test-models/data/armadillo.obj"; + filename = "../../Submodules/common-3d-test-models/data/armadillo.obj"; pp.query("filename", filename); RealBox rb({-100, -75, -100}, {100, 125, 100}); // bounds the default armadillo mesh diff --git a/Integrations/AMReX/PaintEB/main.cpp b/Integrations/AMReX/PaintEB/main.cpp index bf7cee59..7cf1cf86 100644 --- a/Integrations/AMReX/PaintEB/main.cpp +++ b/Integrations/AMReX/PaintEB/main.cpp @@ -112,7 +112,7 @@ main(int argc, char* argv[]) // Mesh file (STL/PLY/VTK/OBJ). Override with 'filename=' in the inputs file. The default is a // small OBJ from the common-3d-test-models submodule (see the "Building and using" docs for how to // fetch it); the path is relative to this example's source folder, where the executable is run. - filename = "../../common-3d-test-models/data/suzanne.obj"; + filename = "../../Submodules/common-3d-test-models/data/suzanne.obj"; pp.query("filename", filename); RealBox rb({-5, -1, 2}, {0, 4, 6}); // bounds the default suzanne mesh diff --git a/REUSE.toml b/REUSE.toml index 99f62e58..9f53e573 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -6,7 +6,7 @@ # Source code carries inline SPDX headers instead; see Source/ and Examples/*/main.cpp. # # NOTE: third-party example meshes are no longer redistributed here; they are pulled in -# via the common-3d-test-models git submodule at the repository root (a separate repository +# via the common-3d-test-models git submodule under Submodules/ (a separate repository # with its own licensing, which REUSE does not descend into). version = 1 diff --git a/common-3d-test-models b/Submodules/common-3d-test-models similarity index 100% rename from common-3d-test-models rename to Submodules/common-3d-test-models diff --git a/Submodules/fcpw b/Submodules/fcpw new file mode 160000 index 00000000..61814ff0 --- /dev/null +++ b/Submodules/fcpw @@ -0,0 +1 @@ +Subproject commit 61814ff0c7b69d61dac3b9725cda1541b1b3ec4f diff --git a/Submodules/nanoflann b/Submodules/nanoflann new file mode 160000 index 00000000..ff0eb50a --- /dev/null +++ b/Submodules/nanoflann @@ -0,0 +1 @@ +Subproject commit ff0eb50a4b972c642c54fe453095b724d9ae7c60 diff --git a/Submodules/picoflann b/Submodules/picoflann new file mode 160000 index 00000000..d5fd165a --- /dev/null +++ b/Submodules/picoflann @@ -0,0 +1 @@ +Subproject commit d5fd165aa8f0f91ceb6277252db3e8f4455cb470 From d2b834364f886fa8ddd6f8c47bb599cc81a66dd4 Mon Sep 17 00:00:00 2001 From: Robert Marskar Date: Sat, 11 Jul 2026 19:29:14 +0200 Subject: [PATCH 2/3] Add Benchmark/ suite: PointCloudBVH vs picoflann/nanoflann, TriMeshSDF vs fcpw Per issue #109, add a benchmark suite comparing EBGeometry against other geometry-query libraries (pinned as submodules under Submodules/). Illustrative and not CI-tested, like Integrations/. - Benchmark/NearestNeighbor: all-nearest-neighbor over a 500k point cloud -- PointCloudBVH vs picoflann vs nanoflann. Flann queries iterated in Hilbert order for a fair (warm-cache) comparison; the one-time sort they need is reported separately (EBGeometry reuses its build order for free). PointCloudBVH ~0.29 us/pt, nanoflann ~0.25, picoflann ~0.47; folding the sort in, PointCloudBVH wins the end-to-end job. - Benchmark/MeshSDF: closest-point on a triangle mesh (armadillo, ~100k tris, float) -- TriMeshSDF vs fcpw, the task both are built for. Mesh parsed once; each builds its own BVH over the same triangles and answers the same queries; distances cross-checked (0 mismatches). TriMeshSDF ~2.6 us/query vs fcpw ~7.5 -- with the caveat, documented, that fcpw runs its scalar Eigen fallback here (Enoki off) while TriMeshSDF is SIMD, so fcpw's vectorized path would narrow the query gap. Adds a Benchmark.rst docs page (wired into the toctree) and a REUSE.toml block for the benchmark build files/READMEs (main.cpp carry inline SPDX). Both benchmarks build via their GNUmakefiles against the Submodules/ deps. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HS5LFQihKoMrE6nbPaQW9R --- Benchmark/MeshSDF/GNUmakefile | 25 +++ Benchmark/MeshSDF/main.cpp | 131 +++++++++++++++ Benchmark/NearestNeighbor/GNUmakefile | 24 +++ Benchmark/NearestNeighbor/main.cpp | 222 ++++++++++++++++++++++++++ Benchmark/README.md | 67 ++++++++ Docs/Sphinx/source/Benchmark.rst | 29 ++++ Docs/Sphinx/source/index.rst | 1 + REUSE.toml | 10 ++ 8 files changed, 509 insertions(+) create mode 100644 Benchmark/MeshSDF/GNUmakefile create mode 100644 Benchmark/MeshSDF/main.cpp create mode 100644 Benchmark/NearestNeighbor/GNUmakefile create mode 100644 Benchmark/NearestNeighbor/main.cpp create mode 100644 Benchmark/README.md create mode 100644 Docs/Sphinx/source/Benchmark.rst diff --git a/Benchmark/MeshSDF/GNUmakefile b/Benchmark/MeshSDF/GNUmakefile new file mode 100644 index 00000000..8ec70447 --- /dev/null +++ b/Benchmark/MeshSDF/GNUmakefile @@ -0,0 +1,25 @@ +# Benchmark: EBGeometry TriMeshSDF vs fcpw (closest-point on a triangle mesh). +# Requires the Submodules/fcpw submodule plus its Eigen dependency, and the mesh submodule: +# git submodule update --init Submodules/fcpw Submodules/common-3d-test-models +# git -C Submodules/fcpw submodule update --init deps/eigen # Eigen only (skip GPU slang-rhi) +# EBGEOMETRY_HOME defaults to the repo root (two levels up). + +EBGEOMETRY_HOME ?= ../.. +CXX ?= g++ +CXXFLAGS ?= -std=c++17 -O3 -march=native + +INCLUDES := -I$(EBGEOMETRY_HOME) \ + -I$(EBGEOMETRY_HOME)/Submodules/fcpw/include \ + -I$(EBGEOMETRY_HOME)/Submodules/fcpw/deps/eigen + +TARGET := MeshSDF.ex + +$(TARGET): main.cpp + $(CXX) $(CXXFLAGS) $(INCLUDES) $< -o $@ + +.PHONY: run clean +run: $(TARGET) + ./$(TARGET) + +clean: + $(RM) $(TARGET) diff --git a/Benchmark/MeshSDF/main.cpp b/Benchmark/MeshSDF/main.cpp new file mode 100644 index 00000000..02713739 --- /dev/null +++ b/Benchmark/MeshSDF/main.cpp @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2026 Robert Marskar +// +// SPDX-License-Identifier: GPL-3.0-or-later + +// Benchmark: EBGeometry TriMeshSDF vs fcpw (https://github.com/rohan-sawhney/fcpw) on closest-point +// queries over a triangle mesh -- the task both libraries are built for (a BVH over triangles, +// answering "closest point on the surface"). Float precision (fcpw is float). +// +// The mesh is parsed once (shared, untimed) from the common-3d-test-models submodule. Each library +// then builds its own BVH over the same triangles (timed) and answers the same random closest-point +// queries (timed). Results are cross-checked: |TriMeshSDF signed distance| vs fcpw's unsigned +// closest-surface distance. Note TriMeshSDF additionally computes the sign (it is a signed-distance +// library); fcpw's findClosestPoint returns the unsigned closest point. + +#include +#include +#include +#include +#include +#include + +#include + +#include + +using T = float; // fcpw is float +using Vec3 = EBGeometry::Vec3T; +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(); +constexpr std::size_t W = EBGeometry::TriangleSoA::DefaultWidth(); +using SDF = EBGeometry::TriMeshSDF; + +int +main(int argc, char** argv) +{ + const std::string objFile = (argc > 1) ? argv[1] : "../../Submodules/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(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 u(T(-1), T(1)); + std::vector 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 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> V; + std::vector 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(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, false /* vectorize (needs Enoki) */); + timer.stop(); + const double fcpwBuildMs = 1.0e3 * timer.seconds(); + + std::vector 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); + + // Cross-check on a spread sample: unsigned closest-surface distance must agree. + std::size_t bad = 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))) { + bad++; + } + } + + std::printf(" %-12s build %7.1f ms query %7.3f us/query\n", "TriMeshSDF", ebBuildMs, ebQueryUs); + std::printf(" %-12s build %7.1f ms query %7.3f us/query\n", "fcpw", fcpwBuildMs, fcpwQueryUs); + std::printf(" cross-check: %zu/%zu sample mismatches (|TriMeshSDF| vs fcpw unsigned distance)\n", bad, sampleSize); + return 0; +} diff --git a/Benchmark/NearestNeighbor/GNUmakefile b/Benchmark/NearestNeighbor/GNUmakefile new file mode 100644 index 00000000..b1236284 --- /dev/null +++ b/Benchmark/NearestNeighbor/GNUmakefile @@ -0,0 +1,24 @@ +# Benchmark: PointCloudBVH vs picoflann vs nanoflann (all-nearest-neighbor). +# Requires the Submodules/{picoflann,nanoflann} submodules: +# git submodule update --init Submodules/picoflann Submodules/nanoflann +# EBGEOMETRY_HOME defaults to the repo root (two levels up). + +EBGEOMETRY_HOME ?= ../.. +CXX ?= g++ +CXXFLAGS ?= -std=c++17 -O3 -march=native + +INCLUDES := -I$(EBGEOMETRY_HOME) \ + -I$(EBGEOMETRY_HOME)/Submodules/picoflann \ + -I$(EBGEOMETRY_HOME)/Submodules/nanoflann/include + +TARGET := NearestNeighbor.ex + +$(TARGET): main.cpp + $(CXX) $(CXXFLAGS) $(INCLUDES) $< -o $@ + +.PHONY: run clean +run: $(TARGET) + ./$(TARGET) + +clean: + $(RM) $(TARGET) diff --git a/Benchmark/NearestNeighbor/main.cpp b/Benchmark/NearestNeighbor/main.cpp new file mode 100644 index 00000000..9b1bcb50 --- /dev/null +++ b/Benchmark/NearestNeighbor/main.cpp @@ -0,0 +1,222 @@ +// SPDX-FileCopyrightText: 2026 Robert Marskar +// +// SPDX-License-Identifier: GPL-3.0-or-later + +// Benchmark: EBGeometry PointCloudBVH vs picoflann vs nanoflann, all-nearest-neighbor. +// 500k random 3D points in the unit cube (double). Every point's nearest OTHER point. All three +// verified against a brute-force sample. Both KD-trees used vanilla; distances are squared throughout. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "nanoflann.hpp" +#include "picoflann.h" + +using T = double; +using Vec3 = EBGeometry::Vec3T; + +constexpr std::size_t numPoints = 500000; +constexpr std::size_t sampleSize = 500; +constexpr std::uint64_t pointSeed = 123456789ULL; + +namespace { + +// picoflann adapter. +struct Vec3Adapter +{ + inline T + operator()(const Vec3& a_p, int a_dim) const + { + return a_p[a_dim]; + } +}; + +// nanoflann dataset adaptor over std::vector. +struct NanoCloud +{ + const std::vector& pts; + inline std::size_t + kdtree_get_point_count() const + { + return pts.size(); + } + inline T + kdtree_get_pt(const std::size_t a_idx, const std::size_t a_dim) const + { + return pts[a_idx][a_dim]; + } + template + bool + kdtree_get_bbox(BBOX&) const + { + return false; + } +}; +using NanoTree = nanoflann::KDTreeSingleIndexAdaptor, NanoCloud, 3>; + +T +bruteForceNN2(std::size_t a_self, const std::vector& a_pos) +{ + T best = std::numeric_limits::max(); + for (std::size_t i = 0; i < a_pos.size(); i++) { + if (i != a_self) { + best = std::min(best, (a_pos[i] - a_pos[a_self]).length2()); + } + } + return best; +} + +} // namespace + +int +main() +{ + std::cout << "All-nearest-neighbor: PointCloudBVH vs picoflann vs nanoflann\n"; + std::cout << " Points = " << numPoints << " (double, unit cube)\n\n"; + + const std::vector positions = EBGeometry::Random::samplePoints(numPoints, pointSeed); + const std::vector meta(numPoints); + + EBGeometry::SimpleTimer timer; + + // Query the flann trees in a spatially-coherent (Hilbert) order too, so their node cache is as warm + // as EBGeometry's leaf-order batch. EBGeometry gets its order free from the build; the flann libs + // must sort -- time that once so it can be folded in if desired. + timer.start(); + const std::vector order = EBGeometry::SFC::order(positions); + timer.stop(); + const double sortUsPerPt = 1.0e6 * timer.seconds() / double(numPoints); + + const std::size_t stride = numPoints / sampleSize; + std::vector truth(sampleSize); + timer.start(); + for (std::size_t s = 0; s < sampleSize; s++) { + truth[s] = bruteForceNN2(s * stride, positions); + } + timer.stop(); + const double bruteUsPerPt = 1.0e6 * timer.seconds() / double(sampleSize); + + auto ok = [&](T a_got, std::size_t a_s) { + return std::abs(a_got - truth[a_s]) <= 1.0e-9 * std::max(truth[a_s], T(1)); + }; + + std::cout << std::left << std::setw(22) << "Method" << std::right << std::setw(12) << "Build(ms)" << std::setw(14) + << "Query(us/pt)" << std::setw(12) << "vs brute" << '\n'; + std::cout << std::string(60, '-') << '\n'; + std::cout << std::fixed; + std::cout << std::left << std::setw(22) << "Brute force" << std::right << std::setw(12) << "--" << std::setw(14) + << std::setprecision(3) << bruteUsPerPt << std::setw(12) << "1.0x" << '\n'; + + auto row = [&](const char* a_name, double a_buildMs, double a_queryUs, std::size_t a_bad) { + std::cout << std::left << std::setw(22) << a_name << std::right << std::setw(12) << std::setprecision(1) + << a_buildMs << std::setw(14) << std::setprecision(3) << a_queryUs << std::setw(11) + << std::setprecision(1) << bruteUsPerPt / a_queryUs << "x" << (a_bad ? " MISMATCH!" : "") << '\n'; + }; + + // ── EBGeometry PointCloudBVH (batched all-NN) ── + { + timer.start(); + const EBGeometry::PointCloudBVH bvh(positions, meta); + timer.stop(); + const double buildMs = 1.0e3 * timer.seconds(); + + timer.start(); + const auto graph = bvh.allNearestNeighbors(1); + timer.stop(); + const double queryUs = 1.0e6 * timer.seconds() / double(numPoints); + + std::size_t bad = 0; + for (std::size_t s = 0; s < sampleSize; s++) { + bad += !ok(graph[s * stride].distanceSquared, s); + } + row("PointCloudBVH", buildMs, queryUs, bad); + } + + // ── picoflann (per-point searchKnn) ── + { + picoflann::KdTreeIndex<3, Vec3Adapter> kdtree; + timer.start(); + kdtree.build(positions); + timer.stop(); + const double buildMs = 1.0e3 * timer.seconds(); + + volatile T sink = T(0); + timer.start(); + for (const std::uint32_t p : order) { + const auto res = kdtree.searchKnn(positions, positions[p], 2); + for (const auto& pr : res) { + if (pr.first != p) { + sink += pr.second; + break; + } + } + } + timer.stop(); + (void)sink; + const double queryUs = 1.0e6 * timer.seconds() / double(numPoints); + + std::size_t bad = 0; + for (std::size_t s = 0; s < sampleSize; s++) { + const auto res = kdtree.searchKnn(positions, positions[s * stride], 2); + T got = std::numeric_limits::max(); + for (const auto& pr : res) { + if (pr.first != s * stride) { + got = pr.second; + break; + } + } + bad += !ok(got, s); + } + row("picoflann", buildMs, queryUs, bad); + } + + // ── nanoflann (per-point findNeighbors, k=2) ── + { + NanoCloud cloud{positions}; + timer.start(); + NanoTree index(3, cloud, nanoflann::KDTreeSingleIndexAdaptorParams(10 /* leaf_max_size */)); + index.buildIndex(); + timer.stop(); + const double buildMs = 1.0e3 * timer.seconds(); + + auto nnOther = [&](std::size_t i) { + std::size_t idx[2]; + T d2[2]; + nanoflann::KNNResultSet rs(2); + rs.init(idx, d2); + const T qp[3] = {positions[i][0], positions[i][1], positions[i][2]}; + index.findNeighbors(rs, qp); + // idx[0] is the point itself (distance 0); take the first neighbor that is not itself. + return (idx[0] != i) ? d2[0] : d2[1]; + }; + + volatile T sink = T(0); + timer.start(); + for (const std::uint32_t p : order) { + sink += nnOther(p); + } + timer.stop(); + (void)sink; + const double queryUs = 1.0e6 * timer.seconds() / double(numPoints); + + std::size_t bad = 0; + for (std::size_t s = 0; s < sampleSize; s++) { + bad += !ok(nnOther(s * stride), s); + } + row("nanoflann", buildMs, queryUs, bad); + } + + std::cout << "\n flann query loops iterate in Hilbert order (warm cache, like EBGeometry's leaf order).\n"; + std::cout << " One-time Hilbert sort the flann libs need for that order: " << std::setprecision(3) << sortUsPerPt + << " us/pt (add to their query if counted;\n EBGeometry reuses its build order for free).\n"; + + return 0; +} diff --git a/Benchmark/README.md b/Benchmark/README.md new file mode 100644 index 00000000..2c04bf86 --- /dev/null +++ b/Benchmark/README.md @@ -0,0 +1,67 @@ +Benchmark +========= + +Head-to-head performance comparisons of EBGeometry against other open-source geometry-query +libraries. Like `Integrations/`, this is **illustrative and not part of CI** — it depends on +external libraries with their own build systems and release cadences (see issue #109). + +The comparison libraries are pinned as git submodules under [`Submodules/`](../Submodules): + +* [nanoflann](https://github.com/jlblancoc/nanoflann) — header-only KD-tree (point kNN) +* [picoflann](https://github.com/rmsalinas/picoflann) — tiny header-only KD-tree (point kNN) +* [fcpw](https://github.com/rohan-sawhney/fcpw) — closest-point / SDF on triangle meshes + +Fetch them (and the mesh submodule) with: + +```bash +git submodule update --init Submodules/nanoflann Submodules/picoflann Submodules/fcpw Submodules/common-3d-test-models +git -C Submodules/fcpw submodule update --init deps/eigen # fcpw's Eigen (skip the GPU slang-rhi dep) +``` + +Each benchmark has a `GNUmakefile` (`make && ./.ex`). Every result is cross-checked against a +brute-force / independent baseline so a wrong answer shows up as a mismatch. + +`NearestNeighbor/` — all-nearest-neighbor on a point cloud +---------------------------------------------------------- + +For every point in a 500,000-point uniform cloud in the unit cube (double precision), find its +nearest *other* point. Compares `PointCloudBVH` vs picoflann vs nanoflann. + +Representative result (one machine): + +``` +Method Build(ms) Query(us/pt) +PointCloudBVH ~78 0.29 +nanoflann ~120 0.25 +picoflann ~65 0.47 +``` + +- The KD-tree queries are iterated in **Hilbert order** so their node cache is as warm as + `PointCloudBVH`'s leaf-order batch (natural order is ~2x slower — unfair). That order costs the + KD-trees a one-time ~0.18 us/pt spatial sort; `PointCloudBVH` reuses the ordering its build already + produced, for free. Folding the sort in: `PointCloudBVH` ~0.29 vs nanoflann ~0.43 vs picoflann ~0.65. +- nanoflann has the fastest raw traversal; `PointCloudBVH` wins the end-to-end all-NN job (free query + order + faster build) and builds ~1.5x faster than nanoflann. + +`MeshSDF/` — closest-point on a triangle mesh +--------------------------------------------- + +Closest-point queries against a triangle mesh (armadillo, ~100k triangles, float) — the task fcpw and +EBGeometry's `TriMeshSDF` are both built for. The mesh is parsed once; each library then builds its +own BVH over the same triangles and answers the same queries. + +Representative result (one machine, 100k queries): + +``` +Method Build(ms) Query(us/query) +TriMeshSDF ~65 2.6 +fcpw ~55 7.5 +``` + +- **Caveat:** fcpw here runs its **scalar Eigen fallback** (`FCPW_USE_ENOKI` off), while `TriMeshSDF` + is SIMD-vectorized (SoA triangle leaves). So the query gap flatters EBGeometry — fcpw's intended + fast path uses Enoki CPU vectorization, which would narrow it. Enabling Enoki is a heavier build + (an extra dependency) and is left as a follow-up. Build times and correctness are directly + comparable; the cross-check confirms the distances agree. +- `TriMeshSDF` computes the *signed* distance (its purpose); fcpw's `findClosestPoint` returns the + unsigned closest point, so the comparison is on unsigned closest-surface distance. diff --git a/Docs/Sphinx/source/Benchmark.rst b/Docs/Sphinx/source/Benchmark.rst new file mode 100644 index 00000000..5115a975 --- /dev/null +++ b/Docs/Sphinx/source/Benchmark.rst @@ -0,0 +1,29 @@ +.. _Chap:Benchmark: + +Benchmarks +========== + +.. important:: + + Like the :ref:`Chap:Integrations` examples, the benchmarks are illustrative and **not built or run + as part of EBGeometry's continuous integration** -- they depend on external libraries with their + own build systems. They pin those libraries as git submodules under :file:`Submodules/`. See the + benchmark tracking issue on GitHub for context and planned additions. + +The :file:`Benchmark/` folder compares EBGeometry against other open-source geometry-query libraries +on tasks they have in common. Fetch the comparison libraries (and the mesh submodule) with: + +.. code-block:: bash + + git submodule update --init Submodules/nanoflann Submodules/picoflann Submodules/fcpw Submodules/common-3d-test-models + git -C Submodules/fcpw submodule update --init deps/eigen # fcpw's Eigen (skip the GPU dep) + +Each benchmark ships a ``GNUmakefile`` (``make && ./.ex``) and cross-checks every result against +a brute-force / independent baseline. See each folder's ``README.md`` for the full detail and +representative numbers. + +* :file:`Benchmark/NearestNeighbor` -- all-nearest-neighbor over a point cloud: ``PointCloudBVH`` vs + `nanoflann `_ vs + `picoflann `_. +* :file:`Benchmark/MeshSDF` -- closest-point on a triangle mesh: ``TriMeshSDF`` vs + `fcpw `_. diff --git a/Docs/Sphinx/source/index.rst b/Docs/Sphinx/source/index.rst index 18594e0f..be4342b7 100644 --- a/Docs/Sphinx/source/index.rst +++ b/Docs/Sphinx/source/index.rst @@ -124,6 +124,7 @@ Examples ExampleRandomCity.rst ExampleOctreeBoundingVolume.rst Integrations.rst + Benchmark.rst Contributing and testing ************************ diff --git a/REUSE.toml b/REUSE.toml index 9f53e573..08c965ab 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -69,6 +69,16 @@ path = [ SPDX-FileCopyrightText = "2022 Robert Marskar " SPDX-License-Identifier = "GPL-3.0-or-later" +# Benchmark comparison programs (illustrative, not CI-tested). main.cpp files carry inline SPDX. +[[annotations]] +path = [ + "Benchmark/README.md", + "Benchmark/**/GNUmakefile", + "Benchmark/**/README.md", +] +SPDX-FileCopyrightText = "2022 Robert Marskar " +SPDX-License-Identifier = "GPL-3.0-or-later" + # Tests: build files and the small self-authored test fixture mesh [[annotations]] path = [ From acd2ba5d74268d00dc9630576269dd64ae44ce23 Mon Sep 17 00:00:00 2001 From: Robert Marskar Date: Sun, 12 Jul 2026 01:36:15 +0200 Subject: [PATCH 3/3] Benchmark/MeshSDF: build fcpw with Enoki and add TriangleMeshDistance to the comparison - fcpw is now built with its Enoki CPU vectorization (FCPW_USE_ENOKI, vectorized MBVH) instead of the scalar Eigen fallback -- a like-for-like float/SIMD comparison with TriMeshSDF. Enoki's headers are vendored inside the fcpw submodule, so no extra fetch is needed; FCPW_SIMD_WIDTH is a GNUmakefile variable (default 8 = AVX2; set 4 for SSE, 16 for AVX-512). - Add TriangleMeshDistance (https://github.com/InteractiveComputerGraphics/TriangleMeshDistance) as a third contender, pinned as a submodule under Submodules/. It is a header-only, double-precision, scalar signed-distance library -- a widely-used reference point, noted as not a same-precision comparison. Its distances are cross-checked against TriMeshSDF alongside fcpw's (0 mismatches). Representative (armadillo ~100k triangles, 100k queries, one machine): TriMeshSDF build ~63 ms query ~2.6 us fcpw (Enoki) build ~46 ms query ~3.5 us (was ~7.5 us scalar) TriangleMeshDistance build ~115 ms query ~10.5 us (double, scalar) Updates the Benchmark README and Benchmark.rst accordingly. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HS5LFQihKoMrE6nbPaQW9R --- .gitmodules | 3 ++ Benchmark/MeshSDF/GNUmakefile | 19 +++++--- Benchmark/MeshSDF/main.cpp | 79 ++++++++++++++++++++++++++------ Benchmark/README.md | 36 +++++++++------ Docs/Sphinx/source/Benchmark.rst | 6 ++- Submodules/TriangleMeshDistance | 1 + 6 files changed, 108 insertions(+), 36 deletions(-) create mode 160000 Submodules/TriangleMeshDistance diff --git a/.gitmodules b/.gitmodules index ed5eb559..061ead1a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,3 +14,6 @@ path = Submodules/fcpw url = https://github.com/rohan-sawhney/fcpw.git shallow = true +[submodule "Submodules/TriangleMeshDistance"] + path = Submodules/TriangleMeshDistance + url = https://github.com/InteractiveComputerGraphics/TriangleMeshDistance diff --git a/Benchmark/MeshSDF/GNUmakefile b/Benchmark/MeshSDF/GNUmakefile index 8ec70447..712f1768 100644 --- a/Benchmark/MeshSDF/GNUmakefile +++ b/Benchmark/MeshSDF/GNUmakefile @@ -1,21 +1,28 @@ -# Benchmark: EBGeometry TriMeshSDF vs fcpw (closest-point on a triangle mesh). -# Requires the Submodules/fcpw submodule plus its Eigen dependency, and the mesh submodule: -# git submodule update --init Submodules/fcpw Submodules/common-3d-test-models +# 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 Submodules/fcpw Submodules/common-3d-test-models Submodules/TriangleMeshDistance # git -C Submodules/fcpw submodule update --init deps/eigen # Eigen only (skip GPU slang-rhi) -# EBGEOMETRY_HOME defaults to the repo root (two levels up). +# 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)/Submodules/fcpw/include \ - -I$(EBGEOMETRY_HOME)/Submodules/fcpw/deps/eigen + -I$(EBGEOMETRY_HOME)/Submodules/fcpw/deps/eigen \ + -I$(EBGEOMETRY_HOME)/Submodules/fcpw/deps/enoki/include \ + -I$(EBGEOMETRY_HOME)/Submodules/TriangleMeshDistance/TriangleMeshDistance/include + +DEFINES := -DFCPW_USE_ENOKI -DFCPW_SIMD_WIDTH=$(FCPW_SIMD_WIDTH) TARGET := MeshSDF.ex $(TARGET): main.cpp - $(CXX) $(CXXFLAGS) $(INCLUDES) $< -o $@ + $(CXX) $(CXXFLAGS) $(DEFINES) $(INCLUDES) $< -o $@ .PHONY: run clean run: $(TARGET) diff --git a/Benchmark/MeshSDF/main.cpp b/Benchmark/MeshSDF/main.cpp index 02713739..b3dd0c3d 100644 --- a/Benchmark/MeshSDF/main.cpp +++ b/Benchmark/MeshSDF/main.cpp @@ -2,15 +2,21 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -// Benchmark: EBGeometry TriMeshSDF vs fcpw (https://github.com/rohan-sawhney/fcpw) on closest-point -// queries over a triangle mesh -- the task both libraries are built for (a BVH over triangles, -// answering "closest point on the surface"). Float precision (fcpw is float). +// 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 BVH over the same triangles (timed) and answers the same random closest-point -// queries (timed). Results are cross-checked: |TriMeshSDF signed distance| vs fcpw's unsigned -// closest-surface distance. Note TriMeshSDF additionally computes the sign (it is a signed-distance -// library); fcpw's findClosestPoint returns the unsigned closest point. +// 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 #include @@ -22,6 +28,7 @@ #include #include +#include using T = float; // fcpw is float using Vec3 = EBGeometry::Vec3T; @@ -101,7 +108,7 @@ main(int argc, char** argv) scene.setObjectCount(1); scene.setObjectVertices(V, 0); scene.setObjectTriangles(F, 0); - scene.build(fcpw::AggregateType::Bvh_SurfaceArea, false /* vectorize (needs Enoki) */); + scene.build(fcpw::AggregateType::Bvh_SurfaceArea, true /* vectorize (Enoki MBVH) */); timer.stop(); const double fcpwBuildMs = 1.0e3 * timer.seconds(); @@ -115,17 +122,61 @@ main(int argc, char** argv) timer.stop(); const double fcpwQueryUs = 1.0e6 * timer.seconds() / double(numQueries); - // Cross-check on a spread sample: unsigned closest-surface distance must agree. - std::size_t bad = 0; + // ── 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 tmdVertices; + std::vector 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(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 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))) { - bad++; + badFcpw++; + } + if (std::abs(ebDist[i] - tmdDist[i]) > T(1.0e-3) * std::max(tmdDist[i], T(1))) { + badTmd++; } } - std::printf(" %-12s build %7.1f ms query %7.3f us/query\n", "TriMeshSDF", ebBuildMs, ebQueryUs); - std::printf(" %-12s build %7.1f ms query %7.3f us/query\n", "fcpw", fcpwBuildMs, fcpwQueryUs); - std::printf(" cross-check: %zu/%zu sample mismatches (|TriMeshSDF| vs fcpw unsigned distance)\n", bad, sampleSize); + 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; } diff --git a/Benchmark/README.md b/Benchmark/README.md index 2c04bf86..47aa5e0d 100644 --- a/Benchmark/README.md +++ b/Benchmark/README.md @@ -10,14 +10,19 @@ The comparison libraries are pinned as git submodules under [`Submodules/`](../S * [nanoflann](https://github.com/jlblancoc/nanoflann) — header-only KD-tree (point kNN) * [picoflann](https://github.com/rmsalinas/picoflann) — tiny header-only KD-tree (point kNN) * [fcpw](https://github.com/rohan-sawhney/fcpw) — closest-point / SDF on triangle meshes +* [TriangleMeshDistance](https://github.com/InteractiveComputerGraphics/TriangleMeshDistance) — header-only signed distance to triangle meshes Fetch them (and the mesh submodule) with: ```bash -git submodule update --init Submodules/nanoflann Submodules/picoflann Submodules/fcpw Submodules/common-3d-test-models +git submodule update --init Submodules/nanoflann Submodules/picoflann Submodules/fcpw \ + Submodules/TriangleMeshDistance Submodules/common-3d-test-models git -C Submodules/fcpw submodule update --init deps/eigen # fcpw's Eigen (skip the GPU slang-rhi dep) ``` +fcpw's Enoki CPU-vectorization headers are vendored inside the fcpw submodule, so no extra fetch is +needed for it. + Each benchmark has a `GNUmakefile` (`make && ./.ex`). Every result is cross-checked against a brute-force / independent baseline so a wrong answer shows up as a mismatch. @@ -46,22 +51,25 @@ picoflann ~65 0.47 `MeshSDF/` — closest-point on a triangle mesh --------------------------------------------- -Closest-point queries against a triangle mesh (armadillo, ~100k triangles, float) — the task fcpw and -EBGeometry's `TriMeshSDF` are both built for. The mesh is parsed once; each library then builds its -own BVH over the same triangles and answers the same queries. +Closest-point queries against a triangle mesh (armadillo, ~100k triangles) — the task `TriMeshSDF`, +fcpw, and TriangleMeshDistance are all built for. The mesh is parsed once; each library then builds +its own structure over the same triangles and answers the same queries. Representative result (one machine, 100k queries): ``` -Method Build(ms) Query(us/query) -TriMeshSDF ~65 2.6 -fcpw ~55 7.5 +Method Build(ms) Query(us/query) +TriMeshSDF ~63 2.6 +fcpw (Enoki) ~46 3.5 +TriangleMeshDistance ~115 10.5 ``` -- **Caveat:** fcpw here runs its **scalar Eigen fallback** (`FCPW_USE_ENOKI` off), while `TriMeshSDF` - is SIMD-vectorized (SoA triangle leaves). So the query gap flatters EBGeometry — fcpw's intended - fast path uses Enoki CPU vectorization, which would narrow it. Enabling Enoki is a heavier build - (an extra dependency) and is left as a follow-up. Build times and correctness are directly - comparable; the cross-check confirms the distances agree. -- `TriMeshSDF` computes the *signed* distance (its purpose); fcpw's `findClosestPoint` returns the - unsigned closest point, so the comparison is on unsigned closest-surface distance. +- **fcpw** is built with its **Enoki CPU vectorization** (`FCPW_USE_ENOKI`, vectorized MBVH), the same + SIMD fast path `TriMeshSDF` uses — a like-for-like float/SIMD comparison. (`FCPW_SIMD_WIDTH` in the + `GNUmakefile` should match your ISA: 4=SSE, 8=AVX2, 16=AVX-512.) fcpw's `findClosestPoint` returns + the *unsigned* closest point. +- **TriangleMeshDistance** is a header-only, **double-precision, scalar** (non-SIMD) signed-distance + library — so its query runs in double and is not a same-precision comparison; it is included as a + widely-used point of reference. +- `TriMeshSDF` and TriangleMeshDistance compute the *signed* distance (their purpose); the comparison + is on unsigned closest-surface distance, and the cross-check confirms all three agree. diff --git a/Docs/Sphinx/source/Benchmark.rst b/Docs/Sphinx/source/Benchmark.rst index 5115a975..fed6ae7d 100644 --- a/Docs/Sphinx/source/Benchmark.rst +++ b/Docs/Sphinx/source/Benchmark.rst @@ -15,7 +15,8 @@ on tasks they have in common. Fetch the comparison libraries (and the mesh submo .. code-block:: bash - git submodule update --init Submodules/nanoflann Submodules/picoflann Submodules/fcpw Submodules/common-3d-test-models + git submodule update --init Submodules/nanoflann Submodules/picoflann Submodules/fcpw \ + Submodules/TriangleMeshDistance Submodules/common-3d-test-models git -C Submodules/fcpw submodule update --init deps/eigen # fcpw's Eigen (skip the GPU dep) Each benchmark ships a ``GNUmakefile`` (``make && ./.ex``) and cross-checks every result against @@ -26,4 +27,5 @@ representative numbers. `nanoflann `_ vs `picoflann `_. * :file:`Benchmark/MeshSDF` -- closest-point on a triangle mesh: ``TriMeshSDF`` vs - `fcpw `_. + `fcpw `_ (built with its Enoki CPU vectorization) vs + `TriangleMeshDistance `_. diff --git a/Submodules/TriangleMeshDistance b/Submodules/TriangleMeshDistance new file mode 160000 index 00000000..5530eaa8 --- /dev/null +++ b/Submodules/TriangleMeshDistance @@ -0,0 +1 @@ +Subproject commit 5530eaa85537cc62b884c3f8a3e64e000129eb93