Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
(e.g., boost_math .NET examples) get picked up as EmbeddedResource
items and cause MSB3822 build errors. -->
<PropertyGroup>
<DefaultItemExcludes>$(DefaultItemExcludes);native\**</DefaultItemExcludes>
<DefaultItemExcludes>$(DefaultItemExcludes);native\**\*.res;native\**\build\**;native\**\target\**</DefaultItemExcludes>
</PropertyGroup>

<!-- CDT.NET (baseline) -->
Expand Down
140 changes: 113 additions & 27 deletions benchmark/CDT.Comparison.Benchmarks/ComparisonBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using CDT;
Expand All @@ -16,7 +17,6 @@
using TnPolygon = TriangleNet.Geometry.Polygon;
using TnSegment = TriangleNet.Geometry.Segment;
using TnVertex = TriangleNet.Geometry.Vertex;
using System.Runtime.InteropServices;

// ---------------------------------------------------------------------------
// Shared input reader
Expand Down Expand Up @@ -69,7 +69,7 @@ public static int VerticesOnly(double[] xs, double[] ys)
for (int i = 0; i < xs.Length; i++)
verts.Add(new V2d<double>(xs[i], ys[i]));

var cdt = new Triangulation<double>(VertexInsertionOrder.Auto);
var cdt = new Triangulation<double>(VertexInsertionOrder.Auto, IntersectingConstraintEdges.TryResolve, 0.0);
cdt.InsertVertices(verts);
return cdt.Triangles.Length;
}
Expand All @@ -84,11 +84,27 @@ public static int Constrained(double[] xs, double[] ys, int[] ev1, int[] ev2)
for (int i = 0; i < ev1.Length; i++)
edges.Add(new CdtEdge(ev1[i], ev2[i]));

var cdt = new Triangulation<double>(VertexInsertionOrder.Auto);
var cdt = new Triangulation<double>(VertexInsertionOrder.Auto, IntersectingConstraintEdges.TryResolve, 0.0);
cdt.InsertVertices(verts);
cdt.InsertEdges(edges);
return cdt.Triangles.Length;
}

public static int Conforming(double[] xs, double[] ys, int[] ev1, int[] ev2)
{
var verts = new List<V2d<double>>(xs.Length);
for (int i = 0; i < xs.Length; i++)
verts.Add(new V2d<double>(xs[i], ys[i]));

var edges = new List<CdtEdge>(ev1.Length);
for (int i = 0; i < ev1.Length; i++)
edges.Add(new CdtEdge(ev1[i], ev2[i]));

var cdt = new Triangulation<double>(VertexInsertionOrder.Auto, IntersectingConstraintEdges.TryResolve, 0.0);
cdt.InsertVertices(verts);
cdt.ConformToEdges(edges);
return cdt.Triangles.Length;
}
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -118,7 +134,32 @@ public static int Constrained(double[] xs, double[] ys, int[] ev1, int[] ev2)
for (int i = 0; i < ev1.Length; i++)
polygon.Add(new TnSegment(verts[ev1[i]], verts[ev2[i]]));

return new TnMesher().Triangulate(polygon).Triangles.Count;
return new TnMesher().Triangulate(polygon, new TriangleNet.Meshing.ConstraintOptions()
{
ConformingDelaunay = false,
SegmentSplitting = 2,
Convex = true
}).Triangles.Count;
}

public static int Conforming(double[] xs, double[] ys, int[] ev1, int[] ev2)
{
var polygon = new TnPolygon(xs.Length);
var verts = new TnVertex[xs.Length];
for (int i = 0; i < xs.Length; i++)
{
verts[i] = new TnVertex(xs[i], ys[i]);
polygon.Add(verts[i]);
}
for (int i = 0; i < ev1.Length; i++)
polygon.Add(new TnSegment(verts[ev1[i]], verts[ev2[i]]));

return new TnMesher().Triangulate(polygon, new TriangleNet.Meshing.ConstraintOptions()
{
ConformingDelaunay = true,
SegmentSplitting = 2,
Convex = true
}).Triangles.Count;
}
}

Expand All @@ -139,6 +180,7 @@ public static int VerticesOnly(double[] xs, double[] ys)
coords[i] = new NtsCoordinate(xs[i], ys[i]);

var builder = new NetTopologySuite.Triangulate.DelaunayTriangulationBuilder();
builder.Tolerance = 0.0;
builder.SetSites(coords);
return builder.GetTriangles(Gf).NumGeometries;
}
Expand All @@ -158,6 +200,7 @@ public static int Conforming(double[] xs, double[] ys, int[] ev1, int[] ev2)
});

var builder = new NetTopologySuite.Triangulate.ConformingDelaunayTriangulationBuilder();
builder.Tolerance = 0.0;
builder.SetSites(new NtsMultiPoint(pts));
builder.Constraints = new NtsMultiLineString(segments);
return builder.GetTriangles(Gf).NumGeometries;
Expand All @@ -178,13 +221,20 @@ private static partial int Triangulate(
double[] xs, double[] ys, int nVerts,
int[] ev1, int[] ev2, int nEdges);

[LibraryImport(Lib, EntryPoint = "cdt_conform_d")]
private static partial int Conform(
double[] xs, double[] ys, int nVerts,
int[] ev1, int[] ev2, int nEdges);

public static int VerticesOnly(double[] xs, double[] ys) =>
Triangulate(xs, ys, xs.Length, [], [], 0);

public static int Constrained(double[] xs, double[] ys, int[] ev1, int[] ev2) =>
Triangulate(xs, ys, xs.Length, ev1, ev2, ev1.Length);
}

public static int Conforming(double[] xs, double[] ys, int[] ev1, int[] ev2) =>
Conform(xs, ys, xs.Length, ev1, ev2, ev1.Length);
}

// ---------------------------------------------------------------------------
// Adapter — Spade (Rust via P/Invoke, spade 2.15.0)
Expand All @@ -201,13 +251,20 @@ private static partial int SpadeTriangulate(
double[] xs, double[] ys, int nVerts,
int[] ev1, int[] ev2, int nEdges);

[LibraryImport(Lib, EntryPoint = "spade_conform")]
private static partial int SpadeConform(
double[] xs, double[] ys, int nVerts,
int[] ev1, int[] ev2, int nEdges);

public static int VerticesOnly(double[] xs, double[] ys) =>
SpadeTriangulate(xs, ys, xs.Length, [], [], 0);

public static int Constrained(double[] xs, double[] ys, int[] ev1, int[] ev2) =>
SpadeTriangulate(xs, ys, xs.Length, ev1, ev2, ev1.Length);
}

public static int Conforming(double[] xs, double[] ys, int[] ev1, int[] ev2) =>
SpadeConform(xs, ys, xs.Length, ev1, ev2, ev1.Length);
}

// ---------------------------------------------------------------------------
// Adapter — CGAL (C++ via P/Invoke, CGAL 5.x/6.x)
Expand All @@ -227,13 +284,20 @@ private static partial int CgalTriangulate(
double[] xs, double[] ys, int nVerts,
int[] ev1, int[] ev2, int nEdges);

[LibraryImport(Lib, EntryPoint = "cgal_conform")]
private static partial int CgalConform(
double[] xs, double[] ys, int nVerts,
int[] ev1, int[] ev2, int nEdges);

public static int VerticesOnly(double[] xs, double[] ys) =>
CgalTriangulate(xs, ys, xs.Length, [], [], 0);

public static int Constrained(double[] xs, double[] ys, int[] ev1, int[] ev2) =>
CgalTriangulate(xs, ys, xs.Length, ev1, ev2, ev1.Length);
}

public static int Conforming(double[] xs, double[] ys, int[] ev1, int[] ev2) =>
CgalConform(xs, ys, xs.Length, ev1, ev2, ev1.Length);
}

// (~2 600 vertices, ~2 600 constraint edges)
// ---------------------------------------------------------------------------
Expand All @@ -258,49 +322,71 @@ public void Setup() =>
[BenchmarkCategory("VerticesOnly")]
public int VO_CdtNet() => CdtNetAdapter.VerticesOnly(_xs, _ys);

[Benchmark(Description = "Triangle.NET")]
[BenchmarkCategory("VerticesOnly")]
public int VO_TriangleNet() => TriangleNetAdapter.VerticesOnly(_xs, _ys);

[Benchmark(Description = "NTS")]
[BenchmarkCategory("VerticesOnly")]
public int VO_Nts() => NtsAdapter.VerticesOnly(_xs, _ys);

[Benchmark(Description = "artem-ogre/CDT (C++)")]
[BenchmarkCategory("VerticesOnly")]
public int VO_NativeCdt() => NativeCdtAdapter.VerticesOnly(_xs, _ys);

[Benchmark(Description = "Spade (Rust)")]
[BenchmarkCategory("VerticesOnly")]
public int VO_Spade() => SpadeAdapter.VerticesOnly(_xs, _ys);

[Benchmark(Description = "CGAL (C++)")]
[BenchmarkCategory("VerticesOnly")]
public int VO_Cgal() => CgalAdapter.VerticesOnly(_xs, _ys);

[Benchmark(Description = "Spade (Rust)")]
[Benchmark(Description = "NTS")]
[BenchmarkCategory("VerticesOnly")]
public int VO_Spade() => SpadeAdapter.VerticesOnly(_xs, _ys);
public int VO_Nts() => NtsAdapter.VerticesOnly(_xs, _ys);

[Benchmark(Description = "Triangle.NET")]
[BenchmarkCategory("VerticesOnly")]
public int VO_TriangleNet() => TriangleNetAdapter.VerticesOnly(_xs, _ys);

// -- Constrained ---------------------------------------------------------

[Benchmark(Baseline = true, Description = "CDT.NET")]
[BenchmarkCategory("Constrained")]
public int CDT_CdtNet() => CdtNetAdapter.Constrained(_xs, _ys, _ev1, _ev2);

[Benchmark(Description = "Triangle.NET")]
[BenchmarkCategory("Constrained")]
public int CDT_TriangleNet() => TriangleNetAdapter.Constrained(_xs, _ys, _ev1, _ev2);

[Benchmark(Description = "NTS (Conforming CDT)")]
[BenchmarkCategory("Constrained")]
public int CDT_Nts() => NtsAdapter.Conforming(_xs, _ys, _ev1, _ev2);

[Benchmark(Description = "artem-ogre/CDT (C++)")]
[BenchmarkCategory("Constrained")]
public int CDT_NativeCdt() => NativeCdtAdapter.Constrained(_xs, _ys, _ev1, _ev2);

[Benchmark(Description = "Spade (Rust)")]
[BenchmarkCategory("Constrained")]
public int CDT_Spade() => SpadeAdapter.Constrained(_xs, _ys, _ev1, _ev2);

[Benchmark(Description = "CGAL (C++)")]
[BenchmarkCategory("Constrained")]
public int CDT_Cgal() => CgalAdapter.Constrained(_xs, _ys, _ev1, _ev2);

[Benchmark(Description = "Spade (Rust)")]
[Benchmark(Description = "Triangle.NET")]
[BenchmarkCategory("Constrained")]
public int CDT_Spade() => SpadeAdapter.Constrained(_xs, _ys, _ev1, _ev2);
public int CDT_TriangleNet() => TriangleNetAdapter.Constrained(_xs, _ys, _ev1, _ev2);

// - Conforming ----------------------------------------------------------

[Benchmark(Baseline = true, Description = "CDT.NET")]
[BenchmarkCategory("Conforming")]
public int CfDT_CdtNet() => CdtNetAdapter.Conforming(_xs, _ys, _ev1, _ev2);

[Benchmark(Description = "artem-ogre/CDT (C++)")]
[BenchmarkCategory("Conforming")]
public int CfDT_NativeCdt() => NativeCdtAdapter.Conforming(_xs, _ys, _ev1, _ev2);

[Benchmark(Description = "Spade (Rust)")]
[BenchmarkCategory("Conforming")]
public int CfDT_Spade() => SpadeAdapter.Conforming(_xs, _ys, _ev1, _ev2);

[Benchmark(Description = "CGAL (C++)")]
[BenchmarkCategory("Conforming")]
public int CfDT_Cgal() => CgalAdapter.Conforming(_xs, _ys, _ev1, _ev2);

[Benchmark(Description = "NTS")]
[BenchmarkCategory("Conforming")]
public int CfDT_Nts() => NtsAdapter.Conforming(_xs, _ys, _ev1, _ev2);

[Benchmark(Description = "Triangle.NET")]
[BenchmarkCategory("Conforming")]
public int CfDT_TriangleNet() => TriangleNetAdapter.Conforming(_xs, _ys, _ev1, _ev2);
}
26 changes: 26 additions & 0 deletions benchmark/CDT.Comparison.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,30 @@

using BenchmarkDotNet.Running;

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

static void Print(string label, Func<int> compute) =>
Console.WriteLine($" {label,-22} {compute(),6:N0} triangles");

const int lineWidth = 38;
Console.WriteLine("Constrained Delaunay Triangulation");
Console.WriteLine(new string('-', lineWidth));
Print("CDT.NET", bench.CDT_CdtNet);
Print("artem-ogre/CDT (C++)", bench.CDT_NativeCdt);
Print("Spade (Rust)", bench.CDT_Spade);
Print("CGAL (C++)", bench.CDT_Cgal);
Print("Triangle.NET", bench.CDT_TriangleNet);
Console.WriteLine(new string('-', lineWidth));

Console.WriteLine("Conforming Delaunay Triangulation");
Console.WriteLine(new string('-', lineWidth));
Print("CDT.NET", bench.CfDT_CdtNet);
Print("artem-ogre/CDT (C++)", bench.CfDT_NativeCdt);
Print("Spade (Rust)", bench.CfDT_Spade);
Print("CGAL (C++)", bench.CfDT_Cgal);
Print("NTS", bench.CfDT_Nts);
Print("Triangle.NET", bench.CfDT_TriangleNet);
Console.WriteLine(new string('-', lineWidth));

BenchmarkSwitcher.FromAssembly(typeof(ComparisonBenchmarks).Assembly).Run(args);
61 changes: 44 additions & 17 deletions benchmark/CDT.Comparison.Benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,30 +88,57 @@ Results are written to `BenchmarkDotNet.Artifacts/` in the current directory.
| CGAL (C++) | `number_of_faces()` counts all finite triangles in the triangulation, consistent with artem-ogre/CDT. First build downloads CGAL 6.1.1 library headers (~10 MB) and the required Boost sub-library headers (~10 MB ZIPs, ~60 MB staged); subsequent builds use the cmake cache. |
| Spade (Rust) | `num_inner_faces()` returns only inner (non-convex-hull) triangles, which is fewer than the C++ CDT counts |

## Counts of triangles by library and category

```
Constrained Delaunay Triangulation
--------------------------------------
CDT.NET 5.239 triangles
artem-ogre/CDT (C++) 5.239 triangles
Spade (Rust) 5.204 triangles
CGAL (C++) 5.204 triangles
Triangle.NET 5.204 triangles
--------------------------------------
Conforming Delaunay Triangulation
--------------------------------------
CDT.NET 5.343 triangles
artem-ogre/CDT (C++) 5.343 triangles
Spade (Rust) 5.308 triangles
CGAL (C++) 5.324 triangles
NTS 5.764 triangles
Triangle.NET 5.204 triangles
--------------------------------------
```

## Benchmark results

> 12th Gen Intel Core i7-12700KF 3.60GHz, 1 CPU, 20 logical and 12 physical cores

| Method | Categories | Mean | Error | StdDev | Ratio | RatioSD |
|----------------------- |------------- |----------:|----------:|----------:|------:|--------:|
| **CDT.NET** | Constrained | 1.198 ms | 0.1065 ms | 0.0058 ms | 1.00 | 0.01 |
| Triangle.NET | Constrained | 4.571 ms | 2.2827 ms | 0.1251 ms | 3.82 | 0.09 |
| 'NTS (Conforming CDT)' | Constrained | 37.066 ms | 8.5571 ms | 0.4690 ms | 30.95 | 0.36 |
| 'artem-ogre/CDT (C++)' | Constrained | 1.788 ms | 0.1284 ms | 0.0070 ms | 1.49 | 0.01 |
| 'CGAL (C++)' | Constrained | 2.538 ms | 0.0574 ms | 0.0031 ms | 2.12 | 0.01 |
| 'Spade (Rust)' | Constrained | 1.255 ms | 0.1050 ms | 0.0058 ms | 1.05 | 0.01 |
| | | | | | | |
| **CDT.NET** | VerticesOnly | 1.048 ms | 0.0371 ms | 0.0020 ms | 1.00 | 0.00 |
| Triangle.NET | VerticesOnly | 1.323 ms | 0.1856 ms | 0.0102 ms | 1.26 | 0.01 |
| NTS | VerticesOnly | 5.519 ms | 2.8885 ms | 0.1583 ms | 5.26 | 0.13 |
| 'CGAL (C++)' | VerticesOnly | 2.063 ms | 0.2154 ms | 0.0118 ms | 1.97 | 0.01 |
| 'artem-ogre/CDT (C++)' | VerticesOnly | 1.557 ms | 0.1013 ms | 0.0056 ms | 1.49 | 0.01 |
| 'Spade (Rust)' | VerticesOnly | 1.028 ms | 0.0803 ms | 0.0044 ms | 0.98 | 0.00 |

| Method | Categories | Mean | Error | StdDev | Ratio |
|----------------------- |------------- |----------:|-----------:|----------:|------:|
| CDT.NET | Conforming | 1.442 ms | 0.1628 ms | 0.0089 ms | 1.00 |
| 'artem-ogre/CDT (C++)' | Conforming | 1.976 ms | 0.0501 ms | 0.0027 ms | 1.37 |
| 'Spade (Rust)' | Conforming | 1.341 ms | 0.2933 ms | 0.0161 ms | 0.93 |
| 'CGAL (C++)' | Conforming | 4.110 ms | 0.3934 ms | 0.0216 ms | 2.85 |
| NTS | Conforming | 38.288 ms | 39.8335 ms | 2.1834 ms | 26.55 |
| Triangle.NET | Conforming | 3.284 ms | 0.6901 ms | 0.0378 ms | 2.28 |
| | | | | | |
| CDT.NET | Constrained | 1.167 ms | 0.0737 ms | 0.0040 ms | 1.00 |
| 'artem-ogre/CDT (C++)' | Constrained | 1.766 ms | 0.0619 ms | 0.0034 ms | 1.51 |
| 'Spade (Rust)' | Constrained | 1.256 ms | 0.1233 ms | 0.0068 ms | 1.08 |
| 'CGAL (C++)' | Constrained | 2.613 ms | 0.3773 ms | 0.0207 ms | 2.24 |
| Triangle.NET | Constrained | 3.290 ms | 1.3341 ms | 0.0731 ms | 2.82 |
| | | | | | |
| CDT.NET | VerticesOnly | 1.072 ms | 0.0045 ms | 0.0002 ms | 1.00 |
| 'artem-ogre/CDT (C++)' | VerticesOnly | 1.568 ms | 0.2550 ms | 0.0140 ms | 1.46 |
| 'Spade (Rust)' | VerticesOnly | 1.038 ms | 0.0224 ms | 0.0012 ms | 0.97 |
| 'CGAL (C++)' | VerticesOnly | 2.156 ms | 0.2064 ms | 0.0113 ms | 2.01 |
| NTS | VerticesOnly | 5.608 ms | 2.5000 ms | 0.1370 ms | 5.23 |
| Triangle.NET | VerticesOnly | 1.355 ms | 0.0418 ms | 0.0023 ms | 1.26 |

### Key takeaways

- **CDT.NET matches the original C++ implementation (artem-ogre/CDT) and Spade within ≤13%** on both constrained and unconstrained triangulation.
- **CDT.NET matches the original C++ implementation (artem-ogre/CDT) and Spade within ≤13%**.
- **CGAL** runs at ~2× CDT.NET. CGAL's `Constrained_Delaunay_triangulation_2` uses a more complex data structure (half-edge DCEL) with additional bookkeeping overhead vs. CDT.NET's compact flat arrays. For raw triangulation throughput CDT.NET is faster.
- **CDT.NET allocates 5–120× less managed memory** than Triangle.NET and NTS: Triangle.NET allocates ~5.7× more, NTS ~121× more.
- **NTS (conforming CDT)** is ~30× slower and allocates ~120× more memory — Steiner-point insertion is the main cost, and the result is semantically different (not true CDT).
Expand Down
Loading