@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,39 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { logBackendOnce, fallbackMessage } = require('./log.js');
5
+
6
+ const BACKEND = process.env.NODE_RED_GPU_BACKEND || 'auto';
7
+
8
+ const nodeRedGpu = (superclass) => class extends superclass {
9
+ constructor(RED, thisNode, config) {
10
+ super(RED, thisNode, config);
11
+
12
+ // Keep the SPIR-V cache in the userDir (-u). Worker threads get a copy of process.env.
13
+ if (!process.env.NODE_RED_GPU_CACHE && RED.settings?.userDir) {
14
+ process.env.NODE_RED_GPU_CACHE = path.join(RED.settings.userDir, 'gpu-cache');
15
+ }
16
+
17
+ let initError = null;
18
+ try {
19
+ if (BACKEND === 'vulkan' || BACKEND === 'auto') {
20
+ const { VulkanBackend } = require('./backends/VulkanBackend.js');
21
+ const vb = new VulkanBackend();
22
+ logBackendOnce((m) => RED.log.info(m), `node-red-gpu: using Vulkan device "${vb._device.deviceName}"`);
23
+ vb.destroyDevice();
24
+ }
25
+ } catch (err) {
26
+ if (BACKEND === 'vulkan') initError = err;
27
+ else logBackendOnce((m) => RED.log.warn(m), fallbackMessage(err));
28
+ }
29
+
30
+ if (initError) {
31
+ thisNode.status({ fill: 'red', shape: 'dot', text: initError.message });
32
+ thisNode.error(initError);
33
+ }
34
+
35
+ thisNode.on('close', (done) => done());
36
+ }
37
+ };
38
+
39
+ module.exports = { nodeRedGpu };
@@ -0,0 +1,242 @@
1
+ 'use strict';
2
+
3
+ const { CpuBackend } = require('./backends/CpuBackend.js');
4
+ const { Kernel } = require('./Kernel.js');
5
+ const { GpuBuffer } = require('./GpuBuffer.js');
6
+ const { transpile } = require('./transpiler/index.js');
7
+ const { GpuCompileError } = require('./errors.js');
8
+ const { logBackendOnce, fallbackMessage } = require('./log.js');
9
+ const { isMainThread } = require('worker_threads');
10
+
11
+ // JS versions of the GLSL-only math calls, so JS kernels also run on the CPU backend
12
+ const MATH_SHIMS = Object.assign(Object.create(Math), {
13
+ clamp: (x, lo, hi) => Math.min(Math.max(x, lo), hi),
14
+ exp2: (x) => 2 ** x,
15
+ mix: (a, b, t) => a * (1 - t) + b * t,
16
+ step: (edge, x) => (x < edge ? 0 : 1),
17
+ smoothstep: (e0, e1, x) => {
18
+ const t = Math.min(Math.max((x - e0) / (e1 - e0), 0), 1);
19
+ return t * t * (3 - 2 * t);
20
+ },
21
+ fract: (x) => x - Math.floor(x),
22
+ mod: (x, y) => x - y * Math.floor(x / y),
23
+ });
24
+ const CASTS = {
25
+ float: (x) => Math.fround(x),
26
+ int: (x) => x | 0,
27
+ uint: (x) => x >>> 0,
28
+ };
29
+
30
+ /** Rebuild the kernel source with the shims in scope, for the CPU backend. */
31
+ function makeCpuKernel(fn) {
32
+ const source = typeof fn === 'function' ? fn.toString() : fn;
33
+ return new Function('Math', 'float', 'int', 'uint', `return (${source});`)(MATH_SHIMS, CASTS.float, CASTS.int, CASTS.uint);
34
+ }
35
+
36
+ const BACKEND = process.env.NODE_RED_GPU_BACKEND || 'auto';
37
+
38
+ function makeBackend() {
39
+ if (BACKEND === 'cpu') return new CpuBackend();
40
+ if (BACKEND === 'vulkan') {
41
+ const { VulkanBackend } = require('./backends/VulkanBackend.js');
42
+ return new VulkanBackend();
43
+ }
44
+ if (BACKEND === 'auto') {
45
+ try {
46
+ const { VulkanBackend } = require('./backends/VulkanBackend.js');
47
+ return new VulkanBackend();
48
+ } catch (err) {
49
+ // In a worker thread the nodeRedGpu mixin on the main thread has already logged the reason
50
+ if (isMainThread) logBackendOnce(console.warn, fallbackMessage(err));
51
+ return new CpuBackend();
52
+ }
53
+ }
54
+ throw new Error(`Unknown GPU backend: ${BACKEND}`);
55
+ }
56
+
57
+ const nodeRedGpuWorker = () => {
58
+ const backend = makeBackend();
59
+ const kernels = new Set();
60
+ const buffers = new Set();
61
+ const pool = new Map(); // byteSize → [GpuBuffer, GpuBuffer, ...]
62
+ const DTYPE_BYTES = { u8: 1, i8: 1, u16: 2, i16: 2, u32: 4, i32: 4, f32: 4, f64: 8 };
63
+ const DTYPE_CTOR = { u8: Uint8Array, i8: Int8Array, u16: Uint16Array, i16: Int16Array, u32: Uint32Array, i32: Int32Array, f32: Float32Array, f64: Float64Array };
64
+
65
+ function registerKernel(name, options, requiredPushConstants = Object.keys(options.pushConstants || {})) {
66
+ const handle = backend.registerKernel(name, options);
67
+ const kernel = new Kernel(handle, backend, name, requiredPushConstants);
68
+ kernels.add(kernel);
69
+ return kernel;
70
+ }
71
+
72
+ function registerJsKernel(name, options) {
73
+ const { kernel: fn, buffers: bufSpec = [], constants = {}, workgroupSize = 256, specConstants = {} } = options;
74
+
75
+ // Transpile JS kernel → GLSL 450
76
+ const { glsl: glslSource, gpuOnlyCalls } = transpile(fn, {
77
+ buffers: bufSpec,
78
+ constants,
79
+ workgroupSize,
80
+ specConstants,
81
+ });
82
+
83
+ // The CPU runs threads one after another, so it cannot honor these calls
84
+ if (backend instanceof CpuBackend && gpuOnlyCalls.size > 0) {
85
+ throw new GpuCompileError(`Kernel '${name}' uses ${[...gpuOnlyCalls].join(', ')}, which needs the Vulkan backend. The CPU backend cannot run it.`);
86
+ }
87
+ const cpuKernel = makeCpuKernel(fn);
88
+
89
+ // Build the registerKernel options from the JS kernel spec
90
+ // Shared buffers are GLSL shared variables — they don't get Vulkan SSBO bindings
91
+ const bindings = bufSpec.filter(b => !b.shared).map(b => b.access || 'readwrite');
92
+ const specConstantSet = new Set(
93
+ Array.isArray(specConstants) ? specConstants : Object.keys(specConstants)
94
+ );
95
+ // Spec constants live in VkSpecializationInfo, not the push_constant block
96
+ const pushConstants = {};
97
+ for (const [k, v] of Object.entries(constants)) {
98
+ if (!specConstantSet.has(k)) pushConstants[k] = v;
99
+ }
100
+ pushConstants.count = 'u32';
101
+ // A dispatch must pass the constants and count; countY and countZ (for 2D and 3D) may be left out
102
+ const requiredPushConstants = Object.keys(pushConstants);
103
+ pushConstants.countY = 'u32';
104
+ pushConstants.countZ = 'u32';
105
+
106
+ // Auto-generate cpuFallback from the original JS function.
107
+ // Supports 1D, 2D, and 3D dispatch via pc.count / pc.countY / pc.countZ.
108
+ const cpuFallback = ({ buffers: cpuBufs, pushConstants: pc, specConstants: spec }) => {
109
+ const countX = pc.count;
110
+ const countY = pc.countY || 1;
111
+ const countZ = pc.countZ || 1;
112
+ const ctx = {
113
+ thread: { x: 0, y: 0, z: 0, localX: 0, localY: 0, localZ: 0 },
114
+ // Spec constants are read like the other constants
115
+ constants: { ...spec, ...pc },
116
+ };
117
+
118
+ // Rebuild the full buffer array, inserting locally-allocated typed arrays for
119
+ // shared buffers (which have no backing SSBO in cpuBufs).
120
+ let ssboBufIdx = 0;
121
+ const allBufs = bufSpec.map(buf => {
122
+ if (buf.shared) {
123
+ return new (DTYPE_CTOR[buf.dtype])(buf.count);
124
+ }
125
+ return cpuBufs[ssboBufIdx++];
126
+ });
127
+
128
+ // Find the output buffer (first readwrite or writeonly) in the full buf list
129
+ const outIdx = bufSpec.findIndex(b => !b.shared && (b.access === 'readwrite' || b.access === 'writeonly'));
130
+ const outBuf = outIdx >= 0 ? allBufs[outIdx] : allBufs[0];
131
+ for (let z = 0; z < countZ; z++) {
132
+ ctx.thread.z = z;
133
+ for (let y = 0; y < countY; y++) {
134
+ ctx.thread.y = y;
135
+ for (let x = 0; x < countX; x++) {
136
+ ctx.thread.x = x;
137
+ const result = cpuKernel.apply(ctx, allBufs);
138
+ if (result !== undefined) {
139
+ const idx = x + y * countX + z * countX * countY;
140
+ outBuf[idx] = result;
141
+ }
142
+ }
143
+ }
144
+ }
145
+ };
146
+
147
+ return registerKernel(name, {
148
+ source: glslSource,
149
+ language: 'glsl',
150
+ bindings,
151
+ pushConstants,
152
+ workgroupSize,
153
+ cpuFallback,
154
+ specConstants,
155
+ }, requiredPushConstants);
156
+ }
157
+
158
+ function createBuffer(count, dtype, options) {
159
+ const byteSize = count * DTYPE_BYTES[dtype];
160
+ const pooled = pool.get(byteSize);
161
+ if (pooled && pooled.length > 0) {
162
+ const buf = pooled.pop();
163
+ if (pooled.length === 0) pool.delete(byteSize);
164
+ // Re-type the view if dtype changed
165
+ if (buf.dtype !== dtype) {
166
+ const Ctor = DTYPE_CTOR[dtype];
167
+ buf._handle.view = new Ctor(buf._handle.view.buffer);
168
+ buf._handle.dtype = dtype;
169
+ buf._handle.count = count;
170
+ }
171
+ buf._destroyed = false;
172
+ buffers.add(buf);
173
+ return buf;
174
+ }
175
+ const handle = backend.createBuffer(count, dtype, options);
176
+ const buf = new GpuBuffer(handle, backend);
177
+ buffers.add(buf);
178
+ return buf;
179
+ }
180
+
181
+ function releaseBuffer(gpuBuffer) {
182
+ if (gpuBuffer._destroyed) return;
183
+ gpuBuffer._destroyed = true;
184
+ buffers.delete(gpuBuffer);
185
+ const byteSize = gpuBuffer.byteLength;
186
+ let list = pool.get(byteSize);
187
+ if (!list) { list = []; pool.set(byteSize, list); }
188
+ list.push(gpuBuffer);
189
+ }
190
+
191
+ function exportBuffer(gpuBuffer) {
192
+ backend.refBuffer(gpuBuffer._handle);
193
+ return {
194
+ _gpuHandle: gpuBuffer._handle._addonHandle,
195
+ dtype: gpuBuffer.dtype,
196
+ count: gpuBuffer.count,
197
+ byteLength: gpuBuffer.byteLength,
198
+ };
199
+ }
200
+
201
+ function importBuffer(ref) {
202
+ const handle = backend.importBuffer(ref);
203
+ const buf = new GpuBuffer(handle, backend);
204
+ buffers.add(buf);
205
+ return buf;
206
+ }
207
+
208
+ async function dispatchChain(specs) {
209
+ const handleSpecs = specs.map(({ kernel, ...rest }, i) => {
210
+ if (!(kernel instanceof Kernel)) throw new TypeError(`dispatchChain: spec ${i} needs a kernel from registerKernel or registerJsKernel in 'kernel'`);
211
+ kernel._checkPushConstants(rest.pushConstants);
212
+ return { ...rest, kernelHandle: kernel._handle };
213
+ });
214
+ return backend.dispatchChain(handleSpecs);
215
+ }
216
+
217
+ function stats() {
218
+ return backend.stats();
219
+ }
220
+
221
+ async function close() {
222
+ for (const buf of buffers) buf.destroy();
223
+ // Flush pooled buffers
224
+ for (const [, list] of pool) {
225
+ for (const buf of list) {
226
+ buf._destroyed = false; // un-flag so destroy() works
227
+ buf.destroy();
228
+ }
229
+ }
230
+ pool.clear();
231
+ for (const kernel of kernels) kernel.destroy();
232
+ backend.destroyDevice();
233
+ }
234
+
235
+ function respecializeKernel(kernel, specValues) {
236
+ backend.respecializeKernel(kernel._handle, specValues);
237
+ }
238
+
239
+ return { registerKernel, registerJsKernel, respecializeKernel, createBuffer, releaseBuffer, exportBuffer, importBuffer, dispatchChain, stats, close };
240
+ };
241
+
242
+ module.exports = { nodeRedGpuWorker };