@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 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(...)`, and resize it in meters with `overlay.setSizeMeters(...)`.
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 is treated as a best-effort alternate backend when a compatible OpenVR runtime is installed but the OpenXR overlay path is unavailable or disabled. It is not currently validated end to end on the main development machine or in CI.
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
- On Windows, runtime probing reports OpenXR overlay and D3D11 graphics-binding capability, but the default backend remains `openvr` during rollout. Set `ELECTRON_VR_ENABLE_OPENXR=1` to opt into the Windows OpenXR path when a compatible runtime exposes `XR_EXTX_overlay` and `XR_KHR_D3D11_enable`. This path is not currently validated end to end on the main development machine or in CI.
72
+ When `XR_EXTX_overlay` is unavailable, Linux x64 can use the explicitly installed API layer before falling back to OpenVR:
72
73
 
73
- `getRuntimeInfo()` also includes `openvrRuntimeInstalled`, `openvrRuntimePath`, and platform-specific OpenXR capability details such as `openxrWindowsD3D11BindingAvailable`, so runtime diagnostics do not need to initialize OpenVR just to check availability. It now also surfaces live session/app details when the runtime exposes them, including `openxrSessionState`, `openxrSessionRunning`, and the current OpenVR scene app fields (`openvrSceneApplicationState`, `openvrSceneProcessId`, `openvrSceneApplicationKey`, `openvrSceneApplicationName`, `openvrSceneApplicationBinaryPath`). OpenXR does not expose a standard cross-runtime "current app" query, so those app-level fields are currently OpenVR-specific. `probeMode` includes the backend-selection decision as well, which makes fallback behavior easier to diagnose even when Linux OpenVR or Windows OpenXR cannot be validated on the current host.
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.
@@ -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 { assertSizeMeters, normalizePlacement } from "./overlayOptions.js";
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
- offscreenContents.setFrameRate(options.frameRate ?? 60);
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
- return this.addon.initializeVR(options);
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
- this.addon.shutdownVR();
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 (textureInfo?.planes?.length) {
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
- releaseTexture(texture);
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
+ }
@@ -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;
@@ -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.2",
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.3.2",
26
- "@covas-labs/electron-vr-prebuilt-win32-x64": "0.3.2"
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",