perf(fill): reuse part triangulations within an overlap check
After 1b, triangulating both polygons on every pair was the largest remaining overlap cost (27% of main-thread samples on the corpus job). PartOverlapChecker now triangulates each part at most once per check, lazily after the bounding-box gate, and passes the triangles to a new internal Collision.HasOverlap overload that runs the unchanged OverlapRegions body. Triangles are only read by clipping and hole subtraction, so reuse gives identical verdicts. Verification: - 49,000 seeded decisions with reused triangles match LegacyCollision; triangles stay bit-identical to a fresh triangulation afterwards. - Debug PolygonTriangulations: 246 -> 40 and 64 -> 36 per grid check; sharing triangles per Program instead fails 23 tests. - Corpus job (169 parts, --engines Default --parallel 1): median 13,398 -> 12,702 ms over 4+4 alternating runs vs 1b, identical outcomes; serialized layout byte-identical to the base. Also records the Follow-up B' (Slices 1a, 1b, 2a) measurements in docs/performance/fill-performance.md.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Math;
|
||||
|
||||
@@ -58,6 +59,34 @@ namespace OpenNest.Geometry
|
||||
return OverlapRegions(a, b, holesA, holesB).Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="HasOverlap(Polygon, Polygon, List{Polygon}, List{Polygon})"/> with the
|
||||
/// per-polygon triangulation supplied by the caller. Triangulations are resolved lazily,
|
||||
/// only after the bounding boxes overlap, and must come from <see cref="Triangulate"/> on
|
||||
/// the same polygon; the verdict is then identical. The triangles are only read.
|
||||
/// </summary>
|
||||
internal static bool HasOverlap(
|
||||
Polygon a,
|
||||
Func<List<Polygon>> trianglesA,
|
||||
Polygon b,
|
||||
Func<List<Polygon>> trianglesB,
|
||||
List<Polygon> holesA = null,
|
||||
List<Polygon> holesB = null
|
||||
)
|
||||
{
|
||||
if (!BoundingBoxesOverlap(a.BoundingBox, b.BoundingBox))
|
||||
return false;
|
||||
|
||||
return OverlapRegions(trianglesA(), trianglesB(), holesA, holesB).Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The triangulation <see cref="Check"/> and <see cref="HasOverlap(Polygon, Polygon, List{Polygon}, List{Polygon})"/>
|
||||
/// use for <paramref name="polygon"/>: ear-clipped triangles with bounds updated. Callers
|
||||
/// that reuse it must not mutate the polygon or the triangles.
|
||||
/// </summary>
|
||||
internal static List<Polygon> Triangulate(Polygon polygon) => TriangulateWithBounds(polygon);
|
||||
|
||||
public static List<CollisionResult> CheckAll(
|
||||
List<Polygon> polygons,
|
||||
List<List<Polygon>> holes = null
|
||||
@@ -108,11 +137,15 @@ namespace OpenNest.Geometry
|
||||
Polygon b,
|
||||
List<Polygon> holesA,
|
||||
List<Polygon> holesB
|
||||
) => OverlapRegions(TriangulateWithBounds(a), TriangulateWithBounds(b), holesA, holesB);
|
||||
|
||||
private static List<Polygon> OverlapRegions(
|
||||
List<Polygon> trisA,
|
||||
List<Polygon> trisB,
|
||||
List<Polygon> holesA,
|
||||
List<Polygon> holesB
|
||||
)
|
||||
{
|
||||
var trisA = TriangulateWithBounds(a);
|
||||
var trisB = TriangulateWithBounds(b);
|
||||
|
||||
var regions = new List<Polygon>();
|
||||
|
||||
foreach (var triA in trisA)
|
||||
@@ -186,6 +219,7 @@ namespace OpenNest.Geometry
|
||||
/// </summary>
|
||||
private static List<Polygon> TriangulateWithBounds(Polygon polygon)
|
||||
{
|
||||
PerfCounters.CountPolygonTriangulation();
|
||||
var tris = ConvexDecomposition.Triangulate(polygon);
|
||||
foreach (var tri in tris)
|
||||
tri.UpdateBounds();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
@@ -6,8 +7,8 @@ namespace OpenNest
|
||||
/// <summary>
|
||||
/// Overlap-only form of <see cref="Part.Intersects"/> for one pass over a fixed set of parts.
|
||||
/// Verdicts match <c>Intersects(other, out _)</c>. Each distinct <see cref="CNC.Program"/>
|
||||
/// (by reference) is prepared once, each part's world polygon is built once, and crossing
|
||||
/// points are not computed. Tiled copies from <see cref="Part.CloneAtOffset"/> share one
|
||||
/// (by reference) is prepared once, each part's world polygon is built and triangulated at
|
||||
/// most once, and crossing points are not computed. Tiled copies from <see cref="Part.CloneAtOffset"/> share one
|
||||
/// Program, so a fill grid prepares its pattern's programs only once.
|
||||
/// Parts must not move, rotate or change Program while an instance is in use. Instances are
|
||||
/// not thread-safe: create one per check.
|
||||
@@ -18,7 +19,7 @@ namespace OpenNest
|
||||
ReferenceEqualityComparer.Instance
|
||||
);
|
||||
|
||||
private readonly Dictionary<Part, Polygon> worldPolygons = new(
|
||||
private readonly Dictionary<Part, PreparedPart> preparedParts = new(
|
||||
ReferenceEqualityComparer.Instance
|
||||
);
|
||||
|
||||
@@ -42,13 +43,18 @@ namespace OpenNest
|
||||
if (perimeter1 == null || perimeter2 == null)
|
||||
return false;
|
||||
|
||||
var polygon1 = WorldPolygon(part1, prepared1);
|
||||
var polygon2 = WorldPolygon(part2, prepared2);
|
||||
var world1 = PreparePart(part1, prepared1);
|
||||
var world2 = PreparePart(part2, prepared2);
|
||||
|
||||
if (polygon1 == null || polygon2 == null)
|
||||
if (world1.Polygon == null || world2.Polygon == null)
|
||||
return false;
|
||||
|
||||
return Collision.HasOverlap(polygon1, polygon2);
|
||||
return Collision.HasOverlap(
|
||||
world1.Polygon,
|
||||
world1.Triangles,
|
||||
world2.Polygon,
|
||||
world2.Triangles
|
||||
);
|
||||
}
|
||||
|
||||
private PreparedProgram Prepare(CNC.Program program)
|
||||
@@ -62,11 +68,12 @@ namespace OpenNest
|
||||
return prepared;
|
||||
}
|
||||
|
||||
private Polygon WorldPolygon(Part part, PreparedProgram prepared)
|
||||
private PreparedPart PreparePart(Part part, PreparedProgram prepared)
|
||||
{
|
||||
if (worldPolygons.TryGetValue(part, out var polygon))
|
||||
return polygon;
|
||||
if (preparedParts.TryGetValue(part, out var world))
|
||||
return world;
|
||||
|
||||
Polygon polygon = null;
|
||||
var local = prepared.GetLocalPolygon();
|
||||
|
||||
if (local != null)
|
||||
@@ -78,8 +85,30 @@ namespace OpenNest
|
||||
polygon.Offset(part.Location);
|
||||
}
|
||||
|
||||
worldPolygons.Add(part, polygon);
|
||||
return polygon;
|
||||
world = new PreparedPart(polygon);
|
||||
preparedParts.Add(part, world);
|
||||
return world;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A part's world polygon and, once a pair first needs it, its triangulation. Both are
|
||||
/// shared by every later pair in this check and are never mutated.
|
||||
/// </summary>
|
||||
private sealed class PreparedPart
|
||||
{
|
||||
private List<Polygon> triangles;
|
||||
|
||||
public PreparedPart(Polygon polygon)
|
||||
{
|
||||
Polygon = polygon;
|
||||
Triangles = GetTriangles;
|
||||
}
|
||||
|
||||
public Polygon Polygon { get; }
|
||||
|
||||
public Func<List<Polygon>> Triangles { get; }
|
||||
|
||||
private List<Polygon> GetTriangles() => triangles ??= Collision.Triangulate(Polygon);
|
||||
}
|
||||
|
||||
private sealed class PreparedProgram
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace OpenNest
|
||||
private static long featureBitmaskCells;
|
||||
private static long crossingPointScans;
|
||||
private static long overlapPolygonPreparations;
|
||||
private static long polygonTriangulations;
|
||||
|
||||
public static long FindBestFits => Interlocked.Read(ref findBestFits);
|
||||
public static long OffsetPerimeterEntities => Interlocked.Read(ref offsetPerimeterEntities);
|
||||
@@ -28,6 +29,7 @@ namespace OpenNest
|
||||
public static long FeatureBitmaskCells => Interlocked.Read(ref featureBitmaskCells);
|
||||
public static long CrossingPointScans => Interlocked.Read(ref crossingPointScans);
|
||||
public static long OverlapPolygonPreparations => Interlocked.Read(ref overlapPolygonPreparations);
|
||||
public static long PolygonTriangulations => Interlocked.Read(ref polygonTriangulations);
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
public static void CountFindBestFits() => Interlocked.Increment(ref findBestFits);
|
||||
@@ -58,6 +60,9 @@ namespace OpenNest
|
||||
public static void CountOverlapPolygonPreparation() =>
|
||||
Interlocked.Increment(ref overlapPolygonPreparations);
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
public static void CountPolygonTriangulation() => Interlocked.Increment(ref polygonTriangulations);
|
||||
|
||||
public static void Reset()
|
||||
{
|
||||
Interlocked.Exchange(ref findBestFits, 0);
|
||||
@@ -69,6 +74,7 @@ namespace OpenNest
|
||||
Interlocked.Exchange(ref featureBitmaskCells, 0);
|
||||
Interlocked.Exchange(ref crossingPointScans, 0);
|
||||
Interlocked.Exchange(ref overlapPolygonPreparations, 0);
|
||||
Interlocked.Exchange(ref polygonTriangulations, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user