├── .gitignore ├── .run └── Publish XRDoctor.run.xml ├── ApiLayerManifest.cs ├── GameDiagnostics.cs ├── HardwareDiagnostics.cs ├── LICENSE ├── OculusDiagnostics.cs ├── OpenXRDiagnostics.cs ├── Program.cs ├── README.md ├── RuntimeManifest.cs ├── SteamVRDiagnostics.cs ├── SystemDiagnostics.cs ├── VirtualDesktopDiagnostics.cs ├── ViveVRDiagnostics.cs ├── VulkanDiagnostics.cs ├── VulkanLayerManifest.cs ├── WindowsMRDiagnostics.cs ├── XRDoctor.csproj ├── XRDoctor.ico ├── XRDoctor.sln ├── actions.json ├── app.vrmanifest ├── knuckles_bindings.json ├── openvr_api.cs ├── openvr_api.dll ├── openvr_api.dll.sig ├── openvr_api.pdb └── openxr_loader.dll /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.*~ 3 | project.lock.json 4 | .DS_Store 5 | *.pyc 6 | nupkg/ 7 | 8 | # Visual Studio Code 9 | .vscode 10 | 11 | # Rider 12 | .idea 13 | 14 | # User-specific files 15 | *.suo 16 | *.user 17 | *.userosscache 18 | *.sln.docstates 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | build/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Oo]ut/ 32 | msbuild.log 33 | msbuild.err 34 | msbuild.wrn 35 | 36 | # Visual Studio 2015 37 | .vs/ 38 | -------------------------------------------------------------------------------- /.run/Publish XRDoctor.run.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ApiLayerManifest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace SLZ.XRDoctor; 4 | 5 | public sealed class ApiLayerManifest { 6 | [JsonProperty("file_format_version", Required = Required.Always)] 7 | public string FileFormatVersion { get; private set; } = null!; 8 | 9 | [JsonProperty("api_layer", Required = Required.Always)] 10 | public ApiLayer ApiLayer { get; private set; } = null!; 11 | 12 | [JsonConstructor] 13 | public ApiLayerManifest() { } 14 | } 15 | 16 | public sealed class ApiLayer { 17 | [JsonProperty("name", Required = Required.Always)] 18 | public string Name { get; private set; } = null!; 19 | 20 | [JsonProperty("library_path", Required = Required.Always)] 21 | public string LibraryPath { get; private set; } = null!; 22 | 23 | [JsonProperty("api_version", Required = Required.Always)] 24 | public string ApiVersion { get; private set; } = null!; 25 | 26 | [JsonProperty("implementation_version", Required = Required.Always)] 27 | public string ImplementationVersion { get; private set; } = null!; 28 | 29 | [JsonProperty("description", Required = Required.Always)] 30 | public string Description { get; private set; } = null!; 31 | 32 | [JsonProperty("functions")] 33 | public Dictionary Functions { get; private set; } = null!; 34 | 35 | [JsonProperty("instance_extensions")] 36 | public List InstanceExtensions { get; private set; } = null!; 37 | 38 | [JsonProperty("disable_environment")] 39 | public string DisableEnvironment { get; private set; } = null!; 40 | 41 | [JsonProperty("enable_environment")] 42 | public string EnableEnvironment { get; private set; } = null!; 43 | 44 | [JsonConstructor] 45 | public ApiLayer() { } 46 | } 47 | 48 | public sealed class InstanceExtension { 49 | [JsonProperty("name")] 50 | public string Name { get; private set; } = null!; 51 | 52 | [JsonProperty("extension_version")] 53 | public object ExtensionVersion { get; private set; } = null!; 54 | 55 | [JsonProperty("entrypoints")] 56 | public List Entrypoints { get; private set; } = null!; 57 | 58 | [JsonConstructor] 59 | public InstanceExtension() { } 60 | } -------------------------------------------------------------------------------- /GameDiagnostics.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace SLZ.XRDoctor; 4 | 5 | public static class GameDiagnostics { 6 | private static readonly Serilog.ILogger Log = Serilog.Log.ForContext(typeof(GameDiagnostics)); 7 | private const string LogTag = "BONELAB"; 8 | 9 | [DllImport("shell32.dll")] 10 | private static extern int SHGetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid rfid, uint dwFlags, 11 | IntPtr hToken, 12 | out IntPtr pszPath); 13 | 14 | public static void Check() { 15 | var appDataPath = ""; 16 | var pszPath = IntPtr.Zero; 17 | try { 18 | var result = 19 | SHGetKnownFolderPath(new Guid("A520A1A4-1780-4FF6-BD18-167343C5AF16"), 0, IntPtr.Zero, out pszPath); 20 | if (result >= 0) { 21 | var path = Marshal.PtrToStringAuto(pszPath); 22 | if (Directory.Exists(path)) { appDataPath = path; } 23 | } 24 | } finally { 25 | if (pszPath != IntPtr.Zero) { Marshal.FreeCoTaskMem(pszPath); } 26 | } 27 | 28 | if (!Directory.Exists(appDataPath)) { 29 | Log.Error("[{LogTag}] Could not find AppData/LocalLow directory: {appDataPath}.", LogTag, appDataPath); 30 | } 31 | 32 | var gamePath = Path.Combine(appDataPath, "Stress Level Zero", "BONELAB"); 33 | if (!Directory.Exists(appDataPath)) { 34 | Log.Error("[{LogTag}] Could not find game's persistend data directory: {gamepath}.", LogTag, gamePath); 35 | } 36 | 37 | var playerLogPath = Path.Combine(gamePath, "Player.log"); 38 | if (File.Exists(playerLogPath)) { 39 | var playerLog = File.ReadAllText(playerLogPath); 40 | Log.Debug("<<<<<<<<<<"); 41 | Log.Debug("PLAYER LOG"); 42 | Log.Debug("{LogContents}", playerLog); 43 | Log.Debug(">>>>>>>>>>"); 44 | } 45 | 46 | var playerLogPrevPath = Path.Combine(gamePath, "Player-prev.log"); 47 | if (File.Exists(playerLogPrevPath)) { 48 | var playerLogPrev = File.ReadAllText(playerLogPrevPath); 49 | Log.Debug("<<<<<<<<<<"); 50 | Log.Debug("PLAYER LOG (PREV)"); 51 | Log.Debug("{LogContents}", playerLogPrev); 52 | Log.Debug(">>>>>>>>>>"); 53 | } 54 | 55 | var settingsJsonPath = Path.Combine(gamePath, "settings.json"); 56 | if (File.Exists(settingsJsonPath)) { 57 | var settingsJson = File.ReadAllText(settingsJsonPath); 58 | Log.Debug("<<<<<<<<<<"); 59 | Log.Debug("Settings"); 60 | Log.Debug("{SettingsContents}", settingsJson); 61 | Log.Debug(">>>>>>>>>>"); 62 | } 63 | 64 | var savesJsonPath = Path.Combine(gamePath, "Saves", "slot_0_save.json"); 65 | if (File.Exists(savesJsonPath)) { 66 | var savesJson = File.ReadAllText(savesJsonPath); 67 | Log.Debug("<<<<<<<<<<"); 68 | Log.Debug("Save"); 69 | Log.Debug("{SaveContents}", savesJson); 70 | Log.Debug(">>>>>>>>>>"); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /HardwareDiagnostics.cs: -------------------------------------------------------------------------------- 1 | using System.Management; 2 | using System.Text.Json; 3 | using System.Text.Json.Nodes; 4 | 5 | namespace SLZ.XRDoctor; 6 | 7 | public static class HardwareDiagnostics { 8 | private static readonly Serilog.ILogger Log = Serilog.Log.ForContext(typeof(HardwareDiagnostics)); 9 | public static void FindHeadsets(out Dictionary headsets) { 10 | headsets = new Dictionary(); 11 | 12 | var mos = new ManagementObjectSearcher(null, @"SELECT * FROM Win32_PnPEntity"); 13 | foreach (var mo in mos.Get().OfType()) { 14 | var args = new object[] {new string[] {"DEVPKEY_Device_BusReportedDeviceDesc"}, null}; 15 | mo.InvokeMethod("GetDeviceProperties", args); 16 | var mbos = (ManagementBaseObject[]) args[1]; 17 | if (mbos.Length == 0) { continue; } 18 | 19 | var properties = mbos[0].Properties.OfType(); 20 | 21 | var dataProp = properties.FirstOrDefault(p => p.Name == "Data"); 22 | var deviceIdProp = properties.FirstOrDefault(p => p.Name == "DeviceID"); 23 | 24 | // Kinda overspecified, but whatever. 25 | { 26 | if (dataProp is {Value: string data and "Index HMD"} && 27 | deviceIdProp is {Value: string deviceId}) { 28 | Log.Information("[{LogTag}] Found headset: {Name} {DeviceID}", "Hardware", data, deviceId); 29 | headsets[data] = deviceId; 30 | } 31 | } 32 | { 33 | if (dataProp is {Value: string data and "Quest 2"} && 34 | deviceIdProp is {Value: string deviceId}) { 35 | headsets[data] = deviceId; 36 | Log.Information("[{LogTag}] Found headset: {Name} {DeviceID}", "Hardware", data, deviceId); 37 | } 38 | } 39 | } 40 | } 41 | 42 | public static void ListHardware() 43 | { 44 | using (var query = new ManagementObjectSearcher("SELECT * FROM Win32_Processor")) 45 | using (var objs = query.Get()) { 46 | foreach (var obj in objs) { 47 | try { 48 | var json = Collection2Json(obj.Properties); 49 | Log.Information("[{LogTag}] {json}", "Processor", json.ToJsonString()); 50 | } catch (Exception e) { 51 | Log.Error(e, "Error logging CPU information."); 52 | } 53 | } 54 | } 55 | 56 | using (var query = new ManagementObjectSearcher("SELECT * FROM Win32_PhysicalMemory")) 57 | using (var objs = query.Get()) { 58 | foreach (var obj in objs) { 59 | var json = Collection2Json(obj.Properties); 60 | Log.Information("[{LogTag}] {json}", "PhysicalMemory", json.ToJsonString()); 61 | } 62 | } 63 | 64 | using (var query = new ManagementObjectSearcher("SELECT AllocatedBaseSize FROM Win32_PageFileUsage")) 65 | using (var objs = query.Get()) { 66 | foreach (var obj in objs) { 67 | var allocatedBaseSize = (uint)obj.GetPropertyValue("AllocatedBaseSize"); 68 | Log.Information("[{LogTag}] PageFileUsage AllocatedBaseSize={allocatedBaseSize}", "PageFileUsage", allocatedBaseSize); 69 | } 70 | } 71 | 72 | using (var query = new ManagementObjectSearcher("SELECT MaximumSize FROM Win32_PageFileSetting")) 73 | using (var objs = query.Get()) { 74 | foreach (var obj in objs) { 75 | var maximumSize = (uint)obj.GetPropertyValue("MaximumSize"); 76 | Log.Information("[{LogTag}] PageFileSetting MaximumSize={maximumSize}", "PageFileSetting", 77 | maximumSize); 78 | } 79 | } 80 | 81 | using (var query = new ManagementObjectSearcher("SELECT * FROM Win32_DiskDrive")) 82 | using (var objs = query.Get()) { 83 | foreach (var obj in objs) { 84 | var json = Collection2Json(obj.Properties); 85 | Log.Information("[{LogTag}] {json}", "DiskDrive", json.ToJsonString()); 86 | } 87 | } 88 | 89 | using (var query = new ManagementObjectSearcher("SELECT * FROM MSFT_PhysicalDisk")) 90 | { 91 | var scope = new ManagementScope(@"\\.\root\microsoft\windows\storage"); 92 | scope.Connect(); 93 | query.Scope = scope; 94 | query.Options.UseAmendedQualifiers = true; 95 | 96 | using (var objs = query.Get()) { 97 | foreach (var obj in objs) { 98 | var json = Collection2Json(obj.Properties); 99 | Log.Information("[{LogTag}] {json}", "PhysicalDisk", json.ToJsonString()); 100 | } 101 | } 102 | } 103 | 104 | } 105 | 106 | private static JsonObject Collection2Json(PropertyDataCollection propertyData) { 107 | var json = new JsonObject(); 108 | foreach (var property in propertyData) 109 | { 110 | json[property.Name] = property.Value switch { 111 | ushort us => us, 112 | short s => s, 113 | uint ui => ui, 114 | int i => i, 115 | ulong ul => ul, 116 | long l => l, 117 | float f => f, 118 | double d => d, 119 | ushort[] usa => Array2JsonArray(usa), 120 | short[] sa => Array2JsonArray(sa), 121 | uint[] uia => Array2JsonArray(uia), 122 | int[] ia => Array2JsonArray(ia), 123 | ulong[] ula => Array2JsonArray(ula), 124 | long[] la => Array2JsonArray(la), 125 | float[] fa => Array2JsonArray(fa), 126 | double[] da => Array2JsonArray(da), 127 | string[] sa => Array2JsonArray(sa), 128 | { } o => o.ToString(), 129 | _ => null, 130 | }; 131 | 132 | // Redact serials 133 | if (property.Name.Contains("serial", StringComparison.InvariantCultureIgnoreCase)) { 134 | json[property.Name] = "not tracked"; 135 | } 136 | } 137 | 138 | return json; 139 | } 140 | 141 | private static JsonArray Array2JsonArray (TType[] array) { 142 | var jsonArray = new JsonArray(); 143 | foreach (var item in array) { 144 | jsonArray.Add(item); 145 | } 146 | return jsonArray; 147 | } 148 | 149 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-present Stress Level Zero, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /OculusDiagnostics.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Win32; 2 | using Newtonsoft.Json; 3 | 4 | namespace SLZ.XRDoctor; 5 | 6 | public static class OculusDiagnostics { 7 | private static readonly Serilog.ILogger Log = Serilog.Log.ForContext(typeof(OculusDiagnostics)); 8 | private const string LogTag = "Oculus"; 9 | 10 | public static void CheckDirectly(out bool hasOculus) { 11 | Log.Information("[{LogTag}] Checking directly for Oculus runtime at {OculusRegistryKey}.", LogTag, 12 | @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Oculus"); 13 | using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Oculus"); 14 | do { 15 | try { 16 | var o = key?.GetValue("InstallLocation"); 17 | if (o is not string installLocation || string.IsNullOrWhiteSpace(installLocation)) { 18 | Log.Information("[{LogTag}] Runtime not found.", LogTag); 19 | break; 20 | } 21 | 22 | var runtimePath = Path.Combine(installLocation, "Support", "oculus-runtime", "oculus_openxr_64.json"); 23 | 24 | if (!File.Exists(runtimePath)) { 25 | Log.Warning("[{LogTag}] Runtime JSON not found at expected path \"{runtimePath}\".", LogTag, 26 | runtimePath); 27 | break; 28 | } 29 | 30 | var runtimeJsonStr = File.ReadAllText(runtimePath); 31 | 32 | if (string.IsNullOrWhiteSpace(runtimeJsonStr)) { 33 | Log.Error("[{LogTag}] Runtime JSON was empty at \"{runtimePath}\".", LogTag, runtimePath); 34 | break; 35 | } 36 | 37 | RuntimeManifest manifest; 38 | try { 39 | manifest = JsonConvert.DeserializeObject(runtimeJsonStr); 40 | } catch (JsonException e) { 41 | Log.Error(e, "[{LogTag}] Runtime JSON did not parse correctly at {runtimePath}.", LogTag, 42 | runtimePath); 43 | break; 44 | } 45 | 46 | Log.Information("[{LogTag}] Runtime found at path \"{location}\".", LogTag, runtimePath); 47 | hasOculus = true; 48 | return; 49 | } catch (Exception e) { Log.Error(e, "[{LogTag}] Error while determining install state.", LogTag); } 50 | } while (false); 51 | 52 | hasOculus = false; 53 | } 54 | } -------------------------------------------------------------------------------- /OpenXRDiagnostics.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Win32; 2 | using Newtonsoft.Json; 3 | 4 | namespace SLZ.XRDoctor; 5 | 6 | public static class OpenXRDiagnostics { 7 | private static readonly Serilog.ILogger Log = Serilog.Log.ForContext(typeof(OpenXRDiagnostics)); 8 | private const string LogTag = "OpenXR"; 9 | 10 | public static void FindActiveRuntime(out string XR_RUNTIME_JSON) { 11 | Log.Information("[{LogTag}] Finding active runtime according to {ActiveRuntimeRegistryKey}", 12 | LogTag, @"HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\OpenXR\1"); 13 | 14 | // I.e. 15 | // C:\Program Files (x86)\Steam\steamapps\common\SteamVR\steamxr_win64.json 16 | // or 17 | // C:\Program Files\Oculus\Support\oculus-runtime\oculus_openxr_64.json 18 | XR_RUNTIME_JSON = null; 19 | 20 | using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Khronos\OpenXR\1"); 21 | var o = key?.GetValue("ActiveRuntime"); 22 | if (o is string activeRuntimeStr && !string.IsNullOrWhiteSpace(activeRuntimeStr)) { 23 | XR_RUNTIME_JSON = activeRuntimeStr; 24 | } 25 | } 26 | 27 | public static void FindRegisteredRuntimes(out Dictionary runtimes) { 28 | Log.Information("[{LogTag}] Listing available runtimes according to {AvailableRuntimesRegistryKey}", 29 | LogTag, @"HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\OpenXR\1\AvailableRuntimes"); 30 | 31 | runtimes = new Dictionary(); 32 | 33 | using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Khronos\OpenXR\1\AvailableRuntimes"); 34 | if (key == null) { return; } 35 | 36 | foreach (var runtimeName in key.GetValueNames()) { 37 | if (key.GetValue(runtimeName) is not (int and 0)) { continue; } 38 | 39 | if (!File.Exists(runtimeName)) { 40 | Log.Error("[{LogTag}] Runtime \"{apiLayer}\" is registered but was not found.", LogTag, runtimeName); 41 | continue; 42 | } 43 | 44 | var runtimeJsonStr = File.ReadAllText(runtimeName); 45 | if (string.IsNullOrWhiteSpace(runtimeJsonStr)) { 46 | Log.Error("[{LogTag}] Runtime \"{apiLayer}\" is registered but its JSON was empty.", LogTag, runtimeName); 47 | continue; 48 | } 49 | 50 | RuntimeManifest manifest; 51 | try { manifest = JsonConvert.DeserializeObject(runtimeJsonStr); } catch (JsonException e) { 52 | Log.Error(e, "[{LogTag}] Runtime \"{apiLayer}\" is registered but its JSON did not parse correctly.", 53 | LogTag, runtimeName); 54 | continue; 55 | } 56 | 57 | runtimes[runtimeName] = manifest; 58 | } 59 | } 60 | 61 | public static void FindImplicitApiLayers(out Dictionary layers) { 62 | Log.Information("[{LogTag}] Listing implicit OpenXR API layers according to {ImplicitApiLayersRegistryKey}", 63 | LogTag, @"HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\OpenXR\1\ApiLayers\Implicit"); 64 | 65 | layers = new Dictionary(); 66 | 67 | using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Khronos\OpenXR\1\ApiLayers\Implicit"); 68 | if (key == null) { 69 | Log.Information("[{LogTag}] No implicit API layers (This isn't unusual).", LogTag); 70 | return; 71 | } 72 | 73 | foreach (var layerJsonPath in key.GetValueNames()) { 74 | if (key.GetValue(layerJsonPath) is not (int and 0)) { continue; } 75 | 76 | if (!File.Exists(layerJsonPath)) { 77 | Log.Error("[{LogTag}] API Layer \"{apiLayer}\" is registered but was not found.", LogTag, layerJsonPath); 78 | continue; 79 | } 80 | 81 | var layerJson = File.ReadAllText(layerJsonPath); 82 | if (string.IsNullOrWhiteSpace(layerJson)) { 83 | Log.Error("[{LogTag}] API Layer \"{apiLayer}\" is registered but was empty.", LogTag, layerJsonPath); 84 | continue; 85 | } 86 | 87 | ApiLayerManifest manifest; 88 | try { manifest = JsonConvert.DeserializeObject(layerJson); } catch (JsonException e) { 89 | Log.Error(e, "[{LogTag}] API Layer \"{apiLayer}\" is registered but did not parse correctly.", 90 | LogTag, layerJsonPath); 91 | continue; 92 | } 93 | 94 | layers[layerJsonPath] = manifest; 95 | } 96 | 97 | foreach (var (path, layerManifest) in layers) { 98 | var isDisabled = false; 99 | var disableVar = layerManifest.ApiLayer.DisableEnvironment; 100 | if (!string.IsNullOrWhiteSpace(disableVar)) { 101 | isDisabled = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(disableVar)); 102 | } 103 | 104 | var isEnabled = false; 105 | var enableVar = layerManifest.ApiLayer.EnableEnvironment; 106 | if (!string.IsNullOrWhiteSpace(enableVar)) { 107 | isEnabled = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(enableVar)); 108 | } 109 | 110 | bool actuallyEnabled; 111 | 112 | // ReSharper disable once ConvertIfStatementToSwitchStatement 113 | // Overly pedantic for avoidance of confusion 114 | if (isDisabled && isEnabled) { 115 | actuallyEnabled = false; 116 | } else if (isDisabled) { 117 | actuallyEnabled = false; 118 | } else if (isEnabled) { 119 | actuallyEnabled = true; 120 | } else { 121 | actuallyEnabled = true; 122 | } 123 | 124 | Log.Information("[{LogTag}] API Layer \"{apiLayer}\" (Enabled: {enabled} DisableEnvironment = {disableEnvironment}, EnableEnvironment = {enableEnvironment}) {pathToManifest}", LogTag, layerManifest.ApiLayer.Name, actuallyEnabled, isDisabled, isEnabled, path); 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Runtime.InteropServices; 3 | using Figgle; 4 | using Serilog; 5 | using Silk.NET.OpenXR; 6 | using SLZ.XRDoctor; 7 | 8 | var programName = FiggleFonts.Standard.Render("SLZ OpenXR Doctor"); 9 | Console.WriteLine(programName); 10 | 11 | const string outputTemplate = "[{Timestamp:HH:mm:ss} {Level}] {Message:l}{NewLine}{Exception}"; 12 | 13 | var utcNowStr = DateTime.UtcNow 14 | .ToString(CultureInfo.InvariantCulture.DateTimeFormat.UniversalSortableDateTimePattern,CultureInfo.InvariantCulture); 15 | var localNowStr = DateTime.UtcNow.ToLocalTime() 16 | .ToString(CultureInfo.InvariantCulture.DateTimeFormat.SortableDateTimePattern, CultureInfo.InvariantCulture); 17 | 18 | var sanitizedNowStr = new string(localNowStr 19 | .Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c) 20 | .ToArray()); 21 | var logFilename = $"xrdoctor-{sanitizedNowStr}.txt"; 22 | 23 | 24 | Log.Logger = new LoggerConfiguration() 25 | .WriteTo.Console(outputTemplate: outputTemplate) 26 | .WriteTo.File(logFilename, outputTemplate: outputTemplate) 27 | .MinimumLevel.Debug() 28 | .CreateLogger(); 29 | 30 | Log.Information("[{LogTag}] Starting log at {UTC} (UTC) {local} (Local)", "XRDoctor", utcNowStr, localNowStr); 31 | Log.Information("[{LogTag}] Version: {Version}", "XRDoctor", typeof(Program).Assembly.GetName().Version); 32 | 33 | OpenXRDiagnostics.FindActiveRuntime(out var XR_RUNTIME_JSON); 34 | if (!string.IsNullOrWhiteSpace(XR_RUNTIME_JSON)) { 35 | Log.Information("[{LogTag}] Active Runtime (XR_RUNTIME_JSON): {XR_RUNTIME_JSON}", "OpenXR", XR_RUNTIME_JSON); 36 | } else { Log.Error("[{LogTag}] Could not find an active OpenXR runtime.", "OpenXR"); } 37 | 38 | OpenXRDiagnostics.FindRegisteredRuntimes(out var runtimes); 39 | foreach (var (runtimeName, manifest) in runtimes) { 40 | if (string.IsNullOrWhiteSpace(manifest.Runtime.Name)) { 41 | Log.Information("[{LogTag}] Available Runtime: {runtime} - {@manifest}", "OpenXR", runtimeName, manifest); 42 | } else { 43 | Log.Information("[{LogTag}] Available Runtime: \"{name}\" - {runtime} - {@manifest}", "OpenXR", manifest.Runtime.Name, 44 | runtimeName, 45 | manifest); 46 | } 47 | } 48 | 49 | OpenXRDiagnostics.FindImplicitApiLayers(out var implicitLayers); 50 | 51 | VulkanDiagnostics.FindImplicitLayers(out var layers); 52 | 53 | OculusDiagnostics.CheckDirectly(out var hasOculus); 54 | SteamVRDiagnostics.CheckDirectly(out var hasSteamVR); 55 | ViveVRDiagnostics.CheckDirectly(out var hasViveVr); 56 | WindowsMRDiagnostics.CheckDirectly(out var hasWindowsMR); 57 | VirtualDesktopDiagnostics.CheckDirectly(out var hasVirtualDesktop); 58 | 59 | HardwareDiagnostics.ListHardware(); 60 | HardwareDiagnostics.FindHeadsets(out var headsets); 61 | 62 | if (XR_RUNTIME_JSON.Contains("Steam", StringComparison.InvariantCultureIgnoreCase)) { 63 | SteamVRDiagnostics.CheckDevices(); 64 | } 65 | 66 | SystemDiagnostics.Check(); 67 | 68 | #region OpenXR 69 | 70 | Log.Information("[{LogTag}] Loading OpenXR.", "OpenXR"); 71 | var xr = XR.GetApi(); 72 | 73 | unsafe { 74 | uint count = 0; 75 | xr.EnumerateApiLayerProperties(count, &count, null); 76 | Span apiLayers = stackalloc ApiLayerProperties[(int) count]; 77 | for (var i = 0; i < count; i++) { apiLayers[i] = new ApiLayerProperties(StructureType.ApiLayerProperties); } 78 | 79 | xr.EnumerateApiLayerProperties(ref count, apiLayers); 80 | 81 | IDictionary supportedLayers = new Dictionary(); 82 | 83 | foreach (var apiLayer in apiLayers) { 84 | string name; 85 | unsafe { name = Marshal.PtrToStringUTF8((IntPtr) apiLayer.LayerName); } 86 | 87 | supportedLayers[name] = apiLayer.LayerVersion; 88 | Log.Information("[{LogTag}] API Layer: Name={Name} Version={Version}", "OpenXR", name, apiLayer.LayerVersion); 89 | } 90 | 91 | ISet supportedExtensions = new HashSet(); 92 | 93 | uint instanceExtensionCount = 0; 94 | xr.EnumerateInstanceExtensionProperties((byte*) IntPtr.Zero, instanceExtensionCount, &instanceExtensionCount, null); 95 | Span exts = stackalloc ExtensionProperties[(int) instanceExtensionCount]; 96 | for (var i = 0; i < instanceExtensionCount; i++) { 97 | exts[i] = new ExtensionProperties(StructureType.TypeExtensionProperties); 98 | } 99 | 100 | xr.EnumerateInstanceExtensionProperties((string) null, ref instanceExtensionCount, exts); 101 | 102 | foreach (var extensionProp in exts) { 103 | string name; 104 | unsafe { name = Marshal.PtrToStringUTF8((IntPtr) extensionProp.ExtensionName); } 105 | 106 | supportedExtensions.Add(name); 107 | Log.Information("[{LogTag}][Instance] Extension: Name={Name} Version={Version}", "OpenXR", name, extensionProp.ExtensionVersion); 108 | } 109 | 110 | var ici = new InstanceCreateInfo(StructureType.InstanceCreateInfo) { 111 | EnabledApiLayerCount = 0, 112 | EnabledApiLayerNames = null, 113 | }; 114 | 115 | var extensions = new List(); 116 | if (supportedExtensions.Contains("XR_KHR_D3D11_enable")) { extensions.Add("XR_KHR_D3D11_enable"); } else { 117 | Log.Error("[{LogTag}] XR_KHR_D3D11_enable extension not supported!", "OpenXR"); 118 | } 119 | 120 | if (supportedExtensions.Contains("XR_FB_display_refresh_rate")) { extensions.Add("XR_FB_display_refresh_rate"); } 121 | 122 | if (supportedExtensions.Contains("XR_EXT_debug_utils")) { extensions.Add("XR_EXT_debug_utils"); } 123 | 124 | var instance = new Instance(); 125 | 126 | var ansiExtensions = extensions.Select(e => Marshal.StringToHGlobalAnsi(e)).ToArray(); 127 | fixed (IntPtr* fixedAnsiExtensions = ansiExtensions) { 128 | ici.EnabledExtensionCount = (uint) extensions.Count; 129 | ici.EnabledExtensionNames = (byte**) fixedAnsiExtensions; 130 | 131 | const string appname = "SLZ OpenXR Doctor"; 132 | ici.ApplicationInfo = new ApplicationInfo() { 133 | ApiVersion = 1ul << 48, 134 | ApplicationVersion = 1, 135 | EngineVersion = 1, 136 | }; 137 | Marshal.Copy(appname.ToCharArray(), 0, (IntPtr) ici.ApplicationInfo.ApplicationName, appname.Length); 138 | ici.ApplicationInfo.EngineName[0] = (byte) '\0'; 139 | 140 | xr.CreateInstance(ici, ref instance); 141 | xr.CurrentInstance = instance; 142 | } 143 | 144 | // INSTANCE 145 | var instanceProperties = new InstanceProperties(StructureType.InstanceProperties); 146 | xr.GetInstanceProperties(instance, ref instanceProperties); 147 | var runtimeName = Marshal.PtrToStringUTF8((IntPtr) instanceProperties.RuntimeName); 148 | Log.Information("[{LogTag}][Instance] Runtime: Name={Name} Version={Version}", "OpenXR", runtimeName, 149 | instanceProperties.RuntimeVersion); 150 | 151 | // SYSTEM 152 | var systemGetInfo = new SystemGetInfo(StructureType.SystemGetInfo) {FormFactor = FormFactor.HeadMountedDisplay}; 153 | ulong systemId = 0; // NOTE: THIS MAY BE ZERO IF STEAMVR IS OPEN BUT LOADED XR RUNTIME IS OCULUS'S 154 | xr.GetSystem((Instance) xr.CurrentInstance, &systemGetInfo, (ulong*) &systemId); 155 | Log.Information("[{LogTag}][System] Id={SystemId}", "OpenXR", systemId); 156 | 157 | var systemProperties = new SystemProperties(StructureType.SystemProperties); 158 | xr.GetSystemProperties(instance, systemId, ref systemProperties); 159 | var systemName = Marshal.PtrToStringUTF8((IntPtr) systemProperties.SystemName); 160 | Log.Information("[{LogTag}][System] Name={Name}", "OpenXR", systemName); 161 | 162 | foreach (var ansiExtension in ansiExtensions) { Marshal.FreeHGlobal(ansiExtension); } 163 | } 164 | 165 | #endregion 166 | 167 | GameDiagnostics.Check(); 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XRDoctor 2 | 3 | Quick and dirty tool to dump diagnostic info about the system, especially as it pertains to OpenXR, into a file. 4 | 5 | # Acknowledgments 6 | Thanks to https://github.com/shiena/OpenXRRuntimeSelector for clues as to runtime locations! -------------------------------------------------------------------------------- /RuntimeManifest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | 4 | namespace SLZ.XRDoctor; 5 | 6 | [Serializable] 7 | public sealed class RuntimeManifest { 8 | [JsonProperty("file_format_version", Required = Required.Always)] 9 | public string FileFormatVersion { get; private set; } = null!; 10 | 11 | [JsonProperty("runtime", Required = Required.Always)] 12 | public Runtime Runtime { get; private set; } = null!; 13 | 14 | [JsonConstructor] 15 | public RuntimeManifest() { } 16 | } 17 | 18 | [Serializable] 19 | public sealed class Runtime { 20 | [JsonProperty("name")] 21 | public string Name { get; private set; } = null!; 22 | 23 | [JsonProperty("library_path", Required = Required.Always)] 24 | public string LibraryPath { get; private set; } = null!; 25 | 26 | [JsonExtensionData] 27 | public IDictionary AdditionalData { get; private set; } = null!; 28 | 29 | [JsonConstructor] 30 | public Runtime() { } 31 | } -------------------------------------------------------------------------------- /SteamVRDiagnostics.cs: -------------------------------------------------------------------------------- 1 | using Gameloop.Vdf; 2 | using Gameloop.Vdf.JsonConverter; 3 | using Microsoft.Win32; 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Linq; 6 | using Valve.VR; 7 | 8 | namespace SLZ.XRDoctor; 9 | 10 | public static class SteamVRDiagnostics { 11 | private static readonly Serilog.ILogger Log = Serilog.Log.ForContext(typeof(SteamVRDiagnostics)); 12 | private const string LogTag = "SteamVR"; 13 | 14 | public static void CheckDirectly(out bool hasSteamVR) { 15 | Log.Information("[{LogTag}] Checking directly for runtime at {SteamRegistryKey}.", LogTag, 16 | @"HKEY_CURRENT_USER\SOFTWARE\Valve\Steam"); 17 | using var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Valve\Steam"); 18 | do { 19 | try { 20 | var o = key?.GetValue("SteamPath"); 21 | if (o is not string steamLocation || string.IsNullOrWhiteSpace(steamLocation)) { 22 | Log.Information("[{LogTag}] Not found.", LogTag); 23 | break; 24 | } 25 | 26 | var lfPath = Path.Combine(steamLocation, "steamapps", "libraryfolders.vdf"); 27 | 28 | if (!File.Exists(lfPath)) { 29 | Log.Warning("[{LogTag}] Could not find libraryfolders.vdf at \"{lfPath}\"", LogTag, lfPath); 30 | break; 31 | } 32 | 33 | var folderCount = 0; 34 | 35 | var lfStr = File.ReadAllText(lfPath); 36 | var lf = VdfConvert.Deserialize(lfStr); 37 | var lfJson = lf.ToJson().Value.Values(); 38 | foreach (var folderDict in lfJson) { 39 | if (folderDict is not JObject folder) { continue; } 40 | 41 | if (!folder.TryGetValue("path", out var pathJToken)) { continue; } 42 | 43 | var path = pathJToken.ToObject(); 44 | if (string.IsNullOrWhiteSpace(path)) { continue; } 45 | 46 | if (!Directory.Exists(path)) { continue; } 47 | 48 | var runtimePath = Path.Combine(path, "steamapps", "common", "SteamVR", "steamxr_win64.json"); 49 | 50 | Log.Information( 51 | "[{LogTag}] Checking Steam install directory for Runtime JSON at path: \"{runtimePath}\".", 52 | LogTag, 53 | runtimePath); 54 | 55 | if (!File.Exists(runtimePath)) { 56 | Log.Warning("[{LogTag}] Runtime JSON not found at expected path \"{runtimePath}\".", 57 | LogTag, 58 | runtimePath); 59 | continue; 60 | } 61 | 62 | var runtimeJsonStr = File.ReadAllText(runtimePath); 63 | 64 | if (string.IsNullOrWhiteSpace(runtimeJsonStr)) { 65 | Log.Error("[{LogTag}] Runtime JSON was empty at \"{runtimePath}\".", LogTag, runtimePath); 66 | break; 67 | } 68 | 69 | RuntimeManifest manifest; 70 | try { 71 | manifest = JsonConvert.DeserializeObject(runtimeJsonStr); 72 | } catch (JsonException e) { 73 | Log.Error(e, "[{LogTag}] JSON did not parse correctly at \"{runtimePath}\".", LogTag, 74 | runtimePath); 75 | break; 76 | } 77 | 78 | Log.Information("[{LogTag}] Found runtime at path \"{location}\".", LogTag, runtimePath); 79 | hasSteamVR = true; 80 | } 81 | } catch (Exception e) { Log.Error(e, "[{LogTag}] Error while determining install state.", LogTag); } 82 | } while (false); 83 | 84 | hasSteamVR = false; 85 | } 86 | 87 | public static void CheckDevices() { 88 | var exePath = AppContext.BaseDirectory; 89 | var appManifest = Path.Combine(exePath, "app.vrmanifest"); 90 | var actionsJson = Path.Combine(exePath, "actions.json"); 91 | 92 | var err = default(EVRInitError); 93 | var sys = OpenVR.Init(ref err, EVRApplicationType.VRApplication_Overlay); 94 | if (err != EVRInitError.None) { Log.Error("[{LogTag}] Error initting {err}", LogTag, err); } 95 | 96 | var appManifestResult = OpenVR.Applications.AddApplicationManifest(appManifest, false); 97 | if (appManifestResult != EVRApplicationError.None) { 98 | Log.Error("[{LogTag}] Error adding app manifest: {appManifestResult}", LogTag, appManifestResult); 99 | } 100 | 101 | var actionManifestResult = OpenVR.Input.SetActionManifestPath(actionsJson); 102 | if (actionManifestResult != EVRInputError.None) { 103 | Log.Error("[{LogTag}] Error setting action manifest: {actionManifestResult}", LogTag, 104 | actionManifestResult); 105 | } 106 | 107 | ulong defaultActionSetHandle = 0; 108 | var ashResult = OpenVR.Input.GetActionSetHandle("/actions/default", ref defaultActionSetHandle); 109 | if (ashResult != EVRInputError.None) { 110 | Log.Error("[{LogTag}] Error getting action set handle for default action set: {ashResult}", LogTag, 111 | ashResult); 112 | } 113 | 114 | VRActiveActionSet_t lhaas = default; 115 | lhaas.ulActionSet = defaultActionSetHandle; 116 | var aas = new[] {lhaas}; 117 | 118 | ETrackedPropertyError ipdError = default; 119 | var ipd = OpenVR.System.GetFloatTrackedDeviceProperty(0, ETrackedDeviceProperty.Prop_UserIpdMeters_Float, 120 | ref ipdError); 121 | if (ipdError == ETrackedPropertyError.TrackedProp_Success) { 122 | Log.Information("[{LogTag}] IPD: {ipd}", LogTag, ipd); 123 | } else { Log.Error("[{LogTag}] Error updating IPD: {ipdError}", LogTag, ipdError); } 124 | 125 | var left = -1; 126 | var right = -1; 127 | 128 | for (uint i = 0; i < OpenVR.k_unMaxTrackedDeviceCount; i++) { 129 | var trackedDeviceClass = OpenVR.System.GetTrackedDeviceClass(i); 130 | 131 | if (trackedDeviceClass != ETrackedDeviceClass.Invalid) { 132 | var propErr = ETrackedPropertyError.TrackedProp_Success; 133 | var count = OpenVR.System.GetStringTrackedDeviceProperty(i, 134 | ETrackedDeviceProperty.Prop_ModelNumber_String, null, 0, ref propErr); 135 | var modelNumber = new System.Text.StringBuilder((int) count); 136 | OpenVR.System.GetStringTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_ModelNumber_String, 137 | modelNumber, count, ref propErr); 138 | 139 | Log.Information("[{LogTag}] Device {i}: modelNumber={modelNumber} class={class}", LogTag, i, 140 | modelNumber, trackedDeviceClass); 141 | } 142 | 143 | if (trackedDeviceClass == ETrackedDeviceClass.Controller) { 144 | var role = OpenVR.System.GetControllerRoleForTrackedDeviceIndex(i); 145 | switch (role) { 146 | case ETrackedControllerRole.LeftHand: 147 | left = (int) i; 148 | break; 149 | case ETrackedControllerRole.RightHand: 150 | right = (int) i; 151 | break; 152 | } 153 | } 154 | } 155 | 156 | OpenVR.Shutdown(); 157 | } 158 | } -------------------------------------------------------------------------------- /SystemDiagnostics.cs: -------------------------------------------------------------------------------- 1 | namespace SLZ.XRDoctor; 2 | 3 | public static class SystemDiagnostics { 4 | private static readonly Serilog.ILogger Log = Serilog.Log.ForContext(typeof(SystemDiagnostics)); 5 | private const string LogTag = "System"; 6 | 7 | public static void Check() { 8 | CheckTime(); 9 | CheckSystem(); 10 | } 11 | 12 | public static void CheckTime() 13 | { 14 | Log.Information( 15 | "[{LogTag}] Timezone ID: {TimezoneID} DisplayName: {DisplayName} StandardName: {StandardName} " + 16 | "DaylightName: {DaylightName} BaseUtcOffset: {BaseUtcOffset} " + 17 | "SupportsDaylightSavingTime: {SupportsDaylightSavingTime} IsDaylightSavingTime: {IsDaylightSavingTime}", 18 | LogTag, 19 | TimeZoneInfo.Local.Id, TimeZoneInfo.Local.DisplayName, TimeZoneInfo.Local.StandardName, 20 | TimeZoneInfo.Local.DaylightName, TimeZoneInfo.Local.BaseUtcOffset, 21 | TimeZoneInfo.Local.SupportsDaylightSavingTime, TimeZoneInfo.Local.IsDaylightSavingTime(DateTime.Now)); 22 | } 23 | 24 | public static void CheckSystem() { 25 | 26 | Log.Information( 27 | "[{LogTag}] OS Version: {OSVersion} ", LogTag, Environment.OSVersion); 28 | 29 | 30 | foreach (var drive in DriveInfo.GetDrives()) { 31 | if (!drive.IsReady) { continue; } 32 | 33 | if (drive.DriveType != DriveType.Fixed) { continue; } 34 | 35 | var availableFreeSpace = drive.AvailableFreeSpace; 36 | var availableFreeSpaceGiB = drive.AvailableFreeSpace / (1024 * 1024 * 1024); 37 | 38 | var totalFreeSpace = drive.TotalFreeSpace; 39 | var totalFreeSpaceGiB = drive.TotalFreeSpace / (1024 * 1024 * 1024); 40 | 41 | var totalSize = drive.TotalSize; 42 | var totalSizeGiB = drive.TotalSize / (1024 * 1024 * 1024); 43 | 44 | Log.Information( 45 | "[{LogTag}] Drive Name={name} Filesystem={filesystem}, " + 46 | "Available Space={availableFreeSpace}, ({availableFreeSpaceGiB} GiB) " + 47 | "Total Free Space={totalFreeSpace} ({totalFreeSpaceGiB} GiB), " + 48 | "Total Size={totalSize} ({totalSizeGiB} GiB)", 49 | LogTag, drive.Name, drive.DriveFormat, availableFreeSpace, availableFreeSpaceGiB, totalFreeSpace, 50 | totalFreeSpaceGiB, totalSize, totalSizeGiB); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /VirtualDesktopDiagnostics.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Win32; 2 | using Newtonsoft.Json; 3 | using SLZ.XRDoctor; 4 | 5 | public static class VirtualDesktopDiagnostics 6 | { 7 | private static readonly Serilog.ILogger Log = Serilog.Log.ForContext(typeof(VirtualDesktopDiagnostics)); 8 | private const string LogTag = "VirtualDesktop"; 9 | 10 | public static void CheckDirectly(out bool hasVirtualDesktop) { 11 | hasVirtualDesktop = false; 12 | 13 | Log.Information("[{LogTag}] Checking directly for runtime.", LogTag); 14 | try { 15 | using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\Folders"); 16 | if (key == null) { return; } 17 | 18 | var openXRDirectory = string.Empty; 19 | foreach (var valueName in key.GetValueNames()) { 20 | if (valueName.EndsWith(@"Virtual Desktop Streamer\OpenXR\")) { 21 | openXRDirectory = valueName; 22 | hasVirtualDesktop = true; 23 | break; 24 | } 25 | } 26 | 27 | if (!hasVirtualDesktop) { 28 | Log.Warning("[{LogTag}] OpenXR directory not found.", LogTag); 29 | return; 30 | } 31 | 32 | if (!Directory.Exists(openXRDirectory)) { 33 | Log.Warning("[{LogTag}] OpenXR directory \"{OpenXRDirectory}\" not found.", LogTag, openXRDirectory); 34 | return; 35 | } 36 | 37 | { 38 | var runtimePath = Path.Combine(openXRDirectory, "virtualdesktop-openxr.json"); 39 | if (!File.Exists(runtimePath)) { 40 | Log.Warning("[{LogTag}] Runtime JSON not found at expected path \"{runtimePath}\".", LogTag, 41 | runtimePath); 42 | goto Check32; 43 | } 44 | 45 | var runtimeJsonStr = File.ReadAllText(runtimePath); 46 | 47 | if (string.IsNullOrWhiteSpace(runtimeJsonStr)) { 48 | Log.Error("[{LogTag}] Runtime JSON was empty at \"{runtimePath}\".", LogTag, runtimePath); 49 | goto Check32; 50 | } 51 | 52 | RuntimeManifest manifest; 53 | try { 54 | manifest = JsonConvert.DeserializeObject(runtimeJsonStr); 55 | } catch (JsonException e) { 56 | Log.Error(e, "[{LogTag}] JSON did not parse correctly at \"{runtimePath}\".", LogTag, 57 | runtimePath); 58 | goto Check32; 59 | } 60 | 61 | Log.Information("[{LogTag}] Found runtime at path \"{location}\".", LogTag, runtimePath); 62 | } 63 | Check32: 64 | { 65 | var runtimePath = Path.Combine(openXRDirectory, "virtualdesktop-openxr-32.json"); 66 | if (!File.Exists(runtimePath)) { 67 | Log.Warning("[{LogTag}] Runtime JSON not found at expected path \"{runtimePath}\".", LogTag, 68 | runtimePath); 69 | return; 70 | } 71 | 72 | var runtimeJsonStr = File.ReadAllText(runtimePath); 73 | 74 | if (string.IsNullOrWhiteSpace(runtimeJsonStr)) { 75 | Log.Error("[{LogTag}] Runtime JSON was empty at \"{runtimePath}\".", LogTag, runtimePath); 76 | return; 77 | } 78 | 79 | RuntimeManifest manifest; 80 | try { 81 | manifest = JsonConvert.DeserializeObject(runtimeJsonStr); 82 | } catch (JsonException e) { 83 | Log.Error(e, "[{LogTag}] JSON did not parse correctly at \"{runtimePath}\".", LogTag, 84 | runtimePath); 85 | return; 86 | } 87 | 88 | Log.Information("[{LogTag}] Found runtime at path \"{location}\".", LogTag, runtimePath); 89 | } 90 | 91 | } catch (Exception e) { 92 | Log.Error(e, "[{LogTag}] Error while determining install state.", LogTag); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /ViveVRDiagnostics.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Win32; 2 | using Newtonsoft.Json; 3 | 4 | namespace SLZ.XRDoctor; 5 | 6 | public static class ViveVRDiagnostics { 7 | private static readonly Serilog.ILogger Log = Serilog.Log.ForContext(typeof(ViveVRDiagnostics)); 8 | private const string LogTag = "ViveVR"; 9 | 10 | public static void CheckDirectly(out bool hasViveVR) { 11 | Log.Information("[{LogTag}] Checking directly for Vive OpenXR runtime at {ViveVRRegistryKey}.", 12 | LogTag, @"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\HtcVive\Updater"); 13 | using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\WOW6432Node\HtcVive\Updater"); 14 | do { 15 | try { 16 | var o = key?.GetValue("AppPath"); 17 | 18 | if (o is not string viveLocation || string.IsNullOrWhiteSpace(viveLocation)) { 19 | Log.Information("[{LogTag}] Vive Updater not found.", LogTag); 20 | break; 21 | } 22 | 23 | var runtimePath = Path.Combine(viveLocation, "App", "ViveVRRuntime", "ViveVR_openxr", 24 | "ViveOpenXR.json"); 25 | 26 | if (!File.Exists(runtimePath)) { 27 | Log.Warning("[{LogTag}] Runtime JSON not found at expected path \"{runtimePath}\".", LogTag, 28 | runtimePath); 29 | break; 30 | } 31 | 32 | var runtimeJsonStr = File.ReadAllText(runtimePath); 33 | 34 | if (string.IsNullOrWhiteSpace(runtimeJsonStr)) { 35 | Log.Error("[{LogTag}] Runtime JSON was empty at \"{runtimePath}\".", LogTag, runtimePath); 36 | break; 37 | } 38 | 39 | RuntimeManifest manifest; 40 | try { 41 | manifest = JsonConvert.DeserializeObject(runtimeJsonStr); 42 | } catch (JsonException e) { 43 | Log.Error(e, "[{LogTag}] Runtime JSON did not parse correctly at {runtimePath}.", LogTag, 44 | runtimePath); 45 | break; 46 | } 47 | 48 | Log.Information("[{LogTag}] Runtime found at path \"{location}\".", LogTag, runtimePath); 49 | hasViveVR = true; 50 | return; 51 | } catch (Exception e) { Log.Error(e, "[{LogTag}] Error while determining install state.", LogTag); } 52 | } while (false); 53 | 54 | hasViveVR = false; 55 | } 56 | } -------------------------------------------------------------------------------- /VulkanDiagnostics.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Win32; 2 | using Newtonsoft.Json; 3 | 4 | 5 | namespace SLZ.XRDoctor; 6 | 7 | public static class VulkanDiagnostics { 8 | private static readonly Serilog.ILogger Log = Serilog.Log.ForContext(typeof(VulkanDiagnostics)); 9 | private const string LogTag = "Vulkan"; 10 | 11 | public static void FindImplicitLayers(out Dictionary layers) { 12 | Log.Information("[{LogTag}] Listing implicit Vulkan API layers according to {ImplicitApiLayersRegistryKey}", 13 | LogTag, @"HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\ImplicitLayers"); 14 | 15 | layers = new Dictionary(); 16 | 17 | using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Khronos\Vulkan\ImplicitLayers"); 18 | if (key == null) { 19 | Log.Information("[{LogTag}] No implicit API layers (This isn't unusual).", LogTag); 20 | return; 21 | } 22 | 23 | foreach (var layerJsonPath in key.GetValueNames()) { 24 | if (key.GetValue(layerJsonPath) is not (int and 0)) { continue; } 25 | 26 | if (!File.Exists(layerJsonPath)) { 27 | Log.Error("[{LogTag}] API Layer \"{apiLayer}\" is registered but was not found.", LogTag, layerJsonPath); 28 | continue; 29 | } 30 | 31 | var layerJson = File.ReadAllText(layerJsonPath); 32 | if (string.IsNullOrWhiteSpace(layerJson)) { 33 | Log.Error("[{LogTag}] API Layer \"{apiLayer}\" is registered but was empty.", LogTag, layerJsonPath); 34 | continue; 35 | } 36 | 37 | VulkanLayerManifest manifest; 38 | try { manifest = JsonConvert.DeserializeObject(layerJson); } catch (JsonException e) { 39 | Log.Error(e, "[{LogTag}] API Layer \"{apiLayer}\" is registered but did not parse correctly.", 40 | LogTag, layerJsonPath); 41 | continue; 42 | } 43 | 44 | layers[layerJsonPath] = manifest; 45 | } 46 | 47 | foreach (var (path, layerManifest) in layers) { 48 | Log.Information("[{LogTag}] API Layer \"{apiLayer}\" {pathToManifest}", LogTag, layerManifest.Layer.Name, path); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /VulkanLayerManifest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | 4 | namespace SLZ.XRDoctor; 5 | 6 | public sealed class VulkanLayerManifest { 7 | [JsonProperty("file_format_version", Required = Required.Always)] 8 | public string FileFormatVersion { get; private set; } 9 | 10 | [JsonProperty("layer")] 11 | public VulkanLayer Layer { get; private set; } 12 | 13 | [JsonProperty("layers")] 14 | public List Layers { get; private set; } 15 | } 16 | 17 | public sealed class VulkanLayer { 18 | [JsonProperty("name")] 19 | public string Name { get; private set; } = null!; 20 | 21 | [JsonProperty("type")] 22 | public string Type { get; private set; } = null!; 23 | 24 | [JsonProperty("library_path")] 25 | public string LibraryPath { get; private set; } = null!; 26 | 27 | [JsonProperty("api_version")] 28 | public string ApiVersion { get; private set; } = null!; 29 | 30 | [JsonProperty("implementation_version")] 31 | public string ImplementationVersion { get; private set; } = null!; 32 | 33 | [JsonProperty("description")] 34 | public string Description { get; private set; } = null!; 35 | 36 | [JsonProperty("functions")] 37 | public Dictionary Functions { get; private set; } = null!; 38 | 39 | [JsonProperty("instance_extensions")] 40 | public List InstanceExtensions { get; private set; } = null!; 41 | 42 | [JsonProperty("device_extensions")] 43 | public List DeviceExtensions { get; private set; } = null!; 44 | 45 | // Apparently malformed in Steam 46 | [JsonProperty("disable_environment")] 47 | public JToken DisableEnvironment { get; private set; } = null!; 48 | 49 | // Apparently malformed in Steam 50 | [JsonProperty("enable_environment")] 51 | public JToken EnableEnvironment { get; private set; } = null!; 52 | 53 | [JsonProperty("component_layers")] 54 | public List ComponentLayers { get; private set; } = null!; 55 | 56 | [JsonProperty("pre_instance_functions")] 57 | public Dictionary PreInstanceFunctions { get; private set; } = null!; 58 | 59 | [JsonConstructor] 60 | public VulkanLayer() { } 61 | } 62 | 63 | public sealed class VulkanInstanceExtension { 64 | [JsonProperty("name")] 65 | public string Name { get; private set; } = null!; 66 | 67 | [JsonProperty("spec_version")] 68 | public object SpecVersion { get; private set; } = null!; 69 | 70 | [JsonConstructor] 71 | public VulkanInstanceExtension() { } 72 | } 73 | 74 | public sealed class VulkanDeviceExtension { 75 | [JsonProperty("name")] 76 | public string Name { get; private set; } = null!; 77 | 78 | [JsonProperty("spec_version")] 79 | public object SpecVersion { get; private set; } = null!; 80 | 81 | [JsonProperty("entrypoints")] 82 | public List Entrypoints { get; private set; } = null!; 83 | 84 | [JsonConstructor] 85 | public VulkanDeviceExtension() { } 86 | } -------------------------------------------------------------------------------- /WindowsMRDiagnostics.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace SLZ.XRDoctor; 4 | 5 | public static class WindowsMRDiagnostics { 6 | private static readonly Serilog.ILogger Log = Serilog.Log.ForContext(typeof(WindowsMRDiagnostics)); 7 | private const string LogTag = "WindowsMR"; 8 | 9 | public static void CheckDirectly(out bool hasWindowsMR) { 10 | var runtimePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), 11 | "MixedRealityRuntime.json"); 12 | Log.Information("[{LogTag}] Checking directly for runtime at path {runtimePath}.", LogTag, runtimePath); 13 | do { 14 | try { 15 | if (!File.Exists(runtimePath)) { 16 | Log.Warning("[{LogTag}] Runtime JSON not found at expected path \"{runtimePath}\".", LogTag, 17 | runtimePath); 18 | continue; 19 | } 20 | 21 | var runtimeJsonStr = File.ReadAllText(runtimePath); 22 | 23 | if (string.IsNullOrWhiteSpace(runtimeJsonStr)) { 24 | Log.Error("[{LogTag}] Runtime JSON was empty at \"{runtimePath}\".", LogTag, runtimePath); 25 | break; 26 | } 27 | 28 | RuntimeManifest manifest; 29 | try { 30 | manifest = JsonConvert.DeserializeObject(runtimeJsonStr); 31 | } catch (JsonException e) { 32 | Log.Error(e, "[{LogTag}] JSON did not parse correctly at \"{runtimePath}\".", LogTag, 33 | runtimePath); 34 | break; 35 | } 36 | 37 | Log.Information("[{LogTag}] Found runtime at path \"{location}\".", LogTag, runtimePath); 38 | hasWindowsMR = true; 39 | } catch (Exception e) { Log.Error(e, "[{LogTag}] Error while determining install state.", LogTag); } 40 | } while (false); 41 | 42 | hasWindowsMR = false; 43 | } 44 | } -------------------------------------------------------------------------------- /XRDoctor.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0-windows 6 | enable 7 | enable 8 | true 9 | XRDoctor 10 | yewnyx 11 | Utility for debugging XR runtime issues 12 | Stress Level Zero, Inc. 13 | Stress Level Zero, Inc. 14 | XRDoctor.ico 15 | https://github.com/StressLevelZero/XRDoctor 16 | https://github.com/StressLevelZero/XRDoctor/blob/master/LICENSE 17 | https://github.com/StressLevelZero/XRDoctor.git 18 | git 19 | 0.0.7 20 | 0.0.8.1 21 | v0.0.7 22 | 0.0.8.1 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | PreserveNewest 43 | 44 | 45 | PreserveNewest 46 | 47 | 48 | PreserveNewest 49 | 50 | 51 | PreserveNewest 52 | 53 | 54 | PreserveNewest 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /XRDoctor.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StressLevelZero/XRDoctor/3b90a457e63806a2bc340ff5d6b587ba5f81fea8/XRDoctor.ico -------------------------------------------------------------------------------- /XRDoctor.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XRDoctor", "XRDoctor.csproj", "{F6506754-D1B3-4873-88C3-2CF205911FBD}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {F6506754-D1B3-4873-88C3-2CF205911FBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {F6506754-D1B3-4873-88C3-2CF205911FBD}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {F6506754-D1B3-4873-88C3-2CF205911FBD}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {F6506754-D1B3-4873-88C3-2CF205911FBD}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /actions.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_bindings": [ 3 | { 4 | "controller_type": "knuckles", 5 | "binding_url": "knuckles_bindings.json" 6 | }], 7 | "actions": [ 8 | { 9 | "name": "/actions/default/in/empty", 10 | "requirement": "optional", 11 | "type": "boolean" 12 | }, 13 | { 14 | "name": "/actions/default/in/SkeletonLeftHand", 15 | "type": "skeleton", 16 | "skeleton": "/skeleton/hand/left" 17 | }, 18 | { 19 | "name": "/actions/default/in/SkeletonRightHand", 20 | "type": "skeleton", 21 | "skeleton": "/skeleton/hand/right" 22 | }, 23 | { 24 | "name" : "/actions/default/in/FingerCurlIndex", 25 | "type" : "vector1" 26 | }, 27 | { 28 | "name" : "/actions/default/in/FingerCurlMiddle", 29 | "type" : "vector1" 30 | }, 31 | { 32 | "name" : "/actions/default/in/FingerCurlRing", 33 | "type" : "vector1" 34 | }, 35 | { 36 | "name" : "/actions/default/in/FingerCurlPinky", 37 | "type" : "vector1" 38 | }, 39 | { 40 | "name": "/actions/default/in/pose", 41 | "type": "pose" 42 | } 43 | ], 44 | "action_sets": [ 45 | { 46 | "name": "/actions/default", 47 | "usage": "leftright" 48 | } 49 | ], 50 | "localization": [ 51 | { 52 | "language_tag": "en_US", 53 | "/actions/default/in/SkeletonLeftHand": "Skeleton (Left)", 54 | "/actions/default/in/SkeletonRightHand": "Skeleton (Right)" 55 | } 56 | ] 57 | } -------------------------------------------------------------------------------- /app.vrmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "source" : "builtin", 3 | "applications": [{ 4 | "app_key": "system.generated.xrdoctor.exe", 5 | "launch_type": "binary", 6 | "binary_path_windows": "XRDoctor.exe", 7 | "is_dashboard_overlay": true, 8 | "action_manifest_path": "./actions.json", 9 | 10 | "strings": { 11 | "en_us": { 12 | "name": "XR Doctor SteamVR Test" 13 | } 14 | } 15 | }] 16 | } -------------------------------------------------------------------------------- /knuckles_bindings.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": { 3 | "/actions/default": { 4 | "chords": [], 5 | "poses": [ 6 | { 7 | "output": "/actions/default/in/pose", 8 | "path": "/user/hand/left/pose/raw" 9 | }, 10 | { 11 | "output": "/actions/default/in/pose", 12 | "path": "/user/hand/right/pose/raw" 13 | } 14 | ], 15 | "skeleton": [ 16 | { 17 | "output": "/actions/default/in/SkeletonLeftHand", 18 | "path": "/user/hand/left/input/skeleton/left" 19 | }, 20 | { 21 | "output": "/actions/default/in/SkeletonRightHand", 22 | "path": "/user/hand/right/input/skeleton/right" 23 | } 24 | ], 25 | "sources": [ 26 | { 27 | "inputs": { 28 | "pull": { 29 | "output": "/actions/default/in/FingerCurlIndex" 30 | } 31 | }, 32 | "mode": "trigger", 33 | "path": "/user/hand/left/input/finger/index" 34 | }, 35 | { 36 | "inputs": { 37 | "pull": { 38 | "output": "/actions/default/in/FingerCurlMiddle" 39 | } 40 | }, 41 | "mode": "trigger", 42 | "path": "/user/hand/left/input/finger/middle" 43 | }, 44 | { 45 | "inputs": { 46 | "pull": { 47 | "output": "/actions/default/in/FingerCurlRing" 48 | } 49 | }, 50 | "mode": "trigger", 51 | "path": "/user/hand/left/input/finger/ring" 52 | }, 53 | { 54 | "inputs": { 55 | "pull": { 56 | "output": "/actions/default/in/FingerCurlPinky" 57 | } 58 | }, 59 | "mode": "trigger", 60 | "path": "/user/hand/left/input/finger/pinky" 61 | } 62 | ] 63 | } 64 | }, 65 | "controller_type": "knuckles", 66 | "description": "My Test Bindings", 67 | "name": "OpenVRSandbox test bindings" 68 | } 69 | -------------------------------------------------------------------------------- /openvr_api.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StressLevelZero/XRDoctor/3b90a457e63806a2bc340ff5d6b587ba5f81fea8/openvr_api.dll -------------------------------------------------------------------------------- /openvr_api.dll.sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StressLevelZero/XRDoctor/3b90a457e63806a2bc340ff5d6b587ba5f81fea8/openvr_api.dll.sig -------------------------------------------------------------------------------- /openvr_api.pdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StressLevelZero/XRDoctor/3b90a457e63806a2bc340ff5d6b587ba5f81fea8/openvr_api.pdb -------------------------------------------------------------------------------- /openxr_loader.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StressLevelZero/XRDoctor/3b90a457e63806a2bc340ff5d6b587ba5f81fea8/openxr_loader.dll --------------------------------------------------------------------------------