@vgai/engine 0.5.49 → 0.5.51
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/dist/audio/pose-guard.d.ts +8 -0
- package/dist/audio/pose-guard.d.ts.map +1 -0
- package/dist/audio/pose-guard.js +51 -0
- package/dist/dev/performance-profiler.d.ts.map +1 -1
- package/dist/dev/performance-profiler.js +21 -8
- package/dist/setup/setup-audio.d.ts.map +1 -1
- package/dist/setup/setup-audio.js +2 -0
- package/package.json +1 -1
- package/src/audio/pose-guard.ts +80 -0
- package/src/dev/performance-profiler.ts +21 -8
- package/src/setup/setup-audio.ts +2 -0
|
@@ -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;
|
|
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(
|
|
3
|
-
if (
|
|
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
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
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;
|
|
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.
|
|
5
|
+
"version": "0.5.51",
|
|
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(
|
|
93
|
-
if (
|
|
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
|
-
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
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]
|
package/src/setup/setup-audio.ts
CHANGED
|
@@ -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
|
|