@nrutils/node-red-utils 0.0.0-stage → 0.2.0

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +190 -0
  3. package/README.md +187 -2
  4. package/index.js +50 -0
  5. package/node-red-base/NodeRedNodeBase.js +143 -0
  6. package/node-red-base/README.md +215 -0
  7. package/node-red-editorcomms/EditorCommsBackend.js +155 -0
  8. package/node-red-editorcomms/EditorCommsFrontend.js +105 -0
  9. package/node-red-editorcomms/README.md +164 -0
  10. package/node-red-gpu/GpuBuffer.js +29 -0
  11. package/node-red-gpu/Kernel.js +35 -0
  12. package/node-red-gpu/README.md +220 -0
  13. package/node-red-gpu/backends/CpuBackend.js +109 -0
  14. package/node-red-gpu/backends/VulkanBackend.js +232 -0
  15. package/node-red-gpu/errors.js +48 -0
  16. package/node-red-gpu/index.js +6 -0
  17. package/node-red-gpu/log.js +18 -0
  18. package/node-red-gpu/native/binding.gyp +14 -0
  19. package/node-red-gpu/native/src/gpu_addon.cpp +1066 -0
  20. package/node-red-gpu/nodeRedGpuMain.js +39 -0
  21. package/node-red-gpu/nodeRedGpuWorker.js +242 -0
  22. package/node-red-gpu/transpiler/emitGlsl.js +497 -0
  23. package/node-red-gpu/transpiler/glslNames.js +60 -0
  24. package/node-red-gpu/transpiler/index.js +81 -0
  25. package/node-red-gpu/transpiler/inferTypes.js +302 -0
  26. package/node-red-gpu/transpiler/validate.js +461 -0
  27. package/node-red-popup/NodeRedPopupBackend.js +69 -0
  28. package/node-red-popup/NodeRedPopupFrontend.js +508 -0
  29. package/node-red-popup/README.md +113 -0
  30. package/node-red-pubsub-bus/NodeRedPubsubBus.js +617 -0
  31. package/node-red-pubsub-bus/NodeRedPubsubBusWorkerProxy.js +239 -0
  32. package/node-red-pubsub-bus/README.md +201 -0
  33. package/node-red-resourceendpoint/NodeRedResourceEndpoint.js +205 -0
  34. package/node-red-resourceendpoint/README.md +119 -0
  35. package/node-red-singleton/NodeRedSingletonBackend.js +30 -0
  36. package/node-red-singleton/NodeRedSingletonFrontend.js +54 -0
  37. package/node-red-singleton/README.md +72 -0
  38. package/node-red-worker/NodeRedWorker.js +443 -0
  39. package/node-red-worker/NodeRedWorkerRuntime.js +659 -0
  40. package/node-red-worker/README.md +273 -0
  41. package/node-red-worker/workerRuntimeLoader.js +55 -0
  42. package/package.json +70 -5
@@ -0,0 +1,497 @@
1
+ 'use strict';
2
+
3
+ // dtype → GLSL type string
4
+ function dtypeToGlsl(dtype) {
5
+ switch (dtype) {
6
+ case 'u8': case 'u16': case 'u32': return 'uint';
7
+ case 'i8': case 'i16': case 'i32': return 'int';
8
+ case 'f32': return 'float';
9
+ case 'f64': return 'double';
10
+ default: return dtype; // pass through if already a glsl type
11
+ }
12
+ }
13
+
14
+ // Capitalize first letter
15
+ function capitalize(s) {
16
+ return s.charAt(0).toUpperCase() + s.slice(1);
17
+ }
18
+
19
+ // Determine access qualifier string
20
+ function accessQualifier(access) {
21
+ switch (access) {
22
+ case 'readonly': return 'readonly ';
23
+ case 'writeonly': return 'writeonly ';
24
+ case 'readwrite': return '';
25
+ default: return '';
26
+ }
27
+ }
28
+
29
+ // Emit a GLSL type string from a _glslType annotation
30
+ function glslTypeStr(type) {
31
+ if (!type) return 'float';
32
+ switch (type) {
33
+ case 'uint': return 'uint';
34
+ case 'int': return 'int';
35
+ case 'float': return 'float';
36
+ case 'double': return 'double';
37
+ case 'bool': return 'bool';
38
+ default: return type;
39
+ }
40
+ }
41
+
42
+ // Format a numeric literal value for a given GLSL type
43
+ function formatLiteral(value, type) {
44
+ if (type === 'uint') {
45
+ return String(Math.trunc(value)) + 'u';
46
+ }
47
+ if (type === 'float' || type === 'double') {
48
+ if (Number.isInteger(value)) {
49
+ return String(value) + '.0';
50
+ }
51
+ return String(value);
52
+ }
53
+ return String(value);
54
+ }
55
+
56
+ // Math.* → GLSL builtin name
57
+ function mathFuncToGlsl(name) {
58
+ const map = {
59
+ abs: 'abs', ceil: 'ceil', floor: 'floor', round: 'round',
60
+ sqrt: 'sqrt', log: 'log', log2: 'log2', exp: 'exp', exp2: 'exp2',
61
+ pow: 'pow', min: 'min', max: 'max', clamp: 'clamp',
62
+ sin: 'sin', cos: 'cos', tan: 'tan',
63
+ asin: 'asin', acos: 'acos', atan: 'atan', atan2: 'atan',
64
+ sign: 'sign', fract: 'fract', mix: 'mix', mod: 'mod',
65
+ trunc: 'trunc',
66
+ };
67
+ return map[name] || name;
68
+ }
69
+
70
+ class GlslEmitter {
71
+ constructor(paramNames, options) {
72
+ this.paramNames = new Set(paramNames);
73
+ this.options = options;
74
+ this.indent = 0;
75
+
76
+ // Find the first output buffer (readwrite or writeonly) for return-value writes
77
+ const buffers = options.buffers || [];
78
+ const outputBuf = buffers.find(b => b.access === 'readwrite' || b.access === 'writeonly');
79
+ this.outputBufName = outputBuf ? outputBuf.name : (buffers[0] ? buffers[0].name : null);
80
+ }
81
+
82
+ pad() {
83
+ return ' '.repeat(this.indent);
84
+ }
85
+
86
+ // Returns true if stmt is a direct assignment to the output buffer: outputBufName[expr] = expr
87
+ _stmtWritesOutput(stmt) {
88
+ if (stmt.type !== 'ExpressionStatement') return false;
89
+ const expr = stmt.expression;
90
+ if (expr.type !== 'AssignmentExpression') return false;
91
+ const left = expr.left;
92
+ if (left.type !== 'MemberExpression' || !left.computed) return false;
93
+ if (left.object.type !== 'Identifier') return false;
94
+ return left.object.name === this.outputBufName;
95
+ }
96
+
97
+ // Recursively scan a statement array for any direct output buffer write
98
+ _scanForOutputWrite(stmts) {
99
+ for (const stmt of stmts) {
100
+ if (this._stmtWritesOutput(stmt)) return true;
101
+ if (stmt.type === 'IfStatement') {
102
+ const cons = stmt.consequent;
103
+ if (cons && this._scanForOutputWrite(
104
+ cons.type === 'BlockStatement' ? cons.body : [cons]
105
+ )) return true;
106
+ const alt = stmt.alternate;
107
+ if (alt && this._scanForOutputWrite(
108
+ alt.type === 'BlockStatement' ? alt.body : [alt]
109
+ )) return true;
110
+ }
111
+ if (stmt.type === 'ForStatement' && stmt.body) {
112
+ if (this._scanForOutputWrite(
113
+ stmt.body.type === 'BlockStatement' ? stmt.body.body : [stmt.body]
114
+ )) return true;
115
+ }
116
+ if (stmt.type === 'BlockStatement') {
117
+ if (this._scanForOutputWrite(stmt.body)) return true;
118
+ }
119
+ }
120
+ return false;
121
+ }
122
+
123
+ emitProgram(ast) {
124
+ // Find the kernel function body (first FunctionDeclaration or arrow in program)
125
+ let body = null;
126
+ for (const node of ast.body) {
127
+ if (node.type === 'FunctionDeclaration') {
128
+ body = node.body.body;
129
+ break;
130
+ }
131
+ if (node.type === 'ExpressionStatement' &&
132
+ node.expression.type === 'AssignmentExpression' &&
133
+ node.expression.right.type === 'ArrowFunctionExpression') {
134
+ body = node.expression.right.body.body;
135
+ break;
136
+ }
137
+ if (node.type === 'VariableDeclaration') {
138
+ const decl = node.declarations[0];
139
+ if (decl && decl.init &&
140
+ (decl.init.type === 'ArrowFunctionExpression' ||
141
+ decl.init.type === 'FunctionExpression')) {
142
+ body = decl.init.body.body;
143
+ break;
144
+ }
145
+ }
146
+ }
147
+ if (!body) {
148
+ throw new Error('emitGlsl: could not find kernel function body in AST');
149
+ }
150
+
151
+ // Detect if the kernel uses direct output buffer writes anywhere in the body.
152
+ // If so, kernels are in "explicit output" mode: return expr just emits return;
153
+ // (the return value is ignored; the kernel is responsible for its own output writes).
154
+ this._hasDirectOutputWrite = this.outputBufName
155
+ ? this._scanForOutputWrite(body)
156
+ : false;
157
+
158
+ const lines = [];
159
+ const opts = this.options;
160
+ const buffers = opts.buffers || [];
161
+ const constants = opts.constants || {};
162
+ const workgroupSize = opts.workgroupSize || 64;
163
+
164
+ // Header
165
+ lines.push('#version 450');
166
+ // workgroupSize can be a number (1D) or [x, y] or [x, y, z]
167
+ const wgs = Array.isArray(opts.workgroupSize) ? opts.workgroupSize : [opts.workgroupSize || 64];
168
+ const wgsX = wgs[0] || 64;
169
+ const wgsY = wgs[1] || 1;
170
+ const wgsZ = wgs[2] || 1;
171
+ let layoutStr = `layout(local_size_x = ${wgsX}`;
172
+ if (wgsY > 1) layoutStr += `, local_size_y = ${wgsY}`;
173
+ if (wgsZ > 1) layoutStr += `, local_size_z = ${wgsZ}`;
174
+ layoutStr += ') in;';
175
+ lines.push(layoutStr);
176
+ lines.push('');
177
+
178
+ // SSBO bindings and shared declarations
179
+ const sharedDecls = [];
180
+ let binding = 0;
181
+ for (let i = 0; i < buffers.length; i++) {
182
+ const buf = buffers[i];
183
+ if (buf.shared) {
184
+ // Emit as GLSL shared variable, not an SSBO
185
+ const glslType = dtypeToGlsl(buf.dtype);
186
+ sharedDecls.push(`shared ${glslType} ${buf.name}[${buf.count}];`);
187
+ continue;
188
+ }
189
+ const glslType = dtypeToGlsl(buf.dtype);
190
+ const qual = accessQualifier(buf.access);
191
+ const blockName = capitalize(buf.name) + binding;
192
+ lines.push(`layout(std430, binding = ${binding}) ${qual}buffer ${blockName} { ${glslType} ${buf.name}[]; };`);
193
+ binding++;
194
+ }
195
+ if (binding > 0) lines.push('');
196
+ for (const decl of sharedDecls) lines.push(decl);
197
+ if (sharedDecls.length > 0) lines.push('');
198
+
199
+ // Specialization constants — emitted before the push_constant block with layout(constant_id = N)
200
+ const specConstantSet = new Set(
201
+ Array.isArray(opts.specConstants) ? opts.specConstants : Object.keys(opts.specConstants || {})
202
+ );
203
+ let specId = 0;
204
+ const constEntries = Object.entries(constants);
205
+ for (const [name, dtype] of constEntries) {
206
+ if (specConstantSet.has(name)) {
207
+ lines.push(`layout(constant_id = ${specId++}) const ${dtypeToGlsl(dtype)} ${name} = ${dtypeToGlsl(dtype) === 'uint' ? '0u' : '0'};`);
208
+ }
209
+ }
210
+ if (specId > 0) lines.push('');
211
+
212
+ // Push constants — always emit count, countY, countZ for dispatch bounds
213
+ // Spec constants are excluded from this block
214
+ const pcConstEntries = constEntries.filter(([name]) => !specConstantSet.has(name));
215
+ lines.push('layout(push_constant) uniform PC {');
216
+ for (const [name, dtype] of pcConstEntries) {
217
+ lines.push(` ${dtypeToGlsl(dtype)} ${name};`);
218
+ }
219
+ lines.push(' uint count;');
220
+ lines.push(' uint countY;');
221
+ lines.push(' uint countZ;');
222
+ lines.push('} pc;');
223
+ lines.push('');
224
+
225
+ // main()
226
+ lines.push('void main() {');
227
+ lines.push(' uint i_thread_x = gl_GlobalInvocationID.x;');
228
+ lines.push(' uint i_thread_y = gl_GlobalInvocationID.y;');
229
+ lines.push(' uint i_thread_z = gl_GlobalInvocationID.z;');
230
+ // Same bounds and output index as the CPU backend: countY and countZ of 0 mean 1
231
+ lines.push(' uint i_count_y = max(pc.countY, 1u);');
232
+ lines.push(' if (i_thread_x >= pc.count || i_thread_y >= i_count_y || i_thread_z >= max(pc.countZ, 1u)) return;');
233
+ lines.push(' uint i_out = i_thread_x + i_thread_y * pc.count + i_thread_z * pc.count * i_count_y;');
234
+ lines.push('');
235
+
236
+ this.indent = 1;
237
+ for (const stmt of body) {
238
+ const emitted = this.emitStatement(stmt);
239
+ if (emitted) lines.push(emitted);
240
+ }
241
+
242
+ lines.push('}');
243
+ return lines.join('\n') + '\n';
244
+ }
245
+
246
+ emitStatement(node) {
247
+ const p = this.pad();
248
+ switch (node.type) {
249
+ case 'VariableDeclaration': {
250
+ const results = [];
251
+ for (const decl of node.declarations) {
252
+ const type = glslTypeStr(decl.init && decl.init._glslType);
253
+ const qualifier = node.kind === 'const' ? 'const ' : '';
254
+ const init = decl.init ? ` = ${this.emitExpr(decl.init)}` : '';
255
+ results.push(`${p}${qualifier}${type} ${decl.id.name}${init};`);
256
+ }
257
+ return results.join('\n');
258
+ }
259
+ case 'ExpressionStatement':
260
+ return `${p}${this.emitExpr(node.expression)};`;
261
+ case 'IfStatement': {
262
+ const test = this.emitExpr(node.test);
263
+ let s = `${p}if (${test}) {\n`;
264
+ this.indent++;
265
+ if (node.consequent.type === 'BlockStatement') {
266
+ for (const stmt of node.consequent.body) s += this.emitStatement(stmt) + '\n';
267
+ } else {
268
+ s += this.emitStatement(node.consequent) + '\n';
269
+ }
270
+ this.indent--;
271
+ s += `${p}}`;
272
+ if (node.alternate) {
273
+ s += ` else {\n`;
274
+ this.indent++;
275
+ if (node.alternate.type === 'BlockStatement') {
276
+ for (const stmt of node.alternate.body) s += this.emitStatement(stmt) + '\n';
277
+ } else {
278
+ s += this.emitStatement(node.alternate) + '\n';
279
+ }
280
+ this.indent--;
281
+ s += `${p}}`;
282
+ }
283
+ return s;
284
+ }
285
+ case 'ForStatement': {
286
+ const init = node.init ? this.emitForInit(node.init) : '';
287
+ const test = node.test ? this.emitExpr(node.test) : '';
288
+ const update = node.update ? this.emitExpr(node.update) : '';
289
+ let s = `${p}for (${init}; ${test}; ${update}) {\n`;
290
+ this.indent++;
291
+ const bodyStmts = node.body.type === 'BlockStatement'
292
+ ? node.body.body : [node.body];
293
+ for (const stmt of bodyStmts) s += this.emitStatement(stmt) + '\n';
294
+ this.indent--;
295
+ s += `${p}}`;
296
+ return s;
297
+ }
298
+ case 'ReturnStatement': {
299
+ if (node.argument && this.outputBufName) {
300
+ if (this._hasDirectOutputWrite) {
301
+ // Explicit-output kernel: kernel manages its own writes; ignore return value
302
+ return `${p}return;`;
303
+ }
304
+ const val = this.emitExpr(node.argument);
305
+ // Row-major index, as on the CPU backend
306
+ return `${p}${this.outputBufName}[i_out] = ${val};\n${p}return;`;
307
+ }
308
+ return `${p}return;`;
309
+ }
310
+ case 'BreakStatement':
311
+ return `${p}break;`;
312
+ case 'ContinueStatement':
313
+ return `${p}continue;`;
314
+ case 'BlockStatement': {
315
+ let s = `${p}{\n`;
316
+ this.indent++;
317
+ for (const stmt of node.body) s += this.emitStatement(stmt) + '\n';
318
+ this.indent--;
319
+ s += `${p}}`;
320
+ return s;
321
+ }
322
+ default:
323
+ return `${p}/* unsupported: ${node.type} */`;
324
+ }
325
+ }
326
+
327
+ // For-loop init can be a VariableDeclaration (without trailing semicolon) or expression
328
+ emitForInit(node) {
329
+ if (node.type === 'VariableDeclaration') {
330
+ const decl = node.declarations[0];
331
+ const type = glslTypeStr(decl.init && decl.init._glslType);
332
+ const qualifier = node.kind === 'const' ? 'const ' : '';
333
+ const init = decl.init ? ` = ${this.emitExpr(decl.init)}` : '';
334
+ return `${qualifier}${type} ${decl.id.name}${init}`;
335
+ }
336
+ return this.emitExpr(node);
337
+ }
338
+
339
+ emitExpr(node) {
340
+ if (!node) return '';
341
+ switch (node.type) {
342
+ case 'Literal': {
343
+ if (typeof node.value === 'boolean') return node.value ? 'true' : 'false';
344
+ if (typeof node.value === 'number') {
345
+ return formatLiteral(node.value, glslTypeStr(node._glslType));
346
+ }
347
+ return String(node.value);
348
+ }
349
+ case 'Identifier':
350
+ return node.name;
351
+
352
+ case 'BinaryExpression': {
353
+ let op = node.operator;
354
+ if (op === '===') op = '==';
355
+ else if (op === '!==') op = '!=';
356
+
357
+ const leftExpr = this.emitExpr(node.left);
358
+ const rightExpr = this.emitExpr(node.right);
359
+
360
+ // float mod → mod(left, right)
361
+ if (op === '%') {
362
+ const ltype = node.left._glslType || '';
363
+ if (ltype === 'float' || ltype === 'double') {
364
+ return `mod(${leftExpr}, ${rightExpr})`;
365
+ }
366
+ }
367
+ return `(${leftExpr} ${op} ${rightExpr})`;
368
+ }
369
+
370
+ case 'UnaryExpression':
371
+ return `${node.operator}(${this.emitExpr(node.argument)})`;
372
+
373
+ case 'UpdateExpression': {
374
+ const arg = this.emitExpr(node.argument);
375
+ return node.prefix ? `${node.operator}${arg}` : `${arg}${node.operator}`;
376
+ }
377
+
378
+ case 'AssignmentExpression': {
379
+ const left = this.emitExpr(node.left);
380
+ const right = this.emitExpr(node.right);
381
+ return `${left} ${node.operator} ${right}`;
382
+ }
383
+
384
+ case 'ConditionalExpression': {
385
+ const test = this.emitExpr(node.test);
386
+ const cons = this.emitExpr(node.consequent);
387
+ const alt = this.emitExpr(node.alternate);
388
+ return `(${test} ? ${cons} : ${alt})`;
389
+ }
390
+
391
+ case 'MemberExpression': {
392
+ // this.thread.x / this.thread.y / this.thread.z / localX / localY / localZ
393
+ if (node.object.type === 'MemberExpression' &&
394
+ node.object.object.type === 'ThisExpression' &&
395
+ node.object.property.name === 'thread') {
396
+ const prop = node.property.name;
397
+ if (prop === 'localX') return 'gl_LocalInvocationID.x';
398
+ if (prop === 'localY') return 'gl_LocalInvocationID.y';
399
+ if (prop === 'localZ') return 'gl_LocalInvocationID.z';
400
+ // x/y/z → gl_GlobalInvocationID
401
+ return `gl_GlobalInvocationID.${prop}`;
402
+ }
403
+ // this.constants.NAME
404
+ if (node.object.type === 'MemberExpression' &&
405
+ node.object.object.type === 'ThisExpression' &&
406
+ node.object.property.name === 'constants') {
407
+ const cname = node.property.name;
408
+ // Spec constants are standalone GLSL constants, not in the pc block
409
+ const specSet = new Set(
410
+ Array.isArray(this.options.specConstants)
411
+ ? this.options.specConstants
412
+ : Object.keys(this.options.specConstants || {})
413
+ );
414
+ if (specSet.has(cname)) return cname;
415
+ return `pc.${cname}`;
416
+ }
417
+ // paramName[expr] — computed member access on a buffer
418
+ if (node.computed) {
419
+ const obj = this.emitExpr(node.object);
420
+ const prop = this.emitExpr(node.property);
421
+ return `${obj}[${prop}]`;
422
+ }
423
+ // generic dot access
424
+ return `${this.emitExpr(node.object)}.${node.property.name}`;
425
+ }
426
+
427
+ case 'CallExpression': {
428
+ // barrier() synchronization primitive
429
+ if (node.callee.type === 'Identifier' && node.callee.name === 'barrier') {
430
+ return 'barrier()';
431
+ }
432
+ // Atomic operations: atomicAdd(buf, idx, val) → atomicAdd(buf[idx], val)
433
+ if (node.callee.type === 'Identifier') {
434
+ const atomicFuncs = new Set([
435
+ 'atomicAdd', 'atomicMin', 'atomicMax',
436
+ 'atomicOr', 'atomicAnd', 'atomicXor',
437
+ 'atomicExchange', 'atomicCompSwap',
438
+ ]);
439
+ if (atomicFuncs.has(node.callee.name)) {
440
+ const bufName = this.emitExpr(node.arguments[0]);
441
+ const index = this.emitExpr(node.arguments[1]);
442
+ if (node.callee.name === 'atomicCompSwap') {
443
+ const compare = this.emitExpr(node.arguments[2]);
444
+ const value = this.emitExpr(node.arguments[3]);
445
+ return `atomicCompSwap(${bufName}[${index}], ${compare}, ${value})`;
446
+ }
447
+ const value = this.emitExpr(node.arguments[2]);
448
+ return `${node.callee.name}(${bufName}[${index}], ${value})`;
449
+ }
450
+ }
451
+ // Type cast calls: float(x), int(x), uint(x)
452
+ if (node.callee.type === 'Identifier') {
453
+ const castFuncs = new Set(['float', 'int', 'uint']);
454
+ if (castFuncs.has(node.callee.name)) {
455
+ return `${node.callee.name}(${this.emitExpr(node.arguments[0])})`;
456
+ }
457
+ }
458
+ // Math.func(args)
459
+ if (node.callee.type === 'MemberExpression' &&
460
+ node.callee.object.type === 'Identifier' &&
461
+ node.callee.object.name === 'Math') {
462
+ const fn = node.callee.property.name;
463
+ const glslFn = mathFuncToGlsl(fn);
464
+ const args = node.arguments.map(a => this.emitExpr(a)).join(', ');
465
+ return `${glslFn}(${args})`;
466
+ }
467
+ // generic call
468
+ const callee = this.emitExpr(node.callee);
469
+ const args = node.arguments.map(a => this.emitExpr(a)).join(', ');
470
+ return `${callee}(${args})`;
471
+ }
472
+
473
+ case 'LogicalExpression': {
474
+ const lop = node.operator; // && / ||
475
+ return `(${this.emitExpr(node.left)} ${lop} ${this.emitExpr(node.right)})`;
476
+ }
477
+
478
+ default:
479
+ return `/* unsupported expr: ${node.type} */`;
480
+ }
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Emit a complete GLSL 450 compute shader string from a type-annotated ESTree AST.
486
+ *
487
+ * @param {object} ast - ESTree program AST (from acorn) with _glslType annotations
488
+ * @param {string[]} paramNames - ordered list of buffer parameter names
489
+ * @param {object} options - { buffers, constants, workgroupSize }
490
+ * @returns {string} GLSL 450 shader source
491
+ */
492
+ function emitGlsl(ast, paramNames, options) {
493
+ const emitter = new GlslEmitter(paramNames, options);
494
+ return emitter.emitProgram(ast);
495
+ }
496
+
497
+ module.exports = { emitGlsl };
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ // Names that a JS kernel must not use for a buffer, a constant or a local variable.
4
+ // The CPU backend accepts them, but glslangValidator rejects them (or the generated
5
+ // GLSL breaks), so the check runs at registration on every backend.
6
+
7
+ // GLSL 4.50 keywords and words reserved for future use (spec section 3.6)
8
+ const KEYWORDS = `
9
+ const uniform buffer shared attribute varying coherent volatile restrict readonly writeonly
10
+ atomic_uint layout centroid flat smooth noperspective patch sample invariant precise
11
+ break continue do for while switch case default if else subroutine in out inout
12
+ int void bool true false float double discard return uint struct lowp mediump highp precision
13
+ common partition active asm class union enum typedef template this resource goto inline noinline
14
+ public static extern external interface long short half fixed unsigned superp input output
15
+ filter sizeof cast namespace using sampler sampler3DRect`;
16
+
17
+ // Built-in functions. A local with one of these names hides the function, and the
18
+ // transpiler emits calls such as min(), max() and mix() for Math.* calls.
19
+ const BUILTIN_FUNCTIONS = `
20
+ radians degrees sin cos tan asin acos atan sinh cosh tanh asinh acosh atanh
21
+ pow exp log exp2 log2 sqrt inversesqrt
22
+ abs sign floor trunc round roundEven ceil fract mod modf min max clamp mix step smoothstep
23
+ isnan isinf floatBitsToInt floatBitsToUint intBitsToFloat uintBitsToFloat fma frexp ldexp
24
+ packUnorm2x16 packSnorm2x16 packUnorm4x8 packSnorm4x8 unpackUnorm2x16 unpackSnorm2x16
25
+ unpackUnorm4x8 unpackSnorm4x8 packHalf2x16 unpackHalf2x16 packDouble2x32 unpackDouble2x32
26
+ length distance dot cross normalize faceforward reflect refract
27
+ matrixCompMult outerProduct transpose determinant inverse
28
+ lessThan lessThanEqual greaterThan greaterThanEqual equal notEqual any all not
29
+ uaddCarry usubBorrow umulExtended imulExtended bitfieldExtract bitfieldInsert bitfieldReverse
30
+ bitCount findLSB findMSB
31
+ textureSize textureQueryLod textureQueryLevels textureSamples texture textureProj textureLod
32
+ textureOffset texelFetch texelFetchOffset textureProjOffset textureLodOffset textureProjLod
33
+ textureProjLodOffset textureGrad textureGradOffset textureProjGrad textureProjGradOffset
34
+ textureGather textureGatherOffset textureGatherOffsets
35
+ atomicCounterIncrement atomicCounterDecrement atomicCounter atomicCounterAdd atomicCounterSubtract
36
+ atomicCounterMin atomicCounterMax atomicCounterAnd atomicCounterOr atomicCounterXor
37
+ atomicCounterExchange atomicCounterCompSwap
38
+ atomicAdd atomicMin atomicMax atomicAnd atomicOr atomicXor atomicExchange atomicCompSwap
39
+ imageSize imageSamples imageLoad imageStore imageAtomicAdd imageAtomicMin imageAtomicMax
40
+ imageAtomicAnd imageAtomicOr imageAtomicXor imageAtomicExchange imageAtomicCompSwap
41
+ dFdx dFdy dFdxFine dFdyFine dFdxCoarse dFdyCoarse fwidth fwidthFine fwidthCoarse
42
+ interpolateAtCentroid interpolateAtSample interpolateAtOffset noise1 noise2 noise3 noise4
43
+ EmitStreamVertex EndStreamPrimitive EmitVertex EndPrimitive
44
+ barrier memoryBarrier memoryBarrierAtomicCounter memoryBarrierBuffer memoryBarrierShared
45
+ memoryBarrierImage groupMemoryBarrier`;
46
+
47
+ // Names in the GLSL that the transpiler generates
48
+ const GENERATED = 'main pc i_thread_x i_thread_y i_thread_z i_count_y i_out';
49
+
50
+ const RESERVED = new Set(`${KEYWORDS} ${BUILTIN_FUNCTIONS} ${GENERATED}`.split(/\s+/).filter(Boolean));
51
+
52
+ // Vector, matrix, sampler and image type names
53
+ const RESERVED_PATTERN = /^(gl_|[ibudhf]?vec[234]$|d?mat[234](x[234])?$|[iu]?(sampler|image|texture)[0-9A-Z]|[iu]?subpassInput)|__/;
54
+
55
+ /** Return true if `name` cannot be used as a GLSL identifier in a JS kernel. */
56
+ function isReservedGlslName(name) {
57
+ return RESERVED.has(name) || RESERVED_PATTERN.test(name);
58
+ }
59
+
60
+ module.exports = { isReservedGlslName };
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const acorn = require('acorn');
4
+ const { validate, ATOMIC_FUNCS } = require('./validate.js');
5
+ const { inferTypes } = require('./inferTypes.js');
6
+ const { emitGlsl } = require('./emitGlsl.js');
7
+ const { GpuCompileError } = require('../errors.js');
8
+
9
+ /**
10
+ * Find calls that only the GPU can honor: barrier() and the atomics.
11
+ * @returns {Set<string>} the names of the calls found
12
+ */
13
+ function findGpuOnlyCalls(node, found = new Set()) {
14
+ if (!node || typeof node !== 'object') return found;
15
+ if (node.type === 'CallExpression' && node.callee.type === 'Identifier' &&
16
+ (node.callee.name === 'barrier' || ATOMIC_FUNCS.has(node.callee.name))) {
17
+ found.add(node.callee.name + '()');
18
+ }
19
+ for (const key of Object.keys(node)) {
20
+ if (key === 'loc') continue;
21
+ const child = node[key];
22
+ if (Array.isArray(child)) child.forEach(c => findGpuOnlyCalls(c, found));
23
+ else if (child && typeof child === 'object') findGpuOnlyCalls(child, found);
24
+ }
25
+ return found;
26
+ }
27
+
28
+ /**
29
+ * Transpile a JS kernel function to a GLSL 450 compute shader string.
30
+ *
31
+ * @param {Function|string} fn - kernel function or its .toString() source
32
+ * @param {object} options - { buffers, constants, output, workgroupSize }
33
+ * @returns {{ glsl: string, gpuOnlyCalls: Set<string> }} GLSL 450 compute shader source,
34
+ * and the calls that the CPU backend cannot run
35
+ * @throws {GpuCompileError} on unsupported constructs or transpilation errors
36
+ */
37
+ function transpile(fn, options) {
38
+ const fnString = typeof fn === 'function' ? fn.toString() : fn;
39
+ const opts = options || {};
40
+
41
+ // Extract parameter names from the function signature
42
+ const paramNames = (opts.buffers || []).map(b => b.name);
43
+
44
+ // Parse the function as an expression. This accepts named and anonymous
45
+ // functions, also `function (a, b)` with a space.
46
+ const source = `const __kernel = (${fnString.trim()});`;
47
+
48
+ let ast;
49
+ try {
50
+ ast = acorn.parse(source, {
51
+ ecmaVersion: 2020,
52
+ sourceType: 'script',
53
+ locations: true,
54
+ });
55
+ } catch (e) {
56
+ throw new GpuCompileError(`Failed to parse kernel function: ${e.message}`, e.message);
57
+ }
58
+
59
+ // Extract the function body from the AST
60
+ const init = ast.body[0].declarations[0].init;
61
+ if (init.type === 'ArrowFunctionExpression') {
62
+ // An arrow function has no own `this`, so this.thread and this.constants fail on the CPU backend
63
+ throw new GpuCompileError('The kernel is an arrow function. Use a `function` expression: arrow functions have no `this`.');
64
+ }
65
+ const funcBody = init.type === 'FunctionExpression' ? init.body : null;
66
+
67
+ if (!funcBody || !funcBody.body) {
68
+ throw new GpuCompileError('Could not extract function body from kernel source');
69
+ }
70
+
71
+ // Validate the AST (throws on unsupported constructs)
72
+ validate(funcBody, paramNames, opts);
73
+
74
+ // Infer GLSL types on AST nodes
75
+ inferTypes(funcBody, paramNames, opts);
76
+
77
+ // Emit the complete GLSL 450 compute shader
78
+ return { glsl: emitGlsl(ast, paramNames, opts), gpuOnlyCalls: findGpuOnlyCalls(funcBody) };
79
+ }
80
+
81
+ module.exports = { transpile };