diff --git a/VDash/MainWindow.xaml.cs b/VDash/MainWindow.xaml.cs index badb8e8..82d925d 100644 --- a/VDash/MainWindow.xaml.cs +++ b/VDash/MainWindow.xaml.cs @@ -35,16 +35,13 @@ namespace VDash throw new Exception($"Error connecting to VR: {initError.ToString()}"); } - Overlay.WindowOverlay overlay = new Overlay.WindowOverlay("TestKey", "Window Dash"); - overlay.Create(); + var discordSelector = new WindowSelectors.FromWindowTitle("Discord", WindowSelectors.FromWindowTitle.TitleSearchType.Contains); + Overlay.WindowOverlay discordOverlay = new Overlay.WindowOverlay("discord", "Discord", discordSelector, true); + discordOverlay.Create(); + Overlay.WindowOverlay obsOverlay = new Overlay.WindowOverlay("obs", "OBS", new WindowSelectors.FromProcessName("obs64"), false); + obsOverlay.Create(); - //var screenStateLogger = new ScreenStateLogger(); - //screenStateLogger.ScreenRefreshed += (sender, data) => - //{ - // Console.WriteLine("Got screen data!"); - //}; - //screenStateLogger.Start(); } } } diff --git a/VDash/Overlay/DashOverlay.cs b/VDash/Overlay/DashOverlay.cs deleted file mode 100644 index 9b168cc..0000000 --- a/VDash/Overlay/DashOverlay.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Valve.VR; - -namespace VDash.Overlay -{ - public class DashOverlay - { - 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; } } - - - public DashOverlay(string key, string name) - { - m_Key = key; - m_Name = name; - } - } -} diff --git a/VDash/Overlay/IOverlay.cs b/VDash/Overlay/IOverlay.cs new file mode 100644 index 0000000..74ddff0 --- /dev/null +++ b/VDash/Overlay/IOverlay.cs @@ -0,0 +1,19 @@ +using System; + +namespace VDash.Overlay +{ + public interface IOverlay : IDisposable + { + event EventHandler OverlayShown; + event EventHandler OverlayHidden; + + ulong Handle { get; } + string Name { get; } + string Key { get; } + + void Create(); + void Destroy(); + string GetOverlayName(); + void SetOverlayName(string newName); + } +} diff --git a/VDash/Overlay/WindowOverlay.cs b/VDash/Overlay/WindowOverlay.cs index 61b292b..2a269f6 100644 --- a/VDash/Overlay/WindowOverlay.cs +++ b/VDash/Overlay/WindowOverlay.cs @@ -1,34 +1,57 @@ -using SharpDX; -using SharpDX.Direct3D11; +using SharpDX.Direct3D11; using SharpDX.DXGI; using System; -using System.Collections.Generic; -using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; -using System.IO; -using System.Linq; 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 : DashOverlay,IDisposable + 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; - public WindowOverlay(string key, string name) : base(key, name) + 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; @@ -43,35 +66,72 @@ namespace VDash.Overlay overlay.SetOverlayWidthInMeters(m_Handle, 2.5f); 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; - //Skip rendering if overlay isn't visible - if ((m_Handle == OpenVR.k_ulOverlayHandleInvalid) || overlay == null || (!overlay.IsOverlayVisible(m_Handle))) - { - //return; - } - - //Taken from here: https://stackoverflow.com/questions/6812068/c-sharp-which-is-the-fastest-way-to-take-a-screen-shot + //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); - #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 ((m_Handle == OpenVR.k_ulOverlayHandleInvalid) || overlay == null || (!overlay.IsOverlayVisible(m_Handle))) + //{ + // Thread.Sleep(100); + // continue; + //} + + #region VR Events VREvent_t vrEvent = new VREvent_t(); @@ -99,7 +159,12 @@ namespace VDash.Overlay } case EVREventType.VREvent_OverlayShown: { - Console.WriteLine("Overlay Shown!"); + OverlayShown?.Invoke(this, new EventArgs()); + break; + } + case EVREventType.VREvent_OverlayHidden: + { + OverlayHidden?.Invoke(this, new EventArgs()); break; } } @@ -108,62 +173,73 @@ namespace VDash.Overlay #endregion #region Acutally Render - - var proc = Process.GetProcessesByName("discord")[1]; - var rect = new User32.Rect(); - User32.GetWindowRect(proc.MainWindowHandle, ref rect); - - int width = rect.right - rect.left; - int height = rect.bottom - rect.top; - - using (var bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb)) + IntPtr windowHandle = IntPtr.Zero; + try { - using (Graphics graphics = Graphics.FromImage(bmp)) + windowHandle = m_Selector.GetWindowHandle(); + } + catch (WindowNotFoundException) + { + //Console.WriteLine($"[{m_Key}] {m_Name}: Window not found."); + } + + 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)) { - graphics.CopyFromScreen(rect.left, rect.top, 0, 0, new Size(width, height), CopyPixelOperation.SourceCopy); - - var data = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); - - // Create Staging texture CPU-accessible - var textureDesc = new Texture2DDescription + using (Graphics graphics = Graphics.FromImage(bmp)) { - CpuAccessFlags = CpuAccessFlags.Read, - BindFlags = BindFlags.None, - Format = Format.B8G8R8A8_UNorm, - Width = width, - Height = height, - OptionFlags = ResourceOptionFlags.None, - MipLevels = 1, - ArraySize = 1, - SampleDescription = { Count = 1, Quality = 0 }, - Usage = ResourceUsage.Staging - }; + graphics.CopyFromScreen(rect.left, rect.top, 0, 0, new Size(width, height), CopyPixelOperation.SourceCopy); - using (var screenTexture = new Texture2D(device, textureDesc, new SharpDX.DataRectangle(data.Scan0, data.Stride))) - //using (var screenTexture = new Texture2D(device, textureDesc)) - { - 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) + var data = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + + //Render using graphics card, in DirectX context. + // Create Staging texture CPU-accessible + var textureDesc = new Texture2DDescription { - Console.WriteLine($"Error rendering: {result}"); + CpuAccessFlags = CpuAccessFlags.Read, + BindFlags = BindFlags.None, + Format = Format.B8G8R8A8_UNorm, + Width = width, + Height = height, + OptionFlags = ResourceOptionFlags.None, + MipLevels = 1, + ArraySize = 1, + SampleDescription = { Count = 1, Quality = 0 }, + Usage = ResourceUsage.Staging + }; + + using (var screenTexture = new Texture2D(device, textureDesc, new SharpDX.DataRectangle(data.Scan0, data.Stride))) + { + 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}"); + } + + frames++; + + bmp.UnlockBits(data); } - - frames++; - - bmp.UnlockBits(data); + //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(); } - //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(); } } #endregion - Thread.Sleep(10); + Thread.Sleep(20); if (DateTime.Now.Subtract(lastFps).TotalMilliseconds > 1000) { @@ -172,6 +248,11 @@ namespace VDash.Overlay lastFps = DateTime.Now; } } + + //Clean up after we exit while loop. + device.Dispose(); + adapter.Dispose(); + factory.Dispose(); } private class User32 @@ -191,7 +272,21 @@ namespace VDash.Overlay public void Dispose() { - //FIXME: Actually implement this... + 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; } } } diff --git a/VDash/ScreenStateLogger.cs b/VDash/ScreenStateLogger.cs deleted file mode 100644 index d61cd82..0000000 --- a/VDash/ScreenStateLogger.cs +++ /dev/null @@ -1,138 +0,0 @@ -using SharpDX; -using SharpDX.Direct3D11; -using SharpDX.DXGI; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Drawing; -using System.Drawing.Imaging; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace VDash -{ - public class ScreenStateLogger - { - private byte[] _previousScreen; - private bool _run, _init; - - public int Size { get; private set; } - public ScreenStateLogger() - { - - } - - public void Start() - { - _run = true; - var factory = new Factory1(); - //Get first adapter - var adapter = factory.GetAdapter1(0); - //Get device from adapter - var device = new SharpDX.Direct3D11.Device(adapter); - //Get front buffer of the adapter - var output = adapter.GetOutput(0); - var output1 = output.QueryInterface(); - - // Width/Height of desktop to capture - int width = output.Description.DesktopBounds.Right; - int height = output.Description.DesktopBounds.Bottom; - - // Create Staging texture CPU-accessible - var textureDesc = new Texture2DDescription - { - CpuAccessFlags = CpuAccessFlags.Read, - BindFlags = BindFlags.None, - Format = Format.B8G8R8A8_UNorm, - Width = width, - Height = height, - OptionFlags = ResourceOptionFlags.None, - MipLevels = 1, - ArraySize = 1, - SampleDescription = { Count = 1, Quality = 0 }, - Usage = ResourceUsage.Staging - }; - var screenTexture = new Texture2D(device, textureDesc); - - Task.Factory.StartNew(() => - { - // Duplicate the output - using (var duplicatedOutput = output1.DuplicateOutput(device)) - { - while (_run) - { - try - { - SharpDX.DXGI.Resource screenResource; - OutputDuplicateFrameInformation duplicateFrameInformation; - - // Try to get duplicated frame within given time is ms - duplicatedOutput.AcquireNextFrame(5, out duplicateFrameInformation, out screenResource); - - // copy resource into memory that can be accessed by the CPU - using (var screenTexture2D = screenResource.QueryInterface()) - device.ImmediateContext.CopyResource(screenTexture2D, screenTexture); - - // Get the desktop capture texture - var mapSource = device.ImmediateContext.MapSubresource(screenTexture, 0, MapMode.Read, SharpDX.Direct3D11.MapFlags.None); - - // Create Drawing.Bitmap - using (var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb)) - { - var boundsRect = new Rectangle(0, 0, width, height); - - // Copy pixels from screen capture Texture to GDI bitmap - var mapDest = bitmap.LockBits(boundsRect, ImageLockMode.WriteOnly, bitmap.PixelFormat); - var sourcePtr = mapSource.DataPointer; - var destPtr = mapDest.Scan0; - for (int y = 0; y < height; y++) - { - // Copy a single line - Utilities.CopyMemory(destPtr, sourcePtr, width * 4); - - // Advance pointers - sourcePtr = IntPtr.Add(sourcePtr, mapSource.RowPitch); - destPtr = IntPtr.Add(destPtr, mapDest.Stride); - } - - // Release source and dest locks - bitmap.UnlockBits(mapDest); - device.ImmediateContext.UnmapSubresource(screenTexture, 0); - - using (var ms = new MemoryStream()) - { - bitmap.Save(ms, ImageFormat.Bmp); - if (duplicateFrameInformation.AccumulatedFrames > 0) - { - ScreenRefreshed?.Invoke(this, ms.ToArray()); - } - _init = true; - } - } - screenResource.Dispose(); - duplicatedOutput.ReleaseFrame(); - } - catch (SharpDXException e) - { - if (e.ResultCode.Code != SharpDX.DXGI.ResultCode.WaitTimeout.Result.Code) - { - Trace.TraceError(e.Message); - Trace.TraceError(e.StackTrace); - } - } - } - } - }); - while (!_init) ; - } - - public void Stop() - { - _run = false; - } - - public EventHandler ScreenRefreshed; - } -} diff --git a/VDash/VDash.csproj b/VDash/VDash.csproj index ac7a33d..e222d45 100644 --- a/VDash/VDash.csproj +++ b/VDash/VDash.csproj @@ -84,7 +84,10 @@ MSBuild:Compile Designer - + + + + MSBuild:Compile Designer @@ -101,7 +104,6 @@ - Code diff --git a/VDash/WindowSelectors/FromProcessName.cs b/VDash/WindowSelectors/FromProcessName.cs new file mode 100644 index 0000000..1685e90 --- /dev/null +++ b/VDash/WindowSelectors/FromProcessName.cs @@ -0,0 +1,50 @@ +using System; +using System.Diagnostics; +using System.Linq; + +namespace VDash.WindowSelectors +{ + public class FromProcessName : IWindowSelector + { + string m_ProcName = ""; + public FromProcessName(string procName) + { + if (string.IsNullOrWhiteSpace(procName)) + { + throw new ArgumentNullException("procName", "procName must not be null, whitespace, or empty."); + } + + m_ProcName = procName; + } + + public Process[] GetProcesses() + { + return Process.GetProcessesByName(m_ProcName); + } + + public Process GetProcess() + { + var procs = GetProcesses(); + + if (procs.Count() <= 0) + { + throw new WindowNotFoundException($"No process found with name {m_ProcName}."); + } + return procs.First(); + } + public IntPtr GetWindowHandle() + { + return GetProcess().MainWindowHandle; + } + + public WindowInfo GetWindowInfo() + { + var ret = WindowInfo.GetWindowList().SingleOrDefault(x => x.hWnd == GetWindowHandle()); + if(ret == null) + { + throw new WindowNotFoundException($"Unable to locate window for process {m_ProcName}."); + } + return ret; + } + } +} diff --git a/VDash/WindowSelectors/FromWindowName.cs b/VDash/WindowSelectors/FromWindowName.cs new file mode 100644 index 0000000..39af695 --- /dev/null +++ b/VDash/WindowSelectors/FromWindowName.cs @@ -0,0 +1,89 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; + +namespace VDash.WindowSelectors +{ + public class FromWindowTitle : IWindowSelector + { + WindowInfo m_Window = null; + public bool RefreshOnGet { get; set; } = false; + public string TitleFilter { get; set; } = ""; + public TitleSearchType SearchType { get; set; } = TitleSearchType.Contains; + + public WindowInfo Window + { + get + { + if (RefreshOnGet) + { + m_Window = GetWindowUsingFilter(); + return m_Window; + } + else + { + if (m_Window == null) + { + m_Window = GetWindowUsingFilter(); + } + return m_Window; + } + } + } + + public void ForceWindowRefresh() + { + m_Window = GetWindowUsingFilter(); + } + + public FromWindowTitle(string windowFilter, TitleSearchType searchType) + { + TitleFilter = windowFilter; + SearchType = searchType; + } + + internal WindowInfo GetWindowUsingFilter() + { + var windows = WindowInfo.GetWindowList(); + if (SearchType == TitleSearchType.Contains) + { + var results = windows.Where(x => x.Title.ToLower().Contains(TitleFilter.ToLower())); + if(results.Count() == 0) + { + throw new WindowNotFoundException("No windows match the filter settings."); + } + + return results.First(); + } + if(SearchType == TitleSearchType.Regex) + { + var results = windows.Where(x => Regex.IsMatch(x.Title, TitleFilter)); + + if (results.Count() == 0) + { + throw new WindowNotFoundException("No windows match the filter settings."); + } + + return results.First(); + } + + throw new WindowNotFoundException("Error fetching matching window."); + } + + public IntPtr GetWindowHandle() + { + return Window.hWnd; + } + + public WindowInfo GetWindowInfo() + { + return Window; + } + + public enum TitleSearchType + { + Contains = 0, + Regex = 1 + } + } +} diff --git a/VDash/WindowSelectors/IWindowSelector.cs b/VDash/WindowSelectors/IWindowSelector.cs new file mode 100644 index 0000000..b38dc8e --- /dev/null +++ b/VDash/WindowSelectors/IWindowSelector.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + +namespace VDash.WindowSelectors +{ + public interface IWindowSelector + { + IntPtr GetWindowHandle(); + WindowInfo GetWindowInfo(); + } + + public class WindowNotFoundException : Exception + { + public WindowNotFoundException(string message) : base(message) + { + + } + } + + public class WindowInfo + { + #region P/Invokes + private delegate bool EnumWindowsProc(IntPtr hWnd, int lPar); + [DllImport("user32.dll")] + private static extern int EnumWindows(EnumWindowsProc callPtr, int lPar); + + [DllImport("user32.dll")] + static extern bool EnumDesktopWindows(IntPtr hDesktop, EnumWindowsProc lpfn, int lPar); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll")] + private static extern bool IsWindowVisible(IntPtr hWnd); + #endregion + + + public string Title { get; private set; } + public IntPtr hWnd { get; private set; } + public bool IsVisible { get; private set; } + + public WindowInfo(string title, IntPtr hWnd, bool isVisible) + { + Title = title; + this.hWnd = hWnd; + this.IsVisible = isVisible; + } + + public override string ToString() + { + return Title; + } + + public static WindowInfo[] GetWindowList() + { + List windows = new List(); + + EnumWindowsProc del = ((IntPtr hWnd, int lPar) => + { + StringBuilder sb = new StringBuilder(255); + + GetWindowText(hWnd, sb, 255); + bool isVisible = IsWindowVisible(hWnd); + + var windowInfo = new WindowInfo(sb.ToString(), hWnd, isVisible); + + windows.Add(windowInfo); + + return true; + }); + + EnumDesktopWindows(IntPtr.Zero, del, 0); + + return windows.ToArray(); + } + } +}