Skip to content

Add CDT.Comparison.Benchmarks — 6-library CDT/Delaunay performance comparison (Windows + Linux)#12

Merged
MichaConrad merged 22 commits intomainfrom
copilot/add-cdt-benchmark-project
Feb 23, 2026
Merged

Add CDT.Comparison.Benchmarks — 6-library CDT/Delaunay performance comparison (Windows + Linux)#12
MichaConrad merged 22 commits intomainfrom
copilot/add-cdt-benchmark-project

Conversation

Copy link
Contributor

Copilot AI commented Feb 23, 2026

Adds a new benchmark/CDT.Comparison.Benchmarks/ project that benchmarks CDT.NET against five other triangulation libraries across two categories: VerticesOnly (unconstrained Delaunay) and Constrained (full CDT with edges). Dataset: Constrained Sweden (~2 600 vertices, ~2 600 constraint edges).

Libraries

# Library Kind CDT type
1 CDT.NET project ref (baseline) True CDT
2 Triangle.NET Unofficial.Triangle.NET 0.0.1 True CDT
3 NetTopologySuite NetTopologySuite 2.6.0 Conforming CDT (Steiner pts)
4 artem-ogre/CDT C++ via P/Invoke True CDT
5 Spade Rust via P/Invoke CDT
6 CGAL 6.1.1 C++ via P/Invoke True CDT

Structure

  • InputReader — shared loader for the existing .txt test input format (same files used by CDT.Tests)
  • Per-library adapter — thin static class accepting double[] xs/ys + int[] ev1/ev2; benchmark methods are one-liners
  • [ShortRunJob] + [MemoryDiagnoser] + [GroupBenchmarksBy(ByCategory)] with CDT.NET as [Baseline] per group
  • Native wrappers built automatically via MSBuild BeforeTargets="Build" targets — no manual steps required on any platform:
    • native/cdt_wrapper/ — CMake + FetchContent (artem-ogre/CDT pinned to tag 1.4.4)
    • native/cgal_wrapper/ — CMake + FetchContent (CGAL 6.1.1 library ZIP + 38 Boost sub-library ZIPs, ~20 MB total, no system install needed)
    • native/spade_wrapper/ — Cargo (spade = "2.15.0")

Native Build Caching

Native wrappers are built only once — subsequent dotnet build invocations (including C#-only changes) skip cmake/cargo entirely by checking whether the output library already exists in $(OutputPath). Running dotnet clean removes the copied native libraries, causing them to be rebuilt on the next dotnet build.

Cross-platform support (Windows, Linux, macOS)

Platform C++ wrappers Rust wrapper
Windows cdt_wrapper.dll, cgal_wrapper.dll spade_wrapper.dll
macOS libcdt_wrapper.dylib, libcgal_wrapper.dylib libspade_wrapper.dylib
Linux libcdt_wrapper.so, libcgal_wrapper.so libspade_wrapper.so

If cmake or cargo are not on PATH, dotnet build emits a clear error message with the install URL rather than a cryptic failure.

CI/CD

CDT.Comparison.Benchmarks is excluded from the normal CI/CD pipeline — the existing workflow builds only CDT.Core, CDT.Tests, CDT.Benchmarks, and CDT.Viz, and runs tests against CDT.Tests directly. This keeps CI fast and unaffected by the native build prerequisites (cmake, cargo, C++ compiler) that are only needed when running the comparison benchmarks locally.

Results (AMD EPYC 7763, .NET 10.0.2, ShortRunJob, Constrained Sweden ~2 600 pts)

Constrained CDT:

Library Mean Ratio Allocated
CDT.NET 1,759 μs 1.00 495 KB
artem-ogre/CDT (C++) 1,961 μs 1.11
Spade (Rust) 1,989 μs 1.13
CGAL (C++) 3,659 μs 2.08
Triangle.NET 6,405 μs 3.64 2,817 KB
NTS (Conforming) 54,282 μs 30.86 59,937 KB

Vertices-only (Delaunay):

Library Mean Ratio Allocated
CDT.NET 1,584 μs 1.00 322 KB
artem-ogre/CDT (C++) 1,586 μs 1.00
Spade (Rust) 1,613 μs 1.02
CGAL (C++) 3,254 μs 2.05
Triangle.NET 2,134 μs 1.35 1,760 KB
NTS 7,960 μs 5.03 4,373 KB

CDT.NET is within ≤13% of the original C++ implementation and Spade on constrained triangulation, ~2× faster than CGAL, and allocates 5–120× less memory than Triangle.NET and NTS.

Notes

  • Native wrappers require cmake + C++17 compiler and cargo at build time; internet access needed for FetchContent / crates.io on first build. See README for full prerequisites per platform.
  • On Windows: Visual Studio 2022 with the "Desktop development with C++" workload + Rust stable (MSVC ABI). A Developer Command Prompt ensures all tools are on PATH.
  • NTS conforming CDT inserts Steiner points — semantically different from true CDT.
  • Native builds excluded from git via .gitignore (native/*/build/, native/*/target/).
Original prompt

Goal

Research and add a new benchmark project that compares the performance of CDT.NET (this repo) against other C# Constrained Delaunay Triangulation (CDT) libraries available on NuGet.

Context

The repository already has a benchmark project at benchmark/CDT.Benchmarks/ using BenchmarkDotNet targeting net10.0. The main library is at src/CDT.Core/CDT.Core.csproj.

Key types in CDT.NET:

  • Triangulation<T> — main CDT class, generic over floating-point type
  • V2d<T> — 2D point/vertex
  • Edge — constraint edge (pair of vertex indices)
  • VertexInsertionOrder — enum (AsProvided, Auto)
  • Methods: InsertVertices(IReadOnlyList<V2d<T>>), InsertEdges(IReadOnlyList<Edge>), EraseSuperTriangle() / EraseOuterTriangles() / EraseOuterTrianglesAndHoles()

Existing benchmark input files are in test/CDT.Tests/inputs/*.txt (format: nVerts nEdges\n x y\n... v1 v2\n...) and are copied to the benchmark output under inputs/.

What to build

Create a new benchmark project: benchmark/CDT.Comparison.Benchmarks/

1. Research — C# CDT libraries to include

Find and include at least these well-known NuGet packages (use latest stable versions):

Library NuGet Package Notes
CDT.NET (this repo) project ref to src/CDT.Core Our implementation
Triangle.NET Triangle.NET or Triangle Classic robust triangulator, widely used in .NET
DelaunatorSharp DelaunatorSharp Port of Mapbox Delaunator, fast Delaunay (may not support constraints natively)

Search NuGet for any additional C# CDT/Delaunay libraries that support constrained edges. Include any you find that are:

  • Actively maintained (updated in last 3 years)
  • Available on NuGet
  • Support at least basic Delaunay triangulation (constraint support preferred)

2. Project file: benchmark/CDT.Comparison.Benchmarks/CDT.Comparison.Benchmarks.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>
  <!-- CDT.NET reference -->
  <ItemGroup>
    <ProjectReference Include="..\..\src\CDT.Core\CDT.Core.csproj" />
  </ItemGroup>
  <!-- Competing libraries -->
  <ItemGroup>
    <!-- Add NuGet packages here -->
    <PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
    <!-- ... -->
  </ItemGroup>
  <!-- Copy test inputs -->
  <ItemGroup>
    <Content Include="..\..\test\CDT.Tests\inputs\*.txt"
             CopyToOutputDirectory="PreserveNewest"
             Link="inputs\%(Filename)%(Extension)" />
  </ItemGroup>
</Project>

3. Benchmark classes

Create benchmark/CDT.Comparison.Benchmarks/ComparisonBenchmarks.cs with:

  • A shared input reader similar to the existing BenchmarkInputReader that loads the same .txt files
  • One benchmark class per dataset (at minimum: Constrained Sweden.txt)
  • For each library, implement benchmark methods for:
    • Vertices only (no constraint edges) — triangulate N random/real points
    • Constrained (vertices + edges) — full CDT with constraint edges
  • Use [BenchmarkCategory] to group by operation type
  • Use [MemoryDiagnoser] to capture allocations
  • Use [ShortRunJob] for quick runs

For libraries that don't support constrained edges natively, only include a "vertices only" benchmark and add a comment explaining why.

4. Adapter pattern

For each competing library, create a thin adapter class/method that:

  • Accepts the same List<(double X, double Y)> points and List<(int V1, int V2)> edges
  • Calls the library's API to perform triangulation
  • Returns a result object (or just int triangle count to prevent dead-code elimination)

This keeps the benchmark methods clean.

5. Program entry point

benchmark/CDT.Comparison.Benchmarks/Program.cs:

using BenchmarkDotNet.Running;
BenchmarkSwitcher.FromAssembly(typeof(ComparisonBenchmarks).Assembly).Run(args);

6. Add to solution

Update CDT.NET.slnx to include the new project.

7. README section (optional but nice)

Add a benchmark/CDT.Comparison.Benchmarks/README.md explaining:

  • Which libraries are being compared
  • How to run the benchmarks
  • Any known limitations per library

Constraints

  • Target net10.0
  • Use BenchmarkDotNet version 0.15.8 (same as existing benchmark project)
  • All benchmark methods must return a value (not void) to prevent JIT dead-code elimination
  • Existing benchmark project at benchmark/CDT.Benchmarks/ must not be modified
  • Follow the same code style as the existing codebase (C# nullable enabled, implicit usings)

This pull request was created from Copilot chat.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 10 commits February 23, 2026 01:03
Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
Copilot AI changed the title [WIP] Research and add new benchmark for CDT.NET performance Add CDT.Comparison.Benchmarks — 6-library CDT/Delaunay performance comparison Feb 23, 2026
Copilot AI requested a review from MichaConrad February 23, 2026 01:33
@MichaConrad
Copy link
Collaborator

@copilot

  • Setup anything needed to run this on Windows, too!
  • Make sure the CDT.Comparison.Benchmarks build works on both platforms without me having to change anything.
  • Make sure the CDT.Comparison.Benchmarks build throws a nice error message when dependencies are missing
  • Make sure I can just run the benchmark on both platforms all wrappers work.
  • Update the readme and add any instructions for Windows if needed

…-dep error messages

Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 23, 2026

@copilot

  • Setup anything needed to run this on Windows, too!
    ...

Done in commit 9d6d4ae. Here's what changed:

Cross-platform native build (csproj)

  • Auto-detects OS at build time and computes the correct library filename: cdt_wrapper.dll / spade_wrapper.dll (Windows), libcdt_wrapper.dylib / libspade_wrapper.dylib (macOS), libcdt_wrapper.so / libspade_wrapper.so (Linux) — no manual changes needed.

CMakeLists.txt (C++ wrapper)

  • Added CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON so the extern "C" entry points are visible to P/Invoke on Windows without __declspec(dllexport).
  • Added RUNTIME_OUTPUT_DIRECTORY / RUNTIME_OUTPUT_DIRECTORY_RELEASE pointing to the build root, so the DLL lands in build/ on all platforms — MSVC otherwise puts it in build/Release/ which broke the MSBuild copy step.

Missing dependency error messages

  • dotnet build now checks for cmake and cargo before trying to compile the wrappers and prints a clear error with the install URL if either is missing.

README — Windows prerequisites

  • Full prerequisites table: Visual Studio 2022 with the "Desktop development with C++" workload, CMake (bundled with VS or standalone), Git for Windows, Rust stable (MSVC ABI via rustup).
  • Developer Command Prompt tip so cmake, cl, and cargo are all on PATH.
  • Removed the outdated "Linux only" limitation note.

Copilot AI changed the title Add CDT.Comparison.Benchmarks — 6-library CDT/Delaunay performance comparison Add CDT.Comparison.Benchmarks — 6-library CDT/Delaunay performance comparison (Windows + Linux) Feb 23, 2026
@MichaConrad
Copy link
Collaborator

@copilot
Great job

now, when I run and compare the returned values from all libs

var t = new ComparisonBenchmarks();
t.Setup();

var m = t.VO_CdtNet();
m = t.VO_NativeCdt();
m = t.VO_Nts();
m = t.VO_Poly2Tri();
m = t.VO_Spade();
m = t.VO_TriangleNet();

All return around 5000, except Poly2Tri which returns only 130. How can that be? Is that a bug in our code or is it just doing something fundamentally different?

Copilot AI and others added 2 commits February 23, 2026 07:31
…e count

Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 23, 2026

@copilot
Great job

...

Not a bug in our adapter — it's a known limitation of Poly2Tri's algorithm.

Root cause: Poly2Tri's sweep-line internally uses precision thresholds relative to the bounding box. The Sweden dataset has geographic coordinates (bbox ≈13×14 degrees, minimum inter-point separation ≈0.006 degrees → ratio ≈0.00046). Many points fall below Poly2Tri's internal epsilon for that coordinate range and are silently skipped. Running the dataset through ConstrainedPointSet outside the benchmark confirms it: 2 619 vertices in → 130 triangles out. Scaling the same coordinates by 100× gives the expected ~5 200 triangles.

This is a well-known Poly2Tri constraint: the algorithm was designed for integer-like or small normalised coordinates, not arbitrary floating-point geographic data. All other libraries in the benchmark (CDT.NET, Triangle.NET, NTS, artem-ogre/CDT, Spade) handle geographic-scale coordinates correctly.

I've added a detailed ⚠ IMPORTANT comment in the Poly2TriAdapter class (commit 899c222) and updated the README's Known Limitations table to explain the discrepancy.

@MichaConrad
Copy link
Collaborator

@copilot

  • Remove Poly2Tri entirely from benchmarks, code and readme

Lets add another library instead

  • CGAL: https://github.com/cgal/cgal this is a very large and comprehensive library with tons of stuff, but it is header-only now so it should be pretty easy and fast to create just a wrapper right?

Copilot AI and others added 5 commits February 23, 2026 08:09
…_package)

Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
…orkflow

Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
…direct_traits.hpp)

Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
…tforms)

Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
…both green

Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 23, 2026

@copilot

  • Remove Poly2Tri entirely from benchmarks, code and readme

...

Done in commit 6e9f891. Poly2Tri is fully removed (NuGet package, adapter, all benchmark methods, README). CGAL is added as a replacement:

  • CGAL 6.1.1 via the *-library.zip release asset (10 MB, header-only)
  • Boost fetched as 38 individual boostorg/<lib> ZIPs (~10 MB total download) — no manual install needed on any platform
  • Cross-platform build verified on windows-latest ✅ (cgal_wrapper.dll produced) and ubuntu-latest ✅ (libcgal_wrapper.so produced) via a dedicated CI workflow that was then removed after confirmation.

@MichaConrad
Copy link
Collaborator

@copilot Run the benchmarks and update the results and conclusions

Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 23, 2026

@copilot Run the benchmarks and update the results and conclusions

Results updated in commit 84eac58. Here's the full picture with CGAL now included:

Constrained CDT (Constrained Sweden, ~2 600 pts + edges, AMD EPYC 7763, .NET 10.0.2):

Library Mean Ratio Allocated
CDT.NET (baseline) 1,759 μs 1.00 495 KB
artem-ogre/CDT (C++) 1,961 μs 1.11
Spade (Rust) 1,989 μs 1.13
CGAL (C++) 3,659 μs 2.08
Triangle.NET 6,405 μs 3.64 2,817 KB
NTS (Conforming CDT) 54,282 μs 30.86 59,937 KB

Vertices-only (Delaunay):

Library Mean Ratio Allocated
CDT.NET (baseline) 1,584 μs 1.00 322 KB
artem-ogre/CDT (C++) 1,586 μs 1.00
Spade (Rust) 1,613 μs 1.02
CGAL (C++) 3,254 μs 2.05
Triangle.NET 2,134 μs 1.35 1,760 KB
NTS 7,960 μs 5.03 4,373 KB

CDT.NET is ~2× faster than CGAL on both categories. CGAL uses a half-edge DCEL data structure with more bookkeeping overhead; CDT.NET's compact flat-array design wins on throughput.

@MichaConrad
Copy link
Collaborator

@copilot can you change the Comparison csproj build the native things only once until we delete the binaries with dotnet clean ?
I don't want to wait for that build to run every time I make a small change in C# code.

…ppers

Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 23, 2026

@copilot can you change the Comparison csproj build the native things only once until we delete the binaries with dotnet clean ?
I don't want to wait for that build to run every time I make a small ...

Done in commit d53571b.

Root cause: $(OutputPath) is empty during PropertyGroup evaluation in SDK-style projects, so pre-computed path properties resolved to just the filename (no directory), making !Exists(...) always true.

Fix: The path expression is now inlined directly in each Target's Condition attribute, which is evaluated at execution time when $(OutputPath) is fully resolved:

Condition="!Exists('$(OutputPath)$(_NativeLibPrefix)cdt_wrapper$(_NativeLibExt)')"

A CleanNativeWrappers target (AfterTargets="Clean") now explicitly deletes the three native libs from the output dir so dotnet clean triggers a re-copy on the next build.

Verified on Linux:

  • ✅ C# change + dotnet build → no cmake/cargo invoked at all (~2.3 s)
  • dotnet clean → deletes libcdt_wrapper.so, libcgal_wrapper.so, libspade_wrapper.so
  • dotnet build after clean → cmake/cargo run from cache (~5 s), libs restored

@MichaConrad MichaConrad marked this pull request as ready for review February 23, 2026 09:49
@MichaConrad
Copy link
Collaborator

@copilot exclude CDT.Comparison.Benchmarks from the normal CI/CD build pipeline. Takes too long and we don't actually need it for running tests or generating the nuget

Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 23, 2026

@copilot exclude CDT.Comparison.Benchmarks from the normal CI/CD build pipeline. Takes too long and we don't actually need it for running tests or generating the nuget

Done in commit 65a7073. Both the build-and-test and publish jobs now build named projects explicitly instead of the full solution, so CDT.Comparison.Benchmarks (and its cmake/cargo build targets) is never touched by CI.

Changes made:

  • Windows restore/build — now lists projects explicitly: CDT.Core, CDT.Tests, CDT.Benchmarks, CDT.Viz (mirrors what Linux already did)
  • Windows test — now passes test/CDT.Tests/CDT.Tests.csproj directly (was scanning the full solution, risking discovery of the comparison project)
  • Publish job restore/build — now targets src/CDT.Core/CDT.Core.csproj only (the only project needed for dotnet pack)

All existing behaviour — test coverage, PR comment, NuGet publish — is preserved.

@github-actions
Copy link

Summary

Summary
Generated on: 2/23/2026 - 10:02:43 AM
Coverage date: 2/23/2026 - 10:02:40 AM
Parser: Cobertura
Assemblies: 1
Classes: 17
Files: 7
Line coverage: 85% (1325 of 1558)
Covered lines: 1325
Uncovered lines: 233
Coverable lines: 1558
Total lines: 3859
Branch coverage: 79.1% (627 of 792)
Covered branches: 627
Total branches: 792
Method coverage: Feature is only available for sponsors
Tag: 87_22301085583

Coverage

CDT.Core - 85%
Name Line Branch
CDT.Core 85% 79.1%
CDT.Box2d`1 73.6% 100%
CDT.CdtUtils 83.1% 72.8%
CDT.CovariantReadOnlyDictionary`3 18.1% 50%
CDT.DictionaryExtensions 100% 100%
CDT.DuplicatesInfo 100%
CDT.DuplicateVertexException 66.6%
CDT.Edge 75% 66.6%
CDT.IntersectingConstraintsException 0%
CDT.KdTree`1 72.7% 65%
CDT.LayerDepth 0%
CDT.Predicates.PredicatesAdaptive 92.6% 83.3%
CDT.Predicates.PredicatesExact 35.3% 100%
CDT.TopologyVerifier 77.2% 63.8%
CDT.Triangle 66.6% 71.4%
CDT.Triangulation`1 93.8% 84.1%
CDT.TriangulationException 100%
CDT.V2d`1 44.4% 25%

@MichaConrad MichaConrad merged commit 3f78ea1 into main Feb 23, 2026
3 checks passed
@MichaConrad MichaConrad deleted the copilot/add-cdt-benchmark-project branch February 23, 2026 10:22
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