@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,613 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CompositorWorker -- off-main-thread compositor running in a Web Worker.
|
|
3
|
+
*
|
|
4
|
+
* The worker maintains a simplified compositor that:
|
|
5
|
+
* - Tracks quantizer definitions (name maps to boundary plus current state)
|
|
6
|
+
* - Evaluates threshold-based quantization
|
|
7
|
+
* - Maintains blend weight overrides
|
|
8
|
+
* - Produces CompositeState on `compute` commands
|
|
9
|
+
* - Uses DirtyFlags for selective recomputation
|
|
10
|
+
*
|
|
11
|
+
* The worker script is inlined as a Blob URL to avoid bundler complexity
|
|
12
|
+
* with separate worker entry files.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Diagnostics } from '@czap/core';
|
|
18
|
+
import type { RuntimeCoordinator } from '@czap/core';
|
|
19
|
+
import type {
|
|
20
|
+
FromWorkerMessage,
|
|
21
|
+
WorkerConfig,
|
|
22
|
+
WorkerUpdate,
|
|
23
|
+
BootstrapQuantizerRegistration,
|
|
24
|
+
ResolvedStateEntry,
|
|
25
|
+
} from './messages.js';
|
|
26
|
+
import { makeResolvedStateEnvelope } from './messages.js';
|
|
27
|
+
|
|
28
|
+
// Re-export types from compositor-types
|
|
29
|
+
export type { CompositorWorkerStartupStage, CompositorWorkerStartupTelemetry } from './compositor-types.js';
|
|
30
|
+
|
|
31
|
+
import type {
|
|
32
|
+
CompositorWorkerState,
|
|
33
|
+
CompositorWorkerStartupStage,
|
|
34
|
+
ResolvedStateAckPayload,
|
|
35
|
+
CompositorWorkerShape,
|
|
36
|
+
CompositorWorkerStartupTelemetry,
|
|
37
|
+
} from './compositor-types.js';
|
|
38
|
+
|
|
39
|
+
// Import startup/lifecycle helpers
|
|
40
|
+
import {
|
|
41
|
+
currentTimeNs,
|
|
42
|
+
recordStartupDiagnosticStage,
|
|
43
|
+
notifyResolvedStateSettled,
|
|
44
|
+
createStartupPacketState,
|
|
45
|
+
buildStartupComputePacket,
|
|
46
|
+
getStartupPacketRuntimeSeed,
|
|
47
|
+
runtimeMatchesStartupSeed,
|
|
48
|
+
setStartupPacketRegistration,
|
|
49
|
+
pushStartupPacketUpdate,
|
|
50
|
+
setStartupPacketInitialState,
|
|
51
|
+
setStartupPacketBlendWeights,
|
|
52
|
+
removeStartupPacketEntries,
|
|
53
|
+
resetStartupPacketTransientState,
|
|
54
|
+
claimCompositorLease,
|
|
55
|
+
parkOrDisposeCompositorLease,
|
|
56
|
+
_send,
|
|
57
|
+
prepareRegistrationForTransfer,
|
|
58
|
+
sameBootstrapRegistration,
|
|
59
|
+
evaluateRegistrationState,
|
|
60
|
+
toResolvedStateEntriesFromAck,
|
|
61
|
+
} from './compositor-startup.js';
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Factory
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
function _createCompositorWorker(
|
|
68
|
+
config?: WorkerConfig,
|
|
69
|
+
startupTelemetry?: CompositorWorkerStartupTelemetry,
|
|
70
|
+
): CompositorWorkerShape {
|
|
71
|
+
const capacity = config?.poolCapacity ?? 64;
|
|
72
|
+
const { worker, runtime, bootstrapSnapshot } = claimCompositorLease(capacity, startupTelemetry);
|
|
73
|
+
const snapshotByName = new Map(bootstrapSnapshot.map((registration) => [registration.name, registration] as const));
|
|
74
|
+
const activeRegistrations = new Map<string, BootstrapQuantizerRegistration>();
|
|
75
|
+
const stateListeners = new Set<(state: CompositorWorkerState) => void>();
|
|
76
|
+
const resolvedStateAckListeners = new Set<(ack: ResolvedStateAckPayload) => void>();
|
|
77
|
+
const metricsListeners = new Set<(fps: number, budgetUsed: number) => void>();
|
|
78
|
+
const confirmedSnapshotNames = new Set<string>();
|
|
79
|
+
const preparedRegistrationCache = new Map<
|
|
80
|
+
string,
|
|
81
|
+
{
|
|
82
|
+
readonly source: BootstrapQuantizerRegistration;
|
|
83
|
+
readonly transferRegistration: BootstrapQuantizerRegistration;
|
|
84
|
+
readonly buffer: ArrayBuffer;
|
|
85
|
+
}
|
|
86
|
+
>();
|
|
87
|
+
const startupPacket = createStartupPacketState(
|
|
88
|
+
bootstrapSnapshot.length > 0 ? 'warm-snapshot' : 'cold',
|
|
89
|
+
bootstrapSnapshot,
|
|
90
|
+
);
|
|
91
|
+
let steadyStatePendingUpdates: WorkerUpdate[] = [];
|
|
92
|
+
let flushScheduled = false;
|
|
93
|
+
let startupMode = true;
|
|
94
|
+
let startupDispatchCompletedNs: number | null = null;
|
|
95
|
+
let startupStatePending = false;
|
|
96
|
+
let resolvedStateDispatchCompletedNs: number | null = null;
|
|
97
|
+
let resolvedStateAckPending = false;
|
|
98
|
+
let lastMetrics: { readonly fps: number; readonly budgetUsed: number } | null = null;
|
|
99
|
+
let lastWorkerError: string | null = null;
|
|
100
|
+
|
|
101
|
+
const getPreparedRegistration = (registration: BootstrapQuantizerRegistration) => {
|
|
102
|
+
const cached = preparedRegistrationCache.get(registration.name);
|
|
103
|
+
/* v8 ignore next — current call sites always delete the cache entry in the same
|
|
104
|
+
synchronous turn that sets it (see `consumePreparedRegistrations`), so this
|
|
105
|
+
cache-hit arm is reserved for a future pre-flight path that warms the cache
|
|
106
|
+
before dispatch; unreachable under today's code paths. */
|
|
107
|
+
if (cached && cached.source === registration && cached.buffer.byteLength > 0) {
|
|
108
|
+
return cached;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { registration: transferRegistration, buffer } = prepareRegistrationForTransfer(registration);
|
|
112
|
+
const prepared = {
|
|
113
|
+
source: registration,
|
|
114
|
+
transferRegistration,
|
|
115
|
+
buffer,
|
|
116
|
+
};
|
|
117
|
+
preparedRegistrationCache.set(registration.name, prepared);
|
|
118
|
+
return prepared;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const consumePreparedRegistrations = (registrations: readonly BootstrapQuantizerRegistration[]) => {
|
|
122
|
+
const buffers: ArrayBuffer[] = [];
|
|
123
|
+
const transferRegistrations = registrations.map((registration) => {
|
|
124
|
+
const prepared = getPreparedRegistration(registration);
|
|
125
|
+
preparedRegistrationCache.delete(registration.name);
|
|
126
|
+
buffers.push(prepared.buffer);
|
|
127
|
+
return prepared.transferRegistration;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
registrations: transferRegistrations,
|
|
132
|
+
buffers,
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const flushPendingUpdates = (): void => {
|
|
137
|
+
flushScheduled = false;
|
|
138
|
+
if (steadyStatePendingUpdates.length === 0) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const updates = steadyStatePendingUpdates;
|
|
143
|
+
steadyStatePendingUpdates = [];
|
|
144
|
+
_send(worker, {
|
|
145
|
+
type: 'apply-updates',
|
|
146
|
+
updates,
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const queueUpdate = (update: WorkerUpdate): void => {
|
|
151
|
+
if (startupMode) {
|
|
152
|
+
pushStartupPacketUpdate(startupPacket, update);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
steadyStatePendingUpdates.push(update);
|
|
157
|
+
if (flushScheduled) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
flushScheduled = true;
|
|
162
|
+
queueMicrotask(flushPendingUpdates);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const markStartupBootstrapForRebuild = (): void => {
|
|
166
|
+
if (startupPacket.bootstrapMode === 'warm-snapshot') {
|
|
167
|
+
startupPacket.bootstrapMode = 'rebuild';
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const applyResolvedStatesToRuntime = (states: readonly ResolvedStateEntry[]): void => {
|
|
172
|
+
for (const entry of states) {
|
|
173
|
+
runtime.markDirty(entry.name);
|
|
174
|
+
runtime.applyState(entry.name, entry.state);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const ensureResolvedStateMode = (): void => {
|
|
179
|
+
if (!startupMode) {
|
|
180
|
+
flushPendingUpdates();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
startupMode = false;
|
|
185
|
+
startupStatePending = false;
|
|
186
|
+
startupDispatchCompletedNs = null;
|
|
187
|
+
flushScheduled = false;
|
|
188
|
+
steadyStatePendingUpdates = [];
|
|
189
|
+
const registrations = Array.from(activeRegistrations.values());
|
|
190
|
+
|
|
191
|
+
if (startupPacket.bootstrapMode !== 'cold') {
|
|
192
|
+
_send(worker, { type: 'init' });
|
|
193
|
+
}
|
|
194
|
+
if (registrations.length > 0) {
|
|
195
|
+
const { registrations: transferRegs, buffers } = consumePreparedRegistrations(registrations);
|
|
196
|
+
_send(
|
|
197
|
+
worker,
|
|
198
|
+
{
|
|
199
|
+
type: 'bootstrap-quantizers',
|
|
200
|
+
registrations: transferRegs,
|
|
201
|
+
},
|
|
202
|
+
buffers,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
resetStartupPacketTransientState(startupPacket);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const sendResolvedStateMessage = (
|
|
210
|
+
type: 'bootstrap-resolved-state' | 'apply-resolved-state',
|
|
211
|
+
states: readonly ResolvedStateEntry[],
|
|
212
|
+
): void => {
|
|
213
|
+
if (states.length === 0) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
ensureResolvedStateMode();
|
|
218
|
+
applyResolvedStatesToRuntime(states);
|
|
219
|
+
const expectAck = resolvedStateAckListeners.size > 0 || startupTelemetry !== undefined;
|
|
220
|
+
const dispatchStartNs = currentTimeNs();
|
|
221
|
+
_send(worker, makeResolvedStateEnvelope(type, states, expectAck));
|
|
222
|
+
resolvedStateDispatchCompletedNs = currentTimeNs();
|
|
223
|
+
resolvedStateAckPending = expectAck;
|
|
224
|
+
recordStartupDiagnosticStage(
|
|
225
|
+
startupTelemetry,
|
|
226
|
+
'request-compute:dispatch-send',
|
|
227
|
+
resolvedStateDispatchCompletedNs - dispatchStartNs,
|
|
228
|
+
);
|
|
229
|
+
recordStartupDiagnosticStage(startupTelemetry, 'request-compute:packet-finalize', 0);
|
|
230
|
+
recordStartupDiagnosticStage(startupTelemetry, 'request-compute:post-send-bookkeeping', 0);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const handleMessage = (e: MessageEvent<FromWorkerMessage>): void => {
|
|
234
|
+
const msg = e.data;
|
|
235
|
+
if (!msg || typeof msg.type !== 'string') return;
|
|
236
|
+
|
|
237
|
+
switch (msg.type) {
|
|
238
|
+
case 'ready':
|
|
239
|
+
break;
|
|
240
|
+
case 'state':
|
|
241
|
+
if (startupStatePending) {
|
|
242
|
+
const eventStartNs = currentTimeNs();
|
|
243
|
+
recordStartupDiagnosticStage(
|
|
244
|
+
startupTelemetry,
|
|
245
|
+
'state-delivery:message-receipt',
|
|
246
|
+
eventStartNs - startupDispatchCompletedNs!,
|
|
247
|
+
);
|
|
248
|
+
for (const [name, state] of Object.entries(msg.state.discrete ?? {})) {
|
|
249
|
+
runtime.applyState(name, state);
|
|
250
|
+
}
|
|
251
|
+
const callbackStartNs = currentTimeNs();
|
|
252
|
+
recordStartupDiagnosticStage(
|
|
253
|
+
startupTelemetry,
|
|
254
|
+
'state-delivery:callback-queue-turn',
|
|
255
|
+
callbackStartNs - eventStartNs,
|
|
256
|
+
);
|
|
257
|
+
for (const cb of stateListeners) cb({ ...msg.state, resolvedStateGenerations: msg.resolvedStateGenerations });
|
|
258
|
+
recordStartupDiagnosticStage(
|
|
259
|
+
startupTelemetry,
|
|
260
|
+
'state-delivery:host-callback-delivery',
|
|
261
|
+
currentTimeNs() - callbackStartNs,
|
|
262
|
+
);
|
|
263
|
+
startupStatePending = false;
|
|
264
|
+
startupDispatchCompletedNs = null;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
for (const [name, state] of Object.entries(msg.state.discrete ?? {})) {
|
|
268
|
+
runtime.applyState(name, state);
|
|
269
|
+
}
|
|
270
|
+
for (const cb of stateListeners) cb({ ...msg.state, resolvedStateGenerations: msg.resolvedStateGenerations });
|
|
271
|
+
break;
|
|
272
|
+
case 'resolved-state-ack':
|
|
273
|
+
if (!resolvedStateAckPending || resolvedStateDispatchCompletedNs === null) {
|
|
274
|
+
notifyResolvedStateSettled(startupTelemetry, toResolvedStateEntriesFromAck(msg));
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
const eventStartNs = currentTimeNs();
|
|
278
|
+
recordStartupDiagnosticStage(
|
|
279
|
+
startupTelemetry,
|
|
280
|
+
'state-delivery:message-receipt',
|
|
281
|
+
eventStartNs - resolvedStateDispatchCompletedNs,
|
|
282
|
+
);
|
|
283
|
+
const callbackStartNs = currentTimeNs();
|
|
284
|
+
recordStartupDiagnosticStage(
|
|
285
|
+
startupTelemetry,
|
|
286
|
+
'state-delivery:callback-queue-turn',
|
|
287
|
+
callbackStartNs - eventStartNs,
|
|
288
|
+
);
|
|
289
|
+
notifyResolvedStateSettled(startupTelemetry, toResolvedStateEntriesFromAck(msg));
|
|
290
|
+
if (resolvedStateAckListeners.size > 0) {
|
|
291
|
+
for (const cb of resolvedStateAckListeners) cb(msg);
|
|
292
|
+
recordStartupDiagnosticStage(
|
|
293
|
+
startupTelemetry,
|
|
294
|
+
'state-delivery:host-callback-delivery',
|
|
295
|
+
currentTimeNs() - callbackStartNs,
|
|
296
|
+
);
|
|
297
|
+
} else {
|
|
298
|
+
recordStartupDiagnosticStage(startupTelemetry, 'state-delivery:host-callback-delivery', 0);
|
|
299
|
+
}
|
|
300
|
+
resolvedStateAckPending = false;
|
|
301
|
+
resolvedStateDispatchCompletedNs = null;
|
|
302
|
+
break;
|
|
303
|
+
case 'metrics':
|
|
304
|
+
lastMetrics = { fps: msg.fps, budgetUsed: msg.budgetUsed };
|
|
305
|
+
for (const cb of metricsListeners) cb(msg.fps, msg.budgetUsed);
|
|
306
|
+
break;
|
|
307
|
+
case 'error':
|
|
308
|
+
lastWorkerError = msg.message;
|
|
309
|
+
Diagnostics.error({
|
|
310
|
+
source: 'czap/worker.compositor-worker',
|
|
311
|
+
code: 'worker-message-error',
|
|
312
|
+
message: 'Compositor worker reported an error.',
|
|
313
|
+
detail: msg.message,
|
|
314
|
+
});
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const handleError = (e: ErrorEvent): void => {
|
|
320
|
+
Diagnostics.error({
|
|
321
|
+
source: 'czap/worker.compositor-worker',
|
|
322
|
+
code: 'worker-unhandled-error',
|
|
323
|
+
message: 'Compositor worker raised an unhandled error.',
|
|
324
|
+
detail: e.message,
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const listenerBindStartNs = currentTimeNs();
|
|
329
|
+
worker.addEventListener('message', handleMessage);
|
|
330
|
+
worker.addEventListener('error', handleError);
|
|
331
|
+
startupTelemetry?.recordStage('listener-bind', currentTimeNs() - listenerBindStartNs);
|
|
332
|
+
|
|
333
|
+
if (startupPacket.bootstrapMode === 'cold') {
|
|
334
|
+
_send(worker, { type: 'init' });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
void lastMetrics;
|
|
338
|
+
void lastWorkerError;
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
get worker(): Worker {
|
|
342
|
+
return worker;
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
get runtime(): RuntimeCoordinator.Shape {
|
|
346
|
+
return runtime;
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
addQuantizer(name, boundary) {
|
|
350
|
+
const registration = {
|
|
351
|
+
name,
|
|
352
|
+
boundaryId: boundary.id,
|
|
353
|
+
states: boundary.states,
|
|
354
|
+
thresholds: boundary.thresholds,
|
|
355
|
+
} satisfies BootstrapQuantizerRegistration;
|
|
356
|
+
const previousRequested = activeRegistrations.get(name);
|
|
357
|
+
if (sameBootstrapRegistration(previousRequested, registration)) {
|
|
358
|
+
if (startupPacket.bootstrapMode === 'warm-snapshot' && snapshotByName.has(name)) {
|
|
359
|
+
confirmedSnapshotNames.add(name);
|
|
360
|
+
}
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
preparedRegistrationCache.delete(name);
|
|
365
|
+
activeRegistrations.set(name, registration);
|
|
366
|
+
const snapshotRegistration = snapshotByName.get(name);
|
|
367
|
+
const isSnapshotMatch = sameBootstrapRegistration(snapshotRegistration, registration);
|
|
368
|
+
|
|
369
|
+
if (runtime.hasQuantizer(name) && !isSnapshotMatch) {
|
|
370
|
+
runtime.removeQuantizer(name);
|
|
371
|
+
}
|
|
372
|
+
if (!runtime.hasQuantizer(name)) {
|
|
373
|
+
runtime.registerQuantizer(name, boundary.states);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (startupPacket.bootstrapMode === 'warm-snapshot' && isSnapshotMatch) {
|
|
377
|
+
confirmedSnapshotNames.add(name);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
confirmedSnapshotNames.delete(name);
|
|
382
|
+
if (snapshotRegistration || startupPacket.bootstrapMode === 'warm-snapshot') {
|
|
383
|
+
markStartupBootstrapForRebuild();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (startupMode) {
|
|
387
|
+
setStartupPacketRegistration(startupPacket, registration);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const { registrations: transferRegistrations, buffers } = consumePreparedRegistrations([registration]);
|
|
392
|
+
_send(worker, { type: 'add-quantizer', ...transferRegistrations[0]! }, buffers);
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
removeQuantizer(name) {
|
|
396
|
+
preparedRegistrationCache.delete(name);
|
|
397
|
+
activeRegistrations.delete(name);
|
|
398
|
+
confirmedSnapshotNames.delete(name);
|
|
399
|
+
runtime.removeQuantizer(name);
|
|
400
|
+
if (snapshotByName.has(name)) {
|
|
401
|
+
markStartupBootstrapForRebuild();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (startupMode) {
|
|
405
|
+
removeStartupPacketEntries(startupPacket, name);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
queueUpdate({ type: 'remove-quantizer', name });
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
evaluate(name, value) {
|
|
412
|
+
if (startupMode && snapshotByName.has(name) && !confirmedSnapshotNames.has(name)) {
|
|
413
|
+
markStartupBootstrapForRebuild();
|
|
414
|
+
}
|
|
415
|
+
if (startupMode) {
|
|
416
|
+
const activeRegistration = activeRegistrations.get(name);
|
|
417
|
+
if (activeRegistration) {
|
|
418
|
+
const nextState = evaluateRegistrationState(activeRegistration, value);
|
|
419
|
+
if (nextState !== activeRegistration.states[0]) {
|
|
420
|
+
confirmedSnapshotNames.delete(name);
|
|
421
|
+
}
|
|
422
|
+
setStartupPacketInitialState(startupPacket, activeRegistration, nextState);
|
|
423
|
+
runtime.markDirty(name);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
runtime.markDirty(name);
|
|
428
|
+
queueUpdate({ type: 'evaluate', name, value });
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
setBlendWeights(name, weights) {
|
|
432
|
+
if (startupMode && snapshotByName.has(name) && !confirmedSnapshotNames.has(name)) {
|
|
433
|
+
markStartupBootstrapForRebuild();
|
|
434
|
+
}
|
|
435
|
+
if (startupMode && setStartupPacketBlendWeights(startupPacket, name, weights)) {
|
|
436
|
+
confirmedSnapshotNames.delete(name);
|
|
437
|
+
runtime.markDirty(name);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
runtime.markDirty(name);
|
|
441
|
+
queueUpdate({ type: 'set-blend', name, weights });
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
bootstrapResolvedState(states) {
|
|
445
|
+
sendResolvedStateMessage('bootstrap-resolved-state', states);
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
applyResolvedState(states) {
|
|
449
|
+
sendResolvedStateMessage('apply-resolved-state', states);
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
requestCompute() {
|
|
453
|
+
const wasStartupMode = startupMode;
|
|
454
|
+
startupMode = false;
|
|
455
|
+
|
|
456
|
+
if (wasStartupMode) {
|
|
457
|
+
startupStatePending = true;
|
|
458
|
+
const runtimeSeed = getStartupPacketRuntimeSeed(startupPacket);
|
|
459
|
+
if (
|
|
460
|
+
startupPacket.bootstrapMode === 'warm-snapshot' &&
|
|
461
|
+
activeRegistrations.size === snapshotByName.size &&
|
|
462
|
+
confirmedSnapshotNames.size === snapshotByName.size &&
|
|
463
|
+
runtimeMatchesStartupSeed(runtime, runtimeSeed)
|
|
464
|
+
) {
|
|
465
|
+
const dispatchStartNs = currentTimeNs();
|
|
466
|
+
_send(worker, { type: 'warm-reset' });
|
|
467
|
+
_send(worker, { type: 'compute' });
|
|
468
|
+
startupDispatchCompletedNs = currentTimeNs();
|
|
469
|
+
recordStartupDiagnosticStage(
|
|
470
|
+
startupTelemetry,
|
|
471
|
+
'request-compute:dispatch-send',
|
|
472
|
+
startupDispatchCompletedNs - dispatchStartNs,
|
|
473
|
+
);
|
|
474
|
+
recordStartupDiagnosticStage(startupTelemetry, 'request-compute:packet-finalize', 0);
|
|
475
|
+
recordStartupDiagnosticStage(startupTelemetry, 'request-compute:post-send-bookkeeping', 0);
|
|
476
|
+
return;
|
|
477
|
+
} else {
|
|
478
|
+
const packetFinalizeStartNs = currentTimeNs();
|
|
479
|
+
const packet = buildStartupComputePacket(startupPacket);
|
|
480
|
+
if (startupPacket.bootstrapMode === 'rebuild') {
|
|
481
|
+
if (!runtimeMatchesStartupSeed(runtime, runtimeSeed)) {
|
|
482
|
+
runtime.reset(runtimeSeed);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const packetFinalizeEndNs = currentTimeNs();
|
|
486
|
+
recordStartupDiagnosticStage(
|
|
487
|
+
startupTelemetry,
|
|
488
|
+
'request-compute:packet-finalize',
|
|
489
|
+
packetFinalizeEndNs - packetFinalizeStartNs,
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
flushScheduled = false;
|
|
493
|
+
const { registrations: transferRegs, buffers } = consumePreparedRegistrations(packet.registrations);
|
|
494
|
+
const transferPacket = { ...packet, registrations: transferRegs };
|
|
495
|
+
const dispatchStartNs = currentTimeNs();
|
|
496
|
+
_send(
|
|
497
|
+
worker,
|
|
498
|
+
{
|
|
499
|
+
type: 'startup-compute',
|
|
500
|
+
packet: transferPacket,
|
|
501
|
+
},
|
|
502
|
+
buffers,
|
|
503
|
+
);
|
|
504
|
+
startupDispatchCompletedNs = currentTimeNs();
|
|
505
|
+
recordStartupDiagnosticStage(
|
|
506
|
+
startupTelemetry,
|
|
507
|
+
'request-compute:dispatch-send',
|
|
508
|
+
startupDispatchCompletedNs - dispatchStartNs,
|
|
509
|
+
);
|
|
510
|
+
recordStartupDiagnosticStage(startupTelemetry, 'request-compute:post-send-bookkeeping', 0);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
flushPendingUpdates();
|
|
516
|
+
_send(worker, { type: 'compute' });
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
onState(callback) {
|
|
520
|
+
stateListeners.add(callback);
|
|
521
|
+
return () => {
|
|
522
|
+
stateListeners.delete(callback);
|
|
523
|
+
};
|
|
524
|
+
},
|
|
525
|
+
|
|
526
|
+
onResolvedStateAck(callback) {
|
|
527
|
+
resolvedStateAckListeners.add(callback);
|
|
528
|
+
return () => {
|
|
529
|
+
resolvedStateAckListeners.delete(callback);
|
|
530
|
+
};
|
|
531
|
+
},
|
|
532
|
+
|
|
533
|
+
onMetrics(callback) {
|
|
534
|
+
metricsListeners.add(callback);
|
|
535
|
+
return () => {
|
|
536
|
+
metricsListeners.delete(callback);
|
|
537
|
+
};
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
dispose() {
|
|
541
|
+
resetStartupPacketTransientState(startupPacket);
|
|
542
|
+
steadyStatePendingUpdates = [];
|
|
543
|
+
flushScheduled = false;
|
|
544
|
+
stateListeners.clear();
|
|
545
|
+
resolvedStateAckListeners.clear();
|
|
546
|
+
metricsListeners.clear();
|
|
547
|
+
preparedRegistrationCache.clear();
|
|
548
|
+
lastMetrics = null;
|
|
549
|
+
lastWorkerError = null;
|
|
550
|
+
if (typeof worker.removeEventListener === 'function') {
|
|
551
|
+
worker.removeEventListener('message', handleMessage);
|
|
552
|
+
worker.removeEventListener('error', handleError);
|
|
553
|
+
}
|
|
554
|
+
parkOrDisposeCompositorLease({
|
|
555
|
+
worker,
|
|
556
|
+
runtime,
|
|
557
|
+
capacity,
|
|
558
|
+
bootstrapSnapshot: Array.from(activeRegistrations.values()),
|
|
559
|
+
});
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ---------------------------------------------------------------------------
|
|
565
|
+
// Export
|
|
566
|
+
// ---------------------------------------------------------------------------
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Factory namespace for the compositor worker.
|
|
570
|
+
*
|
|
571
|
+
* Call {@link CompositorWorker.create} on the main thread to spin up a
|
|
572
|
+
* worker that evaluates quantizer boundaries and emits
|
|
573
|
+
* {@link CompositorWorkerState} snapshots. The returned
|
|
574
|
+
* {@link CompositorWorkerShape} owns the underlying `Worker` -- call
|
|
575
|
+
* `dispose()` (or park via the lease pool) when finished.
|
|
576
|
+
*
|
|
577
|
+
* @example
|
|
578
|
+
* ```ts
|
|
579
|
+
* import { CompositorWorker } from '@czap/worker';
|
|
580
|
+
*
|
|
581
|
+
* const compositor = CompositorWorker.create({ poolCapacity: 64 });
|
|
582
|
+
* compositor.addQuantizer('brightness', {
|
|
583
|
+
* id: 'boundary:brightness',
|
|
584
|
+
* states: ['dim', 'bright'],
|
|
585
|
+
* thresholds: [0.5],
|
|
586
|
+
* });
|
|
587
|
+
* const unsub = compositor.onState((state) => {
|
|
588
|
+
* // state.discrete.brightness === 'bright' | 'dim'
|
|
589
|
+
* });
|
|
590
|
+
* compositor.evaluate('brightness', 0.7);
|
|
591
|
+
* compositor.requestCompute();
|
|
592
|
+
* // ...later:
|
|
593
|
+
* unsub();
|
|
594
|
+
* compositor.dispose();
|
|
595
|
+
* ```
|
|
596
|
+
*/
|
|
597
|
+
export const CompositorWorker = {
|
|
598
|
+
/**
|
|
599
|
+
* Spin up a new compositor worker. Returns immediately; the worker
|
|
600
|
+
* posts `ready` asynchronously. Optionally provide startup telemetry
|
|
601
|
+
* to capture per-stage timings.
|
|
602
|
+
*/
|
|
603
|
+
create: _createCompositorWorker,
|
|
604
|
+
} as const;
|
|
605
|
+
|
|
606
|
+
export declare namespace CompositorWorker {
|
|
607
|
+
/** Public host-side surface returned by {@link CompositorWorker.create}. */
|
|
608
|
+
export type Shape = CompositorWorkerShape;
|
|
609
|
+
/** Named startup stage reported to telemetry sinks. */
|
|
610
|
+
export type StartupStage = CompositorWorkerStartupStage;
|
|
611
|
+
/** Telemetry sink accepted by {@link CompositorWorker.create}. */
|
|
612
|
+
export type StartupTelemetry = CompositorWorkerStartupTelemetry;
|
|
613
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared inline source for `evaluateThresholds` injected into worker blob scripts.
|
|
3
|
+
*
|
|
4
|
+
* Worker blob scripts cannot use ES module imports at runtime, so the
|
|
5
|
+
* threshold-evaluation logic must be inlined as a string. This module is the
|
|
6
|
+
* single source of truth for that string so both compositor-worker.ts and
|
|
7
|
+
* render-worker.ts stay in sync automatically.
|
|
8
|
+
*
|
|
9
|
+
* Canonical TypeScript implementation: `packages/quantizer/src/evaluate.ts`
|
|
10
|
+
* (`evaluate` / `Evaluate.evaluate` in `@czap/quantizer`).
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Inline JavaScript source for the `evaluateThresholds` helper.
|
|
17
|
+
*
|
|
18
|
+
* This is a simplified (no-hysteresis) version of the canonical
|
|
19
|
+
* `Evaluate.evaluate` from `@czap/quantizer`, suitable for embedding in
|
|
20
|
+
* self-contained worker blob scripts.
|
|
21
|
+
*/
|
|
22
|
+
export const EVALUATE_THRESHOLDS_SOURCE = `\
|
|
23
|
+
/**
|
|
24
|
+
* Evaluate which discrete state a value falls into based on thresholds.
|
|
25
|
+
* Thresholds are sorted ascending; the value maps to the state whose
|
|
26
|
+
* threshold it first exceeds (or the first state if below all thresholds).
|
|
27
|
+
*
|
|
28
|
+
* Canonical TypeScript implementation: packages/quantizer/src/evaluate.ts
|
|
29
|
+
*
|
|
30
|
+
* @param {number[]} thresholds
|
|
31
|
+
* @param {string[]} states
|
|
32
|
+
* @param {number} value
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
function evaluateThresholds(thresholds, states, value) {
|
|
36
|
+
for (let i = thresholds.length - 1; i >= 0; i--) {
|
|
37
|
+
if (value >= thresholds[i]) {
|
|
38
|
+
return states[i] || states[0] || "";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return states[0] || "";
|
|
42
|
+
}`;
|