@czap/worker 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 +19 -0
- package/dist/compositor-script.d.ts +19 -0
- package/dist/compositor-script.d.ts.map +1 -0
- package/dist/compositor-script.js +374 -0
- package/dist/compositor-script.js.map +1 -0
- package/dist/compositor-startup.d.ts +200 -0
- package/dist/compositor-startup.d.ts.map +1 -0
- package/dist/compositor-startup.js +490 -0
- package/dist/compositor-startup.js.map +1 -0
- package/dist/compositor-types.d.ts +135 -0
- package/dist/compositor-types.d.ts.map +1 -0
- package/dist/compositor-types.js +7 -0
- package/dist/compositor-types.js.map +1 -0
- package/dist/compositor-worker.d.ts +65 -0
- package/dist/compositor-worker.d.ts.map +1 -0
- package/dist/compositor-worker.js +454 -0
- package/dist/compositor-worker.js.map +1 -0
- package/dist/evaluate-inline.d.ts +22 -0
- package/dist/evaluate-inline.d.ts.map +1 -0
- package/dist/evaluate-inline.js +42 -0
- package/dist/evaluate-inline.js.map +1 -0
- package/dist/host.d.ts +97 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +115 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +254 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +55 -0
- package/dist/messages.js.map +1 -0
- package/dist/render-worker.d.ts +77 -0
- package/dist/render-worker.d.ts.map +1 -0
- package/dist/render-worker.js +396 -0
- package/dist/render-worker.js.map +1 -0
- package/dist/spsc-ring.d.ts +171 -0
- package/dist/spsc-ring.d.ts.map +1 -0
- package/dist/spsc-ring.js +240 -0
- package/dist/spsc-ring.js.map +1 -0
- package/package.json +51 -0
- package/src/compositor-script.ts +374 -0
- package/src/compositor-startup.ts +666 -0
- package/src/compositor-types.ts +175 -0
- package/src/compositor-worker.ts +613 -0
- package/src/evaluate-inline.ts +42 -0
- package/src/host.ts +189 -0
- package/src/index.ts +49 -0
- package/src/messages.ts +343 -0
- package/src/render-worker.ts +454 -0
- package/src/spsc-ring.ts +309 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPSCRing -- lock-free single-producer single-consumer ring buffer
|
|
3
|
+
* backed by SharedArrayBuffer.
|
|
4
|
+
*
|
|
5
|
+
* Designed for real-time compositor state streaming between a Worker
|
|
6
|
+
* (producer) and the main thread (consumer) without blocking either side.
|
|
7
|
+
*
|
|
8
|
+
* ## SharedArrayBuffer requirements
|
|
9
|
+
*
|
|
10
|
+
* SharedArrayBuffer requires the page to be served with the following
|
|
11
|
+
* HTTP headers (COOP/COEP):
|
|
12
|
+
*
|
|
13
|
+
* Cross-Origin-Opener-Policy: same-origin
|
|
14
|
+
* Cross-Origin-Embedder-Policy: require-corp
|
|
15
|
+
*
|
|
16
|
+
* Without these headers, `new SharedArrayBuffer(...)` will throw.
|
|
17
|
+
*
|
|
18
|
+
* ## Memory layout
|
|
19
|
+
*
|
|
20
|
+
* ```
|
|
21
|
+
* Int32Array view (control region):
|
|
22
|
+
* [0]: write cursor (atomically incremented by producer)
|
|
23
|
+
* [1]: read cursor (atomically incremented by consumer)
|
|
24
|
+
*
|
|
25
|
+
* Float64Array view (data region):
|
|
26
|
+
* Offset = 8 bytes (aligned after two Int32 control slots)
|
|
27
|
+
* [0 .. slotCount * slotSize - 1]: ring buffer data slots
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* The producer writes at `writeCursor % slotCount`, the consumer reads
|
|
31
|
+
* at `readCursor % slotCount`. The buffer is full when
|
|
32
|
+
* `write - read === slotCount`, empty when `write === read`.
|
|
33
|
+
*
|
|
34
|
+
* Only `Atomics.load` and `Atomics.store` are used -- no `Atomics.wait`
|
|
35
|
+
* or `Atomics.notify` -- keeping this fully lock-free and non-blocking.
|
|
36
|
+
*
|
|
37
|
+
* @module
|
|
38
|
+
*/
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Constants
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
/** Byte offset where the Int32 control slots live. */
|
|
43
|
+
const WRITE_CURSOR_INDEX = 0;
|
|
44
|
+
const READ_CURSOR_INDEX = 1;
|
|
45
|
+
/**
|
|
46
|
+
* Byte size of the control region: two Int32 values (8 bytes),
|
|
47
|
+
* padded to 8-byte alignment for the Float64 data region.
|
|
48
|
+
*/
|
|
49
|
+
const CONTROL_BYTES = 8;
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Implementation
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
function _createBuffer(slotCount, slotSize) {
|
|
54
|
+
const dataBytes = slotCount * slotSize * Float64Array.BYTES_PER_ELEMENT;
|
|
55
|
+
return new SharedArrayBuffer(CONTROL_BYTES + dataBytes);
|
|
56
|
+
}
|
|
57
|
+
function _makeRing(sab, slotCount, slotSize, role) {
|
|
58
|
+
if (slotCount <= 0 || !Number.isInteger(slotCount)) {
|
|
59
|
+
throw new RangeError(`SPSCRingBuffer: slotCount must be a positive integer, got ${slotCount}`);
|
|
60
|
+
}
|
|
61
|
+
if (slotSize <= 0 || !Number.isInteger(slotSize)) {
|
|
62
|
+
throw new RangeError(`SPSCRingBuffer: slotSize must be a positive integer, got ${slotSize}`);
|
|
63
|
+
}
|
|
64
|
+
const control = new Int32Array(sab, 0, 2);
|
|
65
|
+
const data = new Float64Array(sab, CONTROL_BYTES);
|
|
66
|
+
return {
|
|
67
|
+
push(input) {
|
|
68
|
+
if (role !== 'producer') {
|
|
69
|
+
throw new Error('SPSCRingBuffer: only the producer may push');
|
|
70
|
+
}
|
|
71
|
+
if (input.length !== slotSize) {
|
|
72
|
+
throw new RangeError(`SPSCRingBuffer: expected slot size ${slotSize}, got ${input.length}`);
|
|
73
|
+
}
|
|
74
|
+
const write = Atomics.load(control, WRITE_CURSOR_INDEX);
|
|
75
|
+
const read = Atomics.load(control, READ_CURSOR_INDEX);
|
|
76
|
+
// Full when write - read === slotCount
|
|
77
|
+
if (write - read >= slotCount) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const slotIndex = (write % slotCount) * slotSize;
|
|
81
|
+
for (let i = 0; i < slotSize; i++) {
|
|
82
|
+
data[slotIndex + i] = input[i];
|
|
83
|
+
}
|
|
84
|
+
// Store with release semantics: the data write must be visible
|
|
85
|
+
// before the cursor advances. Atomics.store on Int32Array provides
|
|
86
|
+
// a sequentially consistent store which is stronger than needed
|
|
87
|
+
// but correct.
|
|
88
|
+
Atomics.store(control, WRITE_CURSOR_INDEX, write + 1);
|
|
89
|
+
return true;
|
|
90
|
+
},
|
|
91
|
+
pop(out) {
|
|
92
|
+
if (role !== 'consumer') {
|
|
93
|
+
throw new Error('SPSCRingBuffer: only the consumer may pop');
|
|
94
|
+
}
|
|
95
|
+
if (out.length !== slotSize) {
|
|
96
|
+
throw new RangeError(`SPSCRingBuffer: expected slot size ${slotSize}, got ${out.length}`);
|
|
97
|
+
}
|
|
98
|
+
const write = Atomics.load(control, WRITE_CURSOR_INDEX);
|
|
99
|
+
const read = Atomics.load(control, READ_CURSOR_INDEX);
|
|
100
|
+
// Empty when write === read
|
|
101
|
+
if (write === read) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const slotIndex = (read % slotCount) * slotSize;
|
|
105
|
+
for (let i = 0; i < slotSize; i++) {
|
|
106
|
+
out[i] = data[slotIndex + i];
|
|
107
|
+
}
|
|
108
|
+
Atomics.store(control, READ_CURSOR_INDEX, read + 1);
|
|
109
|
+
return true;
|
|
110
|
+
},
|
|
111
|
+
get capacity() {
|
|
112
|
+
return slotCount;
|
|
113
|
+
},
|
|
114
|
+
get count() {
|
|
115
|
+
const write = Atomics.load(control, WRITE_CURSOR_INDEX);
|
|
116
|
+
const read = Atomics.load(control, READ_CURSOR_INDEX);
|
|
117
|
+
return write - read;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Public API
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
/**
|
|
125
|
+
* Create a matched producer/consumer pair sharing the same SharedArrayBuffer.
|
|
126
|
+
*
|
|
127
|
+
* Typically called on the main thread; the `buffer` (SharedArrayBuffer) is
|
|
128
|
+
* then transferred to the Worker via `postMessage`, and the Worker calls
|
|
129
|
+
* `SPSCRing.attachProducer` to get its side of the ring.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* import { SPSCRing } from '@czap/worker';
|
|
134
|
+
*
|
|
135
|
+
* const { buffer, producer, consumer } = SPSCRing.createPair(64, 4);
|
|
136
|
+
* // producer.push(new Float64Array([1, 2, 3, 4])); // true
|
|
137
|
+
* // consumer.pop(new Float64Array(4)); // true
|
|
138
|
+
* // Transfer buffer to a Worker via postMessage
|
|
139
|
+
* worker.postMessage({ buffer, slotCount: 64, slotSize: 4 });
|
|
140
|
+
* ```
|
|
141
|
+
*
|
|
142
|
+
* @param slotCount - Number of slots in the ring (power of 2 recommended)
|
|
143
|
+
* @param slotSize - Number of Float64 values per slot
|
|
144
|
+
* @returns An object with the shared buffer and producer/consumer ring handles
|
|
145
|
+
*/
|
|
146
|
+
function _createPair(slotCount, slotSize) {
|
|
147
|
+
const buffer = _createBuffer(slotCount, slotSize);
|
|
148
|
+
return {
|
|
149
|
+
buffer,
|
|
150
|
+
producer: _makeRing(buffer, slotCount, slotSize, 'producer'),
|
|
151
|
+
consumer: _makeRing(buffer, slotCount, slotSize, 'consumer'),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Attach as producer to an existing SharedArrayBuffer.
|
|
156
|
+
* Call this inside the Worker that produces data.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```ts
|
|
160
|
+
* import { SPSCRing } from '@czap/worker';
|
|
161
|
+
*
|
|
162
|
+
* // Inside a Worker's message handler:
|
|
163
|
+
* self.onmessage = (e) => {
|
|
164
|
+
* const { buffer, slotCount, slotSize } = e.data;
|
|
165
|
+
* const producer = SPSCRing.attachProducer(buffer, slotCount, slotSize);
|
|
166
|
+
* const data = new Float64Array([1.0, 2.0, 3.0, 4.0]);
|
|
167
|
+
* producer.push(data); // true if buffer not full
|
|
168
|
+
* };
|
|
169
|
+
* ```
|
|
170
|
+
*
|
|
171
|
+
* @param sab - The SharedArrayBuffer from the main thread
|
|
172
|
+
* @param slotCount - Number of slots (must match createPair)
|
|
173
|
+
* @param slotSize - Float64 values per slot (must match createPair)
|
|
174
|
+
* @returns A producer-side {@link SPSCRingBufferShape}
|
|
175
|
+
*/
|
|
176
|
+
function _attachProducer(sab, slotCount, slotSize) {
|
|
177
|
+
return _makeRing(sab, slotCount, slotSize, 'producer');
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Attach as consumer to an existing SharedArrayBuffer.
|
|
181
|
+
* Call this on the main thread that consumes data.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```ts
|
|
185
|
+
* import { SPSCRing } from '@czap/worker';
|
|
186
|
+
*
|
|
187
|
+
* // On the main thread after receiving buffer from Worker:
|
|
188
|
+
* const consumer = SPSCRing.attachConsumer(sharedBuffer, 64, 4);
|
|
189
|
+
* const out = new Float64Array(4);
|
|
190
|
+
* if (consumer.pop(out)) {
|
|
191
|
+
* console.log('Received:', out); // Float64Array [1.0, 2.0, 3.0, 4.0]
|
|
192
|
+
* }
|
|
193
|
+
* ```
|
|
194
|
+
*
|
|
195
|
+
* @param sab - The SharedArrayBuffer shared with the producer
|
|
196
|
+
* @param slotCount - Number of slots (must match createPair)
|
|
197
|
+
* @param slotSize - Float64 values per slot (must match createPair)
|
|
198
|
+
* @returns A consumer-side {@link SPSCRingBufferShape}
|
|
199
|
+
*/
|
|
200
|
+
function _attachConsumer(sab, slotCount, slotSize) {
|
|
201
|
+
return _makeRing(sab, slotCount, slotSize, 'consumer');
|
|
202
|
+
}
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Export
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
/**
|
|
207
|
+
* SPSC ring buffer namespace.
|
|
208
|
+
*
|
|
209
|
+
* Lock-free single-producer single-consumer ring buffer backed by
|
|
210
|
+
* `SharedArrayBuffer`. Designed for real-time compositor state streaming
|
|
211
|
+
* between a Worker (producer) and the main thread (consumer) without
|
|
212
|
+
* blocking either side. Uses only `Atomics.load`/`Atomics.store` --
|
|
213
|
+
* fully non-blocking.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```ts
|
|
217
|
+
* import { SPSCRing } from '@czap/worker';
|
|
218
|
+
*
|
|
219
|
+
* // Main thread: create pair and send buffer to Worker
|
|
220
|
+
* const { buffer, producer, consumer } = SPSCRing.createPair(128, 8);
|
|
221
|
+
* worker.postMessage({ buffer, slotCount: 128, slotSize: 8 });
|
|
222
|
+
*
|
|
223
|
+
* // In Worker: attach as producer
|
|
224
|
+
* // const producer = SPSCRing.attachProducer(buffer, 128, 8);
|
|
225
|
+
* // producer.push(new Float64Array(8));
|
|
226
|
+
*
|
|
227
|
+
* // Main thread: consume in animation loop
|
|
228
|
+
* const out = new Float64Array(8);
|
|
229
|
+
* function frame() {
|
|
230
|
+
* while (consumer.pop(out)) { /* process out *\/ }
|
|
231
|
+
* requestAnimationFrame(frame);
|
|
232
|
+
* }
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
export const SPSCRing = {
|
|
236
|
+
createPair: _createPair,
|
|
237
|
+
attachProducer: _attachProducer,
|
|
238
|
+
attachConsumer: _attachConsumer,
|
|
239
|
+
};
|
|
240
|
+
//# sourceMappingURL=spsc-ring.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spsc-ring.js","sourceRoot":"","sources":["../src/spsc-ring.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@czap/worker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Off-thread compositor and render workers",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Eassa Ayoub <eassa@heyoub.dev>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/heyoub/LiteShip",
|
|
10
|
+
"directory": "packages/worker"
|
|
11
|
+
},
|
|
12
|
+
"bugs": "https://github.com/heyoub/LiteShip/issues",
|
|
13
|
+
"homepage": "https://github.com/heyoub/LiteShip#readme",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"czap",
|
|
16
|
+
"web-worker",
|
|
17
|
+
"compositor",
|
|
18
|
+
"off-main-thread",
|
|
19
|
+
"spsc-ring",
|
|
20
|
+
"offscreen-canvas",
|
|
21
|
+
"typescript"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js",
|
|
31
|
+
"development": "./src/index.ts"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@czap/core": "0.1.0"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=22.0.0"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline worker script implementing a simplified compositor.
|
|
3
|
+
*
|
|
4
|
+
* This string is turned into a Blob URL at runtime so no separate
|
|
5
|
+
* worker file is needed. It cannot use ES module imports since it
|
|
6
|
+
* runs inside a classic Worker created from a Blob.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* JavaScript source of the inline compositor worker.
|
|
13
|
+
*
|
|
14
|
+
* The string is wrapped in a `Blob` at runtime and fed to the
|
|
15
|
+
* `Worker(url)` constructor, so this package ships without a separate
|
|
16
|
+
* worker entry file or bundler glue. Keep this source ES5-compatible:
|
|
17
|
+
* it runs inside a classic Worker and cannot use ES module imports.
|
|
18
|
+
*/
|
|
19
|
+
export const COMPOSITOR_WORKER_SCRIPT = /* js */ `
|
|
20
|
+
"use strict";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Simplified compositor state inside the worker
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/** @type {Map<string, { id: string; states: string[]; thresholds: number[]; currentState: string; currentGeneration: number; cssKey: string|null; glslKey: string|null; ariaKey: string|null; oneHotWeights: Record<string, Record<string, number>>|null; _keysResolved: boolean }>} */
|
|
27
|
+
const quantizers = new Map();
|
|
28
|
+
|
|
29
|
+
/** @type {Map<string, Record<string, number>>} */
|
|
30
|
+
const blendOverrides = new Map();
|
|
31
|
+
|
|
32
|
+
/** @type {Set<string>} */
|
|
33
|
+
const dirtyNames = new Set();
|
|
34
|
+
|
|
35
|
+
const MS_PER_SEC = 1000;
|
|
36
|
+
|
|
37
|
+
/** @type {number} */
|
|
38
|
+
let lastComputeTime = 0;
|
|
39
|
+
let frameCount = 0;
|
|
40
|
+
let fpsAccum = 0;
|
|
41
|
+
let currentFps = 0;
|
|
42
|
+
|
|
43
|
+
function removeQuantizer(name) {
|
|
44
|
+
quantizers.delete(name);
|
|
45
|
+
blendOverrides.delete(name);
|
|
46
|
+
dirtyNames.delete(name);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function evaluateQuantizer(name, value) {
|
|
50
|
+
const q = quantizers.get(name);
|
|
51
|
+
if (q) {
|
|
52
|
+
const newState = evaluateThresholds(q.thresholds, q.states, value);
|
|
53
|
+
if (newState !== q.currentState) {
|
|
54
|
+
q.currentState = newState;
|
|
55
|
+
dirtyNames.add(name);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function setBlendWeights(name, weights) {
|
|
61
|
+
blendOverrides.set(name, weights);
|
|
62
|
+
dirtyNames.add(name);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function applyResolvedStateEntry(entry) {
|
|
66
|
+
const q = quantizers.get(entry.name);
|
|
67
|
+
if (!q) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const nextGeneration = typeof entry.generation === "number" ? entry.generation : q.currentGeneration;
|
|
72
|
+
const changed = entry.state !== q.currentState || nextGeneration !== q.currentGeneration;
|
|
73
|
+
q.currentState = entry.state;
|
|
74
|
+
q.currentGeneration = nextGeneration;
|
|
75
|
+
if (changed) {
|
|
76
|
+
dirtyNames.add(entry.name);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function applyUpdate(update) {
|
|
81
|
+
switch (update.type) {
|
|
82
|
+
case "remove-quantizer":
|
|
83
|
+
removeQuantizer(update.name);
|
|
84
|
+
break;
|
|
85
|
+
case "evaluate":
|
|
86
|
+
evaluateQuantizer(update.name, update.value);
|
|
87
|
+
break;
|
|
88
|
+
case "set-blend":
|
|
89
|
+
setBlendWeights(update.name, update.weights);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function registerQuantizer(registration) {
|
|
95
|
+
const initialState =
|
|
96
|
+
typeof registration.initialState === "string"
|
|
97
|
+
? registration.initialState
|
|
98
|
+
: registration.states[0] || "";
|
|
99
|
+
const thresholdsRaw = registration.thresholds;
|
|
100
|
+
const thresholds = thresholdsRaw instanceof Float64Array
|
|
101
|
+
? Array.from(thresholdsRaw)
|
|
102
|
+
: Array.from(thresholdsRaw);
|
|
103
|
+
quantizers.set(registration.name, {
|
|
104
|
+
id: registration.boundaryId,
|
|
105
|
+
states: Array.from(registration.states),
|
|
106
|
+
thresholds: thresholds,
|
|
107
|
+
currentState: initialState,
|
|
108
|
+
currentGeneration: 0,
|
|
109
|
+
cssKey: null,
|
|
110
|
+
glslKey: null,
|
|
111
|
+
ariaKey: null,
|
|
112
|
+
oneHotWeights: null,
|
|
113
|
+
_keysResolved: false,
|
|
114
|
+
});
|
|
115
|
+
if (registration.blendWeights && typeof registration.blendWeights === "object") {
|
|
116
|
+
blendOverrides.set(registration.name, registration.blendWeights);
|
|
117
|
+
} else {
|
|
118
|
+
blendOverrides.delete(registration.name);
|
|
119
|
+
}
|
|
120
|
+
dirtyNames.add(registration.name);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function resolveOutputKeys(q, name) {
|
|
124
|
+
if (q._keysResolved) return;
|
|
125
|
+
q.cssKey = "--czap-" + name;
|
|
126
|
+
q.glslKey = "u_" + name;
|
|
127
|
+
q.ariaKey = "data-czap-" + name;
|
|
128
|
+
q.oneHotWeights = Object.fromEntries(
|
|
129
|
+
q.states.map((activeState) => [
|
|
130
|
+
activeState,
|
|
131
|
+
Object.fromEntries(
|
|
132
|
+
q.states.map((stateName) => [stateName, stateName === activeState ? 1 : 0]),
|
|
133
|
+
),
|
|
134
|
+
]),
|
|
135
|
+
);
|
|
136
|
+
q._keysResolved = true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resetWorkerState() {
|
|
140
|
+
quantizers.clear();
|
|
141
|
+
blendOverrides.clear();
|
|
142
|
+
dirtyNames.clear();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Evaluate which discrete state a value falls into based on thresholds.
|
|
147
|
+
* Thresholds are sorted ascending; the value maps to the state whose
|
|
148
|
+
* threshold it first exceeds (or the first state if below all thresholds).
|
|
149
|
+
*
|
|
150
|
+
* @param {number[]} thresholds
|
|
151
|
+
* @param {string[]} states
|
|
152
|
+
* @param {number} value
|
|
153
|
+
* @returns {string}
|
|
154
|
+
*/
|
|
155
|
+
function evaluateThresholds(thresholds, states, value) {
|
|
156
|
+
for (let i = thresholds.length - 1; i >= 0; i--) {
|
|
157
|
+
if (value >= thresholds[i]) {
|
|
158
|
+
return states[i] || states[0] || "";
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return states[0] || "";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Build a CompositeState from the current quantizer state.
|
|
166
|
+
* @returns {{ discrete: Record<string, string>; blend: Record<string, Record<string, number>>; outputs: { css: Record<string, number|string>; glsl: Record<string, number>; aria: Record<string, string> } }}
|
|
167
|
+
*/
|
|
168
|
+
function compute() {
|
|
169
|
+
const now = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
170
|
+
|
|
171
|
+
const discrete = {};
|
|
172
|
+
const blend = {};
|
|
173
|
+
const css = {};
|
|
174
|
+
const glsl = {};
|
|
175
|
+
const aria = {};
|
|
176
|
+
const resolvedStateGenerations = {};
|
|
177
|
+
|
|
178
|
+
// Only recompute dirty quantizers if we have a dirty set,
|
|
179
|
+
// otherwise recompute all (initial case or fallback).
|
|
180
|
+
const names = dirtyNames.size > 0
|
|
181
|
+
? Array.from(dirtyNames)
|
|
182
|
+
: Array.from(quantizers.keys());
|
|
183
|
+
|
|
184
|
+
for (const name of names) {
|
|
185
|
+
const q = quantizers.get(name);
|
|
186
|
+
if (!q) continue;
|
|
187
|
+
|
|
188
|
+
// Lazily resolve output keys on first compute
|
|
189
|
+
resolveOutputKeys(q, name);
|
|
190
|
+
|
|
191
|
+
const stateStr = q.currentState;
|
|
192
|
+
discrete[name] = stateStr;
|
|
193
|
+
resolvedStateGenerations[name] = q.currentGeneration;
|
|
194
|
+
|
|
195
|
+
// Blend weights
|
|
196
|
+
const override = blendOverrides.get(name);
|
|
197
|
+
if (override !== undefined) {
|
|
198
|
+
blend[name] = override;
|
|
199
|
+
} else {
|
|
200
|
+
blend[name] = q.oneHotWeights[stateStr] || {};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// CSS output
|
|
204
|
+
css[q.cssKey] = stateStr;
|
|
205
|
+
|
|
206
|
+
// GLSL output: index of current state
|
|
207
|
+
let stateIndex = 0;
|
|
208
|
+
for (let i = 0; i < q.states.length; i++) {
|
|
209
|
+
if (q.states[i] === stateStr) {
|
|
210
|
+
stateIndex = i;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
glsl[q.glslKey] = stateIndex;
|
|
215
|
+
|
|
216
|
+
// ARIA output
|
|
217
|
+
aria[q.ariaKey] = stateStr;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
dirtyNames.clear();
|
|
221
|
+
|
|
222
|
+
// Metrics
|
|
223
|
+
if (lastComputeTime > 0) {
|
|
224
|
+
const dt = now - lastComputeTime;
|
|
225
|
+
frameCount++;
|
|
226
|
+
fpsAccum += dt;
|
|
227
|
+
if (fpsAccum >= MS_PER_SEC) {
|
|
228
|
+
currentFps = Math.round((frameCount * MS_PER_SEC) / fpsAccum);
|
|
229
|
+
frameCount = 0;
|
|
230
|
+
fpsAccum -= MS_PER_SEC;
|
|
231
|
+
|
|
232
|
+
self.postMessage({
|
|
233
|
+
type: "metrics",
|
|
234
|
+
fps: currentFps,
|
|
235
|
+
budgetUsed: dt,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
lastComputeTime = now;
|
|
240
|
+
|
|
241
|
+
return { discrete, blend, outputs: { css, glsl, aria }, resolvedStateGenerations };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// Message handler
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
self.addEventListener("message", function (e) {
|
|
249
|
+
const msg = e.data;
|
|
250
|
+
if (!msg || typeof msg.type !== "string") return;
|
|
251
|
+
|
|
252
|
+
switch (msg.type) {
|
|
253
|
+
case "init": {
|
|
254
|
+
// Reset state on init
|
|
255
|
+
resetWorkerState();
|
|
256
|
+
self.postMessage({ type: "ready" });
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
case "add-quantizer": {
|
|
261
|
+
registerQuantizer(msg);
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
case "bootstrap-quantizers": {
|
|
266
|
+
for (const registration of msg.registrations) {
|
|
267
|
+
registerQuantizer(registration);
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
case "startup-compute": {
|
|
273
|
+
resetWorkerState();
|
|
274
|
+
const packet = msg.packet ?? { registrations: [], updates: [] };
|
|
275
|
+
for (const registration of packet.registrations) {
|
|
276
|
+
registerQuantizer(registration);
|
|
277
|
+
}
|
|
278
|
+
for (const update of packet.updates) {
|
|
279
|
+
applyUpdate(update);
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const state = compute();
|
|
283
|
+
self.postMessage({ type: "state", state: state, resolvedStateGenerations: state.resolvedStateGenerations });
|
|
284
|
+
} catch (err) {
|
|
285
|
+
self.postMessage({
|
|
286
|
+
type: "error",
|
|
287
|
+
message: err instanceof Error ? err.message : String(err),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
case "bootstrap-resolved-state": {
|
|
294
|
+
for (const entry of msg.states) {
|
|
295
|
+
applyResolvedStateEntry(entry);
|
|
296
|
+
}
|
|
297
|
+
if (msg.ack === true) {
|
|
298
|
+
self.postMessage({
|
|
299
|
+
type: "resolved-state-ack",
|
|
300
|
+
generation: typeof msg.states[0]?.generation === "number" ? msg.states[0].generation : 0,
|
|
301
|
+
states: msg.states.map((entry) => ({ name: entry.name, state: entry.state })),
|
|
302
|
+
additionalOutputsChanged: false,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
case "apply-resolved-state": {
|
|
309
|
+
for (const entry of msg.states) {
|
|
310
|
+
applyResolvedStateEntry(entry);
|
|
311
|
+
}
|
|
312
|
+
if (msg.ack === true) {
|
|
313
|
+
self.postMessage({
|
|
314
|
+
type: "resolved-state-ack",
|
|
315
|
+
generation: typeof msg.states[0]?.generation === "number" ? msg.states[0].generation : 0,
|
|
316
|
+
states: msg.states.map((entry) => ({ name: entry.name, state: entry.state })),
|
|
317
|
+
additionalOutputsChanged: false,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
case "remove-quantizer": {
|
|
324
|
+
removeQuantizer(msg.name);
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
case "evaluate": {
|
|
329
|
+
evaluateQuantizer(msg.name, msg.value);
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
case "set-blend": {
|
|
334
|
+
setBlendWeights(msg.name, msg.weights);
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
case "apply-updates": {
|
|
339
|
+
for (const update of msg.updates) {
|
|
340
|
+
applyUpdate(update);
|
|
341
|
+
}
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
case "warm-reset": {
|
|
346
|
+
blendOverrides.clear();
|
|
347
|
+
dirtyNames.clear();
|
|
348
|
+
for (const quantizer of quantizers.values()) {
|
|
349
|
+
quantizer.currentState = quantizer.states[0] || "";
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
case "compute": {
|
|
355
|
+
try {
|
|
356
|
+
const state = compute();
|
|
357
|
+
self.postMessage({ type: "state", state: state, resolvedStateGenerations: state.resolvedStateGenerations });
|
|
358
|
+
} catch (err) {
|
|
359
|
+
self.postMessage({
|
|
360
|
+
type: "error",
|
|
361
|
+
message: err instanceof Error ? err.message : String(err),
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
case "dispose": {
|
|
368
|
+
resetWorkerState();
|
|
369
|
+
self.close();
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
`;
|