@rezi-ui/node 0.1.0-alpha.45 → 0.1.0-alpha.47

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.
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { performance } from "node:perf_hooks";
9
9
  import { parentPort, workerData } from "node:worker_threads";
10
+ import { FRAME_AUDIT_NATIVE_ENABLED, FRAME_AUDIT_NATIVE_RING_BYTES, ZR_DEBUG_CAT_DRAWLIST, ZR_DEBUG_CAT_FRAME, ZR_DEBUG_CAT_PERF, ZR_DEBUG_CODE_DRAWLIST_CMD, ZR_DEBUG_CODE_DRAWLIST_EXECUTE, ZR_DEBUG_CODE_DRAWLIST_VALIDATE, ZR_DEBUG_CODE_FRAME_BEGIN, ZR_DEBUG_CODE_FRAME_PRESENT, ZR_DEBUG_CODE_FRAME_RESIZE, ZR_DEBUG_CODE_FRAME_SUBMIT, ZR_DEBUG_CODE_PERF_DIFF_PATH, ZR_DEBUG_CODE_PERF_TIMING, createFrameAuditLogger, drawlistFingerprint, } from "../frameAudit.js";
10
11
  import { EVENT_POOL_SIZE, FRAME_SAB_CONTROL_CONSUMED_SEQ_WORD, FRAME_SAB_CONTROL_HEADER_WORDS, FRAME_SAB_CONTROL_PUBLISHED_BYTES_WORD, FRAME_SAB_CONTROL_PUBLISHED_SEQ_WORD, FRAME_SAB_CONTROL_PUBLISHED_SLOT_WORD, FRAME_SAB_CONTROL_PUBLISHED_TOKEN_WORD, FRAME_SAB_CONTROL_WORDS_PER_SLOT, FRAME_SAB_SLOT_STATE_FREE, FRAME_SAB_SLOT_STATE_IN_USE, FRAME_SAB_SLOT_STATE_READY, FRAME_TRANSPORT_SAB_V1, FRAME_TRANSPORT_TRANSFER_V1, FRAME_TRANSPORT_VERSION, MAX_POLL_DRAIN_ITERS, } from "./protocol.js";
11
12
  import { computeNextIdleDelay, computeTickTiming } from "./tickTiming.js";
12
13
  /**
@@ -154,12 +155,42 @@ const DEBUG_HEADER_BYTES = 40;
154
155
  const DEBUG_QUERY_MIN_HEADERS_CAP = DEBUG_HEADER_BYTES;
155
156
  const DEBUG_QUERY_MAX_HEADERS_CAP = 1 << 20; // 1 MiB
156
157
  const NO_RECYCLED_DRAWLISTS = Object.freeze([]);
158
+ const DEBUG_DRAWLIST_RECORD_BYTES = 48;
159
+ const DEBUG_FRAME_RECORD_BYTES = 56;
160
+ const DEBUG_PERF_RECORD_BYTES = 24;
161
+ // Must cover sizeof(zr_diff_telemetry_record_t) (includes native trailing pad).
162
+ const DEBUG_DIFF_PATH_RECORD_BYTES = 64;
163
+ const NATIVE_FRAME_AUDIT_CATEGORY_MASK = (1 << ZR_DEBUG_CAT_DRAWLIST) | (1 << ZR_DEBUG_CAT_FRAME) | (1 << ZR_DEBUG_CAT_PERF);
164
+ function nativeFrameCodeName(code) {
165
+ if (code === ZR_DEBUG_CODE_FRAME_BEGIN)
166
+ return "frame.begin";
167
+ if (code === ZR_DEBUG_CODE_FRAME_SUBMIT)
168
+ return "frame.submit";
169
+ if (code === ZR_DEBUG_CODE_FRAME_PRESENT)
170
+ return "frame.present";
171
+ if (code === ZR_DEBUG_CODE_FRAME_RESIZE)
172
+ return "frame.resize";
173
+ return "frame.unknown";
174
+ }
175
+ function nativePerfPhaseName(phase) {
176
+ if (phase === 0)
177
+ return "poll";
178
+ if (phase === 1)
179
+ return "submit";
180
+ if (phase === 2)
181
+ return "present";
182
+ return "unknown";
183
+ }
157
184
  let engineId = null;
158
185
  let running = false;
159
186
  let haveSubmittedDrawlist = false;
160
187
  let pendingFrame = null;
161
188
  let lastConsumedSabPublishedSeq = 0;
162
189
  let frameTransport = Object.freeze({ kind: FRAME_TRANSPORT_TRANSFER_V1 });
190
+ const frameAudit = createFrameAuditLogger("worker");
191
+ const frameAuditBySeq = new Map();
192
+ let nativeFrameAuditEnabled = false;
193
+ let nativeFrameAuditNextRecordId = 1n;
163
194
  let eventPool = [];
164
195
  let discardBuffer = null;
165
196
  let droppedSinceLast = 0;
@@ -170,6 +201,284 @@ let idleDelayMs = 0;
170
201
  let maxIdleDelayMs = 0;
171
202
  let sabWakeArmed = false;
172
203
  let sabWakeEpoch = 0;
204
+ function u64FromView(v, offset) {
205
+ const lo = BigInt(v.getUint32(offset, true));
206
+ const hi = BigInt(v.getUint32(offset + 4, true));
207
+ return (hi << 32n) | lo;
208
+ }
209
+ function setFrameAuditMeta(frameSeq, patch) {
210
+ if (!frameAudit.enabled)
211
+ return;
212
+ const prev = frameAuditBySeq.get(frameSeq);
213
+ const next = {
214
+ frameSeq,
215
+ enqueuedAtMs: prev?.enqueuedAtMs ?? Date.now(),
216
+ transport: prev?.transport ?? FRAME_TRANSPORT_TRANSFER_V1,
217
+ byteLen: prev?.byteLen ?? 0,
218
+ ...(prev ?? {}),
219
+ ...patch,
220
+ };
221
+ frameAuditBySeq.set(frameSeq, next);
222
+ }
223
+ function emitFrameAudit(stage, frameSeq, fields = {}) {
224
+ if (!frameAudit.enabled)
225
+ return;
226
+ const meta = frameAuditBySeq.get(frameSeq);
227
+ frameAudit.emit(stage, {
228
+ frameSeq,
229
+ ageMs: meta ? Math.max(0, Date.now() - meta.enqueuedAtMs) : null,
230
+ ...(meta ?? {}),
231
+ ...fields,
232
+ });
233
+ }
234
+ function deleteFrameAudit(frameSeq) {
235
+ if (!frameAudit.enabled)
236
+ return;
237
+ frameAuditBySeq.delete(frameSeq);
238
+ }
239
+ function maybeEnableNativeFrameAudit() {
240
+ if (!frameAudit.enabled)
241
+ return;
242
+ if (!FRAME_AUDIT_NATIVE_ENABLED)
243
+ return;
244
+ if (engineId === null)
245
+ return;
246
+ let rc = -1;
247
+ try {
248
+ rc = native.engineDebugEnable(engineId, {
249
+ enabled: true,
250
+ ringCapacity: FRAME_AUDIT_NATIVE_RING_BYTES,
251
+ minSeverity: 0,
252
+ categoryMask: NATIVE_FRAME_AUDIT_CATEGORY_MASK,
253
+ captureRawEvents: false,
254
+ captureDrawlistBytes: true,
255
+ });
256
+ }
257
+ catch (err) {
258
+ frameAudit.emit("native.debug.enable_error", { detail: safeDetail(err) });
259
+ nativeFrameAuditEnabled = false;
260
+ return;
261
+ }
262
+ nativeFrameAuditEnabled = rc >= 0;
263
+ nativeFrameAuditNextRecordId = 1n;
264
+ frameAudit.emit("native.debug.enable", {
265
+ rc,
266
+ enabled: nativeFrameAuditEnabled,
267
+ ringCapacity: FRAME_AUDIT_NATIVE_RING_BYTES,
268
+ });
269
+ }
270
+ function drainNativeFrameAudit(reason) {
271
+ if (!frameAudit.enabled || !nativeFrameAuditEnabled)
272
+ return;
273
+ if (engineId === null)
274
+ return;
275
+ const headersCap = DEBUG_HEADER_BYTES * 64;
276
+ const headersBuf = new Uint8Array(headersCap);
277
+ const tryReadDebugPayload = (recordId, code, expectedBytes, opts = {}) => {
278
+ if (engineId === null)
279
+ return null;
280
+ const payload = new Uint8Array(expectedBytes);
281
+ let wrote = 0;
282
+ try {
283
+ wrote = native.engineDebugGetPayload(engineId, recordId, payload);
284
+ }
285
+ catch (err) {
286
+ frameAudit.emit("native.debug.payload_error", {
287
+ reason,
288
+ recordId: recordId.toString(),
289
+ code,
290
+ expectedBytes,
291
+ detail: safeDetail(err),
292
+ });
293
+ return null;
294
+ }
295
+ if (wrote <= 0) {
296
+ frameAudit.emit("native.debug.payload_error", {
297
+ reason,
298
+ recordId: recordId.toString(),
299
+ code,
300
+ expectedBytes,
301
+ wrote,
302
+ detail: "payload read returned non-positive length",
303
+ });
304
+ return null;
305
+ }
306
+ if (wrote < expectedBytes) {
307
+ frameAudit.emit("native.debug.payload_error", {
308
+ reason,
309
+ recordId: recordId.toString(),
310
+ code,
311
+ expectedBytes,
312
+ wrote,
313
+ detail: "short payload read",
314
+ });
315
+ if (opts.allowPartialOnShortRead !== true) {
316
+ return null;
317
+ }
318
+ }
319
+ return payload.subarray(0, Math.min(wrote, payload.byteLength));
320
+ };
321
+ for (let iter = 0; iter < 8; iter++) {
322
+ let result;
323
+ try {
324
+ result = native.engineDebugQuery(engineId, {
325
+ minRecordId: nativeFrameAuditNextRecordId,
326
+ categoryMask: NATIVE_FRAME_AUDIT_CATEGORY_MASK,
327
+ minSeverity: 0,
328
+ maxRecords: Math.floor(headersCap / DEBUG_HEADER_BYTES),
329
+ }, headersBuf);
330
+ }
331
+ catch (err) {
332
+ frameAudit.emit("native.debug.query_error", { reason, detail: safeDetail(err) });
333
+ return;
334
+ }
335
+ const recordsReturned = Number.isInteger(result.recordsReturned) && result.recordsReturned > 0
336
+ ? Math.min(result.recordsReturned, Math.floor(headersCap / DEBUG_HEADER_BYTES))
337
+ : 0;
338
+ if (recordsReturned <= 0)
339
+ return;
340
+ let advanced = false;
341
+ for (let i = 0; i < recordsReturned; i++) {
342
+ const off = i * DEBUG_HEADER_BYTES;
343
+ const dv = new DataView(headersBuf.buffer, headersBuf.byteOffset + off, DEBUG_HEADER_BYTES);
344
+ const recordId = u64FromView(dv, 0);
345
+ const timestampUs = u64FromView(dv, 8);
346
+ const frameId = u64FromView(dv, 16);
347
+ const category = dv.getUint32(24, true);
348
+ const severity = dv.getUint32(28, true);
349
+ const code = dv.getUint32(32, true);
350
+ const payloadSize = dv.getUint32(36, true);
351
+ if (recordId < nativeFrameAuditNextRecordId) {
352
+ continue;
353
+ }
354
+ if (recordId === 0n && frameId === 0n && category === 0 && code === 0 && payloadSize === 0) {
355
+ continue;
356
+ }
357
+ advanced = true;
358
+ nativeFrameAuditNextRecordId = recordId + 1n;
359
+ frameAudit.emit("native.debug.header", {
360
+ reason,
361
+ recordId: recordId.toString(),
362
+ frameId: frameId.toString(),
363
+ timestampUs: timestampUs.toString(),
364
+ category,
365
+ severity,
366
+ code,
367
+ payloadSize,
368
+ });
369
+ if (code === ZR_DEBUG_CODE_DRAWLIST_CMD && payloadSize > 0) {
370
+ const cap = Math.min(Math.max(payloadSize, 1), 4096);
371
+ const view = tryReadDebugPayload(recordId, code, cap, {
372
+ allowPartialOnShortRead: true,
373
+ });
374
+ if (view === null)
375
+ continue;
376
+ const fp = drawlistFingerprint(view);
377
+ frameAudit.emit("native.drawlist.payload", {
378
+ reason,
379
+ recordId: recordId.toString(),
380
+ frameId: frameId.toString(),
381
+ payloadSize: view.byteLength,
382
+ hash32: fp.hash32,
383
+ prefixHash32: fp.prefixHash32,
384
+ head16: fp.head16,
385
+ tail16: fp.tail16,
386
+ });
387
+ continue;
388
+ }
389
+ if ((code === ZR_DEBUG_CODE_DRAWLIST_VALIDATE || code === ZR_DEBUG_CODE_DRAWLIST_EXECUTE) &&
390
+ payloadSize >= DEBUG_DRAWLIST_RECORD_BYTES) {
391
+ const payload = tryReadDebugPayload(recordId, code, DEBUG_DRAWLIST_RECORD_BYTES);
392
+ if (payload === null)
393
+ continue;
394
+ const dvPayload = new DataView(payload.buffer, payload.byteOffset, DEBUG_DRAWLIST_RECORD_BYTES);
395
+ frameAudit.emit("native.drawlist.summary", {
396
+ reason,
397
+ recordId: recordId.toString(),
398
+ frameId: u64FromView(dvPayload, 0).toString(),
399
+ totalBytes: dvPayload.getUint32(8, true),
400
+ cmdCount: dvPayload.getUint32(12, true),
401
+ version: dvPayload.getUint32(16, true),
402
+ validationResult: dvPayload.getInt32(20, true),
403
+ executionResult: dvPayload.getInt32(24, true),
404
+ clipStackMaxDepth: dvPayload.getUint32(28, true),
405
+ textRuns: dvPayload.getUint32(32, true),
406
+ fillRects: dvPayload.getUint32(36, true),
407
+ });
408
+ continue;
409
+ }
410
+ if ((code === ZR_DEBUG_CODE_FRAME_BEGIN ||
411
+ code === ZR_DEBUG_CODE_FRAME_SUBMIT ||
412
+ code === ZR_DEBUG_CODE_FRAME_PRESENT ||
413
+ code === ZR_DEBUG_CODE_FRAME_RESIZE) &&
414
+ payloadSize >= DEBUG_FRAME_RECORD_BYTES) {
415
+ const payload = tryReadDebugPayload(recordId, code, DEBUG_FRAME_RECORD_BYTES);
416
+ if (payload === null)
417
+ continue;
418
+ const dvPayload = new DataView(payload.buffer, payload.byteOffset, DEBUG_FRAME_RECORD_BYTES);
419
+ frameAudit.emit("native.frame.summary", {
420
+ reason,
421
+ recordId: recordId.toString(),
422
+ frameId: u64FromView(dvPayload, 0).toString(),
423
+ code,
424
+ codeName: nativeFrameCodeName(code),
425
+ cols: dvPayload.getUint32(8, true),
426
+ rows: dvPayload.getUint32(12, true),
427
+ drawlistBytes: dvPayload.getUint32(16, true),
428
+ drawlistCmds: dvPayload.getUint32(20, true),
429
+ diffBytesEmitted: dvPayload.getUint32(24, true),
430
+ dirtyLines: dvPayload.getUint32(28, true),
431
+ dirtyCells: dvPayload.getUint32(32, true),
432
+ damageRects: dvPayload.getUint32(36, true),
433
+ usDrawlist: dvPayload.getUint32(40, true),
434
+ usDiff: dvPayload.getUint32(44, true),
435
+ usWrite: dvPayload.getUint32(48, true),
436
+ });
437
+ continue;
438
+ }
439
+ if (code === ZR_DEBUG_CODE_PERF_TIMING && payloadSize >= DEBUG_PERF_RECORD_BYTES) {
440
+ const payload = tryReadDebugPayload(recordId, code, DEBUG_PERF_RECORD_BYTES);
441
+ if (payload === null)
442
+ continue;
443
+ const dvPayload = new DataView(payload.buffer, payload.byteOffset, DEBUG_PERF_RECORD_BYTES);
444
+ const phase = dvPayload.getUint32(8, true);
445
+ frameAudit.emit("native.perf.timing", {
446
+ reason,
447
+ recordId: recordId.toString(),
448
+ frameId: u64FromView(dvPayload, 0).toString(),
449
+ phase,
450
+ phaseName: nativePerfPhaseName(phase),
451
+ usElapsed: dvPayload.getUint32(12, true),
452
+ bytesProcessed: dvPayload.getUint32(16, true),
453
+ });
454
+ continue;
455
+ }
456
+ if (code === ZR_DEBUG_CODE_PERF_DIFF_PATH && payloadSize >= DEBUG_DIFF_PATH_RECORD_BYTES) {
457
+ const payload = tryReadDebugPayload(recordId, code, DEBUG_DIFF_PATH_RECORD_BYTES);
458
+ if (payload === null)
459
+ continue;
460
+ const dvPayload = new DataView(payload.buffer, payload.byteOffset, DEBUG_DIFF_PATH_RECORD_BYTES);
461
+ frameAudit.emit("native.perf.diffPath", {
462
+ reason,
463
+ recordId: recordId.toString(),
464
+ frameId: u64FromView(dvPayload, 0).toString(),
465
+ sweepFramesTotal: u64FromView(dvPayload, 8).toString(),
466
+ damageFramesTotal: u64FromView(dvPayload, 16).toString(),
467
+ scrollAttemptsTotal: u64FromView(dvPayload, 24).toString(),
468
+ scrollHitsTotal: u64FromView(dvPayload, 32).toString(),
469
+ collisionGuardHitsTotal: u64FromView(dvPayload, 40).toString(),
470
+ pathSweepUsed: dvPayload.getUint8(48),
471
+ pathDamageUsed: dvPayload.getUint8(49),
472
+ scrollOptAttempted: dvPayload.getUint8(50),
473
+ scrollOptHit: dvPayload.getUint8(51),
474
+ collisionGuardHitsLast: dvPayload.getUint32(52, true),
475
+ });
476
+ }
477
+ }
478
+ if (!advanced)
479
+ return;
480
+ }
481
+ }
173
482
  function writeResizeBatchV1(buf, cols, rows) {
174
483
  // Batch header (24) + RESIZE record (32) = 56 bytes.
175
484
  const totalSize = 56;
@@ -314,6 +623,9 @@ function startTickLoop(fpsCap) {
314
623
  armSabFrameWake();
315
624
  }
316
625
  function fatal(where, code, detail) {
626
+ if (frameAudit.enabled) {
627
+ frameAudit.emit("fatal", { where, code, detail });
628
+ }
317
629
  postToMain({ type: "fatal", where, code, detail });
318
630
  }
319
631
  function shutdownComplete() {
@@ -336,6 +648,8 @@ function releasePendingFrame(frame, expectedSabState) {
336
648
  function postFrameStatus(frameSeq, completedResult) {
337
649
  if (!Number.isInteger(frameSeq) || frameSeq <= 0)
338
650
  return;
651
+ emitFrameAudit("frame.completed", frameSeq, { completedResult });
652
+ deleteFrameAudit(frameSeq);
339
653
  postToMain({
340
654
  type: "frameStatus",
341
655
  acceptedSeq: frameSeq,
@@ -347,6 +661,7 @@ function postFrameStatus(frameSeq, completedResult) {
347
661
  function postFrameAccepted(frameSeq) {
348
662
  if (!Number.isInteger(frameSeq) || frameSeq <= 0)
349
663
  return;
664
+ emitFrameAudit("frame.accepted", frameSeq);
350
665
  postToMain({
351
666
  type: "frameStatus",
352
667
  acceptedSeq: frameSeq,
@@ -405,9 +720,22 @@ function syncPendingSabFrameFromMailbox() {
405
720
  if (latest === null)
406
721
  return;
407
722
  if (pendingFrame !== null) {
723
+ emitFrameAudit("frame.overwritten", pendingFrame.frameSeq, { reason: "mailbox-latest-wins" });
724
+ deleteFrameAudit(pendingFrame.frameSeq);
408
725
  releasePendingFrame(pendingFrame, FRAME_SAB_SLOT_STATE_READY);
409
726
  }
410
727
  pendingFrame = latest;
728
+ setFrameAuditMeta(latest.frameSeq, {
729
+ transport: latest.transport,
730
+ byteLen: latest.byteLen,
731
+ slotIndex: latest.slotIndex,
732
+ slotToken: latest.slotToken,
733
+ });
734
+ emitFrameAudit("frame.mailbox.latest", latest.frameSeq, {
735
+ slotIndex: latest.slotIndex,
736
+ slotToken: latest.slotToken,
737
+ byteLen: latest.byteLen,
738
+ });
411
739
  }
412
740
  function destroyEngineBestEffort() {
413
741
  const id = engineId;
@@ -425,9 +753,17 @@ function shutdownNow() {
425
753
  running = false;
426
754
  stopTickLoop();
427
755
  if (pendingFrame !== null) {
756
+ emitFrameAudit("frame.dropped", pendingFrame.frameSeq, { reason: "shutdown" });
757
+ deleteFrameAudit(pendingFrame.frameSeq);
428
758
  releasePendingFrame(pendingFrame, FRAME_SAB_SLOT_STATE_READY);
429
759
  pendingFrame = null;
430
760
  }
761
+ if (frameAudit.enabled) {
762
+ for (const [seq] of frameAuditBySeq.entries()) {
763
+ emitFrameAudit("frame.dropped", seq, { reason: "shutdown_pending" });
764
+ }
765
+ frameAuditBySeq.clear();
766
+ }
431
767
  destroyEngineBestEffort();
432
768
  shutdownComplete();
433
769
  // Let worker thread exit naturally once handles are cleared.
@@ -454,12 +790,32 @@ function tick() {
454
790
  if (pendingFrame !== null) {
455
791
  const f = pendingFrame;
456
792
  pendingFrame = null;
793
+ emitFrameAudit("frame.submit.begin", f.frameSeq, {
794
+ transport: f.transport,
795
+ byteLen: f.byteLen,
796
+ ...(f.transport === FRAME_TRANSPORT_SAB_V1
797
+ ? { slotIndex: f.slotIndex, slotToken: f.slotToken }
798
+ : {}),
799
+ });
457
800
  let res = -1;
458
801
  let sabInUse = false;
459
802
  let staleSabFrame = false;
460
803
  try {
461
804
  if (f.transport === FRAME_TRANSPORT_TRANSFER_V1) {
462
- res = native.engineSubmitDrawlist(engineId, new Uint8Array(f.buf, 0, f.byteLen));
805
+ const view = new Uint8Array(f.buf, 0, f.byteLen);
806
+ if (frameAudit.enabled) {
807
+ const fp = drawlistFingerprint(view);
808
+ setFrameAuditMeta(f.frameSeq, {
809
+ transport: f.transport,
810
+ byteLen: f.byteLen,
811
+ hash32: fp.hash32,
812
+ prefixHash32: fp.prefixHash32,
813
+ cmdCount: fp.cmdCount,
814
+ totalSize: fp.totalSize,
815
+ });
816
+ emitFrameAudit("frame.submit.payload", f.frameSeq, fp);
817
+ }
818
+ res = native.engineSubmitDrawlist(engineId, view);
463
819
  }
464
820
  else {
465
821
  if (frameTransport.kind !== FRAME_TRANSPORT_SAB_V1) {
@@ -487,6 +843,20 @@ function tick() {
487
843
  sabInUse = true;
488
844
  const offset = f.slotIndex * frameTransport.slotBytes;
489
845
  const view = frameTransport.data.subarray(offset, offset + f.byteLen);
846
+ if (frameAudit.enabled) {
847
+ const fp = drawlistFingerprint(view);
848
+ setFrameAuditMeta(f.frameSeq, {
849
+ transport: f.transport,
850
+ byteLen: f.byteLen,
851
+ slotIndex: f.slotIndex,
852
+ slotToken: f.slotToken,
853
+ hash32: fp.hash32,
854
+ prefixHash32: fp.prefixHash32,
855
+ cmdCount: fp.cmdCount,
856
+ totalSize: fp.totalSize,
857
+ });
858
+ emitFrameAudit("frame.submit.payload", f.frameSeq, fp);
859
+ }
490
860
  res = native.engineSubmitDrawlist(engineId, view);
491
861
  }
492
862
  }
@@ -494,7 +864,9 @@ function tick() {
494
864
  }
495
865
  catch (err) {
496
866
  releasePendingFrame(f, sabInUse ? FRAME_SAB_SLOT_STATE_IN_USE : FRAME_SAB_SLOT_STATE_READY);
867
+ emitFrameAudit("frame.submit.throw", f.frameSeq, { detail: safeDetail(err) });
497
868
  postFrameStatus(f.frameSeq, -1);
869
+ drainNativeFrameAudit("submit-throw");
498
870
  fatal("engineSubmitDrawlist", -1, `engine_submit_drawlist threw: ${safeDetail(err)}`);
499
871
  running = false;
500
872
  return;
@@ -503,6 +875,8 @@ function tick() {
503
875
  // This frame was superseded in the shared mailbox before submit.
504
876
  // Keep latest-wins behavior without surfacing a fatal protocol error.
505
877
  didFrameWork = true;
878
+ emitFrameAudit("frame.submit.stale", f.frameSeq, { reason: "slot-token-mismatch" });
879
+ deleteFrameAudit(f.frameSeq);
506
880
  syncPendingSabFrameFromMailbox();
507
881
  // Continue with present/event processing on this tick.
508
882
  }
@@ -511,6 +885,8 @@ function tick() {
511
885
  haveSubmittedDrawlist = haveSubmittedDrawlist || didSubmitDrawlistThisTick;
512
886
  didFrameWork = true;
513
887
  releasePendingFrame(f, FRAME_SAB_SLOT_STATE_IN_USE);
888
+ emitFrameAudit("frame.submit.result", f.frameSeq, { submitResult: res });
889
+ drainNativeFrameAudit("post-submit");
514
890
  if (res < 0) {
515
891
  postFrameStatus(f.frameSeq, res);
516
892
  fatal("engineSubmitDrawlist", res, "engine_submit_drawlist failed");
@@ -533,22 +909,31 @@ function tick() {
533
909
  pres = native.enginePresent(engineId);
534
910
  }
535
911
  catch (err) {
912
+ if (submittedFrameSeq !== null)
913
+ emitFrameAudit("frame.present.throw", submittedFrameSeq, { detail: safeDetail(err) });
536
914
  if (submittedFrameSeq !== null)
537
915
  postFrameStatus(submittedFrameSeq, -1);
916
+ drainNativeFrameAudit("present-throw");
538
917
  fatal("enginePresent", -1, `engine_present threw: ${safeDetail(err)}`);
539
918
  running = false;
540
919
  return;
541
920
  }
542
921
  if (pres < 0) {
922
+ if (submittedFrameSeq !== null)
923
+ emitFrameAudit("frame.present.result", submittedFrameSeq, { presentResult: pres });
543
924
  if (submittedFrameSeq !== null)
544
925
  postFrameStatus(submittedFrameSeq, pres);
926
+ drainNativeFrameAudit("present-failed");
545
927
  fatal("enginePresent", pres, "engine_present failed");
546
928
  running = false;
547
929
  return;
548
930
  }
931
+ if (submittedFrameSeq !== null)
932
+ emitFrameAudit("frame.present.result", submittedFrameSeq, { presentResult: pres });
549
933
  }
550
934
  if (submittedFrameSeq !== null) {
551
935
  postFrameStatus(submittedFrameSeq, 0);
936
+ drainNativeFrameAudit("frame-complete");
552
937
  }
553
938
  // 3) drain events (bounded)
554
939
  const discard = discardBuffer;
@@ -668,6 +1053,9 @@ function onMessage(msg) {
668
1053
  running = true;
669
1054
  pendingFrame = null;
670
1055
  lastConsumedSabPublishedSeq = 0;
1056
+ frameAuditBySeq.clear();
1057
+ nativeFrameAuditEnabled = false;
1058
+ nativeFrameAuditNextRecordId = 1n;
671
1059
  if (frameTransport.kind === FRAME_TRANSPORT_SAB_V1) {
672
1060
  Atomics.store(frameTransport.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SEQ_WORD, 0);
673
1061
  Atomics.store(frameTransport.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SLOT_WORD, 0);
@@ -697,6 +1085,15 @@ function onMessage(msg) {
697
1085
  if (shim === null || shim.length === 0) {
698
1086
  maybeInjectInitialResize(maxEventBytes);
699
1087
  }
1088
+ if (frameAudit.enabled) {
1089
+ frameAudit.emit("engine.ready", {
1090
+ engineId: id,
1091
+ frameTransport: frameTransport.kind,
1092
+ maxEventBytes,
1093
+ fpsCap: parsePositiveInt(msg.config.fpsCap) ?? 60,
1094
+ });
1095
+ }
1096
+ maybeEnableNativeFrameAudit();
700
1097
  postToMain({ type: "ready", engineId: id });
701
1098
  const fpsCap = parsePositiveInt(msg.config.fpsCap) ?? 60;
702
1099
  startTickLoop(fpsCap);
@@ -707,6 +1104,10 @@ function onMessage(msg) {
707
1104
  return;
708
1105
  // latest-wins overwrite for transfer-path fallback.
709
1106
  if (pendingFrame !== null) {
1107
+ emitFrameAudit("frame.overwritten", pendingFrame.frameSeq, {
1108
+ reason: "message-latest-wins",
1109
+ });
1110
+ deleteFrameAudit(pendingFrame.frameSeq);
710
1111
  releasePendingFrame(pendingFrame, FRAME_SAB_SLOT_STATE_READY);
711
1112
  }
712
1113
  const frameTransportTag = msg.transport ?? FRAME_TRANSPORT_TRANSFER_V1;
@@ -742,6 +1143,18 @@ function onMessage(msg) {
742
1143
  slotToken: msg.slotToken,
743
1144
  byteLen: msg.byteLen,
744
1145
  };
1146
+ setFrameAuditMeta(msg.frameSeq, {
1147
+ transport: FRAME_TRANSPORT_SAB_V1,
1148
+ byteLen: msg.byteLen,
1149
+ slotIndex: msg.slotIndex,
1150
+ slotToken: msg.slotToken,
1151
+ });
1152
+ emitFrameAudit("frame.received", msg.frameSeq, {
1153
+ transport: FRAME_TRANSPORT_SAB_V1,
1154
+ byteLen: msg.byteLen,
1155
+ slotIndex: msg.slotIndex,
1156
+ slotToken: msg.slotToken,
1157
+ });
745
1158
  }
746
1159
  else {
747
1160
  if (!(msg.drawlist instanceof ArrayBuffer)) {
@@ -762,6 +1175,21 @@ function onMessage(msg) {
762
1175
  buf: msg.drawlist,
763
1176
  byteLen: msg.byteLen,
764
1177
  };
1178
+ if (frameAudit.enabled) {
1179
+ const fp = drawlistFingerprint(new Uint8Array(msg.drawlist, 0, msg.byteLen));
1180
+ setFrameAuditMeta(msg.frameSeq, {
1181
+ transport: FRAME_TRANSPORT_TRANSFER_V1,
1182
+ byteLen: msg.byteLen,
1183
+ hash32: fp.hash32,
1184
+ prefixHash32: fp.prefixHash32,
1185
+ cmdCount: fp.cmdCount,
1186
+ totalSize: fp.totalSize,
1187
+ });
1188
+ emitFrameAudit("frame.received", msg.frameSeq, {
1189
+ transport: FRAME_TRANSPORT_TRANSFER_V1,
1190
+ ...fp,
1191
+ });
1192
+ }
765
1193
  }
766
1194
  idleDelayMs = tickIntervalMs;
767
1195
  scheduleTickNow();
@@ -870,6 +1298,15 @@ function onMessage(msg) {
870
1298
  fatal("engineDebugEnable", -1, `engine_debug_enable threw: ${safeDetail(err)}`);
871
1299
  return;
872
1300
  }
1301
+ if (frameAudit.enabled) {
1302
+ frameAudit.emit("native.debug.enable.user", {
1303
+ rc,
1304
+ captureDrawlistBytes: msg.config.captureDrawlistBytes ?? false,
1305
+ });
1306
+ }
1307
+ nativeFrameAuditEnabled = rc >= 0 && frameAudit.enabled;
1308
+ if (nativeFrameAuditEnabled)
1309
+ nativeFrameAuditNextRecordId = 1n;
873
1310
  postToMain({ type: "debug:enableResult", result: rc });
874
1311
  return;
875
1312
  }
@@ -884,6 +1321,10 @@ function onMessage(msg) {
884
1321
  fatal("engineDebugDisable", -1, `engine_debug_disable threw: ${safeDetail(err)}`);
885
1322
  return;
886
1323
  }
1324
+ nativeFrameAuditEnabled = false;
1325
+ if (frameAudit.enabled) {
1326
+ frameAudit.emit("native.debug.disable.user", { rc });
1327
+ }
887
1328
  postToMain({ type: "debug:disableResult", result: rc });
888
1329
  return;
889
1330
  }
@@ -1001,6 +1442,10 @@ function onMessage(msg) {
1001
1442
  fatal("engineDebugReset", -1, `engine_debug_reset threw: ${safeDetail(err)}`);
1002
1443
  return;
1003
1444
  }
1445
+ if (frameAudit.enabled && rc >= 0) {
1446
+ nativeFrameAuditNextRecordId = 1n;
1447
+ frameAudit.emit("native.debug.reset", { rc });
1448
+ }
1004
1449
  postToMain({ type: "debug:resetResult", result: rc });
1005
1450
  return;
1006
1451
  }