@covas-labs/electron-vr 0.3.2 → 0.5.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.
- package/README.md +44 -5
- package/dist/VROverlay.d.ts +12 -0
- package/dist/VROverlay.js +38 -1
- package/dist/bridge.d.ts +28 -0
- package/dist/bridge.js +254 -5
- package/dist/compatibility.d.ts +56 -0
- package/dist/compatibility.js +210 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/openxrApiLayer.d.ts +13 -0
- package/dist/openxrApiLayer.js +109 -0
- package/dist/overlayOptions.d.ts +1 -0
- package/dist/overlayOptions.js +5 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -53,6 +53,7 @@ app.on("ready", async () => {
|
|
|
53
53
|
overlay = await VROverlay.openWindow(window, {
|
|
54
54
|
name: "Status_HUD",
|
|
55
55
|
sizeMeters: 1,
|
|
56
|
+
curvature: 0,
|
|
56
57
|
placement: {
|
|
57
58
|
mode: "head",
|
|
58
59
|
position: { x: 0, y: 0, z: -1.2 },
|
|
@@ -62,12 +63,50 @@ app.on("ready", async () => {
|
|
|
62
63
|
});
|
|
63
64
|
```
|
|
64
65
|
|
|
65
|
-
You can also reposition the overlay later with `overlay.setPlacement(...)`, toggle it with `overlay.setVisible(...)`,
|
|
66
|
+
You can also reposition the overlay later with `overlay.setPlacement(...)`, toggle it with `overlay.setVisible(...)`, resize it in meters with `overlay.setSizeMeters(...)`, and curve it with `overlay.setCurvature(...)`.
|
|
66
67
|
|
|
67
|
-
`sizeMeters` must be greater than zero, and placement values should be finite numbers.
|
|
68
|
+
`sizeMeters` must be greater than zero, `curvature` must be between `0` and `1`, and placement values should be finite numbers. `curvature: 0` keeps the overlay flat. Positive curvature uses OpenVR overlay curvature on OpenVR and `XR_KHR_composition_layer_cylinder` cylinder layers on OpenXR.
|
|
68
69
|
|
|
69
|
-
On Linux, runtime selection prefers `openxr`, then falls back to `openvr`, then to `mock`. Linux OpenVR
|
|
70
|
+
On Linux, runtime selection prefers `openxr`, then falls back to `openvr`, then to `mock`. Linux OpenVR automatically uses public `TextureType_Vulkan`, regardless of the game's graphics API, with bitmap-to-Vulkan fallback when direct DMA-BUF import is unavailable. `ELECTRON_VR_DISABLE_OPENVR_VULKAN=1` disables it; `ELECTRON_VR_OPENVR_GL_UPLOAD=1` explicitly selects the slower OpenGL diagnostic path.
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
When `XR_EXTX_overlay` is unavailable, Linux x64 can use the explicitly installed API layer before falling back to OpenVR:
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
```bash
|
|
75
|
+
npx electron-vr-openxr-layer install
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The Linux layer supports Vulkan and desktop OpenGL Xlib sessions with one single-plane linear RGB DMA-BUF overlay. Vulkan DMA-BUF import is currently validated for Ubuntu 24.04 AMD RADV with Monado and `hello_xr -g Vulkan`; GLX retains its software snapshot fallback. Xcb, native Wayland host bindings, modifier-backed or multiplane buffers, and curvature are not yet supported.
|
|
79
|
+
|
|
80
|
+
On Windows x64, selection prefers a direct `XR_EXTX_overlay` session, then an installed implicit API layer for D3D11 or D3D12 hosts, then OpenVR, then mock. API-layer installation is explicit:
|
|
81
|
+
|
|
82
|
+
```powershell
|
|
83
|
+
npx electron-vr-openxr-layer install
|
|
84
|
+
npx electron-vr-openxr-layer status
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Use the same command with `enable`, `disable`, or `uninstall`. `npm install` does not register the layer. `ELECTRON_VR_DISABLE_OPENXR_API_LAYER=1` disables it for a process. The API layer supports one flat quad for D3D11 and D3D12 hosts; elevated applications and positive curvature are not yet supported.
|
|
88
|
+
|
|
89
|
+
Applications can manage the same per-user installation through the public API:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const installed = await VROverlay.installOpenXRApiLayer();
|
|
93
|
+
console.log(installed.installed, installed.enabled, installed.manifestPath);
|
|
94
|
+
|
|
95
|
+
const status = await VROverlay.getOpenXRApiLayerStatus();
|
|
96
|
+
await VROverlay.disableOpenXRApiLayer();
|
|
97
|
+
await VROverlay.enableOpenXRApiLayer();
|
|
98
|
+
await VROverlay.uninstallOpenXRApiLayer();
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The methods return `Promise<OpenXRApiLayerStatus>` and reject when the platform is unsupported or the utility fails. Installation should require explicit user consent. It affects subsequently started OpenXR applications, so restart an already running game after installing, enabling, disabling, or uninstalling the layer. These methods support Linux x64 and Windows x64 and do not require administrator/root access for the normal per-user installation.
|
|
102
|
+
|
|
103
|
+
For product UI, prefer the combined readiness report instead of interpreting raw probe fields:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const report = await VROverlay.getCompatibilityReport();
|
|
107
|
+
console.log(report.launch.verdict, report.launch.requiredActions);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`report.launch.wouldWorkNow` answers whether launching a browser overlay now is expected to produce real VR output. Otherwise, the verdict distinguishes an actionable setup/runtime/host requirement from a fundamental incompatibility. The report also exposes detailed readiness states, actionable issues, and per-feature support. See `PRODUCT_INTEGRATION.md` in the repository for the recommended consent, IPC, restart, recovery, diagnostics, and production UI flow.
|
|
111
|
+
|
|
112
|
+
`getRuntimeInfo()` also includes `openvrRuntimeInstalled`, `openvrRuntimePath`, `openxrMode`, API-layer installation and connection state, protocol version, and connected OpenXR host metadata. `probeMode` includes the backend-selection decision.
|
package/dist/VROverlay.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { BrowserWindow } from "electron";
|
|
2
2
|
import type { BrowserWindowConstructorOptions } from "electron";
|
|
3
3
|
import { type BackendKind, type RuntimeInfo, type Quat, type Vec3 } from "./bridge.js";
|
|
4
|
+
import { type OpenXRApiLayerStatus } from "./openxrApiLayer.js";
|
|
5
|
+
import { type VRCompatibilityReport } from "./compatibility.js";
|
|
4
6
|
type OverlayWindowOptions = Omit<BrowserWindowConstructorOptions, "width" | "height" | "webPreferences"> & {
|
|
5
7
|
webPreferences?: BrowserWindowConstructorOptions["webPreferences"];
|
|
6
8
|
};
|
|
@@ -12,6 +14,7 @@ export interface VROverlayOptions {
|
|
|
12
14
|
frameRate?: number;
|
|
13
15
|
windowOptions?: OverlayWindowOptions;
|
|
14
16
|
sizeMeters?: number;
|
|
17
|
+
curvature?: number;
|
|
15
18
|
visible?: boolean;
|
|
16
19
|
placement?: {
|
|
17
20
|
mode: "head" | "world";
|
|
@@ -23,6 +26,7 @@ export interface ExistingWindowVROverlayOptions {
|
|
|
23
26
|
name?: string;
|
|
24
27
|
frameRate?: number;
|
|
25
28
|
sizeMeters?: number;
|
|
29
|
+
curvature?: number;
|
|
26
30
|
visible?: boolean;
|
|
27
31
|
placement?: VROverlayOptions["placement"];
|
|
28
32
|
}
|
|
@@ -34,6 +38,7 @@ export declare class VROverlay {
|
|
|
34
38
|
readonly frameRate: number;
|
|
35
39
|
readonly windowOptions?: VROverlayOptions["windowOptions"];
|
|
36
40
|
private sizeMeters;
|
|
41
|
+
private curvature;
|
|
37
42
|
private visible;
|
|
38
43
|
private placement;
|
|
39
44
|
private readonly vrBridge;
|
|
@@ -44,6 +49,12 @@ export declare class VROverlay {
|
|
|
44
49
|
static getRuntimeInfo(): RuntimeInfo;
|
|
45
50
|
static isAvailable(runtimeInfo?: RuntimeInfo): boolean;
|
|
46
51
|
static hasRealVRRuntime(runtimeInfo?: RuntimeInfo): boolean;
|
|
52
|
+
static getCompatibilityReport(): Promise<VRCompatibilityReport>;
|
|
53
|
+
static installOpenXRApiLayer(): Promise<OpenXRApiLayerStatus>;
|
|
54
|
+
static getOpenXRApiLayerStatus(): Promise<OpenXRApiLayerStatus>;
|
|
55
|
+
static enableOpenXRApiLayer(): Promise<OpenXRApiLayerStatus>;
|
|
56
|
+
static disableOpenXRApiLayer(): Promise<OpenXRApiLayerStatus>;
|
|
57
|
+
static uninstallOpenXRApiLayer(): Promise<OpenXRApiLayerStatus>;
|
|
47
58
|
static openWindow(window: BrowserWindow, options?: ExistingWindowVROverlayOptions): Promise<VROverlay | null>;
|
|
48
59
|
getRuntimeInfo(): RuntimeInfo;
|
|
49
60
|
getSelectedBackend(): BackendKind;
|
|
@@ -54,6 +65,7 @@ export declare class VROverlay {
|
|
|
54
65
|
setPlacement(placement: VROverlayOptions["placement"]): boolean;
|
|
55
66
|
setVisible(visible: boolean): boolean;
|
|
56
67
|
setSizeMeters(sizeMeters: number): boolean;
|
|
68
|
+
setCurvature(curvature: number): boolean;
|
|
57
69
|
private initializeBridgeForWindow;
|
|
58
70
|
private readonly onWindowClosed;
|
|
59
71
|
}
|
package/dist/VROverlay.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { app, BrowserWindow } from "electron";
|
|
2
2
|
import { createVrBridge } from "./bridge.js";
|
|
3
|
-
import {
|
|
3
|
+
import { disableOpenXRApiLayer, enableOpenXRApiLayer, getOpenXRApiLayerStatus, installOpenXRApiLayer, uninstallOpenXRApiLayer } from "./openxrApiLayer.js";
|
|
4
|
+
import { assertCurvature, assertSizeMeters, normalizePlacement } from "./overlayOptions.js";
|
|
5
|
+
import { getVRCompatibilityReport } from "./compatibility.js";
|
|
4
6
|
function mergeWindowOptions(width, height, windowOptions) {
|
|
5
7
|
const requestedOffscreen = windowOptions?.webPreferences?.offscreen;
|
|
6
8
|
const normalizedOffscreen = typeof requestedOffscreen === "object"
|
|
@@ -35,6 +37,7 @@ export class VROverlay {
|
|
|
35
37
|
frameRate;
|
|
36
38
|
windowOptions;
|
|
37
39
|
sizeMeters;
|
|
40
|
+
curvature;
|
|
38
41
|
visible;
|
|
39
42
|
placement;
|
|
40
43
|
vrBridge = createVrBridge();
|
|
@@ -50,6 +53,8 @@ export class VROverlay {
|
|
|
50
53
|
this.windowOptions = options.windowOptions;
|
|
51
54
|
this.sizeMeters = options.sizeMeters ?? 1.0;
|
|
52
55
|
assertSizeMeters(this.sizeMeters);
|
|
56
|
+
this.curvature = options.curvature ?? 0.0;
|
|
57
|
+
assertCurvature(this.curvature);
|
|
53
58
|
this.visible = options.visible ?? true;
|
|
54
59
|
this.placement = normalizePlacement(options.placement);
|
|
55
60
|
}
|
|
@@ -62,6 +67,24 @@ export class VROverlay {
|
|
|
62
67
|
static hasRealVRRuntime(runtimeInfo = VROverlay.getRuntimeInfo()) {
|
|
63
68
|
return runtimeInfo.selectedBackend === "openxr" || runtimeInfo.selectedBackend === "openvr";
|
|
64
69
|
}
|
|
70
|
+
static getCompatibilityReport() {
|
|
71
|
+
return getVRCompatibilityReport();
|
|
72
|
+
}
|
|
73
|
+
static installOpenXRApiLayer() {
|
|
74
|
+
return installOpenXRApiLayer();
|
|
75
|
+
}
|
|
76
|
+
static getOpenXRApiLayerStatus() {
|
|
77
|
+
return getOpenXRApiLayerStatus();
|
|
78
|
+
}
|
|
79
|
+
static enableOpenXRApiLayer() {
|
|
80
|
+
return enableOpenXRApiLayer();
|
|
81
|
+
}
|
|
82
|
+
static disableOpenXRApiLayer() {
|
|
83
|
+
return disableOpenXRApiLayer();
|
|
84
|
+
}
|
|
85
|
+
static uninstallOpenXRApiLayer() {
|
|
86
|
+
return uninstallOpenXRApiLayer();
|
|
87
|
+
}
|
|
65
88
|
static async openWindow(window, options = {}) {
|
|
66
89
|
const [width, height] = window.getContentSize();
|
|
67
90
|
const overlay = new VROverlay({
|
|
@@ -70,6 +93,7 @@ export class VROverlay {
|
|
|
70
93
|
height,
|
|
71
94
|
frameRate: options.frameRate,
|
|
72
95
|
sizeMeters: options.sizeMeters,
|
|
96
|
+
curvature: options.curvature,
|
|
73
97
|
visible: options.visible,
|
|
74
98
|
placement: options.placement
|
|
75
99
|
});
|
|
@@ -174,12 +198,25 @@ export class VROverlay {
|
|
|
174
198
|
}
|
|
175
199
|
return success;
|
|
176
200
|
}
|
|
201
|
+
setCurvature(curvature) {
|
|
202
|
+
assertCurvature(curvature);
|
|
203
|
+
this.curvature = curvature;
|
|
204
|
+
if (!this.isInitialized()) {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
const success = this.vrBridge.setOverlayCurvature(curvature);
|
|
208
|
+
if (!success) {
|
|
209
|
+
console.error("Failed to update VR overlay curvature:", this.vrBridge.getLastError());
|
|
210
|
+
}
|
|
211
|
+
return success;
|
|
212
|
+
}
|
|
177
213
|
initializeBridgeForWindow(window, width, height) {
|
|
178
214
|
const initialized = this.vrBridge.initialize({
|
|
179
215
|
name: this.overlayName,
|
|
180
216
|
width,
|
|
181
217
|
height,
|
|
182
218
|
sizeMeters: this.sizeMeters,
|
|
219
|
+
curvature: this.curvature,
|
|
183
220
|
visible: this.visible,
|
|
184
221
|
placement: this.placement
|
|
185
222
|
});
|
package/dist/bridge.d.ts
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
import type { BrowserWindow } from "electron";
|
|
2
2
|
export type BackendKind = "none" | "openxr" | "openvr" | "mock";
|
|
3
|
+
export type OpenXRMode = "none" | "overlay-session" | "api-layer" | "standard-test-session";
|
|
3
4
|
export interface RuntimeInfo {
|
|
4
5
|
platform: string;
|
|
5
6
|
probeMode: string;
|
|
6
7
|
openxrAvailable: boolean;
|
|
7
8
|
openxrOverlayExtensionAvailable: boolean;
|
|
8
9
|
openxrLinuxEglBindingAvailable: boolean;
|
|
10
|
+
openxrLinuxOpenGlesBindingAvailable: boolean;
|
|
9
11
|
openxrWindowsD3D11BindingAvailable: boolean;
|
|
12
|
+
openxrWindowsD3D12BindingAvailable: boolean;
|
|
13
|
+
openxrMacosMetalBindingAvailable: boolean;
|
|
10
14
|
openxrRuntimeName: string;
|
|
11
15
|
openxrRuntimeManifestPath: string;
|
|
12
16
|
openxrRuntimeLibraryPath: string;
|
|
13
17
|
openxrLoaderPath: string;
|
|
14
18
|
openxrSessionState: string;
|
|
15
19
|
openxrSessionRunning: boolean;
|
|
20
|
+
openxrMode: OpenXRMode;
|
|
21
|
+
openxrApiLayerInstalled: boolean;
|
|
22
|
+
openxrApiLayerEnabled: boolean;
|
|
23
|
+
openxrApiLayerManifestPath: string;
|
|
24
|
+
openxrCompanionConnected: boolean;
|
|
25
|
+
openxrHostProcessId: number;
|
|
26
|
+
openxrHostApplicationName: string;
|
|
27
|
+
openxrHostGraphicsApi: string;
|
|
28
|
+
openxrHostAdapterLuid: string;
|
|
29
|
+
openxrProtocolVersion: number;
|
|
16
30
|
openvrAvailable: boolean;
|
|
17
31
|
openvrRuntimeInstalled: boolean;
|
|
18
32
|
openvrRuntimePath: string;
|
|
@@ -45,6 +59,7 @@ export interface InitializeVROptions {
|
|
|
45
59
|
width: number;
|
|
46
60
|
height: number;
|
|
47
61
|
sizeMeters: number;
|
|
62
|
+
curvature?: number;
|
|
48
63
|
visible: boolean;
|
|
49
64
|
placement: OverlayPlacement;
|
|
50
65
|
}
|
|
@@ -91,12 +106,20 @@ export declare class VrBridge {
|
|
|
91
106
|
private warnedAboutMissingSharedTexture;
|
|
92
107
|
private warnedAboutSoftwareFallback;
|
|
93
108
|
private warnedAboutWindowsSoftwareFallback;
|
|
109
|
+
private warnedAboutEmptyLinuxBitmap;
|
|
94
110
|
private loggedFirstPaint;
|
|
95
111
|
private loggedFirstWindowsHandle;
|
|
96
112
|
private loggedFirstWindowsSubmit;
|
|
97
113
|
private loggedFirstWindowsSoftwareSubmit;
|
|
114
|
+
private loggedFirstLinuxOpenGLSubmit;
|
|
115
|
+
private loggedFirstLinuxOpenXRSoftwareSubmit;
|
|
116
|
+
private loggedFirstLinuxOpenVRVulkanSoftwareSubmit;
|
|
117
|
+
private linuxOpenVRVisibleTexture;
|
|
98
118
|
private windowsReadbackInFlight;
|
|
99
119
|
private windowsReadbackPending;
|
|
120
|
+
private linuxReadbackInFlight;
|
|
121
|
+
private linuxReadbackPending;
|
|
122
|
+
private invalidationTimer;
|
|
100
123
|
attachWindow(window: BrowserWindow, options?: AttachWindowOptions): void;
|
|
101
124
|
detachWindow(): void;
|
|
102
125
|
getRuntimeInfo(): RuntimeInfo;
|
|
@@ -105,10 +128,15 @@ export declare class VrBridge {
|
|
|
105
128
|
setOverlayPlacement(placement: OverlayPlacement): boolean;
|
|
106
129
|
setOverlayVisible(visible: boolean): boolean;
|
|
107
130
|
setOverlaySizeMeters(sizeMeters: number): boolean;
|
|
131
|
+
setOverlayCurvature(curvature: number): boolean;
|
|
108
132
|
shutdown(): void;
|
|
109
133
|
isInitialized(): boolean;
|
|
110
134
|
getLastError(): string | null;
|
|
111
135
|
private submitWindowsSoftwareFrame;
|
|
136
|
+
private submitLinuxBitmapFrame;
|
|
137
|
+
private submitLinuxOpenGLFrame;
|
|
138
|
+
private submitLinuxOpenXRSoftwareFrame;
|
|
139
|
+
private scheduleLinuxCaptureFallback;
|
|
112
140
|
private useWindowsSoftwareFallback;
|
|
113
141
|
private scheduleWindowsCaptureFallback;
|
|
114
142
|
private readonly onPaint;
|
package/dist/bridge.js
CHANGED
|
@@ -110,6 +110,17 @@ function sanitizeRuntimeInfo(runtimeInfo) {
|
|
|
110
110
|
...runtimeInfo,
|
|
111
111
|
openxrSessionState: runtimeInfo.openxrSessionState ?? "unknown",
|
|
112
112
|
openxrSessionRunning: runtimeInfo.openxrSessionRunning ?? false,
|
|
113
|
+
openxrWindowsD3D12BindingAvailable: runtimeInfo.openxrWindowsD3D12BindingAvailable ?? false,
|
|
114
|
+
openxrMode: runtimeInfo.openxrMode ?? "none",
|
|
115
|
+
openxrApiLayerInstalled: runtimeInfo.openxrApiLayerInstalled ?? false,
|
|
116
|
+
openxrApiLayerEnabled: runtimeInfo.openxrApiLayerEnabled ?? false,
|
|
117
|
+
openxrApiLayerManifestPath: runtimeInfo.openxrApiLayerManifestPath ?? "",
|
|
118
|
+
openxrCompanionConnected: runtimeInfo.openxrCompanionConnected ?? false,
|
|
119
|
+
openxrHostProcessId: runtimeInfo.openxrHostProcessId ?? 0,
|
|
120
|
+
openxrHostApplicationName: runtimeInfo.openxrHostApplicationName ?? "",
|
|
121
|
+
openxrHostGraphicsApi: runtimeInfo.openxrHostGraphicsApi ?? "",
|
|
122
|
+
openxrHostAdapterLuid: runtimeInfo.openxrHostAdapterLuid ?? "",
|
|
123
|
+
openxrProtocolVersion: runtimeInfo.openxrProtocolVersion ?? 0,
|
|
113
124
|
openvrSceneApplicationState: runtimeInfo.openvrSceneApplicationState ?? "",
|
|
114
125
|
openvrSceneProcessId: runtimeInfo.openvrSceneProcessId ?? 0,
|
|
115
126
|
openvrSceneApplicationKey: runtimeInfo.openvrSceneApplicationKey ?? "",
|
|
@@ -141,6 +152,25 @@ function bgraToRgba(source) {
|
|
|
141
152
|
}
|
|
142
153
|
return result;
|
|
143
154
|
}
|
|
155
|
+
function bgraToVerticallyFlippedRgba(source, width, height) {
|
|
156
|
+
const result = Buffer.allocUnsafe(source.length);
|
|
157
|
+
const rowBytes = width * 4;
|
|
158
|
+
for (let sourceRow = 0; sourceRow < height; sourceRow += 1) {
|
|
159
|
+
const sourceOffset = sourceRow * rowBytes;
|
|
160
|
+
const targetOffset = (height - sourceRow - 1) * rowBytes;
|
|
161
|
+
for (let columnOffset = 0; columnOffset < rowBytes; columnOffset += 4) {
|
|
162
|
+
result[targetOffset + columnOffset] = source[sourceOffset + columnOffset + 2];
|
|
163
|
+
result[targetOffset + columnOffset + 1] = source[sourceOffset + columnOffset + 1];
|
|
164
|
+
result[targetOffset + columnOffset + 2] = source[sourceOffset + columnOffset];
|
|
165
|
+
result[targetOffset + columnOffset + 3] = source[sourceOffset + columnOffset + 3];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
function isTruthyEnvironmentVariable(name) {
|
|
171
|
+
const value = process.env[name]?.toLowerCase();
|
|
172
|
+
return value !== undefined && value !== "" && value !== "0" && value !== "false" && value !== "off";
|
|
173
|
+
}
|
|
144
174
|
function releaseTexture(texture) {
|
|
145
175
|
if (texture && typeof texture.release === "function") {
|
|
146
176
|
texture.release();
|
|
@@ -170,23 +200,48 @@ export class VrBridge {
|
|
|
170
200
|
warnedAboutMissingSharedTexture = false;
|
|
171
201
|
warnedAboutSoftwareFallback = false;
|
|
172
202
|
warnedAboutWindowsSoftwareFallback = false;
|
|
203
|
+
warnedAboutEmptyLinuxBitmap = false;
|
|
173
204
|
loggedFirstPaint = false;
|
|
174
205
|
loggedFirstWindowsHandle = false;
|
|
175
206
|
loggedFirstWindowsSubmit = false;
|
|
176
207
|
loggedFirstWindowsSoftwareSubmit = false;
|
|
208
|
+
loggedFirstLinuxOpenGLSubmit = false;
|
|
209
|
+
loggedFirstLinuxOpenXRSoftwareSubmit = false;
|
|
210
|
+
loggedFirstLinuxOpenVRVulkanSoftwareSubmit = false;
|
|
211
|
+
linuxOpenVRVisibleTexture = null;
|
|
177
212
|
windowsReadbackInFlight = false;
|
|
178
213
|
windowsReadbackPending = false;
|
|
214
|
+
linuxReadbackInFlight = false;
|
|
215
|
+
linuxReadbackPending = false;
|
|
216
|
+
invalidationTimer = null;
|
|
179
217
|
attachWindow(window, options = {}) {
|
|
180
218
|
this.detachWindow();
|
|
181
219
|
const offscreenContents = window.webContents;
|
|
182
|
-
|
|
220
|
+
const frameRate = options.frameRate ?? 60;
|
|
221
|
+
offscreenContents.setFrameRate(frameRate);
|
|
183
222
|
offscreenContents.on("paint", this.onPaint);
|
|
184
223
|
this.attachedWindow = window;
|
|
224
|
+
if (this.getSelectedBackend() === "openxr" ||
|
|
225
|
+
(process.platform === "linux" &&
|
|
226
|
+
(isTruthyEnvironmentVariable("ELECTRON_VR_OPENVR_GL_UPLOAD") ||
|
|
227
|
+
(this.getSelectedBackend() === "openvr" &&
|
|
228
|
+
!isTruthyEnvironmentVariable("ELECTRON_VR_DISABLE_OPENVR_VULKAN"))))) {
|
|
229
|
+
this.invalidationTimer = setInterval(() => {
|
|
230
|
+
if (this.attachedWindow && !this.attachedWindow.isDestroyed()) {
|
|
231
|
+
this.attachedWindow.webContents.invalidate();
|
|
232
|
+
}
|
|
233
|
+
}, 1000 / frameRate);
|
|
234
|
+
}
|
|
185
235
|
}
|
|
186
236
|
detachWindow() {
|
|
237
|
+
if (this.invalidationTimer) {
|
|
238
|
+
clearInterval(this.invalidationTimer);
|
|
239
|
+
this.invalidationTimer = null;
|
|
240
|
+
}
|
|
187
241
|
if (!this.attachedWindow) {
|
|
188
242
|
return;
|
|
189
243
|
}
|
|
244
|
+
this.linuxReadbackPending = false;
|
|
190
245
|
const offscreenContents = this.attachedWindow.webContents;
|
|
191
246
|
offscreenContents.removeListener("paint", this.onPaint);
|
|
192
247
|
this.attachedWindow = null;
|
|
@@ -198,7 +253,13 @@ export class VrBridge {
|
|
|
198
253
|
return this.getRuntimeInfo().selectedBackend;
|
|
199
254
|
}
|
|
200
255
|
initialize(options) {
|
|
201
|
-
|
|
256
|
+
try {
|
|
257
|
+
return this.addon.initializeVR(options);
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
releaseTexture(this.linuxOpenVRVisibleTexture);
|
|
261
|
+
this.linuxOpenVRVisibleTexture = null;
|
|
262
|
+
}
|
|
202
263
|
}
|
|
203
264
|
setOverlayPlacement(placement) {
|
|
204
265
|
return this.addon.setOverlayPlacement(placement);
|
|
@@ -209,9 +270,18 @@ export class VrBridge {
|
|
|
209
270
|
setOverlaySizeMeters(sizeMeters) {
|
|
210
271
|
return this.addon.setOverlaySizeMeters(sizeMeters);
|
|
211
272
|
}
|
|
273
|
+
setOverlayCurvature(curvature) {
|
|
274
|
+
return this.addon.setOverlayCurvature(curvature);
|
|
275
|
+
}
|
|
212
276
|
shutdown() {
|
|
213
277
|
this.detachWindow();
|
|
214
|
-
|
|
278
|
+
try {
|
|
279
|
+
this.addon.shutdownVR();
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
releaseTexture(this.linuxOpenVRVisibleTexture);
|
|
283
|
+
this.linuxOpenVRVisibleTexture = null;
|
|
284
|
+
}
|
|
215
285
|
}
|
|
216
286
|
isInitialized() {
|
|
217
287
|
return this.addon.isInitialized();
|
|
@@ -241,6 +311,118 @@ export class VrBridge {
|
|
|
241
311
|
}
|
|
242
312
|
return true;
|
|
243
313
|
}
|
|
314
|
+
submitLinuxBitmapFrame(nativeImage, verticallyFlip, allowCaptureFallback = true) {
|
|
315
|
+
const openvrVulkanSoftware = !verticallyFlip && this.getSelectedBackend() === "openvr";
|
|
316
|
+
const transportName = verticallyFlip
|
|
317
|
+
? "OpenVR OpenGL upload"
|
|
318
|
+
: openvrVulkanSoftware
|
|
319
|
+
? "OpenVR Vulkan software upload"
|
|
320
|
+
: "OpenXR API-layer software transport";
|
|
321
|
+
if (!nativeImage) {
|
|
322
|
+
if (!this.warnedAboutMissingSharedTexture) {
|
|
323
|
+
this.warnedAboutMissingSharedTexture = true;
|
|
324
|
+
console.warn(`Linux ${transportName} requires Electron bitmap paint data.`);
|
|
325
|
+
}
|
|
326
|
+
if (allowCaptureFallback) {
|
|
327
|
+
this.scheduleLinuxCaptureFallback();
|
|
328
|
+
}
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
const { width, height } = nativeImage.getSize();
|
|
332
|
+
if (width === 0 || height === 0) {
|
|
333
|
+
if (!this.warnedAboutEmptyLinuxBitmap) {
|
|
334
|
+
this.warnedAboutEmptyLinuxBitmap = true;
|
|
335
|
+
console.warn(`Linux ${transportName} paint bitmap was empty (width=${width}, height=${height}); using capturePage readback.`);
|
|
336
|
+
}
|
|
337
|
+
if (allowCaptureFallback) {
|
|
338
|
+
this.scheduleLinuxCaptureFallback();
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
const bitmap = nativeImage.toBitmap();
|
|
343
|
+
if (bitmap.length === 0) {
|
|
344
|
+
if (!this.warnedAboutEmptyLinuxBitmap) {
|
|
345
|
+
this.warnedAboutEmptyLinuxBitmap = true;
|
|
346
|
+
console.warn(`Linux ${transportName} paint bitmap contained no pixels; using capturePage readback.`);
|
|
347
|
+
}
|
|
348
|
+
if (allowCaptureFallback) {
|
|
349
|
+
this.scheduleLinuxCaptureFallback();
|
|
350
|
+
}
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
const submitted = this.addon.submitSoftwareFrame({
|
|
354
|
+
width,
|
|
355
|
+
height,
|
|
356
|
+
rgbaPixels: verticallyFlip ? bgraToVerticallyFlippedRgba(bitmap, width, height) : bgraToRgba(bitmap)
|
|
357
|
+
});
|
|
358
|
+
if (!submitted) {
|
|
359
|
+
console.error("Failed to submit Linux frame to VR bridge:", this.addon.getLastError());
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
if (verticallyFlip && !this.loggedFirstLinuxOpenGLSubmit) {
|
|
363
|
+
this.loggedFirstLinuxOpenGLSubmit = true;
|
|
364
|
+
console.log("VR overlay submitted first Linux frame through OpenGL upload.");
|
|
365
|
+
}
|
|
366
|
+
else if (openvrVulkanSoftware && !this.loggedFirstLinuxOpenVRVulkanSoftwareSubmit) {
|
|
367
|
+
this.loggedFirstLinuxOpenVRVulkanSoftwareSubmit = true;
|
|
368
|
+
console.log("VR overlay submitted first Linux frame through OpenVR Vulkan software upload.");
|
|
369
|
+
}
|
|
370
|
+
else if (!openvrVulkanSoftware && !verticallyFlip && !this.loggedFirstLinuxOpenXRSoftwareSubmit) {
|
|
371
|
+
this.loggedFirstLinuxOpenXRSoftwareSubmit = true;
|
|
372
|
+
console.log("VR overlay submitted first Linux frame through OpenXR API-layer software transport.");
|
|
373
|
+
}
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
submitLinuxOpenGLFrame(nativeImage, allowCaptureFallback = true) {
|
|
377
|
+
return this.submitLinuxBitmapFrame(nativeImage, true, allowCaptureFallback);
|
|
378
|
+
}
|
|
379
|
+
submitLinuxOpenXRSoftwareFrame(nativeImage, allowCaptureFallback = true) {
|
|
380
|
+
return this.submitLinuxBitmapFrame(nativeImage, false, allowCaptureFallback);
|
|
381
|
+
}
|
|
382
|
+
scheduleLinuxCaptureFallback() {
|
|
383
|
+
if (!this.attachedWindow) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (this.linuxReadbackInFlight) {
|
|
387
|
+
this.linuxReadbackPending = true;
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
this.linuxReadbackInFlight = true;
|
|
391
|
+
const window = this.attachedWindow;
|
|
392
|
+
const offscreenContents = window.webContents;
|
|
393
|
+
const [width, height] = window.getContentSize();
|
|
394
|
+
void offscreenContents.capturePage({ x: 0, y: 0, width, height })
|
|
395
|
+
.then((image) => {
|
|
396
|
+
if (this.attachedWindow !== window || !this.isInitialized()) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const runtimeInfo = this.getRuntimeInfo();
|
|
400
|
+
if (runtimeInfo.selectedBackend === "openxr" && runtimeInfo.openxrMode === "api-layer") {
|
|
401
|
+
if (runtimeInfo.openxrCompanionConnected && runtimeInfo.openxrHostGraphicsApi === "opengl-xlib") {
|
|
402
|
+
this.submitLinuxOpenXRSoftwareFrame(image, false);
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (runtimeInfo.selectedBackend === "openvr" &&
|
|
407
|
+
!isTruthyEnvironmentVariable("ELECTRON_VR_DISABLE_OPENVR_VULKAN") &&
|
|
408
|
+
!isTruthyEnvironmentVariable("ELECTRON_VR_OPENVR_GL_UPLOAD")) {
|
|
409
|
+
this.submitLinuxBitmapFrame(image, false, false);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
this.submitLinuxOpenGLFrame(image, false);
|
|
413
|
+
}
|
|
414
|
+
})
|
|
415
|
+
.catch((error) => {
|
|
416
|
+
console.error("Failed to capture Linux OpenGL upload frame:", error);
|
|
417
|
+
})
|
|
418
|
+
.finally(() => {
|
|
419
|
+
this.linuxReadbackInFlight = false;
|
|
420
|
+
if (this.linuxReadbackPending) {
|
|
421
|
+
this.linuxReadbackPending = false;
|
|
422
|
+
this.scheduleLinuxCaptureFallback();
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
244
426
|
useWindowsSoftwareFallback(nativeImage, warning) {
|
|
245
427
|
if (!this.warnedAboutWindowsSoftwareFallback) {
|
|
246
428
|
this.warnedAboutWindowsSoftwareFallback = true;
|
|
@@ -289,7 +471,43 @@ export class VrBridge {
|
|
|
289
471
|
this.loggedFirstPaint = true;
|
|
290
472
|
console.log(`VR overlay received first paint event (platform=${process.platform}, backend=${this.getSelectedBackend()}, hasTexture=${texture ? "yes" : "no"}, hasBitmap=${nativeImage ? "yes" : "no"}).`);
|
|
291
473
|
}
|
|
474
|
+
const runtimeInfo = this.getRuntimeInfo();
|
|
475
|
+
if (process.platform === "linux" &&
|
|
476
|
+
runtimeInfo.selectedBackend === "openvr" &&
|
|
477
|
+
isTruthyEnvironmentVariable("ELECTRON_VR_OPENVR_VULKAN_SOFTWARE")) {
|
|
478
|
+
try {
|
|
479
|
+
this.submitLinuxBitmapFrame(nativeImage, false);
|
|
480
|
+
}
|
|
481
|
+
finally {
|
|
482
|
+
releaseTexture(texture);
|
|
483
|
+
}
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (process.platform === "linux" &&
|
|
487
|
+
runtimeInfo.selectedBackend === "openxr" &&
|
|
488
|
+
runtimeInfo.openxrMode === "api-layer" &&
|
|
489
|
+
(!runtimeInfo.openxrCompanionConnected || runtimeInfo.openxrHostGraphicsApi === "opengl-xlib")) {
|
|
490
|
+
try {
|
|
491
|
+
if (runtimeInfo.openxrCompanionConnected) {
|
|
492
|
+
this.submitLinuxOpenXRSoftwareFrame(nativeImage);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
finally {
|
|
496
|
+
releaseTexture(texture);
|
|
497
|
+
}
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
292
500
|
if (!texture) {
|
|
501
|
+
if (process.platform === "linux" &&
|
|
502
|
+
this.getSelectedBackend() === "openvr") {
|
|
503
|
+
if (isTruthyEnvironmentVariable("ELECTRON_VR_OPENVR_GL_UPLOAD")) {
|
|
504
|
+
this.submitLinuxOpenGLFrame(nativeImage);
|
|
505
|
+
}
|
|
506
|
+
else if (!isTruthyEnvironmentVariable("ELECTRON_VR_DISABLE_OPENVR_VULKAN")) {
|
|
507
|
+
this.submitLinuxBitmapFrame(nativeImage, false);
|
|
508
|
+
}
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
293
511
|
if (process.platform === "win32" && this.getSelectedBackend() === "openvr") {
|
|
294
512
|
this.useWindowsSoftwareFallback(nativeImage, "Windows shared texture payload was unavailable; falling back to software RGBA upload.");
|
|
295
513
|
return;
|
|
@@ -313,6 +531,7 @@ export class VrBridge {
|
|
|
313
531
|
}
|
|
314
532
|
return;
|
|
315
533
|
}
|
|
534
|
+
let releaseCurrentTexture = true;
|
|
316
535
|
try {
|
|
317
536
|
const textureInfo = texture.textureInfo;
|
|
318
537
|
const handle = textureInfo?.sharedTextureHandle;
|
|
@@ -344,24 +563,54 @@ export class VrBridge {
|
|
|
344
563
|
return;
|
|
345
564
|
}
|
|
346
565
|
if (process.platform === "linux") {
|
|
347
|
-
if (
|
|
566
|
+
if (this.getSelectedBackend() === "openvr" &&
|
|
567
|
+
isTruthyEnvironmentVariable("ELECTRON_VR_OPENVR_GL_UPLOAD")) {
|
|
568
|
+
this.submitLinuxOpenGLFrame(nativeImage);
|
|
569
|
+
}
|
|
570
|
+
else if (textureInfo?.planes?.length) {
|
|
348
571
|
const textureInfoSnapshot = cloneLinuxTextureInfo(textureInfo);
|
|
349
572
|
const submitted = this.addon.submitSharedTexture(textureInfoSnapshot);
|
|
350
573
|
if (!submitted) {
|
|
351
574
|
console.error("Failed to submit Linux frame to VR bridge:", this.addon.getLastError());
|
|
575
|
+
if (this.getSelectedBackend() === "openvr" &&
|
|
576
|
+
!isTruthyEnvironmentVariable("ELECTRON_VR_DISABLE_OPENVR_VULKAN")) {
|
|
577
|
+
this.submitLinuxBitmapFrame(nativeImage, false);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
else if (this.getSelectedBackend() === "openvr" &&
|
|
581
|
+
isTruthyEnvironmentVariable("ELECTRON_VR_DISABLE_OPENVR_VULKAN")) {
|
|
582
|
+
const previousTexture = this.linuxOpenVRVisibleTexture;
|
|
583
|
+
this.linuxOpenVRVisibleTexture = texture;
|
|
584
|
+
releaseCurrentTexture = false;
|
|
585
|
+
releaseTexture(previousTexture);
|
|
352
586
|
}
|
|
353
587
|
}
|
|
354
588
|
else if (!this.warnedAboutMissingSharedTexture) {
|
|
355
589
|
this.warnedAboutMissingSharedTexture = true;
|
|
356
590
|
console.warn("Shared texture metadata was unavailable on paint; preview submission was skipped.");
|
|
357
591
|
}
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (process.platform === "darwin") {
|
|
595
|
+
if (Buffer.isBuffer(handle) && textureInfo?.codedSize) {
|
|
596
|
+
const submitted = this.addon.submitSharedTexture(textureInfo);
|
|
597
|
+
if (!submitted) {
|
|
598
|
+
console.error("Failed to submit macOS frame to VR bridge:", this.addon.getLastError());
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
else if (!this.warnedAboutMissingSharedTexture) {
|
|
602
|
+
this.warnedAboutMissingSharedTexture = true;
|
|
603
|
+
console.warn("IOSurface metadata was unavailable on paint; OpenXR submission was skipped.");
|
|
604
|
+
}
|
|
358
605
|
}
|
|
359
606
|
}
|
|
360
607
|
catch (error) {
|
|
361
608
|
console.error("Error while forwarding frame to VR bridge:", error);
|
|
362
609
|
}
|
|
363
610
|
finally {
|
|
364
|
-
|
|
611
|
+
if (releaseCurrentTexture) {
|
|
612
|
+
releaseTexture(texture);
|
|
613
|
+
}
|
|
365
614
|
}
|
|
366
615
|
};
|
|
367
616
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type BackendKind, type OpenXRMode, type RuntimeInfo } from "./bridge.js";
|
|
2
|
+
import { type OpenXRApiLayerStatus } from "./openxrApiLayer.js";
|
|
3
|
+
export type VRReadiness = "ready" | "setup-required" | "waiting-for-host" | "fallback-only" | "unavailable" | "development-only";
|
|
4
|
+
export type VRLaunchVerdict = "works-now" | "action-required" | "incompatible";
|
|
5
|
+
export type VRFeatureSupport = "supported" | "unsupported" | "runtime-dependent";
|
|
6
|
+
export type VRIssueSeverity = "info" | "warning" | "error";
|
|
7
|
+
export type VRSetupAction = "install-openxr-api-layer" | "enable-openxr-api-layer" | "restart-openxr-apps" | "start-openxr-app" | "install-xr-runtime" | "use-supported-openxr-host" | "none";
|
|
8
|
+
export interface VRFeatureCompatibility {
|
|
9
|
+
flatOverlay: VRFeatureSupport;
|
|
10
|
+
curvature: VRFeatureSupport;
|
|
11
|
+
headLockedPlacement: VRFeatureSupport;
|
|
12
|
+
worldLockedPlacement: VRFeatureSupport;
|
|
13
|
+
alphaTransparency: VRFeatureSupport;
|
|
14
|
+
multipleOverlays: VRFeatureSupport;
|
|
15
|
+
softwareFrameFallback: VRFeatureSupport;
|
|
16
|
+
}
|
|
17
|
+
export interface VRCompatibilityIssue {
|
|
18
|
+
code: string;
|
|
19
|
+
severity: VRIssueSeverity;
|
|
20
|
+
title: string;
|
|
21
|
+
message: string;
|
|
22
|
+
action: VRSetupAction;
|
|
23
|
+
diagnosticDetails?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface VRLaunchAssessment {
|
|
26
|
+
verdict: VRLaunchVerdict;
|
|
27
|
+
wouldWorkNow: boolean;
|
|
28
|
+
fundamentalIncompatibility: boolean;
|
|
29
|
+
message: string;
|
|
30
|
+
requiredActions: VRSetupAction[];
|
|
31
|
+
}
|
|
32
|
+
export interface VRCompatibilityReport {
|
|
33
|
+
readiness: VRReadiness;
|
|
34
|
+
backend: BackendKind;
|
|
35
|
+
openxrMode: OpenXRMode;
|
|
36
|
+
backendLabel: string;
|
|
37
|
+
summary: string;
|
|
38
|
+
launch: VRLaunchAssessment;
|
|
39
|
+
canRenderOverlay: boolean;
|
|
40
|
+
isRealVrBackend: boolean;
|
|
41
|
+
requiresApiLayer: boolean;
|
|
42
|
+
requiresOpenXRAppRestart: boolean;
|
|
43
|
+
apiLayer: OpenXRApiLayerStatus | null;
|
|
44
|
+
compatibleHostGraphicsApis: string[];
|
|
45
|
+
features: VRFeatureCompatibility;
|
|
46
|
+
issues: VRCompatibilityIssue[];
|
|
47
|
+
recommendedAction: VRSetupAction;
|
|
48
|
+
diagnostics: RuntimeInfo;
|
|
49
|
+
}
|
|
50
|
+
export interface VRCompatibilityContext {
|
|
51
|
+
architecture?: string;
|
|
52
|
+
apiLayer?: OpenXRApiLayerStatus | null;
|
|
53
|
+
apiLayerStatusError?: string;
|
|
54
|
+
}
|
|
55
|
+
export declare function analyzeVRCompatibility(runtime: RuntimeInfo, context?: VRCompatibilityContext): VRCompatibilityReport;
|
|
56
|
+
export declare function getVRCompatibilityReport(): Promise<VRCompatibilityReport>;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { createVrBridge } from "./bridge.js";
|
|
2
|
+
import { getOpenXRApiLayerStatus } from "./openxrApiLayer.js";
|
|
3
|
+
const realFeatures = {
|
|
4
|
+
flatOverlay: "supported",
|
|
5
|
+
curvature: "unsupported",
|
|
6
|
+
headLockedPlacement: "supported",
|
|
7
|
+
worldLockedPlacement: "supported",
|
|
8
|
+
alphaTransparency: "supported",
|
|
9
|
+
multipleOverlays: "unsupported",
|
|
10
|
+
softwareFrameFallback: "unsupported"
|
|
11
|
+
};
|
|
12
|
+
function issue(code, severity, title, message, action = "none", diagnosticDetails) {
|
|
13
|
+
return { code, severity, title, message, action, ...(diagnosticDetails ? { diagnosticDetails } : {}) };
|
|
14
|
+
}
|
|
15
|
+
function directOpenXRFeatures() {
|
|
16
|
+
return { ...realFeatures, curvature: "runtime-dependent" };
|
|
17
|
+
}
|
|
18
|
+
function openVRFeatures() {
|
|
19
|
+
return { ...realFeatures, curvature: "supported", softwareFrameFallback: "supported" };
|
|
20
|
+
}
|
|
21
|
+
function unavailableFeatures(platform, backend) {
|
|
22
|
+
const linuxMock = platform === "linux" && backend === "mock";
|
|
23
|
+
return {
|
|
24
|
+
flatOverlay: linuxMock ? "supported" : "unsupported",
|
|
25
|
+
curvature: "unsupported",
|
|
26
|
+
headLockedPlacement: "unsupported",
|
|
27
|
+
worldLockedPlacement: "unsupported",
|
|
28
|
+
alphaTransparency: linuxMock ? "supported" : "unsupported",
|
|
29
|
+
multipleOverlays: "unsupported",
|
|
30
|
+
softwareFrameFallback: linuxMock ? "supported" : "unsupported"
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function analyzeVRCompatibility(runtime, context = {}) {
|
|
34
|
+
const { apiLayer = null, apiLayerStatusError } = context;
|
|
35
|
+
const issues = [];
|
|
36
|
+
const platformSupportsLayer = (runtime.platform === "win32" || runtime.platform === "linux") &&
|
|
37
|
+
(context.architecture ?? process.arch) === "x64";
|
|
38
|
+
const layerInstalled = apiLayer?.installed ?? runtime.openxrApiLayerInstalled;
|
|
39
|
+
const layerEnabled = apiLayer?.enabled ?? runtime.openxrApiLayerEnabled;
|
|
40
|
+
let readiness = "unavailable";
|
|
41
|
+
let backendLabel = "No compatible VR backend";
|
|
42
|
+
let summary = "No compatible XR runtime is ready.";
|
|
43
|
+
let features = unavailableFeatures(runtime.platform, runtime.selectedBackend);
|
|
44
|
+
let compatibleHostGraphicsApis = [];
|
|
45
|
+
let requiresApiLayer = false;
|
|
46
|
+
let recommendedAction = "none";
|
|
47
|
+
if (runtime.selectedBackend === "openxr" && runtime.openxrMode === "overlay-session") {
|
|
48
|
+
readiness = "ready";
|
|
49
|
+
backendLabel = "OpenXR direct overlay";
|
|
50
|
+
summary = "The active OpenXR runtime supports a direct overlay session.";
|
|
51
|
+
features = directOpenXRFeatures();
|
|
52
|
+
compatibleHostGraphicsApis = runtime.platform === "win32" ? ["D3D11"] : ["EGL/OpenGL ES"];
|
|
53
|
+
}
|
|
54
|
+
else if (runtime.selectedBackend === "openxr" && runtime.openxrMode === "api-layer") {
|
|
55
|
+
requiresApiLayer = true;
|
|
56
|
+
features = { ...realFeatures };
|
|
57
|
+
compatibleHostGraphicsApis = runtime.platform === "win32" ? ["D3D11", "D3D12"] : ["Vulkan", "OpenGL Xlib/GLX"];
|
|
58
|
+
backendLabel = "OpenXR application integration";
|
|
59
|
+
if (runtime.openxrCompanionConnected) {
|
|
60
|
+
readiness = "ready";
|
|
61
|
+
summary = `Connected to ${runtime.openxrHostApplicationName || "an OpenXR application"} using ${runtime.openxrHostGraphicsApi || "a supported graphics API"}.`;
|
|
62
|
+
}
|
|
63
|
+
else if (!layerEnabled) {
|
|
64
|
+
readiness = "setup-required";
|
|
65
|
+
summary = "OpenXR application integration is installed but disabled.";
|
|
66
|
+
recommendedAction = "enable-openxr-api-layer";
|
|
67
|
+
issues.push(issue("openxr-api-layer-disabled", "warning", "OpenXR integration is disabled", "Enable the per-user integration and restart OpenXR applications.", "enable-openxr-api-layer"));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
readiness = "waiting-for-host";
|
|
71
|
+
summary = "OpenXR integration is ready. Start or restart a compatible OpenXR application.";
|
|
72
|
+
recommendedAction = "start-openxr-app";
|
|
73
|
+
issues.push(issue("openxr-host-not-connected", "info", "Waiting for a VR application", `Start a compatible ${compatibleHostGraphicsApis.join(" or ")} OpenXR application. If it was already running when integration was enabled, restart it.`, "start-openxr-app"));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (runtime.selectedBackend === "openxr" && runtime.openxrMode === "standard-test-session") {
|
|
77
|
+
readiness = "development-only";
|
|
78
|
+
backendLabel = "OpenXR test session";
|
|
79
|
+
summary = "OpenXR is available as a development test session, not as a production coexistence overlay.";
|
|
80
|
+
features = directOpenXRFeatures();
|
|
81
|
+
issues.push(issue("openxr-test-session-only", "warning", "Development mode only", "This session cannot place the overlay above an unrelated OpenXR application."));
|
|
82
|
+
}
|
|
83
|
+
else if (runtime.selectedBackend === "openvr") {
|
|
84
|
+
readiness = "ready";
|
|
85
|
+
backendLabel = "SteamVR/OpenVR overlay";
|
|
86
|
+
summary = "A registered OpenVR runtime is ready for native overlays.";
|
|
87
|
+
features = openVRFeatures();
|
|
88
|
+
compatibleHostGraphicsApis = runtime.platform === "win32" ? ["SteamVR/OpenVR"] : ["SteamVR/OpenVR with single-plane RGB DMA-BUF"];
|
|
89
|
+
if (runtime.openxrAvailable && platformSupportsLayer && !layerInstalled) {
|
|
90
|
+
issues.push(issue("openxr-api-layer-optional", "info", "OpenXR application support is optional", "SteamVR overlays are ready. Install OpenXR application integration only if overlays are also needed in OpenXR applications without native overlay support.", "install-openxr-api-layer"));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const canInstallLayer = platformSupportsLayer && runtime.openxrAvailable &&
|
|
95
|
+
(runtime.platform === "linux" || runtime.openxrWindowsD3D11BindingAvailable || runtime.openxrWindowsD3D12BindingAvailable);
|
|
96
|
+
if (canInstallLayer && !layerInstalled) {
|
|
97
|
+
readiness = "setup-required";
|
|
98
|
+
backendLabel = "Desktop preview";
|
|
99
|
+
summary = "OpenXR is installed, but application integration must be enabled before overlays can appear in compatible OpenXR applications.";
|
|
100
|
+
requiresApiLayer = true;
|
|
101
|
+
recommendedAction = "install-openxr-api-layer";
|
|
102
|
+
compatibleHostGraphicsApis = runtime.platform === "win32" ? ["D3D11", "D3D12"] : ["Vulkan", "OpenGL Xlib/GLX"];
|
|
103
|
+
issues.push(issue("openxr-api-layer-not-installed", "warning", "Enable OpenXR application integration", "Install the per-user integration after asking for permission. Restart OpenXR applications afterward.", "install-openxr-api-layer"));
|
|
104
|
+
}
|
|
105
|
+
else if (canInstallLayer && !layerEnabled) {
|
|
106
|
+
readiness = "setup-required";
|
|
107
|
+
backendLabel = "Desktop preview";
|
|
108
|
+
summary = "OpenXR application integration is installed but disabled.";
|
|
109
|
+
requiresApiLayer = true;
|
|
110
|
+
recommendedAction = "enable-openxr-api-layer";
|
|
111
|
+
compatibleHostGraphicsApis = runtime.platform === "win32" ? ["D3D11", "D3D12"] : ["Vulkan", "OpenGL Xlib/GLX"];
|
|
112
|
+
issues.push(issue("openxr-api-layer-disabled", "warning", "OpenXR integration is disabled", "Enable the per-user integration and restart OpenXR applications.", "enable-openxr-api-layer"));
|
|
113
|
+
}
|
|
114
|
+
else if (runtime.selectedBackend === "mock") {
|
|
115
|
+
readiness = "fallback-only";
|
|
116
|
+
backendLabel = runtime.platform === "linux" ? "Desktop preview" : "No visible VR output";
|
|
117
|
+
summary = runtime.platform === "linux"
|
|
118
|
+
? "No real XR backend is ready. The overlay can only be previewed on the desktop."
|
|
119
|
+
: "No real XR backend or visible desktop preview is available.";
|
|
120
|
+
issues.push(issue("no-real-xr-backend", "error", "No compatible VR runtime", "Install and configure a supported OpenXR or SteamVR runtime, then check again.", "install-xr-runtime"));
|
|
121
|
+
recommendedAction = "install-xr-runtime";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (apiLayerStatusError) {
|
|
125
|
+
issues.push(issue("openxr-api-layer-status-unavailable", "warning", "Could not inspect OpenXR integration", "OpenXR integration status could not be checked. Retry the check or repair the application installation.", "none", apiLayerStatusError));
|
|
126
|
+
}
|
|
127
|
+
const canRenderOverlay = readiness === "ready" || readiness === "waiting-for-host" ||
|
|
128
|
+
(readiness === "fallback-only" && runtime.platform === "linux" && runtime.selectedBackend === "mock");
|
|
129
|
+
let launch;
|
|
130
|
+
if (readiness === "ready" && (runtime.selectedBackend === "openxr" || runtime.selectedBackend === "openvr")) {
|
|
131
|
+
launch = {
|
|
132
|
+
verdict: "works-now",
|
|
133
|
+
wouldWorkNow: true,
|
|
134
|
+
fundamentalIncompatibility: false,
|
|
135
|
+
message: "Launching the browser overlay now is expected to produce real VR output.",
|
|
136
|
+
requiredActions: []
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
else if (readiness === "setup-required") {
|
|
140
|
+
const actions = recommendedAction === "none" ? [] : [recommendedAction];
|
|
141
|
+
if (requiresApiLayer)
|
|
142
|
+
actions.push("restart-openxr-apps");
|
|
143
|
+
launch = {
|
|
144
|
+
verdict: "action-required",
|
|
145
|
+
wouldWorkNow: false,
|
|
146
|
+
fundamentalIncompatibility: false,
|
|
147
|
+
message: "The overlay is compatible, but setup must be completed before launching it.",
|
|
148
|
+
requiredActions: actions
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
else if (readiness === "waiting-for-host") {
|
|
152
|
+
launch = {
|
|
153
|
+
verdict: "action-required",
|
|
154
|
+
wouldWorkNow: false,
|
|
155
|
+
fundamentalIncompatibility: false,
|
|
156
|
+
message: `The overlay is ready, but real VR output requires a compatible ${compatibleHostGraphicsApis.join(" or ")} OpenXR application to be started or restarted.`,
|
|
157
|
+
requiredActions: ["start-openxr-app"]
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
else if ((readiness === "fallback-only" || readiness === "unavailable") && platformSupportsLayer) {
|
|
161
|
+
launch = {
|
|
162
|
+
verdict: "action-required",
|
|
163
|
+
wouldWorkNow: false,
|
|
164
|
+
fundamentalIncompatibility: false,
|
|
165
|
+
message: "Launching now would not produce real VR output. Install and configure a supported OpenXR or SteamVR runtime first.",
|
|
166
|
+
requiredActions: ["install-xr-runtime"]
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
launch = {
|
|
171
|
+
verdict: "incompatible",
|
|
172
|
+
wouldWorkNow: false,
|
|
173
|
+
fundamentalIncompatibility: true,
|
|
174
|
+
message: "This platform or execution mode cannot provide a production VR overlay with the current implementation.",
|
|
175
|
+
requiredActions: []
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
readiness,
|
|
180
|
+
backend: runtime.selectedBackend,
|
|
181
|
+
openxrMode: runtime.openxrMode,
|
|
182
|
+
backendLabel,
|
|
183
|
+
summary,
|
|
184
|
+
launch,
|
|
185
|
+
canRenderOverlay,
|
|
186
|
+
isRealVrBackend: runtime.selectedBackend === "openxr" || runtime.selectedBackend === "openvr",
|
|
187
|
+
requiresApiLayer,
|
|
188
|
+
requiresOpenXRAppRestart: requiresApiLayer && (!runtime.openxrCompanionConnected || !layerEnabled),
|
|
189
|
+
apiLayer,
|
|
190
|
+
compatibleHostGraphicsApis,
|
|
191
|
+
features,
|
|
192
|
+
issues,
|
|
193
|
+
recommendedAction,
|
|
194
|
+
diagnostics: runtime
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
export async function getVRCompatibilityReport() {
|
|
198
|
+
const runtime = createVrBridge().getRuntimeInfo();
|
|
199
|
+
if ((runtime.platform !== "win32" && runtime.platform !== "linux") || process.arch !== "x64") {
|
|
200
|
+
return analyzeVRCompatibility(runtime);
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
return analyzeVRCompatibility(runtime, { apiLayer: await getOpenXRApiLayerStatus() });
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
return analyzeVRCompatibility(runtime, {
|
|
207
|
+
apiLayerStatusError: error instanceof Error ? error.message : String(error)
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { VrBridge, createVrBridge, type AttachWindowOptions, type BackendKind, type InitializeVROptions, type OverlayPlacement, type Quat, type RuntimeInfo, type Vec3 } from "./bridge.js";
|
|
2
|
+
export { analyzeVRCompatibility, getVRCompatibilityReport, type VRCompatibilityIssue, type VRCompatibilityContext, type VRCompatibilityReport, type VRFeatureCompatibility, type VRFeatureSupport, type VRLaunchAssessment, type VRLaunchVerdict, type VRIssueSeverity, type VRReadiness, type VRSetupAction } from "./compatibility.js";
|
|
3
|
+
export { disableOpenXRApiLayer, enableOpenXRApiLayer, getOpenXRApiLayerStatus, installOpenXRApiLayer, uninstallOpenXRApiLayer, type OpenXRApiLayerStatus } from "./openxrApiLayer.js";
|
|
2
4
|
export { VROverlay, type ExistingWindowVROverlayOptions, type VROverlayOptions } from "./VROverlay.js";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { VrBridge, createVrBridge } from "./bridge.js";
|
|
2
|
+
export { analyzeVRCompatibility, getVRCompatibilityReport } from "./compatibility.js";
|
|
3
|
+
export { disableOpenXRApiLayer, enableOpenXRApiLayer, getOpenXRApiLayerStatus, installOpenXRApiLayer, uninstallOpenXRApiLayer } from "./openxrApiLayer.js";
|
|
2
4
|
export { VROverlay } from "./VROverlay.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface OpenXRApiLayerStatus {
|
|
2
|
+
installed: boolean;
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
registered: boolean | null;
|
|
5
|
+
manifestPath: string;
|
|
6
|
+
scope: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function parseOpenXRApiLayerStatus(output: string): OpenXRApiLayerStatus;
|
|
9
|
+
export declare function getOpenXRApiLayerStatus(): Promise<OpenXRApiLayerStatus>;
|
|
10
|
+
export declare function installOpenXRApiLayer(): Promise<OpenXRApiLayerStatus>;
|
|
11
|
+
export declare function enableOpenXRApiLayer(): Promise<OpenXRApiLayerStatus>;
|
|
12
|
+
export declare function disableOpenXRApiLayer(): Promise<OpenXRApiLayerStatus>;
|
|
13
|
+
export declare function uninstallOpenXRApiLayer(): Promise<OpenXRApiLayerStatus>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
const PREBUILT_PACKAGES = {
|
|
7
|
+
linux: "@covas-labs/electron-vr-prebuilt-linux-x64",
|
|
8
|
+
win32: "@covas-labs/electron-vr-prebuilt-win32-x64"
|
|
9
|
+
};
|
|
10
|
+
function assertSupportedPlatform() {
|
|
11
|
+
if ((process.platform !== "linux" && process.platform !== "win32") || process.arch !== "x64") {
|
|
12
|
+
throw new Error(`OpenXR API-layer management is not supported on ${process.platform}-${process.arch}.`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function resolvePrebuiltCli() {
|
|
16
|
+
const packageName = PREBUILT_PACKAGES[process.platform];
|
|
17
|
+
if (!packageName)
|
|
18
|
+
return null;
|
|
19
|
+
const requires = [
|
|
20
|
+
createRequire(import.meta.url),
|
|
21
|
+
createRequire(resolve(process.cwd(), "package.json"))
|
|
22
|
+
];
|
|
23
|
+
for (const require of requires) {
|
|
24
|
+
try {
|
|
25
|
+
const packageEntry = require.resolve(packageName);
|
|
26
|
+
const cli = resolve(dirname(packageEntry), "openxr-layer-cli.js");
|
|
27
|
+
if (existsSync(cli))
|
|
28
|
+
return cli;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Try the next package resolution context.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function resolveCli() {
|
|
37
|
+
assertSupportedPlatform();
|
|
38
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
39
|
+
const repositoryCli = resolve(currentDir, "..", "..", "..", "tools", "openxr-layer-cli.mjs");
|
|
40
|
+
if (existsSync(repositoryCli))
|
|
41
|
+
return repositoryCli;
|
|
42
|
+
const prebuiltCli = resolvePrebuiltCli();
|
|
43
|
+
if (prebuiltCli)
|
|
44
|
+
return prebuiltCli;
|
|
45
|
+
throw new Error("The OpenXR API-layer utility is unavailable. Rebuild the repository native addon or install the platform prebuilt package.");
|
|
46
|
+
}
|
|
47
|
+
function runCommand(command) {
|
|
48
|
+
const cli = resolveCli();
|
|
49
|
+
return new Promise((resolvePromise, reject) => {
|
|
50
|
+
const child = spawn(process.execPath, [cli, command], {
|
|
51
|
+
env: {
|
|
52
|
+
...process.env,
|
|
53
|
+
ELECTRON_RUN_AS_NODE: "1"
|
|
54
|
+
},
|
|
55
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
56
|
+
});
|
|
57
|
+
let stdout = "";
|
|
58
|
+
let stderr = "";
|
|
59
|
+
child.stdout.on("data", (chunk) => {
|
|
60
|
+
stdout += String(chunk);
|
|
61
|
+
});
|
|
62
|
+
child.stderr.on("data", (chunk) => {
|
|
63
|
+
stderr += String(chunk);
|
|
64
|
+
});
|
|
65
|
+
child.once("error", reject);
|
|
66
|
+
child.once("close", (code, signal) => {
|
|
67
|
+
if (code === 0) {
|
|
68
|
+
resolvePromise(stdout);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const details = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n");
|
|
72
|
+
reject(new Error(`OpenXR API-layer ${command} failed with ${signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`}${details ? `:\n${details}` : "."}`));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export function parseOpenXRApiLayerStatus(output) {
|
|
77
|
+
const values = new Map();
|
|
78
|
+
for (const line of output.split(/\r?\n/)) {
|
|
79
|
+
const separator = line.indexOf("=");
|
|
80
|
+
if (separator > 0)
|
|
81
|
+
values.set(line.slice(0, separator).trim(), line.slice(separator + 1).trim());
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
installed: values.get("installed") === "true",
|
|
85
|
+
enabled: values.get("enabled") === "true",
|
|
86
|
+
registered: values.has("registered") ? values.get("registered") === "true" : null,
|
|
87
|
+
manifestPath: values.get("manifest") ?? "",
|
|
88
|
+
scope: values.get("scope") ?? "current-user"
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export async function getOpenXRApiLayerStatus() {
|
|
92
|
+
return parseOpenXRApiLayerStatus(await runCommand("status"));
|
|
93
|
+
}
|
|
94
|
+
async function runLifecycleCommand(command) {
|
|
95
|
+
await runCommand(command);
|
|
96
|
+
return getOpenXRApiLayerStatus();
|
|
97
|
+
}
|
|
98
|
+
export function installOpenXRApiLayer() {
|
|
99
|
+
return runLifecycleCommand("install");
|
|
100
|
+
}
|
|
101
|
+
export function enableOpenXRApiLayer() {
|
|
102
|
+
return runLifecycleCommand("enable");
|
|
103
|
+
}
|
|
104
|
+
export function disableOpenXRApiLayer() {
|
|
105
|
+
return runLifecycleCommand("disable");
|
|
106
|
+
}
|
|
107
|
+
export function uninstallOpenXRApiLayer() {
|
|
108
|
+
return runLifecycleCommand("uninstall");
|
|
109
|
+
}
|
package/dist/overlayOptions.d.ts
CHANGED
|
@@ -8,4 +8,5 @@ export declare function isFiniteNumber(value: number): boolean;
|
|
|
8
8
|
export declare function assertVec3(value: Vec3, context: string): void;
|
|
9
9
|
export declare function assertQuat(value: Quat, context: string): void;
|
|
10
10
|
export declare function assertSizeMeters(sizeMeters: number): void;
|
|
11
|
+
export declare function assertCurvature(curvature: number): void;
|
|
11
12
|
export declare function normalizePlacement(placement?: OverlayPlacementInput): OverlayPlacement;
|
package/dist/overlayOptions.js
CHANGED
|
@@ -16,6 +16,11 @@ export function assertSizeMeters(sizeMeters) {
|
|
|
16
16
|
throw new RangeError("sizeMeters must be a finite number greater than zero.");
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
export function assertCurvature(curvature) {
|
|
20
|
+
if (!isFiniteNumber(curvature) || curvature < 0 || curvature > 1) {
|
|
21
|
+
throw new RangeError("curvature must be a finite number between 0 and 1.");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
19
24
|
export function normalizePlacement(placement) {
|
|
20
25
|
const mode = placement?.mode ?? "head";
|
|
21
26
|
if (mode !== "head" && mode !== "world") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@covas-labs/electron-vr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Electron VR overlay bridge with OpenXR, OpenVR, and mock fallback backends.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"electron": ">=37"
|
|
23
23
|
},
|
|
24
24
|
"optionalDependencies": {
|
|
25
|
-
"@covas-labs/electron-vr-prebuilt-linux-x64": "0.
|
|
26
|
-
"@covas-labs/electron-vr-prebuilt-win32-x64": "0.
|
|
25
|
+
"@covas-labs/electron-vr-prebuilt-linux-x64": "0.5.0",
|
|
26
|
+
"@covas-labs/electron-vr-prebuilt-win32-x64": "0.5.0"
|
|
27
27
|
},
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"registry": "https://registry.npmjs.org",
|