@gridspace/raster-path 1.0.3 → 1.0.5
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 +40 -2
- package/build/raster-path.js +16 -24
- package/build/raster-worker.js +2450 -0
- package/build/style.css +65 -0
- package/package.json +12 -4
- package/scripts/build-shaders.js +32 -8
- package/src/core/path-planar.js +788 -0
- package/src/core/path-radial.js +651 -0
- package/src/core/raster-config.js +185 -0
- package/src/{index.js → core/raster-path.js} +16 -24
- package/src/core/raster-planar.js +754 -0
- package/src/core/raster-tool.js +104 -0
- package/src/core/raster-worker.js +152 -0
- package/src/core/workload-calibrate.js +416 -0
- package/src/shaders/{radial-raster-v2.wgsl → radial-raster.wgsl} +8 -2
- package/src/shaders/workload-calibrate.wgsl +106 -0
- package/src/test/batch-divisor-benchmark.cjs +286 -0
- package/src/test/calibrate-test.cjs +136 -0
- package/src/test/extreme-work-test.cjs +167 -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/radial-thread-limit-test.cjs +152 -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 +40 -2
- package/src/web/style.css +65 -0
- package/src/workload-calculator.js +318 -0
- package/build/webgpu-worker.js +0 -3011
- package/src/web/webgpu-worker.js +0 -2520
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// extreme-work-test.cjs
|
|
2
|
+
// Test if there's ANY per-thread compute limit by pushing to extreme levels
|
|
3
|
+
|
|
4
|
+
const { app, BrowserWindow } = require('electron');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
let mainWindow;
|
|
8
|
+
|
|
9
|
+
function createWindow() {
|
|
10
|
+
mainWindow = new BrowserWindow({
|
|
11
|
+
width: 1200,
|
|
12
|
+
height: 800,
|
|
13
|
+
show: false,
|
|
14
|
+
webPreferences: {
|
|
15
|
+
nodeIntegration: false,
|
|
16
|
+
contextIsolation: true,
|
|
17
|
+
enableBlinkFeatures: 'WebGPU',
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const htmlPath = path.join(__dirname, '../../build/index.html');
|
|
22
|
+
mainWindow.loadFile(htmlPath);
|
|
23
|
+
|
|
24
|
+
mainWindow.webContents.on('did-finish-load', async () => {
|
|
25
|
+
console.log('✓ Page loaded');
|
|
26
|
+
|
|
27
|
+
const testScript = `
|
|
28
|
+
(async function() {
|
|
29
|
+
console.log('=== Extreme Per-Thread Work Test ===');
|
|
30
|
+
console.log('Testing if there is ANY limit to work per thread...\\n');
|
|
31
|
+
|
|
32
|
+
if (!navigator.gpu) {
|
|
33
|
+
return { error: 'WebGPU not available' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { RasterPath } = await import('./raster-path.js');
|
|
37
|
+
|
|
38
|
+
const raster = new RasterPath({ mode: 'planar', resolution: 0.1 });
|
|
39
|
+
await raster.init();
|
|
40
|
+
|
|
41
|
+
// Test progressively larger workloads on a SINGLE 16x16x1 workgroup (256 threads)
|
|
42
|
+
const testLevels = [
|
|
43
|
+
{ tests: 1_000_000_000, label: '1 billion' },
|
|
44
|
+
{ tests: 2_000_000_000, label: '2 billion' },
|
|
45
|
+
{ tests: 5_000_000_000, label: '5 billion' },
|
|
46
|
+
{ tests: 10_000_000_000, label: '10 billion' },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const results = [];
|
|
50
|
+
|
|
51
|
+
for (const level of testLevels) {
|
|
52
|
+
console.log(\`Testing \${level.label} tests/thread (\${level.tests.toLocaleString()})...\`);
|
|
53
|
+
|
|
54
|
+
const calibrationPromise = new Promise((resolve, reject) => {
|
|
55
|
+
const timeout = setTimeout(() => {
|
|
56
|
+
reject(new Error('Test timed out after 30s'));
|
|
57
|
+
}, 30000);
|
|
58
|
+
|
|
59
|
+
const handler = raster.worker.onmessage;
|
|
60
|
+
raster.worker.onmessage = (e) => {
|
|
61
|
+
if (e.data.type === 'calibrate-complete') {
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
resolve(e.data.data);
|
|
64
|
+
} else if (e.data.type === 'error') {
|
|
65
|
+
clearTimeout(timeout);
|
|
66
|
+
reject(new Error(e.data.message));
|
|
67
|
+
} else {
|
|
68
|
+
handler(e);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const startTime = performance.now();
|
|
74
|
+
|
|
75
|
+
raster.worker.postMessage({
|
|
76
|
+
type: 'calibrate',
|
|
77
|
+
data: {
|
|
78
|
+
calibrationType: 'workgroup',
|
|
79
|
+
options: {
|
|
80
|
+
workgroupSizes: [[16, 16, 1]],
|
|
81
|
+
minWork: level.tests,
|
|
82
|
+
maxWork: level.tests,
|
|
83
|
+
verbose: false,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const result = await calibrationPromise;
|
|
90
|
+
const elapsed = performance.now() - startTime;
|
|
91
|
+
const success = result.safeWorkloadMatrix[0]?.maxWork === level.tests;
|
|
92
|
+
|
|
93
|
+
const status = success ? '✓' : '❌';
|
|
94
|
+
const totalTests = (level.tests * 256).toLocaleString();
|
|
95
|
+
console.log(\` \${status} \${level.label}: \${elapsed.toFixed(0)}ms (\${totalTests} total tests)\\n\`);
|
|
96
|
+
|
|
97
|
+
results.push({
|
|
98
|
+
tests: level.tests,
|
|
99
|
+
label: level.label,
|
|
100
|
+
success,
|
|
101
|
+
elapsed,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (!success) {
|
|
105
|
+
console.log('Found failure point - stopping test.');
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.log(\` ❌ \${level.label}: FAILED - \${error.message}\\n\`);
|
|
110
|
+
results.push({
|
|
111
|
+
tests: level.tests,
|
|
112
|
+
label: level.label,
|
|
113
|
+
success: false,
|
|
114
|
+
error: error.message,
|
|
115
|
+
});
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log('\\n=== Summary ===');
|
|
121
|
+
for (const r of results) {
|
|
122
|
+
const status = r.success ? '✓' : '❌';
|
|
123
|
+
const time = r.elapsed ? \` in \${r.elapsed.toFixed(0)}ms\` : '';
|
|
124
|
+
const err = r.error ? \` - \${r.error}\` : '';
|
|
125
|
+
console.log(\`\${status} \${r.label}\${time}\${err}\`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const maxSuccess = results.filter(r => r.success).pop();
|
|
129
|
+
if (maxSuccess) {
|
|
130
|
+
console.log(\`\\nMax verified work per thread: \${maxSuccess.label} (\${maxSuccess.tests.toLocaleString()} tests)\`);
|
|
131
|
+
console.log('Conclusion: No practical per-thread compute limit detected');
|
|
132
|
+
} else {
|
|
133
|
+
console.log('\\nFound per-thread compute limit');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { success: true, results };
|
|
137
|
+
})();
|
|
138
|
+
`;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const result = await mainWindow.webContents.executeJavaScript(testScript);
|
|
142
|
+
|
|
143
|
+
if (result.error) {
|
|
144
|
+
console.error('❌ Test failed:', result.error);
|
|
145
|
+
app.exit(1);
|
|
146
|
+
} else {
|
|
147
|
+
console.log('\n✅ Extreme work test complete');
|
|
148
|
+
app.exit(0);
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error('❌ Test error:', error);
|
|
152
|
+
app.exit(1);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
mainWindow.webContents.on('console-message', (event, level, message) => {
|
|
157
|
+
console.log(message);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
app.whenReady().then(createWindow);
|
|
162
|
+
|
|
163
|
+
app.on('window-all-closed', () => {
|
|
164
|
+
if (process.platform !== 'darwin') {
|
|
165
|
+
app.quit();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// lathe-cylinder-2-debug.cjs
|
|
2
|
+
// Debug test for lathe-cylinder-2.stl with specific parameters
|
|
3
|
+
// Run multiple times to check for non-deterministic behavior
|
|
4
|
+
|
|
5
|
+
const { app, BrowserWindow } = require('electron');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const TEST_ITERATIONS = 5; // Run test 5 times to check consistency
|
|
9
|
+
|
|
10
|
+
const TEST_CONFIG = {
|
|
11
|
+
model: '../benchmark/fixtures/lathe-cylinder-2.stl',
|
|
12
|
+
tool: '../benchmark/fixtures/tool.stl',
|
|
13
|
+
resolution: 0.05,
|
|
14
|
+
rotationStep: 1.0,
|
|
15
|
+
toolDiameter: 5.0, // Scale tool to 5mm
|
|
16
|
+
xStep: 1,
|
|
17
|
+
yStep: 1
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
console.log('=== Lathe Cylinder 2 Debug Test ===');
|
|
21
|
+
console.log('Testing for non-deterministic behavior / missing toolpaths');
|
|
22
|
+
console.log(`Model: lathe-cylinder-2.stl`);
|
|
23
|
+
console.log(`Resolution: ${TEST_CONFIG.resolution}mm`);
|
|
24
|
+
console.log(`Tool size: ${TEST_CONFIG.toolDiameter}mm`);
|
|
25
|
+
console.log(`Rotation step: ${TEST_CONFIG.rotationStep}°`);
|
|
26
|
+
console.log(`XY steps: ${TEST_CONFIG.xStep}`);
|
|
27
|
+
console.log(`Iterations: ${TEST_ITERATIONS}`);
|
|
28
|
+
console.log('');
|
|
29
|
+
|
|
30
|
+
let win;
|
|
31
|
+
let testResults = [];
|
|
32
|
+
let currentIteration = 0;
|
|
33
|
+
|
|
34
|
+
app.whenReady().then(async () => {
|
|
35
|
+
win = new BrowserWindow({
|
|
36
|
+
width: 1200,
|
|
37
|
+
height: 800,
|
|
38
|
+
show: false,
|
|
39
|
+
webPreferences: {
|
|
40
|
+
nodeIntegration: false,
|
|
41
|
+
contextIsolation: true
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Capture console messages
|
|
46
|
+
win.webContents.on('console-message', (event, level, message) => {
|
|
47
|
+
console.log(message);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await win.loadFile(path.join(__dirname, '../../build/index.html'));
|
|
51
|
+
console.log('✓ Page loaded\n');
|
|
52
|
+
|
|
53
|
+
// Run test multiple times
|
|
54
|
+
await runNextIteration();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
async function runNextIteration() {
|
|
58
|
+
if (currentIteration >= TEST_ITERATIONS) {
|
|
59
|
+
// All iterations complete - analyze results
|
|
60
|
+
analyzeResults();
|
|
61
|
+
app.quit();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
currentIteration++;
|
|
66
|
+
console.log('======================================================================');
|
|
67
|
+
console.log(`ITERATION ${currentIteration} of ${TEST_ITERATIONS}`);
|
|
68
|
+
console.log('======================================================================');
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const result = await runTest();
|
|
72
|
+
testResults.push(result);
|
|
73
|
+
|
|
74
|
+
// Wait a bit between iterations
|
|
75
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
76
|
+
|
|
77
|
+
// Run next iteration
|
|
78
|
+
await runNextIteration();
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error(`✗ Iteration ${currentIteration} failed:`, error);
|
|
81
|
+
testResults.push({
|
|
82
|
+
iteration: currentIteration,
|
|
83
|
+
success: false,
|
|
84
|
+
error: error.message
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Continue with next iteration even on failure
|
|
88
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
89
|
+
await runNextIteration();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function runTest() {
|
|
94
|
+
const result = await win.webContents.executeJavaScript(`
|
|
95
|
+
(async () => {
|
|
96
|
+
const startTime = performance.now();
|
|
97
|
+
|
|
98
|
+
// Load model
|
|
99
|
+
const modelResponse = await fetch('${TEST_CONFIG.model}');
|
|
100
|
+
const modelBuffer = await modelResponse.arrayBuffer();
|
|
101
|
+
|
|
102
|
+
// Parse model triangles
|
|
103
|
+
function parseSTL(arrayBuffer) {
|
|
104
|
+
const view = new DataView(arrayBuffer);
|
|
105
|
+
const numTriangles = view.getUint32(80, true);
|
|
106
|
+
const triangles = new Float32Array(numTriangles * 9);
|
|
107
|
+
|
|
108
|
+
let offset = 84;
|
|
109
|
+
for (let i = 0; i < numTriangles; i++) {
|
|
110
|
+
offset += 12; // Skip normal
|
|
111
|
+
for (let v = 0; v < 3; v++) {
|
|
112
|
+
triangles[i * 9 + v * 3 + 0] = view.getFloat32(offset + 0, true);
|
|
113
|
+
triangles[i * 9 + v * 3 + 1] = view.getFloat32(offset + 4, true);
|
|
114
|
+
triangles[i * 9 + v * 3 + 2] = view.getFloat32(offset + 8, true);
|
|
115
|
+
offset += 12;
|
|
116
|
+
}
|
|
117
|
+
offset += 2; // Skip attribute
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return triangles;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const modelTriangles = parseSTL(modelBuffer);
|
|
124
|
+
|
|
125
|
+
// Load and scale tool
|
|
126
|
+
const toolResponse = await fetch('${TEST_CONFIG.tool}');
|
|
127
|
+
const toolBuffer = await toolResponse.arrayBuffer();
|
|
128
|
+
const toolTriangles = parseSTL(toolBuffer);
|
|
129
|
+
|
|
130
|
+
// Scale tool to target diameter (${TEST_CONFIG.toolDiameter}mm)
|
|
131
|
+
function calculateToolDiameter(triangles) {
|
|
132
|
+
let minX = Infinity, maxX = -Infinity;
|
|
133
|
+
let minY = Infinity, maxY = -Infinity;
|
|
134
|
+
for (let i = 0; i < triangles.length; i += 3) {
|
|
135
|
+
minX = Math.min(minX, triangles[i]);
|
|
136
|
+
maxX = Math.max(maxX, triangles[i]);
|
|
137
|
+
minY = Math.min(minY, triangles[i + 1]);
|
|
138
|
+
maxY = Math.max(maxY, triangles[i + 1]);
|
|
139
|
+
}
|
|
140
|
+
return Math.max(maxX - minX, maxY - minY);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const originalDiameter = calculateToolDiameter(toolTriangles);
|
|
144
|
+
const scale = ${TEST_CONFIG.toolDiameter} / originalDiameter;
|
|
145
|
+
const scaledToolTriangles = new Float32Array(toolTriangles.length);
|
|
146
|
+
for (let i = 0; i < toolTriangles.length; i++) {
|
|
147
|
+
scaledToolTriangles[i] = toolTriangles[i] * scale;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Import worker
|
|
151
|
+
const { RasterPath } = await import('./raster-path.js');
|
|
152
|
+
const rasterPath = new RasterPath({
|
|
153
|
+
mode: 'radial',
|
|
154
|
+
resolution: ${TEST_CONFIG.resolution},
|
|
155
|
+
rotationStep: ${TEST_CONFIG.rotationStep}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Initialize
|
|
159
|
+
await rasterPath.init();
|
|
160
|
+
|
|
161
|
+
const toolStart = performance.now();
|
|
162
|
+
await rasterPath.loadTool({ triangles: scaledToolTriangles });
|
|
163
|
+
const toolTime = performance.now() - toolStart;
|
|
164
|
+
|
|
165
|
+
const terrainStart = performance.now();
|
|
166
|
+
await rasterPath.loadTerrain({
|
|
167
|
+
triangles: modelTriangles,
|
|
168
|
+
zFloor: 0
|
|
169
|
+
});
|
|
170
|
+
const terrainTime = performance.now() - terrainStart;
|
|
171
|
+
|
|
172
|
+
const toolpathStart = performance.now();
|
|
173
|
+
const toolpathResult = await rasterPath.generateToolpaths({
|
|
174
|
+
xStep: ${TEST_CONFIG.xStep},
|
|
175
|
+
yStep: ${TEST_CONFIG.yStep},
|
|
176
|
+
zFloor: 0,
|
|
177
|
+
radiusOffset: 20
|
|
178
|
+
});
|
|
179
|
+
const toolpathTime = performance.now() - toolpathStart;
|
|
180
|
+
|
|
181
|
+
const totalTime = performance.now() - startTime;
|
|
182
|
+
|
|
183
|
+
// Count strips and points
|
|
184
|
+
let numStrips = 0;
|
|
185
|
+
let totalPoints = 0;
|
|
186
|
+
let minPoints = Infinity;
|
|
187
|
+
let maxPoints = -Infinity;
|
|
188
|
+
let emptyStrips = 0;
|
|
189
|
+
|
|
190
|
+
if (toolpathResult) {
|
|
191
|
+
numStrips = toolpathResult.numStrips || 0;
|
|
192
|
+
totalPoints = toolpathResult.totalPoints || 0;
|
|
193
|
+
|
|
194
|
+
// If we have the raw strips data, analyze it
|
|
195
|
+
if (toolpathResult.strips) {
|
|
196
|
+
for (const strip of toolpathResult.strips) {
|
|
197
|
+
const pointCount = strip.length / 3;
|
|
198
|
+
if (pointCount === 0) {
|
|
199
|
+
emptyStrips++;
|
|
200
|
+
}
|
|
201
|
+
minPoints = Math.min(minPoints, pointCount);
|
|
202
|
+
maxPoints = Math.max(maxPoints, pointCount);
|
|
203
|
+
}
|
|
204
|
+
} else if (toolpathResult.result && toolpathResult.result.strips) {
|
|
205
|
+
// Alternative format
|
|
206
|
+
numStrips = toolpathResult.result.strips.length;
|
|
207
|
+
for (const strip of toolpathResult.result.strips) {
|
|
208
|
+
const pointCount = strip.length / 3;
|
|
209
|
+
totalPoints += pointCount;
|
|
210
|
+
if (pointCount === 0) {
|
|
211
|
+
emptyStrips++;
|
|
212
|
+
}
|
|
213
|
+
minPoints = Math.min(minPoints, pointCount);
|
|
214
|
+
maxPoints = Math.max(maxPoints, pointCount);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
success: true,
|
|
221
|
+
triangleCount: modelTriangles.length / 9,
|
|
222
|
+
timing: {
|
|
223
|
+
terrain: terrainTime,
|
|
224
|
+
tool: toolTime,
|
|
225
|
+
toolpath: toolpathTime,
|
|
226
|
+
total: totalTime
|
|
227
|
+
},
|
|
228
|
+
result: {
|
|
229
|
+
numStrips,
|
|
230
|
+
totalPoints,
|
|
231
|
+
minPoints: minPoints === Infinity ? 0 : minPoints,
|
|
232
|
+
maxPoints: maxPoints === -Infinity ? 0 : maxPoints,
|
|
233
|
+
emptyStrips,
|
|
234
|
+
avgPointsPerStrip: numStrips > 0 ? (totalPoints / numStrips).toFixed(1) : 0
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
})()
|
|
238
|
+
`);
|
|
239
|
+
|
|
240
|
+
console.log(`\nResults (Iteration ${currentIteration}):`);
|
|
241
|
+
console.log(` Triangle count: ${result.triangleCount.toLocaleString()}`);
|
|
242
|
+
console.log(` Terrain time: ${result.timing.terrain.toFixed(1)}ms`);
|
|
243
|
+
console.log(` Tool time: ${result.timing.tool.toFixed(1)}ms`);
|
|
244
|
+
console.log(` Toolpath time: ${result.timing.toolpath.toFixed(1)}ms`);
|
|
245
|
+
console.log(` Total time: ${result.timing.total.toFixed(1)}ms`);
|
|
246
|
+
console.log('');
|
|
247
|
+
console.log('Toolpath Output:');
|
|
248
|
+
console.log(` Strips: ${result.result.numStrips}`);
|
|
249
|
+
console.log(` Total points: ${result.result.totalPoints.toLocaleString()}`);
|
|
250
|
+
console.log(` Empty strips: ${result.result.emptyStrips}`);
|
|
251
|
+
console.log(` Min points/strip: ${result.result.minPoints}`);
|
|
252
|
+
console.log(` Max points/strip: ${result.result.maxPoints}`);
|
|
253
|
+
console.log(` Avg points/strip: ${result.result.avgPointsPerStrip}`);
|
|
254
|
+
console.log('');
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
iteration: currentIteration,
|
|
258
|
+
...result
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function analyzeResults() {
|
|
263
|
+
console.log('');
|
|
264
|
+
console.log('======================================================================');
|
|
265
|
+
console.log('CONSISTENCY ANALYSIS');
|
|
266
|
+
console.log('======================================================================');
|
|
267
|
+
|
|
268
|
+
const successful = testResults.filter(r => r.success);
|
|
269
|
+
|
|
270
|
+
if (successful.length === 0) {
|
|
271
|
+
console.log('✗ All iterations failed!');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (successful.length < TEST_ITERATIONS) {
|
|
276
|
+
console.log(`⚠️ Only ${successful.length} of ${TEST_ITERATIONS} iterations succeeded`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check consistency of results
|
|
280
|
+
const totalPointsCounts = successful.map(r => r.result.totalPoints);
|
|
281
|
+
const uniqueTotalPoints = [...new Set(totalPointsCounts)];
|
|
282
|
+
|
|
283
|
+
const numStripsCounts = successful.map(r => r.result.numStrips);
|
|
284
|
+
const uniqueNumStrips = [...new Set(numStripsCounts)];
|
|
285
|
+
|
|
286
|
+
const emptyStripsCounts = successful.map(r => r.result.emptyStrips);
|
|
287
|
+
const uniqueEmptyStrips = [...new Set(emptyStripsCounts)];
|
|
288
|
+
|
|
289
|
+
console.log('\nTotal Points Consistency:');
|
|
290
|
+
if (uniqueTotalPoints.length === 1) {
|
|
291
|
+
console.log(` ✓ All iterations produced ${totalPointsCounts[0].toLocaleString()} points`);
|
|
292
|
+
} else {
|
|
293
|
+
console.log(` ✗ INCONSISTENT! Got ${uniqueTotalPoints.length} different values:`);
|
|
294
|
+
uniqueTotalPoints.forEach(val => {
|
|
295
|
+
const count = totalPointsCounts.filter(v => v === val).length;
|
|
296
|
+
console.log(` ${val.toLocaleString()} points: ${count} times`);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
console.log('\nEmpty Strips Consistency:');
|
|
301
|
+
if (uniqueEmptyStrips.length === 1) {
|
|
302
|
+
console.log(` ${uniqueEmptyStrips[0] === 0 ? '✓' : '⚠️'} All iterations had ${emptyStripsCounts[0]} empty strips`);
|
|
303
|
+
} else {
|
|
304
|
+
console.log(` ✗ INCONSISTENT! Got ${uniqueEmptyStrips.length} different values:`);
|
|
305
|
+
uniqueEmptyStrips.forEach(val => {
|
|
306
|
+
const count = emptyStripsCounts.filter(v => v === val).length;
|
|
307
|
+
console.log(` ${val} empty strips: ${count} times`);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Timing statistics
|
|
312
|
+
const toolpathTimes = successful.map(r => r.timing.toolpath);
|
|
313
|
+
const avgTime = toolpathTimes.reduce((a, b) => a + b, 0) / toolpathTimes.length;
|
|
314
|
+
const minTime = Math.min(...toolpathTimes);
|
|
315
|
+
const maxTime = Math.max(...toolpathTimes);
|
|
316
|
+
const stdDev = Math.sqrt(
|
|
317
|
+
toolpathTimes.reduce((sum, t) => sum + Math.pow(t - avgTime, 2), 0) / toolpathTimes.length
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
console.log('\nToolpath Timing Statistics:');
|
|
321
|
+
console.log(` Average: ${avgTime.toFixed(1)}ms`);
|
|
322
|
+
console.log(` Min: ${minTime.toFixed(1)}ms`);
|
|
323
|
+
console.log(` Max: ${maxTime.toFixed(1)}ms`);
|
|
324
|
+
console.log(` StdDev: ${stdDev.toFixed(1)}ms (${(stdDev / avgTime * 100).toFixed(1)}%)`);
|
|
325
|
+
|
|
326
|
+
// Overall verdict
|
|
327
|
+
console.log('');
|
|
328
|
+
if (uniqueTotalPoints.length === 1 && uniqueEmptyStrips[0] === 0) {
|
|
329
|
+
console.log('✅ RESULTS ARE CONSISTENT - No non-deterministic behavior detected');
|
|
330
|
+
} else {
|
|
331
|
+
console.log('🛑 RESULTS ARE INCONSISTENT - Non-deterministic behavior detected!');
|
|
332
|
+
console.log(' This suggests workgroups may be getting killed or timing out.');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// lathe-cylinder-2-test.cjs
|
|
2
|
+
// Test for lathe-cylinder-2.stl (201 buckets) with bucket batching
|
|
3
|
+
|
|
4
|
+
const { app, BrowserWindow } = require('electron');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
let mainWindow;
|
|
8
|
+
|
|
9
|
+
function createWindow() {
|
|
10
|
+
mainWindow = new BrowserWindow({
|
|
11
|
+
width: 1200,
|
|
12
|
+
height: 800,
|
|
13
|
+
show: false,
|
|
14
|
+
webPreferences: {
|
|
15
|
+
nodeIntegration: false,
|
|
16
|
+
contextIsolation: true,
|
|
17
|
+
enableBlinkFeatures: 'WebGPU',
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const htmlPath = path.join(__dirname, '../../build/index.html');
|
|
22
|
+
mainWindow.loadFile(htmlPath);
|
|
23
|
+
|
|
24
|
+
mainWindow.webContents.on('did-finish-load', async () => {
|
|
25
|
+
console.log('✓ Page loaded');
|
|
26
|
+
|
|
27
|
+
const testScript = `
|
|
28
|
+
(async function() {
|
|
29
|
+
console.log('=== Lathe Cylinder 2 Test (201 buckets) ===');
|
|
30
|
+
|
|
31
|
+
if (!navigator.gpu) {
|
|
32
|
+
return { error: 'WebGPU not available' };
|
|
33
|
+
}
|
|
34
|
+
console.log('✓ WebGPU available');
|
|
35
|
+
|
|
36
|
+
// Import RasterPath
|
|
37
|
+
const { RasterPath } = await import('./raster-path.js');
|
|
38
|
+
|
|
39
|
+
// Load STL files
|
|
40
|
+
console.log('\\nLoading lathe-cylinder-2.stl...');
|
|
41
|
+
const terrainResponse = await fetch('../benchmark/fixtures/lathe-cylinder-2.stl');
|
|
42
|
+
const terrainBuffer = await terrainResponse.arrayBuffer();
|
|
43
|
+
|
|
44
|
+
const toolResponse = await fetch('../benchmark/fixtures/tool.stl');
|
|
45
|
+
const toolBuffer = await toolResponse.arrayBuffer();
|
|
46
|
+
|
|
47
|
+
console.log('✓ Loaded lathe-cylinder-2.stl:', terrainBuffer.byteLength, 'bytes');
|
|
48
|
+
console.log('✓ Loaded tool.stl:', toolBuffer.byteLength, 'bytes');
|
|
49
|
+
|
|
50
|
+
// Parse STL files
|
|
51
|
+
function parseBinarySTL(buffer) {
|
|
52
|
+
const dataView = new DataView(buffer);
|
|
53
|
+
const numTriangles = dataView.getUint32(80, true);
|
|
54
|
+
const positions = new Float32Array(numTriangles * 9);
|
|
55
|
+
let offset = 84;
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < numTriangles; i++) {
|
|
58
|
+
offset += 12; // Skip normal
|
|
59
|
+
for (let j = 0; j < 9; j++) {
|
|
60
|
+
positions[i * 9 + j] = dataView.getFloat32(offset, true);
|
|
61
|
+
offset += 4;
|
|
62
|
+
}
|
|
63
|
+
offset += 2; // Skip attribute byte count
|
|
64
|
+
}
|
|
65
|
+
return positions;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const terrainTriangles = parseBinarySTL(terrainBuffer);
|
|
69
|
+
const toolTriangles = parseBinarySTL(toolBuffer);
|
|
70
|
+
console.log('✓ Parsed terrain:', terrainTriangles.length / 9, 'triangles');
|
|
71
|
+
console.log('✓ Parsed tool:', toolTriangles.length / 9, 'triangles');
|
|
72
|
+
|
|
73
|
+
// Test parameters
|
|
74
|
+
const resolution = 0.5;
|
|
75
|
+
const rotationStep = 5.0;
|
|
76
|
+
const xStep = 1;
|
|
77
|
+
const yStep = 1;
|
|
78
|
+
const zFloor = -50;
|
|
79
|
+
|
|
80
|
+
console.log('\\nTest parameters:');
|
|
81
|
+
console.log(' Resolution:', resolution, 'mm');
|
|
82
|
+
console.log(' Rotation step:', rotationStep, '°');
|
|
83
|
+
console.log(' XY step:', xStep + 'x' + yStep, 'points');
|
|
84
|
+
console.log(' Z floor:', zFloor, 'mm');
|
|
85
|
+
|
|
86
|
+
// Create RasterPath instance for radial mode
|
|
87
|
+
console.log('\\nInitializing RasterPath (radial mode with diagnostic=true)...');
|
|
88
|
+
const raster = new RasterPath({
|
|
89
|
+
mode: 'radial',
|
|
90
|
+
resolution: resolution,
|
|
91
|
+
rotationStep: rotationStep,
|
|
92
|
+
diagnostic: true
|
|
93
|
+
});
|
|
94
|
+
await raster.init();
|
|
95
|
+
console.log('✓ RasterPath initialized');
|
|
96
|
+
|
|
97
|
+
// Load tool
|
|
98
|
+
console.log('\\nLoading tool...');
|
|
99
|
+
const t0 = performance.now();
|
|
100
|
+
const toolData = await raster.loadTool({ triangles: toolTriangles });
|
|
101
|
+
const toolTime = performance.now() - t0;
|
|
102
|
+
console.log('✓ Tool:', toolData.pointCount, 'points in', toolTime.toFixed(1), 'ms');
|
|
103
|
+
|
|
104
|
+
// Load terrain
|
|
105
|
+
console.log('\\nLoading terrain...');
|
|
106
|
+
const t1 = performance.now();
|
|
107
|
+
await raster.loadTerrain({ triangles: terrainTriangles });
|
|
108
|
+
const terrainTime = performance.now() - t1;
|
|
109
|
+
console.log('✓ Terrain loaded in', terrainTime.toFixed(1), 'ms');
|
|
110
|
+
|
|
111
|
+
// Generate toolpaths (this should trigger bucket batching)
|
|
112
|
+
console.log('\\n*** Generating toolpaths (watch for bucket batching messages) ***');
|
|
113
|
+
const t2 = performance.now();
|
|
114
|
+
const toolpaths = await raster.generateToolpaths({
|
|
115
|
+
xStep,
|
|
116
|
+
yStep,
|
|
117
|
+
zFloor
|
|
118
|
+
});
|
|
119
|
+
const toolpathTime = performance.now() - t2;
|
|
120
|
+
|
|
121
|
+
console.log('\\n✓ Generated', toolpaths.length, 'toolpaths in', (toolpathTime / 1000).toFixed(2), 's');
|
|
122
|
+
if (toolpaths.length > 0) {
|
|
123
|
+
console.log(' First toolpath:', toolpaths[0].numScanlines, 'scanlines ×', toolpaths[0].pointsPerLine, 'points');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log('\\n✅ TEST PASSED - No GPU timeout!');
|
|
127
|
+
|
|
128
|
+
return { success: true };
|
|
129
|
+
})();
|
|
130
|
+
`;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const result = await mainWindow.webContents.executeJavaScript(testScript);
|
|
134
|
+
|
|
135
|
+
if (result && result.error) {
|
|
136
|
+
console.error('❌ Test failed:', result.error);
|
|
137
|
+
app.exit(1);
|
|
138
|
+
} else {
|
|
139
|
+
console.log('\n✅ Test completed successfully');
|
|
140
|
+
app.exit(0);
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error('❌ Test execution failed:', error);
|
|
144
|
+
app.exit(1);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
mainWindow.webContents.on('console-message', (event, level, message) => {
|
|
149
|
+
console.log(message);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
app.whenReady().then(createWindow);
|
|
154
|
+
|
|
155
|
+
app.on('window-all-closed', () => {
|
|
156
|
+
app.quit();
|
|
157
|
+
});
|