animader 0.0.0-stage → 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 +121 -0
- package/package.json +23 -4
- package/src/index.js +482 -0
- package/src/log.js +304 -0
- package/src/machine.js +712 -0
- package/src/piece.js +222 -0
- package/src/route.js +116 -0
- package/src/session.js +480 -0
- package/wasm/animader.wasm +0 -0
- package/README.md +0 -3
package/src/session.js
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
// animader/session - the composed frame contract's JS glue (plan 03,
|
|
2
|
+
// phase 4).
|
|
3
|
+
//
|
|
4
|
+
// `Session` holds one machine and one bridge over a shared
|
|
5
|
+
// `animader.wasm` instance, the host's sinks, an optional score shaped
|
|
6
|
+
// like pacer's public `setControl`/`eval` pair, and the registered
|
|
7
|
+
// consumers. `step(events, now)` executes the frame contract's order:
|
|
8
|
+
//
|
|
9
|
+
// derived = union c.derive(events) ; (input-write ...) records, in memory only
|
|
10
|
+
// writes_c = union c.eval(now) ; before the machine steps
|
|
11
|
+
// m = machine.step(merge(events, derived), now)
|
|
12
|
+
// writes_m = bridge.encode(m.outputs)
|
|
13
|
+
// apply registry 2 of writes_c, writes_m ; setControl, BEFORE the score
|
|
14
|
+
// writes_p = score.eval(now) ; its :expr tracks read the pins
|
|
15
|
+
// apply registry 0/1 of writes_c ; raw input first
|
|
16
|
+
// apply registry 0/1 of writes_m ; then the machine
|
|
17
|
+
// apply registry 0/1 of writes_p ; then the score: later wins
|
|
18
|
+
// c.emit(writes_c + writes_m + writes_p, now)
|
|
19
|
+
// host.handle(m.emits) ; the machine's emits, last
|
|
20
|
+
//
|
|
21
|
+
// One rule orders it: the more authored a source is, the later it lands.
|
|
22
|
+
// The score and the consumers are the host's, so the sinks are callbacks
|
|
23
|
+
// (`host.setControl`/`setUniform`/`setParam`/`handle`); a recording fake in
|
|
24
|
+
// the tests, a real pngine/audio/pacer host in production.
|
|
25
|
+
//
|
|
26
|
+
// The machine document and the bridge document are separate because the
|
|
27
|
+
// bridge boundary validates against `schema/bridge.sjon` alone: `machine`
|
|
28
|
+
// is the aggregate document, `bridge` its `(output ...)`/`(route ...)`
|
|
29
|
+
// subset in the machine's declaration order.
|
|
30
|
+
//
|
|
31
|
+
// A consumer is `{ derive(records), eval(now), emit(writes, now) }`, all
|
|
32
|
+
// optional; `derive` returns `(input-write ...)` records stamped with the
|
|
33
|
+
// causing record's `:at` (never appended to the log, never re-stamped) and
|
|
34
|
+
// `eval` returns its own 'PACW'. `emit` receives the merged frame's
|
|
35
|
+
// entries in order. Consumers are the JS glue's business because a sibling
|
|
36
|
+
// runtime is its own wasm.
|
|
37
|
+
//
|
|
38
|
+
// The host's `handle(emits)` step is the contract's last: the boundary
|
|
39
|
+
// carries each stepping call's emitted events out as the 'ANME' blob
|
|
40
|
+
// (`anm_machine_emits`, read after every `machine.step`, `setInput` and
|
|
41
|
+
// `fireTrigger`), and this glue concatenates the frame's calls into the
|
|
42
|
+
// one list the native single-step `Machine.step` would have returned,
|
|
43
|
+
// handing it to the host after every consumer's `emit`.
|
|
44
|
+
//
|
|
45
|
+
// The score's `eval` receives the frame's `now` in session microseconds;
|
|
46
|
+
// a pacer-backed score adapter converts to its own seconds (divide by
|
|
47
|
+
// 1e6), because pacer's clock is f64 seconds while this runtime's is i64
|
|
48
|
+
// microseconds.
|
|
49
|
+
|
|
50
|
+
import { Machine, decodeMeta } from "./machine.js";
|
|
51
|
+
import { decodeWrites, laneValue } from "./route.js";
|
|
52
|
+
|
|
53
|
+
const encoder = new TextEncoder();
|
|
54
|
+
const decoder = new TextDecoder();
|
|
55
|
+
|
|
56
|
+
const IMPORTS = { env: { sjon_host_invoke_plugin: () => 0 } };
|
|
57
|
+
|
|
58
|
+
// 'ANEV' (src/Abi.zig): the blob the log parses to.
|
|
59
|
+
const ANEV_MAGIC = 0x56454e41;
|
|
60
|
+
const ANEV_VERSION = 1;
|
|
61
|
+
const ANEV_HEADER_LEN = 20;
|
|
62
|
+
const ANEV_RECORD_LEN = 32;
|
|
63
|
+
const MAX_EVENTS = 1 << 20;
|
|
64
|
+
const NO_STR = 0xffff;
|
|
65
|
+
|
|
66
|
+
/** Kind ordinals, append-only on the wire (`EventLog.Kind`). */
|
|
67
|
+
const KIND_NAMES = [
|
|
68
|
+
"resize",
|
|
69
|
+
"down",
|
|
70
|
+
"move",
|
|
71
|
+
"up",
|
|
72
|
+
"wheel",
|
|
73
|
+
"key",
|
|
74
|
+
"custom",
|
|
75
|
+
"input-write",
|
|
76
|
+
"mark",
|
|
77
|
+
"adjust",
|
|
78
|
+
"reset",
|
|
79
|
+
"cancel",
|
|
80
|
+
"derived",
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const PACT_MAGIC = 0x54434150;
|
|
84
|
+
const BRIDGE_HANDLE = 1;
|
|
85
|
+
|
|
86
|
+
const CONTROL_REGISTRY = 2;
|
|
87
|
+
const UNIFORM_REGISTRY = 0;
|
|
88
|
+
const PARAM_REGISTRY = 1;
|
|
89
|
+
|
|
90
|
+
/** A boundary failure, a diagnosed document or a malformed blob. */
|
|
91
|
+
export class SessionError extends Error {
|
|
92
|
+
constructor(message) {
|
|
93
|
+
super(message);
|
|
94
|
+
this.name = "SessionError";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function toBytes(blob) {
|
|
99
|
+
if (blob instanceof Uint8Array) return blob;
|
|
100
|
+
if (blob instanceof ArrayBuffer) return new Uint8Array(blob);
|
|
101
|
+
if (ArrayBuffer.isView(blob)) return new Uint8Array(blob.buffer, blob.byteOffset, blob.byteLength);
|
|
102
|
+
throw new TypeError("animader: expected an ArrayBuffer or a typed array");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function micros(value, what) {
|
|
106
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
107
|
+
throw new SessionError(`animader: ${what} must be a safe, non-negative integer (microseconds), got ${value}`);
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The 'ANEV' decoder this glue needs for `derive`: the same record shape
|
|
114
|
+
* `Log.decode` answers. Kept local so the module has no import cycle with
|
|
115
|
+
* `index.js`; the session test pins the same blob through `Log.decode`.
|
|
116
|
+
*
|
|
117
|
+
* @param {Uint8Array} bytes
|
|
118
|
+
* @returns {{ records: Array<object>, strings: string[] }}
|
|
119
|
+
*/
|
|
120
|
+
function decodeEvents(bytes) {
|
|
121
|
+
if (bytes.byteLength < ANEV_HEADER_LEN) {
|
|
122
|
+
throw new SessionError(`animader: 'ANEV' blob is ${bytes.byteLength} bytes, under the header`);
|
|
123
|
+
}
|
|
124
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
125
|
+
if (view.getUint32(0, true) !== ANEV_MAGIC) throw new SessionError("animader: 'ANEV' magic is missing");
|
|
126
|
+
if (view.getUint32(4, true) !== ANEV_VERSION) {
|
|
127
|
+
throw new SessionError(`animader: 'ANEV' version ${view.getUint32(4, true)} is not ${ANEV_VERSION}`);
|
|
128
|
+
}
|
|
129
|
+
const count = view.getUint32(8, true);
|
|
130
|
+
const strtabLen = view.getUint32(12, true);
|
|
131
|
+
if (count > MAX_EVENTS) throw new SessionError(`animader: 'ANEV' count ${count} over the ceiling`);
|
|
132
|
+
if (ANEV_HEADER_LEN + count * ANEV_RECORD_LEN + strtabLen !== bytes.byteLength) {
|
|
133
|
+
throw new SessionError("animader: 'ANEV' length does not match its header");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const strings = [];
|
|
137
|
+
const strtabOff = ANEV_HEADER_LEN + count * ANEV_RECORD_LEN;
|
|
138
|
+
let pos = strtabOff;
|
|
139
|
+
while (pos < bytes.byteLength) {
|
|
140
|
+
const end = bytes.indexOf(0, pos);
|
|
141
|
+
if (end === -1) {
|
|
142
|
+
strings.push(decoder.decode(bytes.subarray(pos)));
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
strings.push(decoder.decode(bytes.subarray(pos, end)));
|
|
146
|
+
pos = end + 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const records = [];
|
|
150
|
+
for (let i = 0; i < count; i++) {
|
|
151
|
+
const o = ANEV_HEADER_LEN + i * ANEV_RECORD_LEN;
|
|
152
|
+
const kind = KIND_NAMES[view.getUint8(o + 8)];
|
|
153
|
+
if (kind === undefined) throw new SessionError(`animader: 'ANEV' record kind ${view.getUint8(o + 8)} is not a Kind`);
|
|
154
|
+
const at = view.getBigInt64(o, true);
|
|
155
|
+
if (at < 0n || at > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
156
|
+
throw new SessionError(`animader: 'ANEV' record :at ${at} is outside the writer's domain`);
|
|
157
|
+
}
|
|
158
|
+
const strRaw = view.getUint16(o + 18, true);
|
|
159
|
+
records.push({
|
|
160
|
+
at: Number(at),
|
|
161
|
+
kind,
|
|
162
|
+
ptr: view.getUint8(o + 9),
|
|
163
|
+
buttons: view.getUint8(o + 10),
|
|
164
|
+
mods: view.getUint8(o + 11),
|
|
165
|
+
x: view.getUint16(o + 12, true),
|
|
166
|
+
y: view.getUint16(o + 14, true),
|
|
167
|
+
p: view.getUint16(o + 16, true),
|
|
168
|
+
str: strRaw === NO_STR ? null : (strings[strRaw] ?? null),
|
|
169
|
+
v: [view.getInt32(o + 20, true), view.getInt32(o + 24, true), view.getInt32(o + 28, true)],
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return { records, strings };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** pacer's 'PACT' blob for the host join, `{name, ncomp, registry}`. */
|
|
176
|
+
export function encodeTargets(targets) {
|
|
177
|
+
const names = targets.map((t) => encoder.encode(t.name));
|
|
178
|
+
let len = 8;
|
|
179
|
+
for (const n of names) len += 4 + n.length;
|
|
180
|
+
const blob = new Uint8Array(len);
|
|
181
|
+
const dv = new DataView(blob.buffer);
|
|
182
|
+
dv.setUint32(0, PACT_MAGIC, true);
|
|
183
|
+
dv.setUint32(4, targets.length, true);
|
|
184
|
+
let pos = 8;
|
|
185
|
+
targets.forEach((t, i) => {
|
|
186
|
+
blob[pos] = t.ncomp;
|
|
187
|
+
blob[pos + 1] = t.registry;
|
|
188
|
+
dv.setUint16(pos + 2, names[i].length, true);
|
|
189
|
+
pos += 4;
|
|
190
|
+
blob.set(names[i], pos);
|
|
191
|
+
pos += names[i].length;
|
|
192
|
+
});
|
|
193
|
+
return blob;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* An 'ANEV' blob holding records [start, end) of `blob`, keeping the full
|
|
198
|
+
* string table so every record's index stays valid (the machine corpus
|
|
199
|
+
* test's slice, the exact-time seam for derived records).
|
|
200
|
+
*/
|
|
201
|
+
function sliceBlob(blob, start, end) {
|
|
202
|
+
const view = new DataView(blob.buffer, blob.byteOffset, blob.byteLength);
|
|
203
|
+
const count = view.getUint32(8, true);
|
|
204
|
+
const strtabLen = view.getUint32(12, true);
|
|
205
|
+
const strtabOff = ANEV_HEADER_LEN + count * ANEV_RECORD_LEN;
|
|
206
|
+
const out = new Uint8Array(ANEV_HEADER_LEN + (end - start) * ANEV_RECORD_LEN + strtabLen);
|
|
207
|
+
out.set(blob.subarray(0, ANEV_HEADER_LEN), 0);
|
|
208
|
+
new DataView(out.buffer).setUint32(8, end - start, true);
|
|
209
|
+
out.set(blob.subarray(ANEV_HEADER_LEN + start * ANEV_RECORD_LEN, ANEV_HEADER_LEN + end * ANEV_RECORD_LEN), ANEV_HEADER_LEN);
|
|
210
|
+
out.set(blob.subarray(strtabOff, strtabOff + strtabLen), ANEV_HEADER_LEN + (end - start) * ANEV_RECORD_LEN);
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Apply every registry-2 entry through `host.setControl`, in wire order.
|
|
216
|
+
* The third argument is the emitter (`machine`, `score` or `consumer`),
|
|
217
|
+
* the frame contract's "more authored lands later" tag.
|
|
218
|
+
*/
|
|
219
|
+
function applyControls(entries, host, source) {
|
|
220
|
+
if (typeof host.setControl !== "function") return;
|
|
221
|
+
for (const entry of entries) {
|
|
222
|
+
if (entry.registry !== CONTROL_REGISTRY) continue;
|
|
223
|
+
host.setControl(entry.targetId, laneValue(entry), source);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Apply registries 0 and 1 through `host.setUniform`/`host.setParam`. */
|
|
228
|
+
function applyValues(entries, host, source) {
|
|
229
|
+
for (const entry of entries) {
|
|
230
|
+
if (entry.registry === UNIFORM_REGISTRY) {
|
|
231
|
+
if (typeof host.setUniform === "function") host.setUniform(entry.targetId, laneValue(entry), source);
|
|
232
|
+
} else if (entry.registry === PARAM_REGISTRY) {
|
|
233
|
+
if (typeof host.setParam === "function") host.setParam(entry.targetId, laneValue(entry), source);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The composed session over one artifact instance. */
|
|
239
|
+
export class Session {
|
|
240
|
+
#e;
|
|
241
|
+
#machine;
|
|
242
|
+
#bridgeHandle = 0;
|
|
243
|
+
#host;
|
|
244
|
+
#score;
|
|
245
|
+
#consumers = [];
|
|
246
|
+
#inputKinds = new Map();
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Instantiate the artifact synchronously (node, tests).
|
|
250
|
+
* @param {BufferSource} bytes
|
|
251
|
+
* @param {object} opts see the constructor
|
|
252
|
+
* @returns {Session}
|
|
253
|
+
*/
|
|
254
|
+
static loadSync(bytes, opts) {
|
|
255
|
+
return new Session(new WebAssembly.Instance(new WebAssembly.Module(bytes), IMPORTS), opts);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Instantiate the artifact from bytes or a compiled module (browser).
|
|
260
|
+
* @param {BufferSource | WebAssembly.Module} source
|
|
261
|
+
* @param {object} opts see the constructor
|
|
262
|
+
* @returns {Promise<Session>}
|
|
263
|
+
*/
|
|
264
|
+
static async load(source, opts) {
|
|
265
|
+
const instance =
|
|
266
|
+
source instanceof WebAssembly.Module
|
|
267
|
+
? await WebAssembly.instantiate(source, IMPORTS)
|
|
268
|
+
: (await WebAssembly.instantiate(source, IMPORTS)).instance;
|
|
269
|
+
return new Session(instance, opts);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @param {WebAssembly.Instance} instance one animader.wasm instance,
|
|
274
|
+
* shared by the machine and the bridge (one of each per instance, the
|
|
275
|
+
* module-global caches).
|
|
276
|
+
* @param {object} opts
|
|
277
|
+
* @param {string} opts.machine the machine document (the machine + bridge
|
|
278
|
+
* aggregate)
|
|
279
|
+
* @param {string} opts.bridge the `(output ...)`/`(route ...)` subset,
|
|
280
|
+
* in the machine's declaration order (the bridge boundary validates
|
|
281
|
+
* against the bridge manifest alone)
|
|
282
|
+
* @param {Array<{name: string, ncomp: number, registry: number}>} opts.targets
|
|
283
|
+
* the host join in target_id order
|
|
284
|
+
* @param {number} [opts.registryCount] the host's registry count
|
|
285
|
+
* @param {object} [opts.host] `{ setControl, setUniform, setParam }`
|
|
286
|
+
* @param {object | null} [opts.score] pacer's pair: `setControl(id, value)`
|
|
287
|
+
* and `eval(now)` returning 'PACW'
|
|
288
|
+
* @param {Array<object>} [opts.consumers]
|
|
289
|
+
*/
|
|
290
|
+
constructor(instance, opts = {}) {
|
|
291
|
+
const { machine, bridge, targets, registryCount = 0, host = {}, score = null, consumers = [] } = opts;
|
|
292
|
+
if (typeof machine !== "string" || typeof bridge !== "string") {
|
|
293
|
+
throw new SessionError("animader: session needs the machine and bridge documents");
|
|
294
|
+
}
|
|
295
|
+
if (!Array.isArray(targets) || targets.length === 0) {
|
|
296
|
+
throw new SessionError("animader: session needs the host's target join");
|
|
297
|
+
}
|
|
298
|
+
this.#e = instance.exports;
|
|
299
|
+
this.#host = host;
|
|
300
|
+
this.#score = score;
|
|
301
|
+
this.#machine = new Machine(instance, machine);
|
|
302
|
+
this.#inputKinds = new Map(decodeMeta(this.#machine.meta()).inputs.map((i) => [i.name, i.kind]));
|
|
303
|
+
this.#loadBridge(bridge, targets, registryCount);
|
|
304
|
+
for (const consumer of consumers) this.registerConsumer(consumer);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Register one consumer, in call order. @param {object} consumer @returns {Session} */
|
|
308
|
+
registerConsumer(consumer) {
|
|
309
|
+
if (consumer === null || typeof consumer !== "object") {
|
|
310
|
+
throw new SessionError("animader: a consumer must be an object");
|
|
311
|
+
}
|
|
312
|
+
this.#consumers.push(consumer);
|
|
313
|
+
return this;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** The loaded machine, for a host that needs its meta or snapshot. */
|
|
317
|
+
get machine() {
|
|
318
|
+
return this.#machine;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* One frame: the frame's drained records as an 'ANEV' blob (or null for
|
|
323
|
+
* an empty batch) and the frame's `now` in session microseconds.
|
|
324
|
+
* @param {Uint8Array | ArrayBuffer | null} events
|
|
325
|
+
* @param {number} now
|
|
326
|
+
* @returns {Session}
|
|
327
|
+
*/
|
|
328
|
+
step(events, now) {
|
|
329
|
+
const at = micros(now, "now");
|
|
330
|
+
const bytes = events === null || events === undefined ? null : toBytes(events);
|
|
331
|
+
const records = bytes === null ? [] : decodeEvents(bytes).records;
|
|
332
|
+
|
|
333
|
+
// 1. The consumers' derived records, in registration order, then the
|
|
334
|
+
// stable merge by :at (consumer order on ties).
|
|
335
|
+
const derived = [];
|
|
336
|
+
for (const consumer of this.#consumers) {
|
|
337
|
+
if (typeof consumer.derive !== "function") continue;
|
|
338
|
+
for (const record of consumer.derive(records)) {
|
|
339
|
+
if (record.kind !== "input-write") {
|
|
340
|
+
throw new SessionError(
|
|
341
|
+
`animader: a consumer's derived record must be an (input-write ...) record, got ${record.kind}`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
if (!Number.isSafeInteger(record.at) || record.at < 0 || record.at > at) {
|
|
345
|
+
throw new SessionError(`animader: a derived record's :at ${record.at} is outside 0..now`);
|
|
346
|
+
}
|
|
347
|
+
derived.push(record);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const ordered = derived
|
|
351
|
+
.map((record, i) => [record, i])
|
|
352
|
+
.sort((a, b) => a[0].at - b[0].at || a[1] - b[1])
|
|
353
|
+
.map(([record]) => record);
|
|
354
|
+
|
|
355
|
+
// 2. The consumers' own writes, before the machine steps.
|
|
356
|
+
const consumerWrites = this.#consumers.map((consumer) => {
|
|
357
|
+
if (typeof consumer.eval !== "function") return [];
|
|
358
|
+
const blob = consumer.eval(at);
|
|
359
|
+
return blob === null || blob === undefined ? [] : decodeWrites(toBytes(blob));
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// 3. The machine: the log's records and the derived ones, merged by :at
|
|
363
|
+
// (the log first on ties), applied at their exact times. The frame's
|
|
364
|
+
// emits are the concatenation of every stepping call the exact-time
|
|
365
|
+
// seam makes, in order.
|
|
366
|
+
const machineEmits = this.#stepMachine(bytes, records, ordered, at);
|
|
367
|
+
|
|
368
|
+
// 4. The machine's values as pacer's 'PACW'.
|
|
369
|
+
const machineWrites = this.#encodeMachine();
|
|
370
|
+
|
|
371
|
+
// 5. Registry 2: consumers, then the machine, before the score. The
|
|
372
|
+
// score owns the pins (pacer's `setControl`); a scoreless host may
|
|
373
|
+
// still take them through its own `setControl`.
|
|
374
|
+
for (const writes of consumerWrites) this.#applyControls(writes, "consumer");
|
|
375
|
+
this.#applyControls(machineWrites, "machine");
|
|
376
|
+
|
|
377
|
+
// 6. The score, whose :expr tracks read the pins just landed.
|
|
378
|
+
let scoreWrites = [];
|
|
379
|
+
if (this.#score !== null && typeof this.#score.eval === "function") {
|
|
380
|
+
const blob = this.#score.eval(at);
|
|
381
|
+
if (blob !== null && blob !== undefined) scoreWrites = decodeWrites(toBytes(blob));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 7. Registries 0/1: consumers, then the machine, then the score.
|
|
385
|
+
for (const writes of consumerWrites) applyValues(writes, this.#host, "consumer");
|
|
386
|
+
applyValues(machineWrites, this.#host, "machine");
|
|
387
|
+
applyValues(scoreWrites, this.#host, "score");
|
|
388
|
+
|
|
389
|
+
// 8. Each consumer reads the merged frame.
|
|
390
|
+
const merged = [...consumerWrites.flat(), ...machineWrites, ...scoreWrites];
|
|
391
|
+
for (const consumer of this.#consumers) {
|
|
392
|
+
if (typeof consumer.emit === "function") consumer.emit(merged.slice(), at);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// 9. The contract's last step: the machine's emitted events go to the
|
|
396
|
+
// host, never to a log.
|
|
397
|
+
if (typeof this.#host.handle === "function") this.#host.handle(machineEmits);
|
|
398
|
+
return this;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Registry-2 entries: the score's pins, or the host's control sink. */
|
|
402
|
+
#applyControls(entries, source) {
|
|
403
|
+
const score = this.#score;
|
|
404
|
+
const sink = score !== null && typeof score.setControl === "function" ? score : this.#host;
|
|
405
|
+
applyControls(entries, sink, source);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
#loadBridge(text, targets, registryCount) { const e = this.#e;
|
|
409
|
+
const pact = encodeTargets(targets);
|
|
410
|
+
const doc = encoder.encode(text);
|
|
411
|
+
if (pact.length + doc.length > e.anm_log_inCap()) {
|
|
412
|
+
throw new SessionError("animader: the bridge inputs do not fit the module's input zone");
|
|
413
|
+
}
|
|
414
|
+
new Uint8Array(e.memory.buffer, e.anm_log_inPtr(), pact.length).set(pact);
|
|
415
|
+
new Uint8Array(e.memory.buffer, e.anm_log_inPtr() + pact.length, doc.length).set(doc);
|
|
416
|
+
const status = e.anm_bridge_load(pact.length, doc.length, registryCount);
|
|
417
|
+
if (status !== BRIDGE_HANDLE) {
|
|
418
|
+
throw new SessionError(
|
|
419
|
+
`animader: the bridge join failed (status ${status}); check the documents and the target join`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
this.#bridgeHandle = status;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
#encodeMachine() {
|
|
426
|
+
const e = this.#e;
|
|
427
|
+
const outputs = this.#machine.outputs();
|
|
428
|
+
const len = outputs.length * 16;
|
|
429
|
+
if (len > e.anm_log_inCap()) throw new SessionError("animader: the machine's values do not fit the input zone");
|
|
430
|
+
const view = new DataView(e.memory.buffer, e.anm_log_inPtr(), len);
|
|
431
|
+
for (const output of outputs) {
|
|
432
|
+
const off = output.index * 16;
|
|
433
|
+
for (let lane = 0; lane < 4; lane++) view.setFloat32(off + lane * 4, output.value[lane], true);
|
|
434
|
+
}
|
|
435
|
+
const status = e.anm_bridge_encode(this.#bridgeHandle, len);
|
|
436
|
+
if (status < 0) {
|
|
437
|
+
throw new SessionError(
|
|
438
|
+
`animader: the bridge encode failed (status ${status}); the bridge document must list the machine's outputs in order`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
return decodeWrites(new Uint8Array(e.memory.buffer, e.anm_log_outPtr(), status).slice());
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* The machine's exact-time walk over the frame's records and derived
|
|
446
|
+
* inputs, returning every stepping call's emits concatenated in order:
|
|
447
|
+
* the one list a native single-step `Machine.step` would have returned.
|
|
448
|
+
*/
|
|
449
|
+
#stepMachine(bytes, records, derived, at) {
|
|
450
|
+
const emits = [];
|
|
451
|
+
if (derived.length === 0) {
|
|
452
|
+
this.#machine.step(bytes, at);
|
|
453
|
+
emits.push(...this.#machine.emits());
|
|
454
|
+
return emits;
|
|
455
|
+
}
|
|
456
|
+
let cursor = 0;
|
|
457
|
+
for (const record of derived) {
|
|
458
|
+
let end = cursor;
|
|
459
|
+
while (end < records.length && records[end].at <= record.at) end += 1;
|
|
460
|
+
this.#machine.step(end > cursor ? sliceBlob(bytes, cursor, end) : null, record.at);
|
|
461
|
+
emits.push(...this.#machine.emits());
|
|
462
|
+
this.#applyInput(record);
|
|
463
|
+
emits.push(...this.#machine.emits());
|
|
464
|
+
cursor = end;
|
|
465
|
+
}
|
|
466
|
+
this.#machine.step(cursor < records.length ? sliceBlob(bytes, cursor, records.length) : null, at);
|
|
467
|
+
emits.push(...this.#machine.emits());
|
|
468
|
+
return emits;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
#applyInput(record) {
|
|
472
|
+
const kind = this.#inputKinds.get(record.name);
|
|
473
|
+
if (kind === "trigger") {
|
|
474
|
+
this.#machine.fireTrigger(record.name);
|
|
475
|
+
} else {
|
|
476
|
+
const lane0 = Array.isArray(record.v) || (record.v && typeof record.v.length === "number") ? record.v[0] ?? 0 : 0;
|
|
477
|
+
this.#machine.setInput(record.name, lane0 | 0);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
Binary file
|
package/README.md
DELETED