Skip to content

Commit

Permalink
Add SteamVR Dashboard Overlay:
Browse files Browse the repository at this point in the history
- The application starts as a desktop window. If SteamVR is running, it will also start as a dashboard overlay, which is effectively a second window independent from the first.
  - In CustomImGuiController, add handling for multiple contexts.
  - Change the execution loop so that we can periodically check if SteamVR is running.
    - The loops are going to need some proper cleaning.
  - Move the VRC Session to the Routine, as both windows will now need to share the same login state.
  - The Windowless class is no longer used.
  - By default, the application now builds with OpenVR support.
  - Remove previous, now obsolete launch options.
  - Add `--no-overlay` launch option to start without supporting Dashboard overlay.
- Handle Dashboard Overlays:
  - Parameter tables now show the key as the last column, as the screen is too wide to see both the key and the value at once.
  - In the input snapshot, add handling for non-square windows. In practice, this is not used, as it was switched back to square windows mid-development.
  - In the costumes tab, don't show the login screen in VR, as we can't write to the login fields from within VR.
- Add a logout button.
- Improve the version number on local runs. This is sent as part of the user agent to comply with VRChat API requirements.
- Commit code related to cookie saving, but it is not enabled as this functionality isn't working yet.
  • Loading branch information
hai-vr committed Sep 8, 2024
1 parent 6fa9b07 commit 8c33dfd
Show file tree
Hide file tree
Showing 20 changed files with 901 additions and 368 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.idea
h-view/bin
h-view/obj
h-view/obj
**/hview.cookies.txt
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,25 @@ This repository is mostly a personal learning project and has three main functio
I made specifically for this purpose ([learn more](https://docs.hai-vr.dev/docs/products/h-view)).
- In addition to the desktop window, it also has an implementation of the ImGui.NET window [being rendered into a SteamVR overlay](https://github.com/hai-vr/h-view/commit/cb1b35057a2f3ced0becdf9f013ef11b3de78291)
using the OpenVR API and Veldrid, and some [basic overlay mouse input](https://github.com/hai-vr/h-view/commit/697f7e61808f3b857940bcd24be05e67b9d3f774).
- If you choose to log-in into the VRChat account, it can switch between avatars. The code responsible for all VRChat account actions [can be inspected here](https://github.com/hai-vr/h-view/blob/main/h-view/src/VRCLogin/HVVrcSession.cs).
- Logging into the VRChat account `Login(username, password)` ([API docs](https://vrchatapi.github.io/docs/api/#get-/auth/user))
- Sending a 2FA code to VRChat `VerifyTwofer(code, method)` ([API docs](https://vrchatapi.github.io/docs/api/#post-/auth/twofactorauth/emailotp/verify))
- Switching avatars `SelectAvatar(avatarId)` ([API docs](https://vrchatapi.github.io/docs/api/#put-/avatars/-avatarId-/select))
- If you choose to log-in into the VRChat account, it can switch between avatars.
- The code responsible for all VRChat account actions [can be inspected here](https://github.com/hai-vr/h-view/blob/main/h-view/src/VRCLogin/HVVrcSession.cs).
- Logging into the VRChat account `Login(username, password)` ([API docs](https://vrchatapi.github.io/docs/api/#get-/auth/user))
- Sending a 2FA code to VRChat `VerifyTwofer(code, method)` ([API docs](https://vrchatapi.github.io/docs/api/#post-/auth/twofactorauth/emailotp/verify))
- Logging out `Logout()` ([API docs](https://vrchatapi.github.io/docs/api/#put-/logout))
- Switching avatars `SelectAvatar(avatarId)` ([API docs](https://vrchatapi.github.io/docs/api/#put-/avatars/-avatarId-/select))
- Logging in will save a cookie file in the program folder, called `hview.cookies.txt`
- This file is used to communicate with your VRChat account. Do not share that file.
- This cookie file will be loaded when you start the program.
- To delete this cookie file, go to Costumes > Login > Logout.

For more information, [open the website page](https://docs.hai-vr.dev/docs/products/h-view).

https://github.com/user-attachments/assets/889a2648-7cda-4cba-bb0b-23cf1c96ddaf

### Launch options

- *No option specified:* Starts as a desktop window.
- `--overlay` Starts as a SteamVR overlay app. The build config must be set to "Debug OVERLAY" or "Release OVERLAY".
- `--simulate-windowless` Starts as a desktop window, but stylized as being the overlay version.
- *No option specified:* Starts as a desktop window. If SteamVR is running, it also creates an additional dashboard overlay.
- `--no-overlay` Starts as a desktop window.

### Third-party acknowledgements

Expand Down
57 changes: 39 additions & 18 deletions h-view/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@
using Hai.HView.Core;
using Hai.HView.Gui;
using Hai.HView.OSC;
using Hai.HView.OVR;

var isSteamVROverlay = false;
#if INCLUDES_OVERLAY
isSteamVROverlay = args.Contains("--overlay");
#if INCLUDES_OPENVR
var isOverlay = !args.Contains("--no-overlay");
#else
var isOverlay = false;
#endif

// Create a desktop window stylized as being the overlay version.
// This does nothing when the --overlay arg is set.
var simulateWindowlessStyle = args.Contains("--simulate-windowless");

// Allow this app to run in both Overlay and Normal mode as separately managed instances.
var serviceName = isSteamVROverlay ? $"{HVApp.AppName}-Overlay" : $"{HVApp.AppName}-Windowed";
var serviceName = isOverlay ? $"{HVApp.AppName}-Overlay" : $"{HVApp.AppName}-Windowed";

var oscPort = HOsc.RandomOscPort();
var queryPort = HQuery.RandomQueryPort();
Expand All @@ -23,35 +25,54 @@
oscQuery.OnVrcOscPortFound += vrcOscPort => oscClient.SetReceiverOscPort(vrcOscPort);

var messageBox = new HMessageBox();
var routine = new HVRoutine(oscClient, oscQuery, messageBox);
var externalService = new HVExternalService();
var routine = new HVRoutine(oscClient, oscQuery, messageBox, externalService);

var ovrThread = new HVOpenVRThread(routine);

// Start services
oscClient.Start();
oscQuery.Start();
routine.Start();
externalService.Start();

void WhenWindowClosed()
{
routine.Finish();
oscQuery.Finish();
oscClient.Finish();
ovrThread.Finish();
}

var uiThread = new Thread(() =>
if (isOverlay)
{
if (!isSteamVROverlay)
Console.WriteLine("Starting as a hybrid desktop / VR app.");
// TODO: Allow the user to completely disable OpenVR integration.
StartNewThread(() =>
{
new HVWindow(routine, WhenWindowClosed, simulateWindowlessStyle).Run();
}
else
ovrThread.Run(); // Loops until desktop window is closed.
WhenWindowClosed();
}, "VR-Thread");
}
else
{
Console.WriteLine("Starting as a desktop window.");
StartNewThread(() =>
{
Console.WriteLine("Overlay mode is enabled (--overlay)");
new HVWindowless(routine, WhenWindowClosed).Run();
}
})
new HVWindow(routine, WhenWindowClosed, simulateWindowlessStyle).Run();
}, "UI-Thread");
}

void StartNewThread(ThreadStart threadStart1, string threadName)
{
CurrentCulture = CultureInfo.InvariantCulture, // We don't want locale-specific numbers
CurrentUICulture = CultureInfo.InvariantCulture
};
uiThread.Start();
var thread = new Thread(threadStart1)
{
CurrentCulture = CultureInfo.InvariantCulture, // We don't want locale-specific numbers
CurrentUICulture = CultureInfo.InvariantCulture,
Name = threadName
};
thread.Start();
}

// Main loop
routine.MainLoop(); // This call does not return until routine.Finish() is called.
16 changes: 8 additions & 8 deletions h-view/h-view.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,25 @@
<RootNamespace>Hai.HView</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Configurations>Debug;Release;Debug OVERLAY;Release OVERLAY</Configurations>
<Configurations>Debug;Release;Debug NoOpenVR;Release NoOpenVR</Configurations>
<Platforms>AnyCPU</Platforms>
<AssemblyVersion>$(ASSEMBLY_VERSION)</AssemblyVersion>
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DefineConstants>TRACE;HV_DEBUG</DefineConstants>
<DefineConstants>TRACE;HV_DEBUG;INCLUDES_OPENVR</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DefineConstants>TRACE</DefineConstants>
<DefineConstants>TRACE;INCLUDES_OPENVR</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)' == 'Debug OVERLAY' ">
<DefineConstants>TRACE;INCLUDES_OVERLAY;HV_DEBUG</DefineConstants>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug NoOpenVR' ">
<DefineConstants>TRACE;HV_DEBUG</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)' == 'Release OVERLAY' ">
<DefineConstants>TRACE;INCLUDES_OVERLAY</DefineConstants>
<PropertyGroup Condition=" '$(Configuration)' == 'Release NoOpenVR' ">
<DefineConstants>TRACE</DefineConstants>
</PropertyGroup>

<ItemGroup>
Expand All @@ -49,7 +49,7 @@
</ItemGroup>

<Choose>
<When Condition="$(DefineConstants.Contains('INCLUDES_OVERLAY'))">
<When Condition="$(DefineConstants.Contains('INCLUDES_OPENVR'))">
<ItemGroup>
<None Update="openvr_api.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
Expand Down
115 changes: 91 additions & 24 deletions h-view/src/HVInnerWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ public partial class HVInnerWindow : IDisposable
{
private const int BorderWidth = 0;
private const int BorderHeight = BorderWidth;
private const int TotalWindowWidth = 600;
private const int TotalWindowHeight = 510;
private const int TotalWindowlessViewportWidth = 800;
private const int TotalWindowlessViewportHeight = 800;
private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoResize;
private const ImGuiWindowFlags WindowFlagsNoCollapse = WindowFlags | ImGuiWindowFlags.NoCollapse;
private const string AvatarTabLabel = "Avatar";
Expand Down Expand Up @@ -64,14 +60,23 @@ public partial class HVInnerWindow : IDisposable
// Tabs
private readonly UiScrollManager _scrollManager = new UiScrollManager();
private readonly UiCostumes _costumesTab;
private readonly int _windowWidth;
private readonly int _windowHeight;
private readonly int _trimWidth;
private readonly int _trimHeight;

public HVInnerWindow(HVRoutine routine, bool isWindowlessStyle)
public HVInnerWindow(HVRoutine routine, bool isWindowlessStyle, int windowWidth, int windowHeight, int innerWidth, int innerHeight)
{
_routine = routine;
_isWindowlessStyle = isWindowlessStyle;
routine.OnManifestChanged += OnManifestChanged;

_costumesTab = new UiCostumes(this, _scrollManager);
_windowWidth = windowWidth;
_windowHeight = windowHeight;
_trimWidth = (windowWidth - innerWidth) / 2;
_trimHeight = (windowHeight - innerHeight) / 2;

_costumesTab = new UiCostumes(this, _routine, _scrollManager, isWindowlessStyle);
}

public void Dispose()
Expand Down Expand Up @@ -104,8 +109,8 @@ private void SubmitUI()
}

var windowHeight = _window.Height - BorderHeight * 2;
ImGui.SetNextWindowPos(new Vector2(BorderWidth, BorderHeight), ImGuiCond.Always);
ImGui.SetNextWindowSize(new Vector2(_window.Width - BorderWidth * 2, windowHeight), ImGuiCond.Always);
ImGui.SetNextWindowPos(new Vector2(BorderWidth + _trimWidth, BorderHeight + _trimHeight), ImGuiCond.Always);
ImGui.SetNextWindowSize(new Vector2(_window.Width - BorderWidth * 2 - _trimWidth * 2, windowHeight - _trimHeight * 2), ImGuiCond.Always);
var flags = WindowFlagsNoCollapse;
if (_isWindowlessStyle)
{
Expand All @@ -116,7 +121,7 @@ private void SubmitUI()
var oscMessages = _routine.UiOscMessages();

_scrollManager.MakeTab(ShortcutsTabLabel, () => ShortcutsTab(oscMessages));
_scrollManager.MakeTab(CostumesTabLabel, () => _costumesTab.CostumesTab(oscMessages));
_scrollManager.MakeUnscrollableTab(CostumesTabLabel, () => _costumesTab.CostumesTab(oscMessages));
if (ImGui.BeginTabItem("Parameters"))
{
ImGui.BeginTabBar("##tabs_parameters");
Expand Down Expand Up @@ -148,11 +153,39 @@ private void SubmitUI()
}
}

public bool UpdateIteration(Stopwatch stopwatch)
{
if (_window.WindowState == WindowState.Minimized)
{
_window.PumpEvents();
return true;
}
SetAsActiveContext();

var deltaTime = stopwatch.ElapsedTicks / (float)Stopwatch.Frequency;
var snapshot = _window.PumpEvents();
if (!_window.Exists) return false;

_controller.Update(deltaTime, snapshot); // Feed the input events to our ImGui controller, which passes them through to ImGui.

SubmitUI();

_cl.Begin();
_cl.SetFramebuffer(_gd.MainSwapchain.Framebuffer);
_cl.ClearColorTarget(0, new RgbaFloat(_clearColor.X, _clearColor.Y, _clearColor.Z, 1f));
_controller.Render(_gd, _cl);
_cl.End();
_gd.SubmitCommands(_cl);
_gd.SwapBuffers(_gd.MainSwapchain);

return true;
}

public void UiLoop()
{
// Create window, GraphicsDevice, and all resources necessary for the demo.
var width = _isWindowlessStyle ? TotalWindowlessViewportWidth : TotalWindowWidth;
var height = _isWindowlessStyle ? TotalWindowlessViewportHeight : TotalWindowHeight;
var width = _windowWidth;
var height = _windowHeight;
VeldridStartup.CreateWindowAndGraphicsDevice(
new WindowCreateInfo(50, 50, width, height, WindowState.Normal, $"{HVApp.AppTitle}"),
new GraphicsDeviceOptions(true, null, true, ResourceBindingModel.Improved, true, true),
Expand All @@ -168,8 +201,7 @@ public void UiLoop()
_controller.WindowResized(_window.Width, _window.Height);
};
_cl = _gd.ResourceFactory.CreateCommandList();
_controller = new CustomImGuiController(_gd, _gd.MainSwapchain.Framebuffer.OutputDescription, _window.Width,
_window.Height);
_controller = new CustomImGuiController(_gd, _gd.MainSwapchain.Framebuffer.OutputDescription, _window.Width, _window.Height);

var timer = Stopwatch.StartNew();
timer.Start();
Expand Down Expand Up @@ -220,10 +252,10 @@ public void UiLoop()

#region Support for SteamVR Overlay

public void SetupWindowlessUi()
public void SetupUi(bool actuallyWindowless)
{
VeldridStartup.CreateWindowAndGraphicsDevice(
new WindowCreateInfo(50, 50, TotalWindowlessViewportWidth, TotalWindowlessViewportHeight, WindowState.Hidden, $"{HVApp.AppTitle}"),
new WindowCreateInfo(50, 50, _windowWidth, _windowHeight, actuallyWindowless ? WindowState.Hidden : WindowState.Normal, actuallyWindowless ? $"{HVApp.AppTitle}-Windowless" : HVApp.AppTitle),
new GraphicsDeviceOptions(true, null, true, ResourceBindingModel.Improved, true, true),
// I am forcing this to Direct3D11, because my current implementation requires
// that GetOverlayTexturePointer / GetTexturePointer would get the IntPtr from D3D11.
Expand All @@ -232,23 +264,29 @@ public void SetupWindowlessUi()
GraphicsBackend.Direct3D11,
out _window,
out _gd);
_window.Resizable = false;
_window.Resizable = !actuallyWindowless;
_window.Resized += () =>
{
// FIXME: It might not be necessary to resize the swapchain, since we don't use that.
// Actually, we don't ever resize the window either.
_gd.MainSwapchain.Resize((uint)_window.Width, (uint)_window.Height);

TeardownFramebuffer();
SetupFramebuffer();

if (actuallyWindowless)
{
TeardownFramebuffer();
SetupFramebuffer();
}
_controller.WindowResized(_window.Width, _window.Height);
};
_cl = _gd.ResourceFactory.CreateCommandList();

SetupFramebuffer();
if (actuallyWindowless)
{
SetupFramebuffer();
}

// I've wasted several hours of dev because I forgot to pass our own framebuffer OutputDescription to this thing.
_controller = new CustomImGuiController(_gd, _overlayFramebuffer.OutputDescription, _window.Width, _window.Height);
_controller = new CustomImGuiController(_gd, actuallyWindowless ? _overlayFramebuffer.OutputDescription : _gd.MainSwapchain.Framebuffer.OutputDescription, _window.Width, _window.Height);
}

private void SetupFramebuffer()
Expand Down Expand Up @@ -304,7 +342,6 @@ public InputSnapshot DoPumpEvents()
public void UpdateAndRender(Stopwatch stopwatch, InputSnapshot snapshot)
{
var deltaTime = stopwatch.ElapsedTicks / (float)Stopwatch.Frequency;
stopwatch.Restart();

_controller.Update(deltaTime, snapshot);

Expand All @@ -318,9 +355,12 @@ public void UpdateAndRender(Stopwatch stopwatch, InputSnapshot snapshot)
_gd.SubmitCommands(_cl);
}

public void TeardownWindowlessUi()
public void TeardownWindowlessUi(bool actuallyWindowless)
{
TeardownFramebuffer();
if (actuallyWindowless)
{
TeardownFramebuffer();
}

// Clean up Veldrid resources
_gd.WaitForIdle();
Expand All @@ -335,4 +375,31 @@ public Vector2 WindowSize()
{
return new Vector2(_window.Width, _window.Height);
}

public void SetAsActiveContext()
{
_controller.SetAsActiveContext();
}

public bool HandleSleep()
{
if (_window.WindowState == WindowState.Minimized)
{
Thread.Sleep(1000 / RefreshEventPollPerSecondWhenMinimized);

// TODO: We need to know when the window is no longer minimized.
// How to properly poll events while minimized?
_window.PumpEvents();

return false;
}

if (!_window.Focused)
{
Thread.Sleep(1000 / RefreshFramesPerSecondWhenUnfocused);
}
// else: Do not limit framerate.

return true;
}
}
Loading

0 comments on commit 8c33dfd

Please sign in to comment.