@gridspace/raster-path 1.0.4 → 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/build/index.html +1 -1
- package/build/raster-path.js +4 -12
- package/build/raster-worker.js +2450 -0
- package/package.json +8 -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} +4 -12
- 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/workload-calibrate.wgsl +106 -0
- package/src/test/calibrate-test.cjs +136 -0
- package/src/test/extreme-work-test.cjs +167 -0
- package/src/test/radial-thread-limit-test.cjs +152 -0
- package/src/web/index.html +1 -1
- package/build/webgpu-worker.js +0 -2800
- package/src/web/webgpu-worker.js +0 -2303
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Workload Calibration Shader
|
|
2
|
+
// Tests GPU watchdog limits by doing configurable amount of work per thread
|
|
3
|
+
|
|
4
|
+
struct Uniforms {
|
|
5
|
+
workgroup_size_x: u32,
|
|
6
|
+
workgroup_size_y: u32,
|
|
7
|
+
workgroup_size_z: u32,
|
|
8
|
+
triangle_tests: u32, // How many intersection tests to run
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@group(0) @binding(0) var<storage, read_write> completion_flags: array<u32>;
|
|
12
|
+
@group(0) @binding(1) var<uniform> uniforms: Uniforms;
|
|
13
|
+
|
|
14
|
+
// Ray-triangle intersection using Möller-Trumbore algorithm
|
|
15
|
+
// This is the actual production code - same ALU/cache characteristics
|
|
16
|
+
fn ray_triangle_intersect(
|
|
17
|
+
ray_origin: vec3<f32>,
|
|
18
|
+
ray_dir: vec3<f32>,
|
|
19
|
+
v0: vec3<f32>,
|
|
20
|
+
v1: vec3<f32>,
|
|
21
|
+
v2: vec3<f32>
|
|
22
|
+
) -> vec2<f32> { // Returns (hit: 0.0 or 1.0, z: intersection_z)
|
|
23
|
+
let EPSILON = 0.0001;
|
|
24
|
+
|
|
25
|
+
// Calculate edges
|
|
26
|
+
let edge1 = v1 - v0;
|
|
27
|
+
let edge2 = v2 - v0;
|
|
28
|
+
|
|
29
|
+
// Cross product: ray_dir × edge2
|
|
30
|
+
let h = cross(ray_dir, edge2);
|
|
31
|
+
|
|
32
|
+
// Dot product: edge1 · h
|
|
33
|
+
let a = dot(edge1, h);
|
|
34
|
+
|
|
35
|
+
// Check if ray is parallel to triangle
|
|
36
|
+
if (abs(a) < EPSILON) {
|
|
37
|
+
return vec2<f32>(0.0, 0.0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let f = 1.0 / a;
|
|
41
|
+
let s = ray_origin - v0;
|
|
42
|
+
let u = f * dot(s, h);
|
|
43
|
+
|
|
44
|
+
// Check if intersection is outside triangle (u parameter)
|
|
45
|
+
if (u < 0.0 || u > 1.0) {
|
|
46
|
+
return vec2<f32>(0.0, 0.0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let q = cross(s, edge1);
|
|
50
|
+
let v = f * dot(ray_dir, q);
|
|
51
|
+
|
|
52
|
+
// Check if intersection is outside triangle (v parameter)
|
|
53
|
+
if (v < 0.0 || u + v > 1.0) {
|
|
54
|
+
return vec2<f32>(0.0, 0.0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Calculate intersection point along ray
|
|
58
|
+
let t = f * dot(edge2, q);
|
|
59
|
+
|
|
60
|
+
if (t > EPSILON) {
|
|
61
|
+
// Ray hit triangle
|
|
62
|
+
let intersection_z = ray_origin.z + t * ray_dir.z;
|
|
63
|
+
return vec2<f32>(1.0, intersection_z);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return vec2<f32>(0.0, 0.0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@compute @workgroup_size(16, 16, 1)
|
|
70
|
+
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
71
|
+
let thread_index = global_id.z * (uniforms.workgroup_size_x * uniforms.workgroup_size_y) +
|
|
72
|
+
global_id.y * uniforms.workgroup_size_x +
|
|
73
|
+
global_id.x;
|
|
74
|
+
|
|
75
|
+
// Synthetic triangle vertices (deterministic, no memory reads needed)
|
|
76
|
+
let v0 = vec3<f32>(0.0, 0.0, 0.0);
|
|
77
|
+
let v1 = vec3<f32>(1.0, 0.0, 0.0);
|
|
78
|
+
let v2 = vec3<f32>(0.5, 1.0, 0.0);
|
|
79
|
+
|
|
80
|
+
// Ray parameters based on thread ID (deterministic)
|
|
81
|
+
let ray_origin = vec3<f32>(
|
|
82
|
+
f32(global_id.x) * 0.1,
|
|
83
|
+
f32(global_id.y) * 0.1,
|
|
84
|
+
10.0
|
|
85
|
+
);
|
|
86
|
+
let ray_dir = vec3<f32>(0.0, 0.0, -1.0);
|
|
87
|
+
|
|
88
|
+
// Perform N intersection tests (configurable workload)
|
|
89
|
+
var hit_count = 0u;
|
|
90
|
+
for (var i = 0u; i < uniforms.triangle_tests; i++) {
|
|
91
|
+
// Slightly vary triangle vertices to prevent compiler optimization
|
|
92
|
+
let offset = f32(i) * 0.001;
|
|
93
|
+
let v0_offset = v0 + vec3<f32>(offset, 0.0, 0.0);
|
|
94
|
+
let v1_offset = v1 + vec3<f32>(0.0, offset, 0.0);
|
|
95
|
+
let v2_offset = v2 + vec3<f32>(offset, offset, 0.0);
|
|
96
|
+
|
|
97
|
+
let result = ray_triangle_intersect(ray_origin, ray_dir, v0_offset, v1_offset, v2_offset);
|
|
98
|
+
if (result.x > 0.5) {
|
|
99
|
+
hit_count += 1u;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Write completion flag (1 = thread completed all work)
|
|
104
|
+
// If this thread was killed by watchdog, this write never happens (stays 0)
|
|
105
|
+
completion_flags[thread_index] = 1u;
|
|
106
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// calibrate-test.cjs
|
|
2
|
+
// Test GPU workload calibration
|
|
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('=== GPU Workload Calibration Test ===');
|
|
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
|
+
// Create RasterPath instance (initializes worker)
|
|
40
|
+
console.log('\\nInitializing worker...');
|
|
41
|
+
const raster = new RasterPath({ mode: 'planar', resolution: 0.1 });
|
|
42
|
+
await raster.init();
|
|
43
|
+
console.log('✓ Worker initialized');
|
|
44
|
+
|
|
45
|
+
// Send calibration request
|
|
46
|
+
console.log('\\nRunning GPU dispatch count calibration...');
|
|
47
|
+
console.log('This will test how many workgroups can be dispatched simultaneously.');
|
|
48
|
+
|
|
49
|
+
const startTime = performance.now();
|
|
50
|
+
|
|
51
|
+
// Send calibrate message to worker
|
|
52
|
+
const calibrationPromise = new Promise((resolve, reject) => {
|
|
53
|
+
const handler = raster.worker.onmessage;
|
|
54
|
+
raster.worker.onmessage = (e) => {
|
|
55
|
+
if (e.data.type === 'calibrate-complete') {
|
|
56
|
+
resolve(e.data.data);
|
|
57
|
+
} else if (e.data.type === 'error') {
|
|
58
|
+
reject(new Error(e.data.message));
|
|
59
|
+
} else {
|
|
60
|
+
handler(e); // Pass through other messages
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
raster.worker.postMessage({
|
|
66
|
+
type: 'calibrate',
|
|
67
|
+
data: {
|
|
68
|
+
calibrationType: 'dispatch',
|
|
69
|
+
options: {
|
|
70
|
+
workgroupSize: [4, 4, 1], // VERY SMALL workgroup (16 threads)
|
|
71
|
+
triangleTests: 1000,
|
|
72
|
+
minDispatch: 1,
|
|
73
|
+
maxDispatch: 1000,
|
|
74
|
+
verbose: true,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const results = await calibrationPromise;
|
|
80
|
+
const elapsed = performance.now() - startTime;
|
|
81
|
+
|
|
82
|
+
console.log('\\n✓ Calibration complete in', elapsed.toFixed(0) + 'ms');
|
|
83
|
+
console.log('\\n=== Results ===');
|
|
84
|
+
console.log('Max safe dispatch count:', results.maxSafeDispatchCount.toLocaleString());
|
|
85
|
+
console.log('Workgroup size:', results.workgroupSize.join('x'));
|
|
86
|
+
console.log('Triangle tests per thread:', results.triangleTests.toLocaleString());
|
|
87
|
+
|
|
88
|
+
const maxThreads = results.maxSafeDispatchCount * results.workgroupSize[0] * results.workgroupSize[1] * results.workgroupSize[2];
|
|
89
|
+
const maxTests = maxThreads * results.triangleTests;
|
|
90
|
+
console.log('\\nMax concurrent threads:', maxThreads.toLocaleString());
|
|
91
|
+
console.log('Max total ray tests:', maxTests.toLocaleString());
|
|
92
|
+
|
|
93
|
+
console.log('\\n=== Dispatch Test Results ===');
|
|
94
|
+
for (const entry of results.results) {
|
|
95
|
+
const status = entry.success ? '✓' : '❌';
|
|
96
|
+
const threads = entry.totalThreads.toLocaleString();
|
|
97
|
+
const time = entry.elapsed.toFixed(1);
|
|
98
|
+
const failed = entry.failedThreads > 0 ? \` (\${entry.failedThreads} failed)\` : '';
|
|
99
|
+
console.log(\` \${status} \${entry.dispatchCount.toString().padStart(6)} workgroups: \${threads.padStart(10)} threads in \${time.padStart(7)}ms\${failed}\`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { success: true, results };
|
|
103
|
+
})();
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const result = await mainWindow.webContents.executeJavaScript(testScript);
|
|
108
|
+
|
|
109
|
+
if (result.error) {
|
|
110
|
+
console.error('❌ Test failed:', result.error);
|
|
111
|
+
app.exit(1);
|
|
112
|
+
} else if (!result.success) {
|
|
113
|
+
console.error('❌ Test returned unsuccessful result');
|
|
114
|
+
app.exit(1);
|
|
115
|
+
} else {
|
|
116
|
+
console.log('\n✅ Calibration test complete');
|
|
117
|
+
app.exit(0);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error('❌ Test error:', error);
|
|
121
|
+
app.exit(1);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
mainWindow.webContents.on('console-message', (event, level, message) => {
|
|
126
|
+
console.log(message);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
app.whenReady().then(createWindow);
|
|
131
|
+
|
|
132
|
+
app.on('window-all-closed', () => {
|
|
133
|
+
if (process.platform !== 'darwin') {
|
|
134
|
+
app.quit();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
@@ -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,152 @@
|
|
|
1
|
+
// radial-thread-limit-test.cjs
|
|
2
|
+
// Test that radial rasterization respects thread limits and produces correct output
|
|
3
|
+
|
|
4
|
+
const { app, BrowserWindow } = require('electron');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
|
|
8
|
+
let mainWindow;
|
|
9
|
+
|
|
10
|
+
function createWindow() {
|
|
11
|
+
mainWindow = new BrowserWindow({
|
|
12
|
+
width: 1200,
|
|
13
|
+
height: 800,
|
|
14
|
+
show: false,
|
|
15
|
+
webPreferences: {
|
|
16
|
+
nodeIntegration: false,
|
|
17
|
+
contextIsolation: true,
|
|
18
|
+
enableBlinkFeatures: 'WebGPU',
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const htmlPath = path.join(__dirname, '../../build/index.html');
|
|
23
|
+
mainWindow.loadFile(htmlPath);
|
|
24
|
+
|
|
25
|
+
mainWindow.webContents.on('did-finish-load', async () => {
|
|
26
|
+
console.log('✓ Page loaded');
|
|
27
|
+
|
|
28
|
+
const testScript = `
|
|
29
|
+
(async function() {
|
|
30
|
+
console.log('=== Radial Thread Limit Test ===\\n');
|
|
31
|
+
|
|
32
|
+
if (!navigator.gpu) {
|
|
33
|
+
return { error: 'WebGPU not available' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { RasterPath } = await import('./raster-path.js');
|
|
37
|
+
|
|
38
|
+
// Read test STL
|
|
39
|
+
const stlPath = '${path.join(__dirname, '../../benchmark/fixtures/lathe-cylinder.stl')}';
|
|
40
|
+
const response = await fetch('file://' + stlPath);
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
return { error: 'Failed to load test STL: ' + stlPath };
|
|
43
|
+
}
|
|
44
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
45
|
+
|
|
46
|
+
// Test with different thread limits
|
|
47
|
+
const threadLimits = [
|
|
48
|
+
{ limit: 256, desc: 'Default limit (256 threads)' },
|
|
49
|
+
{ limit: 128, desc: 'Reduced limit (128 threads)' },
|
|
50
|
+
{ limit: 64, desc: 'Very low limit (64 threads)' },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const results = [];
|
|
54
|
+
|
|
55
|
+
for (const config of threadLimits) {
|
|
56
|
+
console.log(\`Testing: \${config.desc}\`);
|
|
57
|
+
|
|
58
|
+
const raster = new RasterPath({
|
|
59
|
+
mode: 'radial',
|
|
60
|
+
resolution: 0.5,
|
|
61
|
+
rotationStep: 1.0,
|
|
62
|
+
toolWidth: 5.0,
|
|
63
|
+
maxConcurrentThreads: config.limit
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await raster.init();
|
|
67
|
+
|
|
68
|
+
const startTime = performance.now();
|
|
69
|
+
const result = await raster.processSTL(arrayBuffer);
|
|
70
|
+
const elapsed = performance.now() - startTime;
|
|
71
|
+
|
|
72
|
+
// Calculate checksum
|
|
73
|
+
let checksum = 0;
|
|
74
|
+
for (const strip of result.strips) {
|
|
75
|
+
for (let i = 0; i < strip.pathData.length; i++) {
|
|
76
|
+
checksum = (checksum * 31 + strip.pathData[i]) | 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(\` Time: \${elapsed.toFixed(1)}ms\`);
|
|
81
|
+
console.log(\` Strips: \${result.strips.length}\`);
|
|
82
|
+
console.log(\` Checksum: \${checksum}\`);
|
|
83
|
+
console.log('');
|
|
84
|
+
|
|
85
|
+
results.push({
|
|
86
|
+
limit: config.limit,
|
|
87
|
+
desc: config.desc,
|
|
88
|
+
time: elapsed,
|
|
89
|
+
strips: result.strips.length,
|
|
90
|
+
checksum,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Verify all checksums match
|
|
95
|
+
const referenceChecksum = results[0].checksum;
|
|
96
|
+
const allMatch = results.every(r => r.checksum === referenceChecksum);
|
|
97
|
+
|
|
98
|
+
console.log('=== Results ===');
|
|
99
|
+
console.log(\`Reference checksum: \${referenceChecksum}\`);
|
|
100
|
+
console.log(\`All checksums match: \${allMatch ? '✓ YES' : '❌ NO'}\`);
|
|
101
|
+
|
|
102
|
+
if (!allMatch) {
|
|
103
|
+
console.log('\\nChecksum mismatches:');
|
|
104
|
+
for (const r of results) {
|
|
105
|
+
if (r.checksum !== referenceChecksum) {
|
|
106
|
+
console.log(\` \${r.desc}: \${r.checksum} (expected \${referenceChecksum})\`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log('\\nTiming comparison:');
|
|
112
|
+
const baselineTime = results[0].time;
|
|
113
|
+
for (const r of results) {
|
|
114
|
+
const ratio = (r.time / baselineTime).toFixed(2);
|
|
115
|
+
console.log(\` \${r.desc}: \${r.time.toFixed(1)}ms (\${ratio}x)\`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { success: allMatch, results };
|
|
119
|
+
})();
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const result = await mainWindow.webContents.executeJavaScript(testScript);
|
|
124
|
+
|
|
125
|
+
if (result.error) {
|
|
126
|
+
console.error('❌ Test failed:', result.error);
|
|
127
|
+
app.exit(1);
|
|
128
|
+
} else if (!result.success) {
|
|
129
|
+
console.error('❌ Thread limit test failed - checksums do not match');
|
|
130
|
+
app.exit(1);
|
|
131
|
+
} else {
|
|
132
|
+
console.log('\\n✅ Thread limit test passed - all configurations produce identical output');
|
|
133
|
+
app.exit(0);
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error('❌ Test error:', error);
|
|
137
|
+
app.exit(1);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
mainWindow.webContents.on('console-message', (event, level, message) => {
|
|
142
|
+
console.log(message);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
app.whenReady().then(createWindow);
|
|
147
|
+
|
|
148
|
+
app.on('window-all-closed', () => {
|
|
149
|
+
if (process.platform !== 'darwin') {
|
|
150
|
+
app.quit();
|
|
151
|
+
}
|
|
152
|
+
});
|
package/src/web/index.html
CHANGED