v0.23.3 — Mode safe : OpenVR sans vtable (Phase 1 stripped down)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
#define MyAppName "PROSERVE Launcher"
|
#define MyAppName "PROSERVE Launcher"
|
||||||
#define MyAppShortName "PS_Launcher"
|
#define MyAppShortName "PS_Launcher"
|
||||||
#define MyAppVersion "0.23.2"
|
#define MyAppVersion "0.23.3"
|
||||||
#define MyAppPublisher "ASTERION VR"
|
#define MyAppPublisher "ASTERION VR"
|
||||||
#define MyAppURL "https://asterionvr.com"
|
#define MyAppURL "https://asterionvr.com"
|
||||||
#define MyAppExeName "PS_Launcher.exe"
|
#define MyAppExeName "PS_Launcher.exe"
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
<Product>PROSERVE Launcher</Product>
|
<Product>PROSERVE Launcher</Product>
|
||||||
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
||||||
<RootNamespace>PSLauncher.App</RootNamespace>
|
<RootNamespace>PSLauncher.App</RootNamespace>
|
||||||
<Version>0.23.2</Version>
|
<Version>0.23.3</Version>
|
||||||
<AssemblyVersion>0.23.2.0</AssemblyVersion>
|
<AssemblyVersion>0.23.3.0</AssemblyVersion>
|
||||||
<FileVersion>0.23.2.0</FileVersion>
|
<FileVersion>0.23.3.0</FileVersion>
|
||||||
|
|
||||||
<!-- Single-file self-contained publish profile (used by `dotnet publish`) -->
|
<!-- Single-file self-contained publish profile (used by `dotnet publish`) -->
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
|||||||
@@ -62,68 +62,112 @@ public sealed class OpenVrService : IOpenVrService, IDisposable
|
|||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var state = EnsureReady();
|
// ============================================================
|
||||||
LogStateTransition(state);
|
// MODE SAFE : on N'UTILISE PAS la vtable IVRSystem.
|
||||||
var label = ResolveLabel(deviceAlias);
|
//
|
||||||
if (state != VrRuntimeState.Ready)
|
// Pourquoi : les fonctions de la vtable (IsTrackedDeviceConnected,
|
||||||
{
|
// GetBoolTrackedDeviceProperty, etc.) font du SHM/IPC avec vrserver
|
||||||
return new VrDeviceQueryResult(state, false, label, null,
|
// qui peut être dans un état partiellement initialisé pendant et
|
||||||
OpenVRNative.ETrackedDeviceClass.Invalid,
|
// après le démarrage de SteamVR. Ces fonctions accèdent à de la
|
||||||
null, null, null, null, null,
|
// mémoire pas encore mappée → AccessViolation natif → SEH passe
|
||||||
state switch
|
// au travers du CLR via Marshal.GetDelegateForFunctionPointer
|
||||||
{
|
// → process killed sans qu'aucun catch ne se déclenche.
|
||||||
VrRuntimeState.RuntimeMissing => "SteamVR n'est pas installé sur cette machine",
|
//
|
||||||
VrRuntimeState.HmdAbsent => "SteamVR n'est pas lancé ou aucun HMD n'est branché",
|
// Repousser le délai de settle ne suffit pas (race window peut
|
||||||
VrRuntimeState.InitFailed => "Échec d'initialisation OpenVR",
|
// se rouvrir à n'importe quel moment de la session : (re)connexion
|
||||||
_ => null,
|
// 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);
|
case VrRuntimeState.RuntimeMissing:
|
||||||
if (index is null)
|
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,
|
return new VrDeviceQueryResult(state, false, label, null,
|
||||||
OpenVRNative.ETrackedDeviceClass.Invalid,
|
OpenVRNative.ETrackedDeviceClass.Invalid,
|
||||||
false, null, null, null, null,
|
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;
|
case VrRuntimeState.Ready:
|
||||||
var connected = _fnIsConnected!(idx);
|
detail = $"{label} : SteamVR actif, HMD détecté (info détaillée par appareil arrive en Phase 1.5)";
|
||||||
if (!connected)
|
|
||||||
{
|
|
||||||
return new VrDeviceQueryResult(state, true, label, null,
|
return new VrDeviceQueryResult(state, true, label, null,
|
||||||
_fnDeviceClass!(idx),
|
OpenVRNative.ETrackedDeviceClass.HMD,
|
||||||
false, null, null, null, null,
|
true, null, null, null, null,
|
||||||
"Appareil déconnecté");
|
detail);
|
||||||
}
|
|
||||||
|
|
||||||
var deviceClass = _fnDeviceClass!(idx);
|
default:
|
||||||
var activity = _fnActivity!(idx);
|
return new VrDeviceQueryResult(state, false, label, null,
|
||||||
var serial = ReadStringProperty(idx, OpenVRNative.DeviceProperty.Prop_SerialNumber_String);
|
OpenVRNative.ETrackedDeviceClass.Invalid,
|
||||||
|
null, null, null, null, null,
|
||||||
bool? wireless = ReadBoolProperty(idx, OpenVRNative.DeviceProperty.Prop_DeviceIsWireless_Bool);
|
"Échec d'initialisation OpenVR");
|
||||||
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}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Variante safe d'EnsureReady : ne fait JAMAIS de VR_InitInternal2 ni
|
||||||
|
/// de GetGenericInterface. Décide l'état uniquement depuis :
|
||||||
|
/// - <see cref="OpenVRNative.VR_IsRuntimeInstalled"/> (lookup statique
|
||||||
|
/// dans openvr_api.dll, aucun IPC, immédiat),
|
||||||
|
/// - vrserver.exe alive (process check),
|
||||||
|
/// - <see cref="OpenVRNative.VR_IsHmdPresent"/> (consulte un mutex/event
|
||||||
|
/// nommé partagé avec vrserver, mais pas de SHM).
|
||||||
|
/// </summary>
|
||||||
|
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()
|
private VrRuntimeState EnsureReady()
|
||||||
{
|
{
|
||||||
if (!_resolverEnsured)
|
if (!_resolverEnsured)
|
||||||
|
|||||||
@@ -148,6 +148,19 @@ public sealed class SystemHealthService : ISystemHealthService
|
|||||||
return new HealthResult(HealthSeverity.Error,
|
return new HealthResult(HealthSeverity.Error,
|
||||||
result.ErrorDetail ?? $"{result.DeviceLabel} non détecté");
|
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é
|
// Device présent : on assemble un résumé batterie + activité
|
||||||
var battery = result.BatteryPercentage;
|
var battery = result.BatteryPercentage;
|
||||||
var pctText = battery is null ? "—" : $"{Math.Round(battery.Value * 100)}%";
|
var pctText = battery is null ? "—" : $"{Math.Round(battery.Value * 100)}%";
|
||||||
|
|||||||
@@ -1001,11 +1001,11 @@ public static class Strings
|
|||||||
);
|
);
|
||||||
public static string HealthEditorTargetVrDevice => T("APPAREIL VR", "VR DEVICE", "VR 设备", "อุปกรณ์ VR", "جهاز VR");
|
public static string HealthEditorTargetVrDevice => T("APPAREIL VR", "VR DEVICE", "VR 设备", "อุปกรณ์ VR", "جهاز VR");
|
||||||
public static string HealthEditorTargetVrDeviceHint => T(
|
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.",
|
"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.",
|
||||||
"Reads battery + charging + tracking via OpenVR. SteamVR must be running. Runtime is found via SteamVR's installed copy — nothing to bundle.",
|
"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.",
|
||||||
"通过 OpenVR 读取电池 + 充电 + 跟踪状态。SteamVR 必须在运行。运行时通过 SteamVR 的安装副本查找 — 无需打包。",
|
"第 1 阶段:检测 SteamVR 是否运行以及是否插入了 HMD(是则绿色,否则红色)。电池 + 每个设备的信息将在第 1.5 阶段通过单独的子进程提供,以增强对 OpenVR 原生崩溃的鲁棒性。",
|
||||||
"อ่านแบตเตอรี่ + การชาร์จ + การติดตามผ่าน OpenVR ต้อง SteamVR ทำงาน รันไทม์ค้นหาผ่านสำเนาที่ติดตั้งของ SteamVR — ไม่ต้องบรรจุเอง",
|
"เฟส 1: ตรวจจับว่า SteamVR กำลังทำงานและมี HMD เชื่อมต่ออยู่หรือไม่ (เขียวถ้าใช่ แดงถ้าไม่) ข้อมูลแบตเตอรี่ + ต่ออุปกรณ์จะมาในเฟส 1.5 ผ่านโปรเซสลูกแยกเพื่อความทนทานต่อข้อผิดพลาดเนทีฟ OpenVR",
|
||||||
"يقرأ البطارية + الشحن + التتبع عبر OpenVR. يجب تشغيل SteamVR. يتم العثور على وقت التشغيل من خلال نسخة SteamVR المثبتة — لا حاجة للتجميع."
|
"المرحلة 1: تكتشف تشغيل SteamVR مع توصيل HMD (أخضر إذا نعم، أحمر خلاف ذلك). البطارية + المعلومات لكل جهاز ستأتي في المرحلة 1.5 عبر عملية فرعية منفصلة لتكون متينة ضد الأعطال الأصلية OpenVR."
|
||||||
);
|
);
|
||||||
public static string HealthEditorTargetProcess => T("CIBLE — NOM DE PROCESSUS", "TARGET — PROCESS NAME", "目标 — 进程名", "เป้าหมาย — ชื่อโปรเซส", "الهدف — اسم العملية");
|
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 أو اسم المضيف");
|
public static string HealthEditorTargetPing => T("CIBLE — IP OU HOSTNAME", "TARGET — IP OR HOSTNAME", "目标 — IP 或主机名", "เป้าหมาย — IP หรือชื่อโฮสต์", "الهدف — IP أو اسم المضيف");
|
||||||
|
|||||||
Reference in New Issue
Block a user