From 1506e17966c471a2f5bf8098b310abd9b3ee099e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 17:44:24 +0000 Subject: [PATCH 1/6] Initial plan From 278959a2abb4380c5a2f23ab72cdd395e3ed7bbf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 17:53:22 +0000 Subject: [PATCH 2/6] Implement all 5 performance optimization candidates (A-E) Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com> --- benchmark/CDT.Benchmarks/Benchmarks.cs | 3 + src/CDT.Core/KdTree.cs | 8 ++- src/CDT.Core/Triangulation.cs | 82 +++++++++++++++++--------- src/CDT.Core/Types.cs | 10 +++- 4 files changed, 72 insertions(+), 31 deletions(-) diff --git a/benchmark/CDT.Benchmarks/Benchmarks.cs b/benchmark/CDT.Benchmarks/Benchmarks.cs index 0cf35b1..d32de06 100644 --- a/benchmark/CDT.Benchmarks/Benchmarks.cs +++ b/benchmark/CDT.Benchmarks/Benchmarks.cs @@ -4,6 +4,7 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Running; using CDT; @@ -59,6 +60,7 @@ public static (List> Vertices, List Edges) Read(string fileNam /// ~2 600 constraint edges) – the same dataset used in the C++ CDT benchmarks. /// [MemoryDiagnoser] +[EventPipeProfiler(EventPipeProfile.CpuSampling)] [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] [CategoriesColumn] [ShortRunJob] @@ -151,6 +153,7 @@ public Triangulation FullPipeline_Auto() /// to measure per-vertex overhead without the noise of large datasets. /// [MemoryDiagnoser] +[EventPipeProfiler(EventPipeProfile.CpuSampling)] [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] [CategoriesColumn] [ShortRunJob] diff --git a/src/CDT.Core/KdTree.cs b/src/CDT.Core/KdTree.cs index e2c85cd..a124554 100644 --- a/src/CDT.Core/KdTree.cs +++ b/src/CDT.Core/KdTree.cs @@ -55,12 +55,14 @@ public NearestTask(int node, T minX, T minY, T maxX, T maxY, SplitDir dir, T dis private T _minX, _minY, _maxX, _maxY; private bool _boxInitialized; private int _size; + private readonly T _two; private NearestTask[] _stack = new NearestTask[InitialStackDepth]; /// Initializes an empty KD-tree with no bounding box pre-set. public KdTree() { + _two = T.One + T.One; _minX = T.MinValue; _minY = T.MinValue; _maxX = T.MaxValue; _maxY = T.MaxValue; _root = AddNewNode(); @@ -70,6 +72,7 @@ public KdTree() /// Initializes an empty KD-tree with a known bounding box. public KdTree(T minX, T minY, T maxX, T maxY) { + _two = T.One + T.One; _minX = minX; _minY = minY; _maxX = maxX; _maxY = maxY; _root = AddNewNode(); _boxInitialized = true; @@ -226,10 +229,9 @@ private static bool IsInsideBox(T px, T py, T minX, T minY, T maxX, T maxY) => px >= minX && px <= maxX && py >= minY && py <= maxY; [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static T GetMid(T minX, T minY, T maxX, T maxY, SplitDir dir) + private T GetMid(T minX, T minY, T maxX, T maxY, SplitDir dir) { - T two = T.One + T.One; - return dir == SplitDir.X ? (minX + maxX) / two : (minY + maxY) / two; + return dir == SplitDir.X ? (minX + maxX) / _two : (minY + maxY) / _two; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/CDT.Core/Triangulation.cs b/src/CDT.Core/Triangulation.cs index 81b4a0b..f5dba3b 100644 --- a/src/CDT.Core/Triangulation.cs +++ b/src/CDT.Core/Triangulation.cs @@ -203,6 +203,10 @@ public void InsertEdges(IReadOnlyList edges) { var remaining = new List(4); var tppTasks = new List(8); + var polyL = new List(8); + var polyR = new List(8); + var outerTris = new Dictionary(); + var intersected = new List(8); foreach (var e in edges) { remaining.Clear(); @@ -211,7 +215,7 @@ public void InsertEdges(IReadOnlyList edges) { Edge edge = remaining[^1]; remaining.RemoveAt(remaining.Count - 1); - InsertEdgeIteration(edge, new Edge(e.V1 + _nTargetVerts, e.V2 + _nTargetVerts), remaining, tppTasks); + InsertEdgeIteration(edge, new Edge(e.V1 + _nTargetVerts, e.V2 + _nTargetVerts), remaining, tppTasks, polyL, polyR, outerTris, intersected); } } } @@ -355,9 +359,11 @@ private int AddTriangle(Triangle t) private void InsertVertex(int iVert, int walkStart) { var (iT, iTopo) = WalkingSearchTrianglesAt(iVert, walkStart); - var stack = iTopo == Indices.NoNeighbor - ? InsertVertexInsideTriangle(iVert, iT) - : InsertVertexOnEdge(iVert, iT, iTopo, handleFixedSplitEdge: true); + var stack = new Stack(4); + if (iTopo == Indices.NoNeighbor) + InsertVertexInsideTriangle(iVert, iT, stack); + else + InsertVertexOnEdge(iVert, iT, iTopo, handleFixedSplitEdge: true, stack); EnsureDelaunayByEdgeFlips(iVert, stack); TryAddVertexToLocator(iVert); } @@ -406,7 +412,7 @@ private void InsertVertices_KDTreeBFS(int superGeomVertCount, Box2d box) if (T.CreateChecked(boxMaxX - boxMinX) >= T.CreateChecked(boxMaxY - boxMinY)) { - NthElement(indices, lo, midPos, hi, (a, b) => _vertices[a].X.CompareTo(_vertices[b].X)); + NthElement(indices, lo, midPos, hi, new VertexXComparer(_vertices)); T split = _vertices[indices[midPos]].X; InsertVertex(indices[midPos], parent); if (lo < midPos) { queue.Enqueue((lo, midPos, boxMinX, boxMinY, split, boxMaxY, indices[midPos])); } @@ -414,7 +420,7 @@ private void InsertVertices_KDTreeBFS(int superGeomVertCount, Box2d box) } else { - NthElement(indices, lo, midPos, hi, (a, b) => _vertices[a].Y.CompareTo(_vertices[b].Y)); + NthElement(indices, lo, midPos, hi, new VertexYComparer(_vertices)); T split = _vertices[indices[midPos]].Y; InsertVertex(indices[midPos], parent); if (lo < midPos) { queue.Enqueue((lo, midPos, boxMinX, boxMinY, boxMaxX, split, indices[midPos])); } @@ -427,12 +433,27 @@ private void InsertVertices_KDTreeBFS(int superGeomVertCount, Box2d box) // nth_element — O(n) average quickselect for spatial BFS partitioning // ------------------------------------------------------------------------- + private readonly struct VertexXComparer : IComparer + { + private readonly IReadOnlyList> _vertices; + public VertexXComparer(IReadOnlyList> vertices) => _vertices = vertices; + public int Compare(int a, int b) => _vertices[a].X.CompareTo(_vertices[b].X); + } + + private readonly struct VertexYComparer : IComparer + { + private readonly IReadOnlyList> _vertices; + public VertexYComparer(IReadOnlyList> vertices) => _vertices = vertices; + public int Compare(int a, int b) => _vertices[a].Y.CompareTo(_vertices[b].Y); + } + /// /// Rearranges in [, ) /// so the element at position is the one that would be there /// after a full sort; elements before it are ≤ it and elements after are ≥ it. /// - private static void NthElement(int[] arr, int lo, int nth, int hi, Comparison cmp) + private static void NthElement(int[] arr, int lo, int nth, int hi, TComparer cmp) + where TComparer : struct, IComparer { while (lo < hi - 1) { @@ -442,7 +463,7 @@ private static void NthElement(int[] arr, int lo, int nth, int hi, Comparison InsertVertex_FlipFixedEdges(int iV) ? _kdTree.Nearest(_vertices[iV].X, _vertices[iV].Y, _vertices) : 0; var (iT, iTopo) = WalkingSearchTrianglesAt(iV, near); - var stack = iTopo == Indices.NoNeighbor - ? InsertVertexInsideTriangle(iV, iT) - : InsertVertexOnEdge(iV, iT, iTopo, handleFixedSplitEdge: false); + var stack = new Stack(4); + if (iTopo == Indices.NoNeighbor) + InsertVertexInsideTriangle(iV, iT, stack); + else + InsertVertexOnEdge(iV, iT, iTopo, handleFixedSplitEdge: false, stack); int _dbgFlipIter2 = 0; while (stack.Count > 0) @@ -588,7 +611,7 @@ private int WalkTriangles(int startVertex, V2d pos) // Insert vertex inside triangle or on edge // ------------------------------------------------------------------------- - private Stack InsertVertexInsideTriangle(int v, int iT) + private void InsertVertexInsideTriangle(int v, int iT, Stack stack) { int iNewT1 = AddTriangle(); int iNewT2 = AddTriangle(); @@ -606,14 +629,13 @@ private Stack InsertVertexInsideTriangle(int v, int iT) ChangeNeighbor(n2, iT, iNewT1); ChangeNeighbor(n3, iT, iNewT2); - var stack = new Stack(3); + stack.Clear(); stack.Push(iT); stack.Push(iNewT1); stack.Push(iNewT2); - return stack; } - private Stack InsertVertexOnEdge(int v, int iT1, int iT2, bool handleFixedSplitEdge) + private void InsertVertexOnEdge(int v, int iT1, int iT2, bool handleFixedSplitEdge, Stack stack) { int iTnew1 = AddTriangle(); int iTnew2 = AddTriangle(); @@ -649,12 +671,11 @@ private Stack InsertVertexOnEdge(int v, int iT1, int iT2, bool handleFixedS SplitFixedEdge(sharedEdge, v); } - var stack = new Stack(4); + stack.Clear(); stack.Push(iT1); stack.Push(iTnew2); stack.Push(iT2); stack.Push(iTnew1); - return stack; } // ------------------------------------------------------------------------- @@ -760,7 +781,11 @@ private void FlipEdge( private void InsertEdgeIteration( Edge edge, Edge originalEdge, List remaining, - List tppIterations) + List tppIterations, + List polyL, + List polyR, + Dictionary outerTris, + List intersected) { int iA = edge.V1, iB = edge.V2; if (iA == iB) return; @@ -786,14 +811,16 @@ private void InsertEdgeIteration( return; } - var polyL = new List(8) { iA, iVL }; - var polyR = new List(8) { iA, iVR }; - var outerTris = new Dictionary - { - [new Edge(iA, iVL)] = CdtUtils.EdgeNeighbor(_triangles[iT], iA, iVL), - [new Edge(iA, iVR)] = CdtUtils.EdgeNeighbor(_triangles[iT], iA, iVR), - }; - var intersected = new List(8) { iT }; + polyL.Clear(); + polyR.Clear(); + outerTris.Clear(); + intersected.Clear(); + + polyL.Add(iA); polyL.Add(iVL); + polyR.Add(iA); polyR.Add(iVR); + outerTris[new Edge(iA, iVL)] = CdtUtils.EdgeNeighbor(_triangles[iT], iA, iVL); + outerTris[new Edge(iA, iVR)] = CdtUtils.EdgeNeighbor(_triangles[iT], iA, iVR); + intersected.Add(iT); int iV = iA; var t = _triangles[iT]; @@ -1241,7 +1268,8 @@ private int SplitFixedEdgeAt(Edge edge, V2d splitVert, int iT, int iTopo) { int iSplit = _vertices.Count; AddNewVertex(splitVert, Indices.NoNeighbor); - var stack = InsertVertexOnEdge(iSplit, iT, iTopo, handleFixedSplitEdge: false); + var stack = new Stack(4); + InsertVertexOnEdge(iSplit, iT, iTopo, handleFixedSplitEdge: false, stack); TryAddVertexToLocator(iSplit); EnsureDelaunayByEdgeFlips(iSplit, stack); SplitFixedEdge(edge, iSplit); diff --git a/src/CDT.Core/Types.cs b/src/CDT.Core/Types.cs index 7f6475c..1e87ee7 100644 --- a/src/CDT.Core/Types.cs +++ b/src/CDT.Core/Types.cs @@ -134,7 +134,15 @@ public Edge(int v1, int v2) public override bool Equals(object? obj) => obj is Edge e && Equals(e); /// - public override int GetHashCode() => HashCode.Combine(V1, V2); + public override int GetHashCode() + { + // V1 <= V2 is guaranteed by the constructor. Vertex indices are non-negative + // in normal usage; the (uint) cast handles any sign bits correctly via + // two's-complement bit patterns, preserving the equality contract. + // Pack two ints into a ulong, then fold the halves. + ulong packed = ((ulong)(uint)V1 << 32) | (uint)V2; + return unchecked((int)(uint)(packed ^ (packed >> 32))); + } /// public static bool operator ==(Edge a, Edge b) => a.Equals(b); From 59c7289f36ebc17819c0f929f4850ae9635416d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 18:04:27 +0000 Subject: [PATCH 3/6] Profile analysis: identified hotspots for next optimization round Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com> --- src/CDT.Core/Triangulation.cs | 58 ++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/src/CDT.Core/Triangulation.cs b/src/CDT.Core/Triangulation.cs index f5dba3b..98f8842 100644 --- a/src/CDT.Core/Triangulation.cs +++ b/src/CDT.Core/Triangulation.cs @@ -153,6 +153,16 @@ public void InsertVertices(IReadOnlyList> newVertices) bool isFirstInsertion = _kdTree == null && _vertices.Count == 0; + // Pre-allocate backing arrays once we know the incoming vertex count. + // Euler's formula: a planar triangulation of N points has ~2N triangles. + if (isFirstInsertion) + { + int n = newVertices.Count; + _vertices.EnsureCapacity(n + Indices.SuperTriangleVertexCount); + _vertTris.EnsureCapacity(n + Indices.SuperTriangleVertexCount); + _triangles.EnsureCapacity(2 * n + 4); + } + // Build bounding box of new vertices var box = new Box2d(); box.Envelop(newVertices); @@ -722,40 +732,46 @@ private void EdgeFlipInfo( n4 = tOpo.GetNeighbor(CdtUtils.Cw(oi)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool IsFlipNeeded(int iV1, int iV2, int iV3, int iV4) { - if (_fixedEdges.Contains(new Edge(iV2, iV4))) return false; - - var v1 = _vertices[iV1]; - var v2 = _vertices[iV2]; - var v3 = _vertices[iV3]; - var v4 = _vertices[iV4]; + // Skip HashSet lookup when there are no fixed edges (pure vertex-insertion path). + if (_fixedEdges.Count > 0 && _fixedEdges.Contains(new Edge(iV2, iV4))) return false; if (_superGeomType == SuperGeometryType.SuperTriangle) { + // Fast path: no super-triangle vertex involved (the common case after the + // first few insertions). Avoids four index comparisons and four vertex loads. int st = Indices.SuperTriangleVertexCount; - if (iV1 < st) + if (iV1 < st || iV2 < st || iV3 < st || iV4 < st) { + var v1 = _vertices[iV1]; + var v2 = _vertices[iV2]; + var v3 = _vertices[iV3]; + var v4 = _vertices[iV4]; + if (iV1 < st) + { + if (iV2 < st) + return LocatePointLine(v2, v3, v4) == LocatePointLine(v1, v3, v4); + if (iV4 < st) + return LocatePointLine(v4, v2, v3) == LocatePointLine(v1, v2, v3); + return false; + } + if (iV3 < st) + { + if (iV2 < st) + return LocatePointLine(v2, v1, v4) == LocatePointLine(v3, v1, v4); + if (iV4 < st) + return LocatePointLine(v4, v2, v1) == LocatePointLine(v3, v2, v1); + return false; + } if (iV2 < st) return LocatePointLine(v2, v3, v4) == LocatePointLine(v1, v3, v4); if (iV4 < st) return LocatePointLine(v4, v2, v3) == LocatePointLine(v1, v2, v3); - return false; - } - if (iV3 < st) - { - if (iV2 < st) - return LocatePointLine(v2, v1, v4) == LocatePointLine(v3, v1, v4); - if (iV4 < st) - return LocatePointLine(v4, v2, v1) == LocatePointLine(v3, v2, v1); - return false; } - if (iV2 < st) - return LocatePointLine(v2, v3, v4) == LocatePointLine(v1, v3, v4); - if (iV4 < st) - return LocatePointLine(v4, v2, v3) == LocatePointLine(v1, v2, v3); } - return IsInCircumcircle(v1, v2, v3, v4); + return IsInCircumcircle(_vertices[iV1], _vertices[iV2], _vertices[iV3], _vertices[iV4]); } private void FlipEdge( From 48c74ffaa75ae4952d634d947da930166bc068f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 18:14:24 +0000 Subject: [PATCH 4/6] Fix H+I: _two field on Triangulation, thread Stack/List through ConformToEdges; new benchmarks for all APIs Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com> --- benchmark/CDT.Benchmarks/Benchmarks.cs | 67 ++++++++++++++++++++++++++ src/CDT.Core/Triangulation.cs | 58 ++++++++++++---------- src/CDT.Core/Types.cs | 2 +- 3 files changed, 101 insertions(+), 26 deletions(-) diff --git a/benchmark/CDT.Benchmarks/Benchmarks.cs b/benchmark/CDT.Benchmarks/Benchmarks.cs index d32de06..549fa0d 100644 --- a/benchmark/CDT.Benchmarks/Benchmarks.cs +++ b/benchmark/CDT.Benchmarks/Benchmarks.cs @@ -129,6 +129,16 @@ public Triangulation Conforming_Auto() return cdt; } + [Benchmark(Description = "Conforming – AsProvided")] + [BenchmarkCategory("Conforming")] + public Triangulation Conforming_AsProvided() + { + var cdt = new Triangulation(VertexInsertionOrder.AsProvided); + cdt.InsertVertices(_vertices); + cdt.ConformToEdges(_edges); + return cdt; + } + // -- Full pipeline (insert + erase outer + holes) ------------------------ [Benchmark(Description = "Full pipeline – Auto")] @@ -142,6 +152,30 @@ public Triangulation FullPipeline_Auto() cdt.EraseOuterTrianglesAndHoles(); return cdt; } + + // -- Finalization APIs (EraseSuperTriangle / EraseOuterTriangles) --------- + + [Benchmark(Description = "EraseSuperTriangle – Auto")] + [BenchmarkCategory("Finalization")] + public Triangulation EraseSuperTriangle_Auto() + { + var cdt = new Triangulation(VertexInsertionOrder.Auto); + cdt.InsertVertices(_vertices); + cdt.InsertEdges(_edges); + cdt.EraseSuperTriangle(); + return cdt; + } + + [Benchmark(Description = "EraseOuterTriangles – Auto")] + [BenchmarkCategory("Finalization")] + public Triangulation EraseOuterTriangles_Auto() + { + var cdt = new Triangulation(VertexInsertionOrder.Auto); + cdt.InsertVertices(_vertices); + cdt.InsertEdges(_edges); + cdt.EraseOuterTriangles(); + return cdt; + } } // --------------------------------------------------------------------------- @@ -209,4 +243,37 @@ public Triangulation FloatVsDouble_Float() cdt.InsertEdges(ef); return cdt; } + + [Benchmark(Description = "Small – Conforming Auto")] + [BenchmarkCategory("SmallConforming")] + public Triangulation Small_Conforming_Auto() + { + var cdt = new Triangulation(VertexInsertionOrder.Auto); + cdt.InsertVertices(_vertices); + cdt.ConformToEdges(_edges); + return cdt; + } + + [Benchmark(Description = "Small – EraseSuperTriangle Auto")] + [BenchmarkCategory("SmallFinalization")] + public Triangulation Small_EraseSuperTriangle_Auto() + { + var cdt = new Triangulation(VertexInsertionOrder.Auto); + cdt.InsertVertices(_vertices); + cdt.InsertEdges(_edges); + cdt.EraseSuperTriangle(); + return cdt; + } + + [Benchmark(Description = "Small – EraseOuterTrianglesAndHoles Auto")] + [BenchmarkCategory("SmallFinalization")] + public Triangulation Small_EraseOuterTrianglesAndHoles_Auto() + { + var cdt = new Triangulation(VertexInsertionOrder.Auto, + IntersectingConstraintEdges.TryResolve, 0.0); + cdt.InsertVertices(_vertices); + cdt.InsertEdges(_edges); + cdt.EraseOuterTrianglesAndHoles(); + return cdt; + } } diff --git a/src/CDT.Core/Triangulation.cs b/src/CDT.Core/Triangulation.cs index 98f8842..1209154 100644 --- a/src/CDT.Core/Triangulation.cs +++ b/src/CDT.Core/Triangulation.cs @@ -100,6 +100,7 @@ public sealed class Triangulation private readonly VertexInsertionOrder _insertionOrder; private readonly IntersectingConstraintEdges _intersectingEdgesStrategy; private readonly T _minDistToConstraintEdge; + private readonly T _two; private SuperGeometryType _superGeomType; private int _nTargetVerts; @@ -137,6 +138,7 @@ public Triangulation( _insertionOrder = insertionOrder; _intersectingEdgesStrategy = intersectingEdgesStrategy; _minDistToConstraintEdge = minDistToConstraintEdge; + _two = T.One + T.One; _superGeomType = SuperGeometryType.SuperTriangle; _nTargetVerts = 0; _pieceToOriginalsView = new CovariantReadOnlyDictionary, IReadOnlyList>(_pieceToOriginals); @@ -241,6 +243,8 @@ public void InsertEdges(IReadOnlyList edges) public void ConformToEdges(IReadOnlyList edges) { var remaining = new List(8); + var flipStack = new Stack(4); + var flippedFixed = new List(8); foreach (var e in edges) { var shifted = new Edge(e.V1 + _nTargetVerts, e.V2 + _nTargetVerts); @@ -250,7 +254,7 @@ public void ConformToEdges(IReadOnlyList edges) { var task = remaining[^1]; remaining.RemoveAt(remaining.Count - 1); - ConformToEdgeIteration(task.Edge, task.Originals, task.Overlaps, remaining); + ConformToEdgeIteration(task.Edge, task.Originals, task.Overlaps, remaining, flipStack, flippedFixed); } } } @@ -313,18 +317,17 @@ private void AddSuperTriangle(Box2d box) _nTargetVerts = Indices.SuperTriangleVertexCount; _superGeomType = SuperGeometryType.SuperTriangle; - T two = T.One + T.One; - T cx = (box.Min.X + box.Max.X) / two; - T cy = (box.Min.Y + box.Max.Y) / two; + T cx = (box.Min.X + box.Max.X) / _two; + T cy = (box.Min.Y + box.Max.Y) / _two; T w = box.Max.X - box.Min.X; T h = box.Max.Y - box.Min.Y; T r = T.Max(w, h); - r = T.Max(two * r, T.One); + r = T.Max(_two * r, T.One); // Guard against very large numbers - while (cy <= cy - r) r = two * r; + while (cy <= cy - r) r = _two * r; - T R = two * r; + T R = _two * r; T cos30 = ParseT("0.8660254037844386"); T shiftX = R * cos30; @@ -486,15 +489,14 @@ private static void NthElement(int[] arr, int lo, int nth, int hi, TC } } - private List InsertVertex_FlipFixedEdges(int iV) + private void InsertVertex_FlipFixedEdges(int iV, Stack stack, List flipped) { - var flipped = new List(); + flipped.Clear(); // Use KD-tree if available, otherwise fall back to vertex 0 (first super-triangle vertex) int near = _kdTree != null ? _kdTree.Nearest(_vertices[iV].X, _vertices[iV].Y, _vertices) : 0; var (iT, iTopo) = WalkingSearchTrianglesAt(iV, near); - var stack = new Stack(4); if (iTopo == Indices.NoNeighbor) InsertVertexInsideTriangle(iV, iT, stack); else @@ -520,7 +522,6 @@ private List InsertVertex_FlipFixedEdges(int iV) } } TryAddVertexToLocator(iV); - return flipped; } private void EnsureDelaunayByEdgeFlips(int iV1, Stack triStack) @@ -745,30 +746,36 @@ private bool IsFlipNeeded(int iV1, int iV2, int iV3, int iV4) int st = Indices.SuperTriangleVertexCount; if (iV1 < st || iV2 < st || iV3 < st || iV4 < st) { - var v1 = _vertices[iV1]; - var v2 = _vertices[iV2]; - var v3 = _vertices[iV3]; - var v4 = _vertices[iV4]; + // Load only the vertices needed per branch to minimise memory accesses. + // Lines that check iV2/iV4 alone (bottom two) use the same formula as + // the iV1 < st path — intentional: the geometry predicate is symmetric + // in those positions when neither iV1 nor iV3 is a super vertex. if (iV1 < st) { if (iV2 < st) - return LocatePointLine(v2, v3, v4) == LocatePointLine(v1, v3, v4); + return LocatePointLine(_vertices[iV2], _vertices[iV3], _vertices[iV4]) == + LocatePointLine(_vertices[iV1], _vertices[iV3], _vertices[iV4]); if (iV4 < st) - return LocatePointLine(v4, v2, v3) == LocatePointLine(v1, v2, v3); + return LocatePointLine(_vertices[iV4], _vertices[iV2], _vertices[iV3]) == + LocatePointLine(_vertices[iV1], _vertices[iV2], _vertices[iV3]); return false; } if (iV3 < st) { if (iV2 < st) - return LocatePointLine(v2, v1, v4) == LocatePointLine(v3, v1, v4); + return LocatePointLine(_vertices[iV2], _vertices[iV1], _vertices[iV4]) == + LocatePointLine(_vertices[iV3], _vertices[iV1], _vertices[iV4]); if (iV4 < st) - return LocatePointLine(v4, v2, v1) == LocatePointLine(v3, v2, v1); + return LocatePointLine(_vertices[iV4], _vertices[iV2], _vertices[iV1]) == + LocatePointLine(_vertices[iV3], _vertices[iV2], _vertices[iV1]); return false; } if (iV2 < st) - return LocatePointLine(v2, v3, v4) == LocatePointLine(v1, v3, v4); + return LocatePointLine(_vertices[iV2], _vertices[iV3], _vertices[iV4]) == + LocatePointLine(_vertices[iV1], _vertices[iV3], _vertices[iV4]); if (iV4 < st) - return LocatePointLine(v4, v2, v3) == LocatePointLine(v1, v2, v3); + return LocatePointLine(_vertices[iV4], _vertices[iV2], _vertices[iV3]) == + LocatePointLine(_vertices[iV1], _vertices[iV2], _vertices[iV3]); } } return IsInCircumcircle(_vertices[iV1], _vertices[iV2], _vertices[iV3], _vertices[iV4]); @@ -949,7 +956,9 @@ private void HandleIntersectingEdgeStrategy( private void ConformToEdgeIteration( Edge edge, List originals, ushort overlaps, - List remaining) + List remaining, + Stack flipStack, + List flippedFixed) { int iA = edge.V1, iB = edge.V2; if (iA == iB) return; @@ -1008,10 +1017,9 @@ private void ConformToEdgeIteration( int iMid = _vertices.Count; var start = _vertices[iA]; var end = _vertices[iB]; - T two = T.One + T.One; - AddNewVertex(new V2d((start.X + end.X) / two, (start.Y + end.Y) / two), Indices.NoNeighbor); + AddNewVertex(new V2d((start.X + end.X) / _two, (start.Y + end.Y) / _two), Indices.NoNeighbor); - var flippedFixed = InsertVertex_FlipFixedEdges(iMid); + InsertVertex_FlipFixedEdges(iMid, flipStack, flippedFixed); remaining.Add(new ConformToEdgeTask(new Edge(iMid, iB), originals, overlaps)); remaining.Add(new ConformToEdgeTask(new Edge(iA, iMid), originals, overlaps)); diff --git a/src/CDT.Core/Types.cs b/src/CDT.Core/Types.cs index 1e87ee7..aa6c759 100644 --- a/src/CDT.Core/Types.cs +++ b/src/CDT.Core/Types.cs @@ -137,7 +137,7 @@ public Edge(int v1, int v2) public override int GetHashCode() { // V1 <= V2 is guaranteed by the constructor. Vertex indices are non-negative - // in normal usage; the (uint) cast handles any sign bits correctly via + // in normal usage; the (uint) cast handles any sign bit correctly via // two's-complement bit patterns, preserving the equality contract. // Pack two ints into a ulong, then fold the halves. ulong packed = ((ulong)(uint)V1 << 32) | (uint)V2; From 2cfbd857e750804a6204650f13e75aa982ce28c1 Mon Sep 17 00:00:00 2001 From: Michael Conrad Date: Sun, 22 Feb 2026 19:28:14 +0100 Subject: [PATCH 5/6] Update src/CDT.Core/Types.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/CDT.Core/Types.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/CDT.Core/Types.cs b/src/CDT.Core/Types.cs index aa6c759..7f6475c 100644 --- a/src/CDT.Core/Types.cs +++ b/src/CDT.Core/Types.cs @@ -134,15 +134,7 @@ public Edge(int v1, int v2) public override bool Equals(object? obj) => obj is Edge e && Equals(e); /// - public override int GetHashCode() - { - // V1 <= V2 is guaranteed by the constructor. Vertex indices are non-negative - // in normal usage; the (uint) cast handles any sign bit correctly via - // two's-complement bit patterns, preserving the equality contract. - // Pack two ints into a ulong, then fold the halves. - ulong packed = ((ulong)(uint)V1 << 32) | (uint)V2; - return unchecked((int)(uint)(packed ^ (packed >> 32))); - } + public override int GetHashCode() => HashCode.Combine(V1, V2); /// public static bool operator ==(Edge a, Edge b) => a.Equals(b); From 573a462936e76c2462b0ff3577ff902ec65fa556 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 18:29:31 +0000 Subject: [PATCH 6/6] Address reviewer feedback: restore IsFlipNeeded logic, revert Edge.GetHashCode, fix per-vertex Stack allocation Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com> --- src/CDT.Core/Triangulation.cs | 84 +++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/src/CDT.Core/Triangulation.cs b/src/CDT.Core/Triangulation.cs index 1209154..2ae393d 100644 --- a/src/CDT.Core/Triangulation.cs +++ b/src/CDT.Core/Triangulation.cs @@ -199,9 +199,10 @@ public void InsertVertices(IReadOnlyList> newVertices) else { // AsProvided: sequential order, KD-tree walk-start + var stack = new Stack(4); for (int iV = insertStart; iV < _vertices.Count; iV++) { - InsertVertex(iV); + InsertVertex(iV, stack); } } } @@ -369,10 +370,9 @@ private int AddTriangle(Triangle t) // Internal helpers – vertex insertion // ------------------------------------------------------------------------- - private void InsertVertex(int iVert, int walkStart) + private void InsertVertex(int iVert, int walkStart, Stack stack) { var (iT, iTopo) = WalkingSearchTrianglesAt(iVert, walkStart); - var stack = new Stack(4); if (iTopo == Indices.NoNeighbor) InsertVertexInsideTriangle(iVert, iT, stack); else @@ -381,6 +381,20 @@ private void InsertVertex(int iVert, int walkStart) TryAddVertexToLocator(iVert); } + private void InsertVertex(int iVert, int walkStart) + { + var stack = new Stack(4); + InsertVertex(iVert, walkStart, stack); + } + + private void InsertVertex(int iVert, Stack stack) + { + int near = _kdTree != null + ? _kdTree.Nearest(_vertices[iVert].X, _vertices[iVert].Y, _vertices) + : 0; + InsertVertex(iVert, near, stack); + } + private void InsertVertex(int iVert) { // Walk-start from KD-tree nearest point, or vertex 0 as fallback @@ -400,7 +414,8 @@ private void InsertVertices_Randomized(int superGeomVertCount) int j = Random.Shared.Next(i + 1); (indices[i], indices[j]) = (indices[j], indices[i]); } - foreach (int iV in indices) { InsertVertex(iV); } + var stack = new Stack(4); + foreach (int iV in indices) { InsertVertex(iV, stack); } } private void InsertVertices_KDTreeBFS(int superGeomVertCount, Box2d box) @@ -414,12 +429,13 @@ private void InsertVertices_KDTreeBFS(int superGeomVertCount, Box2d box) var queue = new Queue<(int lo, int hi, T boxMinX, T boxMinY, T boxMaxX, T boxMaxY, int parent)>(); queue.Enqueue((0, vertexCount, box.Min.X, box.Min.Y, box.Max.X, box.Max.Y, 0)); + var stack = new Stack(4); while (queue.Count > 0) { var (lo, hi, boxMinX, boxMinY, boxMaxX, boxMaxY, parent) = queue.Dequeue(); int len = hi - lo; if (len == 0) { continue; } - if (len == 1) { InsertVertex(indices[lo], parent); continue; } + if (len == 1) { InsertVertex(indices[lo], parent, stack); continue; } int midPos = lo + len / 2; @@ -427,7 +443,7 @@ private void InsertVertices_KDTreeBFS(int superGeomVertCount, Box2d box) { NthElement(indices, lo, midPos, hi, new VertexXComparer(_vertices)); T split = _vertices[indices[midPos]].X; - InsertVertex(indices[midPos], parent); + InsertVertex(indices[midPos], parent, stack); if (lo < midPos) { queue.Enqueue((lo, midPos, boxMinX, boxMinY, split, boxMaxY, indices[midPos])); } if (midPos + 1 < hi) { queue.Enqueue((midPos + 1, hi, split, boxMinY, boxMaxX, boxMaxY, indices[midPos])); } } @@ -435,7 +451,7 @@ private void InsertVertices_KDTreeBFS(int superGeomVertCount, Box2d box) { NthElement(indices, lo, midPos, hi, new VertexYComparer(_vertices)); T split = _vertices[indices[midPos]].Y; - InsertVertex(indices[midPos], parent); + InsertVertex(indices[midPos], parent, stack); if (lo < midPos) { queue.Enqueue((lo, midPos, boxMinX, boxMinY, boxMaxX, split, indices[midPos])); } if (midPos + 1 < hi) { queue.Enqueue((midPos + 1, hi, boxMinX, split, boxMaxX, boxMaxY, indices[midPos])); } } @@ -739,46 +755,36 @@ private bool IsFlipNeeded(int iV1, int iV2, int iV3, int iV4) // Skip HashSet lookup when there are no fixed edges (pure vertex-insertion path). if (_fixedEdges.Count > 0 && _fixedEdges.Contains(new Edge(iV2, iV4))) return false; + var v1 = _vertices[iV1]; + var v2 = _vertices[iV2]; + var v3 = _vertices[iV3]; + var v4 = _vertices[iV4]; + if (_superGeomType == SuperGeometryType.SuperTriangle) { - // Fast path: no super-triangle vertex involved (the common case after the - // first few insertions). Avoids four index comparisons and four vertex loads. int st = Indices.SuperTriangleVertexCount; - if (iV1 < st || iV2 < st || iV3 < st || iV4 < st) + if (iV1 < st) + { + if (iV2 < st) + return LocatePointLine(v2, v3, v4) == LocatePointLine(v1, v3, v4); + if (iV4 < st) + return LocatePointLine(v4, v2, v3) == LocatePointLine(v1, v2, v3); + return false; + } + if (iV3 < st) { - // Load only the vertices needed per branch to minimise memory accesses. - // Lines that check iV2/iV4 alone (bottom two) use the same formula as - // the iV1 < st path — intentional: the geometry predicate is symmetric - // in those positions when neither iV1 nor iV3 is a super vertex. - if (iV1 < st) - { - if (iV2 < st) - return LocatePointLine(_vertices[iV2], _vertices[iV3], _vertices[iV4]) == - LocatePointLine(_vertices[iV1], _vertices[iV3], _vertices[iV4]); - if (iV4 < st) - return LocatePointLine(_vertices[iV4], _vertices[iV2], _vertices[iV3]) == - LocatePointLine(_vertices[iV1], _vertices[iV2], _vertices[iV3]); - return false; - } - if (iV3 < st) - { - if (iV2 < st) - return LocatePointLine(_vertices[iV2], _vertices[iV1], _vertices[iV4]) == - LocatePointLine(_vertices[iV3], _vertices[iV1], _vertices[iV4]); - if (iV4 < st) - return LocatePointLine(_vertices[iV4], _vertices[iV2], _vertices[iV1]) == - LocatePointLine(_vertices[iV3], _vertices[iV2], _vertices[iV1]); - return false; - } if (iV2 < st) - return LocatePointLine(_vertices[iV2], _vertices[iV3], _vertices[iV4]) == - LocatePointLine(_vertices[iV1], _vertices[iV3], _vertices[iV4]); + return LocatePointLine(v2, v1, v4) == LocatePointLine(v3, v1, v4); if (iV4 < st) - return LocatePointLine(_vertices[iV4], _vertices[iV2], _vertices[iV3]) == - LocatePointLine(_vertices[iV1], _vertices[iV2], _vertices[iV3]); + return LocatePointLine(v4, v2, v1) == LocatePointLine(v3, v2, v1); + return false; } + if (iV2 < st) + return LocatePointLine(v2, v3, v4) == LocatePointLine(v1, v3, v4); + if (iV4 < st) + return LocatePointLine(v4, v2, v3) == LocatePointLine(v1, v2, v3); } - return IsInCircumcircle(_vertices[iV1], _vertices[iV2], _vertices[iV3], _vertices[iV4]); + return IsInCircumcircle(v1, v2, v3, v4); } private void FlipEdge(