From 6a1b48417975159188ddb16d794279559b1b4f66 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:43:27 +0000 Subject: [PATCH 1/5] Initial plan From 5a73f7715ca46e644acaad259171698b327fd174 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:51:43 +0000 Subject: [PATCH 2/5] Apply all 5 performance optimizations (KdTree InlineArray, reuse flip stack, eliminate layer allocs, bool[] in RemoveTriangles, hoist fixed-edge check) Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com> --- src/CDT.Core/KdTree.cs | 32 +++++++++++++------ src/CDT.Core/Triangulation.cs | 58 +++++++++++++++++++---------------- 2 files changed, 55 insertions(+), 35 deletions(-) diff --git a/src/CDT.Core/KdTree.cs b/src/CDT.Core/KdTree.cs index a124554..a84b18d 100644 --- a/src/CDT.Core/KdTree.cs +++ b/src/CDT.Core/KdTree.cs @@ -23,15 +23,26 @@ internal sealed class KdTree private enum SplitDir { X, Y } + [System.Runtime.CompilerServices.InlineArray(NumVerticesInLeaf)] + private struct LeafBuffer { private int _element; } + + private struct LeafData + { + public LeafBuffer Items; + public int Count; + public void Add(int v) => Items[Count++] = v; + public int this[int i] => Items[i]; + } + private struct Node { // children[0] == children[1] => leaf public int Child0, Child1; - public List? Data; // non-null only for leaf nodes + public LeafData Data; // always present; Count==0 for inner nodes public bool IsLeaf => Child0 == Child1; - public static Node NewLeaf() => new() { Child0 = 0, Child1 = 0, Data = new List(NumVerticesInLeaf) }; + public static Node NewLeaf() => new() { Child0 = 0, Child1 = 0 }; } private readonly struct NearestTask @@ -103,7 +114,7 @@ public void Insert(int iPoint, IReadOnlyList> points) if (n.IsLeaf) { // Add point if capacity not reached - if (n.Data!.Count < NumVerticesInLeaf) + if (n.Data.Count < NumVerticesInLeaf) { n.Data.Add(iPoint); return; @@ -123,13 +134,14 @@ public void Insert(int iPoint, IReadOnlyList> points) n.Child0 = c1; n.Child1 = c2; // Move existing points to children - foreach (int ip in n.Data!) + for (int _ip = 0; _ip < n.Data.Count; _ip++) { + int ip = n.Data[_ip]; T cx = points[ip].X, cy = points[ip].Y; int target = WhichChild(cx, cy, mid, dir); - _nodes[target == 0 ? c1 : c2].Data!.Add(ip); + ref Node child = ref CollectionsMarshal.AsSpan(_nodes)[target == 0 ? c1 : c2]; + child.Data.Add(ip); } - n.Data = null; // inner node – no data list needed } T midVal = GetMid(minX, minY, maxX, maxY, dir); @@ -166,8 +178,9 @@ public int Nearest(T qx, T qy, IReadOnlyList> points) ref Node n = ref CollectionsMarshal.AsSpan(_nodes)[task.NodeIndex]; if (n.IsLeaf) { - foreach (int ip in n.Data!) + for (int i = 0; i < n.Data.Count; i++) { + int ip = n.Data[i]; T dx = points[ip].X - qx; T dy = points[ip].Y - qy; T d2 = dx * dx + dy * dy; @@ -314,10 +327,11 @@ private void ExtendTree(T px, T py) private void InitializeRootBox(IReadOnlyList> points) { Node rootNode = _nodes[_root]; - T mxn = points[rootNode.Data![0]].X, myn = points[rootNode.Data[0]].Y; + T mxn = points[rootNode.Data[0]].X, myn = points[rootNode.Data[0]].Y; T mxx = mxn, mxy = myn; - foreach (int ip in rootNode.Data) + for (int i = 0; i < rootNode.Data.Count; i++) { + int ip = rootNode.Data[i]; T cx = points[ip].X, cy = points[ip].Y; if (cx < mxn) mxn = cx; if (cx > mxx) mxx = cx; diff --git a/src/CDT.Core/Triangulation.cs b/src/CDT.Core/Triangulation.cs index ae1ca29..b8bd27e 100644 --- a/src/CDT.Core/Triangulation.cs +++ b/src/CDT.Core/Triangulation.cs @@ -106,6 +106,9 @@ public sealed class Triangulation private SuperGeometryType _superGeomType; private int _nTargetVerts; + // Reusable flip stack — cleared before each use inside InsertVertex + private readonly Stack _flipStack = new(4); + // For each vertex: one adjacent triangle index private int[] _vertTris = []; private int _vertTrisCount; @@ -406,12 +409,6 @@ private void InsertVertex(int iVert, int walkStart, Stack stack) 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 @@ -426,7 +423,7 @@ private void InsertVertex(int iVert) int near = _kdTree != null ? _kdTree.Nearest(_vertices[iVert].X, _vertices[iVert].Y, _vertices) : 0; - InsertVertex(iVert, near); + InsertVertex(iVert, near, _flipStack); } private void InsertVertices_Randomized(int superGeomVertCount) @@ -544,6 +541,7 @@ private void InsertVertex_FlipFixedEdges(int iV, Stack stack, List fl InsertVertexOnEdge(iV, iT, iTopo, handleFixedSplitEdge: false, stack); int _dbgFlipIter2 = 0; + bool hasFixedEdges = _fixedEdges.Count > 0; // hoist — read once while (stack.Count > 0) { if (++_dbgFlipIter2 > 1_000_000) throw new InvalidOperationException($"InsertVertex_FlipFixed infinite loop, iV={iV}"); @@ -552,7 +550,7 @@ private void InsertVertex_FlipFixedEdges(int iV, Stack stack, List fl out int itopo, out int iv2, out int iv3, out int iv4, out int n1, out int n2, out int n3, out int n4); - if (itopo != Indices.NoNeighbor && IsFlipNeeded(iV, iv2, iv3, iv4)) + if (itopo != Indices.NoNeighbor && IsFlipNeeded(iV, iv2, iv3, iv4, hasFixedEdges)) { var flippedEdge = new Edge(iv2, iv4); if (_fixedEdges.Contains(flippedEdge)) @@ -567,6 +565,7 @@ private void InsertVertex_FlipFixedEdges(int iV, Stack stack, List fl private void EnsureDelaunayByEdgeFlips(int iV1, Stack triStack) { + bool hasFixedEdges = _fixedEdges.Count > 0; // hoist — read once int _dbgFlipIter = 0; while (triStack.Count > 0) { @@ -576,7 +575,7 @@ private void EnsureDelaunayByEdgeFlips(int iV1, Stack triStack) EdgeFlipInfo(iT, iV1, out int iTopo, out int iV2, out int iV3, out int iV4, out int n1, out int n2, out int n3, out int n4); - if (iTopo != Indices.NoNeighbor && IsFlipNeeded(iV1, iV2, iV3, iV4)) + if (iTopo != Indices.NoNeighbor && IsFlipNeeded(iV1, iV2, iV3, iV4, hasFixedEdges)) { FlipEdge(iT, iTopo, iV1, iV2, iV3, iV4, n1, n2, n3, n4); triStack.Push(iT); @@ -775,10 +774,10 @@ private void EdgeFlipInfo( } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsFlipNeeded(int iV1, int iV2, int iV3, int iV4) + private bool IsFlipNeeded(int iV1, int iV2, int iV3, int iV4, bool hasFixedEdges) { // 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 (hasFixedEdges && _fixedEdges.Contains(new Edge(iV2, iV4))) return false; var v1 = _vertices[iV1]; var v2 = _vertices[iV2]; @@ -1436,19 +1435,24 @@ private void RemapEdgesNoSuperTriangle(Dictionary> dict) private void RemoveTriangles(HashSet removed) { if (removed.Count == 0) return; + + // Build a flat bool[] for O(1) indexed lookup — replaces all HashSet.Contains calls + var isRemoved = new bool[_trianglesCount]; + foreach (int i in removed) isRemoved[i] = true; + // Build compact mapping: old index → new index var mapping = new int[_trianglesCount]; int newIdx = 0; for (int i = 0; i < _trianglesCount; i++) { - if (removed.Contains(i)) { mapping[i] = Indices.NoNeighbor; continue; } + if (isRemoved[i]) { mapping[i] = Indices.NoNeighbor; continue; } mapping[i] = newIdx++; } // Compact triangle list int write = 0; for (int i = 0; i < _trianglesCount; i++) { - if (removed.Contains(i)) continue; + if (isRemoved[i]) continue; _triangles[write++] = _triangles[i]; } _trianglesCount = write; @@ -1456,9 +1460,9 @@ private void RemoveTriangles(HashSet removed) for (int i = 0; i < _trianglesCount; i++) { ref var t = ref _triangles[i]; - t.N0 = t.N0 == Indices.NoNeighbor ? Indices.NoNeighbor : (removed.Contains(t.N0) ? Indices.NoNeighbor : mapping[t.N0]); - t.N1 = t.N1 == Indices.NoNeighbor ? Indices.NoNeighbor : (removed.Contains(t.N1) ? Indices.NoNeighbor : mapping[t.N1]); - t.N2 = t.N2 == Indices.NoNeighbor ? Indices.NoNeighbor : (removed.Contains(t.N2) ? Indices.NoNeighbor : mapping[t.N2]); + t.N0 = t.N0 == Indices.NoNeighbor ? Indices.NoNeighbor : (isRemoved[t.N0] ? Indices.NoNeighbor : mapping[t.N0]); + t.N1 = t.N1 == Indices.NoNeighbor ? Indices.NoNeighbor : (isRemoved[t.N1] ? Indices.NoNeighbor : mapping[t.N1]); + t.N2 = t.N2 == Indices.NoNeighbor ? Indices.NoNeighbor : (isRemoved[t.N2] ? Indices.NoNeighbor : mapping[t.N2]); } } @@ -1471,27 +1475,30 @@ private ushort[] CalculateTriangleDepths() var depths = new ushort[_trianglesCount]; for (int i = 0; i < depths.Length; i++) depths[i] = ushort.MaxValue; - // Find a triangle touching the super-triangle vertex 0 int seedTri = _vertTrisCount > 0 ? _vertTris[0] : Indices.NoNeighbor; if (seedTri == Indices.NoNeighbor && _trianglesCount > 0) seedTri = 0; - var layerSeeds = new Stack(); - layerSeeds.Push(seedTri); + var currentSeeds = new Stack(); + var nextSeeds = new Stack(); + var behindBoundary = new Dictionary(); + currentSeeds.Push(seedTri); ushort depth = 0; - while (layerSeeds.Count > 0) + while (currentSeeds.Count > 0) { - var nextLayer = PeelLayer(layerSeeds, depth, depths); - layerSeeds = new Stack(nextLayer.Keys); + behindBoundary.Clear(); + PeelLayer(currentSeeds, depth, depths, behindBoundary); + nextSeeds.Clear(); + foreach (var kv in behindBoundary) nextSeeds.Push(kv.Key); + (currentSeeds, nextSeeds) = (nextSeeds, currentSeeds); // swap — no alloc depth++; } return depths; } - private Dictionary PeelLayer( - Stack seeds, ushort layerDepth, ushort[] triDepths) + private void PeelLayer( + Stack seeds, ushort layerDepth, ushort[] triDepths, Dictionary behindBoundary) { - var behindBoundary = new Dictionary(); while (seeds.Count > 0) { int iT = seeds.Pop(); @@ -1522,7 +1529,6 @@ private Dictionary PeelLayer( } } } - return behindBoundary; } // ------------------------------------------------------------------------- From 281710532037f495df87335b8b911493eb161ffe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:11:40 +0000 Subject: [PATCH 3/5] Add defensive _flipStack.Clear() in InsertVertex(int) for explicitness Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com> --- src/CDT.Core/Triangulation.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CDT.Core/Triangulation.cs b/src/CDT.Core/Triangulation.cs index b8bd27e..96009ce 100644 --- a/src/CDT.Core/Triangulation.cs +++ b/src/CDT.Core/Triangulation.cs @@ -423,6 +423,7 @@ private void InsertVertex(int iVert) int near = _kdTree != null ? _kdTree.Nearest(_vertices[iVert].X, _vertices[iVert].Y, _vertices) : 0; + _flipStack.Clear(); // defensive: EnsureDelaunayByEdgeFlips always drains it, but be explicit InsertVertex(iVert, near, _flipStack); } From 4a87f5ce2b92dd39aa0450c96e96619d49fa9279 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:44:18 +0000 Subject: [PATCH 4/5] Revert Change 1 (KdTree InlineArray): 8.8x node bloat caused regression Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com> --- src/CDT.Core/KdTree.cs | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/src/CDT.Core/KdTree.cs b/src/CDT.Core/KdTree.cs index a84b18d..a124554 100644 --- a/src/CDT.Core/KdTree.cs +++ b/src/CDT.Core/KdTree.cs @@ -23,26 +23,15 @@ internal sealed class KdTree private enum SplitDir { X, Y } - [System.Runtime.CompilerServices.InlineArray(NumVerticesInLeaf)] - private struct LeafBuffer { private int _element; } - - private struct LeafData - { - public LeafBuffer Items; - public int Count; - public void Add(int v) => Items[Count++] = v; - public int this[int i] => Items[i]; - } - private struct Node { // children[0] == children[1] => leaf public int Child0, Child1; - public LeafData Data; // always present; Count==0 for inner nodes + public List? Data; // non-null only for leaf nodes public bool IsLeaf => Child0 == Child1; - public static Node NewLeaf() => new() { Child0 = 0, Child1 = 0 }; + public static Node NewLeaf() => new() { Child0 = 0, Child1 = 0, Data = new List(NumVerticesInLeaf) }; } private readonly struct NearestTask @@ -114,7 +103,7 @@ public void Insert(int iPoint, IReadOnlyList> points) if (n.IsLeaf) { // Add point if capacity not reached - if (n.Data.Count < NumVerticesInLeaf) + if (n.Data!.Count < NumVerticesInLeaf) { n.Data.Add(iPoint); return; @@ -134,14 +123,13 @@ public void Insert(int iPoint, IReadOnlyList> points) n.Child0 = c1; n.Child1 = c2; // Move existing points to children - for (int _ip = 0; _ip < n.Data.Count; _ip++) + foreach (int ip in n.Data!) { - int ip = n.Data[_ip]; T cx = points[ip].X, cy = points[ip].Y; int target = WhichChild(cx, cy, mid, dir); - ref Node child = ref CollectionsMarshal.AsSpan(_nodes)[target == 0 ? c1 : c2]; - child.Data.Add(ip); + _nodes[target == 0 ? c1 : c2].Data!.Add(ip); } + n.Data = null; // inner node – no data list needed } T midVal = GetMid(minX, minY, maxX, maxY, dir); @@ -178,9 +166,8 @@ public int Nearest(T qx, T qy, IReadOnlyList> points) ref Node n = ref CollectionsMarshal.AsSpan(_nodes)[task.NodeIndex]; if (n.IsLeaf) { - for (int i = 0; i < n.Data.Count; i++) + foreach (int ip in n.Data!) { - int ip = n.Data[i]; T dx = points[ip].X - qx; T dy = points[ip].Y - qy; T d2 = dx * dx + dy * dy; @@ -327,11 +314,10 @@ private void ExtendTree(T px, T py) private void InitializeRootBox(IReadOnlyList> points) { Node rootNode = _nodes[_root]; - T mxn = points[rootNode.Data[0]].X, myn = points[rootNode.Data[0]].Y; + T mxn = points[rootNode.Data![0]].X, myn = points[rootNode.Data[0]].Y; T mxx = mxn, mxy = myn; - for (int i = 0; i < rootNode.Data.Count; i++) + foreach (int ip in rootNode.Data) { - int ip = rootNode.Data[i]; T cx = points[ip].X, cy = points[ip].Y; if (cx < mxn) mxn = cx; if (cx > mxx) mxx = cx; From e22bbd95d92a7f8b9fb4b852349a6c0d3eb3d0df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:48:47 +0000 Subject: [PATCH 5/5] Replace HashSet.Contains with bool[] in RemoveTriangles (only change) Co-authored-by: MichaCo <5837539+MichaCo@users.noreply.github.com> --- src/CDT.Core/Triangulation.cs | 46 ++++++++++++++++------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/src/CDT.Core/Triangulation.cs b/src/CDT.Core/Triangulation.cs index 96009ce..a6f3039 100644 --- a/src/CDT.Core/Triangulation.cs +++ b/src/CDT.Core/Triangulation.cs @@ -106,9 +106,6 @@ public sealed class Triangulation private SuperGeometryType _superGeomType; private int _nTargetVerts; - // Reusable flip stack — cleared before each use inside InsertVertex - private readonly Stack _flipStack = new(4); - // For each vertex: one adjacent triangle index private int[] _vertTris = []; private int _vertTrisCount; @@ -409,6 +406,12 @@ private void InsertVertex(int iVert, int walkStart, Stack stack) 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 @@ -423,8 +426,7 @@ private void InsertVertex(int iVert) int near = _kdTree != null ? _kdTree.Nearest(_vertices[iVert].X, _vertices[iVert].Y, _vertices) : 0; - _flipStack.Clear(); // defensive: EnsureDelaunayByEdgeFlips always drains it, but be explicit - InsertVertex(iVert, near, _flipStack); + InsertVertex(iVert, near); } private void InsertVertices_Randomized(int superGeomVertCount) @@ -542,7 +544,6 @@ private void InsertVertex_FlipFixedEdges(int iV, Stack stack, List fl InsertVertexOnEdge(iV, iT, iTopo, handleFixedSplitEdge: false, stack); int _dbgFlipIter2 = 0; - bool hasFixedEdges = _fixedEdges.Count > 0; // hoist — read once while (stack.Count > 0) { if (++_dbgFlipIter2 > 1_000_000) throw new InvalidOperationException($"InsertVertex_FlipFixed infinite loop, iV={iV}"); @@ -551,7 +552,7 @@ private void InsertVertex_FlipFixedEdges(int iV, Stack stack, List fl out int itopo, out int iv2, out int iv3, out int iv4, out int n1, out int n2, out int n3, out int n4); - if (itopo != Indices.NoNeighbor && IsFlipNeeded(iV, iv2, iv3, iv4, hasFixedEdges)) + if (itopo != Indices.NoNeighbor && IsFlipNeeded(iV, iv2, iv3, iv4)) { var flippedEdge = new Edge(iv2, iv4); if (_fixedEdges.Contains(flippedEdge)) @@ -566,7 +567,6 @@ private void InsertVertex_FlipFixedEdges(int iV, Stack stack, List fl private void EnsureDelaunayByEdgeFlips(int iV1, Stack triStack) { - bool hasFixedEdges = _fixedEdges.Count > 0; // hoist — read once int _dbgFlipIter = 0; while (triStack.Count > 0) { @@ -576,7 +576,7 @@ private void EnsureDelaunayByEdgeFlips(int iV1, Stack triStack) EdgeFlipInfo(iT, iV1, out int iTopo, out int iV2, out int iV3, out int iV4, out int n1, out int n2, out int n3, out int n4); - if (iTopo != Indices.NoNeighbor && IsFlipNeeded(iV1, iV2, iV3, iV4, hasFixedEdges)) + if (iTopo != Indices.NoNeighbor && IsFlipNeeded(iV1, iV2, iV3, iV4)) { FlipEdge(iT, iTopo, iV1, iV2, iV3, iV4, n1, n2, n3, n4); triStack.Push(iT); @@ -775,10 +775,10 @@ private void EdgeFlipInfo( } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsFlipNeeded(int iV1, int iV2, int iV3, int iV4, bool hasFixedEdges) + 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 (hasFixedEdges && _fixedEdges.Contains(new Edge(iV2, iV4))) return false; + if (_fixedEdges.Count > 0 && _fixedEdges.Contains(new Edge(iV2, iV4))) return false; var v1 = _vertices[iV1]; var v2 = _vertices[iV2]; @@ -1436,11 +1436,9 @@ private void RemapEdgesNoSuperTriangle(Dictionary> dict) private void RemoveTriangles(HashSet removed) { if (removed.Count == 0) return; - // Build a flat bool[] for O(1) indexed lookup — replaces all HashSet.Contains calls var isRemoved = new bool[_trianglesCount]; foreach (int i in removed) isRemoved[i] = true; - // Build compact mapping: old index → new index var mapping = new int[_trianglesCount]; int newIdx = 0; @@ -1476,30 +1474,27 @@ private ushort[] CalculateTriangleDepths() var depths = new ushort[_trianglesCount]; for (int i = 0; i < depths.Length; i++) depths[i] = ushort.MaxValue; + // Find a triangle touching the super-triangle vertex 0 int seedTri = _vertTrisCount > 0 ? _vertTris[0] : Indices.NoNeighbor; if (seedTri == Indices.NoNeighbor && _trianglesCount > 0) seedTri = 0; - var currentSeeds = new Stack(); - var nextSeeds = new Stack(); - var behindBoundary = new Dictionary(); - currentSeeds.Push(seedTri); + var layerSeeds = new Stack(); + layerSeeds.Push(seedTri); ushort depth = 0; - while (currentSeeds.Count > 0) + while (layerSeeds.Count > 0) { - behindBoundary.Clear(); - PeelLayer(currentSeeds, depth, depths, behindBoundary); - nextSeeds.Clear(); - foreach (var kv in behindBoundary) nextSeeds.Push(kv.Key); - (currentSeeds, nextSeeds) = (nextSeeds, currentSeeds); // swap — no alloc + var nextLayer = PeelLayer(layerSeeds, depth, depths); + layerSeeds = new Stack(nextLayer.Keys); depth++; } return depths; } - private void PeelLayer( - Stack seeds, ushort layerDepth, ushort[] triDepths, Dictionary behindBoundary) + private Dictionary PeelLayer( + Stack seeds, ushort layerDepth, ushort[] triDepths) { + var behindBoundary = new Dictionary(); while (seeds.Count > 0) { int iT = seeds.Pop(); @@ -1530,6 +1525,7 @@ private void PeelLayer( } } } + return behindBoundary; } // -------------------------------------------------------------------------