// 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 . 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 _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> _wristFeedFunctionQueue = new ConcurrentQueue>(); private readonly ConcurrentQueue> _hmdFeedFunctionQueue = new ConcurrentQueue>(); static VRCXVRElectron() { Instance = new VRCXVRElectron(); } public VRCXVRElectron() { _deviceListLock = new ReaderWriterLockSlim(); _deviceList = new List(); _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(); Program.VRCXVRInstance = Instance; //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; 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> 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(function, json)); } public override ConcurrentQueue> 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(function, json)); } } }