@gridspace/raster-path 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -5
- package/build/app.js +363 -39
- package/build/index.html +39 -1
- package/build/raster-path.js +13 -13
- package/build/style.css +65 -0
- package/build/webgpu-worker.js +475 -686
- package/package.json +6 -2
- package/scripts/build-shaders.js +1 -1
- package/src/index.js +13 -13
- package/src/shaders/{radial-raster-v2.wgsl → radial-raster.wgsl} +8 -2
- package/src/test/batch-divisor-benchmark.cjs +286 -0
- package/src/test/lathe-cylinder-2-debug.cjs +334 -0
- package/src/test/lathe-cylinder-2-test.cjs +157 -0
- package/src/test/lathe-cylinder-test.cjs +198 -0
- package/src/test/work-estimation-profile.cjs +406 -0
- package/src/test/workload-calculator-demo.cjs +113 -0
- package/src/test/workload-calibration.cjs +310 -0
- package/src/web/app.js +363 -39
- package/src/web/index.html +39 -1
- package/src/web/style.css +65 -0
- package/src/web/webgpu-worker.js +470 -687
- package/src/workload-calculator.js +318 -0
package/src/web/webgpu-worker.js
CHANGED
|
@@ -48,27 +48,33 @@
|
|
|
48
48
|
* ═══════════════════════════════════════════════════════════════════════════
|
|
49
49
|
*/
|
|
50
50
|
|
|
51
|
+
let config = {};
|
|
51
52
|
let device = null;
|
|
53
|
+
let deviceCapabilities = null;
|
|
52
54
|
let isInitialized = false;
|
|
53
55
|
let cachedRasterizePipeline = null;
|
|
54
56
|
let cachedRasterizeShaderModule = null;
|
|
55
57
|
let cachedToolpathPipeline = null;
|
|
56
58
|
let cachedToolpathShaderModule = null;
|
|
57
|
-
let
|
|
58
|
-
let
|
|
59
|
+
let cachedRadialBatchPipeline = null;
|
|
60
|
+
let cachedRadialBatchShaderModule = null;
|
|
61
|
+
let lastlog;
|
|
59
62
|
|
|
60
63
|
const EMPTY_CELL = -1e10;
|
|
61
|
-
const log_pre = '[
|
|
62
|
-
|
|
63
|
-
// url params to control logging
|
|
64
|
-
let { search } = self.location;
|
|
65
|
-
let verbose = search.indexOf('debug') >= 0;
|
|
66
|
-
let quiet = search.indexOf('quiet') >= 0;
|
|
64
|
+
const log_pre = '[Worker]';
|
|
65
|
+
const diagnostic = false;
|
|
67
66
|
|
|
68
67
|
const debug = {
|
|
69
68
|
error: function() { console.error(log_pre, ...arguments) },
|
|
70
69
|
warn: function() { console.warn(log_pre, ...arguments) },
|
|
71
|
-
log: function() {
|
|
70
|
+
log: function() {
|
|
71
|
+
if (!config.quiet) {
|
|
72
|
+
let now = performance.now();
|
|
73
|
+
let since = ((now - (lastlog ?? now)) | 0).toString().padStart(4,' ');
|
|
74
|
+
console.log(log_pre, `[${since}]`, ...arguments);
|
|
75
|
+
lastlog = now;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
72
78
|
ok: function() { console.log(log_pre, '✅', ...arguments) },
|
|
73
79
|
};
|
|
74
80
|
|
|
@@ -140,6 +146,15 @@ async function initWebGPU() {
|
|
|
140
146
|
compute: { module: cachedToolpathShaderModule, entryPoint: 'main' },
|
|
141
147
|
});
|
|
142
148
|
|
|
149
|
+
// Pre-compile radial batch shader module
|
|
150
|
+
cachedRadialBatchShaderModule = device.createShaderModule({ code: radialRasterizeShaderCode });
|
|
151
|
+
|
|
152
|
+
// Pre-create radial batch pipeline
|
|
153
|
+
cachedRadialBatchPipeline = device.createComputePipeline({
|
|
154
|
+
layout: 'auto',
|
|
155
|
+
compute: { module: cachedRadialBatchShaderModule, entryPoint: 'main' },
|
|
156
|
+
});
|
|
157
|
+
|
|
143
158
|
// Store device capabilities
|
|
144
159
|
deviceCapabilities = {
|
|
145
160
|
maxStorageBufferBindingSize: device.limits.maxStorageBufferBindingSize,
|
|
@@ -158,13 +173,13 @@ async function initWebGPU() {
|
|
|
158
173
|
}
|
|
159
174
|
|
|
160
175
|
// Planar rasterization with spatial partitioning
|
|
161
|
-
const rasterizeShaderCode =
|
|
176
|
+
const rasterizeShaderCode = 'SHADER:planar-rasterize';
|
|
162
177
|
|
|
163
178
|
// Planar toolpath generation
|
|
164
|
-
const toolpathShaderCode =
|
|
179
|
+
const toolpathShaderCode = 'SHADER:planar-toolpath';
|
|
165
180
|
|
|
166
|
-
// Radial
|
|
167
|
-
const
|
|
181
|
+
// Radial: Rasterization with rotating ray planes and X-bucketing
|
|
182
|
+
const radialRasterizeShaderCode = 'SHADER:radial-raster';
|
|
168
183
|
|
|
169
184
|
// Calculate bounding box from triangle vertices
|
|
170
185
|
function calculateBounds(triangles) {
|
|
@@ -196,8 +211,6 @@ function buildSpatialGrid(triangles, bounds, cellSize = 5.0) {
|
|
|
196
211
|
const gridHeight = Math.max(1, Math.ceil((bounds.max.y - bounds.min.y) / cellSize));
|
|
197
212
|
const totalCells = gridWidth * gridHeight;
|
|
198
213
|
|
|
199
|
-
// debug.log(`Building spatial grid ${gridWidth}x${gridHeight} (${cellSize}mm cells)`);
|
|
200
|
-
|
|
201
214
|
const grid = new Array(totalCells);
|
|
202
215
|
for (let i = 0; i < totalCells; i++) {
|
|
203
216
|
grid[i] = [];
|
|
@@ -254,7 +267,13 @@ function buildSpatialGrid(triangles, bounds, cellSize = 5.0) {
|
|
|
254
267
|
cellOffsets[totalCells] = currentOffset;
|
|
255
268
|
|
|
256
269
|
const avgPerCell = totalTriangleRefs / totalCells;
|
|
257
|
-
|
|
270
|
+
|
|
271
|
+
// Calculate actual tool diameter from bounds for logging
|
|
272
|
+
const toolWidth = bounds.max.x - bounds.min.x;
|
|
273
|
+
const toolHeight = bounds.max.y - bounds.min.y;
|
|
274
|
+
const toolDiameter = Math.max(toolWidth, toolHeight);
|
|
275
|
+
|
|
276
|
+
debug.log(`Spatial grid: ${gridWidth}x${gridHeight} ${totalTriangleRefs} tri-refs ~${avgPerCell.toFixed(0)}/${cellSize}mm cell (tool: ${toolDiameter.toFixed(2)}mm)`);
|
|
258
277
|
|
|
259
278
|
return {
|
|
260
279
|
gridWidth,
|
|
@@ -283,7 +302,7 @@ async function rasterizeMeshSingle(triangles, stepSize, filterMode, options = {}
|
|
|
283
302
|
debug.log(`First-time init: ${(initEnd - initStart).toFixed(1)}ms`);
|
|
284
303
|
}
|
|
285
304
|
|
|
286
|
-
// debug.log(`
|
|
305
|
+
// debug.log(`Raster ${triangles.length / 9} triangles (step ${stepSize}mm, mode ${filterMode})...`);
|
|
287
306
|
|
|
288
307
|
// Extract options
|
|
289
308
|
// boundsOverride: Optional manual bounds to avoid recalculating from triangles
|
|
@@ -314,14 +333,16 @@ async function rasterizeMeshSingle(triangles, stepSize, filterMode, options = {}
|
|
|
314
333
|
const floatsPerPoint = filterMode === 0 ? 1 : 3;
|
|
315
334
|
const outputSize = totalGridPoints * floatsPerPoint * 4;
|
|
316
335
|
const maxBufferSize = device.limits.maxBufferSize || 268435456; // 256MB default
|
|
317
|
-
const modeStr = filterMode === 0 ? 'terrain (dense Z-only)' : 'tool (sparse XYZ)';
|
|
336
|
+
// const modeStr = filterMode === 0 ? 'terrain (dense Z-only)' : 'tool (sparse XYZ)';
|
|
318
337
|
// debug.log(`Output buffer size: ${(outputSize / 1024 / 1024).toFixed(2)} MB for ${modeStr} (max: ${(maxBufferSize / 1024 / 1024).toFixed(2)} MB)`);
|
|
319
338
|
|
|
320
339
|
if (outputSize > maxBufferSize) {
|
|
321
340
|
throw new Error(`Output buffer too large: ${(outputSize / 1024 / 1024).toFixed(2)} MB exceeds device limit of ${(maxBufferSize / 1024 / 1024).toFixed(2)} MB. Try a larger step size.`);
|
|
322
341
|
}
|
|
323
342
|
|
|
343
|
+
console.time(`${log_pre} Build Spatial Grid`);
|
|
324
344
|
const spatialGrid = buildSpatialGrid(triangles, bounds);
|
|
345
|
+
console.timeEnd(`${log_pre} Build Spatial Grid`);
|
|
325
346
|
|
|
326
347
|
// Create buffers
|
|
327
348
|
const triangleBuffer = device.createBuffer({
|
|
@@ -462,7 +483,7 @@ async function rasterizeMeshSingle(triangles, stepSize, filterMode, options = {}
|
|
|
462
483
|
result = new Float32Array(outputData);
|
|
463
484
|
pointCount = totalGridPoints;
|
|
464
485
|
|
|
465
|
-
if (
|
|
486
|
+
if (config.debug) {
|
|
466
487
|
// Count valid points for logging (sentinel value = -1e10)
|
|
467
488
|
let zeroCount = 0;
|
|
468
489
|
let validCount = 0;
|
|
@@ -802,7 +823,6 @@ async function rasterizeMesh(triangles, stepSize, filterMode, options = {}) {
|
|
|
802
823
|
|
|
803
824
|
const tileResult = await rasterizeMeshSingle(triangles, stepSize, filterMode, {
|
|
804
825
|
...tiles[i].bounds,
|
|
805
|
-
rotationAngleDeg: options.rotationAngleDeg
|
|
806
826
|
});
|
|
807
827
|
|
|
808
828
|
const tileTime = performance.now() - tileStart;
|
|
@@ -1644,412 +1664,22 @@ function stitchToolpathTiles(tileResults, globalBounds, gridStep, xStep, yStep)
|
|
|
1644
1664
|
};
|
|
1645
1665
|
}
|
|
1646
1666
|
|
|
1647
|
-
// Radial
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
const useSharedBuffer = typeof SharedArrayBuffer !== 'undefined' &&
|
|
1664
|
-
numTriangles > 1000000 &&
|
|
1665
|
-
!(triangles.buffer instanceof SharedArrayBuffer);
|
|
1666
|
-
|
|
1667
|
-
if (useSharedBuffer) {
|
|
1668
|
-
debug.log(`Large dataset (${numTriangles.toLocaleString()} triangles), converting to SharedArrayBuffer`);
|
|
1669
|
-
const sab = new SharedArrayBuffer(triangles.byteLength);
|
|
1670
|
-
const sharedTriangles = new Float32Array(sab);
|
|
1671
|
-
sharedTriangles.set(triangles);
|
|
1672
|
-
triangles = sharedTriangles;
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
const bounds = boundsOverride || calculateBounds(triangles);
|
|
1676
|
-
|
|
1677
|
-
// Calculate max radius (distance from X-axis)
|
|
1678
|
-
let maxRadius = 0;
|
|
1679
|
-
for (let i = 0; i < triangles.length; i += 3) {
|
|
1680
|
-
const y = triangles[i + 1];
|
|
1681
|
-
const z = triangles[i + 2];
|
|
1682
|
-
const radius = Math.sqrt(y * y + z * z);
|
|
1683
|
-
maxRadius = Math.max(maxRadius, radius);
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
// Add margin for ray origins to start outside mesh
|
|
1687
|
-
maxRadius *= 1.2;
|
|
1688
|
-
|
|
1689
|
-
const circumference = 2 * Math.PI * maxRadius;
|
|
1690
|
-
const xRange = bounds.max.x - bounds.min.x;
|
|
1691
|
-
const rotationStepRadians = rotationStepDegrees * (Math.PI / 180);
|
|
1692
|
-
|
|
1693
|
-
// Calculate grid height (number of angular scanlines)
|
|
1694
|
-
const gridHeight = Math.ceil(360 / rotationStepDegrees) + 1;
|
|
1695
|
-
|
|
1696
|
-
// Calculate number of tiles based on user config or auto-calculation
|
|
1697
|
-
let numTiles, trianglesPerTileTarget;
|
|
1698
|
-
|
|
1699
|
-
if (params.trianglesPerTile) {
|
|
1700
|
-
// User specified explicit triangles per tile
|
|
1701
|
-
trianglesPerTileTarget = params.trianglesPerTile;
|
|
1702
|
-
numTiles = Math.max(1, Math.ceil(numTriangles / trianglesPerTileTarget));
|
|
1703
|
-
debug.log(`Radial: ${numTriangles} triangles, ${gridHeight} scanlines, ${numTiles} X-tiles (${trianglesPerTileTarget} target tri/tile)`);
|
|
1704
|
-
} else {
|
|
1705
|
-
// Auto-calculate based on workload budget
|
|
1706
|
-
// Total work = numTriangles × gridHeight × culling_efficiency
|
|
1707
|
-
// More tiles = smaller X-slices = better culling = less work per tile
|
|
1708
|
-
const totalWorkload = numTriangles * gridHeight * 0.5; // 0.5 = expected culling efficiency
|
|
1709
|
-
numTiles = Math.max(1, Math.ceil(totalWorkload / MAX_WORKLOAD_PER_TILE));
|
|
1710
|
-
trianglesPerTileTarget = Math.ceil(numTriangles / numTiles);
|
|
1711
|
-
debug.log(`Radial: ${numTriangles} triangles, ${gridHeight} scanlines, ${numTiles} X-tiles (${trianglesPerTileTarget} avg tri/tile, auto-calculated)`);
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
const tileWidth = xRange / numTiles;
|
|
1715
|
-
|
|
1716
|
-
// Create pipelines on first use
|
|
1717
|
-
if (!cachedRadialCullPipeline) {
|
|
1718
|
-
const cullShaderModule = device.createShaderModule({ code: radialCullShaderCode });
|
|
1719
|
-
cachedRadialCullPipeline = device.createComputePipeline({
|
|
1720
|
-
layout: 'auto',
|
|
1721
|
-
compute: { module: cullShaderModule, entryPoint: 'main' }
|
|
1722
|
-
});
|
|
1723
|
-
debug.log('Created radial cull pipeline');
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
if (!cachedRadialRasterizePipeline) {
|
|
1727
|
-
const rasterShaderModule = device.createShaderModule({ code: radialRasterizeShaderCode });
|
|
1728
|
-
cachedRadialRasterizePipeline = device.createComputePipeline({
|
|
1729
|
-
layout: 'auto',
|
|
1730
|
-
compute: { module: rasterShaderModule, entryPoint: 'main' }
|
|
1731
|
-
});
|
|
1732
|
-
debug.log('Created radial rasterize pipeline');
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
// Create shared triangle buffer
|
|
1736
|
-
const triangleBuffer = device.createBuffer({
|
|
1737
|
-
size: triangles.byteLength,
|
|
1738
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
1739
|
-
});
|
|
1740
|
-
device.queue.writeBuffer(triangleBuffer, 0, triangles);
|
|
1741
|
-
|
|
1742
|
-
// Calculate attention bit array size
|
|
1743
|
-
const numWords = Math.ceil(numTriangles / 32);
|
|
1744
|
-
debug.log(`Attention array: ${numWords} words (${numWords * 4} bytes)`);
|
|
1745
|
-
|
|
1746
|
-
// Helper function to process a single tile
|
|
1747
|
-
async function processTile(tileIdx) {
|
|
1748
|
-
const prefix = `Tile ${tileIdx + 1}/${numTiles}:`;
|
|
1749
|
-
try {
|
|
1750
|
-
const tile_min_x = bounds.min.x + tileIdx * tileWidth;
|
|
1751
|
-
const tile_max_x = bounds.min.x + (tileIdx + 1) * tileWidth;
|
|
1752
|
-
|
|
1753
|
-
debug.log(`${prefix} X=[${tile_min_x.toFixed(2)}, ${tile_max_x.toFixed(2)}]`);
|
|
1754
|
-
|
|
1755
|
-
// Pass 1: Cull triangles for this X-tile
|
|
1756
|
-
const tileStartTime = performance.now();
|
|
1757
|
-
|
|
1758
|
-
const attentionBuffer = device.createBuffer({
|
|
1759
|
-
size: numWords * 4,
|
|
1760
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
|
|
1761
|
-
});
|
|
1762
|
-
|
|
1763
|
-
// Clear attention buffer
|
|
1764
|
-
const zeros = new Uint32Array(numWords);
|
|
1765
|
-
device.queue.writeBuffer(attentionBuffer, 0, zeros);
|
|
1766
|
-
|
|
1767
|
-
const cullUniformData = new Float32Array(4);
|
|
1768
|
-
cullUniformData[0] = tile_min_x;
|
|
1769
|
-
cullUniformData[1] = tile_max_x;
|
|
1770
|
-
const cullUniformU32 = new Uint32Array(cullUniformData.buffer);
|
|
1771
|
-
cullUniformU32[2] = numTriangles;
|
|
1772
|
-
|
|
1773
|
-
const cullUniformBuffer = device.createBuffer({
|
|
1774
|
-
size: 16,
|
|
1775
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1776
|
-
});
|
|
1777
|
-
device.queue.writeBuffer(cullUniformBuffer, 0, cullUniformData);
|
|
1778
|
-
|
|
1779
|
-
// CRITICAL: Wait for writeBuffer operations before compute dispatch
|
|
1780
|
-
await device.queue.onSubmittedWorkDone();
|
|
1781
|
-
|
|
1782
|
-
const cullBindGroup = device.createBindGroup({
|
|
1783
|
-
layout: cachedRadialCullPipeline.getBindGroupLayout(0),
|
|
1784
|
-
entries: [
|
|
1785
|
-
{ binding: 0, resource: { buffer: triangleBuffer } },
|
|
1786
|
-
{ binding: 1, resource: { buffer: attentionBuffer } },
|
|
1787
|
-
{ binding: 2, resource: { buffer: cullUniformBuffer } },
|
|
1788
|
-
],
|
|
1789
|
-
});
|
|
1790
|
-
|
|
1791
|
-
const cullEncoder = device.createCommandEncoder();
|
|
1792
|
-
const cullPass = cullEncoder.beginComputePass();
|
|
1793
|
-
cullPass.setPipeline(cachedRadialCullPipeline);
|
|
1794
|
-
cullPass.setBindGroup(0, cullBindGroup);
|
|
1795
|
-
cullPass.dispatchWorkgroups(Math.ceil(numTriangles / 256));
|
|
1796
|
-
cullPass.end();
|
|
1797
|
-
device.queue.submit([cullEncoder.finish()]);
|
|
1798
|
-
await device.queue.onSubmittedWorkDone();
|
|
1799
|
-
|
|
1800
|
-
const cullTime = performance.now() - tileStartTime;
|
|
1801
|
-
// debug.log(` Culling: ${cullTime.toFixed(1)}ms`);
|
|
1802
|
-
|
|
1803
|
-
// Pass 1.5: Read back attention bits and compact to triangle index list
|
|
1804
|
-
const compactStartTime = performance.now();
|
|
1805
|
-
|
|
1806
|
-
// Create staging buffer to read attention bits
|
|
1807
|
-
const attentionStagingBuffer = device.createBuffer({
|
|
1808
|
-
size: numWords * 4,
|
|
1809
|
-
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
1810
|
-
});
|
|
1811
|
-
|
|
1812
|
-
const copyEncoder = device.createCommandEncoder();
|
|
1813
|
-
copyEncoder.copyBufferToBuffer(attentionBuffer, 0, attentionStagingBuffer, 0, numWords * 4);
|
|
1814
|
-
device.queue.submit([copyEncoder.finish()]);
|
|
1815
|
-
await device.queue.onSubmittedWorkDone();
|
|
1816
|
-
|
|
1817
|
-
await attentionStagingBuffer.mapAsync(GPUMapMode.READ);
|
|
1818
|
-
const attentionBits = new Uint32Array(attentionStagingBuffer.getMappedRange());
|
|
1819
|
-
|
|
1820
|
-
// CPU compaction: build list of marked triangle indices
|
|
1821
|
-
const compactIndices = [];
|
|
1822
|
-
for (let triIdx = 0; triIdx < numTriangles; triIdx++) {
|
|
1823
|
-
const wordIdx = Math.floor(triIdx / 32);
|
|
1824
|
-
const bitIdx = triIdx % 32;
|
|
1825
|
-
const isMarked = (attentionBits[wordIdx] & (1 << bitIdx)) !== 0;
|
|
1826
|
-
if (isMarked) {
|
|
1827
|
-
compactIndices.push(triIdx);
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
attentionStagingBuffer.unmap();
|
|
1832
|
-
attentionStagingBuffer.destroy();
|
|
1833
|
-
|
|
1834
|
-
const compactTime = performance.now() - compactStartTime;
|
|
1835
|
-
const cullingEfficiency = ((numTriangles - compactIndices.length) / numTriangles * 100).toFixed(1);
|
|
1836
|
-
debug.log(`${prefix} Compacted ${numTriangles} → ${compactIndices.length} triangles (${cullingEfficiency}% culled) in ${compactTime.toFixed(1)}ms`);
|
|
1837
|
-
|
|
1838
|
-
// Create compact triangle index buffer
|
|
1839
|
-
const compactIndexBuffer = device.createBuffer({
|
|
1840
|
-
size: Math.max(4, compactIndices.length * 4), // At least 4 bytes
|
|
1841
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
1842
|
-
});
|
|
1843
|
-
if (compactIndices.length > 0) {
|
|
1844
|
-
device.queue.writeBuffer(compactIndexBuffer, 0, new Uint32Array(compactIndices));
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
// Pass 2: Rasterize this X-tile
|
|
1848
|
-
const rasterStartTime = performance.now();
|
|
1849
|
-
|
|
1850
|
-
const gridWidth = Math.ceil(tileWidth / stepSize) + 1;
|
|
1851
|
-
const gridHeight = Math.ceil(360 / rotationStepDegrees) + 1; // Number of angular samples
|
|
1852
|
-
const totalCells = gridWidth * gridHeight;
|
|
1853
|
-
|
|
1854
|
-
debug.log(`${prefix} Grid: ${gridWidth}×${gridHeight} = ${totalCells} cells (rotationStep=${rotationStepDegrees}°)`);
|
|
1855
|
-
|
|
1856
|
-
const outputBuffer = device.createBuffer({
|
|
1857
|
-
size: totalCells * 4,
|
|
1858
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
|
|
1859
|
-
});
|
|
1860
|
-
|
|
1861
|
-
// Initialize with EMPTY_CELL
|
|
1862
|
-
const initData = new Float32Array(totalCells);
|
|
1863
|
-
initData.fill(EMPTY_CELL);
|
|
1864
|
-
device.queue.writeBuffer(outputBuffer, 0, initData);
|
|
1865
|
-
|
|
1866
|
-
const rotationOffsetRadians = (params.radialRotationOffset ?? 0) * (Math.PI / 180);
|
|
1867
|
-
|
|
1868
|
-
const rasterUniformData = new Float32Array(16);
|
|
1869
|
-
rasterUniformData[0] = tile_min_x;
|
|
1870
|
-
rasterUniformData[1] = tile_max_x;
|
|
1871
|
-
rasterUniformData[2] = maxRadius;
|
|
1872
|
-
rasterUniformData[3] = rotationStepRadians;
|
|
1873
|
-
rasterUniformData[4] = stepSize;
|
|
1874
|
-
rasterUniformData[5] = zFloor;
|
|
1875
|
-
rasterUniformData[6] = rotationOffsetRadians;
|
|
1876
|
-
rasterUniformData[7] = 0; // padding
|
|
1877
|
-
const rasterUniformU32 = new Uint32Array(rasterUniformData.buffer);
|
|
1878
|
-
rasterUniformU32[8] = gridWidth;
|
|
1879
|
-
rasterUniformU32[9] = gridHeight;
|
|
1880
|
-
rasterUniformU32[10] = compactIndices.length; // Use compact count instead of numTriangles
|
|
1881
|
-
rasterUniformU32[11] = 0; // No longer using attention words
|
|
1882
|
-
|
|
1883
|
-
const rasterUniformBuffer = device.createBuffer({
|
|
1884
|
-
size: 64,
|
|
1885
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1886
|
-
});
|
|
1887
|
-
device.queue.writeBuffer(rasterUniformBuffer, 0, rasterUniformData);
|
|
1888
|
-
|
|
1889
|
-
// CRITICAL: Wait for writeBuffer operations before compute dispatch
|
|
1890
|
-
await device.queue.onSubmittedWorkDone();
|
|
1891
|
-
|
|
1892
|
-
const rasterBindGroup = device.createBindGroup({
|
|
1893
|
-
layout: cachedRadialRasterizePipeline.getBindGroupLayout(0),
|
|
1894
|
-
entries: [
|
|
1895
|
-
{ binding: 0, resource: { buffer: triangleBuffer } },
|
|
1896
|
-
{ binding: 1, resource: { buffer: compactIndexBuffer } }, // Use compact indices instead of attention bits
|
|
1897
|
-
{ binding: 2, resource: { buffer: outputBuffer } },
|
|
1898
|
-
{ binding: 3, resource: { buffer: rasterUniformBuffer } },
|
|
1899
|
-
],
|
|
1900
|
-
});
|
|
1901
|
-
|
|
1902
|
-
const rasterEncoder = device.createCommandEncoder();
|
|
1903
|
-
const rasterPass = rasterEncoder.beginComputePass();
|
|
1904
|
-
rasterPass.setPipeline(cachedRadialRasterizePipeline);
|
|
1905
|
-
rasterPass.setBindGroup(0, rasterBindGroup);
|
|
1906
|
-
const workgroupsX = Math.ceil(gridWidth / 16);
|
|
1907
|
-
const workgroupsY = Math.ceil(gridHeight / 16);
|
|
1908
|
-
rasterPass.dispatchWorkgroups(workgroupsX, workgroupsY);
|
|
1909
|
-
rasterPass.end();
|
|
1910
|
-
|
|
1911
|
-
const stagingBuffer = device.createBuffer({
|
|
1912
|
-
size: totalCells * 4,
|
|
1913
|
-
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
1914
|
-
});
|
|
1915
|
-
|
|
1916
|
-
rasterEncoder.copyBufferToBuffer(outputBuffer, 0, stagingBuffer, 0, totalCells * 4);
|
|
1917
|
-
device.queue.submit([rasterEncoder.finish()]);
|
|
1918
|
-
await device.queue.onSubmittedWorkDone();
|
|
1919
|
-
|
|
1920
|
-
await stagingBuffer.mapAsync(GPUMapMode.READ);
|
|
1921
|
-
const outputData = new Float32Array(stagingBuffer.getMappedRange());
|
|
1922
|
-
const tileResult = new Float32Array(outputData);
|
|
1923
|
-
stagingBuffer.unmap();
|
|
1924
|
-
|
|
1925
|
-
const rasterTime = performance.now() - rasterStartTime;
|
|
1926
|
-
debug.log(`${prefix} Rasterization: ${rasterTime.toFixed(1)}ms`);
|
|
1927
|
-
|
|
1928
|
-
// Cleanup tile buffers
|
|
1929
|
-
attentionBuffer.destroy();
|
|
1930
|
-
cullUniformBuffer.destroy();
|
|
1931
|
-
compactIndexBuffer.destroy();
|
|
1932
|
-
outputBuffer.destroy();
|
|
1933
|
-
rasterUniformBuffer.destroy();
|
|
1934
|
-
stagingBuffer.destroy();
|
|
1935
|
-
|
|
1936
|
-
return {
|
|
1937
|
-
tileIdx,
|
|
1938
|
-
data: tileResult,
|
|
1939
|
-
gridWidth,
|
|
1940
|
-
gridHeight,
|
|
1941
|
-
minX: tile_min_x,
|
|
1942
|
-
maxX: tile_max_x
|
|
1943
|
-
};
|
|
1944
|
-
} catch (error) {
|
|
1945
|
-
debug.error(`Error processing tile ${tileIdx + 1}/${numTiles}:`, error);
|
|
1946
|
-
throw new Error(`Tile ${tileIdx + 1} failed: ${error.message}`);
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
// Process tiles with rolling window to maintain constant concurrency
|
|
1951
|
-
// This keeps GPU busy while preventing browser from allocating too many buffers at once
|
|
1952
|
-
const maxConcurrentTiles = params.maxConcurrentTiles ?? 50;
|
|
1953
|
-
debug.log(`Processing ${numTiles} tiles (max ${maxConcurrentTiles} concurrent)...`);
|
|
1954
|
-
|
|
1955
|
-
let completedTiles = 0;
|
|
1956
|
-
let nextTileIdx = 0;
|
|
1957
|
-
const activeTiles = new Map(); // promise -> tileIdx
|
|
1958
|
-
const tileResults = new Array(numTiles);
|
|
1959
|
-
|
|
1960
|
-
// Helper to start a tile and track it
|
|
1961
|
-
const startTile = (tileIdx) => {
|
|
1962
|
-
const promise = processTile(tileIdx).then(result => {
|
|
1963
|
-
completedTiles++;
|
|
1964
|
-
const percent = Math.round((completedTiles / numTiles) * 100);
|
|
1965
|
-
|
|
1966
|
-
// Report progress
|
|
1967
|
-
self.postMessage({
|
|
1968
|
-
type: 'rasterize-progress',
|
|
1969
|
-
data: {
|
|
1970
|
-
percent,
|
|
1971
|
-
current: completedTiles,
|
|
1972
|
-
total: numTiles
|
|
1973
|
-
}
|
|
1974
|
-
});
|
|
1975
|
-
|
|
1976
|
-
tileResults[tileIdx] = result;
|
|
1977
|
-
activeTiles.delete(promise);
|
|
1978
|
-
return result;
|
|
1979
|
-
});
|
|
1980
|
-
activeTiles.set(promise, tileIdx);
|
|
1981
|
-
return promise;
|
|
1982
|
-
};
|
|
1983
|
-
|
|
1984
|
-
// Start initial window of tiles
|
|
1985
|
-
while (nextTileIdx < numTiles && activeTiles.size < maxConcurrentTiles) {
|
|
1986
|
-
startTile(nextTileIdx++);
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
// As tiles complete, start new ones to maintain window size
|
|
1990
|
-
while (activeTiles.size > 0) {
|
|
1991
|
-
// Wait for at least one tile to complete
|
|
1992
|
-
await Promise.race(activeTiles.keys());
|
|
1993
|
-
|
|
1994
|
-
// Start as many new tiles as needed to fill window
|
|
1995
|
-
while (nextTileIdx < numTiles && activeTiles.size < maxConcurrentTiles) {
|
|
1996
|
-
startTile(nextTileIdx++);
|
|
1997
|
-
}
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
triangleBuffer.destroy();
|
|
2001
|
-
|
|
2002
|
-
const totalTime = performance.now() - startTime;
|
|
2003
|
-
debug.log(`Radial complete in ${totalTime.toFixed(1)}ms`);
|
|
2004
|
-
|
|
2005
|
-
// Stitch tiles together into a single dense array
|
|
2006
|
-
const fullGridHeight = Math.ceil(360 / rotationStepDegrees) + 1; // Number of angular samples
|
|
2007
|
-
const fullGridWidth = Math.ceil(xRange / stepSize) + 1;
|
|
2008
|
-
const stitchedData = new Float32Array(fullGridWidth * fullGridHeight);
|
|
2009
|
-
stitchedData.fill(EMPTY_CELL);
|
|
2010
|
-
|
|
2011
|
-
for (const tile of tileResults) {
|
|
2012
|
-
const tileXOffset = Math.round((tile.minX - bounds.min.x) / stepSize);
|
|
2013
|
-
|
|
2014
|
-
for (let ty = 0; ty < tile.gridHeight; ty++) {
|
|
2015
|
-
for (let tx = 0; tx < tile.gridWidth; tx++) {
|
|
2016
|
-
const tileIdx = ty * tile.gridWidth + tx;
|
|
2017
|
-
const fullX = tileXOffset + tx;
|
|
2018
|
-
const fullY = ty;
|
|
2019
|
-
|
|
2020
|
-
if (fullX >= 0 && fullX < fullGridWidth && fullY >= 0 && fullY < fullGridHeight) {
|
|
2021
|
-
const fullIdx = fullY * fullGridWidth + fullX;
|
|
2022
|
-
stitchedData[fullIdx] = tile.data[tileIdx];
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
// For toolpath generation, bounds.max.y must match the actual grid dimensions
|
|
2029
|
-
// so that createHeightMapFromPoints calculates the correct height:
|
|
2030
|
-
// height = ceil((max.y - min.y) / stepSize) + 1 = gridHeight
|
|
2031
|
-
// Therefore: max.y = (gridHeight - 1) * stepSize
|
|
2032
|
-
const boundsMaxY = (fullGridHeight - 1) * stepSize;
|
|
2033
|
-
|
|
2034
|
-
return {
|
|
2035
|
-
positions: stitchedData,
|
|
2036
|
-
pointCount: stitchedData.length,
|
|
2037
|
-
bounds: {
|
|
2038
|
-
min: { x: bounds.min.x, y: 0, z: 0 },
|
|
2039
|
-
max: { x: bounds.max.x, y: boundsMaxY, z: maxRadius }
|
|
2040
|
-
},
|
|
2041
|
-
conversionTime: totalTime,
|
|
2042
|
-
gridWidth: fullGridWidth,
|
|
2043
|
-
gridHeight: fullGridHeight,
|
|
2044
|
-
isDense: true,
|
|
2045
|
-
maxRadius,
|
|
2046
|
-
circumference,
|
|
2047
|
-
rotationStepDegrees // NEW: needed for wrapping and toolpath generation
|
|
2048
|
-
};
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
// Radial V2: Rasterize model with rotating ray planes and X-bucketing
|
|
2052
|
-
async function radialRasterizeV2(triangles, bucketData, resolution, angleStep, numAngles, maxRadius, toolWidth, zFloor, bounds, startAngle = 0) {
|
|
1667
|
+
// Radial: Rasterize model with rotating ray planes and X-bucketing
|
|
1668
|
+
async function radialRasterize({
|
|
1669
|
+
triangles,
|
|
1670
|
+
bucketData,
|
|
1671
|
+
resolution,
|
|
1672
|
+
angleStep,
|
|
1673
|
+
numAngles,
|
|
1674
|
+
maxRadius,
|
|
1675
|
+
toolWidth,
|
|
1676
|
+
zFloor,
|
|
1677
|
+
bounds,
|
|
1678
|
+
startAngle = 0,
|
|
1679
|
+
reusableBuffers = null,
|
|
1680
|
+
returnBuffersForReuse = false,
|
|
1681
|
+
batchInfo = {}
|
|
1682
|
+
}) {
|
|
2053
1683
|
if (!device) {
|
|
2054
1684
|
throw new Error('WebGPU not initialized');
|
|
2055
1685
|
}
|
|
@@ -2076,50 +1706,65 @@ async function radialRasterizeV2(triangles, bucketData, resolution, angleStep, n
|
|
|
2076
1706
|
const avgTriangles = bucketTriangleCounts.reduce((a, b) => a + b, 0) / bucketTriangleCounts.length;
|
|
2077
1707
|
const workPerWorkgroup = maxTriangles * numAngles * bucketGridWidth * gridYHeight;
|
|
2078
1708
|
|
|
2079
|
-
|
|
2080
|
-
|
|
1709
|
+
// Determine bucket batching to avoid GPU timeouts
|
|
1710
|
+
// Target: keep max work per batch under ~1M ray-triangle tests
|
|
1711
|
+
const maxWorkPerBatch = 1e6;
|
|
1712
|
+
const estimatedWorkPerBucket = avgTriangles * numAngles * bucketGridWidth * gridYHeight;
|
|
1713
|
+
|
|
1714
|
+
// Calculate buckets per batch, but enforce reasonable limits
|
|
1715
|
+
// - Minimum: 10 buckets per batch (unless total < 10)
|
|
1716
|
+
// - Maximum: all buckets if work is reasonable
|
|
1717
|
+
let maxBucketsPerBatch;
|
|
1718
|
+
if (estimatedWorkPerBucket === 0) {
|
|
1719
|
+
maxBucketsPerBatch = bucketData.numBuckets; // Empty model
|
|
1720
|
+
} else {
|
|
1721
|
+
const idealBucketsPerBatch = Math.floor(maxWorkPerBatch / estimatedWorkPerBucket);
|
|
1722
|
+
const minBucketsPerBatch = Math.min(4, bucketData.numBuckets);
|
|
1723
|
+
maxBucketsPerBatch = Math.max(minBucketsPerBatch, idealBucketsPerBatch);
|
|
1724
|
+
// Cap at total buckets
|
|
1725
|
+
maxBucketsPerBatch = Math.min(maxBucketsPerBatch, bucketData.numBuckets);
|
|
1726
|
+
}
|
|
2081
1727
|
|
|
2082
|
-
|
|
2083
|
-
const triangleBuffer = device.createBuffer({
|
|
2084
|
-
size: triangles.byteLength,
|
|
2085
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
2086
|
-
mappedAtCreation: true
|
|
2087
|
-
});
|
|
2088
|
-
new Float32Array(triangleBuffer.getMappedRange()).set(triangles);
|
|
2089
|
-
triangleBuffer.unmap();
|
|
1728
|
+
const numBucketBatches = Math.ceil(bucketData.numBuckets / maxBucketsPerBatch);
|
|
2090
1729
|
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
const bucketView = new ArrayBuffer(bucketInfoSize);
|
|
2100
|
-
const bucketFloatView = new Float32Array(bucketView);
|
|
2101
|
-
const bucketUintView = new Uint32Array(bucketView);
|
|
2102
|
-
|
|
2103
|
-
for (let i = 0; i < bucketData.buckets.length; i++) {
|
|
2104
|
-
const bucket = bucketData.buckets[i];
|
|
2105
|
-
const offset = i * 4;
|
|
2106
|
-
bucketFloatView[offset] = bucket.minX; // f32
|
|
2107
|
-
bucketFloatView[offset + 1] = bucket.maxX; // f32
|
|
2108
|
-
bucketUintView[offset + 2] = bucket.startIndex; // u32
|
|
2109
|
-
bucketUintView[offset + 3] = bucket.count; // u32
|
|
1730
|
+
if (diagnostic) {
|
|
1731
|
+
debug.log(`Radial: ${gridWidth}x${gridYHeight} grid, ${numAngles} angles, ${bucketData.buckets.length} buckets`);
|
|
1732
|
+
debug.log(`Load: min=${minTriangles} max=${maxTriangles} avg=${avgTriangles.toFixed(0)} (${(maxTriangles/avgTriangles).toFixed(2)}x imbalance, worst=${(workPerWorkgroup/1e6).toFixed(1)}M tests)`);
|
|
1733
|
+
debug.log(`Estimated work/bucket: ${(estimatedWorkPerBucket/1e6).toFixed(1)}M tests`);
|
|
1734
|
+
if (numBucketBatches > 1) {
|
|
1735
|
+
debug.log(`Bucket batching: ${numBucketBatches} batches of ~${maxBucketsPerBatch} buckets to avoid timeout`);
|
|
1736
|
+
}
|
|
2110
1737
|
}
|
|
2111
1738
|
|
|
2112
|
-
|
|
2113
|
-
|
|
1739
|
+
// Reuse buffers if provided, otherwise create new ones
|
|
1740
|
+
let triangleBuffer, triangleIndicesBuffer;
|
|
1741
|
+
let shouldCleanupBuffers = false;
|
|
2114
1742
|
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
1743
|
+
if (reusableBuffers) {
|
|
1744
|
+
// Reuse cached buffers from previous angle batch
|
|
1745
|
+
triangleBuffer = reusableBuffers.triangleBuffer;
|
|
1746
|
+
triangleIndicesBuffer = reusableBuffers.triangleIndicesBuffer;
|
|
1747
|
+
} else {
|
|
1748
|
+
// Create new GPU buffers (first batch or non-batched operation)
|
|
1749
|
+
shouldCleanupBuffers = true;
|
|
1750
|
+
|
|
1751
|
+
triangleBuffer = device.createBuffer({
|
|
1752
|
+
size: triangles.byteLength,
|
|
1753
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
1754
|
+
mappedAtCreation: true
|
|
1755
|
+
});
|
|
1756
|
+
new Float32Array(triangleBuffer.getMappedRange()).set(triangles);
|
|
1757
|
+
triangleBuffer.unmap();
|
|
1758
|
+
|
|
1759
|
+
// Create triangle indices buffer
|
|
1760
|
+
triangleIndicesBuffer = device.createBuffer({
|
|
1761
|
+
size: bucketData.triangleIndices.byteLength,
|
|
1762
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
1763
|
+
mappedAtCreation: true
|
|
1764
|
+
});
|
|
1765
|
+
new Uint32Array(triangleIndicesBuffer.getMappedRange()).set(bucketData.triangleIndices);
|
|
1766
|
+
triangleIndicesBuffer.unmap();
|
|
1767
|
+
}
|
|
2123
1768
|
|
|
2124
1769
|
// Create output buffer (all angles, all buckets)
|
|
2125
1770
|
const outputSize = numAngles * bucketData.numBuckets * bucketGridWidth * gridYHeight * 4;
|
|
@@ -2129,84 +1774,115 @@ async function radialRasterizeV2(triangles, bucketData, resolution, angleStep, n
|
|
|
2129
1774
|
});
|
|
2130
1775
|
|
|
2131
1776
|
// CRITICAL: Initialize output buffer with zFloor to avoid reading garbage data
|
|
2132
|
-
// Use mappedAtCreation for deterministic initialization (not writeBuffer!)
|
|
2133
|
-
const initEncoder = device.createCommandEncoder();
|
|
2134
1777
|
const initData = new Float32Array(outputSize / 4);
|
|
2135
1778
|
initData.fill(zFloor);
|
|
2136
1779
|
device.queue.writeBuffer(outputBuffer, 0, initData);
|
|
2137
|
-
//
|
|
2138
|
-
device.queue.submit([initEncoder.finish()]);
|
|
2139
|
-
await device.queue.onSubmittedWorkDone();
|
|
2140
|
-
|
|
2141
|
-
// Create uniforms with proper alignment (f32 and u32 mixed)
|
|
2142
|
-
// Struct layout: f32, f32, u32, f32, f32, u32, f32, u32, f32, f32, u32, u32, f32
|
|
2143
|
-
const uniformBuffer = device.createBuffer({
|
|
2144
|
-
size: 52, // 13 fields * 4 bytes
|
|
2145
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
2146
|
-
mappedAtCreation: true
|
|
2147
|
-
});
|
|
1780
|
+
// Note: No need to wait - GPU will execute writeBuffer before compute shader
|
|
2148
1781
|
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
const
|
|
2152
|
-
|
|
2153
|
-
floatView[0] = resolution; // f32
|
|
2154
|
-
floatView[1] = angleStep * (Math.PI / 180); // f32
|
|
2155
|
-
uintView[2] = numAngles; // u32
|
|
2156
|
-
floatView[3] = maxRadius; // f32
|
|
2157
|
-
floatView[4] = toolWidth; // f32
|
|
2158
|
-
uintView[5] = gridYHeight; // u32
|
|
2159
|
-
floatView[6] = bucketData.buckets[0].maxX - bucketData.buckets[0].minX; // f32 bucketWidth
|
|
2160
|
-
uintView[7] = bucketGridWidth; // u32
|
|
2161
|
-
floatView[8] = bucketMinX; // f32 global_min_x (use bucket range)
|
|
2162
|
-
floatView[9] = zFloor; // f32
|
|
2163
|
-
uintView[10] = 0; // u32 filterMode
|
|
2164
|
-
uintView[11] = bucketData.numBuckets; // u32
|
|
2165
|
-
floatView[12] = startAngle * (Math.PI / 180); // f32 start_angle (radians)
|
|
2166
|
-
|
|
2167
|
-
new Uint8Array(uniformBuffer.getMappedRange()).set(new Uint8Array(uniformView));
|
|
2168
|
-
uniformBuffer.unmap();
|
|
2169
|
-
|
|
2170
|
-
// Create shader and pipeline
|
|
2171
|
-
const shaderModule = device.createShaderModule({ code: radialRasterizeV2ShaderCode });
|
|
2172
|
-
const pipeline = device.createComputePipeline({
|
|
2173
|
-
layout: 'auto',
|
|
2174
|
-
compute: {
|
|
2175
|
-
module: shaderModule,
|
|
2176
|
-
entryPoint: 'main'
|
|
2177
|
-
}
|
|
2178
|
-
});
|
|
1782
|
+
// Prep complete, GPU starting
|
|
1783
|
+
timings.prep = performance.now() - timings.start;
|
|
1784
|
+
const gpuStart = performance.now();
|
|
2179
1785
|
|
|
2180
|
-
//
|
|
2181
|
-
const
|
|
2182
|
-
layout: pipeline.getBindGroupLayout(0),
|
|
2183
|
-
entries: [
|
|
2184
|
-
{ binding: 0, resource: { buffer: triangleBuffer } },
|
|
2185
|
-
{ binding: 1, resource: { buffer: outputBuffer } },
|
|
2186
|
-
{ binding: 2, resource: { buffer: uniformBuffer } },
|
|
2187
|
-
{ binding: 3, resource: { buffer: bucketInfoBuffer } },
|
|
2188
|
-
{ binding: 4, resource: { buffer: triangleIndicesBuffer } }
|
|
2189
|
-
]
|
|
2190
|
-
});
|
|
1786
|
+
// Use cached pipeline (created in initWebGPU)
|
|
1787
|
+
const pipeline = cachedRadialBatchPipeline;
|
|
2191
1788
|
|
|
2192
|
-
|
|
2193
|
-
// Dispatch
|
|
1789
|
+
// Process buckets in batches to avoid GPU timeouts
|
|
2194
1790
|
const commandEncoder = device.createCommandEncoder();
|
|
2195
1791
|
const passEncoder = commandEncoder.beginComputePass();
|
|
2196
1792
|
passEncoder.setPipeline(pipeline);
|
|
2197
|
-
passEncoder.setBindGroup(0, bindGroup);
|
|
2198
|
-
|
|
2199
|
-
// Prep complete, GPU starting
|
|
2200
|
-
timings.prep = performance.now() - timings.start;
|
|
2201
|
-
const gpuStart = performance.now();
|
|
2202
1793
|
|
|
2203
|
-
// Dispatch: (numAngles/8, gridYHeight/8, numBuckets)
|
|
2204
1794
|
const dispatchX = Math.ceil(numAngles / 8);
|
|
2205
1795
|
const dispatchY = Math.ceil(gridYHeight / 8);
|
|
2206
|
-
const dispatchZ = bucketData.numBuckets;
|
|
2207
|
-
debug.log(`[Worker] Dispatch: (${dispatchX}, ${dispatchY}, ${dispatchZ}) = ${dispatchX * 8} angles, ${dispatchY * 8} Y cells, ${dispatchZ} buckets`);
|
|
2208
1796
|
|
|
2209
|
-
|
|
1797
|
+
// Collect buffers to destroy after GPU completes
|
|
1798
|
+
const batchBuffersToDestroy = [];
|
|
1799
|
+
debug.log(`Dispatch (${dispatchX}, ${dispatchY}, ${maxBucketsPerBatch}) in ${numBucketBatches} Chunks`);
|
|
1800
|
+
|
|
1801
|
+
for (let batchIdx = 0; batchIdx < numBucketBatches; batchIdx++) {
|
|
1802
|
+
const startBucket = batchIdx * maxBucketsPerBatch;
|
|
1803
|
+
const endBucket = Math.min(startBucket + maxBucketsPerBatch, bucketData.numBuckets);
|
|
1804
|
+
const bucketsInBatch = endBucket - startBucket;
|
|
1805
|
+
|
|
1806
|
+
// Create bucket info buffer for this batch
|
|
1807
|
+
const bucketInfoSize = bucketsInBatch * 16; // 4 fields * 4 bytes per bucket
|
|
1808
|
+
const bucketInfoBuffer = device.createBuffer({
|
|
1809
|
+
size: bucketInfoSize,
|
|
1810
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
1811
|
+
mappedAtCreation: true
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
const bucketView = new ArrayBuffer(bucketInfoSize);
|
|
1815
|
+
const bucketFloatView = new Float32Array(bucketView);
|
|
1816
|
+
const bucketUintView = new Uint32Array(bucketView);
|
|
1817
|
+
|
|
1818
|
+
for (let i = 0; i < bucketsInBatch; i++) {
|
|
1819
|
+
const bucket = bucketData.buckets[startBucket + i];
|
|
1820
|
+
const offset = i * 4;
|
|
1821
|
+
bucketFloatView[offset] = bucket.minX; // f32
|
|
1822
|
+
bucketFloatView[offset + 1] = bucket.maxX; // f32
|
|
1823
|
+
bucketUintView[offset + 2] = bucket.startIndex; // u32
|
|
1824
|
+
bucketUintView[offset + 3] = bucket.count; // u32
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
new Uint8Array(bucketInfoBuffer.getMappedRange()).set(new Uint8Array(bucketView));
|
|
1828
|
+
bucketInfoBuffer.unmap();
|
|
1829
|
+
|
|
1830
|
+
// Create uniforms for this batch
|
|
1831
|
+
// Struct layout: f32, f32, u32, f32, f32, u32, f32, u32, f32, f32, u32, u32, f32, u32
|
|
1832
|
+
const uniformBuffer = device.createBuffer({
|
|
1833
|
+
size: 56, // 14 fields * 4 bytes
|
|
1834
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1835
|
+
mappedAtCreation: true
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
const uniformView = new ArrayBuffer(56);
|
|
1839
|
+
const floatView = new Float32Array(uniformView);
|
|
1840
|
+
const uintView = new Uint32Array(uniformView);
|
|
1841
|
+
|
|
1842
|
+
floatView[0] = resolution; // f32
|
|
1843
|
+
floatView[1] = angleStep * (Math.PI / 180); // f32
|
|
1844
|
+
uintView[2] = numAngles; // u32
|
|
1845
|
+
floatView[3] = maxRadius; // f32
|
|
1846
|
+
floatView[4] = toolWidth; // f32
|
|
1847
|
+
uintView[5] = gridYHeight; // u32
|
|
1848
|
+
floatView[6] = bucketData.buckets[0].maxX - bucketData.buckets[0].minX; // f32 bucketWidth
|
|
1849
|
+
uintView[7] = bucketGridWidth; // u32
|
|
1850
|
+
floatView[8] = bucketMinX; // f32 global_min_x
|
|
1851
|
+
floatView[9] = zFloor; // f32
|
|
1852
|
+
uintView[10] = 0; // u32 filterMode
|
|
1853
|
+
uintView[11] = bucketData.numBuckets; // u32 (total buckets, for validation)
|
|
1854
|
+
floatView[12] = startAngle * (Math.PI / 180); // f32 start_angle (radians)
|
|
1855
|
+
uintView[13] = startBucket; // u32 bucket_offset
|
|
1856
|
+
|
|
1857
|
+
new Uint8Array(uniformBuffer.getMappedRange()).set(new Uint8Array(uniformView));
|
|
1858
|
+
uniformBuffer.unmap();
|
|
1859
|
+
|
|
1860
|
+
// Create bind group for this batch
|
|
1861
|
+
const bindGroup = device.createBindGroup({
|
|
1862
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
1863
|
+
entries: [
|
|
1864
|
+
{ binding: 0, resource: { buffer: triangleBuffer } },
|
|
1865
|
+
{ binding: 1, resource: { buffer: outputBuffer } },
|
|
1866
|
+
{ binding: 2, resource: { buffer: uniformBuffer } },
|
|
1867
|
+
{ binding: 3, resource: { buffer: bucketInfoBuffer } },
|
|
1868
|
+
{ binding: 4, resource: { buffer: triangleIndicesBuffer } }
|
|
1869
|
+
]
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
passEncoder.setBindGroup(0, bindGroup);
|
|
1873
|
+
|
|
1874
|
+
// Dispatch for this batch
|
|
1875
|
+
const dispatchZ = bucketsInBatch;
|
|
1876
|
+
if (diagnostic) {
|
|
1877
|
+
debug.log(` Batch ${batchIdx + 1}/${numBucketBatches}: Dispatch (${dispatchX}, ${dispatchY}, ${dispatchZ}) = buckets ${startBucket}-${endBucket - 1}`);
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
passEncoder.dispatchWorkgroups(dispatchX, dispatchY, dispatchZ);
|
|
1881
|
+
|
|
1882
|
+
// Save buffers to destroy after GPU completes
|
|
1883
|
+
batchBuffersToDestroy.push(uniformBuffer, bucketInfoBuffer);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
2210
1886
|
passEncoder.end();
|
|
2211
1887
|
|
|
2212
1888
|
// Read back
|
|
@@ -2218,21 +1894,21 @@ async function radialRasterizeV2(triangles, bucketData, resolution, angleStep, n
|
|
|
2218
1894
|
commandEncoder.copyBufferToBuffer(outputBuffer, 0, stagingBuffer, 0, outputSize);
|
|
2219
1895
|
device.queue.submit([commandEncoder.finish()]);
|
|
2220
1896
|
|
|
2221
|
-
//
|
|
1897
|
+
// Wait for GPU to finish before reading results
|
|
2222
1898
|
await device.queue.onSubmittedWorkDone();
|
|
2223
|
-
console.timeEnd('RADIAL COMPUTE');
|
|
2224
|
-
|
|
2225
1899
|
await stagingBuffer.mapAsync(GPUMapMode.READ);
|
|
2226
|
-
const outputData = new Float32Array(stagingBuffer.getMappedRange());
|
|
2227
|
-
const outputCopy = new Float32Array(outputData);
|
|
1900
|
+
// const outputData = new Float32Array(stagingBuffer.getMappedRange());
|
|
1901
|
+
// const outputCopy = new Float32Array(outputData);
|
|
1902
|
+
const outputCopy = new Float32Array(stagingBuffer.getMappedRange().slice());
|
|
2228
1903
|
stagingBuffer.unmap();
|
|
2229
1904
|
|
|
2230
|
-
//
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
1905
|
+
// Now safe to destroy batch buffers (GPU has completed)
|
|
1906
|
+
for (const buffer of batchBuffersToDestroy) {
|
|
1907
|
+
buffer.destroy();
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// Cleanup main buffers
|
|
2234
1911
|
outputBuffer.destroy();
|
|
2235
|
-
uniformBuffer.destroy();
|
|
2236
1912
|
stagingBuffer.destroy();
|
|
2237
1913
|
|
|
2238
1914
|
timings.gpu = performance.now() - gpuStart;
|
|
@@ -2288,12 +1964,256 @@ async function radialRasterizeV2(triangles, bucketData, resolution, angleStep, n
|
|
|
2288
1964
|
timings.stitch = performance.now() - stitchStart;
|
|
2289
1965
|
const totalTime = performance.now() - timings.start;
|
|
2290
1966
|
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
1967
|
+
Object.assign(batchInfo, {
|
|
1968
|
+
'prep': (timings.prep | 0),
|
|
1969
|
+
'raster': (timings.gpu | 0),
|
|
1970
|
+
'stitch': (timings.stitch | 0)
|
|
1971
|
+
});
|
|
1972
|
+
|
|
1973
|
+
const result = { strips, timings };
|
|
1974
|
+
|
|
1975
|
+
// Decide what to do with triangle/indices buffers
|
|
1976
|
+
// Note: bucketInfoBuffer is now created/destroyed per bucket batch within the loop
|
|
1977
|
+
if (returnBuffersForReuse && shouldCleanupBuffers) {
|
|
1978
|
+
// First batch in multi-batch operation: return buffers for subsequent batches to reuse
|
|
1979
|
+
result.reusableBuffers = {
|
|
1980
|
+
triangleBuffer,
|
|
1981
|
+
triangleIndicesBuffer
|
|
1982
|
+
};
|
|
1983
|
+
} else if (shouldCleanupBuffers) {
|
|
1984
|
+
// Single batch operation OR we're NOT supposed to return buffers: destroy them now
|
|
1985
|
+
triangleBuffer.destroy();
|
|
1986
|
+
triangleIndicesBuffer.destroy();
|
|
1987
|
+
}
|
|
1988
|
+
// else: we're reusing buffers from a previous angle batch, don't destroy them (caller will destroy after all angle batches)
|
|
1989
|
+
|
|
1990
|
+
return result;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// Radial: Complete pipeline - rasterize model + generate toolpaths for all strips
|
|
1994
|
+
async function generateRadialToolpaths({
|
|
1995
|
+
triangles,
|
|
1996
|
+
bucketData,
|
|
1997
|
+
toolData,
|
|
1998
|
+
resolution,
|
|
1999
|
+
angleStep,
|
|
2000
|
+
numAngles,
|
|
2001
|
+
maxRadius,
|
|
2002
|
+
toolWidth,
|
|
2003
|
+
zFloor,
|
|
2004
|
+
bounds,
|
|
2005
|
+
xStep,
|
|
2006
|
+
yStep
|
|
2007
|
+
}) {
|
|
2008
|
+
debug.log('radial-generate-toolpaths', { triangles: triangles.length, numAngles, resolution });
|
|
2009
|
+
|
|
2010
|
+
// Batch processing: rasterize angle ranges to avoid memory allocation failure
|
|
2011
|
+
// Calculate safe batch size based on available GPU memory
|
|
2012
|
+
const MAX_BUFFER_SIZE_MB = 1800; // Stay under 2GB WebGPU limit with headroom
|
|
2013
|
+
const bytesPerCell = 4; // f32
|
|
2014
|
+
|
|
2015
|
+
const xSize = bounds.max.x - bounds.min.x;
|
|
2016
|
+
const ySize = bounds.max.y - bounds.min.y;
|
|
2017
|
+
const gridXSize = Math.ceil(xSize / resolution);
|
|
2018
|
+
const gridYHeight = Math.ceil(ySize / resolution);
|
|
2019
|
+
|
|
2020
|
+
// Calculate total memory requirement
|
|
2021
|
+
const cellsPerAngle = gridXSize * gridYHeight;
|
|
2022
|
+
const bytesPerAngle = cellsPerAngle * bytesPerCell;
|
|
2023
|
+
const totalMemoryMB = (numAngles * bytesPerAngle) / (1024 * 1024);
|
|
2024
|
+
|
|
2025
|
+
// Only batch if total memory exceeds threshold
|
|
2026
|
+
const batchDivisor = config?.batchDivisor || 1;
|
|
2027
|
+
let ANGLES_PER_BATCH, numBatches;
|
|
2028
|
+
if (totalMemoryMB > MAX_BUFFER_SIZE_MB) {
|
|
2029
|
+
// Need to batch
|
|
2030
|
+
const maxAnglesPerBatch = Math.floor((MAX_BUFFER_SIZE_MB * 1024 * 1024) / bytesPerAngle);
|
|
2031
|
+
// Apply batch divisor for overhead testing
|
|
2032
|
+
const adjustedMaxAngles = Math.floor(maxAnglesPerBatch / batchDivisor);
|
|
2033
|
+
|
|
2034
|
+
ANGLES_PER_BATCH = Math.max(1, Math.min(adjustedMaxAngles, numAngles));
|
|
2035
|
+
numBatches = Math.ceil(numAngles / ANGLES_PER_BATCH);
|
|
2036
|
+
const batchSizeMB = (ANGLES_PER_BATCH * bytesPerAngle / 1024 / 1024).toFixed(1);
|
|
2037
|
+
debug.log(`Grid: ${gridXSize} x ${gridYHeight}, ${cellsPerAngle.toLocaleString()} cells/angle`);
|
|
2038
|
+
debug.log(`Total memory: ${totalMemoryMB.toFixed(1)}MB exceeds limit, batching required`);
|
|
2039
|
+
if (batchDivisor > 1) {
|
|
2040
|
+
debug.log(`batchDivisor: ${batchDivisor}x (testing overhead: ${maxAnglesPerBatch} → ${adjustedMaxAngles} angles/batch)`);
|
|
2041
|
+
}
|
|
2042
|
+
debug.log(`Batch size: ${ANGLES_PER_BATCH} angles (~${batchSizeMB}MB per batch)`);
|
|
2043
|
+
debug.log(`Processing ${numAngles} angles in ${numBatches} batch(es)`);
|
|
2044
|
+
} else {
|
|
2045
|
+
// Process all angles at once (but still respect batchDivisor for testing)
|
|
2046
|
+
if (batchDivisor > 1) {
|
|
2047
|
+
ANGLES_PER_BATCH = Math.max(10, Math.floor(numAngles / batchDivisor));
|
|
2048
|
+
numBatches = Math.ceil(numAngles / ANGLES_PER_BATCH);
|
|
2049
|
+
debug.log(`Grid: ${gridXSize} x ${gridYHeight}, ${cellsPerAngle.toLocaleString()} cells/angle`);
|
|
2050
|
+
debug.log(`Total memory: ${totalMemoryMB.toFixed(1)}MB (fits in buffer normally)`);
|
|
2051
|
+
debug.log(`batchDivisor: ${batchDivisor}x (artificially creating ${numBatches} batches for overhead testing)`);
|
|
2052
|
+
} else {
|
|
2053
|
+
ANGLES_PER_BATCH = numAngles;
|
|
2054
|
+
numBatches = 1;
|
|
2055
|
+
debug.log(`Grid: ${gridXSize} x ${gridYHeight}, ${cellsPerAngle.toLocaleString()} cells/angle`);
|
|
2056
|
+
debug.log(`Total memory: ${totalMemoryMB.toFixed(1)}MB fits in buffer, processing all ${numAngles} angles in single batch`);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
const allStripToolpaths = [];
|
|
2061
|
+
let totalToolpathPoints = 0;
|
|
2062
|
+
const pipelineStartTime = performance.now();
|
|
2063
|
+
|
|
2064
|
+
// Prepare sparse tool once
|
|
2065
|
+
const sparseToolData = createSparseToolFromPoints(toolData.positions);
|
|
2066
|
+
debug.log(`Created sparse tool: ${sparseToolData.count} points (reusing for all strips)`);
|
|
2067
|
+
|
|
2068
|
+
// Create reusable rasterization buffers if batching (numBatches > 1)
|
|
2069
|
+
// These buffers (triangles, buckets, indices) don't change between batches
|
|
2070
|
+
let batchReuseBuffers = null;
|
|
2071
|
+
let batchTracking = [];
|
|
2072
|
+
|
|
2073
|
+
for (let batchIdx = 0; batchIdx < numBatches; batchIdx++) {
|
|
2074
|
+
const batchStartTime = performance.now();
|
|
2075
|
+
const startAngleIdx = batchIdx * ANGLES_PER_BATCH;
|
|
2076
|
+
const endAngleIdx = Math.min(startAngleIdx + ANGLES_PER_BATCH, numAngles);
|
|
2077
|
+
const batchNumAngles = endAngleIdx - startAngleIdx;
|
|
2078
|
+
const batchStartAngle = startAngleIdx * angleStep;
|
|
2079
|
+
|
|
2080
|
+
const batchInfo = {
|
|
2081
|
+
from: startAngleIdx,
|
|
2082
|
+
to: endAngleIdx
|
|
2083
|
+
};
|
|
2084
|
+
batchTracking.push(batchInfo);
|
|
2085
|
+
|
|
2086
|
+
debug.log(`Batch ${batchIdx + 1}/${numBatches}: angles ${startAngleIdx}-${endAngleIdx - 1} (${batchNumAngles} angles), startAngle=${batchStartAngle.toFixed(1)}°`);
|
|
2087
|
+
|
|
2088
|
+
// Rasterize this batch of strips
|
|
2089
|
+
const rasterStartTime = performance.now();
|
|
2090
|
+
const shouldReturnBuffers = (batchIdx === 0 && numBatches > 1); // First batch of multi-batch operation
|
|
2091
|
+
const batchModelResult = await radialRasterize({
|
|
2092
|
+
triangles,
|
|
2093
|
+
bucketData,
|
|
2094
|
+
resolution,
|
|
2095
|
+
angleStep,
|
|
2096
|
+
numAngles: batchNumAngles,
|
|
2097
|
+
maxRadius,
|
|
2098
|
+
toolWidth,
|
|
2099
|
+
zFloor,
|
|
2100
|
+
bounds,
|
|
2101
|
+
startAngle: batchStartAngle,
|
|
2102
|
+
reusableBuffers: batchReuseBuffers,
|
|
2103
|
+
returnBuffersForReuse: shouldReturnBuffers,
|
|
2104
|
+
batchInfo
|
|
2105
|
+
});
|
|
2106
|
+
|
|
2107
|
+
const rasterTime = performance.now() - rasterStartTime;
|
|
2108
|
+
|
|
2109
|
+
// Capture buffers from first batch for reuse
|
|
2110
|
+
if (batchIdx === 0 && batchModelResult.reusableBuffers) {
|
|
2111
|
+
batchReuseBuffers = batchModelResult.reusableBuffers;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// Find max dimensions for this batch
|
|
2115
|
+
let maxStripWidth = 0;
|
|
2116
|
+
let maxStripHeight = 0;
|
|
2117
|
+
for (const strip of batchModelResult.strips) {
|
|
2118
|
+
maxStripWidth = Math.max(maxStripWidth, strip.gridWidth);
|
|
2119
|
+
maxStripHeight = Math.max(maxStripHeight, strip.gridHeight);
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
// Create reusable buffers for this batch
|
|
2123
|
+
const reusableBuffers = createReusableToolpathBuffers(maxStripWidth, maxStripHeight, sparseToolData, xStep, maxStripHeight);
|
|
2124
|
+
|
|
2125
|
+
// Generate toolpaths for this batch
|
|
2126
|
+
const toolpathStartTime = performance.now();
|
|
2127
|
+
|
|
2128
|
+
for (let i = 0; i < batchModelResult.strips.length; i++) {
|
|
2129
|
+
const strip = batchModelResult.strips[i];
|
|
2130
|
+
const globalStripIdx = startAngleIdx + i;
|
|
2131
|
+
|
|
2132
|
+
if (globalStripIdx % 10 === 0 || globalStripIdx === numAngles - 1) {
|
|
2133
|
+
self.postMessage({
|
|
2134
|
+
type: 'toolpath-progress',
|
|
2135
|
+
data: {
|
|
2136
|
+
percent: Math.round(((globalStripIdx + 1) / numAngles) * 100),
|
|
2137
|
+
current: globalStripIdx + 1,
|
|
2138
|
+
total: numAngles,
|
|
2139
|
+
layer: globalStripIdx + 1
|
|
2140
|
+
}
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
if (!strip.positions || strip.positions.length === 0) continue;
|
|
2145
|
+
|
|
2146
|
+
// DEBUG: Diagnostic logging
|
|
2147
|
+
if (diagnostic && (globalStripIdx === 0 || globalStripIdx === 360)) {
|
|
2148
|
+
debug.log(`BUILD_ID_PLACEHOLDER | Strip ${globalStripIdx} (${strip.angle.toFixed(1)}°) INPUT terrain first 5 Z values: ${strip.positions.slice(0, 5).map(v => v.toFixed(3)).join(',')}`);
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
const stripToolpathResult = await runToolpathComputeWithBuffers(
|
|
2152
|
+
strip.positions,
|
|
2153
|
+
strip.gridWidth,
|
|
2154
|
+
strip.gridHeight,
|
|
2155
|
+
xStep,
|
|
2156
|
+
strip.gridHeight,
|
|
2157
|
+
zFloor,
|
|
2158
|
+
reusableBuffers,
|
|
2159
|
+
pipelineStartTime
|
|
2160
|
+
);
|
|
2161
|
+
|
|
2162
|
+
// DEBUG: Verify toolpath generation output
|
|
2163
|
+
if (diagnostic && (globalStripIdx === 0 || globalStripIdx === 360)) {
|
|
2164
|
+
debug.log(`BUILD_ID_PLACEHOLDER | Strip ${globalStripIdx} (${strip.angle.toFixed(1)}°) OUTPUT toolpath first 5 Z values: ${stripToolpathResult.pathData.slice(0, 5).map(v => v.toFixed(3)).join(',')}`);
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
allStripToolpaths.push({
|
|
2168
|
+
angle: strip.angle,
|
|
2169
|
+
pathData: stripToolpathResult.pathData,
|
|
2170
|
+
numScanlines: stripToolpathResult.numScanlines,
|
|
2171
|
+
pointsPerLine: stripToolpathResult.pointsPerLine,
|
|
2172
|
+
terrainBounds: strip.bounds
|
|
2173
|
+
});
|
|
2174
|
+
|
|
2175
|
+
totalToolpathPoints += stripToolpathResult.pathData.length;
|
|
2176
|
+
}
|
|
2177
|
+
const toolpathTime = performance.now() - toolpathStartTime;
|
|
2295
2178
|
|
|
2296
|
-
|
|
2179
|
+
// Free batch terrain data
|
|
2180
|
+
for (const strip of batchModelResult.strips) {
|
|
2181
|
+
strip.positions = null;
|
|
2182
|
+
}
|
|
2183
|
+
destroyReusableToolpathBuffers(reusableBuffers);
|
|
2184
|
+
|
|
2185
|
+
const batchTotalTime = performance.now() - batchStartTime;
|
|
2186
|
+
|
|
2187
|
+
Object.assign(batchInfo, {
|
|
2188
|
+
'prep': batchInfo.prep || 0,
|
|
2189
|
+
'gpu': batchInfo.gpu || 0,
|
|
2190
|
+
'stitch': batchInfo.stitch || 0,
|
|
2191
|
+
'raster': batchInfo.raster || 0,
|
|
2192
|
+
'mkbuf': 0,
|
|
2193
|
+
'paths': (toolpathTime | 0),
|
|
2194
|
+
'strips': allStripToolpaths.length,
|
|
2195
|
+
'total': (batchTotalTime | 0)
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
console.table(batchTracking);
|
|
2200
|
+
|
|
2201
|
+
// Cleanup cached rasterization buffers after all batches complete
|
|
2202
|
+
if (batchReuseBuffers) {
|
|
2203
|
+
batchReuseBuffers.triangleBuffer.destroy();
|
|
2204
|
+
batchReuseBuffers.triangleIndicesBuffer.destroy();
|
|
2205
|
+
// Note: bucketInfoBuffer is no longer in reusableBuffers (created/destroyed per bucket batch)
|
|
2206
|
+
debug.log(`Destroyed cached GPU buffers after all batches`);
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
const pipelineTotalTime = performance.now() - pipelineStartTime;
|
|
2210
|
+
debug.log(`Complete radial toolpath: ${allStripToolpaths.length} strips, ${totalToolpathPoints} total points in ${pipelineTotalTime.toFixed(0)}ms`);
|
|
2211
|
+
|
|
2212
|
+
return {
|
|
2213
|
+
strips: allStripToolpaths,
|
|
2214
|
+
totalPoints: totalToolpathPoints,
|
|
2215
|
+
numStrips: allStripToolpaths.length
|
|
2216
|
+
};
|
|
2297
2217
|
}
|
|
2298
2218
|
|
|
2299
2219
|
// Handle messages from main thread
|
|
@@ -2309,7 +2229,8 @@ self.onmessage = async function(e) {
|
|
|
2309
2229
|
gpuMemorySafetyMargin: 0.8,
|
|
2310
2230
|
tileOverlapMM: 10,
|
|
2311
2231
|
autoTiling: true,
|
|
2312
|
-
minTileSize: 50
|
|
2232
|
+
minTileSize: 50,
|
|
2233
|
+
batchDivisor: 1 // For testing batching overhead: 1=optimal, 2=2x batches, 4=4x batches, etc.
|
|
2313
2234
|
};
|
|
2314
2235
|
const success = await initWebGPU();
|
|
2315
2236
|
self.postMessage({
|
|
@@ -2357,149 +2278,11 @@ self.onmessage = async function(e) {
|
|
|
2357
2278
|
break;
|
|
2358
2279
|
|
|
2359
2280
|
case 'radial-generate-toolpaths':
|
|
2360
|
-
|
|
2361
|
-
const
|
|
2362
|
-
triangles: radialModelTriangles,
|
|
2363
|
-
bucketData: radialBucketData,
|
|
2364
|
-
toolData: radialToolData,
|
|
2365
|
-
resolution: radialResolution,
|
|
2366
|
-
angleStep: radialAngleStep,
|
|
2367
|
-
numAngles: radialNumAngles,
|
|
2368
|
-
maxRadius: radialMaxRadius,
|
|
2369
|
-
toolWidth: radialToolWidth,
|
|
2370
|
-
zFloor: radialToolpathZFloor,
|
|
2371
|
-
bounds: radialToolpathBounds,
|
|
2372
|
-
xStep: radialXStep,
|
|
2373
|
-
yStep: radialYStep,
|
|
2374
|
-
gridStep: radialGridStep
|
|
2375
|
-
} = data;
|
|
2376
|
-
|
|
2377
|
-
debug.log('[Worker] Starting complete radial toolpath pipeline...');
|
|
2378
|
-
|
|
2379
|
-
// Batch processing: rasterize angle ranges to avoid memory allocation failure
|
|
2380
|
-
const ANGLES_PER_BATCH = 360; // Process 360 angles at a time
|
|
2381
|
-
const numBatches = Math.ceil(radialNumAngles / ANGLES_PER_BATCH);
|
|
2382
|
-
|
|
2383
|
-
debug.log(`[Worker] Processing ${radialNumAngles} angles in ${numBatches} batch(es) of up to ${ANGLES_PER_BATCH} angles`);
|
|
2384
|
-
|
|
2385
|
-
const allStripToolpaths = [];
|
|
2386
|
-
let totalToolpathPoints = 0;
|
|
2387
|
-
const pipelineStartTime = performance.now();
|
|
2388
|
-
|
|
2389
|
-
// Prepare sparse tool once
|
|
2390
|
-
const sparseToolData = createSparseToolFromPoints(radialToolData.positions);
|
|
2391
|
-
debug.log(`[Worker] Created sparse tool: ${sparseToolData.count} points (reusing for all strips)`);
|
|
2392
|
-
|
|
2393
|
-
for (let batchIdx = 0; batchIdx < numBatches; batchIdx++) {
|
|
2394
|
-
const startAngleIdx = batchIdx * ANGLES_PER_BATCH;
|
|
2395
|
-
const endAngleIdx = Math.min(startAngleIdx + ANGLES_PER_BATCH, radialNumAngles);
|
|
2396
|
-
const batchNumAngles = endAngleIdx - startAngleIdx;
|
|
2397
|
-
const batchStartAngle = startAngleIdx * radialAngleStep;
|
|
2398
|
-
|
|
2399
|
-
debug.log(`[Worker] Batch ${batchIdx + 1}/${numBatches}: angles ${startAngleIdx}-${endAngleIdx - 1} (${batchNumAngles} angles), startAngle=${batchStartAngle.toFixed(1)}°`);
|
|
2400
|
-
|
|
2401
|
-
// Rasterize this batch of strips
|
|
2402
|
-
const batchModelResult = await radialRasterizeV2(
|
|
2403
|
-
radialModelTriangles,
|
|
2404
|
-
radialBucketData,
|
|
2405
|
-
radialResolution,
|
|
2406
|
-
radialAngleStep,
|
|
2407
|
-
batchNumAngles,
|
|
2408
|
-
radialMaxRadius,
|
|
2409
|
-
radialToolWidth,
|
|
2410
|
-
radialToolpathZFloor,
|
|
2411
|
-
radialToolpathBounds,
|
|
2412
|
-
batchStartAngle // Start angle for this batch
|
|
2413
|
-
);
|
|
2414
|
-
|
|
2415
|
-
debug.log(`[Worker] Batch ${batchIdx + 1}: Rasterized ${batchModelResult.strips.length} strips, first angle=${batchModelResult.strips[0]?.angle.toFixed(1)}°, last angle=${batchModelResult.strips[batchModelResult.strips.length - 1]?.angle.toFixed(1)}°`);
|
|
2416
|
-
|
|
2417
|
-
// Find max dimensions for this batch
|
|
2418
|
-
let maxStripWidth = 0;
|
|
2419
|
-
let maxStripHeight = 0;
|
|
2420
|
-
for (const strip of batchModelResult.strips) {
|
|
2421
|
-
maxStripWidth = Math.max(maxStripWidth, strip.gridWidth);
|
|
2422
|
-
maxStripHeight = Math.max(maxStripHeight, strip.gridHeight);
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
|
-
// Create reusable buffers for this batch
|
|
2426
|
-
const reusableBuffers = createReusableToolpathBuffers(maxStripWidth, maxStripHeight, sparseToolData, radialXStep, maxStripHeight);
|
|
2427
|
-
|
|
2428
|
-
// Generate toolpaths for this batch
|
|
2429
|
-
debug.log(`[Worker] Batch ${batchIdx + 1}: Generating toolpaths for ${batchModelResult.strips.length} strips...`);
|
|
2430
|
-
for (let i = 0; i < batchModelResult.strips.length; i++) {
|
|
2431
|
-
const strip = batchModelResult.strips[i];
|
|
2432
|
-
const globalStripIdx = startAngleIdx + i;
|
|
2433
|
-
|
|
2434
|
-
if (globalStripIdx % 10 === 0 || globalStripIdx === radialNumAngles - 1) {
|
|
2435
|
-
self.postMessage({
|
|
2436
|
-
type: 'toolpath-progress',
|
|
2437
|
-
data: {
|
|
2438
|
-
percent: Math.round(((globalStripIdx + 1) / radialNumAngles) * 100),
|
|
2439
|
-
current: globalStripIdx + 1,
|
|
2440
|
-
total: radialNumAngles,
|
|
2441
|
-
layer: globalStripIdx + 1
|
|
2442
|
-
}
|
|
2443
|
-
});
|
|
2444
|
-
}
|
|
2445
|
-
|
|
2446
|
-
if (!strip.positions || strip.positions.length === 0) continue;
|
|
2447
|
-
|
|
2448
|
-
// DEBUG: Diagnostic logging (BUILD_ID gets injected during build)
|
|
2449
|
-
// Used to trace data flow through radial toolpath pipeline
|
|
2450
|
-
if (globalStripIdx === 0 || globalStripIdx === 360) {
|
|
2451
|
-
debug.log(`[Worker] BUILD_ID_PLACEHOLDER | Strip ${globalStripIdx} (${strip.angle.toFixed(1)}°) INPUT terrain first 5 Z values: ${strip.positions.slice(0, 5).map(v => v.toFixed(3)).join(',')}`);
|
|
2452
|
-
}
|
|
2453
|
-
|
|
2454
|
-
const stripToolpathResult = await runToolpathComputeWithBuffers(
|
|
2455
|
-
strip.positions,
|
|
2456
|
-
strip.gridWidth,
|
|
2457
|
-
strip.gridHeight,
|
|
2458
|
-
radialXStep,
|
|
2459
|
-
strip.gridHeight,
|
|
2460
|
-
radialToolpathZFloor,
|
|
2461
|
-
reusableBuffers,
|
|
2462
|
-
pipelineStartTime
|
|
2463
|
-
);
|
|
2464
|
-
|
|
2465
|
-
// DEBUG: Verify toolpath generation output
|
|
2466
|
-
if (globalStripIdx === 0 || globalStripIdx === 360) {
|
|
2467
|
-
debug.log(`[Worker] BUILD_ID_PLACEHOLDER | Strip ${globalStripIdx} (${strip.angle.toFixed(1)}°) OUTPUT toolpath first 5 Z values: ${stripToolpathResult.pathData.slice(0, 5).map(v => v.toFixed(3)).join(',')}`);
|
|
2468
|
-
}
|
|
2469
|
-
|
|
2470
|
-
allStripToolpaths.push({
|
|
2471
|
-
angle: strip.angle,
|
|
2472
|
-
pathData: stripToolpathResult.pathData,
|
|
2473
|
-
numScanlines: stripToolpathResult.numScanlines,
|
|
2474
|
-
pointsPerLine: stripToolpathResult.pointsPerLine,
|
|
2475
|
-
terrainBounds: strip.bounds // Include terrain bounds for display
|
|
2476
|
-
});
|
|
2477
|
-
|
|
2478
|
-
totalToolpathPoints += stripToolpathResult.pathData.length;
|
|
2479
|
-
}
|
|
2480
|
-
|
|
2481
|
-
destroyReusableToolpathBuffers(reusableBuffers);
|
|
2482
|
-
|
|
2483
|
-
debug.log(`[Worker] Batch ${batchIdx + 1}: Completed, allStripToolpaths now has ${allStripToolpaths.length} strips total`);
|
|
2484
|
-
|
|
2485
|
-
// Free batch terrain data
|
|
2486
|
-
for (const strip of batchModelResult.strips) {
|
|
2487
|
-
strip.positions = null;
|
|
2488
|
-
}
|
|
2489
|
-
}
|
|
2490
|
-
|
|
2491
|
-
const pipelineTotalTime = performance.now() - pipelineStartTime;
|
|
2492
|
-
debug.log(`[Worker] Complete radial toolpath: ${allStripToolpaths.length} strips, ${totalToolpathPoints} total points in ${pipelineTotalTime.toFixed(0)}ms`);
|
|
2493
|
-
|
|
2494
|
-
const toolpathTransferBuffers = allStripToolpaths.map(strip => strip.pathData.buffer);
|
|
2495
|
-
|
|
2281
|
+
const radialToolpathResult = await generateRadialToolpaths(data);
|
|
2282
|
+
const toolpathTransferBuffers = radialToolpathResult.strips.map(strip => strip.pathData.buffer);
|
|
2496
2283
|
self.postMessage({
|
|
2497
2284
|
type: 'radial-toolpaths-complete',
|
|
2498
|
-
data:
|
|
2499
|
-
strips: allStripToolpaths,
|
|
2500
|
-
totalPoints: totalToolpathPoints,
|
|
2501
|
-
numStrips: allStripToolpaths.length
|
|
2502
|
-
}
|
|
2285
|
+
data: radialToolpathResult
|
|
2503
2286
|
}, toolpathTransferBuffers);
|
|
2504
2287
|
break;
|
|
2505
2288
|
|