diff --git a/OpenNest.Engine/BestFit/ISlideComputer.cs b/OpenNest.Engine/BestFit/ISlideComputer.cs new file mode 100644 index 0000000..9096f9a --- /dev/null +++ b/OpenNest.Engine/BestFit/ISlideComputer.cs @@ -0,0 +1,38 @@ +using System; + +namespace OpenNest.Engine.BestFit +{ + /// + /// Batches directional-distance computations for multiple offset positions. + /// GPU implementations can process all offsets in a single kernel launch. + /// + public interface ISlideComputer : IDisposable + { + /// + /// Computes the minimum directional distance for each offset position. + /// + /// Flat array [x1,y1,x2,y2, ...] for stationary edges. + /// Number of line segments in stationarySegments. + /// Flat array [x1,y1,x2,y2, ...] for moving edges at origin. + /// Number of line segments in movingTemplateSegments. + /// Flat array [dx,dy, dx,dy, ...] of translation offsets. + /// Number of offset positions. + /// Push direction. + /// Array of minimum distances, one per offset position. + double[] ComputeBatch( + double[] stationarySegments, int stationaryCount, + double[] movingTemplateSegments, int movingCount, + double[] offsets, int offsetCount, + PushDirection direction); + + /// + /// Computes minimum directional distance for offsets with per-offset directions. + /// Uploads segment data once for all offsets, reducing GPU round-trips. + /// + double[] ComputeBatchMultiDir( + double[] stationarySegments, int stationaryCount, + double[] movingTemplateSegments, int movingCount, + double[] offsets, int offsetCount, + int[] directions); + } +} diff --git a/OpenNest.Gpu/GpuEvaluatorFactory.cs b/OpenNest.Gpu/GpuEvaluatorFactory.cs index e69edb2..9ee04f4 100644 --- a/OpenNest.Gpu/GpuEvaluatorFactory.cs +++ b/OpenNest.Gpu/GpuEvaluatorFactory.cs @@ -11,6 +11,8 @@ namespace OpenNest.Gpu private static bool _probed; private static bool _gpuAvailable; private static string _deviceName; + private static GpuSlideComputer _slideComputer; + private static readonly object _slideLock = new object(); public static bool GpuAvailable { @@ -46,6 +48,29 @@ namespace OpenNest.Gpu } } + public static ISlideComputer CreateSlideComputer() + { + if (!GpuAvailable) + return null; + + lock (_slideLock) + { + if (_slideComputer != null) + return _slideComputer; + + try + { + _slideComputer = new GpuSlideComputer(); + return _slideComputer; + } + catch (Exception ex) + { + Debug.WriteLine($"[GpuEvaluatorFactory] GPU slide computer failed: {ex.Message}"); + return null; + } + } + } + private static void Probe() { _probed = true; diff --git a/OpenNest.Gpu/GpuSlideComputer.cs b/OpenNest.Gpu/GpuSlideComputer.cs new file mode 100644 index 0000000..b6c2ea0 --- /dev/null +++ b/OpenNest.Gpu/GpuSlideComputer.cs @@ -0,0 +1,460 @@ +using System; +using ILGPU; +using ILGPU.Runtime; +using ILGPU.Algorithms; +using OpenNest.Engine.BestFit; + +namespace OpenNest.Gpu +{ + public class GpuSlideComputer : ISlideComputer + { + private readonly Context _context; + private readonly Accelerator _accelerator; + private readonly object _lock = new object(); + + // ── Kernels ────────────────────────────────────────────────── + + private readonly Action, // stationaryPrep + ArrayView1D, // movingPrep + ArrayView1D, // offsets + ArrayView1D, // results + int, int, int> _kernel; + + private readonly Action, // stationaryPrep + ArrayView1D, // movingPrep + ArrayView1D, // offsets + ArrayView1D, // results + ArrayView1D, // directions + int, int> _kernelMultiDir; + + private readonly Action, // raw + ArrayView1D, // prepared + int> _prepareKernel; + + // ── Buffers ────────────────────────────────────────────────── + + private MemoryBuffer1D? _gpuStationaryRaw; + private MemoryBuffer1D? _gpuStationaryPrep; + private double[]? _lastStationaryData; // Keep CPU copy/ref for content check + + private MemoryBuffer1D? _gpuMovingRaw; + private MemoryBuffer1D? _gpuMovingPrep; + private double[]? _lastMovingData; // Keep CPU copy/ref for content check + + private MemoryBuffer1D? _gpuOffsets; + private MemoryBuffer1D? _gpuResults; + private MemoryBuffer1D? _gpuDirs; + private int _offsetCapacity; + + public GpuSlideComputer() + { + _context = Context.CreateDefault(); + _accelerator = _context.GetPreferredDevice(preferCPU: false) + .CreateAccelerator(_context); + + _kernel = _accelerator.LoadAutoGroupedStreamKernel< + Index1D, + ArrayView1D, + ArrayView1D, + ArrayView1D, + ArrayView1D, + int, int, int>(SlideKernel); + + _kernelMultiDir = _accelerator.LoadAutoGroupedStreamKernel< + Index1D, + ArrayView1D, + ArrayView1D, + ArrayView1D, + ArrayView1D, + ArrayView1D, + int, int>(SlideKernelMultiDir); + + _prepareKernel = _accelerator.LoadAutoGroupedStreamKernel< + Index1D, + ArrayView1D, + ArrayView1D, + int>(PrepareKernel); + } + + public double[] ComputeBatch( + double[] stationarySegments, int stationaryCount, + double[] movingTemplateSegments, int movingCount, + double[] offsets, int offsetCount, + PushDirection direction) + { + var results = new double[offsetCount]; + if (offsetCount == 0 || stationaryCount == 0 || movingCount == 0) + { + Array.Fill(results, double.MaxValue); + return results; + } + + lock (_lock) + { + EnsureStationary(stationarySegments, stationaryCount); + EnsureMoving(movingTemplateSegments, movingCount); + EnsureOffsetBuffers(offsetCount); + + _gpuOffsets!.View.SubView(0, offsetCount * 2).CopyFromCPU(offsets); + + _kernel(offsetCount, + _gpuStationaryPrep!.View, _gpuMovingPrep!.View, + _gpuOffsets.View, _gpuResults!.View, + stationaryCount, movingCount, (int)direction); + + _accelerator.Synchronize(); + _gpuResults.View.SubView(0, offsetCount).CopyToCPU(results); + } + + return results; + } + + public double[] ComputeBatchMultiDir( + double[] stationarySegments, int stationaryCount, + double[] movingTemplateSegments, int movingCount, + double[] offsets, int offsetCount, + int[] directions) + { + var results = new double[offsetCount]; + if (offsetCount == 0 || stationaryCount == 0 || movingCount == 0) + { + Array.Fill(results, double.MaxValue); + return results; + } + + lock (_lock) + { + EnsureStationary(stationarySegments, stationaryCount); + EnsureMoving(movingTemplateSegments, movingCount); + EnsureOffsetBuffers(offsetCount); + + _gpuOffsets!.View.SubView(0, offsetCount * 2).CopyFromCPU(offsets); + _gpuDirs!.View.SubView(0, offsetCount).CopyFromCPU(directions); + + _kernelMultiDir(offsetCount, + _gpuStationaryPrep!.View, _gpuMovingPrep!.View, + _gpuOffsets.View, _gpuResults!.View, _gpuDirs.View, + stationaryCount, movingCount); + + _accelerator.Synchronize(); + _gpuResults.View.SubView(0, offsetCount).CopyToCPU(results); + } + + return results; + } + + public void InvalidateStationary() => _lastStationaryData = null; + public void InvalidateMoving() => _lastMovingData = null; + + private void EnsureStationary(double[] data, int count) + { + // Fast check: if same object or content is identical, skip upload + if (_gpuStationaryPrep != null && + _lastStationaryData != null && + _lastStationaryData.Length == data.Length) + { + // Reference equality or content equality + if (_lastStationaryData == data || + new ReadOnlySpan(_lastStationaryData).SequenceEqual(new ReadOnlySpan(data))) + { + return; + } + } + + _gpuStationaryRaw?.Dispose(); + _gpuStationaryPrep?.Dispose(); + + _gpuStationaryRaw = _accelerator.Allocate1D(data); + _gpuStationaryPrep = _accelerator.Allocate1D(count * 10); + + _prepareKernel(count, _gpuStationaryRaw.View, _gpuStationaryPrep.View, count); + _accelerator.Synchronize(); + + _lastStationaryData = data; // store reference for next comparison + } + + private void EnsureMoving(double[] data, int count) + { + if (_gpuMovingPrep != null && + _lastMovingData != null && + _lastMovingData.Length == data.Length) + { + if (_lastMovingData == data || + new ReadOnlySpan(_lastMovingData).SequenceEqual(new ReadOnlySpan(data))) + { + return; + } + } + + _gpuMovingRaw?.Dispose(); + _gpuMovingPrep?.Dispose(); + + _gpuMovingRaw = _accelerator.Allocate1D(data); + _gpuMovingPrep = _accelerator.Allocate1D(count * 10); + + _prepareKernel(count, _gpuMovingRaw.View, _gpuMovingPrep.View, count); + _accelerator.Synchronize(); + + _lastMovingData = data; + } + + private void EnsureOffsetBuffers(int offsetCount) + { + if (_offsetCapacity >= offsetCount) + return; + + var newCapacity = System.Math.Max(offsetCount, _offsetCapacity * 3 / 2); + + _gpuOffsets?.Dispose(); + _gpuResults?.Dispose(); + _gpuDirs?.Dispose(); + + _gpuOffsets = _accelerator.Allocate1D(newCapacity * 2); + _gpuResults = _accelerator.Allocate1D(newCapacity); + _gpuDirs = _accelerator.Allocate1D(newCapacity); + + _offsetCapacity = newCapacity; + } + + // ── Preparation Kernel ─────────────────────────────────────── + + private static void PrepareKernel( + Index1D index, + ArrayView1D raw, + ArrayView1D prepared, + int count) + { + if (index >= count) return; + var x1 = raw[index * 4 + 0]; + var y1 = raw[index * 4 + 1]; + var x2 = raw[index * 4 + 2]; + var y2 = raw[index * 4 + 3]; + + prepared[index * 10 + 0] = x1; + prepared[index * 10 + 1] = y1; + prepared[index * 10 + 2] = x2; + prepared[index * 10 + 3] = y2; + + var dx = x2 - x1; + var dy = y2 - y1; + + // invD is used for parameter 't'. We use a small epsilon for stability. + prepared[index * 10 + 4] = (XMath.Abs(dx) < 1e-9) ? 0 : 1.0 / dx; + prepared[index * 10 + 5] = (XMath.Abs(dy) < 1e-9) ? 0 : 1.0 / dy; + + prepared[index * 10 + 6] = XMath.Min(x1, x2); + prepared[index * 10 + 7] = XMath.Max(x1, x2); + prepared[index * 10 + 8] = XMath.Min(y1, y2); + prepared[index * 10 + 9] = XMath.Max(y1, y2); + } + + // ── Main Slide Kernels ─────────────────────────────────────── + + private static void SlideKernel( + Index1D index, + ArrayView1D stationaryPrep, + ArrayView1D movingPrep, + ArrayView1D offsets, + ArrayView1D results, + int sCount, int mCount, int direction) + { + if (index >= results.Length) return; + + var dx = offsets[index * 2]; + var dy = offsets[index * 2 + 1]; + + results[index] = ComputeSlideLean( + stationaryPrep, movingPrep, dx, dy, sCount, mCount, direction); + } + + private static void SlideKernelMultiDir( + Index1D index, + ArrayView1D stationaryPrep, + ArrayView1D movingPrep, + ArrayView1D offsets, + ArrayView1D results, + ArrayView1D directions, + int sCount, int mCount) + { + if (index >= results.Length) return; + + var dx = offsets[index * 2]; + var dy = offsets[index * 2 + 1]; + var dir = directions[index]; + + results[index] = ComputeSlideLean( + stationaryPrep, movingPrep, dx, dy, sCount, mCount, dir); + } + + private static double ComputeSlideLean( + ArrayView1D sPrep, + ArrayView1D mPrep, + double dx, double dy, int sCount, int mCount, int direction) + { + const double eps = 0.00001; + var minDist = double.MaxValue; + var horizontal = direction >= 2; + var oppDir = direction ^ 1; + + // ── Forward Pass: moving vertices vs stationary edges ───── + for (int i = 0; i < mCount; i++) + { + var m1x = mPrep[i * 10 + 0] + dx; + var m1y = mPrep[i * 10 + 1] + dy; + var m2x = mPrep[i * 10 + 2] + dx; + var m2y = mPrep[i * 10 + 3] + dy; + + for (int j = 0; j < sCount; j++) + { + var sMin = horizontal ? sPrep[j * 10 + 8] : sPrep[j * 10 + 6]; + var sMax = horizontal ? sPrep[j * 10 + 9] : sPrep[j * 10 + 7]; + + // Test moving vertex 1 against stationary edge j + var mv1 = horizontal ? m1y : m1x; + if (mv1 >= sMin - eps && mv1 <= sMax + eps) + { + var d = RayEdgeLean(m1x, m1y, sPrep, j, direction, eps); + if (d < minDist) minDist = d; + } + + // Test moving vertex 2 against stationary edge j + var mv2 = horizontal ? m2y : m2x; + if (mv2 >= sMin - eps && mv2 <= sMax + eps) + { + var d = RayEdgeLean(m2x, m2y, sPrep, j, direction, eps); + if (d < minDist) minDist = d; + } + } + } + + // ── Reverse Pass: stationary vertices vs moving edges ───── + for (int i = 0; i < sCount; i++) + { + var s1x = sPrep[i * 10 + 0]; + var s1y = sPrep[i * 10 + 1]; + var s2x = sPrep[i * 10 + 2]; + var s2y = sPrep[i * 10 + 3]; + + for (int j = 0; j < mCount; j++) + { + var mMin = horizontal ? (mPrep[j * 10 + 8] + dy) : (mPrep[j * 10 + 6] + dx); + var mMax = horizontal ? (mPrep[j * 10 + 9] + dy) : (mPrep[j * 10 + 7] + dx); + + // Test stationary vertex 1 against moving edge j + var sv1 = horizontal ? s1y : s1x; + if (sv1 >= mMin - eps && sv1 <= mMax + eps) + { + var d = RayEdgeLeanMoving(s1x, s1y, mPrep, j, dx, dy, oppDir, eps); + if (d < minDist) minDist = d; + } + + // Test stationary vertex 2 against moving edge j + var sv2 = horizontal ? s2y : s2x; + if (sv2 >= mMin - eps && sv2 <= mMax + eps) + { + var d = RayEdgeLeanMoving(s2x, s2y, mPrep, j, dx, dy, oppDir, eps); + if (d < minDist) minDist = d; + } + } + } + + return minDist; + } + + private static double RayEdgeLean( + double vx, double vy, + ArrayView1D sPrep, int j, + int direction, double eps) + { + var p1x = sPrep[j * 10 + 0]; + var p1y = sPrep[j * 10 + 1]; + var p2x = sPrep[j * 10 + 2]; + var p2y = sPrep[j * 10 + 3]; + + if (direction >= 2) // Horizontal (Left=2, Right=3) + { + var invDy = sPrep[j * 10 + 5]; + if (invDy == 0) return double.MaxValue; + + var t = (vy - p1y) * invDy; + if (t < -eps || t > 1.0 + eps) return double.MaxValue; + + var ix = p1x + t * (p2x - p1x); + var dist = (direction == 2) ? (vx - ix) : (ix - vx); + + if (dist > eps) return dist; + return (dist >= -eps) ? 0.0 : double.MaxValue; + } + else // Vertical (Up=0, Down=1) + { + var invDx = sPrep[j * 10 + 4]; + if (invDx == 0) return double.MaxValue; + + var t = (vx - p1x) * invDx; + if (t < -eps || t > 1.0 + eps) return double.MaxValue; + + var iy = p1y + t * (p2y - p1y); + var dist = (direction == 1) ? (vy - iy) : (iy - vy); + + if (dist > eps) return dist; + return (dist >= -eps) ? 0.0 : double.MaxValue; + } + } + + private static double RayEdgeLeanMoving( + double vx, double vy, + ArrayView1D mPrep, int j, + double dx, double dy, int direction, double eps) + { + var p1x = mPrep[j * 10 + 0] + dx; + var p1y = mPrep[j * 10 + 1] + dy; + var p2x = mPrep[j * 10 + 2] + dx; + var p2y = mPrep[j * 10 + 3] + dy; + + if (direction >= 2) // Horizontal + { + var invDy = mPrep[j * 10 + 5]; + if (invDy == 0) return double.MaxValue; + + var t = (vy - p1y) * invDy; + if (t < -eps || t > 1.0 + eps) return double.MaxValue; + + var ix = p1x + t * (p2x - p1x); + var dist = (direction == 2) ? (vx - ix) : (ix - vx); + + if (dist > eps) return dist; + return (dist >= -eps) ? 0.0 : double.MaxValue; + } + else // Vertical + { + var invDx = mPrep[j * 10 + 4]; + if (invDx == 0) return double.MaxValue; + + var t = (vx - p1x) * invDx; + if (t < -eps || t > 1.0 + eps) return double.MaxValue; + + var iy = p1y + t * (p2y - p1y); + var dist = (direction == 1) ? (vy - iy) : (iy - vy); + + if (dist > eps) return dist; + return (dist >= -eps) ? 0.0 : double.MaxValue; + } + } + + public void Dispose() + { + _gpuStationaryRaw?.Dispose(); + _gpuStationaryPrep?.Dispose(); + _gpuMovingRaw?.Dispose(); + _gpuMovingPrep?.Dispose(); + _gpuOffsets?.Dispose(); + _gpuResults?.Dispose(); + _gpuDirs?.Dispose(); + _accelerator?.Dispose(); + _context?.Dispose(); + } + } +}