tmmcore 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 +119 -0
- package/package.json +53 -0
- package/src/build.ps1 +121 -0
- package/src/index.js +35 -0
- package/src/tmm.js +602 -0
- package/src/tmmWasm.js +398 -0
- package/src/tmm_kernel.c +589 -0
- package/src/tmm_kernel.wasm +0 -0
package/src/tmmWasm.js
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tmmWasm.js — loader and ergonomic wrappers for the WebAssembly TMM kernel.
|
|
3
|
+
*
|
|
4
|
+
* The kernel (`tmm_kernel.c`, built to `tmm_kernel.wasm`) is a line-by-line port
|
|
5
|
+
* of the JavaScript TMM in `tmm.js`. This module instantiates it — in a browser
|
|
6
|
+
* main thread, a Web Worker, or Node — and exposes wrappers whose signatures
|
|
7
|
+
* mirror the JS functions.
|
|
8
|
+
*
|
|
9
|
+
* Acceleration is opt-in and falls back to JavaScript: if the `.wasm` is
|
|
10
|
+
* unavailable, instantiation fails, or the feature flag is off, the wrappers
|
|
11
|
+
* return `null` and callers use the JS path. Results are identical either way
|
|
12
|
+
* to float64 round-off.
|
|
13
|
+
*
|
|
14
|
+
* Instances are not shared across threads — there is no shared memory, so each
|
|
15
|
+
* context instantiates its own from the same bytes. Use `instantiateTmmWasm()`
|
|
16
|
+
* where you already hold the bytes (a worker receives them in its init message)
|
|
17
|
+
* and `initTmmWasmFromUrl()` where the artifact is fetchable.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
let _instance = null; // TmmWasmInstance | null
|
|
21
|
+
let _enabled = false; // feature flag (default OFF)
|
|
22
|
+
let _initPromise = null; // de-dupe concurrent init
|
|
23
|
+
|
|
24
|
+
// Permissive imports: a STANDALONE_WASM build of pure-math C usually needs no
|
|
25
|
+
// imports, but ALLOW_MEMORY_GROWTH may emit `emscripten_notify_memory_growth`,
|
|
26
|
+
// and some toolchains emit WASI stubs. Cover them so instantiation never throws.
|
|
27
|
+
function wasmImports() {
|
|
28
|
+
return {
|
|
29
|
+
env: { emscripten_notify_memory_growth: () => {} },
|
|
30
|
+
wasi_snapshot_preview1: new Proxy({}, { get: () => () => 0 }),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class TmmWasmInstance {
|
|
35
|
+
constructor(instance) {
|
|
36
|
+
const ex = instance.exports;
|
|
37
|
+
this.exports = ex;
|
|
38
|
+
// STANDALONE_WASM reactor modules expose an initializer that must run
|
|
39
|
+
// before malloc (sets up the allocator + any static ctors). Call it once.
|
|
40
|
+
if (typeof ex._initialize === 'function') ex._initialize();
|
|
41
|
+
else if (typeof ex.__wasm_call_ctors === 'function') ex.__wasm_call_ctors();
|
|
42
|
+
this.memory = ex.memory;
|
|
43
|
+
this.malloc = ex.malloc || ex._malloc;
|
|
44
|
+
this.free = ex.free || ex._free;
|
|
45
|
+
this._tmm_one = ex.tmm_one || ex._tmm_one;
|
|
46
|
+
this._tmm_spectrum = ex.tmm_spectrum || ex._tmm_spectrum;
|
|
47
|
+
this._tmm_jacobian = ex.tmm_jacobian || ex._tmm_jacobian;
|
|
48
|
+
this._tmm_needle_scan = ex.tmm_needle_scan || ex._tmm_needle_scan;
|
|
49
|
+
// Optional (added later for SQP/Newton accel): a .wasm built before the
|
|
50
|
+
// Hessian kernel existed simply lacks it → callers fall back to JS.
|
|
51
|
+
this._tmm_hessian = ex.tmm_hessian || ex._tmm_hessian || null;
|
|
52
|
+
const missingExports = !this.malloc || !this.free || !this._tmm_one ||
|
|
53
|
+
!this._tmm_spectrum || !this._tmm_jacobian || !this._tmm_needle_scan;
|
|
54
|
+
if (missingExports) {
|
|
55
|
+
throw new Error('tmmWasm: required exports missing from module');
|
|
56
|
+
}
|
|
57
|
+
this._scratchPtr = 0; // persistent per-call scratch arena (lazy)
|
|
58
|
+
this._scratchN = 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_alloc(nDoubles) {
|
|
62
|
+
const ptr = this.malloc(nDoubles * 8);
|
|
63
|
+
if (!ptr) throw new Error('tmmWasm: malloc failed');
|
|
64
|
+
return ptr;
|
|
65
|
+
}
|
|
66
|
+
// Fresh view — memory.buffer is detached after any growth, so re-create
|
|
67
|
+
// views AFTER all mallocs for a call are done.
|
|
68
|
+
_view(ptr, nDoubles) {
|
|
69
|
+
return new Float64Array(this.memory.buffer, ptr, nDoubles);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Persistent scratch arena for the per-call hot paths (tmmOne/tmmJacobian).
|
|
73
|
+
// These are invoked thousands of times per optimization run; malloc/free +
|
|
74
|
+
// typed-array churn per call would dominate the JS↔WASM boundary cost and
|
|
75
|
+
// can make per-call WASM SLOWER than JS. Reusing one buffer (grown on demand,
|
|
76
|
+
// never freed between calls) makes each call just write-args / read-result.
|
|
77
|
+
_scratch(nDoubles) {
|
|
78
|
+
if (this._scratchN < nDoubles) {
|
|
79
|
+
if (this._scratchPtr) this.free(this._scratchPtr);
|
|
80
|
+
this._scratchPtr = this._alloc(nDoubles);
|
|
81
|
+
this._scratchN = nDoubles;
|
|
82
|
+
}
|
|
83
|
+
return this._scratchPtr;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Single (λ, θ, pol) — mirrors tmm() in thinFilmMath.js.
|
|
88
|
+
* @returns {{R:number,T:number,A:number}}
|
|
89
|
+
*/
|
|
90
|
+
tmmOne(lambda_nm, theta_deg, polCode /* 0=s,1=p */, n0, ns, layers) {
|
|
91
|
+
const N = layers.length;
|
|
92
|
+
const need = 3 * N + 3; // layers [0..3N) + out [3N..3N+3)
|
|
93
|
+
const ptr = this._scratch(need); // may grow→detach; view created AFTER
|
|
94
|
+
const buf = this._view(ptr, need);
|
|
95
|
+
for (let i = 0; i < N; i++) {
|
|
96
|
+
buf[3 * i + 0] = layers[i].n[0];
|
|
97
|
+
buf[3 * i + 1] = layers[i].n[1];
|
|
98
|
+
buf[3 * i + 2] = layers[i].d;
|
|
99
|
+
}
|
|
100
|
+
const outPtr = ptr + 3 * N * 8;
|
|
101
|
+
this._tmm_one(lambda_nm, theta_deg, polCode | 0,
|
|
102
|
+
n0[0], n0[1], ns[0], ns[1], ptr, N, outPtr);
|
|
103
|
+
return { R: buf[3 * N], T: buf[3 * N + 1], A: buf[3 * N + 2] };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Batched spectrum over a λ grid for BOTH polarizations — the boundary-
|
|
108
|
+
* amortizing path behind evaluateSpectrum().
|
|
109
|
+
* @param {number[]} lambdas
|
|
110
|
+
* @param {[number,number][]} n0List incident ñ per λ
|
|
111
|
+
* @param {[number,number][]} nsList substrate ñ per λ
|
|
112
|
+
* @param {[number,number][][]} layerNK [layer][λ] = ñ
|
|
113
|
+
* @param {number[]} thick layer thicknesses (nm), length N
|
|
114
|
+
* @param {number} theta_deg
|
|
115
|
+
* @returns {{Rs,Ts,As,Rp,Tp,Ap}} each a Float64Array(nLam)
|
|
116
|
+
*/
|
|
117
|
+
tmmSpectrum(lambdas, n0List, nsList, layerNK, thick, theta_deg) {
|
|
118
|
+
const nLam = lambdas.length;
|
|
119
|
+
const N = thick.length;
|
|
120
|
+
|
|
121
|
+
const lamPtr = this._alloc(nLam);
|
|
122
|
+
const n0Ptr = this._alloc(2 * nLam);
|
|
123
|
+
const nsPtr = this._alloc(2 * nLam);
|
|
124
|
+
const mPtr = this._alloc(Math.max(1, 2 * N * nLam));
|
|
125
|
+
const thPtr = this._alloc(Math.max(1, N));
|
|
126
|
+
const rsPtr = this._alloc(nLam), tsPtr = this._alloc(nLam), asPtr = this._alloc(nLam);
|
|
127
|
+
const rpPtr = this._alloc(nLam), tpPtr = this._alloc(nLam), apPtr = this._alloc(nLam);
|
|
128
|
+
|
|
129
|
+
// Views created after all mallocs (buffer may have grown/detached).
|
|
130
|
+
const lam = this._view(lamPtr, nLam);
|
|
131
|
+
const n0v = this._view(n0Ptr, 2 * nLam);
|
|
132
|
+
const nsv = this._view(nsPtr, 2 * nLam);
|
|
133
|
+
const mv = this._view(mPtr, Math.max(1, 2 * N * nLam));
|
|
134
|
+
const thv = this._view(thPtr, Math.max(1, N));
|
|
135
|
+
for (let i = 0; i < nLam; i++) {
|
|
136
|
+
lam[i] = lambdas[i];
|
|
137
|
+
n0v[2 * i] = n0List[i][0]; n0v[2 * i + 1] = n0List[i][1];
|
|
138
|
+
nsv[2 * i] = nsList[i][0]; nsv[2 * i + 1] = nsList[i][1];
|
|
139
|
+
}
|
|
140
|
+
for (let k = 0; k < N; k++) {
|
|
141
|
+
thv[k] = thick[k];
|
|
142
|
+
const row = layerNK[k];
|
|
143
|
+
const base = k * nLam * 2;
|
|
144
|
+
for (let i = 0; i < nLam; i++) {
|
|
145
|
+
mv[base + 2 * i] = row[i][0];
|
|
146
|
+
mv[base + 2 * i + 1] = row[i][1];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this._tmm_spectrum(lamPtr, nLam, n0Ptr, nsPtr, mPtr, thPtr, N, theta_deg,
|
|
151
|
+
rsPtr, tsPtr, asPtr, rpPtr, tpPtr, apPtr);
|
|
152
|
+
|
|
153
|
+
// Copy outputs out of wasm memory before freeing.
|
|
154
|
+
const cp = (p) => Float64Array.from(this._view(p, nLam));
|
|
155
|
+
const res = { Rs: cp(rsPtr), Ts: cp(tsPtr), As: cp(asPtr),
|
|
156
|
+
Rp: cp(rpPtr), Tp: cp(tpPtr), Ap: cp(apPtr) };
|
|
157
|
+
for (const p of [lamPtr, n0Ptr, nsPtr, mPtr, thPtr,
|
|
158
|
+
rsPtr, tsPtr, asPtr, rpPtr, tpPtr, apPtr]) this.free(p);
|
|
159
|
+
return res;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Analytic thickness Jacobian for one (λ, θ, pol) — mirrors
|
|
164
|
+
* tmmThicknessJacobian(). layers used AS-IS (index parity).
|
|
165
|
+
* @returns {{R,T,A, dRdd:Float64Array, dTdd, dAdd, N}}
|
|
166
|
+
*/
|
|
167
|
+
tmmJacobian(lambda_nm, theta_deg, polCode, n0, ns, layers) {
|
|
168
|
+
const N = layers.length;
|
|
169
|
+
const M = Math.max(1, N);
|
|
170
|
+
// arena layout: layers[3N] | dRdd[M] | dTdd[M] | dAdd[M] | base[3]
|
|
171
|
+
const oLay = 0, oDR = 3 * N, oDT = oDR + M, oDA = oDT + M, oBase = oDA + M;
|
|
172
|
+
const need = oBase + 3;
|
|
173
|
+
const ptr = this._scratch(need);
|
|
174
|
+
const buf = this._view(ptr, need);
|
|
175
|
+
for (let i = 0; i < N; i++) {
|
|
176
|
+
buf[3 * i + 0] = layers[i].n[0];
|
|
177
|
+
buf[3 * i + 1] = layers[i].n[1];
|
|
178
|
+
buf[3 * i + 2] = layers[i].d;
|
|
179
|
+
}
|
|
180
|
+
const P = (off) => ptr + off * 8;
|
|
181
|
+
this._tmm_jacobian(lambda_nm, theta_deg, polCode | 0,
|
|
182
|
+
n0[0], n0[1], ns[0], ns[1], P(oLay), N, P(oDR), P(oDT), P(oDA), P(oBase));
|
|
183
|
+
// Re-create the view AFTER the kernel call (like tmmSpectrum): under
|
|
184
|
+
// ALLOW_MEMORY_GROWTH the kernel may grow wasm memory, which detaches the
|
|
185
|
+
// ArrayBuffer `buf` was created over — reading the stale `buf` then yields
|
|
186
|
+
// garbage / throws. `out` is a fresh view over the current buffer.
|
|
187
|
+
const out = this._view(ptr, need);
|
|
188
|
+
return {
|
|
189
|
+
R: out[oBase], T: out[oBase + 1], A: out[oBase + 2], N,
|
|
190
|
+
dRdd: out.slice(oDR, oDR + N),
|
|
191
|
+
dTdd: out.slice(oDT, oDT + N),
|
|
192
|
+
dAdd: out.slice(oDA, oDA + N),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** True if the loaded module carries the Hessian kernel (newer build). */
|
|
197
|
+
hasHessian() { return !!this._tmm_hessian; }
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Analytic thickness Hessian for one (λ, θ, pol) — mirrors
|
|
201
|
+
* tmmThicknessHessian(). Returns first AND second derivatives; the N×N
|
|
202
|
+
* second-derivative blocks are reshaped into nested arrays (one Float64Array
|
|
203
|
+
* row per layer, FULL symmetric) so the shape matches the JS oracle exactly.
|
|
204
|
+
* @returns {{R,T,A, dRdd, dTdd, dAdd, d2Rdd, d2Tdd, d2Add, N}}
|
|
205
|
+
*/
|
|
206
|
+
tmmHessian(lambda_nm, theta_deg, polCode, n0, ns, layers) {
|
|
207
|
+
const N = layers.length;
|
|
208
|
+
const M = Math.max(1, N);
|
|
209
|
+
const NN = Math.max(1, N * N);
|
|
210
|
+
// arena: layers[3N] | dRdd[M] | dTdd[M] | dAdd[M] | d2R[NN] | d2T[NN] | d2A[NN] | base[3]
|
|
211
|
+
const oLay = 0, oDR = 3 * N, oDT = oDR + M, oDA = oDT + M,
|
|
212
|
+
oR2 = oDA + M, oT2 = oR2 + NN, oA2 = oT2 + NN, oBase = oA2 + NN;
|
|
213
|
+
const need = oBase + 3;
|
|
214
|
+
const ptr = this._scratch(need);
|
|
215
|
+
const buf = this._view(ptr, need);
|
|
216
|
+
for (let i = 0; i < N; i++) {
|
|
217
|
+
buf[3 * i + 0] = layers[i].n[0];
|
|
218
|
+
buf[3 * i + 1] = layers[i].n[1];
|
|
219
|
+
buf[3 * i + 2] = layers[i].d;
|
|
220
|
+
}
|
|
221
|
+
const P = (off) => ptr + off * 8;
|
|
222
|
+
this._tmm_hessian(lambda_nm, theta_deg, polCode | 0,
|
|
223
|
+
n0[0], n0[1], ns[0], ns[1], P(oLay), N,
|
|
224
|
+
P(oDR), P(oDT), P(oDA), P(oR2), P(oT2), P(oA2), P(oBase));
|
|
225
|
+
// Fresh view after the call (memory may have grown → buf detached).
|
|
226
|
+
const out = this._view(ptr, need);
|
|
227
|
+
const reshape = (off) => {
|
|
228
|
+
const rows = new Array(N);
|
|
229
|
+
for (let i = 0; i < N; i++) rows[i] = out.slice(off + i * N, off + i * N + N);
|
|
230
|
+
return rows;
|
|
231
|
+
};
|
|
232
|
+
return {
|
|
233
|
+
R: out[oBase], T: out[oBase + 1], A: out[oBase + 2], N,
|
|
234
|
+
dRdd: out.slice(oDR, oDR + N),
|
|
235
|
+
dTdd: out.slice(oDT, oDT + N),
|
|
236
|
+
dAdd: out.slice(oDA, oDA + N),
|
|
237
|
+
d2Rdd: reshape(oR2), d2Tdd: reshape(oT2), d2Add: reshape(oA2),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Analytic needle P-function scan — mirrors tmmNeedleScan() in
|
|
243
|
+
* thinFilmMath.js, reshaping the flat WASM output into the SAME nested
|
|
244
|
+
* structure the synthesis scanners consume.
|
|
245
|
+
* @param {{n:[number,number],d:number}[]} layers used AS-IS (index parity)
|
|
246
|
+
* @param {[number,number][]} candidateNs candidate ñ
|
|
247
|
+
* @param {number[]} intraFracs intra-layer split fractions
|
|
248
|
+
* @returns {{R,T,A,N, gaps:Array, intra:Array}}
|
|
249
|
+
* gaps[pos][ci] = {dR,dT,dA} (pos = 0..N)
|
|
250
|
+
* intra[k][fi] = {frac, perCand:[{dR,dT,dA}]}
|
|
251
|
+
*/
|
|
252
|
+
tmmNeedleScan(lambda_nm, theta_deg, polCode, n0, ns, layers, candidateNs, intraFracs = []) {
|
|
253
|
+
const N = layers.length;
|
|
254
|
+
const nCand = candidateNs.length;
|
|
255
|
+
const nFrac = intraFracs.length;
|
|
256
|
+
const nGap = (N + 1) * nCand * 3;
|
|
257
|
+
const nIntra = Math.max(1, N * nFrac * nCand * 3);
|
|
258
|
+
|
|
259
|
+
const layPtr = this._alloc(Math.max(1, 3 * N));
|
|
260
|
+
const candPtr = this._alloc(Math.max(1, 2 * nCand));
|
|
261
|
+
const fracPtr = this._alloc(Math.max(1, nFrac));
|
|
262
|
+
const basePtr = this._alloc(3);
|
|
263
|
+
const gapPtr = this._alloc(Math.max(1, nGap));
|
|
264
|
+
const intraPtr = this._alloc(nIntra);
|
|
265
|
+
|
|
266
|
+
const lay = this._view(layPtr, Math.max(1, 3 * N));
|
|
267
|
+
for (let i = 0; i < N; i++) {
|
|
268
|
+
lay[3 * i + 0] = layers[i].n[0];
|
|
269
|
+
lay[3 * i + 1] = layers[i].n[1];
|
|
270
|
+
lay[3 * i + 2] = layers[i].d;
|
|
271
|
+
}
|
|
272
|
+
const cand = this._view(candPtr, Math.max(1, 2 * nCand));
|
|
273
|
+
for (let c = 0; c < nCand; c++) { cand[2 * c] = candidateNs[c][0]; cand[2 * c + 1] = candidateNs[c][1]; }
|
|
274
|
+
const frac = this._view(fracPtr, Math.max(1, nFrac));
|
|
275
|
+
for (let i = 0; i < nFrac; i++) frac[i] = intraFracs[i];
|
|
276
|
+
|
|
277
|
+
this._tmm_needle_scan(lambda_nm, theta_deg, polCode | 0,
|
|
278
|
+
n0[0], n0[1], ns[0], ns[1], layPtr, N, candPtr, nCand, fracPtr, nFrac,
|
|
279
|
+
basePtr, gapPtr, intraPtr);
|
|
280
|
+
|
|
281
|
+
// Copy outputs out before freeing, reshaping to the JS nested layout.
|
|
282
|
+
const base = this._view(basePtr, 3);
|
|
283
|
+
const R = base[0], T = base[1], A = base[2];
|
|
284
|
+
const gapV = this._view(gapPtr, Math.max(1, nGap));
|
|
285
|
+
const gaps = new Array(N + 1);
|
|
286
|
+
for (let pos = 0; pos <= N; pos++) {
|
|
287
|
+
const row = new Array(nCand);
|
|
288
|
+
for (let c = 0; c < nCand; c++) {
|
|
289
|
+
const o = (pos * nCand + c) * 3;
|
|
290
|
+
row[c] = { dR: gapV[o], dT: gapV[o + 1], dA: gapV[o + 2] };
|
|
291
|
+
}
|
|
292
|
+
gaps[pos] = row;
|
|
293
|
+
}
|
|
294
|
+
const intra = [];
|
|
295
|
+
if (nFrac > 0) {
|
|
296
|
+
const intraV = this._view(intraPtr, nIntra);
|
|
297
|
+
for (let k = 0; k < N; k++) {
|
|
298
|
+
const rowK = [];
|
|
299
|
+
for (let fi = 0; fi < nFrac; fi++) {
|
|
300
|
+
const perCand = new Array(nCand);
|
|
301
|
+
for (let c = 0; c < nCand; c++) {
|
|
302
|
+
const o = ((k * nFrac + fi) * nCand + c) * 3;
|
|
303
|
+
perCand[c] = { dR: intraV[o], dT: intraV[o + 1], dA: intraV[o + 2] };
|
|
304
|
+
}
|
|
305
|
+
rowK.push({ frac: intraFracs[fi], perCand });
|
|
306
|
+
}
|
|
307
|
+
intra.push(rowK);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
for (const p of [layPtr, candPtr, fracPtr, basePtr, gapPtr, intraPtr]) this.free(p);
|
|
312
|
+
return { R, T, A, gaps, intra, N };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Instantiate from raw bytes (ArrayBuffer / Uint8Array). Sets the singleton. */
|
|
317
|
+
export async function instantiateTmmWasm(bytes) {
|
|
318
|
+
const { instance } = await WebAssembly.instantiate(
|
|
319
|
+
bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), wasmImports());
|
|
320
|
+
_instance = new TmmWasmInstance(instance);
|
|
321
|
+
return _instance;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Renderer/Node helper: fetch the `.wasm` at `url` and instantiate it. */
|
|
325
|
+
export function initTmmWasmFromUrl(url) {
|
|
326
|
+
if (_initPromise) return _initPromise;
|
|
327
|
+
_initPromise = (async () => {
|
|
328
|
+
try {
|
|
329
|
+
const resp = await fetch(url);
|
|
330
|
+
if (!resp.ok) throw new Error(`fetch ${url} → ${resp.status}`);
|
|
331
|
+
const buf = await resp.arrayBuffer();
|
|
332
|
+
await instantiateTmmWasm(buf);
|
|
333
|
+
return true;
|
|
334
|
+
} catch (e) {
|
|
335
|
+
// Not built yet / not found — silent fallback to JS.
|
|
336
|
+
_instance = null;
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
})();
|
|
340
|
+
return _initPromise;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function setTmmWasmEnabled(on) { _enabled = !!on; }
|
|
344
|
+
/** TEST-ONLY: inject a TmmWasmInstance (or null) directly, bypassing the .wasm
|
|
345
|
+
* fetch/instantiate, so the integration seam can be exercised with a mock. */
|
|
346
|
+
export function __setTmmWasmInstanceForTest(inst) { _instance = inst; }
|
|
347
|
+
|
|
348
|
+
// ── Cross-thread plumbing ────────────────────────────────────────────────────
|
|
349
|
+
// The renderer (main thread) loads the .wasm bytes once via IPC, instantiates
|
|
350
|
+
// its own module, and BROADCASTS the same bytes to each pool/worker (workers
|
|
351
|
+
// can't fetch a file:// asset under contextIsolation). Each worker instantiates
|
|
352
|
+
// its OWN module (no shared memory) and enables the flag.
|
|
353
|
+
|
|
354
|
+
let _workerBytes = null; // main-side: raw bytes to hand to workers
|
|
355
|
+
let _workerInitPromise = null; // worker-side: in-flight instantiation
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* MAIN THREAD bootstrap. Store the bytes for worker broadcast and, if the user
|
|
359
|
+
* enabled the feature, instantiate the main-thread module and flip the flag.
|
|
360
|
+
* Safe to call once at startup; failures fall back to JS silently.
|
|
361
|
+
*/
|
|
362
|
+
export async function initTmmWasmMainThread(bytes, enabled) {
|
|
363
|
+
if (bytes) _workerBytes = bytes; // remember for workers + later toggles
|
|
364
|
+
if (!enabled) { _enabled = false; return false; } // toggle off → JS everywhere
|
|
365
|
+
if (!_workerBytes) return false; // artifact never loaded
|
|
366
|
+
if (!_instance) { // instantiate once; reuse on re-toggle
|
|
367
|
+
try { await instantiateTmmWasm(_workerBytes); }
|
|
368
|
+
catch (_) { _instance = null; _enabled = false; return false; }
|
|
369
|
+
}
|
|
370
|
+
_enabled = true;
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** MAIN THREAD: bytes to ship to a worker — only when the feature is active. */
|
|
375
|
+
export function getTmmWasmBytesForWorker() {
|
|
376
|
+
return (_enabled && _workerBytes) ? _workerBytes : null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* WORKER side: kick off one-time instantiation from broadcast bytes and enable
|
|
381
|
+
* the flag in this worker. Idempotent; no-op without bytes or once instantiated.
|
|
382
|
+
*/
|
|
383
|
+
export function noteTmmWasmBytes(bytes) {
|
|
384
|
+
if (!bytes || _instance || _workerInitPromise) return;
|
|
385
|
+
_workerInitPromise = instantiateTmmWasm(bytes)
|
|
386
|
+
.then(() => { _enabled = true; return true; })
|
|
387
|
+
.catch(() => { _instance = null; _enabled = false; return false; });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** WORKER side: await any in-flight instantiation before processing a job. */
|
|
391
|
+
export function awaitTmmWasmReady() {
|
|
392
|
+
return _workerInitPromise || Promise.resolve(_instance !== null);
|
|
393
|
+
}
|
|
394
|
+
export function isTmmWasmEnabled() { return _enabled; }
|
|
395
|
+
export function isTmmWasmReady() { return _instance !== null; }
|
|
396
|
+
/** Active iff the feature flag is on AND a module is instantiated. */
|
|
397
|
+
export function tmmWasmActive() { return _enabled && _instance !== null; }
|
|
398
|
+
export function getTmmWasm() { return _instance; }
|