@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.
@@ -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
+ });
@@ -125,6 +125,6 @@
125
125
  }
126
126
  </script>
127
127
  <script type="module" src="app.js"></script>
128
- <!-- <script type="module" src="webgpu-worker.js"></script> -->
128
+ <!-- <script type="module" src="raster-worker.js"></script> -->
129
129
  </body>
130
130
  </html>