@plasius/gpu-xr 0.1.4 → 0.1.7
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/CHANGELOG.md +40 -0
- package/README.md +28 -0
- package/dist/index.cjs +225 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +218 -8
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/src/index.d.ts +60 -0
- package/src/index.js +274 -7
package/src/index.js
CHANGED
|
@@ -2,6 +2,11 @@ const DEFAULT_VR_SESSION_INIT = Object.freeze({
|
|
|
2
2
|
requiredFeatures: ["local-floor"],
|
|
3
3
|
optionalFeatures: ["bounded-floor", "hand-tracking", "layers"],
|
|
4
4
|
});
|
|
5
|
+
const DEFAULT_MODE_FRAME_RATES = Object.freeze({
|
|
6
|
+
inline: 60,
|
|
7
|
+
"immersive-vr": 90,
|
|
8
|
+
"immersive-ar": 72,
|
|
9
|
+
});
|
|
5
10
|
|
|
6
11
|
export const xrSessionModes = Object.freeze([
|
|
7
12
|
"inline",
|
|
@@ -16,6 +21,9 @@ export const xrReferenceSpaceTypes = Object.freeze([
|
|
|
16
21
|
"bounded-floor",
|
|
17
22
|
"unbounded",
|
|
18
23
|
]);
|
|
24
|
+
export const xrWorkerQueueClass = "render";
|
|
25
|
+
export const xrWorkerSchedulerMode = "dag";
|
|
26
|
+
export const defaultXrWorkerBudgetProfile = "xr";
|
|
19
27
|
|
|
20
28
|
function toStringArray(values) {
|
|
21
29
|
if (!Array.isArray(values)) {
|
|
@@ -31,6 +39,39 @@ function dedupeStrings(values) {
|
|
|
31
39
|
return [...new Set(toStringArray(values))];
|
|
32
40
|
}
|
|
33
41
|
|
|
42
|
+
function readPositiveNumber(value) {
|
|
43
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
44
|
+
? value
|
|
45
|
+
: null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeFrameRates(values) {
|
|
49
|
+
if (!values || typeof values === "string") {
|
|
50
|
+
return Object.freeze([]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let collected;
|
|
54
|
+
try {
|
|
55
|
+
collected = Array.from(values);
|
|
56
|
+
} catch {
|
|
57
|
+
return Object.freeze([]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return Object.freeze(
|
|
61
|
+
[...new Set(collected.map((value) => Number(value)).filter((value) => Number.isFinite(value) && value > 0))].sort(
|
|
62
|
+
(left, right) => right - left
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getDefaultFrameRateForMode(mode) {
|
|
68
|
+
return DEFAULT_MODE_FRAME_RATES[mode] ?? DEFAULT_MODE_FRAME_RATES["immersive-vr"];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getWorkerBudgetProfileForMode(mode) {
|
|
72
|
+
return mode === "inline" ? "realtime" : defaultXrWorkerBudgetProfile;
|
|
73
|
+
}
|
|
74
|
+
|
|
34
75
|
function readNavigator(navigatorOverride) {
|
|
35
76
|
const currentNavigator = navigatorOverride ?? globalThis.navigator;
|
|
36
77
|
if (!currentNavigator || typeof currentNavigator !== "object") {
|
|
@@ -135,6 +176,137 @@ export async function requestXrSession(options = {}) {
|
|
|
135
176
|
return xr.requestSession(mode, init);
|
|
136
177
|
}
|
|
137
178
|
|
|
179
|
+
export function readXrFrameRateCapabilities(session, options = {}) {
|
|
180
|
+
const {
|
|
181
|
+
mode = "immersive-vr",
|
|
182
|
+
fallbackFrameRates = [],
|
|
183
|
+
defaultFrameRate,
|
|
184
|
+
} = options;
|
|
185
|
+
|
|
186
|
+
assertSessionMode(mode);
|
|
187
|
+
|
|
188
|
+
const sessionFrameRate = readPositiveNumber(session?.frameRate);
|
|
189
|
+
const supportedFrameRates = normalizeFrameRates(session?.supportedFrameRates);
|
|
190
|
+
const fallbackRates = normalizeFrameRates(fallbackFrameRates);
|
|
191
|
+
const mergedSupported = supportedFrameRates.length
|
|
192
|
+
? [...supportedFrameRates]
|
|
193
|
+
: [...fallbackRates];
|
|
194
|
+
|
|
195
|
+
if (sessionFrameRate && !mergedSupported.includes(sessionFrameRate)) {
|
|
196
|
+
mergedSupported.push(sessionFrameRate);
|
|
197
|
+
mergedSupported.sort((left, right) => right - left);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const refreshRateHz =
|
|
201
|
+
sessionFrameRate ??
|
|
202
|
+
mergedSupported[0] ??
|
|
203
|
+
readPositiveNumber(defaultFrameRate) ??
|
|
204
|
+
getDefaultFrameRateForMode(mode);
|
|
205
|
+
|
|
206
|
+
return Object.freeze({
|
|
207
|
+
mode,
|
|
208
|
+
currentFrameRate: sessionFrameRate,
|
|
209
|
+
supportedFrameRates: Object.freeze(mergedSupported),
|
|
210
|
+
refreshRateHz,
|
|
211
|
+
canUpdateTargetFrameRate:
|
|
212
|
+
Boolean(session) && typeof session.updateTargetFrameRate === "function",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function createXrPerformanceHint(options = {}) {
|
|
217
|
+
const {
|
|
218
|
+
session = null,
|
|
219
|
+
mode = "immersive-vr",
|
|
220
|
+
preferredFrameRates = [],
|
|
221
|
+
fallbackFrameRates = [],
|
|
222
|
+
defaultFrameRate,
|
|
223
|
+
} = options;
|
|
224
|
+
|
|
225
|
+
const capabilities = readXrFrameRateCapabilities(session, {
|
|
226
|
+
mode,
|
|
227
|
+
fallbackFrameRates,
|
|
228
|
+
defaultFrameRate,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const filteredPreferredFrameRates = normalizeFrameRates(preferredFrameRates).filter(
|
|
232
|
+
(frameRate) =>
|
|
233
|
+
capabilities.supportedFrameRates.length === 0 ||
|
|
234
|
+
capabilities.supportedFrameRates.includes(frameRate)
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const derivedPreferredFrameRates = filteredPreferredFrameRates.length
|
|
238
|
+
? filteredPreferredFrameRates
|
|
239
|
+
: capabilities.supportedFrameRates.length
|
|
240
|
+
? capabilities.supportedFrameRates
|
|
241
|
+
: Object.freeze([capabilities.refreshRateHz]);
|
|
242
|
+
const targetFrameRate =
|
|
243
|
+
derivedPreferredFrameRates[0] ?? capabilities.refreshRateHz;
|
|
244
|
+
const rationale = [];
|
|
245
|
+
|
|
246
|
+
if (capabilities.currentFrameRate) {
|
|
247
|
+
rationale.push(
|
|
248
|
+
`XR session reports a current frame rate of ${capabilities.currentFrameRate}Hz.`
|
|
249
|
+
);
|
|
250
|
+
} else {
|
|
251
|
+
rationale.push("XR session does not expose a current frame rate; using defaults.");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (capabilities.supportedFrameRates.length) {
|
|
255
|
+
rationale.push(
|
|
256
|
+
`XR runtime exposes supported frame rates: ${capabilities.supportedFrameRates.join(", ")}Hz.`
|
|
257
|
+
);
|
|
258
|
+
} else {
|
|
259
|
+
rationale.push("XR runtime does not expose supported frame rates; using fallback targets.");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (filteredPreferredFrameRates.length) {
|
|
263
|
+
rationale.push("Preferred XR frame rates were filtered against runtime-supported values.");
|
|
264
|
+
} else {
|
|
265
|
+
rationale.push("XR target frame rate defaults to the highest available runtime target.");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return Object.freeze({
|
|
269
|
+
...capabilities,
|
|
270
|
+
preferredFrameRates: Object.freeze([...derivedPreferredFrameRates]),
|
|
271
|
+
targetFrameRate,
|
|
272
|
+
targetFrameTimeMs: 1000 / targetFrameRate,
|
|
273
|
+
workerBudget: Object.freeze({
|
|
274
|
+
queueClass: xrWorkerQueueClass,
|
|
275
|
+
schedulerMode: xrWorkerSchedulerMode,
|
|
276
|
+
profile: getWorkerBudgetProfileForMode(mode),
|
|
277
|
+
}),
|
|
278
|
+
rationale: Object.freeze(rationale),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function updateXrTargetFrameRate(session, frameRate) {
|
|
283
|
+
if (!session || typeof session !== "object") {
|
|
284
|
+
throw new Error("XR session is required to update target frame rate.");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const requestedFrameRate = readPositiveNumber(frameRate);
|
|
288
|
+
if (!requestedFrameRate) {
|
|
289
|
+
throw new Error("XR target frame rate must be a finite number greater than zero.");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (typeof session.updateTargetFrameRate !== "function") {
|
|
293
|
+
throw new Error("XR session does not support updateTargetFrameRate(frameRate).");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const supportedFrameRates = normalizeFrameRates(session.supportedFrameRates);
|
|
297
|
+
if (
|
|
298
|
+
supportedFrameRates.length > 0 &&
|
|
299
|
+
!supportedFrameRates.includes(requestedFrameRate)
|
|
300
|
+
) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`XR target frame rate ${requestedFrameRate}Hz is not supported by the active session.`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
await session.updateTargetFrameRate(requestedFrameRate);
|
|
307
|
+
return requestedFrameRate;
|
|
308
|
+
}
|
|
309
|
+
|
|
138
310
|
export function createXrStore(initialState = {}) {
|
|
139
311
|
const listeners = new Set();
|
|
140
312
|
let state = {
|
|
@@ -143,6 +315,11 @@ export function createXrStore(initialState = {}) {
|
|
|
143
315
|
isEntering: false,
|
|
144
316
|
lastError: null,
|
|
145
317
|
supportedModes: {},
|
|
318
|
+
currentFrameRate: null,
|
|
319
|
+
targetFrameRate: null,
|
|
320
|
+
supportedFrameRates: [],
|
|
321
|
+
canUpdateTargetFrameRate: false,
|
|
322
|
+
workerBudgetProfile: null,
|
|
146
323
|
...initialState,
|
|
147
324
|
};
|
|
148
325
|
|
|
@@ -176,6 +353,11 @@ export function createXrStore(initialState = {}) {
|
|
|
176
353
|
isEntering: false,
|
|
177
354
|
lastError: null,
|
|
178
355
|
supportedModes: {},
|
|
356
|
+
currentFrameRate: null,
|
|
357
|
+
targetFrameRate: null,
|
|
358
|
+
supportedFrameRates: [],
|
|
359
|
+
canUpdateTargetFrameRate: false,
|
|
360
|
+
workerBudgetProfile: null,
|
|
179
361
|
...initialState,
|
|
180
362
|
};
|
|
181
363
|
notify();
|
|
@@ -215,6 +397,11 @@ export function createXrManager(options = {}) {
|
|
|
215
397
|
activeSession: null,
|
|
216
398
|
mode: null,
|
|
217
399
|
isEntering: false,
|
|
400
|
+
currentFrameRate: null,
|
|
401
|
+
targetFrameRate: null,
|
|
402
|
+
supportedFrameRates: [],
|
|
403
|
+
canUpdateTargetFrameRate: false,
|
|
404
|
+
workerBudgetProfile: null,
|
|
218
405
|
});
|
|
219
406
|
if (typeof onSessionEnd === "function") {
|
|
220
407
|
onSessionEnd();
|
|
@@ -233,6 +420,63 @@ export function createXrManager(options = {}) {
|
|
|
233
420
|
|
|
234
421
|
const subscribe = (listener) => store.subscribe(listener);
|
|
235
422
|
|
|
423
|
+
const getFrameRateCapabilities = (options = {}) => {
|
|
424
|
+
const state = store.getSnapshot();
|
|
425
|
+
return readXrFrameRateCapabilities(
|
|
426
|
+
options.session ?? state.activeSession,
|
|
427
|
+
{
|
|
428
|
+
mode: options.mode ?? state.mode ?? defaultMode,
|
|
429
|
+
fallbackFrameRates:
|
|
430
|
+
options.fallbackFrameRates ?? state.supportedFrameRates,
|
|
431
|
+
defaultFrameRate:
|
|
432
|
+
options.defaultFrameRate ??
|
|
433
|
+
state.targetFrameRate ??
|
|
434
|
+
state.currentFrameRate ??
|
|
435
|
+
undefined,
|
|
436
|
+
}
|
|
437
|
+
);
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const getPerformanceHint = (options = {}) => {
|
|
441
|
+
const state = store.getSnapshot();
|
|
442
|
+
return createXrPerformanceHint({
|
|
443
|
+
session: options.session ?? state.activeSession,
|
|
444
|
+
mode: options.mode ?? state.mode ?? defaultMode,
|
|
445
|
+
preferredFrameRates:
|
|
446
|
+
options.preferredFrameRates ??
|
|
447
|
+
(state.targetFrameRate ? [state.targetFrameRate] : []),
|
|
448
|
+
fallbackFrameRates:
|
|
449
|
+
options.fallbackFrameRates ?? state.supportedFrameRates,
|
|
450
|
+
defaultFrameRate:
|
|
451
|
+
options.defaultFrameRate ??
|
|
452
|
+
state.targetFrameRate ??
|
|
453
|
+
state.currentFrameRate ??
|
|
454
|
+
undefined,
|
|
455
|
+
});
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const syncSessionPerformanceState = (session, mode, preferredFrameRates = []) => {
|
|
459
|
+
const hint = createXrPerformanceHint({
|
|
460
|
+
session,
|
|
461
|
+
mode,
|
|
462
|
+
preferredFrameRates,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
store.set({
|
|
466
|
+
activeSession: session,
|
|
467
|
+
mode,
|
|
468
|
+
isEntering: false,
|
|
469
|
+
lastError: null,
|
|
470
|
+
currentFrameRate: hint.currentFrameRate,
|
|
471
|
+
targetFrameRate: hint.targetFrameRate,
|
|
472
|
+
supportedFrameRates: hint.supportedFrameRates,
|
|
473
|
+
canUpdateTargetFrameRate: hint.canUpdateTargetFrameRate,
|
|
474
|
+
workerBudgetProfile: hint.workerBudget.profile,
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
return hint;
|
|
478
|
+
};
|
|
479
|
+
|
|
236
480
|
const probeSupport = async (modes = [defaultMode]) => {
|
|
237
481
|
const supportedModes = {};
|
|
238
482
|
for (const mode of modes) {
|
|
@@ -264,13 +508,7 @@ export function createXrManager(options = {}) {
|
|
|
264
508
|
});
|
|
265
509
|
|
|
266
510
|
attachSessionEndHandler(session);
|
|
267
|
-
|
|
268
|
-
store.set({
|
|
269
|
-
activeSession: session,
|
|
270
|
-
mode,
|
|
271
|
-
isEntering: false,
|
|
272
|
-
lastError: null,
|
|
273
|
-
});
|
|
511
|
+
syncSessionPerformanceState(session, mode);
|
|
274
512
|
|
|
275
513
|
if (typeof onSessionStart === "function") {
|
|
276
514
|
onSessionStart(session, mode);
|
|
@@ -292,6 +530,32 @@ export function createXrManager(options = {}) {
|
|
|
292
530
|
return enterSession("immersive-vr", sessionInit);
|
|
293
531
|
};
|
|
294
532
|
|
|
533
|
+
const setTargetFrameRate = async (frameRate) => {
|
|
534
|
+
const state = store.getSnapshot();
|
|
535
|
+
const activeSession = state.activeSession;
|
|
536
|
+
if (!activeSession) {
|
|
537
|
+
throw new Error(
|
|
538
|
+
"Cannot update XR target frame rate without an active XR session."
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
const appliedFrameRate = await updateXrTargetFrameRate(
|
|
544
|
+
activeSession,
|
|
545
|
+
frameRate
|
|
546
|
+
);
|
|
547
|
+
syncSessionPerformanceState(activeSession, state.mode ?? defaultMode, [
|
|
548
|
+
appliedFrameRate,
|
|
549
|
+
]);
|
|
550
|
+
return appliedFrameRate;
|
|
551
|
+
} catch (error) {
|
|
552
|
+
const message =
|
|
553
|
+
error instanceof Error ? error.message : String(error ?? "Unknown XR error");
|
|
554
|
+
store.set({ lastError: message });
|
|
555
|
+
throw error;
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
|
|
295
559
|
const exitSession = async () => {
|
|
296
560
|
const { activeSession } = store.getSnapshot();
|
|
297
561
|
if (!activeSession) {
|
|
@@ -321,8 +585,11 @@ export function createXrManager(options = {}) {
|
|
|
321
585
|
getState,
|
|
322
586
|
subscribe,
|
|
323
587
|
probeSupport,
|
|
588
|
+
getFrameRateCapabilities,
|
|
589
|
+
getPerformanceHint,
|
|
324
590
|
enterSession,
|
|
325
591
|
enterVr,
|
|
592
|
+
setTargetFrameRate,
|
|
326
593
|
exitSession,
|
|
327
594
|
dispose,
|
|
328
595
|
};
|