@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.
- package/CHANGELOG.md +20 -0
- package/LICENSE +190 -0
- package/README.md +187 -2
- package/index.js +50 -0
- package/node-red-base/NodeRedNodeBase.js +143 -0
- package/node-red-base/README.md +215 -0
- package/node-red-editorcomms/EditorCommsBackend.js +155 -0
- package/node-red-editorcomms/EditorCommsFrontend.js +105 -0
- package/node-red-editorcomms/README.md +164 -0
- package/node-red-gpu/GpuBuffer.js +29 -0
- package/node-red-gpu/Kernel.js +35 -0
- package/node-red-gpu/README.md +220 -0
- package/node-red-gpu/backends/CpuBackend.js +109 -0
- package/node-red-gpu/backends/VulkanBackend.js +232 -0
- package/node-red-gpu/errors.js +48 -0
- package/node-red-gpu/index.js +6 -0
- package/node-red-gpu/log.js +18 -0
- package/node-red-gpu/native/binding.gyp +14 -0
- package/node-red-gpu/native/src/gpu_addon.cpp +1066 -0
- package/node-red-gpu/nodeRedGpuMain.js +39 -0
- package/node-red-gpu/nodeRedGpuWorker.js +242 -0
- package/node-red-gpu/transpiler/emitGlsl.js +497 -0
- package/node-red-gpu/transpiler/glslNames.js +60 -0
- package/node-red-gpu/transpiler/index.js +81 -0
- package/node-red-gpu/transpiler/inferTypes.js +302 -0
- package/node-red-gpu/transpiler/validate.js +461 -0
- package/node-red-popup/NodeRedPopupBackend.js +69 -0
- package/node-red-popup/NodeRedPopupFrontend.js +508 -0
- package/node-red-popup/README.md +113 -0
- package/node-red-pubsub-bus/NodeRedPubsubBus.js +617 -0
- package/node-red-pubsub-bus/NodeRedPubsubBusWorkerProxy.js +239 -0
- package/node-red-pubsub-bus/README.md +201 -0
- package/node-red-resourceendpoint/NodeRedResourceEndpoint.js +205 -0
- package/node-red-resourceendpoint/README.md +119 -0
- package/node-red-singleton/NodeRedSingletonBackend.js +30 -0
- package/node-red-singleton/NodeRedSingletonFrontend.js +54 -0
- package/node-red-singleton/README.md +72 -0
- package/node-red-worker/NodeRedWorker.js +443 -0
- package/node-red-worker/NodeRedWorkerRuntime.js +659 -0
- package/node-red-worker/README.md +273 -0
- package/node-red-worker/workerRuntimeLoader.js +55 -0
- 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 };
|