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:
rs189
2025-07-19 09:07:43 +09:00
committed by GitHub
parent 53723d37b0
commit a2dc6ba9a4
53 changed files with 10555 additions and 7865 deletions

View 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;
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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));
}
}
}