@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.
- package/README.md +8 -0
- package/dist/canvas/adapters.js +382 -0
- package/dist/canvas/adapters.js.br +0 -0
- package/dist/canvas/adapters.js.gz +0 -0
- package/dist/canvas/canvasManager.js +1949 -0
- package/dist/canvas/canvasManager.js.br +0 -0
- package/dist/canvas/canvasManager.js.gz +0 -0
- package/dist/canvas/canvasPrivacy.js +233 -0
- package/dist/canvas/canvasPrivacy.js.br +0 -0
- package/dist/canvas/canvasPrivacy.js.gz +0 -0
- package/dist/canvas/codec.js +314 -0
- package/dist/canvas/codec.js.br +0 -0
- package/dist/canvas/codec.js.gz +0 -0
- package/dist/canvas/deviceClass.js +67 -0
- package/dist/canvas/deviceClass.js.br +0 -0
- package/dist/canvas/deviceClass.js.gz +0 -0
- package/dist/canvas/encodeWorker.js +284 -0
- package/dist/canvas/encodeWorker.js.br +0 -0
- package/dist/canvas/encodeWorker.js.gz +0 -0
- package/dist/canvas/governor.js +79 -0
- package/dist/canvas/governor.js.br +0 -0
- package/dist/canvas/governor.js.gz +0 -0
- package/dist/canvas/keyframes.js +183 -0
- package/dist/canvas/keyframes.js.br +0 -0
- package/dist/canvas/keyframes.js.gz +0 -0
- package/dist/canvas/mediaRecorderCapture.js +257 -0
- package/dist/canvas/mediaRecorderCapture.js.br +0 -0
- package/dist/canvas/mediaRecorderCapture.js.gz +0 -0
- package/dist/canvas/objectLog.js +150 -0
- package/dist/canvas/objectLog.js.br +0 -0
- package/dist/canvas/objectLog.js.gz +0 -0
- package/dist/canvas/patch2d.js +1302 -0
- package/dist/canvas/patch2d.js.br +0 -0
- package/dist/canvas/patch2d.js.gz +0 -0
- package/dist/canvas/patchGL.js +371 -0
- package/dist/canvas/patchGL.js.br +0 -0
- package/dist/canvas/patchGL.js.gz +0 -0
- package/dist/canvas/patchWebGPU.js +361 -0
- package/dist/canvas/patchWebGPU.js.br +0 -0
- package/dist/canvas/patchWebGPU.js.gz +0 -0
- package/dist/canvas/piiDetect.js +155 -0
- package/dist/canvas/piiDetect.js.br +0 -0
- package/dist/canvas/piiDetect.js.gz +0 -0
- package/dist/canvas/protocol.js +122 -0
- package/dist/canvas/protocol.js.br +0 -0
- package/dist/canvas/protocol.js.gz +0 -0
- package/dist/canvas/videoCapture.js +252 -0
- package/dist/canvas/videoCapture.js.br +0 -0
- package/dist/canvas/videoCapture.js.gz +0 -0
- package/dist/canvas/workerRelay.js +99 -0
- package/dist/canvas/workerRelay.js.br +0 -0
- package/dist/canvas/workerRelay.js.gz +0 -0
- package/dist/canvas/workerShim.js +227 -0
- package/dist/canvas/workerShim.js.br +0 -0
- package/dist/canvas/workerShim.js.gz +0 -0
- package/dist/canvas-worker.cjs +74 -0
- package/dist/canvas-worker.cjs.br +0 -0
- package/dist/canvas-worker.cjs.gz +0 -0
- package/dist/canvas-worker.js +78 -0
- package/dist/canvas-worker.js.br +0 -0
- package/dist/canvas-worker.js.gz +0 -0
- package/dist/canvasFrameStore.js +198 -0
- package/dist/canvasFrameStore.js.br +0 -0
- package/dist/canvasFrameStore.js.gz +0 -0
- package/dist/chunks/canvasManager-BYn3_oyo.js +1440 -0
- package/dist/chunks/canvasManager-BYn3_oyo.js.br +0 -0
- package/dist/chunks/canvasManager-BYn3_oyo.js.gz +0 -0
- package/dist/chunks/canvasManager-CoX5072G.js +1456 -0
- package/dist/chunks/canvasManager-CoX5072G.js.br +0 -0
- package/dist/chunks/canvasManager-CoX5072G.js.gz +0 -0
- package/dist/chunks/{chunkSerializer-Cd8y9tID.js → chunkSerializer-C_K0Wyk0.js} +1 -1
- package/dist/chunks/chunkSerializer-C_K0Wyk0.js.br +0 -0
- package/dist/chunks/chunkSerializer-C_K0Wyk0.js.gz +0 -0
- package/dist/chunks/{chunkSerializer-pkpgT-FH.js → chunkSerializer-CghPitF4.js} +1 -1
- package/dist/chunks/chunkSerializer-CghPitF4.js.br +0 -0
- package/dist/chunks/chunkSerializer-CghPitF4.js.gz +0 -0
- package/dist/chunks/{index-Bvd8oTpV.js → index-BIj4l3CV.js} +1025 -655
- package/dist/chunks/index-BIj4l3CV.js.br +0 -0
- package/dist/chunks/index-BIj4l3CV.js.gz +0 -0
- package/dist/chunks/{index-CXvHbLRT.js → index-DIYomvdD.js} +1115 -724
- package/dist/chunks/index-DIYomvdD.js.br +0 -0
- package/dist/chunks/index-DIYomvdD.js.gz +0 -0
- package/dist/chunks/patch2d-9voowhJ_.js +816 -0
- package/dist/chunks/patch2d-9voowhJ_.js.br +0 -0
- package/dist/chunks/patch2d-9voowhJ_.js.gz +0 -0
- package/dist/chunks/patch2d-PQxcaE6r.js +886 -0
- package/dist/chunks/patch2d-PQxcaE6r.js.br +0 -0
- package/dist/chunks/patch2d-PQxcaE6r.js.gz +0 -0
- package/dist/graphql.js +12 -1
- package/dist/graphql.js.br +0 -0
- package/dist/graphql.js.gz +0 -0
- package/dist/index.js +144 -2
- package/dist/index.js.br +0 -0
- package/dist/index.js.gz +0 -0
- package/dist/recorder.cjs +2 -2
- package/dist/recorder.cjs.br +0 -0
- package/dist/recorder.cjs.gz +0 -0
- package/dist/recorder.js +52 -41
- package/dist/recorder.js.br +0 -0
- package/dist/recorder.js.gz +0 -0
- package/dist/recorder.umd.cjs +4842 -2215
- package/dist/recorder.umd.cjs.br +0 -0
- package/dist/recorder.umd.cjs.gz +0 -0
- package/dist/recording.js +119 -10
- package/dist/recording.js.br +0 -0
- package/dist/recording.js.gz +0 -0
- package/dist/types/canvas/adapters.d.ts +71 -0
- package/dist/types/canvas/canvasManager.d.ts +273 -0
- package/dist/types/canvas/canvasPrivacy.d.ts +184 -0
- package/dist/types/canvas/codec.d.ts +27 -0
- package/dist/types/canvas/deviceClass.d.ts +40 -0
- package/dist/types/canvas/encodeWorker.d.ts +54 -0
- package/dist/types/canvas/governor.d.ts +44 -0
- package/dist/types/canvas/keyframes.d.ts +81 -0
- package/dist/types/canvas/mediaRecorderCapture.d.ts +91 -0
- package/dist/types/canvas/objectLog.d.ts +75 -0
- package/dist/types/canvas/patch2d.d.ts +339 -0
- package/dist/types/canvas/patchGL.d.ts +74 -0
- package/dist/types/canvas/patchWebGPU.d.ts +37 -0
- package/dist/types/canvas/piiDetect.d.ts +60 -0
- package/dist/types/canvas/protocol.d.ts +176 -0
- package/dist/types/canvas/videoCapture.d.ts +67 -0
- package/dist/types/canvas/workerRelay.d.ts +29 -0
- package/dist/types/canvas/workerShim.d.ts +10 -0
- package/dist/types/canvasFrameStore.d.ts +23 -0
- package/dist/types/graphql.d.ts +1 -1
- package/dist/types/index.d.ts +99 -1
- package/dist/types/types.d.ts +40 -0
- package/dist/types/websocket.d.ts +19 -0
- package/dist/websocket.js +210 -0
- package/dist/websocket.js.br +0 -0
- package/dist/websocket.js.gz +0 -0
- package/package.json +7 -1
- package/dist/chunks/chunkSerializer-Cd8y9tID.js.br +0 -0
- package/dist/chunks/chunkSerializer-Cd8y9tID.js.gz +0 -0
- package/dist/chunks/chunkSerializer-pkpgT-FH.js.br +0 -0
- package/dist/chunks/chunkSerializer-pkpgT-FH.js.gz +0 -0
- package/dist/chunks/index-Bvd8oTpV.js.br +0 -0
- package/dist/chunks/index-Bvd8oTpV.js.gz +0 -0
- package/dist/chunks/index-CXvHbLRT.js.br +0 -0
- 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
|