From 7cb2b01403944d06f7ef9cf09fb8209da67ac167 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Sun, 3 May 2026 20:16:16 +0200 Subject: [PATCH] =?UTF-8?q?v0.23.3=20=E2=80=94=20Mode=20safe=20:=20OpenVR?= =?UTF-8?q?=20sans=20vtable=20(Phase=201=20stripped=20down)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Init OpenVR + binding de la vtable IVRSystem_022 fonctionne (confirmé par les logs : "OpenVR state: Ready, vtable IVRSystem_022 bindée"), mais le PREMIER appel vtable crashe en AccessViolation natif. Pas une race au boot — ça plante même 8s après que vrserver est apparu, et probablement à n'importe quel moment de la session SteamVR. Diagnostic : les fonctions de la vtable (IsTrackedDeviceConnected, GetBoolTrackedDeviceProperty, etc.) accèdent à des shared memories avec vrserver qui peuvent être (re)mappées dynamiquement quand un device se connecte/déconnecte. L'AccessViolation natif passe au travers du CLR via Marshal.GetDelegateForFunctionPointer — non rattrapable par catch (Exception) ni catch (AccessViolationException). Fix : on retire TOUS les appels vtable. OpenVrService.Query utilise maintenant uniquement l'API flat C qui ne fait pas de SHM/IPC : - VR_IsRuntimeInstalled() — lookup statique dans la DLL - VR_IsHmdPresent() — consulte un mutex/event nommé, pas de SHM - GetProcessesByName("vrserver") — process check Windows Résultat : check VR robuste mais réduit à "SteamVR actif + HMD présent oui/non". Plus de batterie, plus d'info per-device, le device picker du dialog devient cosmétique (tous les aliases retournent la même réponse). La batterie + l'info per-device reviendra en Phase 1.5 via une approche sub-process : un PSLauncher.VrProbe.exe séparé qui fait les vtable calls et communique par stdout JSON. Si VrProbe crashe en AccessViolation, le launcher voit juste "exit code != 0" et report "VR query failed" sans être affecté lui-même. Le code de la vtable (delegates, BindVTableDelegates, Init/Shutdown) reste en place dans OpenVrService.cs comme référence pour Phase 1.5 — n'est juste plus appelé depuis Query. Co-Authored-By: Claude Opus 4.7 (1M context) --- installer/PSLauncher.iss | 2 +- src/PSLauncher.App/PSLauncher.App.csproj | 6 +- .../Health/OpenVR/OpenVrService.cs | 144 ++++++++++++------ .../Health/SystemHealthService.cs | 13 ++ src/PSLauncher.Core/Localization/Strings.cs | 10 +- 5 files changed, 116 insertions(+), 59 deletions(-) diff --git a/installer/PSLauncher.iss b/installer/PSLauncher.iss index 8e47839..13013d3 100644 --- a/installer/PSLauncher.iss +++ b/installer/PSLauncher.iss @@ -11,7 +11,7 @@ #define MyAppName "PROSERVE Launcher" #define MyAppShortName "PS_Launcher" -#define MyAppVersion "0.23.2" +#define MyAppVersion "0.23.3" #define MyAppPublisher "ASTERION VR" #define MyAppURL "https://asterionvr.com" #define MyAppExeName "PS_Launcher.exe" diff --git a/src/PSLauncher.App/PSLauncher.App.csproj b/src/PSLauncher.App/PSLauncher.App.csproj index 2bcbffe..f6f506f 100644 --- a/src/PSLauncher.App/PSLauncher.App.csproj +++ b/src/PSLauncher.App/PSLauncher.App.csproj @@ -15,9 +15,9 @@ PROSERVE Launcher © 2026 ASTERION VR — All rights reserved PSLauncher.App - 0.23.2 - 0.23.2.0 - 0.23.2.0 + 0.23.3 + 0.23.3.0 + 0.23.3.0 true diff --git a/src/PSLauncher.Core/Health/OpenVR/OpenVrService.cs b/src/PSLauncher.Core/Health/OpenVR/OpenVrService.cs index 9b311b5..958bfd3 100644 --- a/src/PSLauncher.Core/Health/OpenVR/OpenVrService.cs +++ b/src/PSLauncher.Core/Health/OpenVR/OpenVrService.cs @@ -62,68 +62,112 @@ public sealed class OpenVrService : IOpenVrService, IDisposable { lock (_lock) { - var state = EnsureReady(); - LogStateTransition(state); - var label = ResolveLabel(deviceAlias); - if (state != VrRuntimeState.Ready) - { - return new VrDeviceQueryResult(state, false, label, null, - OpenVRNative.ETrackedDeviceClass.Invalid, - null, null, null, null, null, - state switch - { - VrRuntimeState.RuntimeMissing => "SteamVR n'est pas installé sur cette machine", - VrRuntimeState.HmdAbsent => "SteamVR n'est pas lancé ou aucun HMD n'est branché", - VrRuntimeState.InitFailed => "Échec d'initialisation OpenVR", - _ => null, - }); - } + // ============================================================ + // MODE SAFE : on N'UTILISE PAS la vtable IVRSystem. + // + // Pourquoi : les fonctions de la vtable (IsTrackedDeviceConnected, + // GetBoolTrackedDeviceProperty, etc.) font du SHM/IPC avec vrserver + // qui peut être dans un état partiellement initialisé pendant et + // après le démarrage de SteamVR. Ces fonctions accèdent à de la + // mémoire pas encore mappée → AccessViolation natif → SEH passe + // au travers du CLR via Marshal.GetDelegateForFunctionPointer + // → process killed sans qu'aucun catch ne se déclenche. + // + // Repousser le délai de settle ne suffit pas (race window peut + // se rouvrir à n'importe quel moment de la session : (re)connexion + // d'un device, rechargement d'un driver…). On utilise donc UNIQUEMENT + // l'API flat C : VR_IsRuntimeInstalled + VR_IsHmdPresent qui ne + // touchent pas au SHM. Limitation : pas de batterie, pas de per-device + // info — juste "HMD détecté oui/non". La batterie reviendra en + // Phase 1.5 via un subprocess séparé (PSLauncher.VrProbe.exe) qui + // peut crasher sans tuer le launcher. + // ============================================================ - try + var label = ResolveLabel(deviceAlias); + var state = EnsureReadySafe(); + LogStateTransition(state); + + string detail; + switch (state) { - var index = ResolveDeviceIndex(deviceAlias); - if (index is null) - { + case VrRuntimeState.RuntimeMissing: + return new VrDeviceQueryResult(state, false, label, null, + OpenVRNative.ETrackedDeviceClass.Invalid, + null, null, null, null, null, + "SteamVR n'est pas installé sur cette machine"); + + case VrRuntimeState.HmdAbsent: return new VrDeviceQueryResult(state, false, label, null, OpenVRNative.ETrackedDeviceClass.Invalid, false, null, null, null, null, - $"Appareil « {deviceAlias} » non détecté"); - } + "SteamVR n'est pas lancé ou aucun HMD n'est branché"); - var idx = index.Value; - var connected = _fnIsConnected!(idx); - if (!connected) - { + case VrRuntimeState.Ready: + detail = $"{label} : SteamVR actif, HMD détecté (info détaillée par appareil arrive en Phase 1.5)"; return new VrDeviceQueryResult(state, true, label, null, - _fnDeviceClass!(idx), - false, null, null, null, null, - "Appareil déconnecté"); - } + OpenVRNative.ETrackedDeviceClass.HMD, + true, null, null, null, null, + detail); - var deviceClass = _fnDeviceClass!(idx); - var activity = _fnActivity!(idx); - var serial = ReadStringProperty(idx, OpenVRNative.DeviceProperty.Prop_SerialNumber_String); - - bool? wireless = ReadBoolProperty(idx, OpenVRNative.DeviceProperty.Prop_DeviceIsWireless_Bool); - bool? charging = ReadBoolProperty(idx, OpenVRNative.DeviceProperty.Prop_DeviceIsCharging_Bool); - float? battery = ReadFloatProperty(idx, OpenVRNative.DeviceProperty.Prop_DeviceBatteryPercentage_Float); - - return new VrDeviceQueryResult(state, true, label, serial, - deviceClass, true, charging, wireless, battery, activity, null); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "OpenVR query failed for {Alias}, marking session unhealthy", deviceAlias); - // Marquer l'état comme cassé pour qu'au prochain Query on retente l'init. - MarkSessionBroken(); - return new VrDeviceQueryResult(VrRuntimeState.HmdAbsent, false, label, null, - OpenVRNative.ETrackedDeviceClass.Invalid, - null, null, null, null, null, - $"Erreur OpenVR : {ex.Message}"); + default: + return new VrDeviceQueryResult(state, false, label, null, + OpenVRNative.ETrackedDeviceClass.Invalid, + null, null, null, null, null, + "Échec d'initialisation OpenVR"); } } } + /// + /// Variante safe d'EnsureReady : ne fait JAMAIS de VR_InitInternal2 ni + /// de GetGenericInterface. Décide l'état uniquement depuis : + /// - (lookup statique + /// dans openvr_api.dll, aucun IPC, immédiat), + /// - vrserver.exe alive (process check), + /// - (consulte un mutex/event + /// nommé partagé avec vrserver, mais pas de SHM). + /// + private VrRuntimeState EnsureReadySafe() + { + if (!_resolverEnsured) + { + OpenVRNative.EnsureResolverInstalled(); + _resolverEnsured = true; + } + + try + { + if (!OpenVRNative.VR_IsRuntimeInstalled()) + return VrRuntimeState.RuntimeMissing; + } + catch (DllNotFoundException) { return VrRuntimeState.RuntimeMissing; } + catch (Exception ex) + { + _logger.LogDebug(ex, "VR_IsRuntimeInstalled threw"); + return VrRuntimeState.RuntimeMissing; + } + + // vrserver.exe en l'air ? + if (!IsVrServerRunning()) return VrRuntimeState.HmdAbsent; + + // Cooldown de settle : on garde le délai même en mode safe parce que + // VR_IsHmdPresent consulte un objet de synchro qui peut être pas encore + // créé pendant les premières secondes du boot SteamVR. + if (!HasVrServerSettled()) return VrRuntimeState.HmdAbsent; + + try + { + if (!OpenVRNative.VR_IsHmdPresent()) return VrRuntimeState.HmdAbsent; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "VR_IsHmdPresent threw"); + return VrRuntimeState.HmdAbsent; + } + + return VrRuntimeState.Ready; + } + private VrRuntimeState EnsureReady() { if (!_resolverEnsured) diff --git a/src/PSLauncher.Core/Health/SystemHealthService.cs b/src/PSLauncher.Core/Health/SystemHealthService.cs index 92033a0..ffc4888 100644 --- a/src/PSLauncher.Core/Health/SystemHealthService.cs +++ b/src/PSLauncher.Core/Health/SystemHealthService.cs @@ -148,6 +148,19 @@ public sealed class SystemHealthService : ISystemHealthService return new HealthResult(HealthSeverity.Error, result.ErrorDetail ?? $"{result.DeviceLabel} non détecté"); + // Mode safe (Phase 1) : OpenVR ne nous donne plus de batterie/activity/serial + // pour éviter le crash sur les vtable calls. Quand tous ces champs sont null + // mais qu'on est en Ready+Connected, on affiche le message court fourni par + // OpenVrService (ex. "HMD détecté — Phase 1.5 fournira la batterie"). + if (result.BatteryPercentage is null + && result.IsCharging is null + && result.ActivityLevel is null + && string.IsNullOrEmpty(result.Serial)) + { + return new HealthResult(HealthSeverity.Ok, + result.ErrorDetail ?? $"{result.DeviceLabel} détecté"); + } + // Device présent : on assemble un résumé batterie + activité var battery = result.BatteryPercentage; var pctText = battery is null ? "—" : $"{Math.Round(battery.Value * 100)}%"; diff --git a/src/PSLauncher.Core/Localization/Strings.cs b/src/PSLauncher.Core/Localization/Strings.cs index 77266bc..9b4c4e0 100644 --- a/src/PSLauncher.Core/Localization/Strings.cs +++ b/src/PSLauncher.Core/Localization/Strings.cs @@ -1001,11 +1001,11 @@ public static class Strings ); public static string HealthEditorTargetVrDevice => T("APPAREIL VR", "VR DEVICE", "VR 设备", "อุปกรณ์ VR", "جهاز VR"); public static string HealthEditorTargetVrDeviceHint => T( - "Lit batterie + état de charge + tracking via OpenVR. SteamVR doit tourner. La runtime est trouvée via la copie installée par SteamVR — rien à bundler.", - "Reads battery + charging + tracking via OpenVR. SteamVR must be running. Runtime is found via SteamVR's installed copy — nothing to bundle.", - "通过 OpenVR 读取电池 + 充电 + 跟踪状态。SteamVR 必须在运行。运行时通过 SteamVR 的安装副本查找 — 无需打包。", - "อ่านแบตเตอรี่ + การชาร์จ + การติดตามผ่าน OpenVR ต้อง SteamVR ทำงาน รันไทม์ค้นหาผ่านสำเนาที่ติดตั้งของ SteamVR — ไม่ต้องบรรจุเอง", - "يقرأ البطارية + الشحن + التتبع عبر OpenVR. يجب تشغيل SteamVR. يتم العثور على وقت التشغيل من خلال نسخة SteamVR المثبتة — لا حاجة للتجميع." + "Phase 1 : détecte la présence de SteamVR et d'un HMD branché (vert si oui, rouge sinon). La batterie + l'info par-appareil arrive en Phase 1.5 via un sous-process séparé pour être robuste aux crashs natifs OpenVR.", + "Phase 1: detects SteamVR running with an HMD plugged in (green if yes, red otherwise). Battery + per-device info coming in Phase 1.5 via a separate child process to be robust against OpenVR native crashes.", + "第 1 阶段:检测 SteamVR 是否运行以及是否插入了 HMD(是则绿色,否则红色)。电池 + 每个设备的信息将在第 1.5 阶段通过单独的子进程提供,以增强对 OpenVR 原生崩溃的鲁棒性。", + "เฟส 1: ตรวจจับว่า SteamVR กำลังทำงานและมี HMD เชื่อมต่ออยู่หรือไม่ (เขียวถ้าใช่ แดงถ้าไม่) ข้อมูลแบตเตอรี่ + ต่ออุปกรณ์จะมาในเฟส 1.5 ผ่านโปรเซสลูกแยกเพื่อความทนทานต่อข้อผิดพลาดเนทีฟ OpenVR", + "المرحلة 1: تكتشف تشغيل SteamVR مع توصيل HMD (أخضر إذا نعم، أحمر خلاف ذلك). البطارية + المعلومات لكل جهاز ستأتي في المرحلة 1.5 عبر عملية فرعية منفصلة لتكون متينة ضد الأعطال الأصلية OpenVR." ); public static string HealthEditorTargetProcess => T("CIBLE — NOM DE PROCESSUS", "TARGET — PROCESS NAME", "目标 — 进程名", "เป้าหมาย — ชื่อโปรเซส", "الهدف — اسم العملية"); public static string HealthEditorTargetPing => T("CIBLE — IP OU HOSTNAME", "TARGET — IP OR HOSTNAME", "目标 — IP 或主机名", "เป้าหมาย — IP หรือชื่อโฮสต์", "الهدف — IP أو اسم المضيف");