@vgai/engine 0.5.48 → 0.5.50

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.
@@ -0,0 +1,8 @@
1
+ type PoseReporter = (type: string) => void;
2
+ /** Install once per Three prototype. Later editor installation can add its
3
+ * diagnostic without replacing the runtime wrapper or losing its count. */
4
+ export declare function installAudioPoseGuard(report?: PoseReporter): void;
5
+ /** Non-finite pose updates dropped by this Three instance's guard. */
6
+ export declare function audioPoseUpdatesDropped(): number;
7
+ export {};
8
+ //# sourceMappingURL=pose-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pose-guard.d.ts","sourceRoot":"","sources":["../../src/audio/pose-guard.ts"],"names":[],"mappings":"AAGA,KAAK,YAAY,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AA+D3C;2EAC2E;AAC3E,wBAAgB,qBAAqB,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,IAAI,CAGjE;AAED,sEAAsE;AACtE,wBAAgB,uBAAuB,IAAI,MAAM,CAKhD"}
@@ -0,0 +1,51 @@
1
+ import * as THREE from 'three';
2
+ // The editor bundle and source-served runtime can load this helper separately
3
+ // while sharing Three. Installation belongs to the actual prototype identity.
4
+ const GUARD_STATE = Symbol.for('vgai.audio-pose-guard');
5
+ function poseIsFinite(matrixWorld) {
6
+ return matrixWorld.elements.every(Number.isFinite);
7
+ }
8
+ function guard(prototype, report) {
9
+ const existing = prototype[GUARD_STATE];
10
+ if (existing) {
11
+ if (report)
12
+ existing.report = report;
13
+ return;
14
+ }
15
+ const state = { dropped: 0, ...(report ? { report } : {}) };
16
+ Object.defineProperty(prototype, GUARD_STATE, { value: state });
17
+ const suspendedListeners = new WeakSet();
18
+ const original = prototype.updateMatrixWorld;
19
+ prototype.updateMatrixWorld = function guardedUpdateMatrixWorld(force) {
20
+ // Keep the scene graph current even when Web Audio cannot consume a pose.
21
+ THREE.Object3D.prototype.updateMatrixWorld.call(this, force);
22
+ if (this.context.state !== 'running') {
23
+ if (this._clock) {
24
+ this.timeDelta = 0;
25
+ suspendedListeners.add(this);
26
+ }
27
+ return;
28
+ }
29
+ if (poseIsFinite(this.matrixWorld)) {
30
+ // A hidden tab may have rendered no frames while suspended. Discard
31
+ // that wall-time gap before Three computes its next short pose ramp.
32
+ if (suspendedListeners.delete(this))
33
+ this._clock?.getDelta();
34
+ original.call(this, force);
35
+ return;
36
+ }
37
+ state.dropped++;
38
+ state.report?.(this.type);
39
+ };
40
+ }
41
+ /** Install once per Three prototype. Later editor installation can add its
42
+ * diagnostic without replacing the runtime wrapper or losing its count. */
43
+ export function installAudioPoseGuard(report) {
44
+ guard(THREE.AudioListener.prototype, report);
45
+ guard(THREE.PositionalAudio.prototype, report);
46
+ }
47
+ /** Non-finite pose updates dropped by this Three instance's guard. */
48
+ export function audioPoseUpdatesDropped() {
49
+ return ((THREE.AudioListener.prototype[GUARD_STATE]?.dropped ?? 0) +
50
+ (THREE.PositionalAudio.prototype[GUARD_STATE]?.dropped ?? 0));
51
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"performance-profiler.d.ts","sourceRoot":"","sources":["../../src/dev/performance-profiler.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,yEAAyE;AACzE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;;;OAOG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC9C;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,eAAe,EAAE,CAAC;IAChD;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;CACzC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,QAAQ,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC9C,QAAQ,CAAC,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC/C,QAAQ,CAAC,UAAU,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAClD,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;CACzC;AAUD,qFAAqF;AACrF,wBAAgB,yBAAyB,CAAC,gBAAgB,UAAQ;aAoF3C,OAAO;sBASE,OAAO;;;;;;;mBAkDpB,MAAM;;;yBAqBF,MAAM,SAAS,MAAM,QAAQ,MAAM;;;uBAiBnC,MAAM;wBAWL;QAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB;2DACmD;QACnD,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB;;wBAqCmB,MAAM,IAAI;;;EAejC;AAED,MAAM,MAAM,mBAAmB,GAAG,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
1
+ {"version":3,"file":"performance-profiler.d.ts","sourceRoot":"","sources":["../../src/dev/performance-profiler.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,yEAAyE;AACzE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;;;OAOG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC9C;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,eAAe,EAAE,CAAC;IAChD;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;CACzC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,QAAQ,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC9C,QAAQ,CAAC,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC/C,QAAQ,CAAC,UAAU,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAClD,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;CACzC;AASD,qFAAqF;AACrF,wBAAgB,yBAAyB,CAAC,gBAAgB,UAAQ;aAkG3C,OAAO;sBASE,OAAO;;;;;;;mBAkDpB,MAAM;;;yBAqBF,MAAM,SAAS,MAAM,QAAQ,MAAM;;;uBAiBnC,MAAM;wBAWL;QAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB;2DACmD;QACnD,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB;;wBAqCmB,MAAM,IAAI;;;EAejC;AAED,MAAM,MAAM,mBAAmB,GAAG,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
@@ -1,8 +1,7 @@
1
1
  const now = () => globalThis.performance?.now() ?? Date.now();
2
- function percentile(values, fraction) {
3
- if (values.length === 0)
2
+ function percentile(sorted, fraction) {
3
+ if (sorted.length === 0)
4
4
  return 0;
5
- const sorted = [...values].sort((a, b) => a - b);
6
5
  return sorted[Math.min(sorted.length - 1, Math.floor((sorted.length - 1) * fraction))] ?? 0;
7
6
  }
8
7
  /** Game-owned, bounded external store for browser-native performance diagnostics. */
@@ -58,7 +57,7 @@ export function createPerformanceProfiler(initiallyEnabled = false) {
58
57
  cpuMs: 0,
59
58
  p95Ms: 0,
60
59
  p99Ms: 0,
61
- frames,
60
+ frames: [],
62
61
  phases: [],
63
62
  systems: [],
64
63
  components: [],
@@ -66,15 +65,29 @@ export function createPerformanceProfiler(initiallyEnabled = false) {
66
65
  };
67
66
  function publish() {
68
67
  const latest = frames.at(-1);
69
- const intervals = frames.map((frame) => frame.intervalMs).filter((value) => value > 0);
68
+ // Frame-only subscribers need no percentile work. Keep this window stable
69
+ // so a retained snapshot's first percentile read still describes its frames.
70
+ const snapshotFrames = frames.slice();
71
+ let sortedIntervals;
72
+ const readPercentile = (fraction) => {
73
+ sortedIntervals ??= snapshotFrames
74
+ .map((frame) => frame.intervalMs)
75
+ .filter((value) => value > 0)
76
+ .sort((a, b) => a - b);
77
+ return percentile(sortedIntervals, fraction);
78
+ };
70
79
  snapshot = {
71
80
  enabled,
72
81
  recording,
73
82
  fps: latest?.intervalMs ? 1000 / latest.intervalMs : 0,
74
83
  cpuMs: latest?.cpuMs ?? 0,
75
- p95Ms: percentile(intervals, 0.95),
76
- p99Ms: percentile(intervals, 0.99),
77
- frames,
84
+ get p95Ms() {
85
+ return readPercentile(0.95);
86
+ },
87
+ get p99Ms() {
88
+ return readPercentile(0.99);
89
+ },
90
+ frames: snapshotFrames,
78
91
  phases: latest?.phases ?? [],
79
92
  systems: [...currentSystems].map(([name, ms]) => ({ name, ms })).sort((a, b) => b.ms - a.ms),
80
93
  components: [...currentComponents]
@@ -1 +1 @@
1
- {"version":3,"file":"setup-audio.d.ts","sourceRoot":"","sources":["../../src/setup/setup-audio.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,KAAK,aAAa,EAAuB,MAAM,oBAAoB,CAAC;AAE7E,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,KAAK,CAAC,aAAa,CAAC;IAC9B,UAAU,EAAE,QAAQ,CAAC;IACrB,KAAK,EAAE;QACL,KAAK,EAAE,QAAQ,CAAC;QAChB,GAAG,EAAE,QAAQ,CAAC;QACd,KAAK,EAAE,QAAQ,CAAC;KACjB,CAAC;IACF;;;;;;;;;OASG;IACH,KAAK,EAAE,aAAa,CAAC;IACrB,8EAA8E;IAC9E,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAKD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,YAAY,CAkC7D"}
1
+ {"version":3,"file":"setup-audio.d.ts","sourceRoot":"","sources":["../../src/setup/setup-audio.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,KAAK,aAAa,EAAuB,MAAM,oBAAoB,CAAC;AAG7E,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,KAAK,CAAC,aAAa,CAAC;IAC9B,UAAU,EAAE,QAAQ,CAAC;IACrB,KAAK,EAAE;QACL,KAAK,EAAE,QAAQ,CAAC;QAChB,GAAG,EAAE,QAAQ,CAAC;QACd,KAAK,EAAE,QAAQ,CAAC;KACjB,CAAC;IACF;;;;;;;;;OASG;IACH,KAAK,EAAE,aAAa,CAAC;IACrB,8EAA8E;IAC9E,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAKD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,YAAY,CAmC7D"}
@@ -1,5 +1,6 @@
1
1
  import * as THREE from 'three';
2
2
  import { createAudioBusMixer } from '../audio/bus-mixer';
3
+ import { installAudioPoseGuard } from '../audio/pose-guard';
3
4
  /** The bus every other bus routes into, and the node `masterGain` is. */
4
5
  const MASTER_BUS = 'master';
5
6
  /**
@@ -9,6 +10,7 @@ const MASTER_BUS = 'master';
9
10
  * Attach `listener` to the active camera: `camera.add(audioCtx.listener)`.
10
11
  */
11
12
  export function setupAudio(camera) {
13
+ installAudioPoseGuard();
12
14
  const listener = new THREE.AudioListener();
13
15
  camera.add(listener);
14
16
  const ctx = listener.context;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@vgai/engine",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.5.48",
5
+ "version": "0.5.50",
6
6
  "description": "Readable TypeScript game engine and universal host for Three.js, PixiJS, and React games.",
7
7
  "keywords": [
8
8
  "game-engine",
@@ -0,0 +1,80 @@
1
+ import * as THREE from 'three';
2
+
3
+ type UpdateMatrixWorld = (force?: boolean) => void;
4
+ type PoseReporter = (type: string) => void;
5
+ interface AudioNode3D {
6
+ matrixWorld: THREE.Matrix4;
7
+ type: string;
8
+ context: { readonly state: AudioContextState };
9
+ // Three's AudioListener owns this private clock; PositionalAudio reads its
10
+ // listener's public timeDelta. Only the verified getDelta method is needed.
11
+ _clock?: Pick<THREE.Clock, 'getDelta'>;
12
+ timeDelta?: number;
13
+ }
14
+
15
+ interface GuardState {
16
+ dropped: number;
17
+ report?: PoseReporter;
18
+ }
19
+
20
+ // The editor bundle and source-served runtime can load this helper separately
21
+ // while sharing Three. Installation belongs to the actual prototype identity.
22
+ const GUARD_STATE = Symbol.for('vgai.audio-pose-guard');
23
+ type AudioPrototype = {
24
+ updateMatrixWorld: UpdateMatrixWorld;
25
+ [GUARD_STATE]?: GuardState;
26
+ };
27
+
28
+ function poseIsFinite(matrixWorld: THREE.Matrix4): boolean {
29
+ return matrixWorld.elements.every(Number.isFinite);
30
+ }
31
+
32
+ function guard(prototype: AudioPrototype, report?: PoseReporter): void {
33
+ const existing = prototype[GUARD_STATE];
34
+ if (existing) {
35
+ if (report) existing.report = report;
36
+ return;
37
+ }
38
+ const state: GuardState = { dropped: 0, ...(report ? { report } : {}) };
39
+ Object.defineProperty(prototype, GUARD_STATE, { value: state });
40
+ const suspendedListeners = new WeakSet<AudioNode3D>();
41
+ const original = prototype.updateMatrixWorld;
42
+ prototype.updateMatrixWorld = function guardedUpdateMatrixWorld(
43
+ this: AudioNode3D,
44
+ force?: boolean,
45
+ ): void {
46
+ // Keep the scene graph current even when Web Audio cannot consume a pose.
47
+ THREE.Object3D.prototype.updateMatrixWorld.call(this as unknown as THREE.Object3D, force);
48
+ if (this.context.state !== 'running') {
49
+ if (this._clock) {
50
+ this.timeDelta = 0;
51
+ suspendedListeners.add(this);
52
+ }
53
+ return;
54
+ }
55
+ if (poseIsFinite(this.matrixWorld)) {
56
+ // A hidden tab may have rendered no frames while suspended. Discard
57
+ // that wall-time gap before Three computes its next short pose ramp.
58
+ if (suspendedListeners.delete(this)) this._clock?.getDelta();
59
+ original.call(this, force);
60
+ return;
61
+ }
62
+ state.dropped++;
63
+ state.report?.(this.type);
64
+ };
65
+ }
66
+
67
+ /** Install once per Three prototype. Later editor installation can add its
68
+ * diagnostic without replacing the runtime wrapper or losing its count. */
69
+ export function installAudioPoseGuard(report?: PoseReporter): void {
70
+ guard(THREE.AudioListener.prototype as unknown as AudioPrototype, report);
71
+ guard(THREE.PositionalAudio.prototype as unknown as AudioPrototype, report);
72
+ }
73
+
74
+ /** Non-finite pose updates dropped by this Three instance's guard. */
75
+ export function audioPoseUpdatesDropped(): number {
76
+ return (
77
+ ((THREE.AudioListener.prototype as unknown as AudioPrototype)[GUARD_STATE]?.dropped ?? 0) +
78
+ ((THREE.PositionalAudio.prototype as unknown as AudioPrototype)[GUARD_STATE]?.dropped ?? 0)
79
+ );
80
+ }
@@ -89,9 +89,8 @@ export interface PerformanceSnapshot {
89
89
 
90
90
  const now = () => globalThis.performance?.now() ?? Date.now();
91
91
 
92
- function percentile(values: readonly number[], fraction: number): number {
93
- if (values.length === 0) return 0;
94
- const sorted = [...values].sort((a, b) => a - b);
92
+ function percentile(sorted: readonly number[], fraction: number): number {
93
+ if (sorted.length === 0) return 0;
95
94
  return sorted[Math.min(sorted.length - 1, Math.floor((sorted.length - 1) * fraction))] ?? 0;
96
95
  }
97
96
 
@@ -148,7 +147,7 @@ export function createPerformanceProfiler(initiallyEnabled = false) {
148
147
  cpuMs: 0,
149
148
  p95Ms: 0,
150
149
  p99Ms: 0,
151
- frames,
150
+ frames: [],
152
151
  phases: [],
153
152
  systems: [],
154
153
  components: [],
@@ -157,15 +156,29 @@ export function createPerformanceProfiler(initiallyEnabled = false) {
157
156
 
158
157
  function publish(): void {
159
158
  const latest = frames.at(-1);
160
- const intervals = frames.map((frame) => frame.intervalMs).filter((value) => value > 0);
159
+ // Frame-only subscribers need no percentile work. Keep this window stable
160
+ // so a retained snapshot's first percentile read still describes its frames.
161
+ const snapshotFrames = frames.slice();
162
+ let sortedIntervals: number[] | undefined;
163
+ const readPercentile = (fraction: number): number => {
164
+ sortedIntervals ??= snapshotFrames
165
+ .map((frame) => frame.intervalMs)
166
+ .filter((value) => value > 0)
167
+ .sort((a, b) => a - b);
168
+ return percentile(sortedIntervals, fraction);
169
+ };
161
170
  snapshot = {
162
171
  enabled,
163
172
  recording,
164
173
  fps: latest?.intervalMs ? 1000 / latest.intervalMs : 0,
165
174
  cpuMs: latest?.cpuMs ?? 0,
166
- p95Ms: percentile(intervals, 0.95),
167
- p99Ms: percentile(intervals, 0.99),
168
- frames,
175
+ get p95Ms() {
176
+ return readPercentile(0.95);
177
+ },
178
+ get p99Ms() {
179
+ return readPercentile(0.99);
180
+ },
181
+ frames: snapshotFrames,
169
182
  phases: latest?.phases ?? [],
170
183
  systems: [...currentSystems].map(([name, ms]) => ({ name, ms })).sort((a, b) => b.ms - a.ms),
171
184
  components: [...currentComponents]
@@ -1,5 +1,6 @@
1
1
  import * as THREE from 'three';
2
2
  import { type AudioBusMixer, createAudioBusMixer } from '../audio/bus-mixer';
3
+ import { installAudioPoseGuard } from '../audio/pose-guard';
3
4
 
4
5
  export interface AudioContext {
5
6
  listener: THREE.AudioListener;
@@ -34,6 +35,7 @@ const MASTER_BUS = 'master';
34
35
  * Attach `listener` to the active camera: `camera.add(audioCtx.listener)`.
35
36
  */
36
37
  export function setupAudio(camera: THREE.Camera): AudioContext {
38
+ installAudioPoseGuard();
37
39
  const listener = new THREE.AudioListener();
38
40
  camera.add(listener);
39
41