Generic interfaces implemented for both overlays and WindowSelectors.

This commit is contained in:
jrhodnik 2019-05-24 18:09:46 -04:00
parent 0c220316e8
commit ddbde07f92
9 changed files with 405 additions and 246 deletions

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

19
VDash/Overlay/IOverlay.cs Normal file
View File

@ -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);
}
}

View File

@ -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,10 +173,20 @@ namespace VDash.Overlay
#endregion
#region Acutally Render
IntPtr windowHandle = IntPtr.Zero;
try
{
windowHandle = m_Selector.GetWindowHandle();
}
catch (WindowNotFoundException)
{
//Console.WriteLine($"[{m_Key}] {m_Name}: Window not found.");
}
var proc = Process.GetProcessesByName("discord")[1];
if (windowHandle != IntPtr.Zero)
{
var rect = new User32.Rect();
User32.GetWindowRect(proc.MainWindowHandle, ref rect);
User32.GetWindowRect(windowHandle, ref rect);
int width = rect.right - rect.left;
int height = rect.bottom - rect.top;
@ -124,6 +199,7 @@ namespace VDash.Overlay
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
{
@ -140,7 +216,6 @@ namespace VDash.Overlay
};
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;
@ -161,9 +236,10 @@ namespace VDash.Overlay
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;
}
}
}

View File

@ -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<Output1>();
// 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<Texture2D>())
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<byte[]> ScreenRefreshed;
}
}

View File

@ -84,7 +84,10 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Compile Include="ScreenStateLogger.cs" />
<Compile Include="Overlay\IOverlay.cs" />
<Compile Include="WindowSelectors\FromProcessName.cs" />
<Compile Include="WindowSelectors\FromWindowName.cs" />
<Compile Include="WindowSelectors\IWindowSelector.cs" />
<Page Include="MainWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
@ -101,7 +104,6 @@
</ItemGroup>
<ItemGroup>
<Compile Include="OpenVR\openvr_api.cs" />
<Compile Include="Overlay\DashOverlay.cs" />
<Compile Include="Overlay\WindowOverlay.cs" />
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>

View File

@ -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;
}
}
}

View File

@ -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
}
}
}

View File

@ -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<WindowInfo> windows = new List<WindowInfo>();
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();
}
}
}