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:
aj
2026-09-27 13:49:46 -04:00
parent a27290a29c
commit 1b23ad79f2
8 changed files with 445 additions and 34 deletions
+74 -17
View File
@@ -167,6 +167,12 @@ public class PartOverlapCheckerTests
var expected = LegacyPartOverlap.WorldPolygon(p);
var actual = WorldPolygon(checker, p);
Assert.Equal(PolygonBits(expected), PolygonBits(actual));
// Cached triangles equal a fresh triangulation of the pre-change world polygon.
var expectedTriangles = Collision.Triangulate(expected);
var actualTriangles = WorldTriangles(checker, p);
Assert.Equal(expectedTriangles.Count, actualTriangles.Count);
for (var t = 0; t < expectedTriangles.Count; t++)
Assert.Equal(PolygonBits(expectedTriangles[t]), PolygonBits(actualTriangles[t]));
}
}
}
@@ -208,20 +214,7 @@ public class PartOverlapCheckerTests
[InlineData("pair", 2)]
public void Work_PreparesEachDistinctProgramOncePerCheck(string kind, long distinctPrograms)
{
// Rotated grids: neighbouring boxes overlap but parts do not, so every box-overlapping
// pair reaches an exact test and there is no early exit.
var filler = new FillLinear(new Box(3.1, 5.3, 96, 48), 0.5);
List<Part> grid;
if (kind == "single")
grid = filler.Fill(Fixture("arc"), 0.37, NestDirection.Horizontal);
else
{
var first = Part.CreateAtOrigin(Fixture("concave"), 0);
var second = Part.CreateAtOrigin(Fixture("concave"), System.Math.PI);
second.Offset(new Vector(first.Right + 0.5, first.Bottom));
grid = filler.Fill(FillHelpers.BuildRotatedPattern(new List<Part> { first, second }, 0.37),
NestDirection.Horizontal);
}
var grid = WorkGrid(kind);
Assert.Equal(distinctPrograms, grid.Select(g => g.Program).Distinct(ReferenceEqualityComparer.Instance).Count());
PerfCounters.Reset();
try
@@ -253,8 +246,65 @@ public class PartOverlapCheckerTests
PerfCounters.Reset();
}
}
[Theory]
[InlineData("single")]
[InlineData("pair")]
public void Work_TriangulatesEachPartAtMostOncePerCheck(string kind)
{
var grid = WorkGrid(kind);
// Parts that reach an exact test: their part boxes and their world polygon boxes overlap.
var reaching = new HashSet<Part>(ReferenceEqualityComparer.Instance);
var exactTests = 0;
ForEachBoxOverlappingPair(grid, (a, b) =>
{
if (!PolygonBoxesOverlap(LegacyPartOverlap.WorldPolygon(a), LegacyPartOverlap.WorldPolygon(b)))
return;
exactTests++;
reaching.Add(a);
reaching.Add(b);
});
PerfCounters.Reset();
try
{
ForEachBoxOverlappingPair(grid, (a, b) => a.Intersects(b, out _));
var legacy = PerfCounters.PolygonTriangulations;
PerfCounters.Reset();
Assert.False(FillHelpers.HasOverlappingParts(grid));
var actual = PerfCounters.PolygonTriangulations;
output.WriteLine($"triangulations {kind}: parts={grid.Count}; exact tests={exactTests}; parts reaching={reaching.Count}; old={legacy}; new={actual}");
Assert.True(exactTests > 2);
Assert.Equal(2L * exactTests, legacy);
Assert.Equal(reaching.Count, actual);
}
finally
{
PerfCounters.Reset();
}
}
#endif
private static List<Part> WorkGrid(string kind)
{
// Rotated grids: neighbouring boxes overlap but parts do not, so every box-overlapping
// pair reaches an exact test and there is no early exit.
var filler = new FillLinear(new Box(3.1, 5.3, 96, 48), 0.5);
if (kind == "single")
return filler.Fill(Fixture("arc"), 0.37, NestDirection.Horizontal);
var first = Part.CreateAtOrigin(Fixture("concave"), 0);
var second = Part.CreateAtOrigin(Fixture("concave"), System.Math.PI);
second.Offset(new Vector(first.Right + 0.5, first.Bottom));
return filler.Fill(FillHelpers.BuildRotatedPattern(new List<Part> { first, second }, 0.37),
NestDirection.Horizontal);
}
private static bool PolygonBoxesOverlap(Polygon a, Polygon b)
{
var overlapX = System.Math.Min(a.BoundingBox.Right, b.BoundingBox.Right) - System.Math.Max(a.BoundingBox.Left, b.BoundingBox.Left);
var overlapY = System.Math.Min(a.BoundingBox.Top, b.BoundingBox.Top) - System.Math.Max(a.BoundingBox.Bottom, b.BoundingBox.Bottom);
return overlapX > OpenNest.Math.Tolerance.Epsilon && overlapY > OpenNest.Math.Tolerance.Epsilon;
}
private static void AssertMatchesLegacy(List<Part> parts)
{
var legacy = Capture(() => LegacyPartOverlap.FillHelpersHasOverlappingParts(parts));
@@ -314,11 +364,18 @@ public class PartOverlapCheckerTests
}
}
private static Polygon WorldPolygon(PartOverlapChecker checker, Part part)
private static Polygon WorldPolygon(PartOverlapChecker checker, Part part) =>
(Polygon)PreparedPartMember(checker, part, "Polygon");
private static List<Polygon> WorldTriangles(PartOverlapChecker checker, Part part) =>
((Func<List<Polygon>>)PreparedPartMember(checker, part, "Triangles"))();
private static object PreparedPartMember(PartOverlapChecker checker, Part part, string property)
{
var field = typeof(PartOverlapChecker).GetField("worldPolygons",
var field = typeof(PartOverlapChecker).GetField("preparedParts",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
return ((Dictionary<Part, Polygon>)field.GetValue(checker)!)[part];
var prepared = ((System.Collections.IDictionary)field.GetValue(checker)!)[part]!;
return prepared.GetType().GetProperty(property)!.GetValue(prepared)!;
}
/// <summary>Every other part replaced by a copy moved by (-shift, -shift).</summary>