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:
@@ -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>
|
||||
|
||||
@@ -59,6 +59,87 @@ public class CollisionOverlapOnlyTests
|
||||
Assert.True(boxOverlapClear > 5000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Supplied triangulations, each prepared once and reused across many pairs as
|
||||
/// PartOverlapChecker does, give the same verdicts as the legacy per-call path and are
|
||||
/// never mutated by clipping or hole subtraction.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SeededPreparedTriangles_ReusedAcrossPairs_MatchLegacyVerdicts()
|
||||
{
|
||||
var random = new Random(28092026);
|
||||
var decisions = 0;
|
||||
var overlaps = 0;
|
||||
var boxOverlapClear = 0;
|
||||
for (var group = 0; group < 40; group++)
|
||||
{
|
||||
var polygons = new List<Polygon>();
|
||||
var shapeA = Make(random, group % 6);
|
||||
var shapeB = Make(random, (group / 6) % 6);
|
||||
for (var sample = 0; sample < 25; sample++)
|
||||
{
|
||||
// Offsets gives contact/containment placements within each (A, B) pair. Pulling
|
||||
// every pair into one ~8-unit window makes most cross-pair tests reach clipping
|
||||
// too, so each cached triangulation is reused against many near neighbours.
|
||||
var (ax, ay, bx, by) = Offsets(random, shapeA, shapeB, sample);
|
||||
var dx = random.NextDouble() * 4 - ax;
|
||||
var dy = random.NextDouble() * 4 - ay;
|
||||
polygons.Add(Move(shapeA, ax + dx, ay + dy));
|
||||
polygons.Add(Move(shapeB, bx + dx, by + dy));
|
||||
}
|
||||
var holes = group % 4 == 0
|
||||
? polygons.Select(p => new List<Polygon> { Move(Square(0.5), p.BoundingBox.Left + 0.2, p.BoundingBox.Bottom + 0.2) }).ToList()
|
||||
: null;
|
||||
var triangles = polygons.Select(p => new Lazy<List<Polygon>>(() => Collision.Triangulate(p))).ToArray();
|
||||
for (var i = 0; i < polygons.Count; i++)
|
||||
for (var j = i + 1; j < polygons.Count; j++)
|
||||
{
|
||||
var expected = LegacyCollision.HasOverlap(polygons[i], polygons[j], holes?[i], holes?[j]);
|
||||
var ti = triangles[i];
|
||||
var tj = triangles[j];
|
||||
var actual = Collision.HasOverlap(polygons[i], () => ti.Value, polygons[j], () => tj.Value, holes?[i], holes?[j]);
|
||||
Assert.True(expected == actual, $"group={group} i={i} j={j} expected={expected}");
|
||||
decisions++;
|
||||
if (expected)
|
||||
overlaps++;
|
||||
else if (BoxesOverlap(polygons[i].BoundingBox, polygons[j].BoundingBox))
|
||||
boxOverlapClear++;
|
||||
}
|
||||
for (var i = 0; i < polygons.Count; i++)
|
||||
{
|
||||
if (!triangles[i].IsValueCreated)
|
||||
continue;
|
||||
// Reused triangles must still equal a fresh triangulation, bit for bit.
|
||||
Assert.Equal(TriangleBits(Collision.Triangulate(polygons[i])), TriangleBits(triangles[i].Value));
|
||||
}
|
||||
}
|
||||
output.WriteLine($"prepared-triangle decisions={decisions}; overlaps={overlaps}; bbox-overlap but clear={boxOverlapClear}");
|
||||
Assert.Equal(40 * 50 * 49 / 2, decisions);
|
||||
Assert.True(overlaps > 10000);
|
||||
Assert.True(boxOverlapClear > 2000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreparedTriangles_AreNotResolvedWhenBoundingBoxesMiss()
|
||||
{
|
||||
var a = Square(2);
|
||||
var b = Move(Square(2), 10, 10);
|
||||
Assert.False(Collision.HasOverlap(a, () => throw new InvalidOperationException("A"), b,
|
||||
() => throw new InvalidOperationException("B")));
|
||||
var c = Move(Square(2), 1, 1);
|
||||
var resolved = new List<string>();
|
||||
Assert.True(Collision.HasOverlap(a, () => { resolved.Add("a"); return Collision.Triangulate(a); }, c,
|
||||
() => { resolved.Add("c"); return Collision.Triangulate(c); }));
|
||||
Assert.Equal(new[] { "a", "c" }, resolved);
|
||||
}
|
||||
|
||||
private static long[] TriangleBits(List<Polygon> triangles) => triangles
|
||||
.SelectMany(t => new[] { t.BoundingBox.X, t.BoundingBox.Y, t.BoundingBox.Length, t.BoundingBox.Width }
|
||||
.Concat(t.Vertices.SelectMany(v => new[] { v.X, v.Y })))
|
||||
.Select(BitConverter.DoubleToInt64Bits)
|
||||
.Prepend(triangles.Count)
|
||||
.ToArray();
|
||||
|
||||
[Fact]
|
||||
public void SeededCheck_MatchesLegacyBitwise()
|
||||
{
|
||||
@@ -155,11 +236,17 @@ public class CollisionOverlapOnlyTests
|
||||
Assert.ThrowsAny<Exception>(() => LegacyCollision.Check(a, b));
|
||||
Assert.ThrowsAny<Exception>(() => Collision.HasOverlap(a, b));
|
||||
Assert.ThrowsAny<Exception>(() => Collision.Check(a, b));
|
||||
Assert.ThrowsAny<Exception>(() => Collision.HasOverlap(a, () => Collision.Triangulate(a), b,
|
||||
() => Collision.Triangulate(b)));
|
||||
|
||||
var far = Move(Make(new Random(5), 0), 100, 100);
|
||||
var broken = nullFirst ? a : b;
|
||||
Assert.False(LegacyCollision.Check(broken, far).Overlaps);
|
||||
Assert.False(Collision.HasOverlap(broken, far));
|
||||
var resolved = 0;
|
||||
Assert.False(Collision.HasOverlap(broken, () => { resolved++; return Collision.Triangulate(broken); }, far,
|
||||
() => { resolved++; return Collision.Triangulate(far); }));
|
||||
Assert.Equal(0, resolved);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
Reference in New Issue
Block a user