@nadrif/thread 0.1.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/LICENSE +21 -0
- package/README.md +1156 -0
- package/package.json +133 -0
- package/src/adapters.js +404 -0
- package/src/config/adapters.js +188 -0
- package/src/config/define.js +171 -0
- package/src/config/env.js +218 -0
- package/src/config/frameworks.js +268 -0
- package/src/config/index.js +256 -0
- package/src/config/schema.js +375 -0
- package/src/config.js +52 -0
- package/src/deno.js +29 -0
- package/src/edge.js +38 -0
- package/src/env.js +361 -0
- package/src/error.js +340 -0
- package/src/factory.js +591 -0
- package/src/gpu/adapters.js +227 -0
- package/src/gpu/chains.js +261 -0
- package/src/gpu/env.js +372 -0
- package/src/gpu/gpu.js +1199 -0
- package/src/gpu/helpers.js +240 -0
- package/src/gpu/hooks.js +284 -0
- package/src/gpu/index.js +16 -0
- package/src/gpu/shaders.js +834 -0
- package/src/gpu/special.js +493 -0
- package/src/hooks.js +448 -0
- package/src/index.js +557 -0
- package/src/metrix.js +222 -0
- package/src/node.js +36 -0
- package/src/pool.js +740 -0
- package/src/serializer.js +139 -0
- package/src/thread.js +1246 -0
- package/src/types.js +1354 -0
- package/src/worker-factory.js +347 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Framework adapters for GPU compute.
|
|
3
|
+
*
|
|
4
|
+
* These adapters bridge {@link GPUCompute} to Zustand, Preact Signals,
|
|
5
|
+
* and any generic reactive setter. Unlike the thread adapters which
|
|
6
|
+
* rely on an event emitter, GPU adapters intercept `run()` calls
|
|
7
|
+
* directly because {@link GPUCompute} does not emit events.
|
|
8
|
+
*
|
|
9
|
+
* @module gpu/adapters
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Zustand adapter
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Bind a GPU compute instance's results to a **Zustand** store.
|
|
18
|
+
*
|
|
19
|
+
* Every time `binder.run()` resolves, the specified store action is
|
|
20
|
+
* called with the result. Errors can optionally be routed to a
|
|
21
|
+
* separate action.
|
|
22
|
+
*
|
|
23
|
+
* @param {import('./gpu.js').GPUCompute} gpu
|
|
24
|
+
* The GPU instance to bind.
|
|
25
|
+
* @param {Function} store
|
|
26
|
+
* The Zustand hook (e.g. `useStore`).
|
|
27
|
+
* @param {string} action
|
|
28
|
+
* Name of the store action to call with results.
|
|
29
|
+
* @param {Object} [options={}]
|
|
30
|
+
* @param {string} [options.errorAction]
|
|
31
|
+
* Store action to call with error messages on failure.
|
|
32
|
+
* @param {Function} [options.transform]
|
|
33
|
+
* Transform the result before writing to the store.
|
|
34
|
+
* @param {string} [options.metricsAction]
|
|
35
|
+
* Store action to call with metrics snapshots.
|
|
36
|
+
* @returns {{ run: Function, destroy: Function }}
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```js
|
|
40
|
+
* import { create } from 'zustand';
|
|
41
|
+
* import { createGPUCompute } from './gpu.js';
|
|
42
|
+
* import { createGPUBinder } from './adapters.js';
|
|
43
|
+
*
|
|
44
|
+
* const useStore = create((set) => ({
|
|
45
|
+
* result: null,
|
|
46
|
+
* loading: false,
|
|
47
|
+
* error: null,
|
|
48
|
+
* setData: (data) => set({ result: data, loading: false }),
|
|
49
|
+
* setError: (err) => set({ error: err, loading: false }),
|
|
50
|
+
* setLoading: () => set({ loading: true, error: null }),
|
|
51
|
+
* }));
|
|
52
|
+
*
|
|
53
|
+
* const gpu = createGPUCompute({ shader: myShader });
|
|
54
|
+
* const binder = createGPUBinder(gpu, useStore, 'setData', {
|
|
55
|
+
* errorAction: 'setError',
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* await binder.run('add', data1, data2);
|
|
59
|
+
* const data = useStore.getState().result;
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function createGPUBinder(gpu, store, action, options = {}) {
|
|
63
|
+
const { errorAction = null, transform = null, metricsAction = null } = options;
|
|
64
|
+
|
|
65
|
+
const originalRun = gpu.run.bind(gpu);
|
|
66
|
+
|
|
67
|
+
const wrappedRun = async (...args) => {
|
|
68
|
+
const state = store.getState();
|
|
69
|
+
if (state[action]) state[action].loading?.call ? undefined : undefined;
|
|
70
|
+
if (state.setLoading) state.setLoading();
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const result = await originalRun(...args);
|
|
74
|
+
const finalResult = transform ? transform(result) : result;
|
|
75
|
+
store.getState()[action](finalResult);
|
|
76
|
+
if (metricsAction && store.getState()[metricsAction]) {
|
|
77
|
+
store.getState()[metricsAction](gpu.metrics);
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (errorAction && store.getState()[errorAction]) {
|
|
82
|
+
store.getState()[errorAction](err.message || String(err));
|
|
83
|
+
}
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
run: wrappedRun,
|
|
90
|
+
destroy: () => {
|
|
91
|
+
// wrappedRun holds originalRun in closure — nothing to undo
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Preact Signals adapter
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Bind a GPU compute instance's results to a **Preact Signal**.
|
|
102
|
+
*
|
|
103
|
+
* Every time `binder.run()` resolves, the signal's value is updated.
|
|
104
|
+
* Errors can optionally be written to a separate signal.
|
|
105
|
+
*
|
|
106
|
+
* @param {import('./gpu.js').GPUCompute} gpu
|
|
107
|
+
* The GPU instance to bind.
|
|
108
|
+
* @param {Object} signal
|
|
109
|
+
* A Preact Signal (or any object with `.value` getter/setter).
|
|
110
|
+
* @param {Object} [options={}]
|
|
111
|
+
* @param {Object} [options.errorSignal]
|
|
112
|
+
* A signal to write errors into.
|
|
113
|
+
* @param {Function} [options.transform]
|
|
114
|
+
* Transform the result before writing to the signal.
|
|
115
|
+
* @param {Object} [options.loadingSignal]
|
|
116
|
+
* A signal to write loading state (boolean) into.
|
|
117
|
+
* @returns {{ run: Function, destroy: Function }}
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```js
|
|
121
|
+
* import { signal } from '@preact/signals';
|
|
122
|
+
* import { createGPUCompute } from './gpu.js';
|
|
123
|
+
* import { createGPUSignalBinder } from './adapters.js';
|
|
124
|
+
*
|
|
125
|
+
* const dataSignal = signal(null);
|
|
126
|
+
* const errorSignal = signal(null);
|
|
127
|
+
* const loadingSignal = signal(false);
|
|
128
|
+
*
|
|
129
|
+
* const gpu = createGPUCompute();
|
|
130
|
+
* const binder = createGPUSignalBinder(gpu, dataSignal, {
|
|
131
|
+
* errorSignal,
|
|
132
|
+
* loadingSignal,
|
|
133
|
+
* });
|
|
134
|
+
*
|
|
135
|
+
* await binder.run('ema', priceData, { alpha: 0.3 });
|
|
136
|
+
* console.log(dataSignal.value); // Float32Array of EMA values
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
export function createGPUSignalBinder(gpu, signal, options = {}) {
|
|
140
|
+
const { errorSignal = null, transform = null, loadingSignal = null } = options;
|
|
141
|
+
|
|
142
|
+
const originalRun = gpu.run.bind(gpu);
|
|
143
|
+
|
|
144
|
+
const wrappedRun = async (...args) => {
|
|
145
|
+
if (loadingSignal) loadingSignal.value = true;
|
|
146
|
+
if (errorSignal) errorSignal.value = null;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const result = await originalRun(...args);
|
|
150
|
+
signal.value = transform ? transform(result) : result;
|
|
151
|
+
if (errorSignal) errorSignal.value = null;
|
|
152
|
+
if (loadingSignal) loadingSignal.value = false;
|
|
153
|
+
return result;
|
|
154
|
+
} catch (err) {
|
|
155
|
+
if (errorSignal) errorSignal.value = err.message || String(err);
|
|
156
|
+
if (loadingSignal) loadingSignal.value = false;
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
run: wrappedRun,
|
|
163
|
+
destroy: () => {},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Generic reactive adapter
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Bind a GPU compute instance's results to **any** reactive state
|
|
173
|
+
* system via a setter callback.
|
|
174
|
+
*
|
|
175
|
+
* Works with MobX, Redux, Vue refs, Svelte stores, or plain `setState`
|
|
176
|
+
* functions.
|
|
177
|
+
*
|
|
178
|
+
* @param {import('./gpu.js').GPUCompute} gpu
|
|
179
|
+
* The GPU instance to bind.
|
|
180
|
+
* @param {Function} setter
|
|
181
|
+
* Called with `(result)` on success.
|
|
182
|
+
* @param {Object} [options={}]
|
|
183
|
+
* @param {Function} [options.onError]
|
|
184
|
+
* Called with `(error)` on failure.
|
|
185
|
+
* @param {Function} [options.transform]
|
|
186
|
+
* Transform the result before passing to the setter.
|
|
187
|
+
* @param {Function} [options.onMetrics]
|
|
188
|
+
* Called with metrics snapshots after each run.
|
|
189
|
+
* @returns {{ run: Function, destroy: Function }}
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```js
|
|
193
|
+
* import { createGPUStoreBinder } from './adapters.js';
|
|
194
|
+
*
|
|
195
|
+
* const gpu = createGPUCompute();
|
|
196
|
+
* const [data, setData] = useState(null);
|
|
197
|
+
*
|
|
198
|
+
* const binder = createGPUStoreBinder(gpu, setData, {
|
|
199
|
+
* onError: (err) => console.error(err),
|
|
200
|
+
* });
|
|
201
|
+
*
|
|
202
|
+
* await binder.run('ema', priceData, { alpha: 0.3 });
|
|
203
|
+
* // data is now set to the result
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
export function createGPUStoreBinder(gpu, setter, options = {}) {
|
|
207
|
+
const { onError = null, transform = null, onMetrics = null } = options;
|
|
208
|
+
|
|
209
|
+
const originalRun = gpu.run.bind(gpu);
|
|
210
|
+
|
|
211
|
+
const wrappedRun = async (...args) => {
|
|
212
|
+
try {
|
|
213
|
+
const result = await originalRun(...args);
|
|
214
|
+
setter(transform ? transform(result) : result);
|
|
215
|
+
if (onMetrics) onMetrics(gpu.metrics);
|
|
216
|
+
return result;
|
|
217
|
+
} catch (err) {
|
|
218
|
+
if (onError) onError(err.message || String(err));
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
run: wrappedRun,
|
|
225
|
+
destroy: () => {},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Pipeline chaining classes for GPU compute.
|
|
3
|
+
*
|
|
4
|
+
* Provides {@link PipelineChain} and {@link DataPipelineChain} for
|
|
5
|
+
* fluent, sequential GPU operation composition.
|
|
6
|
+
*
|
|
7
|
+
* @module gpu/chains
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { GPUComputeError } from '../error.js';
|
|
11
|
+
import { boxAll, isOldRunFormat, jsToWgsl, getParamNames } from './helpers.js';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// PipelineChain — fluent API for chaining GPU operations
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Fluent pipeline chain for sequencing GPU operations.
|
|
19
|
+
*
|
|
20
|
+
* Created by {@link GPUCompute.pipe}. Each method queues an operation,
|
|
21
|
+
* and `.result()` executes them all sequentially, feeding outputs
|
|
22
|
+
* as inputs to the next step.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```js
|
|
26
|
+
* const chain = gpu.pipe()
|
|
27
|
+
* .multiply({ inputs: { a: data }, uniforms: { b: new Float32Array([2.0]) } })
|
|
28
|
+
* .add({ uniforms: { b: new Float32Array([1.0]) } })
|
|
29
|
+
* .sqrt();
|
|
30
|
+
*
|
|
31
|
+
* const output = await chain.result();
|
|
32
|
+
* // output.result → Float32Array
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export class PipelineChain {
|
|
36
|
+
/**
|
|
37
|
+
* Create a new pipeline chain.
|
|
38
|
+
*
|
|
39
|
+
* @param {import('./gpu.js').GPUCompute} gpu - GPUCompute instance.
|
|
40
|
+
*/
|
|
41
|
+
constructor(gpu) {
|
|
42
|
+
/** @type {import('./gpu.js').GPUCompute} */
|
|
43
|
+
this._gpu = gpu;
|
|
44
|
+
|
|
45
|
+
/** @type {Array<{ name: string, input: Object }>} */
|
|
46
|
+
this._steps = [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Add an operation to the chain.
|
|
51
|
+
*
|
|
52
|
+
* Accepts any of these forms:
|
|
53
|
+
* - `add('op', { inputs: {...}, uniforms: {...} })` — old format
|
|
54
|
+
* - `add('op', { a: data }, { b: scalar })` — new flat format
|
|
55
|
+
* - `add(x => Math.sqrt(x))` — JS function (auto-named, single input)
|
|
56
|
+
*
|
|
57
|
+
* @param {string|Function} nameOrFn - Operation name or JS function.
|
|
58
|
+
* @param {Object} [inputsOrSpec={}] - Input data (flat) or old-format spec.
|
|
59
|
+
* @param {Object} [uniforms={}] - Uniform values (flat).
|
|
60
|
+
* @returns {PipelineChain} This chain (for chaining).
|
|
61
|
+
*/
|
|
62
|
+
add(nameOrFn, inputsOrSpec = {}, uniforms = {}) {
|
|
63
|
+
// JS function shorthand: add(x => sqrt(x))
|
|
64
|
+
if (typeof nameOrFn === 'function') {
|
|
65
|
+
const fn = nameOrFn;
|
|
66
|
+
const inputNames = getParamNames(fn);
|
|
67
|
+
const wgslBody = jsToWgsl(fn);
|
|
68
|
+
const opName = `_pipe_${wgslBody.replace(/\W+/g, '_').slice(0, 32)}`;
|
|
69
|
+
|
|
70
|
+
// Build CPU fallback
|
|
71
|
+
const cpuFn = async (input) => {
|
|
72
|
+
const src = input.inputs[inputNames[0]];
|
|
73
|
+
const result = new Float32Array(src.length);
|
|
74
|
+
for (let i = 0; i < src.length; i++) result[i] = fn(src[i]);
|
|
75
|
+
return { result };
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
this._gpu.define(opName, {
|
|
79
|
+
inputs: inputNames,
|
|
80
|
+
outputs: ['result'],
|
|
81
|
+
body: `result[i] = ${wgslBody};`,
|
|
82
|
+
fn: cpuFn,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
this._steps.push({ name: opName, input: {} });
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Named op
|
|
90
|
+
let input;
|
|
91
|
+
if (isOldRunFormat(inputsOrSpec)) {
|
|
92
|
+
input = inputsOrSpec;
|
|
93
|
+
} else {
|
|
94
|
+
input = { inputs: boxAll(inputsOrSpec), uniforms: boxAll(uniforms) };
|
|
95
|
+
}
|
|
96
|
+
this._steps.push({ name: nameOrFn, input });
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Execute all queued operations sequentially.
|
|
102
|
+
*
|
|
103
|
+
* Each step's output is merged into the next step's inputs.
|
|
104
|
+
* The final step's output is returned.
|
|
105
|
+
*
|
|
106
|
+
* @param {Object} [opts={}]
|
|
107
|
+
* @param {AbortSignal} [opts.signal] - Cancellation signal.
|
|
108
|
+
* @returns {Promise<Object<string, TypedArray>>}
|
|
109
|
+
*/
|
|
110
|
+
async result(opts = {}) {
|
|
111
|
+
if (this._steps.length === 0) {
|
|
112
|
+
throw new GPUComputeError('Pipeline chain is empty — add steps first');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Seed initial inputs from opts (e.g. result({ inputs: { data } }))
|
|
116
|
+
let carriedOutputs = {};
|
|
117
|
+
if (opts.inputs) {
|
|
118
|
+
carriedOutputs = boxAll(opts.inputs);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < this._steps.length; i++) {
|
|
122
|
+
const { name, input } = this._steps[i];
|
|
123
|
+
|
|
124
|
+
if (opts.signal?.aborted) {
|
|
125
|
+
throw new GPUComputeError('Pipeline chain cancelled', opts.signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Look up op definition to know its expected input names
|
|
129
|
+
const opDef = this._gpu._ops?.get(name);
|
|
130
|
+
const expectedInputs = opDef?.inputs || [];
|
|
131
|
+
|
|
132
|
+
// Remap carried outputs to match the op's expected input names
|
|
133
|
+
// If op expects `x` but carried has `result`, map result → x
|
|
134
|
+
let remappedCarried = { ...carriedOutputs };
|
|
135
|
+
if (expectedInputs.length === 1) {
|
|
136
|
+
const expectedName = expectedInputs[0];
|
|
137
|
+
if (!(expectedName in remappedCarried) && Object.keys(remappedCarried).length === 1) {
|
|
138
|
+
const [onlyKey] = Object.keys(remappedCarried);
|
|
139
|
+
if (onlyKey !== expectedName) {
|
|
140
|
+
remappedCarried = { [expectedName]: remappedCarried[onlyKey] };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Merge: explicit step inputs > remapped carried outputs
|
|
146
|
+
const mergedInput = {
|
|
147
|
+
...input,
|
|
148
|
+
inputs: { ...remappedCarried, ...(input.inputs || {}) },
|
|
149
|
+
signal: opts.signal,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// If user provided outputs in opts, only pass them for the first step
|
|
153
|
+
if (i === 0 && opts.outputs) {
|
|
154
|
+
mergedInput.outputs = opts.outputs;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const result = await this._gpu.run(name, mergedInput);
|
|
158
|
+
carriedOutputs = result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return carriedOutputs;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Number of steps in the chain.
|
|
166
|
+
*
|
|
167
|
+
* @type {number}
|
|
168
|
+
*/
|
|
169
|
+
get length() {
|
|
170
|
+
return this._steps.length;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// DataPipelineChain — pipe(data, count) with Proxy-based named methods
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* A pipeline chain that carries initial data and supports named methods.
|
|
180
|
+
*
|
|
181
|
+
* Created by `gpu.pipe(data, count)`. Predefined ops become methods
|
|
182
|
+
* via a Proxy, and `.result()` executes the chain.
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```js
|
|
186
|
+
* const output = await gpu.pipe(new Float32Array([1, 2, 3]), 3)
|
|
187
|
+
* .ema({ alpha: 0.3 })
|
|
188
|
+
* .double()
|
|
189
|
+
* .result();
|
|
190
|
+
* // output.result → Float32Array
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
export class DataPipelineChain extends PipelineChain {
|
|
194
|
+
/**
|
|
195
|
+
* @param {import('./gpu.js').GPUCompute} gpu
|
|
196
|
+
* @param {TypedArray} data - Initial input data.
|
|
197
|
+
* @param {number} count - Element count.
|
|
198
|
+
*/
|
|
199
|
+
constructor(gpu, data, count) {
|
|
200
|
+
super(gpu);
|
|
201
|
+
/** @type {TypedArray} */
|
|
202
|
+
this._data = data;
|
|
203
|
+
/** @type {number} */
|
|
204
|
+
this._count = count;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Execute all queued operations with the carried data.
|
|
209
|
+
*
|
|
210
|
+
* The first step receives `this._data` as its first input.
|
|
211
|
+
* Subsequent steps receive the previous step's output.
|
|
212
|
+
*
|
|
213
|
+
* @param {Object} [opts={}]
|
|
214
|
+
* @param {AbortSignal} [opts.signal] - Cancellation signal.
|
|
215
|
+
* @returns {Promise<Object<string, TypedArray>>}
|
|
216
|
+
*/
|
|
217
|
+
async result(opts = {}) {
|
|
218
|
+
if (this._steps.length === 0) {
|
|
219
|
+
throw new GPUComputeError('Pipeline chain is empty — add steps first');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Seed with the carried data, mapped to the first step's expected input name
|
|
223
|
+
const firstOpDef = this._gpu._ops?.get(this._steps[0].name);
|
|
224
|
+
const firstName = firstOpDef?.inputs?.[0] || 'data';
|
|
225
|
+
let carriedOutputs = { [firstName]: this._data };
|
|
226
|
+
|
|
227
|
+
for (let i = 0; i < this._steps.length; i++) {
|
|
228
|
+
const { name, input } = this._steps[i];
|
|
229
|
+
|
|
230
|
+
if (opts.signal?.aborted) {
|
|
231
|
+
throw new GPUComputeError('Pipeline chain cancelled', opts.signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const opDef = this._gpu._ops?.get(name);
|
|
235
|
+
const expectedInputs = opDef?.inputs || [];
|
|
236
|
+
|
|
237
|
+
// Remap carried outputs to match the op's expected input names
|
|
238
|
+
let remappedCarried = { ...carriedOutputs };
|
|
239
|
+
if (expectedInputs.length === 1) {
|
|
240
|
+
const expectedName = expectedInputs[0];
|
|
241
|
+
if (!(expectedName in remappedCarried) && Object.keys(remappedCarried).length === 1) {
|
|
242
|
+
const [onlyKey] = Object.keys(remappedCarried);
|
|
243
|
+
if (onlyKey !== expectedName) {
|
|
244
|
+
remappedCarried = { [expectedName]: remappedCarried[onlyKey] };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const mergedInput = {
|
|
250
|
+
...input,
|
|
251
|
+
inputs: { ...remappedCarried, ...(input.inputs || {}) },
|
|
252
|
+
signal: opts.signal,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const result = await this._gpu.run(name, mergedInput);
|
|
256
|
+
carriedOutputs = result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return carriedOutputs;
|
|
260
|
+
}
|
|
261
|
+
}
|