fix: correct GPU overlap detection coordinate system mismatch
The GPU pair evaluator reported false-positive overlaps for all candidates because the bitmap coordinate system didn't account for Part.CreateAtOrigin's Location offset. When rotation produced negative coordinates, CreateAtOrigin sets Location = -bbox.Location (non-zero), but the offset formula assumed Location was always (0,0). Two fixes: - Rasterize bitmaps from Part.CreateAtOrigin directly (new FromPart method) instead of separately rotating polygons and computing bbox, eliminating any Polygon.Rotate vs Program.Rotate mismatch - Correct offset formula to include the Location shift: (Part2Offset - partB.Location) instead of raw Part2Offset Also optimized post-kernel bounding computation: pre-compute vertices once per rotation group and process results with Parallel.For, matching the CPU evaluator's concurrency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using ILGPU;
|
using ILGPU;
|
||||||
using ILGPU.Runtime;
|
using ILGPU.Runtime;
|
||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
@@ -33,13 +34,17 @@ namespace OpenNest.Gpu
|
|||||||
if (candidates.Count == 0)
|
if (candidates.Count == 0)
|
||||||
return new List<BestFitResult>();
|
return new List<BestFitResult>();
|
||||||
|
|
||||||
// No dilation — candidate positions already include spacing
|
// Rasterize A from a Part created at the origin — guarantees the
|
||||||
// (baked in by RotationSlideStrategy via half-spacing offset lines).
|
// bitmap coordinate system exactly matches Part.CreateAtOrigin.
|
||||||
var bitmapA = PartBitmap.FromDrawing(_drawing, _cellSize);
|
var partA = Part.CreateAtOrigin(_drawing);
|
||||||
|
var bitmapA = PartBitmap.FromPart(partA, _cellSize);
|
||||||
|
|
||||||
if (bitmapA.Width == 0 || bitmapA.Height == 0)
|
if (bitmapA.Width == 0 || bitmapA.Height == 0)
|
||||||
return candidates.Select(c => MakeEmptyResult(c)).ToList();
|
return candidates.Select(c => MakeEmptyResult(c)).ToList();
|
||||||
|
|
||||||
|
// Pre-compute A vertices once for all rotation groups
|
||||||
|
var verticesA = GetPartVertices(partA);
|
||||||
|
|
||||||
// Group candidates by Part2Rotation so we rasterize B once per unique rotation
|
// Group candidates by Part2Rotation so we rasterize B once per unique rotation
|
||||||
var groups = candidates
|
var groups = candidates
|
||||||
.Select((c, i) => new { Candidate = c, OriginalIndex = i })
|
.Select((c, i) => new { Candidate = c, OriginalIndex = i })
|
||||||
@@ -53,8 +58,9 @@ namespace OpenNest.Gpu
|
|||||||
var rotation = group.Key;
|
var rotation = group.Key;
|
||||||
var groupItems = group.ToList();
|
var groupItems = group.ToList();
|
||||||
|
|
||||||
// Rasterize B at this rotation
|
// Rasterize B at this rotation from a Part created at origin
|
||||||
var bitmapB = PartBitmap.FromDrawingRotated(_drawing, rotation, _cellSize);
|
var partB = Part.CreateAtOrigin(_drawing, rotation);
|
||||||
|
var bitmapB = PartBitmap.FromPart(partB, _cellSize);
|
||||||
|
|
||||||
if (bitmapB.Width == 0 || bitmapB.Height == 0)
|
if (bitmapB.Width == 0 || bitmapB.Height == 0)
|
||||||
{
|
{
|
||||||
@@ -63,6 +69,10 @@ namespace OpenNest.Gpu
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-compute B vertices at origin for this rotation group
|
||||||
|
var verticesB = GetPartVertices(partB);
|
||||||
|
var locationB = partB.Location;
|
||||||
|
|
||||||
// Use the max dimensions so both bitmaps fit on the same grid
|
// Use the max dimensions so both bitmaps fit on the same grid
|
||||||
var gridWidth = System.Math.Max(bitmapA.Width, bitmapB.Width);
|
var gridWidth = System.Math.Max(bitmapA.Width, bitmapB.Width);
|
||||||
var gridHeight = System.Math.Max(bitmapA.Height, bitmapB.Height);
|
var gridHeight = System.Math.Max(bitmapA.Height, bitmapB.Height);
|
||||||
@@ -70,20 +80,23 @@ namespace OpenNest.Gpu
|
|||||||
var paddedA = PadBitmap(bitmapA, gridWidth, gridHeight);
|
var paddedA = PadBitmap(bitmapA, gridWidth, gridHeight);
|
||||||
var paddedB = PadBitmap(bitmapB, gridWidth, gridHeight);
|
var paddedB = PadBitmap(bitmapB, gridWidth, gridHeight);
|
||||||
|
|
||||||
// Pack candidate offsets: convert world offset to cell offset
|
// The CPU evaluator replaces partB.Location with Part2Offset,
|
||||||
|
// so the world-space shift from B's bitmap position is
|
||||||
|
// (Part2Offset - partB.Location). We convert that shift to
|
||||||
|
// pixel coordinates, adjusting for the bitmap origin difference.
|
||||||
var candidateCount = groupItems.Count;
|
var candidateCount = groupItems.Count;
|
||||||
var offsets = new float[candidateCount * 3];
|
var offsets = new int[candidateCount * 2];
|
||||||
|
|
||||||
for (var i = 0; i < candidateCount; i++)
|
for (var i = 0; i < candidateCount; i++)
|
||||||
{
|
{
|
||||||
var c = groupItems[i].Candidate;
|
var c = groupItems[i].Candidate;
|
||||||
// Convert world-space offset to cell-space offset relative to bitmapA origin
|
var shiftX = c.Part2Offset.X - locationB.X;
|
||||||
offsets[i * 3 + 0] = (float)((c.Part2Offset.X - bitmapA.OriginX + bitmapB.OriginX) / _cellSize);
|
var shiftY = c.Part2Offset.Y - locationB.Y;
|
||||||
offsets[i * 3 + 1] = (float)((c.Part2Offset.Y - bitmapA.OriginY + bitmapB.OriginY) / _cellSize);
|
offsets[i * 2 + 0] = (int)System.Math.Round((shiftX + bitmapB.OriginX - bitmapA.OriginX) / _cellSize);
|
||||||
offsets[i * 3 + 2] = (float)c.Part2Rotation;
|
offsets[i * 2 + 1] = (int)System.Math.Round((shiftY + bitmapB.OriginY - bitmapA.OriginY) / _cellSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
var resultScores = new float[candidateCount];
|
var resultScores = new int[candidateCount];
|
||||||
|
|
||||||
using var gpuPaddedA = _accelerator.Allocate1D(paddedA);
|
using var gpuPaddedA = _accelerator.Allocate1D(paddedA);
|
||||||
using var gpuPaddedB = _accelerator.Allocate1D(paddedB);
|
using var gpuPaddedB = _accelerator.Allocate1D(paddedB);
|
||||||
@@ -94,9 +107,9 @@ namespace OpenNest.Gpu
|
|||||||
Index1D,
|
Index1D,
|
||||||
ArrayView1D<int, Stride1D.Dense>,
|
ArrayView1D<int, Stride1D.Dense>,
|
||||||
ArrayView1D<int, Stride1D.Dense>,
|
ArrayView1D<int, Stride1D.Dense>,
|
||||||
ArrayView1D<float, Stride1D.Dense>,
|
ArrayView1D<int, Stride1D.Dense>,
|
||||||
ArrayView1D<float, Stride1D.Dense>,
|
ArrayView1D<int, Stride1D.Dense>,
|
||||||
int, int>(NestingKernel);
|
int, int>(OverlapKernel);
|
||||||
|
|
||||||
kernel(candidateCount, gpuPaddedA.View, gpuPaddedB.View,
|
kernel(candidateCount, gpuPaddedA.View, gpuPaddedB.View,
|
||||||
gpuOffsets.View, gpuResults.View, gridWidth, gridHeight);
|
gpuOffsets.View, gpuResults.View, gridWidth, gridHeight);
|
||||||
@@ -104,12 +117,13 @@ namespace OpenNest.Gpu
|
|||||||
_accelerator.Synchronize();
|
_accelerator.Synchronize();
|
||||||
gpuResults.CopyToCPU(resultScores);
|
gpuResults.CopyToCPU(resultScores);
|
||||||
|
|
||||||
// Map results back — compute proper bounding metrics for valid candidates
|
// Process results in parallel — pre-computed vertices avoid
|
||||||
for (var i = 0; i < candidateCount; i++)
|
// per-candidate Part creation, and Parallel.For matches the
|
||||||
|
// CPU evaluator's Parallel.ForEach concurrency.
|
||||||
|
Parallel.For(0, candidateCount, i =>
|
||||||
{
|
{
|
||||||
var item = groupItems[i];
|
var item = groupItems[i];
|
||||||
var score = resultScores[i];
|
var hasOverlap = resultScores[i] > 0;
|
||||||
var hasOverlap = score <= 0f;
|
|
||||||
|
|
||||||
if (hasOverlap)
|
if (hasOverlap)
|
||||||
{
|
{
|
||||||
@@ -127,56 +141,53 @@ namespace OpenNest.Gpu
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
allResults[item.OriginalIndex] = ComputeBoundingResult(item.Candidate, trueArea);
|
allResults[item.OriginalIndex] = ComputeBoundingResult(
|
||||||
|
item.Candidate, trueArea, verticesA, verticesB, locationB);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return allResults.ToList();
|
return allResults.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void NestingKernel(
|
/// <summary>
|
||||||
|
/// Overlap kernel using integer cell offsets pre-rounded on the CPU.
|
||||||
|
/// Shares one A and one B bitmap across all candidates — only the
|
||||||
|
/// integer offset varies per candidate.
|
||||||
|
/// </summary>
|
||||||
|
private static void OverlapKernel(
|
||||||
Index1D index,
|
Index1D index,
|
||||||
ArrayView1D<int, Stride1D.Dense> partBitmapA,
|
ArrayView1D<int, Stride1D.Dense> partBitmapA,
|
||||||
ArrayView1D<int, Stride1D.Dense> partBitmapB,
|
ArrayView1D<int, Stride1D.Dense> partBitmapB,
|
||||||
ArrayView1D<float, Stride1D.Dense> candidateOffsets,
|
ArrayView1D<int, Stride1D.Dense> candidateOffsets,
|
||||||
ArrayView1D<float, Stride1D.Dense> results,
|
ArrayView1D<int, Stride1D.Dense> results,
|
||||||
int gridWidth,
|
int gridWidth,
|
||||||
int gridHeight)
|
int gridHeight)
|
||||||
{
|
{
|
||||||
var candidateIdx = index * 3;
|
var offsetX = candidateOffsets[index * 2];
|
||||||
var offsetX = candidateOffsets[candidateIdx];
|
var offsetY = candidateOffsets[index * 2 + 1];
|
||||||
var offsetY = candidateOffsets[candidateIdx + 1];
|
|
||||||
// rotation is already baked into partBitmapB, offset is what matters
|
|
||||||
|
|
||||||
var overlapCount = 0;
|
var overlapCount = 0;
|
||||||
var totalOccupied = 0;
|
|
||||||
|
|
||||||
for (var y = 0; y < gridHeight; y++)
|
for (var y = 0; y < gridHeight; y++)
|
||||||
{
|
{
|
||||||
for (var x = 0; x < gridWidth; x++)
|
for (var x = 0; x < gridWidth; x++)
|
||||||
{
|
{
|
||||||
var cellA = partBitmapA[y * gridWidth + x];
|
var cellA = partBitmapA[y * gridWidth + x];
|
||||||
|
if (cellA != 1) continue;
|
||||||
|
|
||||||
// Apply offset to look up part B's cell
|
var bx = x - offsetX;
|
||||||
var bx = (int)(x - offsetX);
|
var by = y - offsetY;
|
||||||
var by = (int)(y - offsetY);
|
|
||||||
|
|
||||||
var cellB = 0;
|
|
||||||
if (bx >= 0 && bx < gridWidth && by >= 0 && by < gridHeight)
|
if (bx >= 0 && bx < gridWidth && by >= 0 && by < gridHeight)
|
||||||
cellB = partBitmapB[by * gridWidth + bx];
|
{
|
||||||
|
if (partBitmapB[by * gridWidth + bx] == 1)
|
||||||
if (cellA == 1 && cellB == 1)
|
overlapCount++;
|
||||||
overlapCount++;
|
}
|
||||||
if (cellA == 1 || cellB == 1)
|
|
||||||
totalOccupied++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (overlapCount > 0)
|
results[index] = overlapCount;
|
||||||
results[index] = 0f;
|
|
||||||
else
|
|
||||||
results[index] = (float)totalOccupied / (gridWidth * gridHeight);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int[] PadBitmap(PartBitmap bitmap, int targetWidth, int targetHeight)
|
private static int[] PadBitmap(PartBitmap bitmap, int targetWidth, int targetHeight)
|
||||||
@@ -199,23 +210,17 @@ namespace OpenNest.Gpu
|
|||||||
|
|
||||||
private const double ChordTolerance = 0.01;
|
private const double ChordTolerance = 0.01;
|
||||||
|
|
||||||
private BestFitResult ComputeBoundingResult(PairCandidate candidate, double trueArea)
|
private static BestFitResult ComputeBoundingResult(
|
||||||
|
PairCandidate candidate, double trueArea,
|
||||||
|
List<Vector> verticesA, List<Vector> verticesB, Vector locationB)
|
||||||
{
|
{
|
||||||
var part1 = new Part(_drawing);
|
var shift = candidate.Part2Offset - locationB;
|
||||||
var bbox1 = part1.Program.BoundingBox();
|
|
||||||
part1.Offset(-bbox1.Location.X, -bbox1.Location.Y);
|
|
||||||
part1.UpdateBounds();
|
|
||||||
|
|
||||||
var part2 = new Part(_drawing);
|
var allPoints = new List<Vector>(verticesA.Count + verticesB.Count);
|
||||||
if (!candidate.Part2Rotation.IsEqualTo(0))
|
allPoints.AddRange(verticesA);
|
||||||
part2.Rotate(candidate.Part2Rotation);
|
|
||||||
var bbox2 = part2.Program.BoundingBox();
|
|
||||||
part2.Offset(-bbox2.Location.X, -bbox2.Location.Y);
|
|
||||||
part2.Location = candidate.Part2Offset;
|
|
||||||
part2.UpdateBounds();
|
|
||||||
|
|
||||||
var allPoints = GetPartVertices(part1);
|
foreach (var v in verticesB)
|
||||||
allPoints.AddRange(GetPartVertices(part2));
|
allPoints.Add(v + shift);
|
||||||
|
|
||||||
double bestArea, bestWidth, bestHeight, bestRotation;
|
double bestArea, bestWidth, bestHeight, bestRotation;
|
||||||
|
|
||||||
@@ -230,10 +235,9 @@ namespace OpenNest.Gpu
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var combinedBox = ((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
|
bestArea = 0;
|
||||||
bestArea = combinedBox.Area();
|
bestWidth = 0;
|
||||||
bestWidth = combinedBox.Width;
|
bestHeight = 0;
|
||||||
bestHeight = combinedBox.Height;
|
|
||||||
bestRotation = 0;
|
bestRotation = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,34 @@ namespace OpenNest.Gpu
|
|||||||
return Rasterize(polygons, cellSize, spacingDilation);
|
return Rasterize(polygons, cellSize, spacingDilation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rasterizes a Part that was created with Part.CreateAtOrigin.
|
||||||
|
/// Extracts polygons from the Part's program and offsets by Location,
|
||||||
|
/// guaranteeing the bitmap matches the exact coordinate space used
|
||||||
|
/// by the CPU PairEvaluator.
|
||||||
|
/// </summary>
|
||||||
|
public static PartBitmap FromPart(Part part, double cellSize = DefaultCellSize)
|
||||||
|
{
|
||||||
|
var entities = ConvertProgram.ToGeometry(part.Program)
|
||||||
|
.Where(e => e.Layer != SpecialLayers.Rapid);
|
||||||
|
var shapes = Helper.GetShapes(entities);
|
||||||
|
|
||||||
|
var polygons = new List<Polygon>();
|
||||||
|
|
||||||
|
foreach (var shape in shapes)
|
||||||
|
{
|
||||||
|
if (!shape.IsClosed())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var polygon = shape.ToPolygonWithTolerance(0.05);
|
||||||
|
polygon.Close();
|
||||||
|
polygon.Offset(part.Location);
|
||||||
|
polygons.Add(polygon);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Rasterize(polygons, cellSize, 0);
|
||||||
|
}
|
||||||
|
|
||||||
private static PartBitmap Rasterize(List<Polygon> polygons, double cellSize, double spacingDilation)
|
private static PartBitmap Rasterize(List<Polygon> polygons, double cellSize, double spacingDilation)
|
||||||
{
|
{
|
||||||
if (polygons.Count == 0)
|
if (polygons.Count == 0)
|
||||||
@@ -126,6 +154,80 @@ namespace OpenNest.Gpu
|
|||||||
return polygons;
|
return polygons;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Blits two locally-rasterized bitmaps into a shared world-space grid.
|
||||||
|
/// B is placed at the given world-space offset. Returns two aligned int[]
|
||||||
|
/// arrays of identical dimensions with no fractional offset math.
|
||||||
|
/// </summary>
|
||||||
|
public static (int[] cellsA, int[] cellsB, int width, int height) BlitPair(
|
||||||
|
PartBitmap bitmapA, PartBitmap bitmapB, double offsetX, double offsetY)
|
||||||
|
{
|
||||||
|
var cellSize = bitmapA.CellSize;
|
||||||
|
|
||||||
|
// B's world-space origin when placed at the offset
|
||||||
|
var bWorldOriginX = bitmapB.OriginX + offsetX;
|
||||||
|
var bWorldOriginY = bitmapB.OriginY + offsetY;
|
||||||
|
|
||||||
|
// Combined world-space bounds
|
||||||
|
var combinedMinX = System.Math.Min(bitmapA.OriginX, bWorldOriginX);
|
||||||
|
var combinedMinY = System.Math.Min(bitmapA.OriginY, bWorldOriginY);
|
||||||
|
var combinedMaxX = System.Math.Max(
|
||||||
|
bitmapA.OriginX + bitmapA.Width * cellSize,
|
||||||
|
bWorldOriginX + bitmapB.Width * cellSize);
|
||||||
|
var combinedMaxY = System.Math.Max(
|
||||||
|
bitmapA.OriginY + bitmapA.Height * cellSize,
|
||||||
|
bWorldOriginY + bitmapB.Height * cellSize);
|
||||||
|
|
||||||
|
var sharedWidth = (int)System.Math.Ceiling((combinedMaxX - combinedMinX) / cellSize);
|
||||||
|
var sharedHeight = (int)System.Math.Ceiling((combinedMaxY - combinedMinY) / cellSize);
|
||||||
|
|
||||||
|
if (sharedWidth <= 0 || sharedHeight <= 0)
|
||||||
|
return (Array.Empty<int>(), Array.Empty<int>(), 0, 0);
|
||||||
|
|
||||||
|
var cellsA = new int[sharedWidth * sharedHeight];
|
||||||
|
var cellsB = new int[sharedWidth * sharedHeight];
|
||||||
|
|
||||||
|
// Integer cell offsets for A within the shared grid
|
||||||
|
var aOffX = (int)System.Math.Round((bitmapA.OriginX - combinedMinX) / cellSize);
|
||||||
|
var aOffY = (int)System.Math.Round((bitmapA.OriginY - combinedMinY) / cellSize);
|
||||||
|
|
||||||
|
// Integer cell offsets for B within the shared grid
|
||||||
|
var bOffX = (int)System.Math.Round((bWorldOriginX - combinedMinX) / cellSize);
|
||||||
|
var bOffY = (int)System.Math.Round((bWorldOriginY - combinedMinY) / cellSize);
|
||||||
|
|
||||||
|
// Blit A into the shared grid
|
||||||
|
for (var y = 0; y < bitmapA.Height; y++)
|
||||||
|
{
|
||||||
|
for (var x = 0; x < bitmapA.Width; x++)
|
||||||
|
{
|
||||||
|
if (bitmapA.Cells[y * bitmapA.Width + x] == 1)
|
||||||
|
{
|
||||||
|
var sx = aOffX + x;
|
||||||
|
var sy = aOffY + y;
|
||||||
|
if (sx >= 0 && sx < sharedWidth && sy >= 0 && sy < sharedHeight)
|
||||||
|
cellsA[sy * sharedWidth + sx] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blit B into the shared grid
|
||||||
|
for (var y = 0; y < bitmapB.Height; y++)
|
||||||
|
{
|
||||||
|
for (var x = 0; x < bitmapB.Width; x++)
|
||||||
|
{
|
||||||
|
if (bitmapB.Cells[y * bitmapB.Width + x] == 1)
|
||||||
|
{
|
||||||
|
var sx = bOffX + x;
|
||||||
|
var sy = bOffY + y;
|
||||||
|
if (sx >= 0 && sx < sharedWidth && sy >= 0 && sy < sharedHeight)
|
||||||
|
cellsB[sy * sharedWidth + sx] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (cellsA, cellsB, sharedWidth, sharedHeight);
|
||||||
|
}
|
||||||
|
|
||||||
private static void Dilate(int[] cells, int width, int height, int radius)
|
private static void Dilate(int[] cells, int width, int height, int radius)
|
||||||
{
|
{
|
||||||
var source = (int[])cells.Clone();
|
var source = (int[])cells.Clone();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Reflection;
|
|||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using OpenNest.Actions;
|
using OpenNest.Actions;
|
||||||
using OpenNest.Collections;
|
using OpenNest.Collections;
|
||||||
|
using OpenNest.Engine.BestFit;
|
||||||
using OpenNest.Gpu;
|
using OpenNest.Gpu;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.IO;
|
using OpenNest.IO;
|
||||||
@@ -41,6 +42,9 @@ namespace OpenNest.Forms
|
|||||||
EnableCheck();
|
EnableCheck();
|
||||||
UpdateStatus();
|
UpdateStatus();
|
||||||
UpdateGpuStatus();
|
UpdateGpuStatus();
|
||||||
|
|
||||||
|
if (GpuEvaluatorFactory.GpuAvailable)
|
||||||
|
BestFitCache.CreateEvaluator = (drawing, spacing) => GpuEvaluatorFactory.Create(drawing, spacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetNestName(DateTime date, int id)
|
private string GetNestName(DateTime date, int id)
|
||||||
|
|||||||
Reference in New Issue
Block a user