using SharpDX; using SharpDX.Direct3D11; using SharpDX.DXGI; using System; using System.ComponentModel; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; using Valve.VR; using VDash.WindowSelectors; namespace VDash.Overlay { public class WindowOverlay : IOverlay { public event EventHandler OverlayShown; public event EventHandler OverlayHidden; public CVRSystem VRSystem { get; private set; } private Task m_EventPumpTask = null; bool m_Created = false; bool m_Running = true; public bool UpdateDashboardName { get; set; } = false; internal ulong m_Handle; public ulong Handle { get { return m_Handle; } } internal string m_Key; public string Key { get { return m_Key; } } internal string m_Name; public string Name { get { return m_Name; } } IWindowSelector m_Selector = null; public WindowOverlay(string key, string name, IWindowSelector selector, bool updateDashboardName) { m_Key = key; m_Name = name; m_Selector = selector; UpdateDashboardName = updateDashboardName; } public void Create() { if (m_Created) { throw new Exception("This overlay has already been created."); } //ConsoleManager.Show(); ulong thumbHandle = OpenVR.k_ulOverlayHandleInvalid; //Actually create the overlay var overlay = OpenVR.Overlay; EVROverlayError err = overlay.CreateDashboardOverlay(m_Key, m_Name, ref m_Handle, ref thumbHandle); if (err != EVROverlayError.None) { throw new Exception($"Error creating overlay: {err}"); } overlay.SetOverlayInputMethod(m_Handle, VROverlayInputMethod.Mouse); m_Running = true; m_EventPumpTask = Task.Factory.StartNew(() => { EventPump(); }); m_Created = true; } public void Destroy() { if (!m_Created) { throw new Exception("This object is not created."); } m_Created = false; //Signal render thread to exit m_Running = false; //Wait for render thread to exit m_EventPumpTask.Wait(); var err = OpenVR.Overlay.DestroyOverlay(m_Handle); if( err != EVROverlayError.None) { throw new Exception($"Error destroying overlay: {err}"); } } private void EventPump() { #region Rendering setup var overlay = OpenVR.Overlay; //Partially taken from here: https://stackoverflow.com/questions/6812068/c-sharp-which-is-the-fastest-way-to-take-a-screen-shot //Desktop Duplication didn't work well, hence is why we're sticking with GDI. Still this is faster than using SetTextureRaw. var factory = new Factory1(); //Get first adapter var adapter = factory.GetAdapter1(0); //Get device from adapter var device = new SharpDX.Direct3D11.Device(adapter); int lastWidth = 0; int lastHeight = 0; // Create Staging texture CPU-accessible //Actual screen texture. //var screenTexture = new Texture2D(device, textureDesc); Texture2D screenTexture = null; DataStream stream = null; #endregion //For FPS counter. int frames = 0; DateTime lastFps = DateTime.MinValue; DateTime lastNameCheck = DateTime.MinValue; while (m_Running) { //Update the SteamVR dash name if necessary, only once a second. if (UpdateDashboardName && DateTime.Now.Subtract(lastNameCheck).TotalMilliseconds >= 1000) { var window = m_Selector.GetWindowInfo(); if(GetOverlayName() != window.Title) { SetOverlayName(window.Title); } lastNameCheck = DateTime.Now; } //Skip rendering if overlay isn't visible #if !DEBUG if ((m_Handle == Openvr.k_uloverlayhandleinvalid) || overlay == null || (!overlay.isoverlayvisible(m_handle))) { thread.sleep(100); continue; } #endif IntPtr windowHandle = IntPtr.Zero; try { windowHandle = m_Selector.GetWindowHandle(); } catch (WindowNotFoundException) { //Console.WriteLine($"[{m_Key}] {m_Name}: Window not found."); } #region VR Events VREvent_t vrEvent = new VREvent_t(); while (OpenVR.Overlay.PollNextOverlayEvent(m_Handle, ref vrEvent, (uint)Marshal.SizeOf(vrEvent))) { switch ((EVREventType)vrEvent.eventType) { case EVREventType.VREvent_MouseMove: { if (windowHandle != IntPtr.Zero) { var vrX = vrEvent.data.mouse.x; var vrY = vrEvent.data.mouse.y; var rect = new User32.Rect(); User32.GetWindowRect(windowHandle, ref rect); int width = rect.right - rect.left; int height = rect.bottom - rect.top; var point = new User32.POINT() { x = (int)vrX, y = height - (int)vrY }; var succ = User32.ClientToScreen(windowHandle, ref point); if (!succ) { Console.WriteLine("ClientToScreen failed."); } succ = User32.SetCursorPos((int)point.x, (int)point.y); if (!succ) { Console.WriteLine("SetCursorPos failed."); } } break; } case EVREventType.VREvent_MouseButtonDown: { var clickType = (EVRMouseButton)vrEvent.data.mouse.button; Console.WriteLine($"Mouse button down: {clickType}"); var winClickType = User32.MouseEventFlags.MOUSEEVENTF_LEFTDOWN; switch (clickType) { case EVRMouseButton.Left: { winClickType = User32.MouseEventFlags.MOUSEEVENTF_LEFTDOWN; break; } case EVRMouseButton.Middle: { winClickType = User32.MouseEventFlags.MOUSEEVENTF_MIDDLEDOWN; break; } case EVRMouseButton.Right: { winClickType = User32.MouseEventFlags.MOUSEEVENTF_RIGHTDOWN; break; } } User32.mouse_event(winClickType, 0, 0, 0, UIntPtr.Zero); break; } case EVREventType.VREvent_MouseButtonUp: { var clickType = (EVRMouseButton)vrEvent.data.mouse.button; Console.WriteLine($"Mouse button up: {clickType}"); var winClickType = User32.MouseEventFlags.MOUSEEVENTF_LEFTUP; switch (clickType) { case EVRMouseButton.Left: { winClickType = User32.MouseEventFlags.MOUSEEVENTF_LEFTUP; break; } case EVRMouseButton.Middle: { winClickType = User32.MouseEventFlags.MOUSEEVENTF_MIDDLEUP; break; } case EVRMouseButton.Right: { winClickType = User32.MouseEventFlags.MOUSEEVENTF_RIGHTUP; break; } } User32.mouse_event(winClickType, 0, 0, 0, UIntPtr.Zero); break; } case EVREventType.VREvent_OverlayShown: { OverlayShown?.Invoke(this, new EventArgs()); break; } case EVREventType.VREvent_OverlayHidden: { OverlayHidden?.Invoke(this, new EventArgs()); break; } } } #endregion #region Acutally Render if (windowHandle != IntPtr.Zero) { var rect = new User32.Rect(); User32.GetWindowRect(windowHandle, ref rect); int width = rect.right - rect.left; int height = rect.bottom - rect.top; using (var bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb)) { using (Graphics graphics = Graphics.FromImage(bmp)) { graphics.CopyFromScreen(rect.left, rect.top, 0, 0, new Size(width, height), CopyPixelOperation.SourceCopy); //Draw cursor User32.CURSORINFO pci; pci.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(User32.CURSORINFO)); if (User32.GetCursorInfo(out pci)) { User32.POINT cursorPoint = new User32.POINT() { x = pci.ptScreenPos.X, y = pci.ptScreenPos.Y}; if (User32.ScreenToClient(windowHandle, ref cursorPoint)) { if (pci.flags == User32.CURSOR_SHOWING) { User32.DrawIcon(graphics.GetHdc(), (int)cursorPoint.x, (int)cursorPoint.y, pci.hCursor); graphics.ReleaseHdc(); } } else { Console.WriteLine("Error getting coords to draw cursor."); } } else { string errorMessage = new Win32Exception(Marshal.GetLastWin32Error()).Message; Console.WriteLine($"Error getting cursor info: {errorMessage}"); } var data = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); if (screenTexture == null || (lastWidth != data.Width || lastHeight != data.Height)) { lastWidth = data.Width; lastHeight = data.Height; if (screenTexture != null) { screenTexture.Dispose(); //Dispose not freeing screenTexture: https://gamedev.stackexchange.com/questions/93311/freeing-texture-memory-in-sharpdx-what-am-i-missing device.ImmediateContext.ClearState(); device.ImmediateContext.Flush(); } var textureDesc = new Texture2DDescription { CpuAccessFlags = CpuAccessFlags.Write, BindFlags = BindFlags.ShaderResource, Format = Format.B8G8R8A8_UNorm, //Format = Format.B8G8R8A8_UNorm_SRgb, Width = data.Width, Height = data.Height, OptionFlags = ResourceOptionFlags.None, MipLevels = 1, ArraySize = 1, SampleDescription = { Count = 1, Quality = 0 }, Usage = ResourceUsage.Dynamic }; screenTexture = new Texture2D(device, textureDesc); stream = new DataStream(4 * width * height, true, true); overlay.SetOverlayWidthInMeters(m_Handle, 2.5f); HmdVector2_t vecWindowSize = new HmdVector2_t() { v0 = width, v1 = height }; OpenVR.Overlay.SetOverlayMouseScale(m_Handle, ref vecWindowSize); Console.WriteLine("Updated!"); } //Render using graphics card, in DirectX context. //using () { var screenView = new ShaderResourceView(device, screenTexture); device.ImmediateContext.PixelShader.SetShaderResource(0, screenView); var dr = new SharpDX.DataRectangle(data.Scan0, data.Stride); DataBox databox = device.ImmediateContext.MapSubresource(screenTexture, 0, MapMode.WriteDiscard, SharpDX.Direct3D11.MapFlags.None, out stream); if (!databox.IsEmpty) { int count = 4 * data.Width * data.Height; //Stride compensation. Sometimes the BMP and DX texture strides are different. This fixes it. if (databox.RowPitch == data.Stride) { stream.Write(data.Scan0, 0, count); } //Truncate the extra BMP bytes. else if (databox.RowPitch < data.Stride) { for (int i = 0; i < data.Height; i++) { stream.Write(data.Scan0, (data.Stride * i), databox.RowPitch); } } //Add some junk on empty bytes on the end to make everyone happy. else if(databox.RowPitch > data.Stride) { int difference = databox.RowPitch - data.Stride; byte[] junk = new byte[difference]; for (int i = 0; i < data.Height; i++) { stream.Write(data.Scan0, (data.Stride * i), data.Stride); stream.Write(junk, 0, difference); } } } VRTextureBounds_t bounds = new VRTextureBounds_t(); OpenVR.Overlay.GetOverlayTextureBounds(m_Handle, ref bounds); var texture = new Texture_t(); texture.handle = screenTexture.NativePointer; texture.eType = ETextureType.DirectX; texture.eColorSpace = EColorSpace.Auto; EVROverlayError result = overlay.SetOverlayTexture(m_Handle, ref texture); if (result != EVROverlayError.None) { Console.WriteLine($"Error rendering: {result}"); } device.ImmediateContext.UnmapSubresource(screenTexture, 0); frames++; bmp.UnlockBits(data); } } } } #endregion Thread.Sleep(20); if (DateTime.Now.Subtract(lastFps).TotalMilliseconds > 1000) { //Console.WriteLine($"{frames} fps."); frames = 0; lastFps = DateTime.Now; } } //Clean up after we exit while loop. device.Dispose(); adapter.Dispose(); factory.Dispose(); } private class User32 { [StructLayout(LayoutKind.Sequential)] public struct Rect { public int left; public int top; public int right; public int bottom; } [StructLayout(LayoutKind.Sequential)] public struct POINT { public long x; public long y; } [StructLayout(LayoutKind.Sequential)] public struct CURSORINFO { public Int32 cbSize; public Int32 flags; public IntPtr hCursor; public Point ptScreenPos; } public const Int32 CURSOR_SHOWING = 0x00000001; [DllImport("user32.dll")] public static extern IntPtr GetWindowRect(IntPtr hWnd, ref Rect rect); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool SetCursorPos(int x, int y); [DllImport("user32.dll")] public static extern bool ClientToScreen(IntPtr hwnd, ref POINT lpPoint); [DllImport("user32.dll")] public static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint); [DllImport("user32.dll")] public static extern bool GetCursorInfo(out CURSORINFO pci); [DllImport("user32.dll")] public static extern bool DrawIcon(IntPtr hDC, int X, int Y, IntPtr hIcon); [DllImport("user32.dll")] public static extern void mouse_event(MouseEventFlags dwFlags, int dx, int dy, uint dwData, UIntPtr dwExtraInfo); public enum MouseEventFlags { MOUSEEVENTF_ABSOLUTE = 0x8000, MOUSEEVENTF_LEFTDOWN = 0x0002, MOUSEEVENTF_LEFTUP = 0x0004, MOUSEEVENTF_MIDDLEDOWN = 0x0020, MOUSEEVENTF_MIDDLEUP = 0x0040, MOUSEEVENTF_MOVE = 0x0001, MOUSEEVENTF_RIGHTDOWN = 0x0008, MOUSEEVENTF_RIGHTUP = 0x0010, MOUSEEVENTF_XDOWN = 0x0080, MOUSEEVENTF_XUP = 0x0100, MOUSEEVENTF_WHEEL = 0x0800, MOUSEEVENTF_HWHEEL = 0x01000 } } public void Dispose() { Destroy(); } public string GetOverlayName() { EVROverlayError err = EVROverlayError.None; StringBuilder sb = new StringBuilder(255); uint res = OpenVR.Overlay.GetOverlayName(m_Handle, sb, 255, ref err); return sb.ToString(); } public void SetOverlayName(string newName) { OpenVR.Overlay.SetOverlayName(m_Handle, newName); m_Name = newName; } } }