mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 06:43:51 +02:00
Linux: SteamVR overlay support (#1299)
* fix: open folder and select item on linux * feat: linux wrist overlay * feat: linux hmd overlay * feat: replace unix sockets with shm on linux * fix: reduce linux wrist overlay fps * fix: hide electron offscreen windows * fix: destroy electron offscreen windows when not in use * fix: open folder and select item on linux * feat: cpu, uptime and device monitoring on linux * feat: native wayland gl context with x11 fallback on linux * fix: use platform agnostic wording for common folders * fix: crash dumps folder button on linux * fix: enable missing VR notification options on linux * fix: update cef, eslint config to include updated AppApiVr names * merge: rebase linux VR changes to upstream * Clean up * Load custom file contents rather than path Fixes loading custom file in debug mode * fix: call SetVR on linux as well * fix: AppApiVrElectron init, properly create and dispose of shm * Handle avatar history error * Lint * Change overlay dispose logic * macOS DOTNET_ROOT * Remove moving dotnet bin * Fix * fix: init overlay on SteamVR restart * Fix fetching empty instance, fix user dialog not fetching * Trim direct access inputs * Make icon higher res, because mac build would fail 😂 * macOS fixes * will it build? that's the question * fix: ensure offscreen windows are ready before vrinit * will it build? that's the question * will it build? that's the question * meow * one, more, time * Fix crash and overlay ellipsis * a --------- Co-authored-by: Natsumi <cmcooper123@hotmail.com>
This commit is contained in:
@@ -1,43 +1,41 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using CefSharp;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class AppApiVr
|
||||
public class AppApiVrCef : AppApiVr
|
||||
{
|
||||
public static readonly AppApiVr Instance;
|
||||
|
||||
static AppApiVr()
|
||||
static AppApiVrCef()
|
||||
{
|
||||
Instance = new AppApiVr();
|
||||
Instance = new AppApiVrCef();
|
||||
}
|
||||
|
||||
public void Init()
|
||||
public override void Init()
|
||||
{
|
||||
// Create Instance before Cef tries to bind it
|
||||
}
|
||||
|
||||
public void VrInit()
|
||||
public override void VrInit()
|
||||
{
|
||||
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.store.vr.vrInit", "");
|
||||
}
|
||||
|
||||
public void ToggleSystemMonitor(bool enabled)
|
||||
public override void ToggleSystemMonitor(bool enabled)
|
||||
{
|
||||
SystemMonitor.Instance.Start(enabled);
|
||||
SystemMonitorCef.Instance.Start(enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current CPU usage as a percentage.
|
||||
/// </summary>
|
||||
/// <returns>The current CPU usage as a percentage.</returns>
|
||||
public float CpuUsage()
|
||||
public override float CpuUsage()
|
||||
{
|
||||
return SystemMonitor.Instance.CpuUsage;
|
||||
return SystemMonitorCef.Instance.CpuUsage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -45,7 +43,7 @@ namespace VRCX
|
||||
/// Each sub-array contains the type of device and its current state
|
||||
/// </summary>
|
||||
/// <returns>An array of arrays containing information about the connected VR devices.</returns>
|
||||
public string[][] GetVRDevices()
|
||||
public override string[][] GetVRDevices()
|
||||
{
|
||||
return Program.VRCXVRInstance.GetDevices();
|
||||
}
|
||||
@@ -54,36 +52,42 @@ namespace VRCX
|
||||
/// Returns the number of milliseconds that the system has been running.
|
||||
/// </summary>
|
||||
/// <returns>The number of milliseconds that the system has been running.</returns>
|
||||
public double GetUptime()
|
||||
public override double GetUptime()
|
||||
{
|
||||
return SystemMonitor.Instance.UpTime;
|
||||
return SystemMonitorCef.Instance.UpTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current language of the operating system.
|
||||
/// </summary>
|
||||
/// <returns>The current language of the operating system.</returns>
|
||||
public string CurrentCulture()
|
||||
public override string CurrentCulture()
|
||||
{
|
||||
return CultureInfo.CurrentCulture.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file path of the custom user js file, if it exists.
|
||||
/// </summary>
|
||||
/// <returns>The file path of the custom user js file, or an empty string if it doesn't exist.</returns>
|
||||
public string CustomVrScriptPath()
|
||||
|
||||
public override string CustomVrScript()
|
||||
{
|
||||
var output = string.Empty;
|
||||
var filePath = Path.Join(Program.AppDataDirectory, "customvr.js");
|
||||
if (File.Exists(filePath))
|
||||
output = filePath;
|
||||
return output;
|
||||
return File.ReadAllText(filePath);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public bool IsRunningUnderWine()
|
||||
public override bool IsRunningUnderWine()
|
||||
{
|
||||
return Wine.GetIfWine();
|
||||
}
|
||||
|
||||
public override List<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue()
|
||||
{
|
||||
throw new NotImplementedException("GetExecuteVrFeedFunctionQueue is not implemented in AppApiVrCef.");
|
||||
}
|
||||
|
||||
public override List<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
|
||||
{
|
||||
throw new NotImplementedException("GetExecuteVrOverlayFunctionQueue is not implemented in AppApiVrCef.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,22 +96,22 @@ namespace VRCX
|
||||
});
|
||||
}
|
||||
|
||||
public string CustomCssPath()
|
||||
public string CustomCss()
|
||||
{
|
||||
var output = string.Empty;
|
||||
var filePath = Path.Join(Program.AppDataDirectory, "custom.css");
|
||||
if (File.Exists(filePath))
|
||||
output = filePath;
|
||||
return output;
|
||||
return File.ReadAllText(filePath);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public string CustomScriptPath()
|
||||
public string CustomScript()
|
||||
{
|
||||
var output = string.Empty;
|
||||
var filePath = Path.Join(Program.AppDataDirectory, "custom.js");
|
||||
if (File.Exists(filePath))
|
||||
output = filePath;
|
||||
return output;
|
||||
return File.ReadAllText(filePath);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public string CurrentCulture()
|
||||
|
||||
19
Dotnet/AppApi/Common/AppApiVrCommon.cs
Normal file
19
Dotnet/AppApi/Common/AppApiVrCommon.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace VRCX;
|
||||
|
||||
public abstract partial class AppApiVr
|
||||
{
|
||||
public static AppApiVr Instance;
|
||||
public abstract void Init();
|
||||
public abstract void VrInit();
|
||||
public abstract void ToggleSystemMonitor(bool enabled);
|
||||
public abstract float CpuUsage();
|
||||
public abstract string[][] GetVRDevices();
|
||||
public abstract double GetUptime();
|
||||
public abstract string CurrentCulture();
|
||||
public abstract string CustomVrScript();
|
||||
public abstract bool IsRunningUnderWine();
|
||||
public abstract List<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue();
|
||||
public abstract List<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue();
|
||||
}
|
||||
@@ -9,27 +9,30 @@ namespace VRCX
|
||||
public partial class AppApiElectron : AppApi
|
||||
{
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
|
||||
public override void ShowDevTools()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public override void SetVR(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand)
|
||||
{
|
||||
Program.VRCXVRInstance.SetActive(active, hmdOverlay, wristOverlay, menuButton, overlayHand);
|
||||
}
|
||||
|
||||
|
||||
public override void RefreshVR()
|
||||
{
|
||||
Program.VRCXVRInstance.Restart();
|
||||
}
|
||||
|
||||
public override void RestartVR()
|
||||
{
|
||||
Program.VRCXVRInstance.Restart();
|
||||
}
|
||||
|
||||
public override void SetZoom(double zoomLevel)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public override async Task<double> GetZoom()
|
||||
{
|
||||
return 1;
|
||||
@@ -47,23 +50,25 @@ namespace VRCX
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public override void ExecuteAppFunction(string function, string json)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ExecuteVrFeedFunction(string function, string json)
|
||||
{
|
||||
Program.VRCXVRInstance.ExecuteVrFeedFunction(function, json);
|
||||
}
|
||||
|
||||
public override void ExecuteVrOverlayFunction(string function, string json)
|
||||
{
|
||||
Program.VRCXVRInstance.ExecuteVrOverlayFunction(function, json);
|
||||
}
|
||||
|
||||
|
||||
public override void FocusWindow()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public override void ChangeTheme(int value)
|
||||
{
|
||||
}
|
||||
@@ -71,7 +76,7 @@ namespace VRCX
|
||||
public override void DoFunny()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public override string GetClipboard()
|
||||
{
|
||||
var process = new Process
|
||||
@@ -102,7 +107,7 @@ namespace VRCX
|
||||
public override void SetStartup(bool enabled)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public override void CopyImageToClipboard(string path)
|
||||
{
|
||||
if (!File.Exists(path) ||
|
||||
@@ -113,7 +118,7 @@ namespace VRCX
|
||||
!path.EndsWith(".bmp") &&
|
||||
!path.EndsWith(".webp")))
|
||||
return;
|
||||
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
@@ -124,7 +129,7 @@ namespace VRCX
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
try
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
process.WaitForExit();
|
||||
@@ -138,16 +143,16 @@ namespace VRCX
|
||||
public override void FlashWindow()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public override void SetUserAgent()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public override bool IsRunningUnderWine()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
102
Dotnet/AppApi/Electron/AppApiVrElectron.cs
Normal file
102
Dotnet/AppApi/Electron/AppApiVrElectron.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class AppApiVrElectron : AppApiVr
|
||||
{
|
||||
static AppApiVrElectron()
|
||||
{
|
||||
Instance = new AppApiVrElectron();
|
||||
}
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
}
|
||||
|
||||
public override void VrInit()
|
||||
{
|
||||
}
|
||||
|
||||
public override List<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue()
|
||||
{
|
||||
var list = new List<KeyValuePair<string, string>>();
|
||||
while (Program.VRCXVRInstance.GetExecuteVrFeedFunctionQueue().TryDequeue(out var item))
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public override List<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
|
||||
{
|
||||
var list = new List<KeyValuePair<string, string>>();
|
||||
while (Program.VRCXVRInstance.GetExecuteVrOverlayFunctionQueue().TryDequeue(out var item))
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public override void ToggleSystemMonitor(bool enabled)
|
||||
{
|
||||
SystemMonitorElectron.Instance.Start(enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current CPU usage as a percentage.
|
||||
/// </summary>
|
||||
/// <returns>The current CPU usage as a percentage.</returns>
|
||||
public override float CpuUsage()
|
||||
{
|
||||
return SystemMonitorElectron.Instance.CpuUsage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an array of arrays containing information about the connected VR devices.
|
||||
/// Each sub-array contains the type of device and its current state
|
||||
/// </summary>
|
||||
/// <returns>An array of arrays containing information about the connected VR devices.</returns>
|
||||
public override string[][] GetVRDevices()
|
||||
{
|
||||
return Program.VRCXVRInstance.GetDevices();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of milliseconds that the system has been running.
|
||||
/// </summary>
|
||||
/// <returns>The number of milliseconds that the system has been running.</returns>
|
||||
public override double GetUptime()
|
||||
{
|
||||
return SystemMonitorElectron.Instance.UpTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current language of the operating system.
|
||||
/// </summary>
|
||||
/// <returns>The current language of the operating system.</returns>
|
||||
public override string CurrentCulture()
|
||||
{
|
||||
return CultureInfo.CurrentCulture.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file path of the custom user js file, if it exists.
|
||||
/// </summary>
|
||||
/// <returns>The file path of the custom user js file, or an empty string if it doesn't exist.</returns>
|
||||
public override string CustomVrScript()
|
||||
{
|
||||
var filePath = Path.Join(Program.AppDataDirectory, "customvr.js");
|
||||
if (File.Exists(filePath))
|
||||
return File.ReadAllText(filePath);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public override bool IsRunningUnderWine()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,8 +45,7 @@ namespace VRCX
|
||||
),
|
||||
IsLocal = true
|
||||
});
|
||||
|
||||
// cefSettings.CefCommandLineArgs.Add("allow-universal-access-from-files");
|
||||
|
||||
// cefSettings.CefCommandLineArgs.Add("ignore-certificate-errors");
|
||||
// cefSettings.CefCommandLineArgs.Add("disable-plugins");
|
||||
cefSettings.CefCommandLineArgs.Add("disable-spell-checking");
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace VRCX
|
||||
repository.Register("Discord", Discord.Instance);
|
||||
repository.Register("AssetBundleManager", AssetBundleManager.Instance);
|
||||
}
|
||||
|
||||
|
||||
public static void ApplyVrJavascriptBindings(IJavascriptObjectRepository repository)
|
||||
{
|
||||
repository.NameConverter = null;
|
||||
|
||||
@@ -18,7 +18,7 @@ using CefSharp;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
internal class IPCClient
|
||||
public class IPCClient
|
||||
{
|
||||
private static readonly UTF8Encoding noBomEncoding = new UTF8Encoding(false, false);
|
||||
private readonly NamedPipeServerStream _ipcServer;
|
||||
|
||||
@@ -11,7 +11,7 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
internal class IPCServer
|
||||
public class IPCServer
|
||||
{
|
||||
public static readonly IPCServer Instance;
|
||||
public static readonly List<IPCClient> Clients = new List<IPCClient>();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,9 +11,9 @@ using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class SystemMonitor
|
||||
public class SystemMonitorCef
|
||||
{
|
||||
public static readonly SystemMonitor Instance;
|
||||
public static readonly SystemMonitorCef Instance;
|
||||
public float CpuUsage;
|
||||
public double UpTime;
|
||||
private bool _enabled;
|
||||
@@ -22,9 +22,9 @@ namespace VRCX
|
||||
private Thread _thread;
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
static SystemMonitor()
|
||||
static SystemMonitorCef()
|
||||
{
|
||||
Instance = new SystemMonitor();
|
||||
Instance = new SystemMonitorCef();
|
||||
}
|
||||
|
||||
public void Start(bool enabled)
|
||||
@@ -5,6 +5,7 @@
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -24,7 +25,7 @@ using Device4 = SharpDX.Direct3D11.Device4;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class VRCXVR : VRCXVRInterface
|
||||
public class VRCXVRCef : VRCXVRInterface
|
||||
{
|
||||
public static VRCXVRInterface Instance;
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
@@ -57,12 +58,12 @@ namespace VRCX
|
||||
private bool _wristOverlayWasActive;
|
||||
|
||||
|
||||
static VRCXVR()
|
||||
static VRCXVRCef()
|
||||
{
|
||||
Instance = new VRCXVR();
|
||||
Instance = new VRCXVRCef();
|
||||
}
|
||||
|
||||
public VRCXVR()
|
||||
public VRCXVRCef()
|
||||
{
|
||||
_deviceListLock = new ReaderWriterLockSlim();
|
||||
_deviceList = new List<string[]>();
|
||||
@@ -90,7 +91,7 @@ namespace VRCX
|
||||
public override void Restart()
|
||||
{
|
||||
Exit();
|
||||
Instance = new VRCXVR();
|
||||
Instance = new VRCXVRCef();
|
||||
Instance.Init();
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("console.log('VRCXVR Restarted');");
|
||||
}
|
||||
@@ -834,6 +835,11 @@ namespace VRCX
|
||||
return err;
|
||||
}
|
||||
|
||||
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void ExecuteVrFeedFunction(string function, string json)
|
||||
{
|
||||
if (_wristOverlay == null) return;
|
||||
@@ -842,6 +848,11 @@ namespace VRCX
|
||||
_wristOverlay.ExecuteScriptAsync($"$app.{function}", json);
|
||||
}
|
||||
|
||||
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void ExecuteVrOverlayFunction(string function, string json)
|
||||
{
|
||||
if (_hmdOverlay == null) return;
|
||||
@@ -5,6 +5,7 @@
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -761,6 +762,11 @@ namespace VRCX
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void ExecuteVrFeedFunction(string function, string json)
|
||||
{
|
||||
@@ -769,6 +775,11 @@ namespace VRCX
|
||||
Restart();
|
||||
_wristOverlay.ExecuteScriptAsync($"$app.{function}", json);
|
||||
}
|
||||
|
||||
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void ExecuteVrOverlayFunction(string function, string json)
|
||||
{
|
||||
204
Dotnet/Overlay/Electron/GLContextWayland.cs
Normal file
204
Dotnet/Overlay/Electron/GLContextWayland.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
public static class GLContextWayland
|
||||
{
|
||||
// Wayland
|
||||
[DllImport("libwayland-client.so.0")]
|
||||
private static extern IntPtr wl_display_connect(string name);
|
||||
|
||||
[DllImport("libwayland-client.so.0")]
|
||||
private static extern void wl_display_disconnect(IntPtr display);
|
||||
|
||||
[DllImport("libwayland-client.so.0")]
|
||||
private static extern int wl_display_roundtrip(IntPtr display);
|
||||
|
||||
// EGL
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern IntPtr eglGetDisplay(IntPtr display_id);
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern bool eglInitialize(IntPtr display, out int major, out int minor);
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern bool eglTerminate(IntPtr display);
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern bool eglChooseConfig(IntPtr display, int[] attrib_list, IntPtr[] configs, int config_size, out int num_config);
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern IntPtr eglCreatePbufferSurface(IntPtr display, IntPtr config, int[] attrib_list);
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern IntPtr eglCreateContext(IntPtr display, IntPtr config, IntPtr share_context, int[] attrib_list);
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern bool eglMakeCurrent(IntPtr display, IntPtr draw, IntPtr read, IntPtr context);
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern bool eglDestroySurface(IntPtr display, IntPtr surface);
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern bool eglDestroyContext(IntPtr display, IntPtr context);
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern int eglGetError();
|
||||
|
||||
[DllImport("libEGL.so.1")]
|
||||
private static extern IntPtr eglQueryString(IntPtr display, int name);
|
||||
|
||||
[DllImport("libGL.so.1")]
|
||||
private static extern IntPtr glGetString(int name);
|
||||
|
||||
// EGL constants
|
||||
private const int EGL_SURFACE_TYPE = 0x3033;
|
||||
private const int EGL_PBUFFER_BIT = 0x0001;
|
||||
private const int EGL_RENDERABLE_TYPE = 0x3040;
|
||||
private const int EGL_OPENGL_BIT = 0x0008;
|
||||
private const int EGL_OPENGL_ES2_BIT = 0x0004;
|
||||
private const int EGL_RED_SIZE = 0x3024;
|
||||
private const int EGL_GREEN_SIZE = 0x3023;
|
||||
private const int EGL_BLUE_SIZE = 0x3022;
|
||||
private const int EGL_ALPHA_SIZE = 0x3021;
|
||||
private const int EGL_DEPTH_SIZE = 0x3025;
|
||||
private const int EGL_NONE = 0x3038;
|
||||
private const int EGL_WIDTH = 0x3057;
|
||||
private const int EGL_HEIGHT = 0x3056;
|
||||
private const int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
|
||||
private const int EGL_VENDOR = 0x3053;
|
||||
private const int EGL_VERSION = 0x3054;
|
||||
private const int EGL_EXTENSIONS = 0x3055;
|
||||
|
||||
// OpenGL constants
|
||||
private const int GL_VENDOR = 0x1F00;
|
||||
private const int GL_RENDERER = 0x1F01;
|
||||
private const int GL_VERSION = 0x1F02;
|
||||
|
||||
private static IntPtr waylandDisplay = IntPtr.Zero;
|
||||
private static IntPtr eglDisplay = IntPtr.Zero;
|
||||
private static IntPtr eglContext = IntPtr.Zero;
|
||||
private static IntPtr eglSurface = IntPtr.Zero;
|
||||
|
||||
public static bool Initialise()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Connect to Wayland display
|
||||
waylandDisplay = wl_display_connect(null);
|
||||
if (waylandDisplay == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("Failed to connect to Wayland display");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Perform initial roundtrip to ensure connection is established
|
||||
wl_display_roundtrip(waylandDisplay);
|
||||
|
||||
// Get EGL display
|
||||
eglDisplay = eglGetDisplay(waylandDisplay);
|
||||
if (eglDisplay == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("Failed to get EGL display");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize EGL
|
||||
if (!eglInitialize(eglDisplay, out int major, out int minor))
|
||||
{
|
||||
Console.WriteLine($"Failed to initialize EGL. Error: 0x{eglGetError():X}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.WriteLine($"EGL version: {major}.{minor}");
|
||||
|
||||
// Choose EGL config for offscreen rendering
|
||||
int[] configAttribs = {
|
||||
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
|
||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
|
||||
EGL_RED_SIZE, 8,
|
||||
EGL_GREEN_SIZE, 8,
|
||||
EGL_BLUE_SIZE, 8,
|
||||
EGL_ALPHA_SIZE, 8,
|
||||
EGL_DEPTH_SIZE, 24,
|
||||
EGL_NONE
|
||||
};
|
||||
|
||||
IntPtr[] configs = new IntPtr[10];
|
||||
if (!eglChooseConfig(eglDisplay, configAttribs, configs, 10, out int numConfigs) || numConfigs == 0)
|
||||
{
|
||||
Console.WriteLine($"Failed to choose EGL config. Error: 0x{eglGetError():X}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Found {numConfigs} EGL configs");
|
||||
IntPtr config = configs[0];
|
||||
|
||||
// Create a minimal pbuffer surface (offscreen)
|
||||
int[] pbufferAttribs = {
|
||||
EGL_WIDTH, 1,
|
||||
EGL_HEIGHT, 1,
|
||||
EGL_NONE
|
||||
};
|
||||
|
||||
eglSurface = eglCreatePbufferSurface(eglDisplay, config, pbufferAttribs);
|
||||
if (eglSurface == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine($"Failed to create EGL pbuffer surface. Error: 0x{eglGetError():X}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create OpenGL context
|
||||
int[] contextAttribs = {
|
||||
EGL_NONE
|
||||
};
|
||||
|
||||
eglContext = eglCreateContext(eglDisplay, config, IntPtr.Zero, contextAttribs);
|
||||
if (eglContext == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine($"Failed to create EGL context. Error: 0x{eglGetError():X}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make context current
|
||||
if (!eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext))
|
||||
{
|
||||
Console.WriteLine($"Failed to make EGL context current. Error: 0x{eglGetError():X}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.WriteLine("OpenGL context created successfully (Wayland/EGL offscreen)");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to initialize OpenGL context: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Cleanup()
|
||||
{
|
||||
if (eglDisplay != IntPtr.Zero)
|
||||
{
|
||||
if (eglContext != IntPtr.Zero)
|
||||
{
|
||||
eglMakeCurrent(eglDisplay, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
|
||||
eglDestroyContext(eglDisplay, eglContext);
|
||||
}
|
||||
if (eglSurface != IntPtr.Zero)
|
||||
eglDestroySurface(eglDisplay, eglSurface);
|
||||
|
||||
eglTerminate(eglDisplay);
|
||||
}
|
||||
|
||||
if (waylandDisplay != IntPtr.Zero)
|
||||
{
|
||||
wl_display_disconnect(waylandDisplay);
|
||||
}
|
||||
|
||||
waylandDisplay = IntPtr.Zero;
|
||||
eglDisplay = IntPtr.Zero;
|
||||
eglContext = IntPtr.Zero;
|
||||
eglSurface = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
170
Dotnet/Overlay/Electron/GLContextX11.cs
Normal file
170
Dotnet/Overlay/Electron/GLContextX11.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
public static class GLContextX11
|
||||
{
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern IntPtr XOpenDisplay(IntPtr display);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XDefaultScreen(IntPtr display);
|
||||
|
||||
[DllImport("libGL.so.1")]
|
||||
private static extern IntPtr glXChooseVisual(IntPtr display, int screen, int[] attribList);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern IntPtr XCreateSimpleWindow(IntPtr display, IntPtr parent, int x, int y, uint width, uint height, uint border_width, ulong border, ulong background);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XMapWindow(IntPtr display, IntPtr window);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XUnmapWindow(IntPtr display, IntPtr window);
|
||||
|
||||
[DllImport("libGL.so.1")]
|
||||
private static extern IntPtr glXCreateContext(IntPtr display, IntPtr visual, IntPtr shareList, bool direct);
|
||||
|
||||
[DllImport("libGL.so.1")]
|
||||
private static extern bool glXMakeCurrent(IntPtr display, IntPtr drawable, IntPtr context);
|
||||
|
||||
[DllImport("libGL.so.1")]
|
||||
private static extern void glXDestroyContext(IntPtr display, IntPtr context);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XDestroyWindow(IntPtr display, IntPtr window);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XCloseDisplay(IntPtr display);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XFlush(IntPtr display);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern IntPtr XRootWindow(IntPtr display, int screen);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XStoreName(IntPtr display, IntPtr window, string window_name);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XChangeWindowAttributes(IntPtr display, IntPtr window, ulong valuemask, ref XSetWindowAttributes attributes);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XMoveWindow(IntPtr display, IntPtr window, int x, int y);
|
||||
|
||||
[DllImport("libX11.so.6")]
|
||||
private static extern int XResizeWindow(IntPtr display, IntPtr window, uint width, uint height);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct XSetWindowAttributes
|
||||
{
|
||||
public IntPtr background_pixmap;
|
||||
public ulong background_pixel;
|
||||
public IntPtr border_pixmap;
|
||||
public ulong border_pixel;
|
||||
public int bit_gravity;
|
||||
public int win_gravity;
|
||||
public int backing_store;
|
||||
public ulong backing_planes;
|
||||
public ulong backing_pixel;
|
||||
public bool save_under;
|
||||
public long event_mask;
|
||||
public long do_not_propagate_mask;
|
||||
public bool override_redirect;
|
||||
public IntPtr colormap;
|
||||
public IntPtr cursor;
|
||||
}
|
||||
|
||||
private const ulong CWOverrideRedirect = 0x00000200;
|
||||
private const ulong CWBackPixel = 0x00000002;
|
||||
private const ulong CWBorderPixel = 0x00000008;
|
||||
|
||||
private static IntPtr display = IntPtr.Zero;
|
||||
private static IntPtr window = IntPtr.Zero;
|
||||
private static IntPtr context = IntPtr.Zero;
|
||||
private static bool windowVisible = false;
|
||||
|
||||
public static bool Initialise()
|
||||
{
|
||||
try
|
||||
{
|
||||
display = XOpenDisplay(IntPtr.Zero);
|
||||
if (display == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("No X11 display available");
|
||||
return false;
|
||||
}
|
||||
|
||||
int screen = XDefaultScreen(display);
|
||||
|
||||
// Request a visual that supports OpenGL
|
||||
int[] attribs = { 4, 1, 5, 1, 12, 1, 0 }; // GLX_RGBA, GLX_DOUBLEBUFFER, GLX_DEPTH_SIZE, 1, None
|
||||
IntPtr visual = glXChooseVisual(display, screen, attribs);
|
||||
if (visual == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("Failed to find a GLX visual");
|
||||
return false;
|
||||
}
|
||||
|
||||
IntPtr root = XRootWindow(display, screen);
|
||||
|
||||
// Create window off-screen and very small to minimize visibility
|
||||
window = XCreateSimpleWindow(display, root, -10000, -10000, 1, 1, 0, 0, 0);
|
||||
if (window == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("Failed to create X11 window");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set window attributes to make it invisible
|
||||
XSetWindowAttributes attrs = new XSetWindowAttributes();
|
||||
attrs.override_redirect = true; // Bypass window manager
|
||||
attrs.background_pixel = 0;
|
||||
attrs.border_pixel = 0;
|
||||
|
||||
XChangeWindowAttributes(display, window, CWOverrideRedirect | CWBackPixel | CWBorderPixel, ref attrs);
|
||||
|
||||
// Give it a name that indicates it's a hidden OpenGL context
|
||||
XStoreName(display, window, "Hidden OpenGL Context");
|
||||
|
||||
context = glXCreateContext(display, visual, IntPtr.Zero, true);
|
||||
if (context == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("Failed to create GLX context");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!glXMakeCurrent(display, window, context))
|
||||
{
|
||||
Console.WriteLine("Failed to make GLX context current");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't map the window at all - keep it completely hidden
|
||||
// The GLX context will work without the window being visible
|
||||
XFlush(display);
|
||||
|
||||
Console.WriteLine("OpenGL context created successfully (X11/GLX)");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to initialize OpenGL context: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Cleanup()
|
||||
{
|
||||
if (display != IntPtr.Zero)
|
||||
{
|
||||
if (context != IntPtr.Zero)
|
||||
glXDestroyContext(display, context);
|
||||
if (window != IntPtr.Zero)
|
||||
XDestroyWindow(display, window);
|
||||
XCloseDisplay(display);
|
||||
display = IntPtr.Zero;
|
||||
window = IntPtr.Zero;
|
||||
context = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
106
Dotnet/Overlay/Electron/GLTextureWriter.cs
Normal file
106
Dotnet/Overlay/Electron/GLTextureWriter.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Valve.VR;
|
||||
|
||||
public class GLTextureWriter : IDisposable
|
||||
{
|
||||
private uint _textureId;
|
||||
private readonly int _width, _height;
|
||||
private byte[] _buffer;
|
||||
|
||||
// OpenGL P/Invoke declarations
|
||||
[DllImport("libGL.so.1", EntryPoint = "glGenTextures")]
|
||||
private static extern void glGenTextures(int n, out uint textures);
|
||||
|
||||
[DllImport("libGL.so.1", EntryPoint = "glBindTexture")]
|
||||
private static extern void glBindTexture(uint target, uint texture);
|
||||
|
||||
[DllImport("libGL.so.1", EntryPoint = "glTexParameteri")]
|
||||
private static extern void glTexParameteri(uint target, uint pname, int param);
|
||||
|
||||
[DllImport("libGL.so.1", EntryPoint = "glTexImage2D")]
|
||||
private static extern void glTexImage2D(uint target, int level, int internalformat, int width, int height, int border, uint format, uint type, IntPtr pixels);
|
||||
|
||||
[DllImport("libGL.so.1", EntryPoint = "glTexSubImage2D")]
|
||||
private static extern void glTexSubImage2D(uint target, int level, int xoffset, int yoffset, int width, int height, uint format, uint type, byte[] pixels);
|
||||
|
||||
[DllImport("libGL.so.1", EntryPoint = "glDeleteTextures")]
|
||||
private static extern void glDeleteTextures(int n, ref uint textures);
|
||||
|
||||
// OpenGL constants
|
||||
private const uint GL_TEXTURE_2D = 0x0DE1;
|
||||
private const uint GL_TEXTURE_MIN_FILTER = 0x2801;
|
||||
private const uint GL_TEXTURE_MAG_FILTER = 0x2800;
|
||||
private const uint GL_TEXTURE_WRAP_S = 0x2802;
|
||||
private const uint GL_TEXTURE_WRAP_T = 0x2803;
|
||||
private const uint GL_LINEAR = 0x2601;
|
||||
private const uint GL_CLAMP_TO_EDGE = 0x812F;
|
||||
private const uint GL_RGBA = 0x1908;
|
||||
private const uint GL_BGRA = 0x80E1;
|
||||
private const uint GL_UNSIGNED_BYTE = 0x1401;
|
||||
|
||||
public GLTextureWriter(int width, int height)
|
||||
{
|
||||
_width = width;
|
||||
_height = height;
|
||||
_buffer = new byte[width * height * 4]; // 4 bytes per pixel (RGBA)
|
||||
InitTexture();
|
||||
}
|
||||
|
||||
private void InitTexture()
|
||||
{
|
||||
glGenTextures(1, out _textureId);
|
||||
glBindTexture(GL_TEXTURE_2D, _textureId);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, (int)GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, (int)GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, (int)GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, (int)GL_CLAMP_TO_EDGE);
|
||||
|
||||
// Allocate texture storage with no initial data.
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, (int)GL_RGBA, _width, _height, 0,
|
||||
GL_BGRA, GL_UNSIGNED_BYTE, IntPtr.Zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies image data into the internal buffer.
|
||||
/// </summary>
|
||||
public void WriteImageToBuffer(byte[] data)
|
||||
{
|
||||
if (data.Length != _buffer.Length)
|
||||
throw new ArgumentException("Data size does not match texture size.");
|
||||
|
||||
System.Buffer.BlockCopy(data, 0, _buffer, 0, _buffer.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the OpenGL texture with the current buffer.
|
||||
/// </summary>
|
||||
public void UpdateTexture()
|
||||
{
|
||||
glBindTexture(GL_TEXTURE_2D, _textureId);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, _width, _height,
|
||||
GL_BGRA, GL_UNSIGNED_BYTE, _buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a Texture_t structure for use with OpenVR.
|
||||
/// </summary>
|
||||
public Texture_t AsTextureT()
|
||||
{
|
||||
return new Texture_t
|
||||
{
|
||||
handle = (IntPtr)_textureId,
|
||||
eType = ETextureType.OpenGL,
|
||||
eColorSpace = EColorSpace.Auto
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_textureId != 0)
|
||||
{
|
||||
glDeleteTextures(1, ref _textureId);
|
||||
_textureId = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
214
Dotnet/Overlay/Electron/SystemMonitorElectron.cs
Normal file
214
Dotnet/Overlay/Electron/SystemMonitorElectron.cs
Normal file
@@ -0,0 +1,214 @@
|
||||
// Copyright(c) 2019-2025 pypy, Natsumi and individual contributors.
|
||||
// All rights reserved.
|
||||
//
|
||||
// This work is licensed under the terms of the MIT license.
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class SystemMonitorElectron
|
||||
{
|
||||
public static readonly SystemMonitorElectron Instance;
|
||||
public float CpuUsage;
|
||||
public double UpTime;
|
||||
private bool _enabled;
|
||||
private long _lastTotalTime;
|
||||
private long _lastIdleTime;
|
||||
private bool _firstReading = true; // Add this flag
|
||||
private Thread _thread;
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
static SystemMonitorElectron()
|
||||
{
|
||||
Instance = new SystemMonitorElectron();
|
||||
}
|
||||
|
||||
public void Init()
|
||||
{
|
||||
}
|
||||
|
||||
public void Start(bool enabled)
|
||||
{
|
||||
if (enabled == _enabled)
|
||||
return;
|
||||
|
||||
_enabled = enabled;
|
||||
if (enabled)
|
||||
StartThread();
|
||||
else
|
||||
Exit();
|
||||
}
|
||||
|
||||
internal void Exit()
|
||||
{
|
||||
CpuUsage = 0;
|
||||
UpTime = 0;
|
||||
_firstReading = true; // Reset flag
|
||||
try
|
||||
{
|
||||
if (_thread != null)
|
||||
{
|
||||
_thread.Interrupt();
|
||||
_thread.Join();
|
||||
_thread = null;
|
||||
}
|
||||
}
|
||||
catch (ThreadInterruptedException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void StartThread()
|
||||
{
|
||||
Exit();
|
||||
|
||||
try
|
||||
{
|
||||
ReadCpuInfo();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"Failed to initialize CPU monitoring: {ex}");
|
||||
return;
|
||||
}
|
||||
|
||||
_thread = new Thread(ThreadProc)
|
||||
{
|
||||
IsBackground = true
|
||||
};
|
||||
_thread.Start();
|
||||
}
|
||||
|
||||
private void ThreadProc()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (_enabled)
|
||||
{
|
||||
UpdateMetrics();
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warn($"SystemMonitor thread exception: {ex}");
|
||||
}
|
||||
|
||||
Exit();
|
||||
}
|
||||
|
||||
private void UpdateMetrics()
|
||||
{
|
||||
try
|
||||
{
|
||||
CpuUsage = GetCpuUsage();
|
||||
UpTime = GetUptime();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warn($"Error updating metrics: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private float GetCpuUsage()
|
||||
{
|
||||
try
|
||||
{
|
||||
var cpuInfo = ReadCpuInfo();
|
||||
if (cpuInfo == null) return 0;
|
||||
|
||||
var totalTime = cpuInfo.Value.TotalTime;
|
||||
var idleTime = cpuInfo.Value.IdleTime;
|
||||
|
||||
// Skip the first reading to establish baseline
|
||||
if (_firstReading)
|
||||
{
|
||||
_lastTotalTime = totalTime;
|
||||
_lastIdleTime = idleTime;
|
||||
_firstReading = false;
|
||||
return 0; // Return 0 for first reading
|
||||
}
|
||||
|
||||
var totalTimeDiff = totalTime - _lastTotalTime;
|
||||
var idleTimeDiff = idleTime - _lastIdleTime;
|
||||
|
||||
if (totalTimeDiff > 0)
|
||||
{
|
||||
var cpuUsage = 100.0f * (1.0f - (float)idleTimeDiff / totalTimeDiff);
|
||||
_lastTotalTime = totalTime;
|
||||
_lastIdleTime = idleTime;
|
||||
|
||||
// Clamp to reasonable range
|
||||
return Math.Max(0, Math.Min(100, cpuUsage));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warn($"Error reading CPU usage: {ex}");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private double GetUptime()
|
||||
{
|
||||
try
|
||||
{
|
||||
var uptimeContent = File.ReadAllText("/proc/uptime");
|
||||
var parts = uptimeContent.Split(' ');
|
||||
if (parts.Length > 0 && double.TryParse(parts[0], out var uptimeSeconds))
|
||||
{
|
||||
return TimeSpan.FromSeconds(uptimeSeconds).TotalMilliseconds;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warn($"Error reading uptime: {ex}");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private (long TotalTime, long IdleTime)? ReadCpuInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
var statContent = File.ReadAllText("/proc/stat");
|
||||
var lines = statContent.Split('\n');
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.StartsWith("cpu "))
|
||||
{
|
||||
var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length >= 5)
|
||||
{
|
||||
// CPU time values: user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice
|
||||
var user = long.Parse(parts[1]);
|
||||
var nice = long.Parse(parts[2]);
|
||||
var system = long.Parse(parts[3]);
|
||||
var idle = long.Parse(parts[4]);
|
||||
var iowait = parts.Length > 5 ? long.Parse(parts[5]) : 0;
|
||||
var irq = parts.Length > 6 ? long.Parse(parts[6]) : 0;
|
||||
var softirq = parts.Length > 7 ? long.Parse(parts[7]) : 0;
|
||||
var steal = parts.Length > 8 ? long.Parse(parts[8]) : 0;
|
||||
|
||||
var totalTime = user + nice + system + idle + iowait + irq + softirq + steal;
|
||||
return (totalTime, idle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warn($"Error reading /proc/stat: {ex}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
921
Dotnet/Overlay/Electron/VRCXVRElectron.cs
Normal file
921
Dotnet/Overlay/Electron/VRCXVRElectron.cs
Normal file
@@ -0,0 +1,921 @@
|
||||
// Copyright(c) 2019-2025 pypy, Natsumi and individual contributors.
|
||||
// All rights reserved.
|
||||
//
|
||||
// This work is licensed under the terms of the MIT license.
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using NLog;
|
||||
using Valve.VR;
|
||||
using System.Numerics;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class VRCXVRElectron : VRCXVRInterface
|
||||
{
|
||||
public static VRCXVRInterface Instance;
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly float[] _rotation = { 0f, 0f, 0f };
|
||||
private static readonly float[] _translation = { 0f, 0f, 0f };
|
||||
private static readonly float[] _translationLeft = { -7f / 100f, -5f / 100f, 6f / 100f };
|
||||
private static readonly float[] _translationRight = { 7f / 100f, -5f / 100f, 6f / 100f };
|
||||
private static readonly float[] _rotationLeft = { 90f * (float)(Math.PI / 180f), 90f * (float)(Math.PI / 180f), -90f * (float)(Math.PI / 180f) };
|
||||
private static readonly float[] _rotationRight = { -90f * (float)(Math.PI / 180f), -90f * (float)(Math.PI / 180f), -90f * (float)(Math.PI / 180f) };
|
||||
private readonly List<string[]> _deviceList;
|
||||
private readonly ReaderWriterLockSlim _deviceListLock;
|
||||
private bool _active;
|
||||
private bool _menuButton;
|
||||
private int _overlayHand;
|
||||
private GLTextureWriter _wristOverlayTextureWriter;
|
||||
private GLTextureWriter _hmdOverlayTextureWriter;
|
||||
private Thread _thread;
|
||||
private DateTime _nextOverlayUpdate;
|
||||
|
||||
private ulong _hmdOverlayHandle;
|
||||
private bool _hmdOverlayActive;
|
||||
private bool _hmdOverlayWasActive;
|
||||
|
||||
private ulong _wristOverlayHandle;
|
||||
private bool _wristOverlayActive;
|
||||
private bool _wristOverlayWasActive;
|
||||
|
||||
private const string WRIST_OVERLAY_SHM_PATH = "/dev/shm/vrcx_wrist_overlay";
|
||||
private const string HMD_OVERLAY_SHM_PATH = "/dev/shm/vrcx_hmd_overlay";
|
||||
private const int WRIST_FRAME_WIDTH = 512;
|
||||
private const int WRIST_FRAME_HEIGHT = 512;
|
||||
private const int WRIST_FRAME_SIZE = WRIST_FRAME_WIDTH * WRIST_FRAME_HEIGHT * 4; // RGBA
|
||||
private byte[] wristFrameBuffer = new byte[WRIST_FRAME_SIZE];
|
||||
private const int HMD_FRAME_WIDTH = 1024;
|
||||
private const int HMD_FRAME_HEIGHT = 1024;
|
||||
private const int HMD_FRAME_SIZE = HMD_FRAME_WIDTH * HMD_FRAME_HEIGHT * 4; // RGBA
|
||||
private byte[] hmdFrameBuffer = new byte[HMD_FRAME_SIZE];
|
||||
private MemoryMappedFile _wristOverlayMMF;
|
||||
private MemoryMappedViewAccessor _wristOverlayAccessor;
|
||||
private MemoryMappedFile _hmdOverlayMMF;
|
||||
private MemoryMappedViewAccessor _hmdOverlayAccessor;
|
||||
private readonly ConcurrentQueue<KeyValuePair<string, string>> _wristFeedFunctionQueue = new ConcurrentQueue<KeyValuePair<string, string>>();
|
||||
private readonly ConcurrentQueue<KeyValuePair<string, string>> _hmdFeedFunctionQueue = new ConcurrentQueue<KeyValuePair<string, string>>();
|
||||
|
||||
static VRCXVRElectron()
|
||||
{
|
||||
Instance = new VRCXVRElectron();
|
||||
}
|
||||
|
||||
public VRCXVRElectron()
|
||||
{
|
||||
_deviceListLock = new ReaderWriterLockSlim();
|
||||
_deviceList = new List<string[]>();
|
||||
_thread = new Thread(ThreadLoop)
|
||||
{
|
||||
IsBackground = true
|
||||
};
|
||||
}
|
||||
|
||||
// NOTE
|
||||
// 메모리 릭 때문에 미리 생성해놓고 계속 사용함
|
||||
public override void Init()
|
||||
{
|
||||
_thread.Start();
|
||||
}
|
||||
|
||||
public override void Exit()
|
||||
{
|
||||
var thread = _thread;
|
||||
_thread = null;
|
||||
thread?.Interrupt();
|
||||
thread?.Join();
|
||||
|
||||
_wristOverlayAccessor?.Dispose();
|
||||
_wristOverlayAccessor = null;
|
||||
_wristOverlayMMF?.Dispose();
|
||||
_wristOverlayMMF = null;
|
||||
|
||||
_hmdOverlayAccessor?.Dispose();
|
||||
_hmdOverlayAccessor = null;
|
||||
_hmdOverlayMMF?.Dispose();
|
||||
_hmdOverlayMMF = null;
|
||||
|
||||
GLContextX11.Cleanup();
|
||||
GLContextWayland.Cleanup();
|
||||
}
|
||||
|
||||
public override void Restart()
|
||||
{
|
||||
Exit();
|
||||
Instance = new VRCXVRElectron();
|
||||
Instance.Init();
|
||||
//MainForm.Instance.Browser.ExecuteScriptAsync("console.log('VRCXVR Restarted');");
|
||||
}
|
||||
|
||||
private void SetupTextures()
|
||||
{
|
||||
bool contextInitialised = false;
|
||||
|
||||
// Check if we're running on Wayland
|
||||
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY")) ||
|
||||
Environment.GetEnvironmentVariable("XDG_SESSION_TYPE")?.ToLower() == "wayland")
|
||||
{
|
||||
contextInitialised = GLContextWayland.Initialise();
|
||||
}
|
||||
else
|
||||
{
|
||||
contextInitialised = GLContextX11.Initialise();
|
||||
}
|
||||
|
||||
if (!contextInitialised)
|
||||
{
|
||||
contextInitialised = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY")) ?
|
||||
GLContextX11.Initialise() : GLContextWayland.Initialise();
|
||||
}
|
||||
|
||||
if (!contextInitialised)
|
||||
{
|
||||
throw new Exception("Failed to initialise OpenGL context");
|
||||
}
|
||||
|
||||
_wristOverlayTextureWriter = new GLTextureWriter(512, 512);
|
||||
_wristOverlayTextureWriter.UpdateTexture();
|
||||
|
||||
_hmdOverlayTextureWriter = new GLTextureWriter(1024, 1024);
|
||||
_hmdOverlayTextureWriter.UpdateTexture();
|
||||
}
|
||||
|
||||
private void UpgradeDevice()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public byte[] GetLatestWristOverlayFrame()
|
||||
{
|
||||
if (_wristOverlayAccessor == null) return null;
|
||||
byte ready = _wristOverlayAccessor.ReadByte(0);
|
||||
if (ready == 1)
|
||||
{
|
||||
_wristOverlayAccessor.ReadArray(1, wristFrameBuffer, 0, WRIST_FRAME_SIZE);
|
||||
_wristOverlayAccessor.Write(0, (byte)0); // reset flag
|
||||
return wristFrameBuffer;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public byte[] GetLatestHmdOverlayFrame()
|
||||
{
|
||||
if (_hmdOverlayAccessor == null) return null;
|
||||
byte ready = _hmdOverlayAccessor.ReadByte(0);
|
||||
if (ready == 1)
|
||||
{
|
||||
_hmdOverlayAccessor.ReadArray(1, hmdFrameBuffer, 0, HMD_FRAME_SIZE);
|
||||
_hmdOverlayAccessor.Write(0, (byte)0); // reset flag
|
||||
return hmdFrameBuffer;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void FlipImageVertically(byte[] imageData, int width, int height)
|
||||
{
|
||||
int stride = width * 4; // 4 bytes per pixel (RGBA)
|
||||
byte[] tempRow = new byte[stride];
|
||||
|
||||
for (int y = 0; y < height / 2; y++)
|
||||
{
|
||||
int topIndex = y * stride;
|
||||
int bottomIndex = (height - 1 - y) * stride;
|
||||
|
||||
// Swap rows
|
||||
Buffer.BlockCopy(imageData, topIndex, tempRow, 0, stride);
|
||||
Buffer.BlockCopy(imageData, bottomIndex, imageData, topIndex, stride);
|
||||
Buffer.BlockCopy(tempRow, 0, imageData, bottomIndex, stride);
|
||||
}
|
||||
}
|
||||
|
||||
private void ThreadLoop()
|
||||
{
|
||||
var active = false;
|
||||
var e = new VREvent_t();
|
||||
var nextInit = DateTime.MinValue;
|
||||
var nextDeviceUpdate = DateTime.MinValue;
|
||||
_nextOverlayUpdate = DateTime.MinValue;
|
||||
var overlayIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
|
||||
var overlayVisible1 = false;
|
||||
var overlayVisible2 = false;
|
||||
var dashboardHandle = 0UL;
|
||||
|
||||
while (_thread != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_active)
|
||||
Thread.Sleep(1);
|
||||
else
|
||||
Thread.Sleep(32);
|
||||
}
|
||||
catch (ThreadInterruptedException)
|
||||
{
|
||||
}
|
||||
|
||||
if (_active)
|
||||
{
|
||||
var system = OpenVR.System;
|
||||
if (system == null)
|
||||
{
|
||||
if (DateTime.UtcNow.CompareTo(nextInit) <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var _err = EVRInitError.None;
|
||||
system = OpenVR.Init(ref _err, EVRApplicationType.VRApplication_Background);
|
||||
nextInit = DateTime.UtcNow.AddSeconds(5);
|
||||
if (system == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
active = true;
|
||||
SetupTextures();
|
||||
}
|
||||
|
||||
while (system.PollNextEvent(ref e, (uint)Marshal.SizeOf(e)))
|
||||
{
|
||||
var type = (EVREventType)e.eventType;
|
||||
if (type == EVREventType.VREvent_Quit)
|
||||
{
|
||||
active = false;
|
||||
IsHmdAfk = false;
|
||||
OpenVR.Shutdown();
|
||||
nextInit = DateTime.UtcNow.AddSeconds(10);
|
||||
system = null;
|
||||
|
||||
_wristOverlayHandle = 0;
|
||||
_hmdOverlayHandle = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (system != null)
|
||||
{
|
||||
if (DateTime.UtcNow.CompareTo(nextDeviceUpdate) >= 0)
|
||||
{
|
||||
overlayIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
|
||||
UpdateDevices(system, ref overlayIndex);
|
||||
if (overlayIndex != OpenVR.k_unTrackedDeviceIndexInvalid)
|
||||
{
|
||||
_nextOverlayUpdate = DateTime.UtcNow.AddSeconds(10);
|
||||
}
|
||||
|
||||
nextDeviceUpdate = DateTime.UtcNow.AddSeconds(0.1);
|
||||
}
|
||||
|
||||
var overlay = OpenVR.Overlay;
|
||||
if (overlay != null)
|
||||
{
|
||||
var dashboardVisible = overlay.IsDashboardVisible();
|
||||
//var err = ProcessDashboard(overlay, ref dashboardHandle, dashboardVisible);
|
||||
//if (err != EVROverlayError.None &&
|
||||
// dashboardHandle != 0)
|
||||
//{
|
||||
// overlay.DestroyOverlay(dashboardHandle);
|
||||
// dashboardHandle = 0;
|
||||
// logger.Error(err);
|
||||
//}
|
||||
|
||||
if (_wristOverlayActive)
|
||||
{
|
||||
var err = ProcessOverlay1(overlay, ref _wristOverlayHandle, ref overlayVisible1,
|
||||
dashboardVisible, overlayIndex);
|
||||
if (err != EVROverlayError.None &&
|
||||
_wristOverlayHandle != 0)
|
||||
{
|
||||
overlay.DestroyOverlay(_wristOverlayHandle);
|
||||
_wristOverlayHandle = 0;
|
||||
logger.Error(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (_hmdOverlayActive)
|
||||
{
|
||||
var err = ProcessOverlay2(overlay, ref _hmdOverlayHandle, ref overlayVisible2,
|
||||
dashboardVisible);
|
||||
if (err != EVROverlayError.None &&
|
||||
_hmdOverlayHandle != 0)
|
||||
{
|
||||
overlay.DestroyOverlay(_hmdOverlayHandle);
|
||||
_hmdOverlayHandle = 0;
|
||||
logger.Error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (active)
|
||||
{
|
||||
active = false;
|
||||
IsHmdAfk = false;
|
||||
OpenVR.Shutdown();
|
||||
_deviceListLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_deviceList.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_deviceListLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_wristOverlayAccessor?.Dispose();
|
||||
_wristOverlayAccessor = null;
|
||||
_wristOverlayMMF?.Dispose();
|
||||
_wristOverlayMMF = null;
|
||||
|
||||
_hmdOverlayAccessor?.Dispose();
|
||||
_hmdOverlayAccessor = null;
|
||||
_hmdOverlayMMF?.Dispose();
|
||||
_hmdOverlayMMF = null;
|
||||
|
||||
GLContextX11.Cleanup();
|
||||
GLContextWayland.Cleanup();
|
||||
}
|
||||
|
||||
public override void SetActive(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand)
|
||||
{
|
||||
_active = active;
|
||||
_hmdOverlayActive = hmdOverlay;
|
||||
_wristOverlayActive = wristOverlay;
|
||||
_menuButton = menuButton;
|
||||
_overlayHand = overlayHand;
|
||||
|
||||
if (_hmdOverlayActive != _hmdOverlayWasActive && _hmdOverlayHandle != 0)
|
||||
{
|
||||
OpenVR.Overlay.DestroyOverlay(_hmdOverlayHandle);
|
||||
_hmdOverlayHandle = 0;
|
||||
|
||||
_hmdOverlayAccessor?.Dispose();
|
||||
_hmdOverlayAccessor = null;
|
||||
_hmdOverlayMMF?.Dispose();
|
||||
_hmdOverlayMMF = null;
|
||||
}
|
||||
|
||||
_hmdOverlayWasActive = _hmdOverlayActive;
|
||||
|
||||
if (_wristOverlayActive != _wristOverlayWasActive && _wristOverlayHandle != 0)
|
||||
{
|
||||
OpenVR.Overlay.DestroyOverlay(_wristOverlayHandle);
|
||||
_wristOverlayHandle = 0;
|
||||
|
||||
_wristOverlayAccessor?.Dispose();
|
||||
_wristOverlayAccessor = null;
|
||||
_wristOverlayMMF?.Dispose();
|
||||
_wristOverlayMMF = null;
|
||||
}
|
||||
|
||||
_wristOverlayWasActive = _wristOverlayActive;
|
||||
|
||||
if (!_active)
|
||||
{
|
||||
GLContextX11.Cleanup();
|
||||
GLContextWayland.Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Refresh()
|
||||
{
|
||||
//_wristOverlay.Reload();
|
||||
//_hmdOverlay.Reload();
|
||||
}
|
||||
|
||||
public override string[][] GetDevices()
|
||||
{
|
||||
_deviceListLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return _deviceList.ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_deviceListLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDevices(CVRSystem system, ref uint overlayIndex)
|
||||
{
|
||||
_deviceListLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_deviceList.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_deviceListLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
var sb = new StringBuilder(256);
|
||||
var state = new VRControllerState_t();
|
||||
var poses = new TrackedDevicePose_t[OpenVR.k_unMaxTrackedDeviceCount];
|
||||
system.GetDeviceToAbsoluteTrackingPose(ETrackingUniverseOrigin.TrackingUniverseStanding, 0, poses);
|
||||
for (var i = 0u; i < OpenVR.k_unMaxTrackedDeviceCount; ++i)
|
||||
{
|
||||
var devClass = system.GetTrackedDeviceClass(i);
|
||||
switch (devClass)
|
||||
{
|
||||
case ETrackedDeviceClass.HMD:
|
||||
var success = system.GetControllerState(i, ref state, (uint)Marshal.SizeOf(state));
|
||||
if (!success)
|
||||
break; // this fails while SteamVR overlay is open
|
||||
|
||||
var prox = state.ulButtonPressed & (1UL << ((int)EVRButtonId.k_EButton_ProximitySensor));
|
||||
var isHmdAfk = prox == 0;
|
||||
if (isHmdAfk != IsHmdAfk)
|
||||
{
|
||||
IsHmdAfk = isHmdAfk;
|
||||
Program.AppApiInstance.CheckGameRunning();
|
||||
}
|
||||
|
||||
var headsetErr = ETrackedPropertyError.TrackedProp_Success;
|
||||
var headsetBatteryPercentage = system.GetFloatTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_DeviceBatteryPercentage_Float, ref headsetErr);
|
||||
if (headsetErr != ETrackedPropertyError.TrackedProp_Success)
|
||||
{
|
||||
// Headset has no battery, skip displaying it
|
||||
break;
|
||||
}
|
||||
|
||||
var headset = new[]
|
||||
{
|
||||
"headset",
|
||||
system.IsTrackedDeviceConnected(i)
|
||||
? "connected"
|
||||
: "disconnected",
|
||||
// Currently neither VD or SteamLink report charging state
|
||||
"discharging",
|
||||
(headsetBatteryPercentage * 100).ToString(),
|
||||
poses[i].eTrackingResult.ToString()
|
||||
};
|
||||
_deviceListLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_deviceList.Add(headset);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_deviceListLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
break;
|
||||
case ETrackedDeviceClass.Controller:
|
||||
case ETrackedDeviceClass.GenericTracker:
|
||||
case ETrackedDeviceClass.TrackingReference:
|
||||
{
|
||||
var err = ETrackedPropertyError.TrackedProp_Success;
|
||||
var batteryPercentage = system.GetFloatTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_DeviceBatteryPercentage_Float, ref err);
|
||||
if (err != ETrackedPropertyError.TrackedProp_Success)
|
||||
{
|
||||
batteryPercentage = 1f;
|
||||
}
|
||||
|
||||
err = ETrackedPropertyError.TrackedProp_Success;
|
||||
var isCharging = system.GetBoolTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_DeviceIsCharging_Bool, ref err);
|
||||
if (err != ETrackedPropertyError.TrackedProp_Success)
|
||||
{
|
||||
isCharging = false;
|
||||
}
|
||||
|
||||
sb.Clear();
|
||||
system.GetStringTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_TrackingSystemName_String, sb, (uint)sb.Capacity, ref err);
|
||||
var isOculus = sb.ToString().IndexOf("oculus", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
// Oculus : B/Y, Bit 1, Mask 2
|
||||
// Oculus : A/X, Bit 7, Mask 128
|
||||
// Vive : Menu, Bit 1, Mask 2,
|
||||
// Vive : Grip, Bit 2, Mask 4
|
||||
var role = system.GetControllerRoleForTrackedDeviceIndex(i);
|
||||
if (role == ETrackedControllerRole.LeftHand || role == ETrackedControllerRole.RightHand)
|
||||
{
|
||||
if (_overlayHand == 0 ||
|
||||
(_overlayHand == 1 && role == ETrackedControllerRole.LeftHand) ||
|
||||
(_overlayHand == 2 && role == ETrackedControllerRole.RightHand))
|
||||
{
|
||||
if (system.GetControllerState(i, ref state, (uint)Marshal.SizeOf(state)) &&
|
||||
(state.ulButtonPressed & (_menuButton ? 2u : isOculus ? 128u : 4u)) != 0)
|
||||
{
|
||||
_nextOverlayUpdate = DateTime.MinValue;
|
||||
if (role == ETrackedControllerRole.LeftHand)
|
||||
{
|
||||
Array.Copy(_translationLeft, _translation, 3);
|
||||
Array.Copy(_rotationLeft, _rotation, 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
Array.Copy(_translationRight, _translation, 3);
|
||||
Array.Copy(_rotationRight, _rotation, 3);
|
||||
}
|
||||
|
||||
overlayIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var type = string.Empty;
|
||||
if (devClass == ETrackedDeviceClass.Controller)
|
||||
{
|
||||
if (role == ETrackedControllerRole.LeftHand)
|
||||
{
|
||||
type = "leftController";
|
||||
}
|
||||
else if (role == ETrackedControllerRole.RightHand)
|
||||
{
|
||||
type = "rightController";
|
||||
}
|
||||
else
|
||||
{
|
||||
type = "controller";
|
||||
}
|
||||
}
|
||||
else if (devClass == ETrackedDeviceClass.GenericTracker)
|
||||
{
|
||||
type = "tracker";
|
||||
}
|
||||
else if (devClass == ETrackedDeviceClass.TrackingReference)
|
||||
{
|
||||
type = "base";
|
||||
}
|
||||
|
||||
var item = new[]
|
||||
{
|
||||
type,
|
||||
system.IsTrackedDeviceConnected(i)
|
||||
? "connected"
|
||||
: "disconnected",
|
||||
isCharging
|
||||
? "charging"
|
||||
: "discharging",
|
||||
(batteryPercentage * 100).ToString(),
|
||||
poses[i].eTrackingResult.ToString()
|
||||
};
|
||||
_deviceListLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_deviceList.Add(item);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_deviceListLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal EVROverlayError ProcessDashboard(CVROverlay overlay, ref ulong dashboardHandle, bool dashboardVisible)
|
||||
{
|
||||
var err = EVROverlayError.None;
|
||||
|
||||
if (dashboardHandle == 0)
|
||||
{
|
||||
err = overlay.FindOverlay("VRCX", ref dashboardHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
if (err != EVROverlayError.UnknownOverlay)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
ulong thumbnailHandle = 0;
|
||||
err = overlay.CreateDashboardOverlay("VRCX", "VRCX", ref dashboardHandle, ref thumbnailHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
var iconPath = Path.Join(Program.BaseDirectory, "VRCX.png");
|
||||
err = overlay.SetOverlayFromFile(thumbnailHandle, iconPath);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
err = overlay.SetOverlayWidthInMeters(dashboardHandle, 1.5f);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
err = overlay.SetOverlayInputMethod(dashboardHandle, VROverlayInputMethod.Mouse);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var e = new VREvent_t();
|
||||
|
||||
while (overlay.PollNextOverlayEvent(dashboardHandle, ref e, (uint)Marshal.SizeOf(e)))
|
||||
{
|
||||
var type = (EVREventType)e.eventType;
|
||||
if (type == EVREventType.VREvent_MouseMove)
|
||||
{
|
||||
var m = e.data.mouse;
|
||||
//var s = _wristOverlay.Size;
|
||||
//_wristOverlay.GetBrowserHost().SendMouseMoveEvent((int)(m.x * s.Width), s.Height - (int)(m.y * s.Height), false, CefEventFlags.None);
|
||||
}
|
||||
else if (type == EVREventType.VREvent_MouseButtonDown)
|
||||
{
|
||||
var m = e.data.mouse;
|
||||
//var s = _wristOverlay.Size;
|
||||
//_wristOverlay.GetBrowserHost().SendMouseClickEvent((int)(m.x * s.Width), s.Height - (int)(m.y * s.Height), MouseButtonType.Left, false, 1, CefEventFlags.LeftMouseButton);
|
||||
}
|
||||
else if (type == EVREventType.VREvent_MouseButtonUp)
|
||||
{
|
||||
var m = e.data.mouse;
|
||||
//var s = _wristOverlay.Size;
|
||||
//_wristOverlay.GetBrowserHost().SendMouseClickEvent((int)(m.x * s.Width), s.Height - (int)(m.y * s.Height), MouseButtonType.Left, true, 1, CefEventFlags.None);
|
||||
}
|
||||
}
|
||||
|
||||
if (dashboardVisible)
|
||||
{
|
||||
//var texture = new Texture_t
|
||||
//{
|
||||
// handle = _texture1.NativePointer
|
||||
//};
|
||||
//err = overlay.SetOverlayTexture(dashboardHandle, ref texture);
|
||||
//if (err != EVROverlayError.None)
|
||||
//{
|
||||
// return err;
|
||||
//}
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
internal EVROverlayError ProcessOverlay1(CVROverlay overlay, ref ulong overlayHandle, ref bool overlayVisible, bool dashboardVisible, uint overlayIndex)
|
||||
{
|
||||
var err = EVROverlayError.None;
|
||||
|
||||
if (overlayHandle == 0)
|
||||
{
|
||||
err = overlay.FindOverlay("VRCX1", ref overlayHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
if (err != EVROverlayError.UnknownOverlay)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
overlayVisible = false;
|
||||
err = overlay.CreateOverlay("VRCX1", "VRCX1", ref overlayHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
err = overlay.SetOverlayAlpha(overlayHandle, 0.9f);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
err = overlay.SetOverlayWidthInMeters(overlayHandle, 1f);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
err = overlay.SetOverlayInputMethod(overlayHandle, VROverlayInputMethod.None);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
_wristOverlayMMF = MemoryMappedFile.CreateFromFile(WRIST_OVERLAY_SHM_PATH, FileMode.Open, null, WRIST_FRAME_SIZE + 1);
|
||||
_wristOverlayAccessor = _wristOverlayMMF.CreateViewAccessor();
|
||||
}
|
||||
}
|
||||
|
||||
if (overlayIndex != OpenVR.k_unTrackedDeviceIndexInvalid)
|
||||
{
|
||||
// http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices
|
||||
// Scaling-Rotation-Translation
|
||||
var m = Matrix4x4.CreateScale(0.25f);
|
||||
m *= Matrix4x4.CreateRotationX(_rotation[0]);
|
||||
m *= Matrix4x4.CreateRotationY(_rotation[1]);
|
||||
m *= Matrix4x4.CreateRotationZ(_rotation[2]);
|
||||
m *= Matrix4x4.CreateTranslation(new Vector3(_translation[0], _translation[1], _translation[2]));
|
||||
var hm34 = new HmdMatrix34_t
|
||||
{
|
||||
m0 = m.M11,
|
||||
m1 = m.M21,
|
||||
m2 = m.M31,
|
||||
m3 = m.M41,
|
||||
m4 = m.M12,
|
||||
m5 = m.M22,
|
||||
m6 = m.M32,
|
||||
m7 = m.M42,
|
||||
m8 = m.M13,
|
||||
m9 = m.M23,
|
||||
m10 = m.M33,
|
||||
m11 = m.M43
|
||||
};
|
||||
err = overlay.SetOverlayTransformTrackedDeviceRelative(overlayHandle, overlayIndex, ref hm34);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dashboardVisible &&
|
||||
DateTime.UtcNow.CompareTo(_nextOverlayUpdate) <= 0)
|
||||
{
|
||||
if (_wristOverlayTextureWriter != null)
|
||||
{
|
||||
byte[] imageData = GetLatestWristOverlayFrame();
|
||||
if (imageData != null)
|
||||
{
|
||||
FlipImageVertically(imageData, WRIST_FRAME_WIDTH, WRIST_FRAME_HEIGHT);
|
||||
_wristOverlayTextureWriter.WriteImageToBuffer(imageData);
|
||||
_wristOverlayTextureWriter.UpdateTexture();
|
||||
|
||||
Texture_t texture = _wristOverlayTextureWriter.AsTextureT();
|
||||
err = OpenVR.Overlay.SetOverlayTexture(overlayHandle, ref texture);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!overlayVisible)
|
||||
{
|
||||
err = overlay.ShowOverlay(overlayHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
overlayVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (overlayVisible)
|
||||
{
|
||||
err = overlay.HideOverlay(overlayHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
overlayVisible = false;
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
internal EVROverlayError ProcessOverlay2(CVROverlay overlay, ref ulong overlayHandle, ref bool overlayVisible, bool dashboardVisible)
|
||||
{
|
||||
var err = EVROverlayError.None;
|
||||
|
||||
if (overlayHandle == 0)
|
||||
{
|
||||
err = overlay.FindOverlay("VRCX2", ref overlayHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
if (err != EVROverlayError.UnknownOverlay)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
overlayVisible = false;
|
||||
err = overlay.CreateOverlay("VRCX2", "VRCX2", ref overlayHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
err = overlay.SetOverlayAlpha(overlayHandle, 0.9f);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
err = overlay.SetOverlayWidthInMeters(overlayHandle, 1f);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
err = overlay.SetOverlayInputMethod(overlayHandle, VROverlayInputMethod.None);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
var m = Matrix4x4.CreateScale(1f);
|
||||
m *= Matrix4x4.CreateTranslation(0, -0.3f, -1.5f);
|
||||
var hm34 = new HmdMatrix34_t
|
||||
{
|
||||
m0 = m.M11,
|
||||
m1 = m.M21,
|
||||
m2 = m.M31,
|
||||
m3 = m.M41,
|
||||
m4 = m.M12,
|
||||
m5 = m.M22,
|
||||
m6 = m.M32,
|
||||
m7 = m.M42,
|
||||
m8 = m.M13,
|
||||
m9 = m.M23,
|
||||
m10 = m.M33,
|
||||
m11 = m.M43
|
||||
};
|
||||
err = overlay.SetOverlayTransformTrackedDeviceRelative(overlayHandle, OpenVR.k_unTrackedDeviceIndex_Hmd, ref hm34);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
_hmdOverlayMMF = MemoryMappedFile.CreateFromFile(HMD_OVERLAY_SHM_PATH, FileMode.Open, null, HMD_FRAME_SIZE + 1);
|
||||
_hmdOverlayAccessor = _hmdOverlayMMF.CreateViewAccessor();
|
||||
}
|
||||
}
|
||||
|
||||
if (!dashboardVisible)
|
||||
{
|
||||
if (_hmdOverlayTextureWriter != null)
|
||||
{
|
||||
byte[] imageData = GetLatestHmdOverlayFrame();
|
||||
if (imageData != null)
|
||||
{
|
||||
FlipImageVertically(imageData, HMD_FRAME_WIDTH, HMD_FRAME_HEIGHT);
|
||||
_hmdOverlayTextureWriter.WriteImageToBuffer(imageData);
|
||||
_hmdOverlayTextureWriter.UpdateTexture();
|
||||
|
||||
Texture_t texture = _hmdOverlayTextureWriter.AsTextureT();
|
||||
err = OpenVR.Overlay.SetOverlayTexture(overlayHandle, ref texture);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!overlayVisible)
|
||||
{
|
||||
err = overlay.ShowOverlay(overlayHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
overlayVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (overlayVisible)
|
||||
{
|
||||
err = overlay.HideOverlay(overlayHandle);
|
||||
if (err != EVROverlayError.None)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
overlayVisible = false;
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue()
|
||||
{
|
||||
return _wristFeedFunctionQueue;
|
||||
}
|
||||
|
||||
public override void ExecuteVrFeedFunction(string function, string json)
|
||||
{
|
||||
//if (_hmdOverlaySocket == null || !_hmdOverlaySocket.Connected) return;
|
||||
// if (_wristOverlay.IsLoading)
|
||||
// Restart();
|
||||
|
||||
_wristFeedFunctionQueue.Enqueue(new KeyValuePair<string, string>(function, json));
|
||||
}
|
||||
|
||||
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
|
||||
{
|
||||
return _hmdFeedFunctionQueue;
|
||||
}
|
||||
|
||||
public override void ExecuteVrOverlayFunction(string function, string json)
|
||||
{
|
||||
//if (_hmdOverlaySocket == null || !_hmdOverlaySocket.Connected) return;
|
||||
// if (_hmdOverlay.IsLoading)
|
||||
// Restart();
|
||||
|
||||
_hmdFeedFunctionQueue.Enqueue(new KeyValuePair<string, string>(function, json));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
namespace VRCX;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
public abstract class VRCXVRInterface
|
||||
{
|
||||
public bool IsHmdAfk;
|
||||
|
||||
public abstract void Init();
|
||||
public abstract void Exit();
|
||||
public abstract void Refresh();
|
||||
@@ -12,4 +14,6 @@ public abstract class VRCXVRInterface
|
||||
public abstract string[][] GetDevices();
|
||||
public abstract void ExecuteVrFeedFunction(string function, string json);
|
||||
public abstract void ExecuteVrOverlayFunction(string function, string json);
|
||||
public abstract ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue();
|
||||
public abstract ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue();
|
||||
}
|
||||
@@ -24,10 +24,9 @@ namespace VRCX
|
||||
public static string Version { get; private set; }
|
||||
public static bool LaunchDebug;
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
#if !LINUX
|
||||
public static VRCXVRInterface VRCXVRInstance { get; private set; }
|
||||
#endif
|
||||
public static AppApi AppApiInstance { get; private set; }
|
||||
public static AppApiVr AppApiVrInstance { get; private set; }
|
||||
|
||||
private static void SetProgramDirectories()
|
||||
{
|
||||
@@ -74,7 +73,7 @@ namespace VRCX
|
||||
try
|
||||
{
|
||||
var versionFile = File.ReadAllText(Path.Join(BaseDirectory, "Version")).Trim();
|
||||
|
||||
|
||||
// look for trailing git hash "-22bcd96" to indicate nightly build
|
||||
var version = versionFile.Split('-');
|
||||
if (version.Length > 0 && version[^1].Length == 7)
|
||||
@@ -234,7 +233,8 @@ namespace VRCX
|
||||
SQLiteLegacy.Instance.Init();
|
||||
AppApiInstance = new AppApiCef();
|
||||
|
||||
AppApiVr.Instance.Init();
|
||||
AppApiVrInstance = new AppApiVrCef();
|
||||
AppApiVrInstance.Init();
|
||||
ProcessMonitor.Instance.Init();
|
||||
Discord.Instance.Init();
|
||||
WebApi.Instance.Init();
|
||||
@@ -246,7 +246,7 @@ namespace VRCX
|
||||
if (VRCXStorage.Instance.Get("VRCX_DisableVrOverlayGpuAcceleration") == "true")
|
||||
VRCXVRInstance = new VRCXVRLegacy();
|
||||
else
|
||||
VRCXVRInstance = new VRCXVR();
|
||||
VRCXVRInstance = new VRCXVRCef();
|
||||
VRCXVRInstance.Init();
|
||||
|
||||
Application.Run(new MainForm());
|
||||
@@ -260,7 +260,7 @@ namespace VRCX
|
||||
WebApi.Instance.Exit();
|
||||
|
||||
Discord.Instance.Exit();
|
||||
SystemMonitor.Instance.Exit();
|
||||
SystemMonitorCef.Instance.Exit();
|
||||
VRCXStorage.Instance.Save();
|
||||
SQLiteLegacy.Instance.Exit();
|
||||
ProcessMonitor.Instance.Exit();
|
||||
@@ -285,6 +285,9 @@ namespace VRCX
|
||||
|
||||
AppApiInstance = new AppApiElectron();
|
||||
// ProcessMonitor.Instance.Init();
|
||||
|
||||
VRCXVRInstance = new VRCXVRElectron();
|
||||
VRCXVRInstance.Init();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -121,8 +121,8 @@
|
||||
<Compile Remove="Cef\**" />
|
||||
<Content Remove="AppApi\Cef\**" />
|
||||
<Compile Remove="AppApi\Cef\**" />
|
||||
<Content Remove="Overlay\**" />
|
||||
<Compile Remove="Overlay\**" />
|
||||
<Content Remove="Overlay\Cef\**" />
|
||||
<Compile Remove="Overlay\Cef\**" />
|
||||
<Content Remove="DBMerger\**" />
|
||||
<Compile Remove="DBMerger\**" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -94,6 +94,7 @@ namespace VRCX
|
||||
catch (UriFormatException)
|
||||
{
|
||||
VRCXStorage.Instance.Set("VRCX_ProxyServer", string.Empty);
|
||||
VRCXStorage.Instance.Flush();
|
||||
const string message = "The proxy server URI you used is invalid.\nVRCX will close, please correct the proxy URI.";
|
||||
#if !LINUX
|
||||
System.Windows.Forms.MessageBox.Show(message, "Invalid Proxy URI", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
|
||||
Reference in New Issue
Block a user