├── .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
--------------------------------------------------------------------------------