@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,35 @@
1
+ 'use strict';
2
+
3
+ class Kernel {
4
+ /**
5
+ * @param {string[]} requiredPushConstants - names that every dispatch must pass in pushConstants
6
+ */
7
+ constructor(kernelHandle, backend, name, requiredPushConstants) {
8
+ this._handle = kernelHandle;
9
+ this._backend = backend;
10
+ this._name = name;
11
+ this._required = requiredPushConstants;
12
+ this._destroyed = false;
13
+ }
14
+
15
+ /** Throw if pushConstants lacks a declared value: Vulkan would read 0, the CPU undefined. */
16
+ _checkPushConstants(pushConstants) {
17
+ const missing = this._required.filter(k => pushConstants?.[k] === undefined);
18
+ if (missing.length > 0) {
19
+ throw new TypeError(`Kernel '${this._name}': pushConstants has no value for ${missing.map(k => `'${k}'`).join(', ')}`);
20
+ }
21
+ }
22
+
23
+ async dispatch({ buffers, pushConstants, groups, groupsY = 1, groupsZ = 1 }) {
24
+ this._checkPushConstants(pushConstants);
25
+ return this._backend.dispatch(this._handle, { buffers, pushConstants, groups, groupsY, groupsZ });
26
+ }
27
+
28
+ destroy() {
29
+ if (this._destroyed) return;
30
+ this._destroyed = true;
31
+ this._backend.destroyKernel(this._handle);
32
+ }
33
+ }
34
+
35
+ module.exports = { Kernel };
@@ -0,0 +1,220 @@
1
+ # node-red-gpu (experimental)
2
+
3
+ Run compute kernels on the GPU through Vulkan, from a [worker thread](../node-red-worker/README.md). You write the kernel in a small subset of JavaScript, or in GLSL.
4
+
5
+ This module is experimental and its API can change. It is tested on a Raspberry Pi 5 (V3D GPU). The native Vulkan addon is optional and not built on install. Without it, kernels run on the CPU backend, which needs no GPU.
6
+
7
+ ## Usage
8
+
9
+ An edge filter for gray images: a blur kernel, then a Sobel kernel. The node starts a worker and hands it the image:
10
+
11
+ ```js
12
+ const path = require('path');
13
+ const { NodeRedNodeBase, nodeRedWorker, nodeRedGpu } = require('@nrutils/node-red-utils');
14
+
15
+ // msg.payload is { width, height, pixels }: a Buffer with one gray byte per pixel
16
+ module.exports = NodeRedNodeBase.createNode('gpu-edges', [nodeRedWorker, nodeRedGpu], (RED, thisNode, config) => {
17
+ const worker = thisNode.nodeRedWorker.init(path.join(__dirname, 'edge-worker.js'));
18
+
19
+ thisNode.on('input', (msg, send, done) => {
20
+ worker.request('edges', msg.payload, [], { timeout: 5000 }).then((edges) => {
21
+ msg.payload = { ...msg.payload, pixels: Buffer.from(edges.buffer) };
22
+ send(msg);
23
+ done();
24
+ }, done);
25
+ });
26
+ });
27
+ ```
28
+
29
+ The worker owns the GPU context:
30
+
31
+ ```js
32
+ // edge-worker.js
33
+ const { nodeRedGpuWorker } = require('@nrutils/node-red-utils/node-red-gpu');
34
+
35
+ module.exports = function (worker) {
36
+ const gpu = nodeRedGpuWorker();
37
+ const options = {
38
+ buffers: [{ name: 'src', dtype: 'f32', access: 'readonly' }, { name: 'dst', dtype: 'f32', access: 'writeonly' }],
39
+ constants: { width: 'u32', height: 'u32' },
40
+ workgroupSize: [16, 16] // 2D: one thread per pixel; return writes dst[y * count + x]
41
+ };
42
+ const blur = gpu.registerJsKernel('blur', { ...options, kernel: function (src, dst) {
43
+ const x = this.thread.x;
44
+ const y = this.thread.y;
45
+ const w = this.constants.width;
46
+ if (x < 1 || y < 1 || x + 1 >= w || y + 1 >= this.constants.height) return src[y * w + x];
47
+ let sum = 0.0;
48
+ for (let dy = 0; dy < 3; dy++) {
49
+ for (let dx = 0; dx < 3; dx++) sum += src[(y + dy - 1) * w + x + dx - 1];
50
+ }
51
+ return sum / 9.0;
52
+ } });
53
+ const edges = gpu.registerJsKernel('edges', { ...options, kernel: function (src, dst) {
54
+ const x = this.thread.x;
55
+ const y = this.thread.y;
56
+ const w = this.constants.width;
57
+ if (x < 1 || y < 1 || x + 1 >= w || y + 1 >= this.constants.height) return 0.0;
58
+ const i = y * w + x;
59
+ const gx = src[i - w + 1] + 2.0 * src[i + 1] + src[i + w + 1] - src[i - w - 1] - 2.0 * src[i - 1] - src[i + w - 1];
60
+ const gy = src[i + w - 1] + 2.0 * src[i + w] + src[i + w + 1] - src[i - w - 1] - 2.0 * src[i - w] - src[i - w + 1];
61
+ return Math.sqrt(gx * gx + gy * gy);
62
+ } });
63
+
64
+ worker.handle('edges', async ({ width, height, pixels }, transferList) => {
65
+ // Released buffers are reused by the next request of the same size
66
+ const [a, b, c] = [0, 1, 2].map(() => gpu.createBuffer(width * height, 'f32'));
67
+ try {
68
+ a.view.set(pixels); // bytes to f32
69
+ const pushConstants = { count: width, countY: height, width, height };
70
+ const groups = Math.ceil(width / 16);
71
+ const groupsY = Math.ceil(height / 16);
72
+ // One submit: edges reads what blur wrote
73
+ await gpu.dispatchChain([
74
+ { kernel: blur, buffers: [a, b], pushConstants, groups, groupsY },
75
+ { kernel: edges, buffers: [b, c], pushConstants, groups, groupsY }
76
+ ]);
77
+ const out = Uint8ClampedArray.from(c.view);
78
+ transferList.push(out.buffer); // moved to the main thread, not copied
79
+ return out;
80
+ } finally {
81
+ [a, b, c].forEach((buf) => gpu.releaseBuffer(buf));
82
+ }
83
+ });
84
+ worker.parentPorts.subscribe('close', () => gpu.close());
85
+ };
86
+ ```
87
+
88
+ The two kernels run in one `dispatchChain`, and the blurred image stays in buffer `b` between them. After each request the three buffers go back to the pool, so the next image of the same size reuses them. Without the Vulkan addon, the same kernels run on the CPU backend.
89
+
90
+ `request()` is described in [node-red-worker](../node-red-worker/README.md#usage). If the kernel throws, or the worker stops, the request rejects and `done(err)` reports it.
91
+
92
+ The `nodeRedGpu` mixin on the main thread checks that Vulkan loads and logs one line per process: the device name, or the reason for the CPU fallback. If you force the Vulkan backend and it fails, the node shows a red status and logs the error. The mixin also points the SPIR-V cache at the Node-RED user directory (see [Build the Vulkan addon](#build-the-vulkan-addon-linux)).
93
+
94
+ ### Constants and push constants
95
+
96
+ The word "constants" has two sides:
97
+
98
+ - At registration, `constants` declares the names and types: `constants: { width: 'u32', height: 'u32' }`. The kernel reads them as `this.constants.width`.
99
+ - At dispatch, `pushConstants` gives the values: `pushConstants: { count: width, countY: height, width, height }`. The name comes from Vulkan, which sends them to the GPU as push constants.
100
+
101
+ A JS kernel also has the push constants `count`, `countY` and `countZ` (see [JS kernels](#js-kernels)). Every dispatch must give `count` and each declared constant. If one is missing, `dispatch` and `dispatchChain` throw an error that names it. `countY` and `countZ` can be left out. A raw kernel must get every name in its `pushConstants`. Specialization constants (`specConstants`) are not passed at dispatch; see `respecializeKernel`.
102
+
103
+ ## Backends
104
+
105
+ Set `NODE_RED_GPU_BACKEND` before you start Node-RED:
106
+
107
+ - `auto` (default): try Vulkan, else use the CPU and log a warning with the reason.
108
+ - `vulkan`: Vulkan or an error.
109
+ - `cpu`: always the CPU.
110
+
111
+ The CPU backend calls a JavaScript function for each kernel. `registerJsKernel` makes that function from your kernel. A raw `registerKernel` call needs a `cpuFallback` function; without one, it throws on the CPU backend. `cpuFallback` gets `{ buffers, pushConstants, specConstants, groups, groupsY, groupsZ }`, where `buffers` are typed arrays.
112
+
113
+ ## Raw GLSL kernels
114
+
115
+ ```js
116
+ const ADD_GLSL = `#version 450
117
+ layout(local_size_x = 256) in;
118
+ layout(std430, binding = 0) readonly buffer A { uint a[]; };
119
+ layout(std430, binding = 1) readonly buffer B { uint b[]; };
120
+ layout(std430, binding = 2) writeonly buffer O { uint o[]; };
121
+ layout(push_constant) uniform P { uint count; } p;
122
+ void main() {
123
+ uint i = gl_GlobalInvocationID.x;
124
+ if (i < p.count) o[i] = a[i] + b[i];
125
+ }`;
126
+
127
+ const add = gpu.registerKernel('add', {
128
+ source: ADD_GLSL,
129
+ language: 'glsl', // or 'spirv' with a SPIR-V binary
130
+ bindings: ['readonly', 'readonly', 'writeonly'],
131
+ pushConstants: { count: 'u32' },
132
+ workgroupSize: 256,
133
+ cpuFallback({ buffers: [a, b, out], pushConstants: p }) {
134
+ for (let i = 0; i < p.count; i++) out[i] = a[i] + b[i];
135
+ }
136
+ });
137
+ ```
138
+
139
+ ## API
140
+
141
+ `nodeRedGpuWorker()` returns a GPU context for the current thread. Names in `[ ]` are optional.
142
+
143
+ - `registerJsKernel(name, { kernel, [buffers], [constants], [specConstants], [workgroupSize] })`: transpile a JS kernel to GLSL and register it. Returns a kernel.
144
+ - `registerKernel(name, { source, [language], [bindings], [pushConstants], [specConstants], [workgroupSize], [cpuFallback] })`: register a GLSL or SPIR-V kernel. Returns a kernel. `language` is `'glsl'` if you leave it out. The CPU backend needs `cpuFallback`.
145
+ - `createBuffer(count, dtype)`: returns a buffer. It reuses a released buffer of the same byte size if one exists. The contents are not cleared, so a reused buffer holds old data. Call `buffer.view.fill(0)` if the kernel needs zeros.
146
+ - `releaseBuffer(buffer)`: give a buffer back for reuse.
147
+ - `stats()`: device, buffer, kernel and dispatch counters.
148
+ - `close()`: destroy all buffers and kernels of this context. Call it on `close` in the worker.
149
+
150
+ - `dispatchChain([{ kernel, buffers, [pushConstants], groups, [groupsY], [groupsZ] }])`: run several kernels in order. `kernel` is a kernel from `registerKernel` or `registerJsKernel`. Each kernel sees the writes of the kernel before it. With Vulkan this is one queue submit with memory barriers between the kernels.
151
+ - `respecializeKernel(kernel, values)`: set new specialization constant values. A constant that is not in `values` becomes 0. With Vulkan this rebuilds the pipeline; the CPU backend keeps the values for the next dispatch. Throws if the kernel has no specialization constants.
152
+
153
+ These need the Vulkan backend:
154
+
155
+ - `exportBuffer(buffer)` and `importBuffer(ref)`: pass a buffer to another worker without a copy. The ref is a plain object that you can publish.
156
+
157
+ A kernel has `dispatch({ buffers, [pushConstants], groups, [groupsY], [groupsZ] })`, which returns a Promise, and `destroy()`.
158
+
159
+ A buffer has `view` (a typed array; with Vulkan it maps GPU memory), `count`, `dtype`, `byteLength`, `destroy()`, and `detach()`, which returns the `ArrayBuffer` and destroys the buffer.
160
+
161
+ Data types: `u8` `u16` `u32` `i8` `i16` `i32` `f32` `f64`.
162
+
163
+ ## JS kernels
164
+
165
+ The kernel is a `function`, named or anonymous, with one parameter per buffer, in the order of `buffers`. Give each parameter the same name as its buffer. `registerJsKernel` throws a `GpuCompileError` on both backends for these mistakes, so a kernel that registers on the CPU backend also compiles on the GPU:
166
+
167
+ - An arrow function. It has no `this`, so use `function`.
168
+ - A variable from outside the function. The CPU backend rebuilds the kernel from its source, and the GPU cannot see JavaScript variables. Pass values in `this.constants`.
169
+ - A buffer, constant or local variable with a GLSL keyword or built-in name, such as `output`, `out`, `in`, `input`, `texture`, `mix`, `min`, `vec3` or a name that starts with `gl_`. The error names the identifier.
170
+
171
+ Inside the kernel you can use:
172
+
173
+ - `this.thread.x`, `.y`, `.z`, `.localX`, `.localY`, `.localZ`, and `this.constants.<name>`.
174
+ - `let` and `const`, `for`, `if`/`else`, the ternary operator, arithmetic, bitwise and comparison operators, `buf[index]` reads and writes.
175
+ - `return value` to write to the output buffer (the first buffer with `writeonly` or `readwrite` access), at index `x + y * count + z * count * countY`.
176
+ - `Math.abs`, `ceil`, `floor`, `round`, `sign`, `min`, `max`, `sqrt`, `pow`, `exp`, `log`, `log2`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`.
177
+
178
+ You cannot use `while`, nested functions, objects, arrays, strings, classes, `async` or `require`.
179
+
180
+ The transpiler also accepts GLSL-only calls:
181
+
182
+ - `float()`, `int()`, `uint()` and `Math.clamp`, `exp2`, `mix`, `step`, `smoothstep`, `fract`, `mod`. The CPU backend runs them with small JavaScript versions. Integer division stays different: in JavaScript `uint(v) / 2` gives a fraction, in GLSL it rounds down.
183
+ - `atomicAdd()` and the other atomics, and `barrier()`. They need the Vulkan backend. On the CPU backend, `registerJsKernel` throws a `GpuCompileError` that names the call.
184
+
185
+ Buffer options:
186
+
187
+ ```js
188
+ { name: 'data', dtype: 'f32' } // readwrite storage buffer
189
+ { name: 'data', dtype: 'f32', access: 'readonly' } // or 'writeonly'
190
+ { name: 'tile', dtype: 'f32', shared: true, count: 256 } // workgroup shared memory
191
+ ```
192
+
193
+ The context adds the push constants `count`, `countY` and `countZ`. Threads with `x >= count`, `y >= countY` or `z >= countZ` do not run; a `countY` or `countZ` of 0 counts as 1. For a 2D kernel, set `workgroupSize: [16, 16]` and pass `count`, `countY`, `groups` and `groupsY` to `dispatch`. `return v` then writes to index `y * count + x` of the output buffer, on the GPU and on the CPU.
194
+
195
+ ## Build the Vulkan addon (Linux)
196
+
197
+ These steps are for Debian, Ubuntu and Raspberry Pi OS.
198
+
199
+ ```bash
200
+ # build
201
+ sudo apt install g++ make python3 libvulkan-dev
202
+ # runtime
203
+ sudo apt install mesa-vulkan-drivers glslang-tools vulkan-tools
204
+
205
+ cd node_modules/@nrutils/node-red-utils/node-red-gpu/native
206
+ npx node-gyp rebuild
207
+ ```
208
+
209
+ - `glslangValidator` (from `glslang-tools`) must be on the `PATH`. It compiles GLSL to SPIR-V at kernel registration.
210
+ - `node-addon-api` must be in `node_modules`. It is an optional dependency of this package, so `npm install` puts it there unless you install with `--omit=optional`.
211
+ - Run `vulkaninfo --summary` to check the GPU. If you see only `llvmpipe`, the driver cannot reach the GPU; check the permissions on `/dev/dri/renderD*`. The addon picks a hardware device and uses `llvmpipe` only if there is no other.
212
+ - Compiled SPIR-V is cached in `gpu-cache/` in the Node-RED user directory (the `-u` option; `~/.node-red` by default). The `nodeRedGpu` mixin sets `NODE_RED_GPU_CACHE` from `RED.settings.userDir`, and worker threads read it from there. Set `NODE_RED_GPU_CACHE` yourself to use another directory. Outside Node-RED the default is `~/.node-red/gpu-cache/`.
213
+
214
+ From a clone of this repository, `npm run test:gpu` runs a raw GLSL kernel and a JS kernel on the selected backend.
215
+
216
+ ## How it works
217
+
218
+ All workers in the process share one Vulkan device. Buffer handles are valid in every worker, which is what makes `exportBuffer` work. On GPUs with unified memory, such as the Pi 5, `view` maps the GPU memory, so there is no copy between JavaScript and the GPU. Dispatches run off the JavaScript thread.
219
+
220
+ [Back to node-red-utils](../README.md)
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+
3
+ // The CPU backend ignores GLSL source entirely and calls cpuFallback instead.
4
+ // Kernels from registerJsKernel get a cpuFallback automatically. Raw kernels need their own.
5
+ // cpuFallback gets { buffers, pushConstants, specConstants, groups, groupsY, groupsZ }.
6
+
7
+ const { GpuCompileError, GpuOutOfMemoryError } = require('../errors.js');
8
+
9
+ const DTYPE_CTOR = {
10
+ u8: Uint8Array,
11
+ i8: Int8Array,
12
+ u16: Uint16Array,
13
+ i16: Int16Array,
14
+ u32: Uint32Array,
15
+ i32: Int32Array,
16
+ f32: Float32Array,
17
+ f64: Float64Array,
18
+ };
19
+
20
+ class CpuBackend {
21
+ constructor() {
22
+ this._kernels = new Map();
23
+ this._buffers = new Set();
24
+ this._dispatches = 0;
25
+ this._nextId = 1;
26
+ }
27
+
28
+ registerKernel(name, options) {
29
+ if (!options.cpuFallback) throw new GpuCompileError(`Kernel '${name}' requires cpuFallback for the CPU backend`);
30
+ // Spec constant values, as the Vulkan backend sets them: the default, else 0
31
+ const specConstants = {};
32
+ for (const [k, v] of Object.entries(options.specConstants || {})) {
33
+ specConstants[k] = (typeof v === 'object' ? v.default : 0) || 0;
34
+ }
35
+ const handle = { id: this._nextId++, name, cpuFallback: options.cpuFallback, specConstants };
36
+ this._kernels.set(handle.id, handle);
37
+ return handle;
38
+ }
39
+
40
+ createBuffer(count, dtype, _options) {
41
+ const Ctor = DTYPE_CTOR[dtype];
42
+ if (!Ctor) throw new TypeError(`Unknown dtype: ${dtype}`);
43
+ let typedArray;
44
+ try {
45
+ typedArray = new Ctor(count);
46
+ } catch (e) {
47
+ throw new GpuOutOfMemoryError(`Failed to allocate ${count} × ${dtype}`, count * Ctor.BYTES_PER_ELEMENT);
48
+ }
49
+ const handle = { view: typedArray, count, dtype, byteLength: typedArray.byteLength };
50
+ this._buffers.add(handle);
51
+ return handle;
52
+ }
53
+
54
+ async dispatch(kernelHandle, { buffers, pushConstants, groups, groupsY, groupsZ }) {
55
+ const views = buffers.map(b => b._handle.view);
56
+ kernelHandle.cpuFallback({ buffers: views, pushConstants, specConstants: kernelHandle.specConstants, groups, groupsY, groupsZ });
57
+ this._dispatches++;
58
+ }
59
+
60
+ // Run the kernels in order; each one sees the writes of the one before
61
+ async dispatchChain(specs) {
62
+ for (const s of specs) {
63
+ await this.dispatch(s.kernelHandle, { ...s, groupsY: s.groupsY || 1, groupsZ: s.groupsZ || 1 });
64
+ }
65
+ }
66
+
67
+ // There is no pipeline to rebuild: store the values for the next dispatch
68
+ respecializeKernel(handle, specValues) {
69
+ if (Object.keys(handle.specConstants).length === 0) {
70
+ throw new Error(`respecializeKernel: kernel '${handle.name}' has no specialization constants`);
71
+ }
72
+ // Like Vulkan, a value that is not given becomes 0
73
+ for (const k of Object.keys(handle.specConstants)) handle.specConstants[k] = specValues[k] ?? 0;
74
+ }
75
+
76
+ refBuffer(_handle) {
77
+ throw new Error('CpuBackend does not support shared GPU buffers');
78
+ }
79
+
80
+ importBuffer(_ref) {
81
+ throw new Error('CpuBackend does not support shared GPU buffers');
82
+ }
83
+
84
+ destroyBuffer(handle) {
85
+ this._buffers.delete(handle);
86
+ }
87
+
88
+ destroyKernel(handle) {
89
+ this._kernels.delete(handle.id);
90
+ }
91
+
92
+ destroyDevice() {
93
+ this._buffers.clear();
94
+ this._kernels.clear();
95
+ }
96
+
97
+ stats() {
98
+ let totalBytes = 0;
99
+ for (const b of this._buffers) totalBytes += b.byteLength;
100
+ return {
101
+ device: { name: 'cpu', driverVersion: 'n/a', api: 'cpu' },
102
+ buffers: { count: this._buffers.size, totalBytes, hostVisible: this._buffers.size, deviceLocal: 0 },
103
+ kernels: { count: this._kernels.size, cacheHits: 0, cacheMisses: 0 },
104
+ dispatches: { submitted: this._dispatches, completed: this._dispatches, inFlight: 0, lastDurationUs: 0 },
105
+ };
106
+ }
107
+ }
108
+
109
+ module.exports = { CpuBackend };
@@ -0,0 +1,232 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const crypto = require('crypto');
7
+ const { execFileSync } = require('child_process');
8
+ const { GpuAddonMissingError, GpuDeviceLostError, GpuCompileError, GpuOutOfMemoryError, GpuDispatchError } = require('../errors.js');
9
+
10
+ let addon;
11
+ try {
12
+ addon = require('../native/build/Release/gpu_addon.node');
13
+ } catch (e) {
14
+ throw new GpuAddonMissingError(`Failed to load gpu_addon: ${e.message}`);
15
+ }
16
+
17
+ // Read at compile time: the nodeRedGpu mixin sets NODE_RED_GPU_CACHE from the Node-RED userDir
18
+ const cacheDir = () => process.env.NODE_RED_GPU_CACHE || path.join(os.homedir(), '.node-red', 'gpu-cache');
19
+
20
+ const DTYPE_CTOR = {
21
+ u8: Uint8Array, i8: Int8Array, u16: Uint16Array, i16: Int16Array,
22
+ u32: Uint32Array, i32: Int32Array, f32: Float32Array, f64: Float64Array,
23
+ };
24
+
25
+ const PC_ENCODERS = {
26
+ u32: (view, off, val) => { view.setUint32(off, val, true); return 4; },
27
+ i32: (view, off, val) => { view.setInt32(off, val, true); return 4; },
28
+ f32: (view, off, val) => { view.setFloat32(off, val, true); return 4; },
29
+ u64: (view, off, val) => { view.setBigUint64(off, BigInt(val), true); return 8; },
30
+ };
31
+
32
+ const PC_SIZES = { u32: 4, i32: 4, f32: 4, u64: 8 };
33
+
34
+ function compileSpirvFromGlsl(source) {
35
+ const hash = crypto.createHash('sha256').update(source).digest('hex');
36
+ const CACHE_DIR = cacheDir();
37
+ const spvPath = path.join(CACHE_DIR, `${hash}.spv`);
38
+ if (fs.existsSync(spvPath)) return { spv: fs.readFileSync(spvPath), cacheHit: true };
39
+
40
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
41
+ const compPath = path.join(CACHE_DIR, `${hash}.comp`);
42
+ fs.writeFileSync(compPath, source, 'utf8');
43
+ try {
44
+ execFileSync('glslangValidator', ['--target-env', 'vulkan1.2', '-S', 'comp', '-o', spvPath, compPath]);
45
+ } catch (e) {
46
+ // glslangValidator prints its errors to stdout
47
+ throw new GpuCompileError(`glslangValidator failed`, `${e.stdout || ''}${e.stderr || ''}` || e.message);
48
+ } finally {
49
+ try { fs.unlinkSync(compPath); } catch (_) {}
50
+ }
51
+ return { spv: fs.readFileSync(spvPath), cacheHit: false };
52
+ }
53
+
54
+ function encodePushConstants(schema, values) {
55
+ const totalBytes = Object.values(schema).reduce((sum, t) => sum + (PC_SIZES[t] || 4), 0);
56
+ const ab = new ArrayBuffer(totalBytes);
57
+ const view = new DataView(ab);
58
+ let offset = 0;
59
+ for (const [field, type] of Object.entries(schema)) {
60
+ const encoder = PC_ENCODERS[type];
61
+ if (!encoder) throw new TypeError(`Unknown push constant type: ${type}`);
62
+ offset += encoder(view, offset, values[field] ?? 0);
63
+ }
64
+ return ab;
65
+ }
66
+
67
+ class VulkanBackend {
68
+ constructor() {
69
+ let deviceInfo;
70
+ try {
71
+ deviceInfo = addon.openDevice();
72
+ } catch (e) {
73
+ throw new GpuDeviceLostError(`openDevice failed: ${e.message}`);
74
+ }
75
+ this._device = deviceInfo;
76
+ this._kernels = new Map();
77
+ this._buffers = new Set();
78
+ this._nextId = 1;
79
+ this._cacheHits = 0;
80
+ this._cacheMisses = 0;
81
+ }
82
+
83
+ registerKernel(name, options) {
84
+ const { source, language = 'glsl', bindings = [], pushConstants = {}, workgroupSize = 64, specConstants = {} } = options;
85
+ let spirv;
86
+ if (language === 'spirv') {
87
+ spirv = source instanceof ArrayBuffer ? source : Buffer.from(source).buffer;
88
+ } else {
89
+ const { spv, cacheHit } = compileSpirvFromGlsl(source);
90
+ cacheHit ? this._cacheHits++ : this._cacheMisses++;
91
+ spirv = spv.buffer.slice(spv.byteOffset, spv.byteOffset + spv.byteLength);
92
+ }
93
+ const pcSize = Object.values(pushConstants).reduce((s, t) => s + (PC_SIZES[t] || 4), 0);
94
+
95
+ // Build spec constant count and initial values buffer
96
+ const specConstantNames = Object.keys(specConstants);
97
+ const specCount = specConstantNames.length;
98
+ let addonHandle;
99
+ try {
100
+ if (specCount > 0) {
101
+ // Encode initial spec values (all 0 by default — caller calls respecializeKernel to update)
102
+ const specSchema = {};
103
+ for (const [k, v] of Object.entries(specConstants)) {
104
+ specSchema[k] = typeof v === 'object' ? v.dtype : (v || 'u32');
105
+ }
106
+ const initialValues = {};
107
+ for (const [k, v] of Object.entries(specConstants)) {
108
+ initialValues[k] = (typeof v === 'object' ? v.default : 0) || 0;
109
+ }
110
+ const specAb = encodePushConstants(specSchema, initialValues);
111
+ addonHandle = addon.registerKernel(spirv, bindings.length, pcSize, workgroupSize, specCount, specAb);
112
+ } else {
113
+ addonHandle = addon.registerKernel(spirv, bindings.length, pcSize, workgroupSize);
114
+ }
115
+ } catch (e) {
116
+ throw new GpuCompileError(`registerKernel '${name}' failed: ${e.message}`);
117
+ }
118
+
119
+ // Build schema for spec constants (for respecializeKernel encoding)
120
+ const specConstantSchema = {};
121
+ for (const [k, v] of Object.entries(specConstants)) {
122
+ specConstantSchema[k] = typeof v === 'object' ? v.dtype : (v || 'u32');
123
+ }
124
+
125
+ const handle = {
126
+ id: this._nextId++,
127
+ name,
128
+ _addonHandle: addonHandle,
129
+ pushConstantSchema: pushConstants,
130
+ specConstantSchema,
131
+ specConstantCount: specCount,
132
+ };
133
+ this._kernels.set(handle.id, handle);
134
+ return handle;
135
+ }
136
+
137
+ respecializeKernel(handle, specValues) {
138
+ if (!handle.specConstantCount) {
139
+ throw new Error(`respecializeKernel: kernel '${handle.name}' has no specialization constants`);
140
+ }
141
+ const ab = encodePushConstants(handle.specConstantSchema, specValues);
142
+ try {
143
+ addon.respecializeKernel(handle._addonHandle, ab);
144
+ } catch (e) {
145
+ throw new GpuCompileError(`respecializeKernel '${handle.name}' failed: ${e.message}`);
146
+ }
147
+ }
148
+
149
+ createBuffer(count, dtype, _options) {
150
+ const Ctor = DTYPE_CTOR[dtype];
151
+ if (!Ctor) throw new TypeError(`Unknown dtype: ${dtype}`);
152
+ const byteSize = count * Ctor.BYTES_PER_ELEMENT;
153
+ let result;
154
+ try {
155
+ result = addon.createBuffer(byteSize);
156
+ } catch (e) {
157
+ throw new GpuOutOfMemoryError(`createBuffer failed for ${byteSize} bytes: ${e.message}`, byteSize);
158
+ }
159
+ const view = new Ctor(result.buffer);
160
+ const handle = { view, count, dtype, byteLength: byteSize, _addonHandle: result.handle };
161
+ this._buffers.add(handle);
162
+ return handle;
163
+ }
164
+
165
+ async dispatch(kernelHandle, { buffers, pushConstants, groups, groupsY = 1, groupsZ = 1 }) {
166
+ const handleArray = buffers.map(b => b._handle._addonHandle);
167
+ const pcArrayBuffer = encodePushConstants(kernelHandle.pushConstantSchema, pushConstants || {});
168
+ try {
169
+ await addon.dispatch(kernelHandle._addonHandle, handleArray, pcArrayBuffer, groups, groupsY, groupsZ);
170
+ } catch (e) {
171
+ throw new GpuDispatchError(`dispatch failed: ${e.message}`);
172
+ }
173
+ }
174
+
175
+ refBuffer(handle) {
176
+ addon.refBuffer(handle._addonHandle);
177
+ }
178
+
179
+ importBuffer(ref) {
180
+ const Ctor = DTYPE_CTOR[ref.dtype];
181
+ if (!Ctor) throw new TypeError(`Unknown dtype: ${ref.dtype}`);
182
+ const ab = addon.getBufferView(ref._gpuHandle);
183
+ const view = new Ctor(ab);
184
+ const handle = { view, count: ref.count, dtype: ref.dtype, byteLength: ref.byteLength, _addonHandle: ref._gpuHandle };
185
+ this._buffers.add(handle);
186
+ return handle;
187
+ }
188
+
189
+ async dispatchChain(specs) {
190
+ const nativeSpecs = specs.map(s => ({
191
+ kernel: s.kernelHandle._addonHandle,
192
+ buffers: s.buffers.map(b => b._handle._addonHandle),
193
+ pushConstants: encodePushConstants(s.kernelHandle.pushConstantSchema, s.pushConstants || {}),
194
+ groupsX: s.groups,
195
+ groupsY: s.groupsY || 1,
196
+ groupsZ: s.groupsZ || 1,
197
+ }));
198
+ try {
199
+ await addon.dispatchChain(nativeSpecs);
200
+ } catch (e) {
201
+ throw new GpuDispatchError(`dispatchChain failed: ${e.message}`);
202
+ }
203
+ }
204
+
205
+ destroyBuffer(handle) {
206
+ addon.destroyBuffer(handle._addonHandle);
207
+ this._buffers.delete(handle);
208
+ }
209
+
210
+ destroyKernel(handle) {
211
+ addon.destroyKernel(handle._addonHandle);
212
+ this._kernels.delete(handle.id);
213
+ }
214
+
215
+ destroyDevice() {
216
+ this._buffers.clear();
217
+ this._kernels.clear();
218
+ addon.destroyDevice();
219
+ }
220
+
221
+ stats() {
222
+ const s = addon.stats();
223
+ return {
224
+ device: { name: this._device.deviceName, driverVersion: this._device.driverVersion, api: this._device.apiVersion, memoryBudgetMB: this._device.memoryBudgetMB },
225
+ buffers: { count: s.buffersAllocated, totalBytes: s.buffersBytes, hostVisible: s.buffersAllocated, deviceLocal: 0 },
226
+ kernels: { count: s.kernelsRegistered, cacheHits: this._cacheHits, cacheMisses: this._cacheMisses },
227
+ dispatches: { submitted: s.dispatchesCompleted, completed: s.dispatchesCompleted, inFlight: 0, lastDurationUs: s.lastDispatchUs, lastGpuExecutionUs: s.lastGpuExecutionUs || 0 },
228
+ };
229
+ }
230
+ }
231
+
232
+ module.exports = { VulkanBackend };
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ class GpuError extends Error {
4
+ constructor(message, code) {
5
+ super(message);
6
+ this.name = 'GpuError';
7
+ this.code = code;
8
+ }
9
+ }
10
+
11
+ class GpuCompileError extends GpuError {
12
+ constructor(message, diagnostics = []) {
13
+ super(message, 'GPU_COMPILE_ERROR');
14
+ this.name = 'GpuCompileError';
15
+ this.diagnostics = diagnostics;
16
+ }
17
+ }
18
+
19
+ class GpuOutOfMemoryError extends GpuError {
20
+ constructor(message, requestedBytes) {
21
+ super(message, 'GPU_OUT_OF_MEMORY');
22
+ this.name = 'GpuOutOfMemoryError';
23
+ this.requestedBytes = requestedBytes;
24
+ }
25
+ }
26
+
27
+ class GpuDispatchError extends GpuError {
28
+ constructor(message) {
29
+ super(message, 'GPU_DISPATCH_FAILED');
30
+ this.name = 'GpuDispatchError';
31
+ }
32
+ }
33
+
34
+ class GpuDeviceLostError extends GpuError {
35
+ constructor(message) {
36
+ super(message, 'GPU_DEVICE_LOST');
37
+ this.name = 'GpuDeviceLostError';
38
+ }
39
+ }
40
+
41
+ class GpuAddonMissingError extends GpuError {
42
+ constructor(message) {
43
+ super(message, 'GPU_ADDON_MISSING');
44
+ this.name = 'GpuAddonMissingError';
45
+ }
46
+ }
47
+
48
+ module.exports = { GpuError, GpuCompileError, GpuOutOfMemoryError, GpuDispatchError, GpuDeviceLostError, GpuAddonMissingError };
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ const { nodeRedGpu } = require('./nodeRedGpuMain.js');
4
+ const { nodeRedGpuWorker } = require('./nodeRedGpuWorker.js');
5
+
6
+ module.exports = { nodeRedGpu, nodeRedGpuWorker };
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ // The backend choice is logged once per process, not once per node or worker
4
+ let logged = false;
5
+
6
+ /** Call log(message) the first time only. */
7
+ function logBackendOnce(log, message) {
8
+ if (logged) return;
9
+ logged = true;
10
+ log(message);
11
+ }
12
+
13
+ /** The fallback message: the first line of the error, without the require stack. */
14
+ function fallbackMessage(err) {
15
+ return `node-red-gpu: Vulkan is not available (${String(err.message).split('\n')[0]}). Using the CPU backend.`;
16
+ }
17
+
18
+ module.exports = { logBackendOnce, fallbackMessage };