@oyna360/lobby-sdk 0.1.0

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 (211) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/dist/avatar-accessories.d.ts +16 -0
  4. package/dist/avatar-accessories.d.ts.map +1 -0
  5. package/dist/avatar-accessories.js +214 -0
  6. package/dist/avatar-accessories.js.map +1 -0
  7. package/dist/avatar-asset-manager.d.ts +54 -0
  8. package/dist/avatar-asset-manager.d.ts.map +1 -0
  9. package/dist/avatar-asset-manager.js +215 -0
  10. package/dist/avatar-asset-manager.js.map +1 -0
  11. package/dist/avatar-clips.d.ts +17 -0
  12. package/dist/avatar-clips.d.ts.map +1 -0
  13. package/dist/avatar-clips.js +47 -0
  14. package/dist/avatar-clips.js.map +1 -0
  15. package/dist/avatar-config.d.ts +4 -0
  16. package/dist/avatar-config.d.ts.map +1 -0
  17. package/dist/avatar-config.js +9 -0
  18. package/dist/avatar-config.js.map +1 -0
  19. package/dist/avatar-factory.d.ts +64 -0
  20. package/dist/avatar-factory.d.ts.map +1 -0
  21. package/dist/avatar-factory.js +464 -0
  22. package/dist/avatar-factory.js.map +1 -0
  23. package/dist/avatar-instance.d.ts +31 -0
  24. package/dist/avatar-instance.d.ts.map +1 -0
  25. package/dist/avatar-instance.js +56 -0
  26. package/dist/avatar-instance.js.map +1 -0
  27. package/dist/avatar-material-pipeline.d.ts +34 -0
  28. package/dist/avatar-material-pipeline.d.ts.map +1 -0
  29. package/dist/avatar-material-pipeline.js +580 -0
  30. package/dist/avatar-material-pipeline.js.map +1 -0
  31. package/dist/avatar-rig-builder.d.ts +74 -0
  32. package/dist/avatar-rig-builder.d.ts.map +1 -0
  33. package/dist/avatar-rig-builder.js +347 -0
  34. package/dist/avatar-rig-builder.js.map +1 -0
  35. package/dist/draco.d.ts +7 -0
  36. package/dist/draco.d.ts.map +1 -0
  37. package/dist/draco.js +82 -0
  38. package/dist/draco.js.map +1 -0
  39. package/dist/humanoid-animator.d.ts +35 -0
  40. package/dist/humanoid-animator.d.ts.map +1 -0
  41. package/dist/humanoid-animator.js +191 -0
  42. package/dist/humanoid-animator.js.map +1 -0
  43. package/dist/humanoid-bone-names.d.ts +35 -0
  44. package/dist/humanoid-bone-names.d.ts.map +1 -0
  45. package/dist/humanoid-bone-names.js +126 -0
  46. package/dist/humanoid-bone-names.js.map +1 -0
  47. package/dist/humanoid-locomotion.d.ts +101 -0
  48. package/dist/humanoid-locomotion.d.ts.map +1 -0
  49. package/dist/humanoid-locomotion.js +300 -0
  50. package/dist/humanoid-locomotion.js.map +1 -0
  51. package/dist/humanoid-rig.d.ts +51 -0
  52. package/dist/humanoid-rig.d.ts.map +1 -0
  53. package/dist/humanoid-rig.js +304 -0
  54. package/dist/humanoid-rig.js.map +1 -0
  55. package/dist/index.d.ts +60 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +42 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/lobby-chat-ui.d.ts +3 -0
  60. package/dist/lobby-chat-ui.d.ts.map +1 -0
  61. package/dist/lobby-chat-ui.js +258 -0
  62. package/dist/lobby-chat-ui.js.map +1 -0
  63. package/dist/lobby-colliders.d.ts +10 -0
  64. package/dist/lobby-colliders.d.ts.map +1 -0
  65. package/dist/lobby-colliders.js +225 -0
  66. package/dist/lobby-colliders.js.map +1 -0
  67. package/dist/lobby-connection-ui.d.ts +8 -0
  68. package/dist/lobby-connection-ui.d.ts.map +1 -0
  69. package/dist/lobby-connection-ui.js +68 -0
  70. package/dist/lobby-connection-ui.js.map +1 -0
  71. package/dist/lobby-debug.d.ts +50 -0
  72. package/dist/lobby-debug.d.ts.map +1 -0
  73. package/dist/lobby-debug.js +132 -0
  74. package/dist/lobby-debug.js.map +1 -0
  75. package/dist/lobby-font.d.ts +5 -0
  76. package/dist/lobby-font.d.ts.map +1 -0
  77. package/dist/lobby-font.js +35 -0
  78. package/dist/lobby-font.js.map +1 -0
  79. package/dist/lobby-music.d.ts +40 -0
  80. package/dist/lobby-music.d.ts.map +1 -0
  81. package/dist/lobby-music.js +250 -0
  82. package/dist/lobby-music.js.map +1 -0
  83. package/dist/lobby-orientation-ui.d.ts +10 -0
  84. package/dist/lobby-orientation-ui.d.ts.map +1 -0
  85. package/dist/lobby-orientation-ui.js +64 -0
  86. package/dist/lobby-orientation-ui.js.map +1 -0
  87. package/dist/lobby-perf-diag.d.ts +254 -0
  88. package/dist/lobby-perf-diag.d.ts.map +1 -0
  89. package/dist/lobby-perf-diag.js +775 -0
  90. package/dist/lobby-perf-diag.js.map +1 -0
  91. package/dist/lobby-plaza-diag.d.ts +103 -0
  92. package/dist/lobby-plaza-diag.d.ts.map +1 -0
  93. package/dist/lobby-plaza-diag.js +534 -0
  94. package/dist/lobby-plaza-diag.js.map +1 -0
  95. package/dist/lobby-presence-ui.d.ts +4 -0
  96. package/dist/lobby-presence-ui.d.ts.map +1 -0
  97. package/dist/lobby-presence-ui.js +89 -0
  98. package/dist/lobby-presence-ui.js.map +1 -0
  99. package/dist/lobby-real-device-diag.d.ts +119 -0
  100. package/dist/lobby-real-device-diag.d.ts.map +1 -0
  101. package/dist/lobby-real-device-diag.js +468 -0
  102. package/dist/lobby-real-device-diag.js.map +1 -0
  103. package/dist/lobby-spawn.d.ts +15 -0
  104. package/dist/lobby-spawn.d.ts.map +1 -0
  105. package/dist/lobby-spawn.js +18 -0
  106. package/dist/lobby-spawn.js.map +1 -0
  107. package/dist/lobby-ui-i18n.d.ts +11 -0
  108. package/dist/lobby-ui-i18n.d.ts.map +1 -0
  109. package/dist/lobby-ui-i18n.js +21 -0
  110. package/dist/lobby-ui-i18n.js.map +1 -0
  111. package/dist/lobby-voice-ui.d.ts +3 -0
  112. package/dist/lobby-voice-ui.d.ts.map +1 -0
  113. package/dist/lobby-voice-ui.js +105 -0
  114. package/dist/lobby-voice-ui.js.map +1 -0
  115. package/dist/local-player-controller.d.ts +80 -0
  116. package/dist/local-player-controller.d.ts.map +1 -0
  117. package/dist/local-player-controller.js +422 -0
  118. package/dist/local-player-controller.js.map +1 -0
  119. package/dist/name-tag.d.ts +3 -0
  120. package/dist/name-tag.d.ts.map +1 -0
  121. package/dist/name-tag.js +59 -0
  122. package/dist/name-tag.js.map +1 -0
  123. package/dist/network-client.d.ts +87 -0
  124. package/dist/network-client.d.ts.map +1 -0
  125. package/dist/network-client.js +182 -0
  126. package/dist/network-client.js.map +1 -0
  127. package/dist/platform-bridge.d.ts +17 -0
  128. package/dist/platform-bridge.d.ts.map +1 -0
  129. package/dist/platform-bridge.js +90 -0
  130. package/dist/platform-bridge.js.map +1 -0
  131. package/dist/platform-lobby.d.ts +176 -0
  132. package/dist/platform-lobby.d.ts.map +1 -0
  133. package/dist/platform-lobby.js +679 -0
  134. package/dist/platform-lobby.js.map +1 -0
  135. package/dist/platform-types.d.ts +56 -0
  136. package/dist/platform-types.d.ts.map +1 -0
  137. package/dist/platform-types.js +3 -0
  138. package/dist/platform-types.js.map +1 -0
  139. package/dist/player-label.d.ts +9 -0
  140. package/dist/player-label.d.ts.map +1 -0
  141. package/dist/player-label.js +14 -0
  142. package/dist/player-label.js.map +1 -0
  143. package/dist/playground/playground-layout.d.ts +14 -0
  144. package/dist/playground/playground-layout.d.ts.map +1 -0
  145. package/dist/playground/playground-layout.js +320 -0
  146. package/dist/playground/playground-layout.js.map +1 -0
  147. package/dist/playground/playground-system.d.ts +30 -0
  148. package/dist/playground/playground-system.d.ts.map +1 -0
  149. package/dist/playground/playground-system.js +202 -0
  150. package/dist/playground/playground-system.js.map +1 -0
  151. package/dist/plaza-layout.d.ts +30 -0
  152. package/dist/plaza-layout.d.ts.map +1 -0
  153. package/dist/plaza-layout.js +527 -0
  154. package/dist/plaza-layout.js.map +1 -0
  155. package/dist/plugins.d.ts +5 -0
  156. package/dist/plugins.d.ts.map +1 -0
  157. package/dist/plugins.js +26 -0
  158. package/dist/plugins.js.map +1 -0
  159. package/dist/protocol.d.ts +276 -0
  160. package/dist/protocol.d.ts.map +1 -0
  161. package/dist/protocol.js +107 -0
  162. package/dist/protocol.js.map +1 -0
  163. package/dist/quality.d.ts +39 -0
  164. package/dist/quality.d.ts.map +1 -0
  165. package/dist/quality.js +95 -0
  166. package/dist/quality.js.map +1 -0
  167. package/dist/remote-player-manager.d.ts +48 -0
  168. package/dist/remote-player-manager.d.ts.map +1 -0
  169. package/dist/remote-player-manager.js +339 -0
  170. package/dist/remote-player-manager.js.map +1 -0
  171. package/dist/scene-manager.d.ts +26 -0
  172. package/dist/scene-manager.d.ts.map +1 -0
  173. package/dist/scene-manager.js +169 -0
  174. package/dist/scene-manager.js.map +1 -0
  175. package/dist/spawn-utils.d.ts +8 -0
  176. package/dist/spawn-utils.d.ts.map +1 -0
  177. package/dist/spawn-utils.js +29 -0
  178. package/dist/spawn-utils.js.map +1 -0
  179. package/dist/starter-layout.d.ts +17 -0
  180. package/dist/starter-layout.d.ts.map +1 -0
  181. package/dist/starter-layout.js +102 -0
  182. package/dist/starter-layout.js.map +1 -0
  183. package/dist/third-person-camera.d.ts +35 -0
  184. package/dist/third-person-camera.d.ts.map +1 -0
  185. package/dist/third-person-camera.js +181 -0
  186. package/dist/third-person-camera.js.map +1 -0
  187. package/dist/types.d.ts +164 -0
  188. package/dist/types.d.ts.map +1 -0
  189. package/dist/types.js +2 -0
  190. package/dist/types.js.map +1 -0
  191. package/dist/voice-chat.d.ts +78 -0
  192. package/dist/voice-chat.d.ts.map +1 -0
  193. package/dist/voice-chat.js +321 -0
  194. package/dist/voice-chat.js.map +1 -0
  195. package/dist/yield-to-render.d.ts +3 -0
  196. package/dist/yield-to-render.d.ts.map +1 -0
  197. package/dist/yield-to-render.js +11 -0
  198. package/dist/yield-to-render.js.map +1 -0
  199. package/dist/zones.d.ts +36 -0
  200. package/dist/zones.d.ts.map +1 -0
  201. package/dist/zones.js +68 -0
  202. package/dist/zones.js.map +1 -0
  203. package/docs/00-overview.md +47 -0
  204. package/docs/01-connect.md +85 -0
  205. package/docs/02-github.md +43 -0
  206. package/docs/03-modes.md +70 -0
  207. package/docs/04-api.md +106 -0
  208. package/docs/05-features.md +55 -0
  209. package/docs/06-controls.md +34 -0
  210. package/docs/07-troubleshooting.md +39 -0
  211. package/package.json +65 -0
@@ -0,0 +1,775 @@
1
+ const phaseStats = new Map();
2
+ const phaseSamples = new Map();
3
+ const recentMarks = [];
4
+ const MAX_MARKS = 200;
5
+ const MAX_PHASE_SAMPLES = 800;
6
+ let enabled = false;
7
+ let drawCallsApprox = 0;
8
+ let lastFrameStart = 0;
9
+ const frameTimes = [];
10
+ const MAX_FRAME_TIMES = 480;
11
+ let remoteUpdateMsAccum = 0;
12
+ let remoteUpdateCount = 0;
13
+ let localUpdateMsAccum = 0;
14
+ let localUpdateCount = 0;
15
+ export function isLobbyPerfDiagEnabled() {
16
+ return enabled;
17
+ }
18
+ export function setLobbyPerfDiagEnabled(on) {
19
+ enabled = on;
20
+ }
21
+ export function resetLobbyPerfDiag(opts) {
22
+ if (!opts?.keepPhases) {
23
+ phaseStats.clear();
24
+ phaseSamples.clear();
25
+ recentMarks.length = 0;
26
+ }
27
+ frameTimes.length = 0;
28
+ drawCallsApprox = 0;
29
+ remoteUpdateMsAccum = 0;
30
+ remoteUpdateCount = 0;
31
+ localUpdateMsAccum = 0;
32
+ localUpdateCount = 0;
33
+ }
34
+ function bumpPhase(phase, ms) {
35
+ const cur = phaseStats.get(phase) ?? { count: 0, totalMs: 0, maxMs: 0, lastMs: 0 };
36
+ cur.count += 1;
37
+ cur.totalMs += ms;
38
+ cur.lastMs = ms;
39
+ if (ms > cur.maxMs)
40
+ cur.maxMs = ms;
41
+ phaseStats.set(phase, cur);
42
+ let samples = phaseSamples.get(phase);
43
+ if (!samples) {
44
+ samples = [];
45
+ phaseSamples.set(phase, samples);
46
+ }
47
+ samples.push(ms);
48
+ if (samples.length > MAX_PHASE_SAMPLES)
49
+ samples.shift();
50
+ recentMarks.push({ phase, ms, at: performance.now() });
51
+ if (recentMarks.length > MAX_MARKS)
52
+ recentMarks.shift();
53
+ }
54
+ /** Time a sync/async section when diag is enabled. Zero overhead when disabled. */
55
+ export async function lobbyPerfTimeAsync(phase, fn, detail) {
56
+ if (!enabled)
57
+ return fn();
58
+ const t0 = performance.now();
59
+ try {
60
+ return await fn();
61
+ }
62
+ finally {
63
+ const ms = performance.now() - t0;
64
+ bumpPhase(phase, ms);
65
+ if (detail) {
66
+ const last = recentMarks[recentMarks.length - 1];
67
+ if (last)
68
+ last.detail = detail;
69
+ }
70
+ }
71
+ }
72
+ export function lobbyPerfTimeSync(phase, fn, detail) {
73
+ if (!enabled)
74
+ return fn();
75
+ const t0 = performance.now();
76
+ try {
77
+ return fn();
78
+ }
79
+ finally {
80
+ const ms = performance.now() - t0;
81
+ bumpPhase(phase, ms);
82
+ if (detail) {
83
+ const last = recentMarks[recentMarks.length - 1];
84
+ if (last)
85
+ last.detail = detail;
86
+ }
87
+ }
88
+ }
89
+ export function lobbyPerfMark(phase, ms, detail) {
90
+ if (!enabled)
91
+ return;
92
+ bumpPhase(phase, ms);
93
+ if (detail) {
94
+ const last = recentMarks[recentMarks.length - 1];
95
+ if (last)
96
+ last.detail = detail;
97
+ }
98
+ }
99
+ export function lobbyPerfNoteRemoteUpdate(ms) {
100
+ if (!enabled)
101
+ return;
102
+ remoteUpdateMsAccum += ms;
103
+ remoteUpdateCount += 1;
104
+ bumpPhase('remote_update', ms);
105
+ }
106
+ export function lobbyPerfNoteLocalUpdate(ms) {
107
+ if (!enabled)
108
+ return;
109
+ localUpdateMsAccum += ms;
110
+ localUpdateCount += 1;
111
+ bumpPhase('local_update', ms);
112
+ }
113
+ export function lobbyPerfBeginFrame() {
114
+ if (!enabled)
115
+ return;
116
+ lastFrameStart = performance.now();
117
+ }
118
+ export function lobbyPerfEndFrame(engine, scene) {
119
+ if (!enabled)
120
+ return;
121
+ const ms = performance.now() - lastFrameStart;
122
+ frameTimes.push(ms);
123
+ if (frameTimes.length > MAX_FRAME_TIMES)
124
+ frameTimes.shift();
125
+ // Approximate draw calls: count enabled visible meshes that will draw
126
+ // (Babylon SceneInstrumentation is not always present in the peer build).
127
+ let draws = 0;
128
+ for (const mesh of scene.meshes) {
129
+ if (mesh.isDisposed())
130
+ continue;
131
+ if (!mesh.isEnabled())
132
+ continue;
133
+ if (mesh.isVisible === false)
134
+ continue;
135
+ if ((mesh.visibility ?? 1) <= 0)
136
+ continue;
137
+ if (typeof mesh.getTotalVertices === 'function' && mesh.getTotalVertices() <= 0)
138
+ continue;
139
+ draws += 1;
140
+ }
141
+ drawCallsApprox = draws;
142
+ const gpu = engine
143
+ .getGPUFrameTimeCounter?.();
144
+ if (gpu && typeof gpu.current === 'number' && gpu.current > 0) {
145
+ bumpPhase('gpu_frame_time', gpu.current);
146
+ }
147
+ }
148
+ function percentile(sorted, p) {
149
+ if (!sorted.length)
150
+ return 0;
151
+ const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1));
152
+ return sorted[idx];
153
+ }
154
+ function distFromSamples(samples, fallback) {
155
+ const sorted = samples?.length ? [...samples].sort((a, b) => a - b) : [];
156
+ const count = fallback?.count ?? sorted.length;
157
+ const totalMs = fallback?.totalMs ?? sorted.reduce((a, b) => a + b, 0);
158
+ const maxMs = fallback?.maxMs ?? (sorted.length ? sorted[sorted.length - 1] : 0);
159
+ const lastMs = fallback?.lastMs ?? (sorted.length ? sorted[sorted.length - 1] : 0);
160
+ return {
161
+ count,
162
+ totalMs,
163
+ maxMs,
164
+ lastMs,
165
+ avgMs: count ? totalMs / count : 0,
166
+ p50Ms: percentile(sorted, 50),
167
+ p95Ms: percentile(sorted, 95),
168
+ p99Ms: percentile(sorted, 99),
169
+ };
170
+ }
171
+ function isImportRootName(name) {
172
+ const base = (name ?? '').replace(/__a\d+$/, '').toLowerCase();
173
+ return base === '__root__' || base === 'world';
174
+ }
175
+ export function captureLobbyPerfMemory() {
176
+ const perf = performance;
177
+ if (!perf.memory) {
178
+ return { available: false, note: 'not available in current runtime (performance.memory)' };
179
+ }
180
+ return {
181
+ available: true,
182
+ usedJSHeapBytes: perf.memory.usedJSHeapSize,
183
+ totalJSHeapBytes: perf.memory.totalJSHeapSize,
184
+ jsHeapLimitBytes: perf.memory.jsHeapSizeLimit,
185
+ };
186
+ }
187
+ function collectTexturesFromMaterial(mat, textures) {
188
+ const anyMat = mat;
189
+ for (const key of [
190
+ 'albedoTexture',
191
+ 'diffuseTexture',
192
+ 'bumpTexture',
193
+ 'opacityTexture',
194
+ 'emissiveTexture',
195
+ 'metallicTexture',
196
+ 'ambientTexture',
197
+ 'reflectionTexture',
198
+ ]) {
199
+ const tex = anyMat[key];
200
+ if (tex)
201
+ textures.add(tex);
202
+ }
203
+ }
204
+ export function collectLobbyAvatarCosts(scene) {
205
+ const roots = [];
206
+ for (const node of scene.transformNodes) {
207
+ if (!node.isDisposed() && node.metadata?.isLobbyAvatarRoot)
208
+ roots.push(node);
209
+ }
210
+ for (const mesh of scene.meshes) {
211
+ if (!mesh.isDisposed() && mesh.metadata?.isLobbyAvatarRoot) {
212
+ roots.push(mesh);
213
+ }
214
+ }
215
+ const out = [];
216
+ for (const root of roots) {
217
+ const name = root.name ?? 'unnamed';
218
+ let kind = 'other';
219
+ if (name.startsWith('remote-'))
220
+ kind = 'remote';
221
+ else if (name.includes('local') || name === 'avatar' || name.startsWith('player'))
222
+ kind = 'local';
223
+ // Dev lobby local is often named after user; treat non-remote lobby roots as local when only one.
224
+ if (kind === 'other' && !name.startsWith('remote-'))
225
+ kind = 'local';
226
+ let meshCount = 0;
227
+ let skinnedMeshCount = 0;
228
+ let boneCount = 0;
229
+ let vertexCount = 0;
230
+ let triangleCount = 0;
231
+ let alwaysSelectCount = 0;
232
+ let doubleSidedCount = 0;
233
+ const skeletons = new Set();
234
+ const materials = new Set();
235
+ const textures = new Set();
236
+ let meshes = [];
237
+ try {
238
+ meshes = root.getChildMeshes?.(false) ?? [];
239
+ }
240
+ catch {
241
+ meshes = [];
242
+ }
243
+ for (const mesh of meshes) {
244
+ if (!mesh || mesh.isDisposed())
245
+ continue;
246
+ const v = typeof mesh.getTotalVertices === 'function' ? mesh.getTotalVertices() : 0;
247
+ if (v <= 0 && isImportRootName(mesh.name))
248
+ continue;
249
+ meshCount += 1;
250
+ if (mesh.alwaysSelectAsActiveMesh)
251
+ alwaysSelectCount += 1;
252
+ const side = mesh.overrideMaterialSideOrientation ??
253
+ mesh.sideOrientation;
254
+ if (side === 2)
255
+ doubleSidedCount += 1;
256
+ if (mesh.skeleton && v >= 24 && !isImportRootName(mesh.name)) {
257
+ skinnedMeshCount += 1;
258
+ skeletons.add(mesh.skeleton);
259
+ }
260
+ if (v > 0) {
261
+ vertexCount += v;
262
+ const idx = mesh.getIndices?.();
263
+ if (idx?.length)
264
+ triangleCount += Math.floor(idx.length / 3);
265
+ else
266
+ triangleCount += Math.floor(v / 3);
267
+ }
268
+ if (mesh.material) {
269
+ materials.add(mesh.material);
270
+ collectTexturesFromMaterial(mesh.material, textures);
271
+ const subs = mesh.material.subMaterials;
272
+ if (Array.isArray(subs)) {
273
+ for (const sub of subs) {
274
+ if (sub) {
275
+ materials.add(sub);
276
+ collectTexturesFromMaterial(sub, textures);
277
+ }
278
+ }
279
+ }
280
+ }
281
+ }
282
+ for (const sk of skeletons)
283
+ boneCount += sk.bones?.length ?? 0;
284
+ const groups = root.metadata?.animationGroups ?? [];
285
+ const sourceClips = root.metadata?.sourceClips ?? [];
286
+ out.push({
287
+ name,
288
+ kind,
289
+ meshCount,
290
+ skinnedMeshCount,
291
+ skeletonCount: skeletons.size,
292
+ boneCount,
293
+ materialCount: materials.size,
294
+ textureCount: textures.size,
295
+ animationGroupCount: Math.max(groups.length, sourceClips.length),
296
+ vertexCount,
297
+ triangleCount,
298
+ alwaysSelectCount,
299
+ doubleSidedCount,
300
+ rigidGlb: !!root.metadata?.rigidGlb,
301
+ boneDriven: !!root.metadata?.boneDriven,
302
+ });
303
+ }
304
+ return out;
305
+ }
306
+ function collectSceneStats(scene, engine, label) {
307
+ const meshes = scene.meshes.filter((m) => !m.isDisposed());
308
+ let active = 0;
309
+ let alwaysSelect = 0;
310
+ let doubleSided = 0;
311
+ let skinned = 0;
312
+ let tris = 0;
313
+ let verts = 0;
314
+ const skeletons = new Set();
315
+ const materials = new Set();
316
+ const textures = new Set();
317
+ for (const mesh of meshes) {
318
+ const enabledMesh = mesh.isEnabled() !== false && mesh.isVisible !== false && (mesh.visibility ?? 1) > 0;
319
+ const v = typeof mesh.getTotalVertices === 'function' ? mesh.getTotalVertices() : 0;
320
+ if (enabledMesh && v > 0)
321
+ active += 1;
322
+ if (mesh.alwaysSelectAsActiveMesh)
323
+ alwaysSelect += 1;
324
+ const side = mesh.overrideMaterialSideOrientation ??
325
+ mesh.sideOrientation;
326
+ if (side === 2)
327
+ doubleSided += 1;
328
+ if (mesh.skeleton && v >= 24 && !isImportRootName(mesh.name)) {
329
+ skinned += 1;
330
+ skeletons.add(mesh.skeleton);
331
+ }
332
+ if (v > 0) {
333
+ verts += v;
334
+ const idx = mesh.getIndices?.();
335
+ if (idx && idx.length)
336
+ tris += Math.floor(idx.length / 3);
337
+ else
338
+ tris += Math.floor(v / 3);
339
+ }
340
+ const mat = mesh.material;
341
+ if (mat) {
342
+ materials.add(mat);
343
+ collectTexturesFromMaterial(mat, textures);
344
+ const subs = mat.subMaterials;
345
+ if (Array.isArray(subs)) {
346
+ for (const sub of subs) {
347
+ if (sub) {
348
+ materials.add(sub);
349
+ collectTexturesFromMaterial(sub, textures);
350
+ }
351
+ }
352
+ }
353
+ }
354
+ }
355
+ let boneCount = 0;
356
+ for (const sk of skeletons)
357
+ boneCount += sk.bones?.length ?? 0;
358
+ let avatarRoots = 0;
359
+ for (const node of scene.transformNodes) {
360
+ if (!node.isDisposed() && node.metadata?.isLobbyAvatarRoot)
361
+ avatarRoots += 1;
362
+ }
363
+ for (const mesh of meshes) {
364
+ if (mesh.metadata?.isLobbyAvatarRoot)
365
+ avatarRoots += 1;
366
+ }
367
+ let remotes = 0;
368
+ for (const node of scene.transformNodes) {
369
+ if (!node.isDisposed() && typeof node.name === 'string' && node.name.startsWith('remote-')) {
370
+ if (node.metadata?.isLobbyAvatarRoot)
371
+ remotes += 1;
372
+ }
373
+ }
374
+ const sortedFrames = [...frameTimes].sort((a, b) => a - b);
375
+ const avgFrame = sortedFrames.length
376
+ ? sortedFrames.reduce((a, b) => a + b, 0) / sortedFrames.length
377
+ : 0;
378
+ const lastFrame = frameTimes.length ? frameTimes[frameTimes.length - 1] : 0;
379
+ const glow = scene.metadata?.plazaGlow;
380
+ const phases = {};
381
+ const phaseDist = {};
382
+ for (const [k, v] of phaseStats) {
383
+ phases[k] = { ...v };
384
+ phaseDist[k] = distFromSamples(phaseSamples.get(k), v);
385
+ }
386
+ const unavailable = [
387
+ 'shader_switches: not available in current runtime',
388
+ 'material_switches: not available in current runtime',
389
+ 'texture_binds: not available in current runtime',
390
+ 'render_target_changes: not available in current runtime',
391
+ 'precise_GPU_frame_time: not available unless engine GPU timer extension is active',
392
+ 'GPU_memory: not available in current runtime',
393
+ 'texture_memory_bytes: not available in current runtime',
394
+ 'SceneInstrumentation drawCalls: not wired in this peer build (using mesh-count approx)',
395
+ ];
396
+ return {
397
+ at: new Date().toISOString(),
398
+ label,
399
+ fps: engine.getFps(),
400
+ frameTimeMs: lastFrame,
401
+ avgFrameTimeMs: avgFrame,
402
+ p50FrameTimeMs: percentile(sortedFrames, 50),
403
+ p95FrameTimeMs: percentile(sortedFrames, 95),
404
+ p99FrameTimeMs: percentile(sortedFrames, 99),
405
+ maxFrameTimeMs: sortedFrames.length ? sortedFrames[sortedFrames.length - 1] : 0,
406
+ drawCallsApprox,
407
+ meshCount: meshes.length,
408
+ activeMeshCount: active,
409
+ alwaysSelectCount: alwaysSelect,
410
+ doubleSidedCount: doubleSided,
411
+ skinnedMeshCount: skinned,
412
+ skeletonCount: skeletons.size,
413
+ boneCount,
414
+ materialCount: scene.materials.length,
415
+ uniqueMaterialIds: materials.size,
416
+ textureCount: textures.size,
417
+ triangleApprox: tris,
418
+ vertexApprox: verts,
419
+ lobbyAvatarRoots: avatarRoots,
420
+ remotePlayers: remotes,
421
+ disableUniformBuffers: !!engine.disableUniformBuffers,
422
+ bloomEnabled: null,
423
+ glowIntensity: glow?.intensity ?? null,
424
+ memory: captureLobbyPerfMemory(),
425
+ phases,
426
+ phaseDist,
427
+ recentMarks: recentMarks.slice(-80),
428
+ avatarCosts: collectLobbyAvatarCosts(scene),
429
+ unavailable,
430
+ };
431
+ }
432
+ export function captureLobbyPerfSnapshot(scene, engine, label = 'snapshot') {
433
+ return collectSceneStats(scene, engine, label);
434
+ }
435
+ /** Sample FPS/frame metrics for durationMs while the render loop runs. */
436
+ export async function sampleLobbyPerf(scene, engine, label, durationMs = 3000, intervalMs = 100) {
437
+ const was = enabled;
438
+ enabled = true;
439
+ const fpsSamples = [];
440
+ const ftSamples = [];
441
+ const drawSamples = [];
442
+ const activeSamples = [];
443
+ const skinnedSamples = [];
444
+ const tEnd = performance.now() + durationMs;
445
+ while (performance.now() < tEnd) {
446
+ await new Promise((r) => setTimeout(r, intervalMs));
447
+ const snap = collectSceneStats(scene, engine, label);
448
+ fpsSamples.push(snap.fps);
449
+ ftSamples.push(snap.frameTimeMs || snap.avgFrameTimeMs);
450
+ drawSamples.push(snap.drawCallsApprox);
451
+ activeSamples.push(snap.activeMeshCount);
452
+ skinnedSamples.push(snap.skinnedMeshCount);
453
+ }
454
+ const avg = (arr) => (arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0);
455
+ const sortedFt = [...ftSamples].sort((a, b) => a - b);
456
+ const snapshot = collectSceneStats(scene, engine, label);
457
+ enabled = was;
458
+ return {
459
+ label,
460
+ durationMs,
461
+ samples: fpsSamples.length,
462
+ avgFps: avg(fpsSamples),
463
+ minFps: fpsSamples.length ? Math.min(...fpsSamples) : 0,
464
+ maxFps: fpsSamples.length ? Math.max(...fpsSamples) : 0,
465
+ avgFrameTimeMs: avg(ftSamples),
466
+ p50FrameTimeMs: percentile(sortedFt, 50),
467
+ p95FrameTimeMs: percentile(sortedFt, 95),
468
+ p99FrameTimeMs: percentile(sortedFt, 99),
469
+ maxFrameTimeMs: ftSamples.length ? Math.max(...ftSamples) : 0,
470
+ avgDrawCalls: avg(drawSamples),
471
+ avgActiveMeshes: avg(activeSamples),
472
+ avgSkinnedMeshes: avg(skinnedSamples),
473
+ phaseDist: snapshot.phaseDist,
474
+ memory: snapshot.memory,
475
+ snapshot,
476
+ };
477
+ }
478
+ /** Temporary toggle for measurement — does not change spawn defaults. */
479
+ export function diagSetAlwaysSelectAsActiveMesh(scene, on) {
480
+ let n = 0;
481
+ for (const mesh of scene.meshes) {
482
+ if (!mesh.metadata?.isLobbyAvatar)
483
+ continue;
484
+ mesh.alwaysSelectAsActiveMesh = on;
485
+ n += 1;
486
+ }
487
+ return n;
488
+ }
489
+ /**
490
+ * Temporary DoubleSide / backFaceCulling toggle for lobby avatar meshes only.
491
+ * Baseline project default is DoubleSide ON + backFaceCulling false — restore after A/B.
492
+ * Does not change MaterialPipeline spawn behavior.
493
+ */
494
+ export function diagSetAvatarDoubleSide(scene, on) {
495
+ const DOUBLESIDE = 2;
496
+ const FRONTSIDE = 0;
497
+ let n = 0;
498
+ for (const mesh of scene.meshes) {
499
+ if (!mesh.metadata?.isLobbyAvatar)
500
+ continue;
501
+ const m = mesh;
502
+ if (on) {
503
+ m.sideOrientation = DOUBLESIDE;
504
+ m.overrideMaterialSideOrientation = DOUBLESIDE;
505
+ if (mesh.material)
506
+ mesh.material.backFaceCulling = false;
507
+ }
508
+ else {
509
+ m.sideOrientation = FRONTSIDE;
510
+ m.overrideMaterialSideOrientation = FRONTSIDE;
511
+ if (mesh.material)
512
+ mesh.material.backFaceCulling = true;
513
+ }
514
+ n += 1;
515
+ }
516
+ return n;
517
+ }
518
+ /** Temporary toggle for measurement — does not change SceneManager defaults. */
519
+ export function diagSetDisableUniformBuffers(engine, on) {
520
+ engine.disableUniformBuffers = on;
521
+ return on;
522
+ }
523
+ /** Non-sensitive WebGL/caps snapshot for render reports. */
524
+ export function captureWebGlInfo(engine) {
525
+ let glInfo = null;
526
+ try {
527
+ glInfo = engine
528
+ .getGlInfo?.() ?? null;
529
+ }
530
+ catch {
531
+ glInfo = null;
532
+ }
533
+ const caps = engine.getCaps?.() ?? null;
534
+ const maxTextureSize = typeof caps?.maxTextureSize === 'number' ? caps.maxTextureSize : null;
535
+ const maxVertexTextureImageUnits = typeof caps?.maxVertexTextureImageUnits === 'number'
536
+ ? caps.maxVertexTextureImageUnits
537
+ : null;
538
+ const parallelShaderCompile = typeof caps?.parallelShaderCompile === 'boolean' ? caps.parallelShaderCompile : null;
539
+ const gpuCounter = engine
540
+ .getGPUFrameTimeCounter?.();
541
+ const gpuFrameTimerAvailable = !!(gpuCounter && typeof gpuCounter.current === 'number');
542
+ return {
543
+ webglVersion: glInfo?.version ?? null,
544
+ renderer: glInfo?.renderer ?? null,
545
+ vendor: glInfo?.vendor ?? null,
546
+ maxTextureSize,
547
+ maxVertexTextureImageUnits,
548
+ parallelShaderCompile,
549
+ gpuFrameTimerAvailable,
550
+ note: gpuFrameTimerAvailable
551
+ ? 'GPU frame counter object present; treat values as advisory unless consistently non-zero'
552
+ : 'precise GPU timing unavailable in current runtime — report CPU/scene_render only',
553
+ };
554
+ }
555
+ /** Avatar-only costs (excludes plaza NPCs / props). */
556
+ export function summarizeAvatarRenderCosts(scene, label = 'avatar-render') {
557
+ const all = collectLobbyAvatarCosts(scene);
558
+ const avatars = all.filter((a) => a.skinnedMeshCount > 0 ||
559
+ a.name === 'local-player' ||
560
+ a.name.startsWith('remote-'));
561
+ let skinnedMeshCount = 0;
562
+ let skeletonCount = 0;
563
+ let boneCount = 0;
564
+ let triangleCount = 0;
565
+ let vertexCount = 0;
566
+ let materialCount = 0;
567
+ let textureCount = 0;
568
+ let alwaysSelectCount = 0;
569
+ let doubleSidedCount = 0;
570
+ let animationGroupCount = 0;
571
+ for (const a of avatars) {
572
+ skinnedMeshCount += a.skinnedMeshCount;
573
+ skeletonCount += a.skeletonCount;
574
+ boneCount += a.boneCount;
575
+ triangleCount += a.triangleCount;
576
+ vertexCount += a.vertexCount;
577
+ materialCount += a.materialCount;
578
+ textureCount += a.textureCount;
579
+ alwaysSelectCount += a.alwaysSelectCount;
580
+ doubleSidedCount += a.doubleSidedCount;
581
+ animationGroupCount += a.animationGroupCount;
582
+ }
583
+ let sceneActive = 0;
584
+ let sceneDraws = 0;
585
+ let sceneTriangleApprox = 0;
586
+ let sceneVertexApprox = 0;
587
+ const sceneSkeletons = new Set();
588
+ let sceneBoneCount = 0;
589
+ for (const mesh of scene.meshes) {
590
+ if (mesh.isDisposed())
591
+ continue;
592
+ if (!mesh.isEnabled() || mesh.isVisible === false || (mesh.visibility ?? 1) <= 0)
593
+ continue;
594
+ const v = typeof mesh.getTotalVertices === 'function' ? mesh.getTotalVertices() : 0;
595
+ if (v <= 0)
596
+ continue;
597
+ sceneActive += 1;
598
+ sceneDraws += 1;
599
+ sceneVertexApprox += v;
600
+ const idx = typeof mesh.getIndices === 'function' ? mesh.getIndices() : null;
601
+ if (idx?.length)
602
+ sceneTriangleApprox += Math.floor(idx.length / 3);
603
+ else
604
+ sceneTriangleApprox += Math.floor(v / 3);
605
+ const sk = mesh.skeleton;
606
+ if (sk && !sceneSkeletons.has(sk)) {
607
+ sceneSkeletons.add(sk);
608
+ sceneBoneCount += sk.bones?.length ?? 0;
609
+ }
610
+ }
611
+ return {
612
+ label,
613
+ skinnedAvatarCount: avatars.filter((a) => a.skinnedMeshCount > 0).length,
614
+ skinnedMeshCount,
615
+ skeletonCount,
616
+ boneCount,
617
+ triangleCount,
618
+ vertexCount,
619
+ materialCount,
620
+ textureCount,
621
+ alwaysSelectCount,
622
+ doubleSidedCount,
623
+ animationGroupCount,
624
+ sceneActiveMeshCount: sceneActive,
625
+ sceneDrawCallsApprox: sceneDraws,
626
+ sceneTriangleApprox,
627
+ sceneVertexApprox,
628
+ sceneMaterialCount: scene.materials.length,
629
+ sceneTextureCount: scene.textures.length,
630
+ sceneSkeletonCount: sceneSkeletons.size,
631
+ sceneBoneCount,
632
+ avatars,
633
+ };
634
+ }
635
+ function phaseOrNull(name) {
636
+ const v = phaseStats.get(name);
637
+ if (!v)
638
+ return null;
639
+ return distFromSamples(phaseSamples.get(name), v);
640
+ }
641
+ /** Sample while render loop runs; focuses report on avatar render cost. */
642
+ export async function sampleAvatarRender(scene, engine, label, durationMs = 3000) {
643
+ return runSampleAvatarRender(scene, engine, label, durationMs);
644
+ }
645
+ async function runSampleAvatarRender(scene, engine, label, durationMs = 3000) {
646
+ // Clear frame/phase samples so percentiles reflect this window only.
647
+ resetLobbyPerfDiag({ keepPhases: false });
648
+ setLobbyPerfDiagEnabled(true);
649
+ const series = await sampleLobbyPerf(scene, engine, label, durationMs);
650
+ const avatarCosts = summarizeAvatarRenderCosts(scene, label);
651
+ const snap = series.snapshot;
652
+ let alwaysOn = 0;
653
+ let doubleOn = 0;
654
+ let avatarMeshes = 0;
655
+ for (const mesh of scene.meshes) {
656
+ if (!mesh.metadata?.isLobbyAvatar)
657
+ continue;
658
+ avatarMeshes += 1;
659
+ if (mesh.alwaysSelectAsActiveMesh)
660
+ alwaysOn += 1;
661
+ const side = mesh.overrideMaterialSideOrientation ??
662
+ mesh.sideOrientation;
663
+ if (side === 2)
664
+ doubleOn += 1;
665
+ }
666
+ return {
667
+ label,
668
+ durationMs,
669
+ avatarCosts,
670
+ series,
671
+ phases: {
672
+ scene_render: phaseOrNull('scene_render'),
673
+ remote_update: phaseOrNull('remote_update'),
674
+ local_update: phaseOrNull('local_update'),
675
+ frame_logic: phaseOrNull('frame_logic'),
676
+ },
677
+ webgl: captureWebGlInfo(engine),
678
+ toggles: {
679
+ alwaysSelectAsActiveMesh: avatarMeshes > 0 ? alwaysOn === avatarMeshes : true,
680
+ doubleSide: avatarMeshes > 0 ? doubleOn === avatarMeshes : true,
681
+ disableUniformBuffers: !!engine.disableUniformBuffers,
682
+ },
683
+ unavailable: snap.unavailable ?? [],
684
+ };
685
+ }
686
+ export function getLobbyPerfPhaseSummary() {
687
+ const out = {};
688
+ for (const [k, v] of phaseStats) {
689
+ out[k] = distFromSamples(phaseSamples.get(k), v);
690
+ }
691
+ return {
692
+ phases: out,
693
+ remoteUpdateAvgMs: remoteUpdateCount ? remoteUpdateMsAccum / remoteUpdateCount : 0,
694
+ localUpdateAvgMs: localUpdateCount ? localUpdateMsAccum / localUpdateCount : 0,
695
+ remoteUpdateCount,
696
+ localUpdateCount,
697
+ recentMarks: recentMarks.slice(-80),
698
+ };
699
+ }
700
+ /** Build a structured spawn-timeline view from recent marks (measurement only). */
701
+ export function extractSpawnTimelineFromMarks(marks = recentMarks) {
702
+ const phasesOfInterest = [
703
+ 'remote_placeholder',
704
+ 'remote_yield',
705
+ 'draco_ensure',
706
+ 'glb_container_load',
707
+ 'glb_instantiate',
708
+ 'harden_materials',
709
+ 'humanoid_bind',
710
+ 'accessories',
711
+ 'albedo_tint',
712
+ 'createAsync_total',
713
+ 'remote_spawn_total',
714
+ ];
715
+ const byPhase = {};
716
+ for (const m of marks) {
717
+ if (!phasesOfInterest.includes(m.phase))
718
+ continue;
719
+ (byPhase[m.phase] ??= []).push({ ms: m.ms, detail: m.detail, at: m.at });
720
+ }
721
+ return byPhase;
722
+ }
723
+ export function attachLobbyPerfDiag(getScene, getEngine, win = window) {
724
+ const handle = {
725
+ enable: (on = true) => setLobbyPerfDiagEnabled(on),
726
+ disable: () => setLobbyPerfDiagEnabled(false),
727
+ reset: (opts) => resetLobbyPerfDiag(opts),
728
+ snapshot: (label) => captureLobbyPerfSnapshot(getScene(), getEngine(), label ?? 'snapshot'),
729
+ sample: (label, durationMs) => sampleLobbyPerf(getScene(), getEngine(), label, durationMs),
730
+ phases: () => getLobbyPerfPhaseSummary(),
731
+ report: (label) => {
732
+ const snapshot = captureLobbyPerfSnapshot(getScene(), getEngine(), label ?? 'report');
733
+ return {
734
+ snapshot,
735
+ phases: getLobbyPerfPhaseSummary(),
736
+ spawnTimeline: extractSpawnTimelineFromMarks(),
737
+ memory: snapshot.memory,
738
+ };
739
+ },
740
+ memory: () => captureLobbyPerfMemory(),
741
+ avatarCosts: () => collectLobbyAvatarCosts(getScene()),
742
+ avatarRenderCosts: (label) => summarizeAvatarRenderCosts(getScene(), label ?? 'avatar-render'),
743
+ sampleAvatarRender: (label, durationMs) => runSampleAvatarRender(getScene(), getEngine(), label, durationMs),
744
+ webglInfo: () => captureWebGlInfo(getEngine()),
745
+ setAlwaysSelectAsActiveMesh: (on) => diagSetAlwaysSelectAsActiveMesh(getScene(), on),
746
+ setAvatarDoubleSide: (on) => diagSetAvatarDoubleSide(getScene(), on),
747
+ setDisableUniformBuffers: (on) => diagSetDisableUniformBuffers(getEngine(), on),
748
+ getToggles: () => {
749
+ const scene = getScene();
750
+ const engine = getEngine();
751
+ let alwaysSelectCount = 0;
752
+ let doubleSideCount = 0;
753
+ for (const mesh of scene.meshes) {
754
+ if (!mesh.metadata?.isLobbyAvatar)
755
+ continue;
756
+ if (mesh.alwaysSelectAsActiveMesh)
757
+ alwaysSelectCount += 1;
758
+ const side = mesh
759
+ .overrideMaterialSideOrientation ??
760
+ mesh.sideOrientation;
761
+ if (side === 2)
762
+ doubleSideCount += 1;
763
+ }
764
+ return {
765
+ alwaysSelectCount,
766
+ doubleSideCount,
767
+ disableUniformBuffers: !!engine
768
+ .disableUniformBuffers,
769
+ };
770
+ },
771
+ };
772
+ win.__OYNA360_LOBBY_PERF__ = handle;
773
+ return handle;
774
+ }
775
+ //# sourceMappingURL=lobby-perf-diag.js.map