@sailfish-ai/recorder 1.12.5 → 1.12.6

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.
Files changed (141) hide show
  1. package/README.md +8 -0
  2. package/dist/canvas/adapters.js +382 -0
  3. package/dist/canvas/adapters.js.br +0 -0
  4. package/dist/canvas/adapters.js.gz +0 -0
  5. package/dist/canvas/canvasManager.js +1949 -0
  6. package/dist/canvas/canvasManager.js.br +0 -0
  7. package/dist/canvas/canvasManager.js.gz +0 -0
  8. package/dist/canvas/canvasPrivacy.js +233 -0
  9. package/dist/canvas/canvasPrivacy.js.br +0 -0
  10. package/dist/canvas/canvasPrivacy.js.gz +0 -0
  11. package/dist/canvas/codec.js +314 -0
  12. package/dist/canvas/codec.js.br +0 -0
  13. package/dist/canvas/codec.js.gz +0 -0
  14. package/dist/canvas/deviceClass.js +67 -0
  15. package/dist/canvas/deviceClass.js.br +0 -0
  16. package/dist/canvas/deviceClass.js.gz +0 -0
  17. package/dist/canvas/encodeWorker.js +284 -0
  18. package/dist/canvas/encodeWorker.js.br +0 -0
  19. package/dist/canvas/encodeWorker.js.gz +0 -0
  20. package/dist/canvas/governor.js +79 -0
  21. package/dist/canvas/governor.js.br +0 -0
  22. package/dist/canvas/governor.js.gz +0 -0
  23. package/dist/canvas/keyframes.js +183 -0
  24. package/dist/canvas/keyframes.js.br +0 -0
  25. package/dist/canvas/keyframes.js.gz +0 -0
  26. package/dist/canvas/mediaRecorderCapture.js +257 -0
  27. package/dist/canvas/mediaRecorderCapture.js.br +0 -0
  28. package/dist/canvas/mediaRecorderCapture.js.gz +0 -0
  29. package/dist/canvas/objectLog.js +150 -0
  30. package/dist/canvas/objectLog.js.br +0 -0
  31. package/dist/canvas/objectLog.js.gz +0 -0
  32. package/dist/canvas/patch2d.js +1302 -0
  33. package/dist/canvas/patch2d.js.br +0 -0
  34. package/dist/canvas/patch2d.js.gz +0 -0
  35. package/dist/canvas/patchGL.js +371 -0
  36. package/dist/canvas/patchGL.js.br +0 -0
  37. package/dist/canvas/patchGL.js.gz +0 -0
  38. package/dist/canvas/patchWebGPU.js +361 -0
  39. package/dist/canvas/patchWebGPU.js.br +0 -0
  40. package/dist/canvas/patchWebGPU.js.gz +0 -0
  41. package/dist/canvas/piiDetect.js +155 -0
  42. package/dist/canvas/piiDetect.js.br +0 -0
  43. package/dist/canvas/piiDetect.js.gz +0 -0
  44. package/dist/canvas/protocol.js +122 -0
  45. package/dist/canvas/protocol.js.br +0 -0
  46. package/dist/canvas/protocol.js.gz +0 -0
  47. package/dist/canvas/videoCapture.js +252 -0
  48. package/dist/canvas/videoCapture.js.br +0 -0
  49. package/dist/canvas/videoCapture.js.gz +0 -0
  50. package/dist/canvas/workerRelay.js +99 -0
  51. package/dist/canvas/workerRelay.js.br +0 -0
  52. package/dist/canvas/workerRelay.js.gz +0 -0
  53. package/dist/canvas/workerShim.js +227 -0
  54. package/dist/canvas/workerShim.js.br +0 -0
  55. package/dist/canvas/workerShim.js.gz +0 -0
  56. package/dist/canvas-worker.cjs +74 -0
  57. package/dist/canvas-worker.cjs.br +0 -0
  58. package/dist/canvas-worker.cjs.gz +0 -0
  59. package/dist/canvas-worker.js +78 -0
  60. package/dist/canvas-worker.js.br +0 -0
  61. package/dist/canvas-worker.js.gz +0 -0
  62. package/dist/canvasFrameStore.js +198 -0
  63. package/dist/canvasFrameStore.js.br +0 -0
  64. package/dist/canvasFrameStore.js.gz +0 -0
  65. package/dist/chunks/canvasManager-BYn3_oyo.js +1440 -0
  66. package/dist/chunks/canvasManager-BYn3_oyo.js.br +0 -0
  67. package/dist/chunks/canvasManager-BYn3_oyo.js.gz +0 -0
  68. package/dist/chunks/canvasManager-CoX5072G.js +1456 -0
  69. package/dist/chunks/canvasManager-CoX5072G.js.br +0 -0
  70. package/dist/chunks/canvasManager-CoX5072G.js.gz +0 -0
  71. package/dist/chunks/{chunkSerializer-Cd8y9tID.js → chunkSerializer-C_K0Wyk0.js} +1 -1
  72. package/dist/chunks/chunkSerializer-C_K0Wyk0.js.br +0 -0
  73. package/dist/chunks/chunkSerializer-C_K0Wyk0.js.gz +0 -0
  74. package/dist/chunks/{chunkSerializer-pkpgT-FH.js → chunkSerializer-CghPitF4.js} +1 -1
  75. package/dist/chunks/chunkSerializer-CghPitF4.js.br +0 -0
  76. package/dist/chunks/chunkSerializer-CghPitF4.js.gz +0 -0
  77. package/dist/chunks/{index-Bvd8oTpV.js → index-BIj4l3CV.js} +1025 -655
  78. package/dist/chunks/index-BIj4l3CV.js.br +0 -0
  79. package/dist/chunks/index-BIj4l3CV.js.gz +0 -0
  80. package/dist/chunks/{index-CXvHbLRT.js → index-DIYomvdD.js} +1115 -724
  81. package/dist/chunks/index-DIYomvdD.js.br +0 -0
  82. package/dist/chunks/index-DIYomvdD.js.gz +0 -0
  83. package/dist/chunks/patch2d-9voowhJ_.js +816 -0
  84. package/dist/chunks/patch2d-9voowhJ_.js.br +0 -0
  85. package/dist/chunks/patch2d-9voowhJ_.js.gz +0 -0
  86. package/dist/chunks/patch2d-PQxcaE6r.js +886 -0
  87. package/dist/chunks/patch2d-PQxcaE6r.js.br +0 -0
  88. package/dist/chunks/patch2d-PQxcaE6r.js.gz +0 -0
  89. package/dist/graphql.js +12 -1
  90. package/dist/graphql.js.br +0 -0
  91. package/dist/graphql.js.gz +0 -0
  92. package/dist/index.js +144 -2
  93. package/dist/index.js.br +0 -0
  94. package/dist/index.js.gz +0 -0
  95. package/dist/recorder.cjs +2 -2
  96. package/dist/recorder.cjs.br +0 -0
  97. package/dist/recorder.cjs.gz +0 -0
  98. package/dist/recorder.js +52 -41
  99. package/dist/recorder.js.br +0 -0
  100. package/dist/recorder.js.gz +0 -0
  101. package/dist/recorder.umd.cjs +4842 -2215
  102. package/dist/recorder.umd.cjs.br +0 -0
  103. package/dist/recorder.umd.cjs.gz +0 -0
  104. package/dist/recording.js +119 -10
  105. package/dist/recording.js.br +0 -0
  106. package/dist/recording.js.gz +0 -0
  107. package/dist/types/canvas/adapters.d.ts +71 -0
  108. package/dist/types/canvas/canvasManager.d.ts +273 -0
  109. package/dist/types/canvas/canvasPrivacy.d.ts +184 -0
  110. package/dist/types/canvas/codec.d.ts +27 -0
  111. package/dist/types/canvas/deviceClass.d.ts +40 -0
  112. package/dist/types/canvas/encodeWorker.d.ts +54 -0
  113. package/dist/types/canvas/governor.d.ts +44 -0
  114. package/dist/types/canvas/keyframes.d.ts +81 -0
  115. package/dist/types/canvas/mediaRecorderCapture.d.ts +91 -0
  116. package/dist/types/canvas/objectLog.d.ts +75 -0
  117. package/dist/types/canvas/patch2d.d.ts +339 -0
  118. package/dist/types/canvas/patchGL.d.ts +74 -0
  119. package/dist/types/canvas/patchWebGPU.d.ts +37 -0
  120. package/dist/types/canvas/piiDetect.d.ts +60 -0
  121. package/dist/types/canvas/protocol.d.ts +176 -0
  122. package/dist/types/canvas/videoCapture.d.ts +67 -0
  123. package/dist/types/canvas/workerRelay.d.ts +29 -0
  124. package/dist/types/canvas/workerShim.d.ts +10 -0
  125. package/dist/types/canvasFrameStore.d.ts +23 -0
  126. package/dist/types/graphql.d.ts +1 -1
  127. package/dist/types/index.d.ts +99 -1
  128. package/dist/types/types.d.ts +40 -0
  129. package/dist/types/websocket.d.ts +19 -0
  130. package/dist/websocket.js +210 -0
  131. package/dist/websocket.js.br +0 -0
  132. package/dist/websocket.js.gz +0 -0
  133. package/package.json +7 -1
  134. package/dist/chunks/chunkSerializer-Cd8y9tID.js.br +0 -0
  135. package/dist/chunks/chunkSerializer-Cd8y9tID.js.gz +0 -0
  136. package/dist/chunks/chunkSerializer-pkpgT-FH.js.br +0 -0
  137. package/dist/chunks/chunkSerializer-pkpgT-FH.js.gz +0 -0
  138. package/dist/chunks/index-Bvd8oTpV.js.br +0 -0
  139. package/dist/chunks/index-Bvd8oTpV.js.gz +0 -0
  140. package/dist/chunks/index-CXvHbLRT.js.br +0 -0
  141. package/dist/chunks/index-CXvHbLRT.js.gz +0 -0
Binary file
Binary file
@@ -0,0 +1,371 @@
1
+ /**
2
+ * WebGL1/2 command capture — port of the validated bench core
3
+ * (test-applications/canvas-recorder-bench/src/capture/patchGL.ts;
4
+ * evanw/webgl-recorder lineage, binary-encoded). Creator calls intern their
5
+ * results via the same __create meta-op the 2D path uses, so the shared codec
6
+ * + replayer machinery handles GL streams unchanged.
7
+ *
8
+ * Replay fidelity is "deterministic on the same GPU/driver" — pixel keyframes
9
+ * remain the bit-exact truth, exactly like the 2D path.
10
+ *
11
+ * SDK adaptations: SSR-safe installation (no top-level globals), untracked-
12
+ * context notifier for contexts created before install, DiscardSink
13
+ * short-circuit, attachGLRecorderToContext for mid-stream attach.
14
+ */
15
+ import { DIRTY_OP, DiscardSink } from "./protocol";
16
+ import { BufferSink, serializeTypedArray } from "./protocol";
17
+ import { deflate } from "./codec";
18
+ import { ignore2DContext, isTaintError } from "./patch2d";
19
+ /** methods whose return value is an interned object */
20
+ const CREATOR_RE = /^create[A-Z]/;
21
+ const OTHER_CREATORS = new Set(["getUniformLocation", "getExtension"]);
22
+ /** pure readbacks — never recorded, never replayed */
23
+ const PURE_RE = /^(get(?!Extension$|UniformLocation$)|is[A-Z]|check|read|finish$|flush$)/;
24
+ let installed = false;
25
+ const glRecorders = new WeakMap();
26
+ // ── GL texture-source capture ───────────────────────────────────────────────
27
+ // GL texture uploads (texImage2D/texSubImage2D) from a STATIC image source
28
+ // (HTMLImageElement / ImageBitmap / ImageData) are captured as imagedata assets
29
+ // so textured content REPLAYS (and becomes click-to-code pickable) instead of
30
+ // rendering blank. We deliberately capture only STATIC sources (dedup'd by
31
+ // identity → captured once) and bound the count, so a dynamic video/canvas
32
+ // texture uploaded per-frame can't blow up bandwidth (those stay uncaptured —
33
+ // the prior behavior). Rasterization needs a DOM canvas, so element sources are
34
+ // skipped under SSR/worker (ImageData, which has raw pixels, still captures).
35
+ /** Max captured texture assets per recorder — bounds bandwidth for an app that
36
+ * uploads many distinct (e.g. per-frame ImageData) textures. Past this, capture
37
+ * degrades to "unsupported" (blank on replay), never unbounded. */
38
+ const MAX_GL_TEX_ASSETS = 64;
39
+ /** [audit B5] Max RAW (uncompressed RGBA) bytes of a single capturable texture.
40
+ * The captured asset is deflated and shipped as ONE SFCV frame, which the
41
+ * transport drops if it exceeds the 8 MiB per-frame cap (canvasManager
42
+ * MAX_FRAME_BYTES). Pixel data is often incompressible (photos/noise), so the
43
+ * deflated size can approach the raw size — gate on raw bytes with headroom for
44
+ * the deflate stream + frame envelope so a too-large texture is reported as
45
+ * `texture-too-large` UP FRONT (the player shows an "8 MB limit" badge) instead
46
+ * of the op referencing an asset that is then silently dropped at send → a blank
47
+ * texture with no explanation. 7 MiB raw ≈ a 1290² RGBA texture. */
48
+ const MAX_GL_TEX_RAW_BYTES = 7 * 1024 * 1024;
49
+ /** Raw RGBA byte size of a static texture source from its dimensions (no
50
+ * rasterization — cheap enough to gate before capture). */
51
+ function texSourceRawBytes(src) {
52
+ if (typeof ImageData !== "undefined" && src instanceof ImageData) {
53
+ return src.width * src.height * 4;
54
+ }
55
+ const s = src;
56
+ const w = Number(s.naturalWidth || s.width) || 0;
57
+ const h = Number(s.naturalHeight || s.height) || 0;
58
+ return w * h * 4;
59
+ }
60
+ let _texScratch = null;
61
+ function texScratchCtx(w, h) {
62
+ if (typeof document === "undefined")
63
+ return null; // worker / SSR
64
+ if (!_texScratch) {
65
+ _texScratch = document.createElement("canvas");
66
+ const c0 = _texScratch.getContext("2d");
67
+ if (c0)
68
+ ignore2DContext(c0); // our own scratch — never record it
69
+ }
70
+ if (_texScratch.width !== w || _texScratch.height !== h) {
71
+ _texScratch.width = w;
72
+ _texScratch.height = h;
73
+ }
74
+ const ctx = _texScratch.getContext("2d");
75
+ if (ctx)
76
+ ignore2DContext(ctx);
77
+ return ctx;
78
+ }
79
+ /** A static, capturable texture source. Dynamic sources (video / canvas /
80
+ * OffscreenCanvas) are intentionally excluded (they change per-frame). */
81
+ function isStaticTexSource(o) {
82
+ return ((typeof ImageData !== "undefined" && o instanceof ImageData) ||
83
+ (typeof HTMLImageElement !== "undefined" && o instanceof HTMLImageElement) ||
84
+ (typeof ImageBitmap !== "undefined" && o instanceof ImageBitmap));
85
+ }
86
+ /** Rasterize an element source to ImageData (ImageData passes through). Returns
87
+ * null when it can't be read for a NON-error reason (no DOM, zero-size). A tainted
88
+ * / cross-origin source whose `getImageData` THROWS is propagated to the caller
89
+ * ([audit B13] so it can emit a taint signal), not swallowed. */
90
+ function texSourceToImageData(src) {
91
+ if (typeof ImageData !== "undefined" && src instanceof ImageData)
92
+ return src;
93
+ const s = src;
94
+ const w = Number(s.naturalWidth || s.width) || 0;
95
+ const h = Number(s.naturalHeight || s.height) || 0;
96
+ if (!w || !h)
97
+ return null;
98
+ const ctx = texScratchCtx(w, h);
99
+ if (!ctx)
100
+ return null;
101
+ ctx.clearRect(0, 0, w, h);
102
+ ctx.drawImage(src, 0, 0);
103
+ return ctx.getImageData(0, 0, w, h); // throws on a tainted source → caller handles
104
+ }
105
+ /**
106
+ * Draw-site stamp routing (Phase 3b): when stamped app code calls
107
+ * `__sfds(glctx, id).drawArrays(...)`, canvasManager's __sfDrawSite calls this
108
+ * with the GL context; if it has a recorder, the id rides the next op.
109
+ * Returns true when handled (ctx is a recorded GL context).
110
+ */
111
+ export function noteGLPendingSiteId(ctx, id) {
112
+ const rec = glRecorders.get(ctx);
113
+ if (!rec)
114
+ return false;
115
+ rec.pendingSiteId = id;
116
+ return true;
117
+ }
118
+ let untrackedNotify = null;
119
+ export function setUntrackedGLContextNotifier(fn) {
120
+ untrackedNotify = fn;
121
+ }
122
+ let glAutoConfig = null;
123
+ const glAutoRecorders = new Map();
124
+ export function setGLAutoAttach(config) {
125
+ glAutoConfig = config;
126
+ if (config)
127
+ glAutoRecorders.clear();
128
+ }
129
+ export function pruneUnboundGLRecorders() {
130
+ for (const [canvas, rec] of glAutoRecorders) {
131
+ if (!rec.bound) {
132
+ glRecorders.delete(rec.rawCtx);
133
+ glAutoRecorders.delete(canvas);
134
+ }
135
+ }
136
+ }
137
+ export class GLRecorder {
138
+ now;
139
+ cpuMs = 0;
140
+ bound = false;
141
+ rawCtx = null;
142
+ /** ids for GL objects, scoped to this recorder's stream */
143
+ refs = new WeakMap();
144
+ nextId = 1_000_000; // distinct range from 2D object ids (cosmetic)
145
+ sink;
146
+ // GL texture-source capture (asset ids in a distinct 2M+ range; dedup static
147
+ // sources by identity; bounded count).
148
+ // [audit B6] In-flight texture-encode promises — a Set with auto-removal so it
149
+ // stays bounded to actually-pending work (see ContextRecorder.trackEncode).
150
+ pendingEncodes = new Set();
151
+ assetBySource = new WeakMap();
152
+ nextAssetId = 2_000_000;
153
+ assetCount = 0;
154
+ constructor(sink, now) {
155
+ this.now = now;
156
+ this.sink = sink;
157
+ }
158
+ /** Await pending texture-asset encodes so they ship before the stream stops
159
+ * (mirrors ContextRecorder.settle). The manager awaits this at teardown. */
160
+ async settle() {
161
+ // [audit B12] Loop-drain: if an encode is registered while we await (it
162
+ // shouldn't, now that teardown detaches first — but defensively), await it too
163
+ // so no asset is left un-shipped.
164
+ while (this.pendingEncodes.size)
165
+ await Promise.all([...this.pendingEncodes]);
166
+ }
167
+ /** [audit B6] Register an in-flight encode + auto-remove on completion so the
168
+ * set never grows unbounded. (then/finally swallow avoids unhandled rejection.) */
169
+ trackEncode(p) {
170
+ this.pendingEncodes.add(p);
171
+ p.then(() => { }, () => { }).finally(() => this.pendingEncodes.delete(p));
172
+ }
173
+ /** Capture a static GL texture source as an imagedata asset (dedup'd by source
174
+ * identity, bounded). Returns the asset ref to serialize; an unreadable /
175
+ * tainted / over-cap source returns `unsupported` (uncaptured → blank replay,
176
+ * the prior behavior). */
177
+ captureTexSource(src, t) {
178
+ const existing = this.assetBySource.get(src);
179
+ if (existing !== undefined)
180
+ return { $: "imagedata", id: existing };
181
+ if (this.assetCount >= MAX_GL_TEX_ASSETS)
182
+ return { $: "unsupported", kind: "texture-cap" };
183
+ // [audit B5] Reject before rasterizing: a texture whose deflated asset would
184
+ // exceed the 8 MiB transport frame cap is reported as `texture-too-large` so
185
+ // the player shows a badge, rather than emitting an asset ref that gets
186
+ // silently dropped at send → an unexplained blank texture. (Not added to the
187
+ // dedup map / asset count, so re-uploads of the same source report the same.)
188
+ if (texSourceRawBytes(src) > MAX_GL_TEX_RAW_BYTES) {
189
+ return { $: "unsupported", kind: "texture-too-large" };
190
+ }
191
+ let img;
192
+ try {
193
+ img = texSourceToImageData(src);
194
+ }
195
+ catch (e) {
196
+ // [audit B13] A cross-origin/tainted texture source throws on readback. Emit
197
+ // a taint signal so the player paints its A40 taint placeholder/badge,
198
+ // instead of silently replaying a blank textured draw with no explanation
199
+ // (the 2D image path already signals taint this way).
200
+ if (isTaintError(e)) {
201
+ this.sink.onTaint?.(t);
202
+ return { $: "unsupported", kind: "texture-tainted" };
203
+ }
204
+ return { $: "unsupported", kind: "texture" };
205
+ }
206
+ if (!img)
207
+ return { $: "unsupported", kind: src.constructor?.name ?? "texture" };
208
+ const id = this.nextAssetId++;
209
+ this.assetCount++;
210
+ this.assetBySource.set(src, id);
211
+ const w = img.width, h = img.height;
212
+ // Copy the pixels now — the source may be reused/mutated after this call.
213
+ const bytes = new Uint8Array(img.data.buffer.slice(img.data.byteOffset, img.data.byteOffset + img.data.byteLength));
214
+ this.trackEncode(deflate(bytes)
215
+ .then((z) => {
216
+ this.sink.onAssetBytes(id, z.length, t);
217
+ this.sink.onAssetData?.({ id, format: "imagedata", w, h, t }, z);
218
+ })
219
+ .catch((e) => this.sink.note(`GL texture asset deflate failed: ${e.message}`)));
220
+ return { $: "imagedata", id };
221
+ }
222
+ /** Bind the strategy's sink, draining anything buffered since getContext. */
223
+ swapSink(sink) {
224
+ const old = this.sink;
225
+ this.sink = sink;
226
+ this.bound = true;
227
+ if (old instanceof BufferSink) {
228
+ for (const op of old.ops)
229
+ sink.onOp(op);
230
+ for (const a of old.assetEvents)
231
+ sink.onAssetBytes(a.id, a.bytes, a.t);
232
+ for (const d of old.assetData)
233
+ sink.onAssetData?.(d.info, d.payload); // GL texture assets
234
+ for (const n of old.notes)
235
+ sink.note(n);
236
+ }
237
+ }
238
+ /** draw-site stamp id for the next op (Phase 3b); see ContextRecorder.pendingSiteId */
239
+ pendingSiteId;
240
+ recordCall(op, args, result) {
241
+ // Video-mode dirtiness only: zero serialize/encode cost.
242
+ if (this.sink instanceof DiscardSink) {
243
+ this.sink.onOp(DIRTY_OP);
244
+ this.pendingSiteId = undefined;
245
+ return;
246
+ }
247
+ const t0 = performance.now();
248
+ const t = this.now();
249
+ const sfId = this.pendingSiteId;
250
+ this.pendingSiteId = undefined;
251
+ if ((CREATOR_RE.test(op) || OTHER_CREATORS.has(op)) && result && typeof result === "object") {
252
+ const id = this.nextId++;
253
+ this.refs.set(result, id);
254
+ this.sink.onOp({
255
+ t, op: "__create", create: true,
256
+ args: [op, id, ...args.map((a) => this.arg(a, t))],
257
+ });
258
+ }
259
+ else {
260
+ const rec = { t, op, args: args.map((a) => this.arg(a, t)) };
261
+ if (sfId)
262
+ rec.sfId = sfId;
263
+ this.sink.onOp(rec);
264
+ }
265
+ this.cpuMs += performance.now() - t0;
266
+ }
267
+ arg(a, _t) {
268
+ if (a === null || a === undefined)
269
+ return null;
270
+ switch (typeof a) {
271
+ case "number": return Number.isFinite(a) ? a : 0;
272
+ case "string": return a;
273
+ case "boolean": return a;
274
+ case "object": break;
275
+ default: return { $: "unsupported", kind: typeof a };
276
+ }
277
+ const obj = a;
278
+ const ref = this.refs.get(obj);
279
+ if (ref !== undefined)
280
+ return { $: "obj", id: ref };
281
+ if (ArrayBuffer.isView(obj) || obj instanceof ArrayBuffer) {
282
+ return serializeTypedArray(obj);
283
+ }
284
+ if (Array.isArray(obj))
285
+ return { $: "arr", v: obj.map((n) => Number(n) || 0) };
286
+ // GL texture source (texImage2D/texSubImage2D) — capture static image /
287
+ // ImageData sources as imagedata assets so textured content replays + is
288
+ // pickable. Dynamic (video/canvas) sources fall through to unsupported.
289
+ if (isStaticTexSource(obj))
290
+ return this.captureTexSource(obj, _t);
291
+ return { $: "unsupported", kind: obj.constructor?.name ?? "object" };
292
+ }
293
+ }
294
+ export function installGLHooks() {
295
+ if (installed)
296
+ return;
297
+ installed = true;
298
+ const protos = [];
299
+ if (typeof WebGLRenderingContext !== "undefined")
300
+ protos.push(WebGLRenderingContext.prototype);
301
+ if (typeof WebGL2RenderingContext !== "undefined")
302
+ protos.push(WebGL2RenderingContext.prototype);
303
+ for (const proto of protos) {
304
+ for (const name of Object.getOwnPropertyNames(proto)) {
305
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
306
+ if (!desc || typeof desc.value !== "function" || name === "constructor")
307
+ continue;
308
+ if (PURE_RE.test(name) && !OTHER_CREATORS.has(name))
309
+ continue;
310
+ const orig = desc.value;
311
+ proto[name] = function (...args) {
312
+ const result = orig.apply(this, args);
313
+ const rec = glRecorders.get(this);
314
+ if (rec)
315
+ rec.recordCall(name, args, result);
316
+ else if (untrackedNotify)
317
+ untrackedNotify(this);
318
+ return result;
319
+ };
320
+ }
321
+ }
322
+ // auto-attach on context creation (chained over any earlier getContext patch)
323
+ if (typeof HTMLCanvasElement === "undefined")
324
+ return;
325
+ const origGetContext = HTMLCanvasElement.prototype.getContext;
326
+ HTMLCanvasElement.prototype.getContext = function (type, attrs) {
327
+ const result = origGetContext.call(this, type, attrs);
328
+ if ((type === "webgl" || type === "webgl2" || type === "experimental-webgl") &&
329
+ result && glAutoConfig && !glRecorders.has(result)) {
330
+ const rec = new GLRecorder(new BufferSink(), glAutoConfig.now);
331
+ rec.rawCtx = result;
332
+ glRecorders.set(result, rec);
333
+ glAutoRecorders.set(this, rec);
334
+ }
335
+ return result;
336
+ };
337
+ }
338
+ export function attachGLRecorder(canvas, contextType, sink, now) {
339
+ const auto = glAutoRecorders.get(canvas);
340
+ if (auto) {
341
+ auto.swapSink(sink);
342
+ return auto;
343
+ }
344
+ const ctx = canvas.getContext(contextType);
345
+ if (!ctx)
346
+ return null;
347
+ const rec = new GLRecorder(sink, now);
348
+ rec.rawCtx = ctx;
349
+ rec.bound = true;
350
+ glRecorders.set(ctx, rec);
351
+ return rec;
352
+ }
353
+ /** Mid-stream attach to a known GL context (created before hooks installed). */
354
+ export function attachGLRecorderToContext(ctx, sink, now) {
355
+ const rec = new GLRecorder(sink, now);
356
+ rec.rawCtx = ctx;
357
+ rec.bound = true;
358
+ glRecorders.set(ctx, rec);
359
+ return rec;
360
+ }
361
+ export function detachGLRecorder(canvas, contextType) {
362
+ try {
363
+ const ctx = canvas.getContext(contextType);
364
+ if (ctx)
365
+ glRecorders.delete(ctx);
366
+ }
367
+ catch { /* gone */ }
368
+ }
369
+ export function detachGLRecorderFromContext(ctx) {
370
+ glRecorders.delete(ctx);
371
+ }
Binary file
Binary file