Merge overlays, move overlay to separate process (#44)

* refactor: merge two overlay offScreenBrowser into one

* Electron support for shared overlay

* Separate overlay into its own process

* fix: invalid overlay texture size

* Handle duplicate processes

* Remove logging

---------

Co-authored-by: pa <maplenagisa@gmail.com>
Co-authored-by: rs189 <35667100+rs189@users.noreply.github.com>
This commit is contained in:
Natsumi
2026-01-12 11:31:10 +13:00
committed by Natsumi
parent 9135adf6d1
commit d2fd205476
41 changed files with 1122 additions and 2108 deletions

View File

@@ -7,6 +7,7 @@ using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
@@ -31,17 +32,15 @@ namespace VRCX
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();
var updateVars = new OverlayVars
{
Active = active,
HmdOverlay = hmdOverlay,
WristOverlay = wristOverlay,
MenuButton = menuButton,
OverlayHand = overlayHand
};
OverlayServer.Instance.UpdateVars(updateVars);
}
public override void SetZoom(double zoomLevel)
@@ -116,14 +115,15 @@ namespace VRCX
return File.Exists(Path.Join(Program.AppDataDirectory, "update.exe"));
}
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);
var message = new OverlayMessage
{
Type = OverlayMessageType.JsFunctionCall,
FunctionName = function,
Data = json
};
OverlayServer.Instance.SendMessage(message);
}
public override void FocusWindow()

View File

@@ -23,7 +23,6 @@ namespace VRCX
{
var isGameRunning = false;
var isSteamVRRunning = false;
var isHmdAfk = false;
if (ProcessMonitor.Instance.IsProcessRunning("VRChat"))
isGameRunning = true;
@@ -31,12 +30,9 @@ namespace VRCX
if (ProcessMonitor.Instance.IsProcessRunning("vrserver"))
isSteamVRRunning = true;
if (Program.VRCXVRInstance != null)
isHmdAfk = Program.VRCXVRInstance.IsHmdAfk;
// TODO: fix this throwing an exception for being called before the browser is ready. somehow it gets past the checks
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
MainForm.Instance.Browser.ExecuteScriptAsync("window?.$pinia?.game.updateIsGameRunning", isGameRunning, isSteamVRRunning, isHmdAfk);
MainForm.Instance.Browser.ExecuteScriptAsync("window?.$pinia?.game.updateIsGameRunning", isGameRunning, isSteamVRRunning);
}
public override bool IsGameRunning()

View File

@@ -9,8 +9,6 @@ namespace VRCX
// AppApi
public abstract void ShowDevTools();
public abstract void SetVR(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand);
public abstract void RefreshVR();
public abstract void RestartVR();
public abstract void SetZoom(double zoomLevel);
public abstract Task<double> GetZoom();
public abstract void DesktopNotification(string BoldText, string Text = "", string Image = "");
@@ -18,7 +16,6 @@ namespace VRCX
public abstract void RestartApplication(bool isUpgrade);
public abstract bool CheckForUpdateExe();
public abstract void ExecuteVrFeedFunction(string function, string json);
public abstract void ExecuteVrOverlayFunction(string function, string json);
public abstract void FocusWindow();
public abstract void ChangeTheme(int value);

View File

@@ -19,16 +19,6 @@ namespace VRCX
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)
{
}
@@ -51,11 +41,6 @@ namespace VRCX
return false;
}
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);

View File

@@ -23,7 +23,10 @@ namespace VRCX
internal void Init()
{
var isOverlay = StartupArgs.LaunchArguments.IsOverlay;
var userDataDir = Path.Join(Program.AppDataDirectory, "userdata");
if (isOverlay)
userDataDir = Path.Join(Program.AppDataDirectory, "overlay/userdata");
// delete userdata if Cef version has been downgraded, fixes VRCX not opening after a downgrade
CheckCefVersion(userDataDir);
@@ -39,6 +42,8 @@ namespace VRCX
BrowserSubprocessPath = Environment.ProcessPath,
BackgroundColor = 0xFF0A0A0A
};
if (isOverlay)
cefSettings.LogFile = Path.Join(Program.AppDataDirectory, "overlay/logs/cef.log");
cefSettings.RegisterScheme(new CefCustomScheme
{
@@ -58,7 +63,7 @@ namespace VRCX
cefSettings.CefCommandLineArgs.Add("disable-pdf-extension");
cefSettings.CefCommandLineArgs["autoplay-policy"] = "no-user-gesture-required";
cefSettings.CefCommandLineArgs.Add("disable-web-security");
cefSettings.CefCommandLineArgs.Add("disk-cache-size", "2147483647");
// cefSettings.CefCommandLineArgs.Add("disk-cache-size", "2147483647");
cefSettings.CefCommandLineArgs.Add("unsafely-disable-devtools-self-xss-warnings");
cefSettings.CefCommandLineArgs.Add("do-not-de-elevate"); // fix program failing to start when running as admin
@@ -78,9 +83,13 @@ namespace VRCX
// Discover network targets, Configure...
// Add Remote Target: localhost:8089
logger.Info("Debug mode enabled");
cefSettings.RemoteDebuggingPort = 8089;
cefSettings.RemoteDebuggingPort = !isOverlay ? 8089 : 8090;
cefSettings.CefCommandLineArgs["remote-allow-origins"] = "*";
}
// load extensions in debug mode
if (Program.LaunchDebug && !isOverlay)
{
var extensionsPath = Path.Join(Program.AppDataDirectory, "extensions");
Directory.CreateDirectory(extensionsPath);
@@ -111,10 +120,11 @@ namespace VRCX
}
}
CefSharpSettings.ShutdownOnExit = false;
CefSharpSettings.ShutdownOnExit = true;
CefSharpSettings.SubprocessExitIfParentProcessClosed = true;
CefSharpSettings.ConcurrentTaskExecution = true;
if (Cef.Initialize(cefSettings, false) == false)
if (!Cef.Initialize(cefSettings, false))
{
logger.Error("Cef failed to initialize");
throw new Exception("Cef.Initialize()");

View File

@@ -15,11 +15,5 @@ namespace VRCX
repository.Register("Discord", Discord.Instance);
repository.Register("AssetBundleManager", AssetBundleManager.Instance);
}
public static void ApplyVrJavascriptBindings(IJavascriptObjectRepository repository)
{
repository.NameConverter = null;
repository.Register("AppApiVr", AppApiVr.Instance);
}
}
}

View File

@@ -13,6 +13,5 @@ public abstract partial class AppApiVr
public abstract double GetUptime();
public abstract string CurrentCulture();
public abstract string CustomVrScript();
public abstract List<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue();
public abstract List<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue();
}

View File

@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using CefSharp;
using VRCX.Overlay;
namespace VRCX
{
@@ -20,8 +20,12 @@ namespace VRCX
public override void VrInit()
{
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
MainForm.Instance.Browser.ExecuteScriptAsync("window?.$pinia?.vr.vrInit();");
// IPC to main process that VR has initialized
var message = new OverlayMessage
{
Type = OverlayMessageType.OverlayConnected
};
OverlayClient.SendMessage(message);
}
public override void ToggleSystemMonitor(bool enabled)
@@ -45,7 +49,7 @@ namespace VRCX
/// <returns>An array of arrays containing information about the connected VR devices.</returns>
public override string[][] GetVRDevices()
{
return Program.VRCXVRInstance.GetDevices();
return OverlayProgram.VRCXVRInstance.GetDevices();
}
/// <summary>
@@ -75,11 +79,6 @@ namespace VRCX
return string.Empty;
}
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.");

View File

@@ -3,151 +3,273 @@ using CefSharp.Enums;
using CefSharp.OffScreen;
using CefSharp.Structs;
using System;
using System.Runtime.InteropServices;
using System.Threading;
using NLog;
using Silk.NET.Core.Native;
using Silk.NET.Direct3D11;
using Range = CefSharp.Structs.Range;
namespace VRCX
namespace VRCX;
public class OffScreenBrowser : ChromiumWebBrowser, IRenderHandler
{
public class OffScreenBrowser : ChromiumWebBrowser, IRenderHandler
// new
private ComPtr<ID3D11Device1> _device1;
private ComPtr<ID3D11DeviceContext> _deviceContext;
private ComPtr<ID3D11Query> _query;
private ComPtr<ID3D11Texture2D> _renderTarget;
// legacy
private readonly bool _isLegacy;
private readonly ReaderWriterLockSlim _paintBufferLock = new();
private GCHandle _paintBuffer;
private int _width;
private int _height;
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
public OffScreenBrowser(string address, int width, int height, bool isLegacy)
: base(address, automaticallyCreateBrowser: false)
{
private ComPtr<ID3D11Device1> _device1;
private ComPtr<ID3D11DeviceContext> _deviceContext;
private ComPtr<ID3D11Query> _query;
private ComPtr<ID3D11Texture2D> _renderTarget;
_isLegacy = isLegacy;
var windowInfo = new WindowInfo();
windowInfo.SetAsWindowless(IntPtr.Zero);
windowInfo.WindowlessRenderingEnabled = true;
windowInfo.SharedTextureEnabled = !_isLegacy;
windowInfo.Width = width;
windowInfo.Height = height;
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
public OffScreenBrowser(string address, int width, int height)
: base(address, automaticallyCreateBrowser: false)
var browserSettings = new BrowserSettings()
{
var windowInfo = new WindowInfo();
windowInfo.SetAsWindowless(IntPtr.Zero);
windowInfo.WindowlessRenderingEnabled = true;
windowInfo.SharedTextureEnabled = true;
windowInfo.Width = width;
windowInfo.Height = height;
DefaultEncoding = "UTF-8",
WindowlessFrameRate = !_isLegacy ? 60 : 24
};
var browserSettings = new BrowserSettings()
{
DefaultEncoding = "UTF-8",
WindowlessFrameRate = 60
};
CreateBrowser(windowInfo, browserSettings);
CreateBrowser(windowInfo, browserSettings);
Size = new System.Drawing.Size(width, height);
RenderHandler = this;
Size = new System.Drawing.Size(width, height);
RenderHandler = this;
JavascriptObjectRepository.NameConverter = null;
JavascriptObjectRepository.Register("AppApiVr", AppApiVr.Instance);
}
JavascriptBindings.ApplyVrJavascriptBindings(JavascriptObjectRepository);
// new
public void UpdateRender(ComPtr<ID3D11Device> device, ComPtr<ID3D11DeviceContext> deviceContext, ComPtr<ID3D11Texture2D> renderTarget)
{
_device1.Dispose();
_device1 = device.QueryInterface<ID3D11Device1>();
_deviceContext = deviceContext;
_renderTarget = renderTarget;
_query.Dispose();
device.CreateQuery(new QueryDesc
{
Query = Query.Event,
MiscFlags = 0
}, ref _query);
}
// legacy
public void RenderToTexture(ComPtr<ID3D11DeviceContext> deviceContext, ComPtr<ID3D11Texture2D> texture)
{
// Safeguard against uninitialized texture
unsafe
{
if ((IntPtr)texture.Handle == IntPtr.Zero)
return;
}
public void UpdateRender(ComPtr<ID3D11Device> device, ComPtr<ID3D11DeviceContext> deviceContext, ComPtr<ID3D11Texture2D> renderTarget)
_paintBufferLock.EnterReadLock();
try
{
_device1.Dispose();
_device1 = device.QueryInterface<ID3D11Device1>();
_deviceContext = deviceContext;
_renderTarget = renderTarget;
_query.Dispose();
device.CreateQuery(new QueryDesc
{
Query = Query.Event,
MiscFlags = 0
}, ref _query);
}
public new void Dispose()
{
RenderHandler = null;
base.Dispose();
}
ScreenInfo? IRenderHandler.GetScreenInfo()
{
return new ScreenInfo
{
DeviceScaleFactor = 1.0F
};
}
bool IRenderHandler.GetScreenPoint(int viewX, int viewY, out int screenX, out int screenY)
{
screenX = viewX;
screenY = viewY;
return false;
}
Rect IRenderHandler.GetViewRect()
{
return new Rect(0, 0, Size.Width, Size.Height);
}
void IRenderHandler.OnAcceleratedPaint(PaintElementType type, Rect dirtyRect, AcceleratedPaintInfo paintInfo)
{
if (type != PaintElementType.View)
if (_width <= 0 ||
_height <= 0)
return;
MappedSubresource mappedSubresource = default;
deviceContext.Map(texture, 0, Map.WriteDiscard, 0, ref mappedSubresource);
unsafe
{
if ((IntPtr)_device1.Handle == IntPtr.Zero)
return;
if ((IntPtr)_deviceContext.Handle == IntPtr.Zero)
return;
if ((IntPtr)_query.Handle == IntPtr.Zero)
return;
if ((IntPtr)_renderTarget.Handle == IntPtr.Zero)
return;
using ComPtr<ID3D11Texture2D> cefTexture =
_device1.OpenSharedResource1<ID3D11Texture2D>(paintInfo.SharedTextureHandle.ToPointer());
_deviceContext.CopyResource(_renderTarget, cefTexture);
_deviceContext.End(_query);
_deviceContext.Flush();
while (_deviceContext.GetData(_query, IntPtr.Zero.ToPointer(), 0, 0) == 1)
if ((IntPtr)mappedSubresource.PData != IntPtr.Zero)
{
Thread.Yield();
var sourcePtr = _paintBuffer.AddrOfPinnedObject();
var destinationPtr = (IntPtr)mappedSubresource.PData;
var pitch = _width * 4;
var rowPitch = mappedSubresource.RowPitch;
if (pitch == rowPitch)
{
WinApi.RtlCopyMemory(
destinationPtr,
sourcePtr,
(uint)(_width * _height * 4)
);
}
else
{
for (var y = _height; y > 0; --y)
{
WinApi.RtlCopyMemory(
destinationPtr,
sourcePtr,
(uint)pitch
);
sourcePtr += pitch;
destinationPtr += (IntPtr)rowPitch;
}
}
}
deviceContext.Unmap(texture, 0);
}
}
void IRenderHandler.OnCursorChange(IntPtr cursor, CursorType type, CursorInfo customCursorInfo)
{
}
void IRenderHandler.OnImeCompositionRangeChanged(Range selectedRange, Rect[] characterBounds)
{
}
void IRenderHandler.OnPaint(PaintElementType type, Rect dirtyRect, IntPtr buffer, int width, int height)
{
}
void IRenderHandler.OnPopupShow(bool show)
{
}
void IRenderHandler.OnPopupSize(Rect rect)
{
}
void IRenderHandler.OnVirtualKeyboardRequested(IBrowser browser, TextInputMode inputMode)
{
}
bool IRenderHandler.StartDragging(IDragData dragData, DragOperationsMask mask, int x, int y)
{
return false;
}
void IRenderHandler.UpdateDragCursor(DragOperationsMask operation)
finally
{
_paintBufferLock.ExitReadLock();
}
}
}
public new void Dispose()
{
RenderHandler = null;
base.Dispose();
// legacy
_paintBufferLock.EnterWriteLock();
try
{
if (_paintBuffer.IsAllocated)
_paintBuffer.Free();
}
finally
{
_paintBufferLock.ExitWriteLock();
}
_paintBufferLock.Dispose();
}
ScreenInfo? IRenderHandler.GetScreenInfo()
{
return new ScreenInfo
{
DeviceScaleFactor = 1.0F
};
}
bool IRenderHandler.GetScreenPoint(int viewX, int viewY, out int screenX, out int screenY)
{
screenX = viewX;
screenY = viewY;
return false;
}
Rect IRenderHandler.GetViewRect()
{
return new Rect(0, 0, Size.Width, Size.Height);
}
// new
void IRenderHandler.OnAcceleratedPaint(PaintElementType type, Rect dirtyRect, AcceleratedPaintInfo paintInfo)
{
if (_isLegacy)
return;
if (type != PaintElementType.View)
return;
unsafe
{
if ((IntPtr)_device1.Handle == IntPtr.Zero)
return;
if ((IntPtr)_deviceContext.Handle == IntPtr.Zero)
return;
if ((IntPtr)_query.Handle == IntPtr.Zero)
return;
if ((IntPtr)_renderTarget.Handle == IntPtr.Zero)
return;
using ComPtr<ID3D11Texture2D> cefTexture =
_device1.OpenSharedResource1<ID3D11Texture2D>(paintInfo.SharedTextureHandle.ToPointer());
_deviceContext.CopyResource(_renderTarget, cefTexture);
_deviceContext.End(_query);
_deviceContext.Flush();
while (_deviceContext.GetData(_query, IntPtr.Zero.ToPointer(), 0, 0) == 1)
{
Thread.Yield();
}
}
}
void IRenderHandler.OnCursorChange(IntPtr cursor, CursorType type, CursorInfo customCursorInfo)
{
}
void IRenderHandler.OnImeCompositionRangeChanged(Range selectedRange, Rect[] characterBounds)
{
}
// legacy
void IRenderHandler.OnPaint(PaintElementType type, Rect dirtyRect, IntPtr buffer, int width, int height)
{
if (!_isLegacy)
return;
if (type != PaintElementType.View)
return;
_paintBufferLock.EnterWriteLock();
try
{
if (_width != width ||
_height != height)
{
_width = width;
_height = height;
if (_paintBuffer.IsAllocated)
{
_paintBuffer.Free();
}
_paintBuffer = GCHandle.Alloc(
new byte[_width * _height * 4],
GCHandleType.Pinned
);
}
WinApi.RtlCopyMemory(
_paintBuffer.AddrOfPinnedObject(),
buffer,
(uint)(width * height * 4)
);
}
finally
{
_paintBufferLock.ExitWriteLock();
}
}
void IRenderHandler.OnPopupShow(bool show)
{
}
void IRenderHandler.OnPopupSize(Rect rect)
{
}
void IRenderHandler.OnVirtualKeyboardRequested(IBrowser browser, TextInputMode inputMode)
{
}
bool IRenderHandler.StartDragging(IDragData dragData, DragOperationsMask mask, int x, int y)
{
return false;
}
void IRenderHandler.UpdateDragCursor(DragOperationsMask operation)
{
}
}

View File

@@ -1,212 +0,0 @@
using CefSharp;
using CefSharp.Enums;
using CefSharp.OffScreen;
using CefSharp.Structs;
using System;
using System.Runtime.InteropServices;
using System.Threading;
using Silk.NET.Core.Native;
using Silk.NET.Direct3D11;
using Range = CefSharp.Structs.Range;
namespace VRCX
{
public class OffScreenBrowserLegacy : ChromiumWebBrowser, IRenderHandler
{
private readonly ReaderWriterLockSlim _paintBufferLock;
private GCHandle _paintBuffer;
private int _width;
private int _height;
public OffScreenBrowserLegacy(string address, int width, int height)
: base(address, automaticallyCreateBrowser: false)
{
_paintBufferLock = new ReaderWriterLockSlim();
var windowInfo = new WindowInfo();
windowInfo.SetAsWindowless(IntPtr.Zero);
windowInfo.WindowlessRenderingEnabled = true;
windowInfo.SharedTextureEnabled = false;
windowInfo.Width = width;
windowInfo.Height = height;
var browserSettings = new BrowserSettings()
{
DefaultEncoding = "UTF-8",
WindowlessFrameRate = 24
};
CreateBrowser(windowInfo, browserSettings);
Size = new System.Drawing.Size(width, height);
RenderHandler = this;
JavascriptBindings.ApplyVrJavascriptBindings(JavascriptObjectRepository);
}
public new void Dispose()
{
RenderHandler = null;
base.Dispose();
_paintBufferLock.EnterWriteLock();
try
{
if (_paintBuffer.IsAllocated == true)
{
_paintBuffer.Free();
}
}
finally
{
_paintBufferLock.ExitWriteLock();
}
_paintBufferLock.Dispose();
}
public void RenderToTexture(ComPtr<ID3D11DeviceContext> deviceContext, ComPtr<ID3D11Texture2D> texture)
{
// Safeguard against uninitialized texture
unsafe
{
if ((IntPtr)texture.Handle == IntPtr.Zero)
return;
}
_paintBufferLock.EnterReadLock();
try
{
if (_width > 0 &&
_height > 0)
{
MappedSubresource mappedSubresource = default;
deviceContext.Map(texture, 0, Map.WriteDiscard, 0, ref mappedSubresource);
unsafe
{
if ((IntPtr)mappedSubresource.PData != IntPtr.Zero)
{
var sourcePtr = _paintBuffer.AddrOfPinnedObject();
var destinationPtr = (IntPtr)mappedSubresource.PData;
var pitch = _width * 4;
var rowPitch = mappedSubresource.RowPitch;
if (pitch == rowPitch)
{
WinApi.RtlCopyMemory(
destinationPtr,
sourcePtr,
(uint)(_width * _height * 4)
);
}
else
{
for (var y = _height; y > 0; --y)
{
WinApi.RtlCopyMemory(
destinationPtr,
sourcePtr,
(uint)pitch
);
sourcePtr += pitch;
destinationPtr += (IntPtr)rowPitch;
}
}
}
deviceContext.Unmap(texture, 0);
}
}
}
finally
{
_paintBufferLock.ExitReadLock();
}
}
ScreenInfo? IRenderHandler.GetScreenInfo()
{
return null;
}
bool IRenderHandler.GetScreenPoint(int viewX, int viewY, out int screenX, out int screenY)
{
screenX = viewX;
screenY = viewY;
return false;
}
Rect IRenderHandler.GetViewRect()
{
return new Rect(0, 0, Size.Width, Size.Height);
}
void IRenderHandler.OnAcceleratedPaint(PaintElementType type, Rect dirtyRect, AcceleratedPaintInfo paintInfo)
{
// NOT USED
}
void IRenderHandler.OnCursorChange(IntPtr cursor, CursorType type, CursorInfo customCursorInfo)
{
}
void IRenderHandler.OnImeCompositionRangeChanged(Range selectedRange, Rect[] characterBounds)
{
}
void IRenderHandler.OnPaint(PaintElementType type, Rect dirtyRect, IntPtr buffer, int width, int height)
{
if (type == PaintElementType.View)
{
_paintBufferLock.EnterWriteLock();
try
{
if (_width != width ||
_height != height)
{
_width = width;
_height = height;
if (_paintBuffer.IsAllocated == true)
{
_paintBuffer.Free();
}
_paintBuffer = GCHandle.Alloc(
new byte[_width * _height * 4],
GCHandleType.Pinned
);
}
WinApi.RtlCopyMemory(
_paintBuffer.AddrOfPinnedObject(),
buffer,
(uint)(width * height * 4)
);
}
finally
{
_paintBufferLock.ExitWriteLock();
}
}
}
void IRenderHandler.OnPopupShow(bool show)
{
}
void IRenderHandler.OnPopupSize(Rect rect)
{
}
void IRenderHandler.OnVirtualKeyboardRequested(IBrowser browser, TextInputMode inputMode)
{
}
bool IRenderHandler.StartDragging(IDragData dragData, DragOperationsMask mask, int x, int y)
{
return false;
}
void IRenderHandler.UpdateDragCursor(DragOperationsMask operation)
{
}
}
}

View File

@@ -0,0 +1,120 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net.WebSockets;
using System.Text.Json;
using System.Threading.Tasks;
using NLog;
using VRCX.Overlay;
using Websocket.Client;
namespace VRCX;
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
public static class OverlayClient
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
private static readonly Uri WebsocketUri = new("ws://127.0.0.1:34582");
private static WebsocketClient? _websocketClient;
public static bool ConnectedAndActive =>
_websocketClient != null && _websocketClient.IsRunning &&
OverlayProgram.VRCXVRInstance.IsActive();
public static async Task Init()
{
if (_websocketClient != null)
return;
var clientFactory = new Func<ClientWebSocket>(() =>
{
var client = new ClientWebSocket();
client.Options.KeepAliveInterval = TimeSpan.FromSeconds(5);
client.Options.SetRequestHeader("user-agent", "VRCX Overlay Client");
return client;
});
_websocketClient = new WebsocketClient(WebsocketUri, clientFactory)
{
Name = "VRCX Overlay Client",
ReconnectTimeout = null,
ErrorReconnectTimeout = TimeSpan.FromMilliseconds(300)
};
_websocketClient.ReconnectionHappened.Subscribe(info =>
{
logger.Info("Connection happened, type: {0}", info?.Type.ToString());
var message = new OverlayMessage
{
Type = OverlayMessageType.OverlayConnected,
Data = "VRCX Overlay Client Connected"
};
_websocketClient.Send(JsonSerializer.Serialize(message));
});
_websocketClient.DisconnectionHappened.Subscribe(info =>
{
logger.Info("Disconnection happened, type: {0}", info?.Type.ToString());
});
_websocketClient.MessageReceived.Subscribe(msg =>
{
try
{
var message = JsonSerializer.Deserialize<OverlayMessage>(msg.Text!);
HandleMessage(message);
}
catch (Exception e)
{
logger.Error(e, "Error handling message");
}
});
_websocketClient.Start();
logger.Info("VRCX overlay client initialized");
}
private static void HandleMessage(OverlayMessage message)
{
logger.Trace("Message received: {0}", message.Type.ToString());
switch (message.Type)
{
case OverlayMessageType.OverlayConnected:
break;
case OverlayMessageType.JsFunctionCall:
OverlayProgram.VRCXVRInstance.ExecuteVrOverlayFunction(message.FunctionName, message.Data);
break;
case OverlayMessageType.UpdateVars:
var overlayVars = message.OverlayVars;
if (overlayVars == null)
{
logger.Error("UpdateVars is null");
return;
}
OverlayProgram.VRCXVRInstance.SetActive(overlayVars.Active, overlayVars.HmdOverlay, overlayVars.WristOverlay,
overlayVars.MenuButton, overlayVars.OverlayHand);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
public static async Task Exit()
{
if (_websocketClient == null)
return;
_websocketClient.Stop(WebSocketCloseStatus.NormalClosure, "Exiting");
_websocketClient.Dispose();
_websocketClient = null;
}
public static void SendMessage(OverlayMessage message)
{
if (_websocketClient == null || !_websocketClient.IsRunning)
return;
_websocketClient.Send(JsonSerializer.Serialize(message));
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using NLog;
namespace VRCX.Overlay;
internal static class OverlayProgram
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
public static VRCXVRInterface VRCXVRInstance;
public static void OverlayMain()
{
logger.Info("VRCX Overlay starting...");
CefService.Instance.Init();
AppApiVr.Instance = new AppApiVrCef();
var isLegacy = VRCXStorage.Instance.Get("VRCX_DisableVrOverlayGpuAcceleration") == "true";
VRCXVRInstance = new VRCXVRCef(isLegacy);
VRCXVRInstance.Init();
OverlayClient.Init();
logger.Info("VRCX Overlay started...");
QuitProcess();
ApplicationConfiguration.Initialize();
var context = new ApplicationContext();
Application.Run(context);
Exit();
}
private static void Exit()
{
logger.Info("VRCX Overlay exiting...");
// CefService.Instance.Exit();
OverlayClient.Exit();
VRCXVRInstance.Exit();
Environment.Exit(0);
}
private static async Task QuitProcess()
{
await Task.Delay(5000);
while (OverlayClient.ConnectedAndActive)
{
await Task.Delay(500);
}
Exit();
}
}

View File

@@ -12,12 +12,12 @@ using Silk.NET.Core.Native;
using Silk.NET.Direct3D11;
using Silk.NET.DXGI;
using Valve.VR;
using VRCX.Overlay;
namespace VRCX
{
public class VRCXVRCef : 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 };
@@ -25,11 +25,11 @@ namespace VRCX
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 static OffScreenBrowser _wristOverlay;
private static OffScreenBrowser _hmdOverlay;
private static OffScreenBrowser _sharedOverlay;
private readonly List<string[]> _deviceList;
private readonly ReaderWriterLockSlim _deviceListLock;
private readonly bool _isLegacy;
private bool _active;
private bool _menuButton;
private int _overlayHand;
@@ -44,6 +44,11 @@ namespace VRCX
private bool _wristOverlayActive;
private bool _wristOverlayWasActive;
private const int HMD_HEIGHT = 1024;
private const int WRIST_SIZE = 512;
private const int TOTAL_WIDTH = 1024;
private const int TOTAL_HEIGHT = HMD_HEIGHT + WRIST_SIZE; // 1024 + 512 = 1536
private DXGI _dxgi;
private D3D11 _d3d11;
private ComPtr<IDXGIFactory2> _factory;
@@ -52,16 +57,11 @@ namespace VRCX
private ComPtr<ID3D11Multithread> _multithread;
private ComPtr<ID3D11DeviceContext> _deviceContext;
private ComPtr<ID3D11Texture2D> _texture1;
private ComPtr<ID3D11Texture2D> _texture2;
private ComPtr<ID3D11Texture2D> _sharedTexture;
static VRCXVRCef()
{
Instance = new VRCXVRCef();
}
public VRCXVRCef()
public VRCXVRCef(bool isLegacy)
{
_isLegacy = isLegacy;
_deviceListLock = new ReaderWriterLockSlim();
_deviceList = new List<string[]>();
_thread = new Thread(ThreadLoop)
@@ -70,8 +70,6 @@ namespace VRCX
};
}
// NOTE
// 메모리 릭 때문에 미리 생성해놓고 계속 사용함
public override void Init()
{
_thread.Start();
@@ -88,10 +86,8 @@ namespace VRCX
public override void Restart()
{
Exit();
Instance = new VRCXVRCef();
Instance.Init();
Program.VRCXVRInstance = Instance;
MainForm.Instance.Browser.ExecuteScriptAsync("console.log('VRCXVR Restarted');");
OverlayProgram.VRCXVRInstance = new VRCXVRCef(_isLegacy);
OverlayProgram.VRCXVRInstance.Init();
}
private void SetupTextures()
@@ -111,6 +107,7 @@ namespace VRCX
_device.Dispose();
_deviceContext.Dispose();
SilkMarshal.ThrowHResult
(
_d3d11.CreateDevice
@@ -128,51 +125,39 @@ namespace VRCX
)
);
if ((IntPtr)_sharedTexture.Handle != IntPtr.Zero)
{
_sharedTexture.Dispose();
}
SilkMarshal.ThrowHResult
(
_device.CreateTexture2D(new Texture2DDesc
{
Width = TOTAL_WIDTH,
Height = TOTAL_HEIGHT,
MipLevels = 1,
ArraySize = 1,
Format = Format.FormatB8G8R8A8Unorm,
SampleDesc = new SampleDesc
{
Count = 1,
Quality = 0
},
BindFlags = (uint)BindFlag.ShaderResource,
CPUAccessFlags = _isLegacy ? (uint)CpuAccessFlag.Write : (uint)CpuAccessFlag.None,
Usage = _isLegacy ? Usage.Dynamic : Usage.Default
}, null, ref _sharedTexture)
);
// new
_sharedOverlay?.UpdateRender(_device, _deviceContext, _sharedTexture);
_multithread = _device.QueryInterface<ID3D11Multithread>();
_multithread.SetMultithreadProtected(true);
if (Program.LaunchDebug)
_device.SetInfoQueueCallback(msg => logger.Info(SilkMarshal.PtrToString((nint)msg.PDescription)!));
_texture1.Dispose();
SilkMarshal.ThrowHResult
(
_device.CreateTexture2D(new Texture2DDesc
{
Width = 512,
Height = 512,
MipLevels = 1,
ArraySize = 1,
Format = Format.FormatB8G8R8A8Unorm,
SampleDesc = new SampleDesc
{
Count = 1,
Quality = 0
},
BindFlags = (uint)BindFlag.ShaderResource
}, null, ref _texture1)
);
_wristOverlay?.UpdateRender(_device, _deviceContext, _texture1);
_texture2.Dispose();
SilkMarshal.ThrowHResult
(
_device.CreateTexture2D(new Texture2DDesc
{
Width = 1024,
Height = 1024,
MipLevels = 1,
ArraySize = 1,
Format = Format.FormatB8G8R8A8Unorm,
SampleDesc = new SampleDesc
{
Count = 1,
Quality = 0
},
BindFlags = (uint)BindFlag.ShaderResource
}, null, ref _texture2)
);
_hmdOverlay?.UpdateRender(_device, _deviceContext, _texture2);
}
}
@@ -188,20 +173,17 @@ namespace VRCX
var overlayVisible2 = false;
var dashboardHandle = 0UL;
_wristOverlay = new OffScreenBrowser(
Program.LaunchDebug ? "http://localhost:9000/vr.html?wrist" : "file://vrcx/vr.html?wrist",
512,
512
);
_hmdOverlay = new OffScreenBrowser(
Program.LaunchDebug ? "http://localhost:9000/vr.html?hmd" : "file://vrcx/vr.html?hmd",
1024,
1024
_sharedOverlay = new OffScreenBrowser(
Program.LaunchDebug ? "http://localhost:9000/vr.html" : "file://vrcx/vr.html",
TOTAL_WIDTH,
TOTAL_HEIGHT,
_isLegacy
);
while (_thread != null)
{
if (_isLegacy && (_wristOverlayActive || _hmdOverlayActive))
_sharedOverlay.RenderToTexture(_deviceContext, _sharedTexture);
try
{
Thread.Sleep(32);
@@ -267,18 +249,18 @@ namespace VRCX
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);
}
// var err = ProcessDashboard(overlay, ref dashboardHandle, dashboardVisible);
// if (err != EVROverlayError.None &&
// dashboardHandle != 0)
// {
// overlay.DestroyOverlay(dashboardHandle);
// dashboardHandle = 0;
// logger.Error(err);
// }
if (_wristOverlayActive)
{
err = ProcessOverlay1(overlay, ref _wristOverlayHandle, ref overlayVisible1,
var err = ProcessOverlay1(overlay, ref _wristOverlayHandle, ref overlayVisible1,
dashboardVisible, overlayIndex);
if (err != EVROverlayError.None &&
_wristOverlayHandle != 0)
@@ -291,7 +273,7 @@ namespace VRCX
if (_hmdOverlayActive)
{
err = ProcessOverlay2(overlay, ref _hmdOverlayHandle, ref overlayVisible2,
var err = ProcessOverlay2(overlay, ref _hmdOverlayHandle, ref overlayVisible2,
dashboardVisible);
if (err != EVROverlayError.None &&
_hmdOverlayHandle != 0)
@@ -321,13 +303,18 @@ namespace VRCX
}
}
_hmdOverlay?.Dispose();
_wristOverlay?.Dispose();
_texture2.Dispose();
_texture1.Dispose();
_device.Dispose();
_adapter.Dispose();
_factory.Dispose();
_sharedOverlay?.Dispose();
_sharedOverlay = null;
_sharedTexture.Dispose();
_sharedTexture = default;
_deviceContext.Dispose();
_multithread.Dispose();
_dxgi?.Dispose();
_d3d11?.Dispose();
}
public override void SetActive(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand)
@@ -355,10 +342,14 @@ namespace VRCX
_wristOverlayWasActive = _wristOverlayActive;
}
public override bool IsActive()
{
return _active;
}
public override void Refresh()
{
_wristOverlay.Reload();
_hmdOverlay.Reload();
_sharedOverlay.Reload();
}
public override string[][] GetDevices()
@@ -405,7 +396,12 @@ namespace VRCX
if (isHmdAfk != IsHmdAfk)
{
IsHmdAfk = isHmdAfk;
Program.AppApiInstance.CheckGameRunning();
var message = new OverlayMessage
{
Type = OverlayMessageType.IsHmdAfk,
Data = IsHmdAfk.ToString()
};
OverlayClient.SendMessage(message);
}
var headsetErr = ETrackedPropertyError.TrackedProp_Success;
@@ -593,37 +589,22 @@ namespace VRCX
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)
{
unsafe
{
var texture = new Texture_t
{
handle = (IntPtr)_texture1.Handle
handle = (IntPtr)_sharedTexture.Handle
};
var bounds = new VRTextureBounds_t
{
uMin = 0f,
uMax = 1f,
vMin = (float)(TOTAL_HEIGHT - HMD_HEIGHT) / TOTAL_HEIGHT,
vMax = 1f
};
overlay.SetOverlayTextureBounds(dashboardHandle, ref bounds);
err = overlay.SetOverlayTexture(dashboardHandle, ref texture);
if (err != EVROverlayError.None)
{
@@ -707,30 +688,29 @@ namespace VRCX
}
}
if (!dashboardVisible &&
DateTime.UtcNow.CompareTo(_nextOverlayUpdate) <= 0)
if (!dashboardVisible && DateTime.UtcNow.CompareTo(_nextOverlayUpdate) <= 0)
{
unsafe
{
{
var texture = new Texture_t
{
handle = (IntPtr)_texture1.Handle
handle = (IntPtr)_sharedTexture.Handle
};
err = overlay.SetOverlayTexture(overlayHandle, ref texture);
if (err != EVROverlayError.None)
{
return err;
}
}
var bounds = new VRTextureBounds_t
{
uMin = 0f,
uMax = 0.5f,
vMin = 0f,
vMax = (float)WRIST_SIZE / TOTAL_HEIGHT
};
overlay.SetOverlayTextureBounds(overlayHandle, ref bounds);
if (!overlayVisible)
{
err = overlay.ShowOverlay(overlayHandle);
if (err != EVROverlayError.None)
{
return err;
}
overlayVisible = true;
}
}
@@ -818,23 +798,23 @@ namespace VRCX
{
var texture = new Texture_t
{
handle = (IntPtr)_texture2.Handle
handle = (IntPtr)_sharedTexture.Handle
};
err = overlay.SetOverlayTexture(overlayHandle, ref texture);
if (err != EVROverlayError.None)
{
return err;
}
}
var bounds = new VRTextureBounds_t
{
uMin = 0f,
uMax = 1f,
vMin = (float)(TOTAL_HEIGHT - HMD_HEIGHT) / TOTAL_HEIGHT,
vMax = 1f
};
overlay.SetOverlayTextureBounds(overlayHandle, ref bounds);
if (!overlayVisible)
{
err = overlay.ShowOverlay(overlayHandle);
if (err != EVROverlayError.None)
{
return err;
}
overlayVisible = true;
}
}
@@ -852,19 +832,6 @@ 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;
// if (_wristOverlay.IsLoading)
// Restart();
_wristOverlay.ExecuteScriptAsync($"$vr.{function}", json);
}
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
{
throw new NotImplementedException();
@@ -872,10 +839,10 @@ namespace VRCX
public override void ExecuteVrOverlayFunction(string function, string json)
{
if (_hmdOverlay == null) return;
// if (_hmdOverlay.IsLoading)
// Restart();
_hmdOverlay.ExecuteScriptAsync($"$vr.{function}", json);
if (_sharedOverlay == null || _sharedOverlay.IsLoading || !_sharedOverlay.CanExecuteJavascriptInMainFrame)
return;
_sharedOverlay.ExecuteScriptAsync($"$vr.{function}", json);
}
}
}

View File

@@ -1,856 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using CefSharp;
using NLog;
using Silk.NET.Core.Native;
using Silk.NET.Direct3D11;
using Silk.NET.DXGI;
using Valve.VR;
namespace VRCX
{
public class VRCXVRLegacy : 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 static OffScreenBrowserLegacy _wristOverlay;
private static OffScreenBrowserLegacy _hmdOverlay;
private readonly List<string[]> _deviceList;
private readonly ReaderWriterLockSlim _deviceListLock;
private bool _active;
private bool _hmdOverlayActive;
private bool _menuButton;
private int _overlayHand;
private Thread _thread;
private bool _wristOverlayActive;
private DateTime _nextOverlayUpdate;
private DXGI _dxgi;
private D3D11 _d3d11;
private ComPtr<IDXGIFactory2> _factory;
private ComPtr<IDXGIAdapter> _adapter;
private ComPtr<ID3D11Device> _device;
private ComPtr<ID3D11Multithread> _multithread;
private ComPtr<ID3D11DeviceContext> _deviceContext;
private ComPtr<ID3D11Texture2D> _texture1;
private ComPtr<ID3D11Texture2D> _texture2;
static VRCXVRLegacy()
{
Instance = new VRCXVRLegacy();
}
public VRCXVRLegacy()
{
_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();
}
public override void Restart()
{
Exit();
Instance = new VRCXVRLegacy();
Instance.Init();
Program.VRCXVRInstance = Instance;
MainForm.Instance.Browser.ExecuteScriptAsync("console.log('VRCXVR Restarted');");
}
private void SetupTextures()
{
unsafe
{
_dxgi?.Dispose();
_dxgi = DXGI.GetApi(null);
_d3d11?.Dispose();
_d3d11 = D3D11.GetApi(null);
_factory.Dispose();
SilkMarshal.ThrowHResult(_dxgi.CreateDXGIFactory<IDXGIFactory2>(out _factory));
_adapter.Dispose();
SilkMarshal.ThrowHResult(_factory.EnumAdapters((uint)OpenVR.System.GetD3D9AdapterIndex(),
ref _adapter));
_device.Dispose();
_deviceContext.Dispose();
SilkMarshal.ThrowHResult
(
_d3d11.CreateDevice
(
_adapter,
D3DDriverType.Unknown,
Software: default,
(uint)(CreateDeviceFlag.BgraSupport | (Program.LaunchDebug ? CreateDeviceFlag.Debug : 0)),
null,
0,
D3D11.SdkVersion,
ref _device,
null,
ref _deviceContext
)
);
_multithread = _device.QueryInterface<ID3D11Multithread>();
_multithread.SetMultithreadProtected(true);
if (Program.LaunchDebug)
_device.SetInfoQueueCallback(msg => logger.Info(SilkMarshal.PtrToString((nint)msg.PDescription)!));
_texture1.Dispose();
SilkMarshal.ThrowHResult
(
_device.CreateTexture2D(new Texture2DDesc
{
Width = 512,
Height = 512,
MipLevels = 1,
ArraySize = 1,
Format = Format.FormatB8G8R8A8Unorm,
SampleDesc = new SampleDesc
{
Count = 1,
Quality = 0
},
BindFlags = (uint)BindFlag.ShaderResource,
CPUAccessFlags = (uint)CpuAccessFlag.Write,
Usage = Usage.Dynamic
}, null, ref _texture1)
);
_texture2.Dispose();
SilkMarshal.ThrowHResult
(
_device.CreateTexture2D(new Texture2DDesc
{
Width = 1024,
Height = 1024,
MipLevels = 1,
ArraySize = 1,
Format = Format.FormatB8G8R8A8Unorm,
SampleDesc = new SampleDesc
{
Count = 1,
Quality = 0
},
BindFlags = (uint)BindFlag.ShaderResource,
CPUAccessFlags = (uint)CpuAccessFlag.Write,
Usage = Usage.Dynamic
}, null, ref _texture2)
);
}
}
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;
var overlayHandle1 = 0UL;
var overlayHandle2 = 0UL;
_wristOverlay = new OffScreenBrowserLegacy(
Program.LaunchDebug ? "http://localhost:9000/vr.html?wrist" : "file://vrcx/vr.html?wrist",
512,
512
);
_hmdOverlay = new OffScreenBrowserLegacy(
Program.LaunchDebug ? "http://localhost:9000/vr.html?hmd" : "file://vrcx/vr.html?hmd",
1024,
1024
);
while (_thread != null)
{
if (_wristOverlayActive)
_wristOverlay.RenderToTexture(_deviceContext, _texture1);
if (_hmdOverlayActive)
_hmdOverlay.RenderToTexture(_deviceContext, _texture2);
try
{
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;
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);
}
err = ProcessOverlay1(overlay, ref overlayHandle1, ref overlayVisible1, dashboardVisible, overlayIndex);
if (err != EVROverlayError.None &&
overlayHandle1 != 0)
{
overlay.DestroyOverlay(overlayHandle1);
overlayHandle1 = 0;
logger.Error(err);
}
err = ProcessOverlay2(overlay, ref overlayHandle2, ref overlayVisible2, dashboardVisible);
if (err != EVROverlayError.None &&
overlayHandle2 != 0)
{
overlay.DestroyOverlay(overlayHandle2);
overlayHandle2 = 0;
logger.Error(err);
}
}
}
}
else if (active)
{
active = false;
IsHmdAfk = false;
OpenVR.Shutdown();
_deviceListLock.EnterWriteLock();
try
{
_deviceList.Clear();
}
finally
{
_deviceListLock.ExitWriteLock();
}
}
}
_hmdOverlay?.Dispose();
_wristOverlay?.Dispose();
_texture2.Dispose();
_texture1.Dispose();
_device.Dispose();
_adapter.Dispose();
_factory.Dispose();
}
public override void SetActive(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand)
{
_active = active;
_hmdOverlayActive = hmdOverlay;
_wristOverlayActive = wristOverlay;
_menuButton = menuButton;
_overlayHand = overlayHand;
}
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;
}
err = overlay.SetOverlayFlag(dashboardHandle, VROverlayFlags.NoDashboardTab, true);
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)
{
unsafe
{
var texture = new Texture_t
{
handle = (IntPtr)_texture1.Handle
};
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;
}
}
}
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)
{
unsafe
{
var texture = new Texture_t
{
handle = (IntPtr)_texture1.Handle
};
err = 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;
}
}
}
if (!dashboardVisible)
{
unsafe
{
var texture = new Texture_t
{
handle = (IntPtr)_texture2.Handle
};
err = 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()
{
throw new NotImplementedException();
}
public override void ExecuteVrFeedFunction(string function, string json)
{
if (_wristOverlay == null) return;
if (_wristOverlay.IsLoading)
Restart();
_wristOverlay.ExecuteScriptAsync($"$vr.{function}", json);
}
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
{
throw new NotImplementedException();
}
public override void ExecuteVrOverlayFunction(string function, string json)
{
if (_hmdOverlay == null) return;
if (_hmdOverlay.IsLoading)
Restart();
_hmdOverlay.ExecuteScriptAsync($"$vr.{function}", json);
}
}
}

View File

@@ -1,104 +0,0 @@
namespace VRCX
{
partial class VRForm
{
/// <summary>
/// 필수 디자이너 변수입니다.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 사용 중인 모든 리소스를 정리합니다.
/// </summary>
/// <param name="disposing">관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form
/// <summary>
/// 디자이너 지원에 필요한 메서드입니다.
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.timer = new System.Windows.Forms.Timer(this.components);
this.panel1 = new System.Windows.Forms.Panel();
this.panel2 = new System.Windows.Forms.Panel();
this.button_refresh = new System.Windows.Forms.Button();
this.button_devtools = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// panel1
//
this.panel1.Location = new System.Drawing.Point(0, 0);
this.panel1.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(731, 768);
this.panel1.TabIndex = 0;
//
// panel2
//
this.panel2.Location = new System.Drawing.Point(740, 0);
this.panel2.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4);
this.panel2.Name = "panel2";
this.panel2.Size = new System.Drawing.Size(731, 768);
this.panel2.TabIndex = 1;
//
// button_refresh
//
this.button_refresh.Location = new System.Drawing.Point(17, 777);
this.button_refresh.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4);
this.button_refresh.Name = "button_refresh";
this.button_refresh.Size = new System.Drawing.Size(107, 34);
this.button_refresh.TabIndex = 27;
this.button_refresh.Text = "Refresh";
this.button_refresh.UseVisualStyleBackColor = true;
this.button_refresh.Click += new System.EventHandler(this.button_refresh_Click);
//
// button_devtools
//
this.button_devtools.Location = new System.Drawing.Point(133, 777);
this.button_devtools.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4);
this.button_devtools.Name = "button_devtools";
this.button_devtools.Size = new System.Drawing.Size(107, 34);
this.button_devtools.TabIndex = 27;
this.button_devtools.Text = "DevTools";
this.button_devtools.UseVisualStyleBackColor = true;
this.button_devtools.Click += new System.EventHandler(this.button_devtools_Click);
//
// VRForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(144F, 144F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
this.ClientSize = new System.Drawing.Size(1483, 830);
this.Controls.Add(this.button_devtools);
this.Controls.Add(this.button_refresh);
this.Controls.Add(this.panel2);
this.Controls.Add(this.panel1);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4);
this.MaximizeBox = false;
this.Name = "VRForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "VR";
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Timer timer;
private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.Panel panel2;
private System.Windows.Forms.Button button_refresh;
private System.Windows.Forms.Button button_devtools;
}
}

View File

@@ -1,64 +0,0 @@
using System.IO;
using System.Windows.Forms;
using CefSharp;
using CefSharp.WinForms;
namespace VRCX
{
public partial class VRForm : WinformBase
{
public static VRForm Instance;
private ChromiumWebBrowser _browser1;
private ChromiumWebBrowser _browser2;
public VRForm()
{
Instance = this;
InitializeComponent();
_browser1 = new ChromiumWebBrowser(
Path.Join(Program.BaseDirectory, "html/vr.html?wrist")
)
{
DragHandler = new CefNoopDragHandler(),
RequestHandler = new CustomRequestHandler(),
BrowserSettings =
{
DefaultEncoding = "UTF-8",
},
Dock = DockStyle.Fill
};
_browser2 = new ChromiumWebBrowser(
Path.Join(Program.BaseDirectory, "html/vr.html?hmd")
)
{
DragHandler = new CefNoopDragHandler(),
BrowserSettings =
{
DefaultEncoding = "UTF-8",
},
Dock = DockStyle.Fill
};
JavascriptBindings.ApplyVrJavascriptBindings(_browser1.JavascriptObjectRepository);
JavascriptBindings.ApplyVrJavascriptBindings(_browser2.JavascriptObjectRepository);
panel1.Controls.Add(_browser1);
panel2.Controls.Add(_browser2);
}
private void button_refresh_Click(object sender, System.EventArgs e)
{
_browser1.ExecuteScriptAsync("location.reload()");
_browser2.ExecuteScriptAsync("location.reload()");
Program.VRCXVRInstance.Refresh();
}
private void button_devtools_Click(object sender, System.EventArgs e)
{
_browser1.ShowDevTools();
_browser2.ShowDevTools();
}
}
}

View File

@@ -1,123 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="timer.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root>

View File

@@ -19,16 +19,6 @@ namespace VRCX
{
}
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>>();

View File

@@ -27,8 +27,7 @@ namespace VRCX
private bool _active;
private bool _menuButton;
private int _overlayHand;
private GLTextureWriter _wristOverlayTextureWriter;
private GLTextureWriter _hmdOverlayTextureWriter;
private GLTextureWriter _overlayTextureWriter;
private Thread _thread;
private DateTime _nextOverlayUpdate;
@@ -40,22 +39,22 @@ namespace VRCX
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 string OVERLAY_SHM_PATH = "/dev/shm/vrcx_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>>();
private const int SHARED_FRAME_SIZE = SHARED_FRAME_WIDTH * SHARED_FRAME_HEIGHT * 4;
private const int SHARED_FRAME_WIDTH = 1024;
private const int SHARED_FRAME_HEIGHT = WRIST_FRAME_HEIGHT + HMD_FRAME_HEIGHT;
private byte[] frameBuffer = new byte[SHARED_FRAME_SIZE];
private MemoryMappedFile _overlayMMF;
private MemoryMappedViewAccessor _overlayAccessor;
private readonly ConcurrentQueue<KeyValuePair<string, string>> _overlayFunctionQueue = new ConcurrentQueue<KeyValuePair<string, string>>();
static VRCXVRElectron()
{
@@ -86,15 +85,10 @@ namespace VRCX
thread?.Interrupt();
thread?.Join();
_wristOverlayAccessor?.Dispose();
_wristOverlayAccessor = null;
_wristOverlayMMF?.Dispose();
_wristOverlayMMF = null;
_hmdOverlayAccessor?.Dispose();
_hmdOverlayAccessor = null;
_hmdOverlayMMF?.Dispose();
_hmdOverlayMMF = null;
_overlayAccessor?.Dispose();
_overlayAccessor = null;
_overlayMMF?.Dispose();
_overlayMMF = null;
GLContextX11.Cleanup();
GLContextWayland.Cleanup();
@@ -135,11 +129,8 @@ namespace VRCX
throw new Exception("Failed to initialise OpenGL context");
}
_wristOverlayTextureWriter = new GLTextureWriter(512, 512);
_wristOverlayTextureWriter.UpdateTexture();
_hmdOverlayTextureWriter = new GLTextureWriter(1024, 1024);
_hmdOverlayTextureWriter.UpdateTexture();
_overlayTextureWriter = new GLTextureWriter(SHARED_FRAME_WIDTH, SHARED_FRAME_HEIGHT);
_overlayTextureWriter.UpdateTexture();
}
private void UpgradeDevice()
@@ -147,33 +138,20 @@ namespace VRCX
}
public byte[] GetLatestWristOverlayFrame()
public byte[] GetLatestOverlayFrame()
{
if (_wristOverlayAccessor == null) return null;
byte ready = _wristOverlayAccessor.ReadByte(0);
if (_overlayAccessor == null) return null;
byte ready = _overlayAccessor.ReadByte(0);
if (ready == 1)
{
_wristOverlayAccessor.ReadArray(1, wristFrameBuffer, 0, WRIST_FRAME_SIZE);
_wristOverlayAccessor.Write(0, (byte)0); // reset flag
return wristFrameBuffer;
_overlayAccessor.ReadArray(1, frameBuffer, 0, SHARED_FRAME_SIZE);
_overlayAccessor.Write(0, (byte)0); // reset flag
return frameBuffer;
}
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)
private static void FlipImageVertically(ref byte[] imageData, int width, int height)
{
int stride = width * 4; // 4 bytes per pixel (RGBA)
byte[] tempRow = new byte[stride];
@@ -326,15 +304,10 @@ namespace VRCX
}
}
_wristOverlayAccessor?.Dispose();
_wristOverlayAccessor = null;
_wristOverlayMMF?.Dispose();
_wristOverlayMMF = null;
_hmdOverlayAccessor?.Dispose();
_hmdOverlayAccessor = null;
_hmdOverlayMMF?.Dispose();
_hmdOverlayMMF = null;
_overlayAccessor?.Dispose();
_overlayAccessor = null;
_overlayMMF?.Dispose();
_overlayMMF = null;
GLContextX11.Cleanup();
GLContextWayland.Cleanup();
@@ -352,35 +325,32 @@ namespace VRCX
{
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)
if (!_active || (!_hmdOverlayActive && !_wristOverlayActive))
{
_overlayAccessor?.Dispose();
_overlayAccessor = null;
_overlayMMF?.Dispose();
_overlayMMF = null;
GLContextX11.Cleanup();
GLContextWayland.Cleanup();
}
}
public override bool IsActive()
{
return _active;
}
public override void Refresh()
{
//_wristOverlay.Reload();
@@ -606,31 +576,6 @@ namespace VRCX
}
}
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
@@ -686,8 +631,8 @@ namespace VRCX
return err;
}
_wristOverlayMMF = MemoryMappedFile.CreateFromFile(WRIST_OVERLAY_SHM_PATH, FileMode.Open, null, WRIST_FRAME_SIZE + 1);
_wristOverlayAccessor = _wristOverlayMMF.CreateViewAccessor();
_overlayMMF = MemoryMappedFile.CreateFromFile(OVERLAY_SHM_PATH, FileMode.Open, null, SHARED_FRAME_SIZE + 1);
_overlayAccessor = _overlayMMF.CreateViewAccessor();
}
}
@@ -725,16 +670,24 @@ namespace VRCX
if (!dashboardVisible &&
DateTime.UtcNow.CompareTo(_nextOverlayUpdate) <= 0)
{
if (_wristOverlayTextureWriter != null)
if (_overlayTextureWriter != null)
{
byte[] imageData = GetLatestWristOverlayFrame();
byte[] imageData = GetLatestOverlayFrame();
if (imageData != null)
{
FlipImageVertically(imageData, WRIST_FRAME_WIDTH, WRIST_FRAME_HEIGHT);
_wristOverlayTextureWriter.WriteImageToBuffer(imageData);
_wristOverlayTextureWriter.UpdateTexture();
FlipImageVertically(ref imageData, SHARED_FRAME_WIDTH, SHARED_FRAME_HEIGHT);
_overlayTextureWriter.WriteImageToBuffer(imageData);
_overlayTextureWriter.UpdateTexture();
Texture_t texture = _wristOverlayTextureWriter.AsTextureT();
Texture_t texture = _overlayTextureWriter.AsTextureT();
var bounds = new VRTextureBounds_t
{
uMin = 0f,
uMax = 0.5f,
vMin = 0f,
vMax = (float)WRIST_FRAME_HEIGHT / SHARED_FRAME_HEIGHT
};
overlay.SetOverlayTextureBounds(overlayHandle, ref bounds);
err = OpenVR.Overlay.SetOverlayTexture(overlayHandle, ref texture);
if (err != EVROverlayError.None)
{
@@ -830,23 +783,31 @@ namespace VRCX
return err;
}
_hmdOverlayMMF = MemoryMappedFile.CreateFromFile(HMD_OVERLAY_SHM_PATH, FileMode.Open, null, HMD_FRAME_SIZE + 1);
_hmdOverlayAccessor = _hmdOverlayMMF.CreateViewAccessor();
_overlayMMF = MemoryMappedFile.CreateFromFile(OVERLAY_SHM_PATH, FileMode.Open, null, SHARED_FRAME_SIZE + 1);
_overlayAccessor = _overlayMMF.CreateViewAccessor();
}
}
if (!dashboardVisible)
{
if (_hmdOverlayTextureWriter != null)
if (_overlayTextureWriter != null)
{
byte[] imageData = GetLatestHmdOverlayFrame();
byte[] imageData = GetLatestOverlayFrame();
if (imageData != null)
{
FlipImageVertically(imageData, HMD_FRAME_WIDTH, HMD_FRAME_HEIGHT);
_hmdOverlayTextureWriter.WriteImageToBuffer(imageData);
_hmdOverlayTextureWriter.UpdateTexture();
FlipImageVertically(ref imageData, SHARED_FRAME_WIDTH, SHARED_FRAME_HEIGHT);
_overlayTextureWriter.WriteImageToBuffer(imageData);
_overlayTextureWriter.UpdateTexture();
Texture_t texture = _hmdOverlayTextureWriter.AsTextureT();
Texture_t texture = _overlayTextureWriter.AsTextureT();
var bounds = new VRTextureBounds_t
{
uMin = 0f,
uMax = 1f,
vMin = (float)(SHARED_FRAME_HEIGHT - HMD_FRAME_HEIGHT) / SHARED_FRAME_HEIGHT,
vMax = 1f
};
overlay.SetOverlayTextureBounds(overlayHandle, ref bounds);
err = OpenVR.Overlay.SetOverlayTexture(overlayHandle, ref texture);
if (err != EVROverlayError.None)
{
@@ -880,23 +841,9 @@ namespace VRCX
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;
return _overlayFunctionQueue;
}
public override void ExecuteVrOverlayFunction(string function, string json)
@@ -905,7 +852,7 @@ namespace VRCX
// if (_hmdOverlay.IsLoading)
// Restart();
_hmdFeedFunctionQueue.Enqueue(new KeyValuePair<string, string>(function, json));
_overlayFunctionQueue.Enqueue(new KeyValuePair<string, string>(function, json));
}
}
}

View File

@@ -1,8 +1,8 @@
namespace VRCX;
using System.Collections.Generic;
using System.Collections.Concurrent;
namespace VRCX;
public abstract class VRCXVRInterface
{
public bool IsHmdAfk;
@@ -11,9 +11,8 @@ public abstract class VRCXVRInterface
public abstract void Refresh();
public abstract void Restart();
public abstract void SetActive(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand);
public abstract bool IsActive();
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();
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using NLog;
namespace VRCX;
public class OverlayManager
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
private static Process _process;
public static void StartOverlay()
{
if (OverlayServer.IsConnected() ||
_process != null && !_process.HasExited)
{
logger.Error("Overlay server already started");
return;
}
StartOverlayProcess();
}
private static void StartOverlayProcess()
{
if (Environment.ProcessPath == null)
{
logger.Error("Cannot start Overlay process without a process path");
return;
}
var args = new List<string>();
args.Add(StartupArgs.VrcxLaunchArguments.Overlay);
if (Program.LaunchDebug)
args.Add(StartupArgs.VrcxLaunchArguments.IsDebugPrefix);
if (StartupArgs.LaunchArguments.ConfigDirectory != null)
args.Add($"{StartupArgs.VrcxLaunchArguments.ConfigDirectoryPrefix}={StartupArgs.LaunchArguments.ConfigDirectory}");
var startInfo = new ProcessStartInfo
{
FileName = Environment.ProcessPath,
Arguments = string.Join(' ', args),
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = Program.BaseDirectory
};
_process = Process.Start(startInfo);
logger.Info("Overlay process started");
}
}

View File

@@ -0,0 +1,17 @@
using System.Text.Json.Serialization;
namespace VRCX;
public class OverlayMessage
{
public OverlayMessageType Type { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? FunctionName { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Data { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public OverlayVars? OverlayVars { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace VRCX;
public enum OverlayMessageType
{
OverlayConnected,
JsFunctionCall,
UpdateVars,
IsHmdAfk
}

View File

@@ -0,0 +1,212 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using CefSharp;
using NLog;
namespace VRCX;
public class OverlayServer
{
public static OverlayServer Instance { get; private set; }
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
private static readonly Lock SendLock = new();
private static readonly ConcurrentDictionary<WebSocket, byte> ConnectedWebSockets = new();
private static CancellationTokenSource _cancellationToken;
private static OverlayVars _overlayVars;
static OverlayServer()
{
Instance = new OverlayServer();
}
public async Task Init()
{
if (_cancellationToken != null && _cancellationToken.IsCancellationRequested)
return;
_cancellationToken = new CancellationTokenSource();
try
{
var listener = new HttpListener();
listener.Prefixes.Add("http://127.0.0.1:34582/");
listener.Start();
logger.Info("Overlay IPC server started");
while (_cancellationToken != null && !_cancellationToken.IsCancellationRequested)
{
var listenerContext = await listener.GetContextAsync();
if (listenerContext.Request.IsWebSocketRequest)
{
ProcessRequest(listenerContext);
}
else
{
listenerContext.Response.StatusCode = 400;
listenerContext.Response.Close();
}
}
}
catch (Exception e)
{
logger.Error(e);
}
}
public async Task Exit()
{
if (_cancellationToken == null || _cancellationToken.IsCancellationRequested)
return;
foreach (var webSocket in ConnectedWebSockets.Keys)
{
if (webSocket == null || webSocket.State != WebSocketState.Open)
continue;
try
{
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Server shutting down", CancellationToken.None);
}
catch (Exception e)
{
logger.Error(e);
}
}
await _cancellationToken.CancelAsync();
ConnectedWebSockets.Clear();
_cancellationToken = null;
}
private async Task ProcessRequest(HttpListenerContext listenerContext)
{
WebSocketContext webSocketContext;
try
{
webSocketContext = await listenerContext.AcceptWebSocketAsync(null);
}
catch (Exception e)
{
listenerContext.Response.StatusCode = 500;
listenerContext.Response.Close();
logger.Error(e);
return;
}
var webSocket = webSocketContext.WebSocket;
try
{
ConnectedWebSockets.TryAdd(webSocket, 0);
logger.Info("Overlay IPC connected, count: {0}", ConnectedWebSockets.Count);
var receiveBuffer = new byte[1024 * 5];
while (webSocket.State == WebSocketState.Open)
{
var receiveResult = await webSocket.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), CancellationToken.None);
switch (receiveResult.MessageType)
{
case WebSocketMessageType.Text:
var text = Encoding.UTF8.GetString(receiveBuffer, 0, receiveResult.Count);
var message = JsonSerializer.Deserialize<OverlayMessage>(text);
HandleMessage(message);
continue;
case WebSocketMessageType.Close:
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
break;
case WebSocketMessageType.Binary:
default:
await webSocket.CloseAsync(WebSocketCloseStatus.InvalidMessageType, "Invalid message type", CancellationToken.None);
break;
}
}
}
catch (Exception e)
{
logger.Error(e);
}
finally
{
webSocket.Dispose();
ConnectedWebSockets.TryRemove(webSocket, out _);
logger.Info("Overlay IPC disconnected, count: {0}", ConnectedWebSockets.Count);
}
}
private async Task HandleMessage(OverlayMessage message)
{
logger.Trace($"Overlay IPC message received: {message.Type.ToString()}");
switch (message.Type)
{
case OverlayMessageType.OverlayConnected:
var helloMessage = new OverlayMessage
{
Type = OverlayMessageType.UpdateVars,
OverlayVars = _overlayVars
};
SendMessage(helloMessage);
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading &&
MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
MainForm.Instance.Browser.ExecuteScriptAsync("window?.$pinia?.vr.vrInit();");
break;
case OverlayMessageType.IsHmdAfk:
var isHmdAfk = string.Equals(message.Data, "true", StringComparison.OrdinalIgnoreCase);
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
MainForm.Instance.Browser.ExecuteScriptAsync("window?.$pinia?.game.updateIsHmdAfk", isHmdAfk);
break;
case OverlayMessageType.JsFunctionCall:
case OverlayMessageType.UpdateVars:
default:
throw new ArgumentOutOfRangeException();
}
}
public void SendMessage(OverlayMessage message)
{
lock (SendLock)
{
var buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(message)));
var connectedWebSockets = ConnectedWebSockets.Keys;
logger.Trace($"Sending message to overlay Clients: {connectedWebSockets.Count}, IPC: {message.Type.ToString()}");
foreach (var webSocket in connectedWebSockets)
{
if (webSocket == null || webSocket.State != WebSocketState.Open)
continue;
webSocket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);
}
}
}
public void UpdateVars(OverlayVars overlayVars)
{
_overlayVars = overlayVars;
if (!IsConnected() && overlayVars.Active)
{
OverlayManager.StartOverlay();
return;
}
var helloMessage = new OverlayMessage
{
Type = OverlayMessageType.UpdateVars,
OverlayVars = overlayVars
};
SendMessage(helloMessage);
}
public static bool IsConnected()
{
return ConnectedWebSockets.Keys.Any(webSocket => webSocket != null && webSocket.State == WebSocketState.Open);
}
}

View File

@@ -0,0 +1,10 @@
namespace VRCX;
public class OverlayVars
{
public bool Active { get; set; }
public bool HmdOverlay { get; set; }
public bool WristOverlay { get; set; }
public bool MenuButton { get; set; }
public int OverlayHand { get; set; }
}

View File

@@ -8,6 +8,7 @@ using System.Text.Json;
using System.Threading;
#if !LINUX
using System.Windows.Forms;
using VRCX.Overlay;
#endif
namespace VRCX
@@ -20,9 +21,7 @@ namespace VRCX
public static string Version { get; private set; }
public static bool LaunchDebug;
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
public static VRCXVRInterface VRCXVRInstance { get; set; }
public static AppApi AppApiInstance { get; private set; }
public static AppApiVr AppApiVrInstance { get; private set; }
private static void SetProgramDirectories()
{
@@ -86,11 +85,15 @@ namespace VRCX
private static void ConfigureLogger()
{
var fileName = Path.Join(AppDataDirectory, "logs", "VRCX.log");
if (StartupArgs.LaunchArguments.IsOverlay)
fileName = Path.Join(AppDataDirectory, "logs", "VRCX.Overlay.log");
LogManager.Setup().LoadConfiguration(builder =>
{
var fileTarget = new FileTarget("fileTarget")
{
FileName = Path.Join(AppDataDirectory, "logs", "VRCX.log"),
FileName = fileName,
//Layout = "${longdate} [${level:uppercase=true}] ${logger} - ${message} ${exception:format=tostring}",
// Layout with padding between the level/logger and message so that the message always starts at the same column
Layout =
@@ -183,7 +186,7 @@ namespace VRCX
AppApiInstance.OpenLink("https://github.com/vrcx-team/VRCX/wiki#how-to-repair-vrcx-database");
}
}
#endregion
catch (Exception e)
@@ -215,6 +218,9 @@ namespace VRCX
VRCXStorage.Instance.Load();
ConfigureLogger();
GetVersion();
if (StartupArgs.LaunchArguments.IsOverlay)
OverlayProgram.OverlayMain();
Update.Check();
Application.EnableVisualStyles();
@@ -225,43 +231,36 @@ namespace VRCX
if (!string.IsNullOrEmpty(StartupArgs.LaunchArguments.LaunchCommand))
logger.Info("Launch Command: {0}", StartupArgs.LaunchArguments.LaunchCommand);
logger.Debug("Wine detection: {0}", Wine.GetIfWine());
IPCServer.Instance.Init();
SQLite.Instance.Init();
AppApiInstance = new AppApiCef();
AppApiVrInstance = new AppApiVrCef();
AppApiVrInstance.Init();
ProcessMonitor.Instance.Init();
Discord.Instance.Init();
WebApi.Instance.Init();
LogWatcher.Instance.Init();
AutoAppLaunchManager.Instance.Init();
CefService.Instance.Init();
IPCServer.Instance.Init();
if (VRCXStorage.Instance.Get("VRCX_DisableVrOverlayGpuAcceleration") == "true")
VRCXVRInstance = new VRCXVRLegacy();
else
VRCXVRInstance = new VRCXVRCef();
VRCXVRInstance.Init();
OverlayServer.Instance.Init();
Application.Run(new MainForm());
logger.Info("{0} Exiting...", Version);
WebApi.Instance.SaveCookies();
VRCXVRInstance.Exit();
OverlayServer.Instance.Exit();
CefService.Instance.Exit();
AutoAppLaunchManager.Instance.Exit();
LogWatcher.Instance.Exit();
WebApi.Instance.Exit();
Discord.Instance.Exit();
SystemMonitorCef.Instance.Exit();
VRCXStorage.Instance.Save();
SQLite.Instance.Exit();
ProcessMonitor.Instance.Exit();
}
#else
public static VRCXVRInterface VRCXVRInstance;
public static void PreInit(string version, string[] args)
{
Version = version;
@@ -280,7 +279,7 @@ namespace VRCX
logger.Info("Launch Command: {0}", StartupArgs.LaunchArguments.LaunchCommand);
AppApiInstance = new AppApiElectron();
VRCXVRInstance = new VRCXVRElectron();
VRCXVRInstance.Init();
}

View File

@@ -49,7 +49,7 @@ namespace VRCX
#if !LINUX
var disableClosing = LaunchArguments.IsUpgrade || // we're upgrading, allow it
!string.IsNullOrEmpty(CommandLineArgsParser.GetArgumentValue(args, CefSharpArguments.SubProcessTypeArgument)); // we're launching a subprocess, allow it
!string.IsNullOrEmpty(CommandLineArgsParser.GetArgumentValue(args, CefSharpArguments.SubProcessTypeArgument)); // we're launching a subprocess, allow it
// if we're launching a second instance with same config directory, focus the first instance then exit
if (!disableClosing && IsDuplicateProcessRunning(LaunchArguments))
@@ -74,6 +74,9 @@ namespace VRCX
if (arg.StartsWith(VrcxLaunchArguments.IsDebugPrefix))
arguments.IsDebug = true;
if (arg == VrcxLaunchArguments.Overlay)
arguments.IsOverlay = true;
if (arg.StartsWith(VrcxLaunchArguments.LaunchCommandPrefix) && arg.Length > VrcxLaunchArguments.LaunchCommandPrefix.Length)
arguments.LaunchCommand = arg.Substring(VrcxLaunchArguments.LaunchCommandPrefix.Length);
@@ -100,6 +103,9 @@ namespace VRCX
public const string IsDebugPrefix = "--debug";
public bool IsDebug { get; set; } = false;
public const string Overlay = "--overlay";
public bool IsOverlay { get; set; } = false;
public const string LaunchCommandPrefix = "/uri=vrcx://";
public const string LinuxLaunchCommandPrefix = "vrcx://";
@@ -138,6 +144,19 @@ namespace VRCX
if (commandLine.Contains(SubProcessTypeArgument)) // ignore subprocesses
continue;
if (launchArguments.IsOverlay)
{
if (commandLine.Contains(VrcxLaunchArguments.Overlay))
{
Console.WriteLine(@"Another overlay instance is already running. Exiting this instance.");
Environment.Exit(0);
}
continue; // we are an overlay, ignore non-overlay instances
}
if (commandLine.Contains(VrcxLaunchArguments.Overlay))
continue; // we aren't an overlay, ignore overlay instances
var processArguments = ParseArgs(commandLine.Split(' '));
if (processArguments.ConfigDirectory == launchArguments.ConfigDirectory)
{

View File

@@ -107,10 +107,7 @@
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
<PackageReference Include="SourceGear.sqlite3" Version="3.50.4.5" />
<PackageReference Include="System.Data.SQLite" Version="2.0.2" />
<PackageReference Include="System.Drawing.Common" Version="10.0.1" />
<PackageReference Include="System.Management" Version="10.0.1" />
<PackageReference Include="System.Text.Json" Version="10.0.1" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="Websocket.Client" Version="5.3.0" />
</ItemGroup>
@@ -125,6 +122,10 @@
<ItemGroup>
<Content Remove="obj1\**" />
<Compile Remove="obj1\**" />
<Content Remove="AppApi\Electron\**" />
<Compile Remove="AppApi\Electron\**" />
<Content Remove="Overlay\Electron\**" />
<Compile Remove="Overlay\Electron\**" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">

View File

@@ -81,10 +81,7 @@
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
<PackageReference Include="SourceGear.sqlite3" Version="3.50.4.5" />
<PackageReference Include="System.Data.SQLite" Version="2.0.2" />
<PackageReference Include="System.Drawing.Common" Version="10.0.1" />
<PackageReference Include="System.Management" Version="10.0.1" />
<PackageReference Include="System.Text.Json" Version="10.0.1" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="Websocket.Client" Version="5.3.0" />
</ItemGroup>
@@ -97,6 +94,8 @@
<Compile Remove="AppApi\Cef\**" />
<Content Remove="Overlay\Cef\**" />
<Compile Remove="Overlay\Cef\**" />
<Content Remove="OverlayWebSocket\**" />
<Compile Remove="OverlayWebSocket\**" />
<Content Remove="DBMerger\**" />
<Compile Remove="DBMerger\**" />
</ItemGroup>

View File

@@ -86,10 +86,7 @@
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
<PackageReference Include="SourceGear.sqlite3" Version="3.50.4.5" />
<PackageReference Include="System.Data.SQLite" Version="1.0.119" /> <!--DO NOT UPGRADE-->
<PackageReference Include="System.Drawing.Common" Version="10.0.1" />
<PackageReference Include="System.Management" Version="10.0.1" />
<PackageReference Include="System.Text.Json" Version="10.0.1" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="Websocket.Client" Version="5.3.0" />
</ItemGroup>
@@ -102,6 +99,8 @@
<Compile Remove="AppApi\Cef\**" />
<Content Remove="Overlay\Cef\**" />
<Compile Remove="Overlay\Cef\**" />
<Content Remove="OverlayWebSocket\**" />
<Compile Remove="OverlayWebSocket\**" />
<Content Remove="DBMerger\**" />
<Compile Remove="DBMerger\**" />
</ItemGroup>

View File

@@ -84,22 +84,22 @@ if (fs.existsSync(armPath)) {
const InteropApi = require('./InteropApi');
const interopApi = new InteropApi();
const WRIST_FRAME_WIDTH = 512;
const WRIST_FRAME_HEIGHT = 512;
const WRIST_FRAME_SIZE = WRIST_FRAME_WIDTH * WRIST_FRAME_HEIGHT * 4;
const WRIST_SHM_PATH = '/dev/shm/vrcx_wrist_overlay';
const OVERLAY_WRIST_FRAME_WIDTH = 512;
const OVERLAY_WRIST_FRAME_HEIGHT = 512;
const OVERLAY_HMD_FRAME_WIDTH = 1024;
const OVERLAY_HMD_FRAME_HEIGHT = 1024;
const OVERLAY_SHARED_HEIGHT =
OVERLAY_WRIST_FRAME_HEIGHT + OVERLAY_HMD_FRAME_HEIGHT;
const OVERLAY_SHARED_WIDTH = Math.max(
OVERLAY_WRIST_FRAME_WIDTH,
OVERLAY_HMD_FRAME_WIDTH
);
const OVERLAY_FRAME_SIZE =
OVERLAY_SHARED_WIDTH * OVERLAY_SHARED_HEIGHT * 4;
const OVERLAY_SHM_PATH = '/dev/shm/vrcx_overlay';
function createWristOverlayWindowShm() {
fs.writeFileSync(WRIST_SHM_PATH, Buffer.alloc(WRIST_FRAME_SIZE + 1));
}
const HMD_FRAME_WIDTH = 1024;
const HMD_FRAME_HEIGHT = 1024;
const HMD_FRAME_SIZE = HMD_FRAME_WIDTH * HMD_FRAME_HEIGHT * 4;
const HMD_SHM_PATH = '/dev/shm/vrcx_hmd_overlay';
function createHmdOverlayWindowShm() {
fs.writeFileSync(HMD_SHM_PATH, Buffer.alloc(HMD_FRAME_SIZE + 1));
function createOverlayWindowShm() {
fs.writeFileSync(OVERLAY_SHM_PATH, Buffer.alloc(OVERLAY_FRAME_SIZE + 1));
}
const version = getVersion();
@@ -218,21 +218,11 @@ ipcMain.handle('app:restart', () => {
}
});
ipcMain.handle('app:getWristOverlayWindow', () => {
if (wristOverlayWindow && wristOverlayWindow.webContents) {
ipcMain.handle('app:getOverlayWindow', () => {
if (overlayWindow && overlayWindow.webContents) {
return (
!wristOverlayWindow.webContents.isLoading() &&
wristOverlayWindow.webContents.isPainting()
);
}
return false;
});
ipcMain.handle('app:getHmdOverlayWindow', () => {
if (hmdOverlayWindow && hmdOverlayWindow.webContents) {
return (
!hmdOverlayWindow.webContents.isLoading() &&
hmdOverlayWindow.webContents.isPainting()
!overlayWindow.webContents.isLoading() &&
overlayWindow.webContents.isPainting()
);
}
return false;
@@ -241,22 +231,16 @@ ipcMain.handle('app:getHmdOverlayWindow', () => {
ipcMain.handle(
'app:updateVr',
(event, active, hmdOverlay, wristOverlay, menuButton, overlayHand) => {
if (!active) {
if (!active || (!hmdOverlay && !wristOverlay)) {
disposeOverlay();
return;
}
isOverlayActive = true;
if (!hmdOverlay) {
destroyHmdOverlayWindow();
} else if (active && !hmdOverlayWindow) {
createHmdOverlayWindowOffscreen();
}
if (!wristOverlay) {
destroyWristOverlayWindow();
} else if (active && !wristOverlayWindow) {
createWristOverlayWindowOffscreen();
if (active && !overlayWindow) {
try {
createOverlayWindowOffscreen();
} catch (err) {
console.error('Error creating overlay windows:', err);
}
}
}
);
@@ -412,19 +396,24 @@ function createWindow() {
});
}
let wristOverlayWindow = undefined;
let overlayWindow = undefined;
function createWristOverlayWindowOffscreen() {
if (!fs.existsSync(WRIST_SHM_PATH)) {
createWristOverlayWindowShm();
function createOverlayWindowOffscreen() {
if (process.platform !== 'linux') {
console.error('Offscreen overlay is only supported on Linux.');
return;
}
isOverlayActive = true;
if (!fs.existsSync(OVERLAY_SHM_PATH)) {
createOverlayWindowShm();
}
const x = parseInt(VRCXStorage.Get('VRCX_LocationX')) || 0;
const y = parseInt(VRCXStorage.Get('VRCX_LocationY')) || 0;
const width = WRIST_FRAME_WIDTH;
const height = WRIST_FRAME_HEIGHT;
const width = OVERLAY_SHARED_WIDTH;
const height = OVERLAY_SHARED_HEIGHT;
wristOverlayWindow = new BrowserWindow({
overlayWindow = new BrowserWindow({
x,
y,
width,
@@ -440,112 +429,36 @@ function createWristOverlayWindowOffscreen() {
preload: path.join(__dirname, 'preload.js')
}
});
wristOverlayWindow.webContents.setFrameRate(2);
overlayWindow.webContents.setFrameRate(48);
let indexPath = path.join(rootDir, 'build/html/vr.html');
let fileUrl = `file://${path.join(rootDir, 'build/html/vr.html')}`;
if (debug) {
indexPath = 'http://localhost:9000/vr.html';
fileUrl = 'http://localhost:9000/vr.html';
}
const fileUrl = `file://${indexPath}?wrist`;
wristOverlayWindow.loadURL(fileUrl, { userAgent: version });
overlayWindow.loadURL(fileUrl, { userAgent: version });
// Use paint event for offscreen rendering
wristOverlayWindow.webContents.on('paint', (event, dirty, image) => {
overlayWindow.webContents.on('paint', (event, dirty, image) => {
const buffer = image.toBitmap();
//console.log('Captured wrist frame via paint event, size:', buffer.length);
writeWristFrame(buffer);
//console.log('Captured frame via paint event, size:', buffer.length);
writeOverlayFrame(buffer);
});
}
function writeWristFrame(imageBuffer) {
function writeOverlayFrame(imageBuffer) {
try {
const fd = fs.openSync(WRIST_SHM_PATH, 'r+');
const buffer = Buffer.alloc(WRIST_FRAME_SIZE + 1);
const fd = fs.openSync(OVERLAY_SHM_PATH, 'r+');
const buffer = Buffer.alloc(OVERLAY_FRAME_SIZE + 1);
buffer[0] = 0; // not ready
imageBuffer.copy(buffer, 1, 0, WRIST_FRAME_SIZE);
imageBuffer.copy(buffer, 1, 0, OVERLAY_FRAME_SIZE);
buffer[0] = 1; // ready
fs.writeSync(fd, buffer);
fs.closeSync(fd);
//console.log('Wrote wrist frame to shared memory');
//console.log('Wrote frame to shared memory');
} catch (err) {
console.error('Error writing wrist frame to shared memory:', err);
console.error('Error writing frame to shared memory:', err);
}
}
function destroyWristOverlayWindow() {
if (wristOverlayWindow && !wristOverlayWindow.isDestroyed()) {
wristOverlayWindow.close();
}
wristOverlayWindow = undefined;
}
let hmdOverlayWindow = undefined;
function createHmdOverlayWindowOffscreen() {
if (!fs.existsSync(HMD_SHM_PATH)) {
createHmdOverlayWindowShm();
}
const x = parseInt(VRCXStorage.Get('VRCX_LocationX')) || 0;
const y = parseInt(VRCXStorage.Get('VRCX_LocationY')) || 0;
const width = HMD_FRAME_WIDTH;
const height = HMD_FRAME_HEIGHT;
hmdOverlayWindow = new BrowserWindow({
x,
y,
width,
height,
icon: path.join(rootDir, 'images/VRCX.png'),
autoHideMenuBar: true,
transparent: true,
frame: false,
show: false,
webPreferences: {
partition: 'vrcx-vr-overlay',
offscreen: true,
preload: path.join(__dirname, 'preload.js')
}
});
hmdOverlayWindow.webContents.setFrameRate(48);
let indexPath = path.join(rootDir, 'build/html/vr.html');
if (debug) {
indexPath = 'http://localhost:9000/vr.html';
}
const fileUrl = `file://${indexPath}?hmd`;
hmdOverlayWindow.loadURL(fileUrl, { userAgent: version });
// Use paint event for offscreen rendering
hmdOverlayWindow.webContents.on('paint', (event, dirty, image) => {
const buffer = image.toBitmap();
//console.log('Captured HMD frame via paint event, size:', buffer.length);
writeHmdFrame(buffer);
});
}
function writeHmdFrame(imageBuffer) {
try {
const fd = fs.openSync(HMD_SHM_PATH, 'r+');
const buffer = Buffer.alloc(HMD_FRAME_SIZE + 1);
buffer[0] = 0; // not ready
imageBuffer.copy(buffer, 1, 0, HMD_FRAME_SIZE);
buffer[0] = 1; // ready
fs.writeSync(fd, buffer);
fs.closeSync(fd);
//console.log('Wrote HMD frame to shared memory');
} catch (err) {
console.error('Error writing HMD frame to shared memory:', err);
}
}
function destroyHmdOverlayWindow() {
if (hmdOverlayWindow && !hmdOverlayWindow.isDestroyed()) {
hmdOverlayWindow.close();
}
hmdOverlayWindow = undefined;
}
let tray = null;
let trayIcon = null;
let trayIconNotify = null;
@@ -931,16 +844,6 @@ function applyWindowState() {
app.whenReady().then(() => {
createWindow();
createTray();
if (process.platform === 'linux') {
try {
createWristOverlayWindowOffscreen();
createHmdOverlayWindowOffscreen();
} catch (err) {
console.error('Error creating overlay windows:', err);
}
}
installVRCX();
app.on('activate', function () {
@@ -959,21 +862,13 @@ function disposeOverlay() {
if (!isOverlayActive) {
return;
}
if (overlayWindow && !overlayWindow.isDestroyed()) {
overlayWindow.close();
}
overlayWindow = undefined;
isOverlayActive = false;
if (wristOverlayWindow) {
wristOverlayWindow.close();
wristOverlayWindow = undefined;
}
if (hmdOverlayWindow) {
hmdOverlayWindow.close();
hmdOverlayWindow = undefined;
}
if (fs.existsSync(WRIST_SHM_PATH)) {
fs.unlinkSync(WRIST_SHM_PATH);
}
if (fs.existsSync(HMD_SHM_PATH)) {
fs.unlinkSync(HMD_SHM_PATH);
if (fs.existsSync(OVERLAY_SHM_PATH)) {
fs.unlinkSync(OVERLAY_SHM_PATH);
}
}

View File

@@ -39,9 +39,7 @@ contextBridge.exposeInMainWorld('electron', {
desktopNotification: (title, body, icon) =>
ipcRenderer.invoke('notification:showNotification', title, body, icon),
restartApp: () => ipcRenderer.invoke('app:restart'),
getWristOverlayWindow: () =>
ipcRenderer.invoke('app:getWristOverlayWindow'),
getHmdOverlayWindow: () => ipcRenderer.invoke('app:getHmdOverlayWindow'),
getOverlayWindow: () => ipcRenderer.invoke('app:getOverlayWindow'),
updateVr: (active, hmdOverlay, wristOverlay, menuButton, overlayHand) =>
ipcRenderer.invoke(
'app:updateVr',

View File

@@ -853,11 +853,11 @@ export const useFriendStore = defineStore('Friend', () => {
reconnectWebSocket();
}
function updateOnlineFriendCounter() {
function updateOnlineFriendCounter(forceUpdate = false) {
const onlineFriendCounts =
vipFriends.value.length + onlineFriends.value.length;
if (onlineFriendCounts !== onlineFriendCount.value) {
AppApi.ExecuteVrFeedFunction(
if (onlineFriendCounts !== onlineFriendCount.value || forceUpdate) {
AppApi.ExecuteVrOverlayFunction(
'updateOnlineFriendCount',
`${onlineFriendCounts}`
);

View File

@@ -146,11 +146,7 @@ export const useGameStore = defineStore('Game', () => {
}
// use in C#
async function updateIsGameRunning(
isGameRunningArg,
isSteamVRRunningArg,
isHmdAfkArg
) {
async function updateIsGameRunning(isGameRunningArg, isSteamVRRunningArg) {
const avatarStore = useAvatarStore();
if (advancedSettingsStore.gameLogDisabled) {
return;
@@ -186,11 +182,15 @@ export const useGameStore = defineStore('Game', () => {
isSteamVRRunning.value = isSteamVRRunningArg;
console.log('isSteamVRRunning:', isSteamVRRunningArg);
}
vrStore.updateOpenVR();
}
// use in C#
function updateIsHmdAfk(isHmdAfkArg) {
if (isHmdAfkArg !== isHmdAfk.value) {
isHmdAfk.value = isHmdAfkArg;
console.log('isHmdAfk:', isHmdAfkArg);
console.log('isHmdAfk', isHmdAfkArg);
}
vrStore.updateOpenVR();
}
async function checkVRChatDebugLogging() {
@@ -258,6 +258,7 @@ export const useGameStore = defineStore('Game', () => {
getVRChatCacheSize,
updateIsGameRunning,
getVRChatRegistryKey,
checkVRChatDebugLogging
checkVRChatDebugLogging,
updateIsHmdAfk
};
});

View File

@@ -233,7 +233,7 @@ export const useSharedFeedStore = defineStore('SharedFeed', () => {
}
}
}
AppApi.ExecuteVrFeedFunction(
AppApi.ExecuteVrOverlayFunction(
'wristFeedUpdate',
JSON.stringify(wristFeed)
);

View File

@@ -131,8 +131,7 @@ export const useUpdateLoopStore = defineStore('UpdateLoop', () => {
state.nextGameRunningCheck = 1;
gameStore.updateIsGameRunning(
await AppApi.IsGameRunning(),
await AppApi.IsSteamVRRunning(),
false
await AppApi.IsSteamVRRunning()
);
vrStore.vrInit(); // TODO: make this event based
}

View File

@@ -45,8 +45,7 @@ export const useVrStore = defineStore('Vr', () => {
updateVrNowPlaying();
// run these methods again to send data to the overlay
sharedFeedStore.updateSharedFeed(true);
friendStore.onlineFriendCount = 0; // force an update
friendStore.updateOnlineFriendCounter();
friendStore.updateOnlineFriendCounter(true); // force an update
}
async function saveOpenVROption() {
@@ -59,7 +58,6 @@ export const useVrStore = defineStore('Vr', () => {
function updateVrNowPlaying() {
const json = JSON.stringify(gameLogStore.nowPlaying);
AppApi.ExecuteVrFeedFunction('nowPlayingUpdate', json);
AppApi.ExecuteVrOverlayFunction('nowPlayingUpdate', json);
}
@@ -91,7 +89,6 @@ export const useVrStore = defineStore('Vr', () => {
onlineFor
};
const json = JSON.stringify(lastLocation);
AppApi.ExecuteVrFeedFunction('lastLocationUpdate', json);
AppApi.ExecuteVrOverlayFunction('lastLocationUpdate', json);
}
@@ -142,7 +139,6 @@ export const useVrStore = defineStore('Vr', () => {
/** @type {string} */
const json = JSON.stringify(VRConfigVars);
AppApi.ExecuteVrFeedFunction('configUpdate', json);
AppApi.ExecuteVrOverlayFunction('configUpdate', json);
}
@@ -185,6 +181,9 @@ export const useVrStore = defineStore('Vr', () => {
newState.menuButton,
newState.overlayHand
);
if (!newState.active) {
gameStore.updateIsHmdAfk(false);
}
if (LINUX) {
window.electron.updateVr(

View File

@@ -65,8 +65,7 @@ declare global {
) => void;
onBrowserFocus: (Function: (event: any) => void) => void;
restartApp: () => Promise<void>;
getWristOverlayWindow: () => Promise<boolean>;
getHmdOverlayWindow: () => Promise<boolean>;
getOverlayWindow: () => Promise<boolean>;
updateVr: (
active: bool,
hmdOverlay: bool,
@@ -168,8 +167,6 @@ declare global {
menuButton: boolean,
overlayHand: number
): Promise<void>;
RefreshVR(): Promise<void>;
RestartVR(): Promise<void>;
SetZoom(zoomLevel: number): Promise<void>;
GetZoom(): Promise<number>;
DesktopNotification(
@@ -179,7 +176,6 @@ declare global {
): Promise<void>;
RestartApplication(isUpgrade: boolean): Promise<void>;
CheckForUpdateExe(): Promise<boolean>;
ExecuteVrFeedFunction(key: string, json: string): Promise<void>;
ExecuteVrOverlayFunction(key: string, json: string): Promise<void>;
FocusWindow(): Promise<void>;
ChangeTheme(value: number): Promise<void>;
@@ -367,7 +363,6 @@ declare global {
GetUptime(): Promise<number>;
CurrentCulture(): Promise<string>;
CustomVrScript(): Promise<string>;
GetExecuteVrFeedFunctionQueue(): Promise<Map<string, string>>;
GetExecuteVrOverlayFunctionQueue(): Promise<Map<string, string>>;
};

View File

@@ -1,9 +1,6 @@
<template>
<div
id="x-app"
class="x-app x-app-type"
:class="{ background: appType === 'wrist' && config && config.backgroundEnabled }">
<template v-if="appType === 'wrist' && !vrState.isWristDisabled">
<div id="x-app" class="x-app x-app-type">
<div class="wrist" :class="{ background: config && config.backgroundEnabled }">
<div class="x-container" style="flex: 1">
<div class="x-friend-list" ref="list" style="color: #aaa">
<template v-if="config && config.minimalFeed">
@@ -1283,9 +1280,9 @@
<span style="display: inline-block">{{ t('vr.status.online') }} {{ onlineFriendCount }}</span>
<span style="display: inline-block; margin-left: 5px">{{ customInfo }}</span>
</div>
</template>
</div>
<!-- HMD Overlay -->
<template v-else-if="appType === 'hmd' && !vrState.isHmdDisabled">
<div class="hmd">
<svg class="np-progress-circle">
<circle
class="np-progress-circle-stroke"
@@ -1406,7 +1403,7 @@
d="M102.9,75l11.3-11.3c10.3-10.3,11.5-26.1,3.8-37.8l17.4-17.4L126.9,0l-17.4,17.4C97.9,9.7,82,11,71.8,21.2L60.5,32.5C102,74,60.8,32.9,102.9,75z"></path>
</svg>
</div>
</template>
</div>
</div>
</template>
@@ -1437,7 +1434,6 @@
const { t, locale } = useI18n();
const vrState = reactive({
appType: new URLSearchParams(window.location.search).has('wrist') ? 'wrist' : 'hmd',
appLanguage: 'en',
currentCulture: 'en-gb',
currentTime: new Date().toJSON(),
@@ -1513,10 +1509,8 @@
if (LINUX) {
updateVrElectronLoop();
}
if (vrState.appType === 'wrist') {
refreshCustomScript();
updateStatsLoop();
}
refreshCustomScript();
updateStatsLoop();
setDatetimeFormat();
nextTick(() => {
@@ -1614,25 +1608,17 @@
function nowPlayingUpdate(json) {
vrState.nowPlaying = JSON.parse(json);
if (vrState.appType === 'hmd') {
const circle = /** @type {SVGCircleElement | null} */ (
document.querySelector('.np-progress-circle-stroke')
);
const circle = /** @type {SVGCircleElement} */ (document.querySelector('.np-progress-circle-stroke'));
if (!circle) {
return;
}
if (vrState.lastLocation.progressPie && vrState.nowPlaying.percentage !== 0) {
circle.style.opacity = (0.5).toString();
const circumference = circle.getTotalLength();
circle.style.strokeDashoffset = (
circumference -
(vrState.nowPlaying.percentage / 100) * circumference
).toString();
} else {
circle.style.opacity = '0';
}
if (vrState.lastLocation.progressPie && vrState.nowPlaying.percentage !== 0) {
circle.style.opacity = (0.5).toString();
const circumference = circle.getTotalLength();
circle.style.strokeDashoffset = (
circumference -
(vrState.nowPlaying.percentage / 100) * circumference
).toString();
} else {
circle.style.opacity = '0';
}
updateFeedLength();
}
@@ -1647,7 +1633,7 @@
}
function updateFeedLength() {
if (vrState.appType === 'hmd' || vrState.wristFeed.length === 0) {
if (vrState.wristFeed.length === 0) {
return;
}
let length = 16;
@@ -1787,36 +1773,19 @@
async function updateVrElectronLoop() {
try {
if (vrState.appType === 'wrist') {
const wristOverlayQueue = await AppApiVr.GetExecuteVrFeedFunctionQueue();
if (wristOverlayQueue) {
wristOverlayQueue.forEach((item) => {
// item[0] is the function name, item[1] is already an object
const fullFunctionName = item[0];
const jsonArg = item[1];
const overlayQueue = await AppApiVr.GetExecuteVrOverlayFunctionQueue();
if (overlayQueue) {
overlayQueue.forEach((item) => {
// item[0] is the function name, item[1] is already an object
const fullFunctionName = item[0];
const jsonArg = item[1];
if (typeof window.$vr === 'object' && typeof window.$vr[fullFunctionName] === 'function') {
window.$vr[fullFunctionName](jsonArg);
} else {
console.error(`$vr.${fullFunctionName} is not defined or is not a function`);
}
});
}
} else {
const hmdOverlayQueue = await AppApiVr.GetExecuteVrOverlayFunctionQueue();
if (hmdOverlayQueue) {
hmdOverlayQueue.forEach((item) => {
// item[0] is the function name, item[1] is already an object
const fullFunctionName = item[0];
const jsonArg = item[1];
if (typeof window.$vr === 'object' && typeof window.$vr[fullFunctionName] === 'function') {
window.$vr[fullFunctionName](jsonArg);
} else {
console.error(`$vr.${fullFunctionName} is not defined or is not a function`);
}
});
}
if (typeof window.$vr === 'object' && typeof window.$vr[fullFunctionName] === 'function') {
window.$vr[fullFunctionName](jsonArg);
} else {
console.error(`$vr.${fullFunctionName} is not defined or is not a function`);
}
});
}
} catch (err) {
console.error(err);
@@ -2126,7 +2095,6 @@
}
const {
appType,
config,
wristFeed,
devices,

View File

@@ -240,6 +240,30 @@ button {
.x-app-type {
color: #fff;
width: 1024px;
height: 1536px;
position: relative;
overflow: hidden;
}
.wrist {
position: absolute;
top: 0px;
left: 0;
width: 512px;
height: 512px;
z-index: 20;
display: flex;
flex-direction: column;
}
.hmd {
position: absolute;
top: 512px;
left: 0;
width: 1024px;
height: 1024px;
z-index: 10;
}
.background {