diff --git a/app/App.config b/app/App.config
new file mode 100644
index 0000000..8e15646
--- /dev/null
+++ b/app/App.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/App.xaml b/app/App.xaml
new file mode 100644
index 0000000..1e29147
--- /dev/null
+++ b/app/App.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/app/App.xaml.cs b/app/App.xaml.cs
new file mode 100644
index 0000000..0eefccb
--- /dev/null
+++ b/app/App.xaml.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Threading.Tasks;
+using System.Threading;
+using System.Windows;
+using System.Linq;
+using System.Windows.Interop;
+using System.Windows.Media;
+
+namespace ParsecVDisplay
+{
+ public partial class App : Application
+ {
+ const string ID = "QpHOX8IBUHBznGtqk9xm1";
+ public const string NAME = "ParsecVDisplay";
+ public const string VERSION = "0.45.0";
+
+ public static bool Silent { get; private set; }
+
+ static App()
+ {
+ var displays = Display.GetAllDisplays();
+ return;
+ }
+
+ protected override void OnStartup(StartupEventArgs e)
+ {
+ if (e.Args.Length >= 2 && e.Args[0] == "-custom")
+ {
+ var modes = Display.ParseModes(e.Args[1]);
+ ParsecVDD.SetCustomDisplayModes(modes);
+
+ Shutdown();
+ return;
+ }
+
+ Silent = e.Args.Contains("-silent");
+
+ var signal = new EventWaitHandle(false,
+ EventResetMode.AutoReset, ID, out var isOwned);
+
+ if (!isOwned)
+ {
+ signal.Set();
+ Shutdown();
+ return;
+ }
+
+ var status = ParsecVDD.QueryStatus();
+ if (status != Device.Status.OK)
+ {
+ if (status == Device.Status.RESTART_REQUIRED)
+ {
+ MessageBox.Show("You must restart your PC to complete the driver setup.",
+ NAME, MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+ else if (status == Device.Status.DISABLED)
+ {
+ MessageBox.Show($"{ParsecVDD.ADAPTER} is disabled, please enable it.",
+ NAME, MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+ else if (status == Device.Status.NOT_INSTALLED)
+ {
+ MessageBox.Show("Please install the driver first.",
+ NAME, MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+ else
+ {
+ MessageBox.Show($"The driver is not OK, please check again. Current status: {status}.",
+ NAME, MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+
+ Shutdown();
+ return;
+ }
+
+ if (ParsecVDD.Init() == false)
+ {
+ MessageBox.Show("Failed to obtain the device handle, please check the driver installation again.",
+ NAME, MessageBoxButton.OK, MessageBoxImage.Warning);
+
+ Shutdown();
+ return;
+ }
+
+ Task.Run(() =>
+ {
+ while (signal.WaitOne())
+ {
+ Tray.ShowApp();
+ }
+ });
+
+ base.OnStartup(e);
+
+ // Disable GPU to prevent flickering when adding display
+ RenderOptions.ProcessRenderMode = RenderMode.SoftwareOnly;
+ }
+
+ protected override void OnExit(ExitEventArgs e)
+ {
+ ParsecVDD.Uninit();
+ base.OnExit(e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Components/Button.xaml b/app/Components/Button.xaml
new file mode 100644
index 0000000..5e43b8d
--- /dev/null
+++ b/app/Components/Button.xaml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/Components/Button.xaml.cs b/app/Components/Button.xaml.cs
new file mode 100644
index 0000000..3eb78c7
--- /dev/null
+++ b/app/Components/Button.xaml.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace ParsecVDisplay.Components
+{
+ public partial class Button : UserControl
+ {
+ public event EventHandler Click;
+ public object Children { get; set; }
+
+ public Button()
+ {
+ InitializeComponent();
+ DataContext = this;
+ }
+
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ Click?.Invoke(sender, e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Components/CloseButton.xaml b/app/Components/CloseButton.xaml
new file mode 100644
index 0000000..6d51eb1
--- /dev/null
+++ b/app/Components/CloseButton.xaml
@@ -0,0 +1,36 @@
+
+
+
\ No newline at end of file
diff --git a/app/Components/CloseButton.xaml.cs b/app/Components/CloseButton.xaml.cs
new file mode 100644
index 0000000..7173dee
--- /dev/null
+++ b/app/Components/CloseButton.xaml.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace ParsecVDisplay.Components
+{
+ public partial class CloseButton : UserControl
+ {
+ public CloseButton()
+ {
+ InitializeComponent();
+ }
+
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ var window = Window.GetWindow(this);
+ window?.Close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Components/CustomPage.xaml b/app/Components/CustomPage.xaml
new file mode 100644
index 0000000..72df497
--- /dev/null
+++ b/app/Components/CustomPage.xaml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/Components/CustomPage.xaml.cs b/app/Components/CustomPage.xaml.cs
new file mode 100644
index 0000000..699ab31
--- /dev/null
+++ b/app/Components/CustomPage.xaml.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+
+namespace ParsecVDisplay.Components
+{
+ public partial class CustomPage : Page
+ {
+ TextBox[] TextBoxes;
+
+ public CustomPage()
+ {
+ InitializeComponent();
+ }
+
+ private void ApplyChanges(object sender, EventArgs e)
+ {
+ var modes = new List();
+
+ for (int i = 0; i < 15; i += 3)
+ {
+ var tw = TextBoxes[i].Text;
+ var th = TextBoxes[i + 1].Text;
+ var thz = TextBoxes[i + 2].Text;
+
+ if (int.TryParse(tw, out var width)
+ && int.TryParse(th, out var height)
+ && int.TryParse(thz, out var hz))
+ {
+ // Check negative values & limit 8K resolution
+ if (width < 0 || width > 7680 || height < 0 || height > 4320 || hz < 0)
+ {
+ MessageBox.Show($"Found invalid value in slot {i / 3 + 1}.",
+ App.NAME, MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+ else
+ {
+ modes.Add(new Display.Mode(width, height, hz));
+ }
+ }
+ }
+
+ if (modes.Count > 0)
+ {
+ if (Helper.IsAdmin())
+ {
+ ParsecVDD.SetCustomDisplayModes(modes);
+ }
+ else
+ {
+ var args = $"-custom \"{Display.DumpModes(modes)}\"";
+ if (Helper.RunAdminTask(args) == false)
+ {
+ MessageBox.Show("Could not set custom resolutions, access denied!",
+ App.NAME, MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+ }
+ }
+
+ Window.GetWindow(this)?.Close();
+ }
+
+ private void Page_Loaded(object sender, RoutedEventArgs e)
+ {
+ TextBoxes = FindVisualChildren(this).ToArray();
+
+ var modes = ParsecVDD.GetCustomDisplayModes();
+
+ for (int i = 0, j = 0; i < 15 && j < modes.Count; i += 3, j++)
+ {
+ TextBoxes[i].Text = $"{modes[j].Width}";
+ TextBoxes[i + 1].Text = $"{modes[j].Height}";
+ TextBoxes[i + 2].Text = $"{modes[j].Hz}";
+ }
+ }
+
+ private void Page_Unloaded(object sender, RoutedEventArgs e)
+ {
+ }
+
+ static IEnumerable FindVisualChildren(DependencyObject depObj) where T : DependencyObject
+ {
+ if (depObj == null) yield return (T)Enumerable.Empty();
+ for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
+ {
+ DependencyObject ithChild = VisualTreeHelper.GetChild(depObj, i);
+ if (ithChild == null) continue;
+ if (ithChild is T t) yield return t;
+ foreach (T childOfChild in FindVisualChildren(ithChild)) yield return childOfChild;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Components/DisplayItem.xaml b/app/Components/DisplayItem.xaml
new file mode 100644
index 0000000..05d438c
--- /dev/null
+++ b/app/Components/DisplayItem.xaml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/Components/DisplayItem.xaml.cs b/app/Components/DisplayItem.xaml.cs
new file mode 100644
index 0000000..172a7c6
--- /dev/null
+++ b/app/Components/DisplayItem.xaml.cs
@@ -0,0 +1,205 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace ParsecVDisplay.Components
+{
+ public partial class DisplayItem : UserControl
+ {
+ int Index = -1;
+ public bool Active { get; set; } = true;
+ public string DisplayNum { get; set; } = "1";
+ public string DisplayName { get; set; } = "Display [1]";
+ public string DisplayPath { get; set; } = "\\\\.\\DISPLAY1";
+ public string DisplayMode { get; set; } = "1920 x 1080 @ 60 Hz";
+
+ Display Display;
+ Display.ModeSet SelectedResolution;
+
+ public DisplayItem()
+ {
+ InitializeComponent();
+
+ xResolution.Items.Clear();
+ xRefreshRate.Items.Clear();
+
+ DataContext = this;
+ }
+
+ internal DisplayItem(Display display) : this()
+ {
+ Display = display;
+
+ Active = display.Active;
+ Index = display.Address - 0x100;
+
+ DisplayNum = $"{display.Identifier}";
+ DisplayName = $"Display [{display.Identifier}]";
+ DisplayPath = display.DeviceName;
+
+ if (display.Active)
+ {
+ DisplayMode = display.CurrentMode.ToString();
+ }
+ else
+ {
+ DisplayMode = "[offline]";
+ }
+ }
+
+ bool UpdateRefreshRates()
+ {
+ xRefreshRate.Items.Clear();
+ var list = SelectedResolution.RefreshRates;
+
+ bool hasDefault = false;
+ MenuItem _60hz = null;
+
+ for (int i = 0; i < list.Count; i++)
+ {
+ int hz = list[i];
+
+ var mi = new MenuItem
+ {
+ Header = $"{hz} Hz",
+ IsCheckable = true,
+ IsChecked = Display.CurrentMode.Hz == hz,
+ };
+
+ if (hz == 60) _60hz = mi;
+ if (!hasDefault) hasDefault = mi.IsChecked;
+
+ xRefreshRate.Items.Add(mi);
+ }
+
+ if (!hasDefault && _60hz != null)
+ {
+ _60hz.IsChecked = true;
+ return true;
+ }
+
+ return false;
+ }
+
+ private void UserControl_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ e.Handled = true;
+ }
+
+ private void UserControl_MouseUp(object sender, MouseButtonEventArgs e)
+ {
+ e.Handled = true;
+ ContextMenu.DataContext = this;
+ ContextMenu.IsOpen = true;
+
+ if (Active && SelectedResolution == null)
+ {
+ foreach (var res in Display.SupportedResolutions)
+ {
+ bool @checked = Display.CurrentMode.Width == res.Width
+ && Display.CurrentMode.Height == res.Height;
+
+ if (@checked)
+ SelectedResolution = res;
+
+ xResolution.Items.Add(new MenuItem
+ {
+ IsCheckable = true,
+ IsChecked = @checked,
+ Header = $"{res.Width} × {res.Height}",
+ });
+ }
+
+ UpdateRefreshRates();
+
+ int oridentationIndex = (int)Display.CurrentOrientation;
+ (xOrientation.Items[oridentationIndex] as MenuItem).IsChecked = true;
+ }
+ }
+
+ private void ChangeResolution(object sender, RoutedEventArgs e)
+ {
+ if (Active && e.OriginalSource != null)
+ {
+ for (int i = 0; i < xResolution.Items.Count; i++)
+ {
+ var item = xResolution.Items[i] as MenuItem;
+ if (item == e.OriginalSource)
+ {
+ item.IsChecked = true;
+ SelectedResolution = Display.SupportedResolutions[i];
+ int hz = UpdateRefreshRates() ? 60 : Display.CurrentMode.Hz;
+ Display.ChangeMode(SelectedResolution.Width, SelectedResolution.Height, hz, null);
+ }
+ else
+ {
+ item.IsChecked = false;
+ }
+ }
+ }
+ }
+
+ private void ChangeOrientation(object sender, RoutedEventArgs e)
+ {
+ if (Active && e.OriginalSource != null)
+ {
+ for (int i = 0; i < xOrientation.Items.Count; i++)
+ {
+ var item = xOrientation.Items[i] as MenuItem;
+ if (item == e.OriginalSource)
+ {
+ item.IsChecked = true;
+ var orientation = (Display.Orientation)i;
+ Display.ChangeMode(null, null, null, orientation);
+ }
+ else
+ {
+ item.IsChecked = false;
+ }
+ }
+ }
+ }
+
+ private void ChangeRefreshRate(object sender, RoutedEventArgs e)
+ {
+ if (Active && e.OriginalSource != null)
+ {
+ for (int i = 0; i < xRefreshRate.Items.Count; i++)
+ {
+ var item = xRefreshRate.Items[i] as MenuItem;
+ if (item == e.OriginalSource)
+ {
+ item.IsChecked = true;
+ int hz = SelectedResolution.RefreshRates[i];
+ Display.ChangeMode(null, null, hz, null);
+ }
+ else
+ {
+ item.IsChecked = false;
+ }
+ }
+ }
+ }
+
+ private void TakeScreenshot(object sender, RoutedEventArgs e)
+ {
+ Task.Run(() =>
+ {
+ var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".png");
+ Display.TakeScreenshot(path);
+ Helper.ShellExec(path);
+ });
+ }
+
+ private void RemoveDisplay(object sender, RoutedEventArgs e)
+ {
+ if (Index != -1)
+ {
+ ParsecVDD.RemoveDisplay(Index);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Config.cs b/app/Config.cs
new file mode 100644
index 0000000..3429a87
--- /dev/null
+++ b/app/Config.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Reflection;
+using Microsoft.Win32;
+
+namespace ParsecVDisplay
+{
+ internal static class Config
+ {
+ static string REG_PATH => $"HKEY_CURRENT_USER\\SOFTWARE\\{App.NAME}";
+ static string REG_STARTUP_PATH => @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
+
+ public static int DisplayCount
+ {
+ get => GetInt(nameof(DisplayCount));
+ set => SetInt(nameof(DisplayCount), value);
+ }
+
+ public static bool FallbackDisplay
+ {
+ get => GetInt(nameof(FallbackDisplay)) != 0;
+ set => SetInt(nameof(FallbackDisplay), value ? 1 : 0);
+ }
+
+ public static bool KeepScreenOn
+ {
+ get
+ {
+ bool enable = GetInt(nameof(KeepScreenOn)) != 0;
+ Helper.StayAwake(enable);
+ return enable;
+ }
+ set
+ {
+ SetInt(nameof(KeepScreenOn), value ? 1 : 0);
+ Helper.StayAwake(value);
+ }
+ }
+
+ static int GetInt(string key, int @default = 0)
+ {
+ var value = Registry.GetValue(REG_PATH, key, null);
+ return value == null ? @default : Convert.ToInt32(value);
+ }
+
+ static void SetInt(string key, int value)
+ {
+ Registry.SetValue(REG_PATH, key, value, RegistryValueKind.DWord);
+ }
+
+ public static bool RunOnStartup
+ {
+ get
+ {
+ using (var key = Registry.CurrentUser.OpenSubKey(REG_STARTUP_PATH, false))
+ {
+ return key.GetValue(App.NAME) != null;
+ }
+ }
+ set
+ {
+ using (var key = Registry.CurrentUser.OpenSubKey(REG_STARTUP_PATH, true))
+ {
+ if (value)
+ {
+ var exePath = Assembly.GetExecutingAssembly().Location;
+ key.SetValue(App.NAME, $"\"{exePath}\" -silent", RegistryValueKind.String);
+ }
+ else
+ {
+ key.DeleteValue(App.NAME, false);
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Device.cs b/app/Device.cs
new file mode 100644
index 0000000..63a7de6
--- /dev/null
+++ b/app/Device.cs
@@ -0,0 +1,448 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace ParsecVDisplay
+{
+ internal static unsafe class Device
+ {
+ public enum Status
+ {
+ OK,
+ INACCESSIBLE,
+ UNKNOW,
+ UNKNOW_PROBLEM,
+ DISABLED,
+ DRIVER_ERROR,
+ RESTART_REQUIRED,
+ DISABLED_SERVICE,
+ NOT_INSTALLED
+ }
+
+ public static Status QueryStatus(string guid, string devId)
+ {
+ var status = Status.INACCESSIBLE;
+
+ var devInfoData = new Native.SP_DEVINFO_DATA();
+ devInfoData.cbSize = sizeof(Native.SP_DEVINFO_DATA);
+
+ var classGuid = Guid.Parse(guid);
+ var devInfo = Native.SetupDiGetClassDevsA(ref classGuid, null, null, Native.DIGCF_PRESENT);
+
+ if (devInfo != Native.INVALID_HANDLE_VALUE)
+ {
+ bool foundProp = false;
+ uint deviceIndex = 0;
+
+ do
+ {
+ if (!Native.SetupDiEnumDeviceInfo(devInfo, deviceIndex, &devInfoData))
+ break;
+
+ int requiredSize = 0;
+ Native.SetupDiGetDeviceRegistryPropertyA(devInfo, &devInfoData,
+ Native.SPDRP_HARDWAREID, null, null, 0, &requiredSize);
+
+ if (requiredSize > 0)
+ {
+ uint regDataType = 0;
+ IntPtr propBuffer = Marshal.AllocHGlobal(requiredSize);
+
+ if (Native.SetupDiGetDeviceRegistryPropertyA(
+ devInfo,
+ &devInfoData,
+ Native.SPDRP_HARDWAREID,
+ ®DataType,
+ (void*)propBuffer,
+ requiredSize,
+ &requiredSize))
+ {
+ if (regDataType == Native.REG_SZ || regDataType == Native.REG_MULTI_SZ)
+ {
+ for (IntPtr cp = propBuffer; ; cp += Native.lstrlenA(cp) + 1)
+ {
+ if (cp == (IntPtr)(0) || *(byte*)cp == 0 || (ulong)cp >= (ulong)(propBuffer + requiredSize))
+ {
+ status = Status.NOT_INSTALLED;
+ goto except;
+ }
+
+ if (devId.Equals(Marshal.PtrToStringAnsi(cp)))
+ break;
+ }
+
+ foundProp = true;
+ uint devStatus, devProblemNum;
+
+ if (Native.CM_Get_DevNode_Status(&devStatus, &devProblemNum, devInfoData.DevInst, 0) != Native.CR_SUCCESS)
+ {
+ status = Status.NOT_INSTALLED;
+ goto except;
+ }
+
+ if ((devStatus & (Native.DN_DRIVER_LOADED | Native.DN_STARTED)) != 0)
+ {
+ status = Status.OK;
+ }
+ else if ((devStatus & Native.DN_HAS_PROBLEM) != 0)
+ {
+ switch (devProblemNum)
+ {
+ case Native.CM_PROB_NEED_RESTART:
+ status = Status.RESTART_REQUIRED;
+ break;
+ case Native.CM_PROB_DISABLED:
+ case Native.CM_PROB_HARDWARE_DISABLED:
+ status = Status.DISABLED;
+ break;
+ case Native.CM_PROB_DISABLED_SERVICE:
+ status = Status.DISABLED_SERVICE;
+ break;
+ default:
+ if (devProblemNum == Native.CM_PROB_FAILED_POST_START)
+ status = Status.DRIVER_ERROR;
+ else
+ status = Status.UNKNOW_PROBLEM;
+ break;
+ }
+ }
+ else
+ {
+ status = Status.UNKNOW;
+ }
+ }
+ }
+
+ except:
+ Marshal.FreeHGlobal(propBuffer);
+ }
+
+ ++deviceIndex;
+ } while (!foundProp);
+
+ if (!foundProp && Marshal.GetLastWin32Error() != 0)
+ status = Status.NOT_INSTALLED;
+
+ Native.SetupDiDestroyDeviceInfoList(devInfo);
+ }
+
+ return status;
+ }
+
+ public static bool OpenHandle(string guid, out IntPtr handle)
+ {
+ handle = IntPtr.Zero;
+
+ var interfaceGuid = Guid.Parse(guid);
+ var devInfo = Native.SetupDiGetClassDevsA(ref interfaceGuid,
+ null, null, Native.DIGCF_PRESENT | Native.DIGCF_DEVICEINTERFACE);
+
+ if (devInfo != Native.INVALID_HANDLE_VALUE)
+ {
+ var devInterface = new Native.SP_DEVICE_INTERFACE_DATA();
+ devInterface.cbSize = sizeof(Native.SP_DEVICE_INTERFACE_DATA);
+
+ for (uint i = 0; Native.SetupDiEnumDeviceInterfaces(devInfo, null, ref interfaceGuid, i, &devInterface); ++i)
+ {
+ int detailSize = 0;
+ Native.SetupDiGetDeviceInterfaceDetailA(devInfo, &devInterface, null, 0, &detailSize, null);
+
+ var detail = (Native.SP_DEVICE_INTERFACE_DETAIL_DATA_A*)Marshal.AllocHGlobal(detailSize);
+ detail->cbSize = sizeof(Native.SP_DEVICE_INTERFACE_DETAIL_DATA_A);
+
+ if (Native.SetupDiGetDeviceInterfaceDetailA(devInfo, &devInterface, detail, detailSize, &detailSize, null))
+ {
+ handle = Native.CreateFileA(&detail->DevicePath,
+ Native.GENERIC_READ | Native.GENERIC_WRITE,
+ Native.FILE_SHARE_READ | Native.FILE_SHARE_WRITE,
+ null,
+ Native.OPEN_EXISTING,
+ Native.FILE_ATTRIBUTE_NORMAL | Native.FILE_FLAG_NO_BUFFERING | Native.FILE_FLAG_OVERLAPPED | Native.FILE_FLAG_WRITE_THROUGH,
+ null);
+
+ if (handle != IntPtr.Zero && handle != Native.INVALID_HANDLE_VALUE)
+ break;
+ }
+
+ Marshal.FreeHGlobal((IntPtr)detail);
+ }
+
+ Native.SetupDiDestroyDeviceInfoList(devInfo);
+ }
+
+ return handle != IntPtr.Zero
+ && handle != Native.INVALID_HANDLE_VALUE;
+ }
+
+ public static void CloseHandle(IntPtr handle)
+ {
+ if (handle != IntPtr.Zero && handle != Native.INVALID_HANDLE_VALUE)
+ {
+ Native.CloseHandle(handle);
+ }
+ }
+
+ public static string GetDeviceDescription(uint devInst)
+ {
+ uint propType;
+ int length = 128 * sizeof(ushort);
+ var buffer = stackalloc byte[length];
+
+ Native.CM_Get_DevNode_PropertyW(devInst,
+ ref Native.DEVPROPKEY.Device_DeviceDesc, &propType, buffer, &length, 0);
+
+ return Marshal.PtrToStringUni((IntPtr)buffer);
+ }
+
+ public static DateTime GetDeviceLastArrival(uint devInst)
+ {
+ uint propType;
+ long lastArrival;
+ int bufferLength = sizeof(long);
+
+ Native.CM_Get_DevNode_PropertyW(devInst,
+ ref Native.DEVPROPKEY.Device_LastArrivalDate, &propType, &lastArrival, &bufferLength, 0);
+
+ return DateTime.FromFileTime(lastArrival);
+ }
+
+ public static bool GetParentDeviceInstance(uint devInst, out uint parentInst, out string parentId)
+ {
+ if (Native.CM_Get_Parent(out parentInst, devInst, 0) == 0)
+ {
+ byte[] idBuffer = new byte[Native.MAX_DEVICE_ID_LEN];
+ Native.CM_Get_Device_IDA(parentInst, idBuffer, idBuffer.Length, 0);
+
+ parentId = Encoding.ASCII.GetString(idBuffer).TrimEnd('\0');
+ return true;
+ }
+
+ parentInst = 0;
+ parentId = string.Empty;
+ return false;
+ }
+
+ public static bool GetDeviceInstance(string deviceId, out uint devInst)
+ {
+ return Native.CM_Locate_DevNodeA(out devInst, deviceId, 0) == 0;
+ }
+
+ static class Native
+ {
+ public const int MAX_DEVICE_ID_LEN = 200;
+ public static readonly IntPtr INVALID_HANDLE_VALUE = (IntPtr)(-1);
+
+ public const uint GENERIC_READ = 0x80000000;
+ public const uint GENERIC_WRITE = 0x40000000;
+
+ public const uint FILE_SHARE_READ = 0x1;
+ public const uint FILE_SHARE_WRITE = 0x2;
+
+ public const uint OPEN_EXISTING = 3;
+
+ public const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;
+ public const uint FILE_FLAG_NO_BUFFERING = 0x20000000;
+ public const uint FILE_FLAG_OVERLAPPED = 0x40000000;
+ public const uint FILE_FLAG_WRITE_THROUGH = 0x80000000;
+
+ public const uint DIGCF_PRESENT = 0x2;
+ public const uint DIGCF_DEVICEINTERFACE = 0x10;
+
+ public const uint SPDRP_HARDWAREID = 0x1;
+
+ public const uint REG_SZ = 1;
+ public const uint REG_MULTI_SZ = 7;
+
+ public const uint CR_SUCCESS = 0;
+
+ public const uint CM_PROB_NEED_RESTART = 0x0000000E; // requires restart
+ public const uint CM_PROB_DISABLED = 0x00000016; // devinst is disabled
+ public const uint CM_PROB_HARDWARE_DISABLED = 0x0000001D; // device disabled
+ public const uint CM_PROB_DISABLED_SERVICE = 0x00000020; // service's Start = 4
+ public const uint CM_PROB_FAILED_POST_START = 0x0000002B; // The drivers set the device state to failed
+
+ public const uint DN_DRIVER_LOADED = 0x00000002; // Has Register_Device_Driver
+ public const uint DN_STARTED = 0x00000008; // Is currently configured
+ public const uint DN_HAS_PROBLEM = 0x00000400; // Need device installer
+
+ public static bool IsValidHandle(IntPtr handle)
+ {
+ return handle != IntPtr.Zero
+ && handle != (IntPtr)(-1);
+ }
+
+ [DllImport("kernel32.dll", CallingConvention = CallingConvention.Cdecl)]
+ public static extern int lstrlenA(
+ IntPtr lpString);
+
+ [DllImport("kernel32.dll")]
+ public static extern IntPtr CreateFileA(
+ char* lpFileName,
+ uint dwDesiredAccess,
+ uint dwShareMode,
+ void* lpSecurityAttributes,
+ uint dwCreationDisposition,
+ uint dwFlagsAndAttributes,
+ void* hTemplateFile);
+
+ [DllImport("kernel32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool CloseHandle(IntPtr handle);
+
+ [DllImport("kernel32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool DeviceIoControl(
+ IntPtr device, uint code,
+ void* lpInBuffer, int nInBufferSize,
+ void* lpOutBuffer, int nOutBufferSize,
+ IntPtr lpBytesReturned,
+ ref OVERLAPPED lpOverlapped
+ );
+
+ [DllImport("kernel32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool GetOverlappedResult(
+ IntPtr handle,
+ ref OVERLAPPED lpOverlapped,
+ out uint lpNumberOfBytesTransferred,
+ [MarshalAs(UnmanagedType.Bool)] bool bWait
+ );
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct OVERLAPPED
+ {
+ public IntPtr Internal;
+ public IntPtr InternalHigh;
+ public IntPtr Pointer;
+ public IntPtr hEvent;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct RECT
+ {
+ public int left;
+ public int top;
+ public int right;
+ public int bottom;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SP_DEVICE_INTERFACE_DATA
+ {
+ public int cbSize;
+ public Guid InterfaceClassGuid;
+ public uint Flags;
+ IntPtr _Reserved;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SP_DEVINFO_DATA
+ {
+ public int cbSize;
+ public Guid ClassGuid;
+ public uint DevInst;
+ IntPtr _Reserved;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SP_DEVICE_INTERFACE_DETAIL_DATA_A
+ {
+ public int cbSize;
+ public char DevicePath;
+ }
+
+ [DllImport("setupapi.dll")]
+ public static extern IntPtr SetupDiGetClassDevsA(
+ ref Guid ClassGuid,
+ void* Enumerator,
+ void* hwndParent,
+ uint Flags);
+
+ [DllImport("setupapi.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool SetupDiEnumDeviceInterfaces(
+ IntPtr DeviceInfoSet,
+ SP_DEVINFO_DATA* DeviceInfoData,
+ ref Guid InterfaceClassGuid,
+ uint MemberIndex,
+ SP_DEVICE_INTERFACE_DATA* DeviceInterfaceData);
+
+ [DllImport("setupapi.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool SetupDiGetDeviceInterfaceDetailA(
+ IntPtr DeviceInfoSet,
+ SP_DEVICE_INTERFACE_DATA* DeviceInterfaceData,
+ void* DeviceInterfaceDetailData,
+ int DeviceInterfaceDetailDataSize,
+ int* RequiredSize,
+ SP_DEVINFO_DATA* DeviceInfoData);
+
+ [DllImport("setupapi.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool SetupDiDestroyDeviceInfoList(
+ IntPtr DeviceInfoSet);
+
+ [DllImport("setupapi.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool SetupDiEnumDeviceInfo(
+ IntPtr DeviceInfoSet,
+ uint MemberIndex,
+ SP_DEVINFO_DATA* DeviceInfoData);
+
+ [DllImport("setupapi.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool SetupDiGetDeviceRegistryPropertyA(
+ IntPtr DeviceInfoSet,
+ SP_DEVINFO_DATA* DeviceInfoData,
+ uint Property,
+ uint* PropertyRegDataType,
+ void* PropertyBuffer,
+ int PropertyBufferSize,
+ int* RequiredSize);
+
+ [DllImport("setupapi.dll")]
+ public static extern uint CM_Get_DevNode_Status(
+ uint* pulStatus,
+ uint* pulProblemNumber,
+ uint dnDevInst,
+ uint ulFlags);
+
+ [DllImport("setupapi.dll")]
+ public static extern uint CM_Get_Parent(out uint pdnDevInst, uint dnDevInst, uint ulFlags);
+
+ [DllImport("setupapi.dll")]
+ public static extern uint CM_Get_Device_IDA(uint dnDevInst, byte[] Buffer, int BufferLen, uint ulFlags);
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct DEVPROPKEY
+ {
+ public Guid fmtid;
+ public uint pid;
+
+ public static DEVPROPKEY Device_LastArrivalDate = new DEVPROPKEY
+ {
+ fmtid = Guid.Parse("{83DA6326-97A6-4088-9453-A1923F573B29}"),
+ pid = 102,
+ };
+
+ public static DEVPROPKEY Device_DeviceDesc = new DEVPROPKEY
+ {
+ fmtid = Guid.Parse("{A45C254E-DF1C-4EFD-8020-67D146A850E0}"),
+ pid = 2
+ };
+ }
+
+ [DllImport("CfgMgr32.dll")]
+ public static extern uint CM_Get_DevNode_PropertyW(
+ uint dnDevInst,
+ ref DEVPROPKEY PropertyKey,
+ uint* PropertyType,
+ void* PropertyBuffer,
+ int* PropertyBufferSize,
+ uint ulFlags);
+
+ [DllImport("CfgMgr32.dll", CharSet = CharSet.Ansi)]
+ public static extern uint CM_Locate_DevNodeA(out uint devInst, string deviceId, uint flags);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Display.cs b/app/Display.cs
new file mode 100644
index 0000000..521971e
--- /dev/null
+++ b/app/Display.cs
@@ -0,0 +1,411 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Microsoft.Win32;
+
+namespace ParsecVDisplay
+{
+ internal class Display
+ {
+ public enum Orientation
+ {
+ Angle0 = 0, // landscape
+ Angle90, // portrait
+ Angle180, // landscape (flipped)
+ Angle270 // portrait (flipped)
+ }
+
+ public class Mode
+ {
+ public int Width;
+ public int Height;
+ public int Hz;
+
+ public Mode()
+ {
+ }
+
+ public Mode(int width, int height, int hz)
+ {
+ Width = width;
+ Height = height;
+ Hz = hz;
+ }
+
+ public Mode(ulong bits)
+ {
+ Width = unchecked((ushort)bits);
+ Height = unchecked((ushort)(bits >> 16));
+ Hz = unchecked((ushort)(bits >> 32));
+ }
+
+ public ulong Bits => (uint)(Width & 0xFFFF)
+ | ((ulong)(Height & 0xFFFF) << 16)
+ | ((ulong)(Hz & 0xFFFF) << 32);
+
+ public string Resolution => $"{Width} × {Height}";
+ public string RefreshRate => $"{Hz} Hz";
+ public override string ToString() => $"{Resolution} @ {RefreshRate}";
+ }
+
+ public class ModeSet
+ {
+ public int Width;
+ public int Height;
+ public List RefreshRates;
+ }
+
+ public bool Active;
+ public int Identifier;
+ public int CloneOf;
+ public int Address;
+ public DateTime LastArrival;
+
+ public string Adapter;
+ public string AdapterInstance;
+ public DateTime AdapterArrival;
+
+ public string DeviceName;
+ public string DisplayName;
+
+ public Mode CurrentMode;
+ public Orientation CurrentOrientation;
+ public List ModeList;
+ public List SupportedResolutions;
+
+ Display()
+ {
+ ModeList = new List();
+ CurrentOrientation = Orientation.Angle0;
+ SupportedResolutions = new List();
+ }
+
+ public override string ToString()
+ {
+ var str = $"[{Identifier}] {DeviceName} ({DisplayName}#{Address})";
+ if (CloneOf > 0 && CloneOf < Identifier) str += $" (clone of [{CloneOf}])";
+ return str;
+ }
+
+ void FetchAllModes()
+ {
+ var devMode = new Native.DEVMODE();
+ devMode.dmSize = (short)Marshal.SizeOf(typeof(Native.DEVMODE));
+
+ var set = new Dictionary>();
+
+ for (int num = -1; Native.EnumDisplaySettings(DeviceName, num, ref devMode); num++)
+ {
+ var mode = new Mode
+ {
+ Width = devMode.dmPelsWidth,
+ Height = devMode.dmPelsHeight,
+ Hz = devMode.dmDisplayFrequency,
+ };
+
+ if (num == -1)
+ {
+ CurrentMode = mode;
+ CurrentOrientation = devMode.dmDisplayOrientation;
+ }
+ else
+ {
+ ModeList.Add(mode);
+
+ mode.Hz = 0;
+ if (set.TryGetValue(mode.Bits, out var rrs))
+ {
+ rrs.Add(devMode.dmDisplayFrequency);
+ }
+ else
+ {
+ set[mode.Bits] = new HashSet { devMode.dmDisplayFrequency };
+ }
+ }
+ }
+
+ foreach (var kv in set)
+ {
+ var mode = new Mode(kv.Key);
+ var rrs = kv.Value.ToList();
+ rrs.Sort((a, b) => a - b);
+
+ SupportedResolutions.Add(new ModeSet
+ {
+ Width = mode.Width,
+ Height = mode.Height,
+ RefreshRates = rrs,
+ });
+ }
+
+ SupportedResolutions.Sort((a, b)
+ => b.Width == a.Width ? (b.Height - a.Height) : (b.Width - a.Width));
+ }
+
+ public bool ChangeMode(int? width, int? height, int? hz, Orientation? orientation)
+ {
+ var mode = new Native.DEVMODE();
+ mode.dmSize = (short)Marshal.SizeOf(typeof(Native.DEVMODE));
+
+ if (Native.EnumDisplaySettings(DeviceName, -1, ref mode))
+ {
+ if (width.HasValue) mode.dmPelsWidth = width.Value;
+ if (height.HasValue) mode.dmPelsHeight = height.Value;
+ if (hz.HasValue) mode.dmDisplayFrequency = hz.Value;
+
+ if (orientation.HasValue)
+ {
+ var newDO = orientation.Value;
+ mode.dmDisplayOrientation = newDO;
+
+ if (((int)newDO + (int)CurrentOrientation) % 2 != 0)
+ {
+ int t = mode.dmPelsWidth;
+ mode.dmPelsWidth = mode.dmPelsHeight;
+ mode.dmPelsHeight = t;
+ }
+ }
+
+ return Native.ChangeDisplaySettingsEx(DeviceName,
+ ref mode, IntPtr.Zero, 1, IntPtr.Zero) == 0;
+ }
+
+ return false;
+ }
+
+ public void TakeScreenshot(string saveFile)
+ {
+ int width = CurrentMode.Width;
+ int height = CurrentMode.Height;
+
+ using (var bmp = new Bitmap(width, height))
+ using (var gfx = Graphics.FromImage(bmp))
+ {
+ var hdc = Native.CreateDC(IntPtr.Zero, DeviceName, IntPtr.Zero, IntPtr.Zero);
+
+ var dstHdc = gfx.GetHdc();
+ Native.BitBlt(dstHdc, 0, 0, width, height, hdc, 0, 0, 0x00CC0020);
+ gfx.ReleaseHdc(dstHdc);
+
+ bmp.Save(saveFile, System.Drawing.Imaging.ImageFormat.Png);
+ Native.DeleteDC(hdc);
+ }
+ }
+
+ public static string DumpModes(List modes)
+ {
+ return string.Join(",", modes.Select(m => m.Bits.ToString("x")));
+ }
+
+ public static List ParseModes(string modes)
+ {
+ var list = new List();
+ var tokens = modes.Trim().Split(',');
+ foreach (var mode in tokens)
+ if (ulong.TryParse(mode, NumberStyles.HexNumber, null, out var bits))
+ list.Add(new Mode(bits));
+
+ return list;
+ }
+
+ public static List GetAllDisplays()
+ {
+ var displayMap = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var cloneGroups = new List>();
+
+ var paths = GetDisplayPaths();
+
+ var dd = new Native.DISPLAY_DEVICE();
+ dd.cb = Marshal.SizeOf(typeof(Native.DISPLAY_DEVICE));
+
+ for (int i = 0; Native.EnumDisplayDevices(null, i, ref dd, 0); i++)
+ {
+ Display prevActiveDisplay = null;
+
+ var dd2 = new Native.DISPLAY_DEVICE();
+ dd2.cb = Marshal.SizeOf(typeof(Native.DISPLAY_DEVICE));
+
+ for (int j = 0; Native.EnumDisplayDevices(dd.DeviceName, j, ref dd2, Native.EDD_GET_DEVICE_INTERFACE_NAME); j++)
+ {
+ if ((dd2.StateFlags & Native.DISPLAY_DEVICE_ATTACHED) == 0)
+ continue;
+
+ var pathIdx = paths.FindIndex(p => dd2.DeviceID.Contains(p.Replace('\\', '#')));
+ if (pathIdx < 0) continue;
+
+ if (!displayMap.ContainsKey(paths[pathIdx]))
+ {
+ var display = new Display
+ {
+ Active = (dd2.StateFlags & Native.DISPLAY_DEVICE_ACTIVE) != 0,
+ Address = ParseDisplayAddress(paths[pathIdx]),
+ DeviceName = dd.DeviceName,
+ DisplayName = ParseDisplayCode(dd2.DeviceID),
+ };
+
+ if (display.Active)
+ {
+ if (prevActiveDisplay == null)
+ {
+ prevActiveDisplay = display;
+ }
+ else
+ {
+ cloneGroups.Add(new Tuple(prevActiveDisplay, display));
+ }
+
+ display.FetchAllModes();
+ }
+
+ Device.GetDeviceInstance(paths[pathIdx], out uint devInst);
+ display.LastArrival = Device.GetDeviceLastArrival(devInst);
+
+ Device.GetParentDeviceInstance(devInst, out uint parentInst, out display.AdapterInstance);
+ display.Adapter = Device.GetDeviceDescription(parentInst);
+ display.AdapterArrival = Device.GetDeviceLastArrival(parentInst);
+
+ displayMap.Add(paths[pathIdx], display);
+ paths.RemoveAt(pathIdx);
+ }
+ }
+ }
+
+ var displays = displayMap.Values.ToList();
+
+ // Sort displays by adapter arrival time
+ displays.Sort((a, b) =>
+ {
+ if (a.AdapterInstance == b.AdapterInstance)
+ return a.LastArrival.CompareTo(b.LastArrival);
+ return a.AdapterArrival.CompareTo(b.AdapterArrival);
+ });
+
+ // Fill display identifier
+ for (int i = 0; i < displays.Count; i++)
+ displays[i].Identifier = i + 1;
+
+ // Fill clone of identifier
+ foreach (var group in cloneGroups)
+ {
+ group.Item1.CloneOf = group.Item2.Identifier;
+ group.Item2.CloneOf = group.Item1.Identifier;
+ }
+
+ cloneGroups.Clear();
+ return displays;
+ }
+
+ static List GetDisplayPaths()
+ {
+ using (var key = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Services\monitor\Enum", false))
+ {
+ var paths = new List();
+ int count = Convert.ToInt32(key.GetValue("Count", 0));
+
+ for (int i = 0; i < count; ++i)
+ {
+ var path = key.GetValue($"{i}");
+ paths.Add(Convert.ToString(path));
+ }
+
+ return paths;
+ }
+ }
+
+ static int ParseDisplayAddress(string path)
+ {
+ var index = path.LastIndexOf("uid", StringComparison.OrdinalIgnoreCase);
+ int.TryParse(path.Substring(index + 3), out var address);
+ return address;
+ }
+
+ static string ParseDisplayCode(string id)
+ {
+ var tokens = id.Split('#');
+ return tokens.Length >= 2 ? tokens[1] : tokens[0];
+ }
+
+ static class Native
+ {
+ public const uint EDD_GET_DEVICE_INTERFACE_NAME = 0x1;
+ public const uint DISPLAY_DEVICE_ACTIVE = 0x1;
+ public const uint DISPLAY_DEVICE_ATTACHED = 0x2;
+
+ [DllImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool EnumDisplayDevices(string lpDevice, int iDevNum, ref DISPLAY_DEVICE lpDisplayDevice, uint dwFlags);
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
+ public struct DISPLAY_DEVICE
+ {
+ public int cb;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
+ public string DeviceName;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
+ public string DeviceString;
+ public uint StateFlags;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
+ public string DeviceID;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
+ public string DeviceKey;
+ }
+
+ [DllImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE devMode);
+
+ [DllImport("user32.dll")]
+ public static extern int ChangeDisplaySettingsEx(string lpszDeviceName, ref DEVMODE lpDevMode,
+ IntPtr hwnd, uint dwflags, IntPtr lParam);
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct DEVMODE
+ {
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
+ public string dmDeviceName;
+ public short dmSpecVersion;
+ public short dmDriverVersion;
+ public short dmSize;
+ public short dmDriverExtra;
+ public int dmFields;
+ public int dmPositionX;
+ public int dmPositionY;
+ public Orientation dmDisplayOrientation;
+ public int dmDisplayFixedOutput;
+ public short dmColor;
+ public short dmDuplex;
+ public short dmYResolution;
+ public short dmTTOption;
+ public short dmCollate;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
+ public string dmFormName;
+ public short dmLogPixels;
+ public int dmBitsPerPel;
+ public int dmPelsWidth;
+ public int dmPelsHeight;
+ public int dmDisplayFlags;
+ public int dmDisplayFrequency;
+ public int dmICMMethod;
+ public int dmICMIntent;
+ public int dmMediaType;
+ public int dmDitherType;
+ public int dmReserved1;
+ public int dmReserved2;
+ public int dmPanningWidth;
+ public int dmPanningHeight;
+ }
+
+ [DllImport("gdi32.dll")]
+ public static extern IntPtr CreateDC(IntPtr pwszDriver, string pwszDevice, IntPtr pszPort, IntPtr devmode);
+
+ [DllImport("gdi32.dll")]
+ public static extern int BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, int rasterOperation);
+
+ [DllImport("gdi32.dll")]
+ public static extern int DeleteDC(IntPtr hdc);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Helper.cs b/app/Helper.cs
new file mode 100644
index 0000000..5b56f80
--- /dev/null
+++ b/app/Helper.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Security.Principal;
+
+namespace ParsecVDisplay
+{
+ internal static class Helper
+ {
+ public static bool ShellExec(string file, string args = "", string cwd = null, bool admin = false)
+ {
+ try
+ {
+ var a = Assembly.GetAssembly(typeof(Process));
+ var _p = a.GetType("System.Diagnostics.Process");
+ var _psi = a.GetType("System.Diagnostics.ProcessStartInfo");
+
+ var psi = Activator.CreateInstance(_psi);
+ _psi.GetProperty("FileName").SetValue(psi, file);
+ _psi.GetProperty("UseShellExecute").SetValue(psi, true);
+
+ if (!string.IsNullOrEmpty(args))
+ _psi.GetProperty("Arguments").SetValue(psi, args);
+ if (!string.IsNullOrEmpty(cwd))
+ _psi.GetProperty("WorkingDirectory").SetValue(psi, cwd);
+ if (admin)
+ _psi.GetProperty("Verb").SetValue(psi, "runas");
+
+ var s = _p.GetMethod("Start", new Type[] { _psi });
+ s.Invoke(null, new object[] { psi });
+
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ public static void OpenLink(string url)
+ {
+ if (!string.IsNullOrEmpty(url) && url.StartsWith("https://"))
+ ShellExec(url);
+ }
+
+ public static bool IsAdmin()
+ {
+ using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
+ {
+ WindowsPrincipal principal = new WindowsPrincipal(identity);
+ return principal.IsInRole(WindowsBuiltInRole.Administrator);
+ }
+ }
+
+ public static bool RunAdminTask(string args)
+ {
+ var exe = Assembly.GetExecutingAssembly().Location;
+ var cwd = Path.GetDirectoryName(exe);
+
+ return ShellExec(exe, args, cwd, true);
+ }
+
+ public static void StayAwake(bool enable)
+ {
+ const uint ES_CONTINUOUS = 0x80000000;
+ const uint ES_DISPLAY_REQUIRED = 0x00000002;
+
+ uint flags = ES_CONTINUOUS;
+ if (enable) flags |= ES_DISPLAY_REQUIRED;
+
+ SetThreadExecutionState(flags);
+ }
+
+ [DllImport("kernel32.dll")]
+ static extern int SetThreadExecutionState(uint esFlags);
+ }
+}
\ No newline at end of file
diff --git a/app/MainWindow.xaml b/app/MainWindow.xaml
new file mode 100644
index 0000000..f93d6ab
--- /dev/null
+++ b/app/MainWindow.xaml
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/MainWindow.xaml.cs b/app/MainWindow.xaml.cs
new file mode 100644
index 0000000..046b8fd
--- /dev/null
+++ b/app/MainWindow.xaml.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Interop;
+using System.Windows.Navigation;
+
+namespace ParsecVDisplay
+{
+ public partial class MainWindow : Window
+ {
+ public MainWindow()
+ {
+ InitializeComponent();
+ xAppName.Content += $" v{App.VERSION}";
+
+ // prevent frame history
+ xFrame.Navigating += (_, e) => { e.Cancel = e.NavigationMode != NavigationMode.New; };
+ xFrame.Navigated += (_, e) => { xFrame.NavigationService.RemoveBackEntry(); };
+
+ xDisplays.Children.Clear();
+ xNoDisplay.Visibility = Visibility.Hidden;
+ }
+
+ protected override void OnSourceInitialized(EventArgs e)
+ {
+ base.OnSourceInitialized(e);
+
+ var hwnd = new WindowInteropHelper(this).EnsureHandle();
+ Shadow.ApplyShadow(hwnd);
+ }
+
+ protected override void OnClosing(CancelEventArgs e)
+ {
+ e.Cancel = true;
+
+ if (xFrame.Content != null)
+ {
+ xFrame.Visibility = Visibility.Hidden;
+ xFrame.Content = null;
+ xDisplays.Visibility = Visibility.Visible;
+ xButtons.Visibility = Visibility.Visible;
+ }
+ else
+ {
+ this.Hide();
+ }
+ }
+
+ private void Grid_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.LeftButton == MouseButtonState.Pressed)
+ DragMove();
+ }
+
+ private void Window_Loaded(object sender, RoutedEventArgs e)
+ {
+ Loaded -= Window_Loaded;
+
+ if (App.Silent)
+ Hide();
+
+ ContextMenu.DataContext = this;
+ Tray.Init(this, ContextMenu);
+ ContextMenu = null;
+
+ ParsecVDD.DisplayChanged += DisplayChanged;
+ ParsecVDD.Invalidate();
+ }
+
+ private void Window_Unloaded(object sender, RoutedEventArgs e)
+ {
+ ParsecVDD.DisplayChanged -= DisplayChanged;
+ Tray.Uninit();
+ }
+
+ private void DisplayChanged(List displays, bool noMonitors)
+ {
+ xDisplays.Children.Clear();
+ xNoDisplay.Visibility = displays.Count <= 0 ? Visibility.Visible : Visibility.Hidden;
+
+ foreach (var display in displays)
+ {
+ var item = new Components.DisplayItem(display);
+ xDisplays.Children.Add(item);
+ }
+
+ xAdd.IsEnabled = true;
+
+ if (noMonitors && Config.FallbackDisplay)
+ {
+ AddDisplay(null, EventArgs.Empty);
+ }
+ }
+
+ private void AddDisplay(object sender, EventArgs e)
+ {
+ if (ParsecVDD.DisplayCount >= ParsecVDD.MAX_DISPLAYS)
+ {
+ MessageBox.Show(this,
+ $"Could not add more virtual displays, you have exceeded the maximum number ({ParsecVDD.MAX_DISPLAYS}).",
+ Title, MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+ else
+ {
+ ParsecVDD.AddDisplay();
+ xAdd.IsEnabled = false;
+ }
+ }
+
+ private void RemoveLastDisplay(object sender, EventArgs e)
+ {
+ xAdd.IsEnabled = false;
+ ParsecVDD.RemoveLastDisplay();
+ }
+
+ private void OpenCustom(object sender, EventArgs e)
+ {
+ xDisplays.Visibility = Visibility.Hidden;
+ xButtons.Visibility = Visibility.Hidden;
+ xFrame.Content = new Components.CustomPage();
+ xFrame.Visibility = Visibility.Visible;
+ }
+
+ private void OpenSettings(object sender, EventArgs e)
+ {
+ Helper.ShellExec("ms-settings:display");
+ }
+
+ private void SyncSettings(object sender, EventArgs e)
+ {
+ xAdd.IsEnabled = false;
+ xDisplays.Children.Clear();
+
+ ParsecVDD.Invalidate();
+ }
+
+ private void QueryStatus(object sender, EventArgs e)
+ {
+ if (e is MouseEventArgs mbe)
+ mbe.Handled = true;
+
+ Tray.ShowApp();
+
+ var status = ParsecVDD.QueryStatus();
+ var version = ParsecVDD.QueryVersion();
+
+ MessageBox.Show(this,
+ $"Parsec Virtual Display v{version}\nDriver status: {status}",
+ App.NAME, MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+
+ private void ExitApp(object sender, EventArgs e)
+ {
+ if (ParsecVDD.DisplayCount > 0)
+ if (MessageBox.Show(this,
+ "All added virtual displays will be unplugged.\nDo you still want to exit?",
+ App.NAME, MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes)
+ return;
+
+ Tray.Uninit();
+ Application.Current.Shutdown();
+ }
+
+ private void OpenRepoLink(object sender, MouseButtonEventArgs e)
+ {
+ e.Handled = true;
+ Helper.OpenLink("https://github.com/nomi-san/parsec-vdd");
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/ParsecVDD.cs b/app/ParsecVDD.cs
new file mode 100644
index 0000000..8197d4b
--- /dev/null
+++ b/app/ParsecVDD.cs
@@ -0,0 +1,310 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Win32;
+
+namespace ParsecVDisplay
+{
+ internal static class ParsecVDD
+ {
+ public const string DISPLAY_ID = "PSCCDD0";
+ public const string DISPLAY_NAME = "ParsecVDA";
+
+ public const string ADAPTER = "Parsec Virtual Display Adapter";
+ public const string ADAPTER_GUID = "{00b41627-04c4-429e-a26e-0265cf50c8fa}";
+
+ public const string HARDWARE_ID = @"Root\Parsec\VDA";
+ public const string CLASS_GUID = "{4d36e968-e325-11ce-bfc1-08002be10318}";
+
+ static IntPtr VddHandle;
+ static Task UpdateTask;
+ static CancellationTokenSource Cancellation;
+
+ // actually 16 devices could be created per adapter
+ // so just use a half to avoid plugging lag
+ public static int MAX_DISPLAYS => 8;
+
+ public static int DisplayCount { get; private set; } = 0;
+
+ public delegate void DisplayChangedCallback(List displays, bool noMonitors);
+ public static event DisplayChangedCallback DisplayChanged;
+
+ public static bool Init()
+ {
+ if (Device.OpenHandle(ADAPTER_GUID, out VddHandle))
+ {
+ Cancellation = new CancellationTokenSource();
+ UpdateTask = Task.Run(() => UpdateRoutine(Cancellation.Token), Cancellation.Token);
+
+ SystemEvents.DisplaySettingsChanged += DisplaySettingsChanged;
+ SystemEvents.SessionEnding += SessionEnding;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public static void Uninit()
+ {
+ //Config.DisplayCount = DisplayCount;
+ SystemEvents.DisplaySettingsChanged -= DisplaySettingsChanged;
+
+ Cancellation?.Cancel();
+ UpdateTask?.Wait();
+
+ Device.CloseHandle(VddHandle);
+ }
+
+ static async void UpdateRoutine(CancellationToken token)
+ {
+ // TODO: restore added displays
+
+ //int initialCount = Config.DisplayCount;
+ //if (initialCount > 0)
+ //{
+ // for (int i = 0; i < initialCount; i++)
+ // AddDisplay();
+ //}
+
+ var sw = Stopwatch.StartNew();
+
+ while (!token.IsCancellationRequested)
+ {
+ long start = sw.ElapsedMilliseconds;
+
+ Core.Update(VddHandle);
+
+ if (token.IsCancellationRequested)
+ break;
+
+ if ((sw.ElapsedMilliseconds - start) < 100)
+ {
+ await Task.Delay(80);
+ }
+ }
+ }
+
+ public static void Invalidate()
+ {
+ DisplaySettingsChanged(null, EventArgs.Empty);
+ }
+
+ static void DisplaySettingsChanged(object sender, EventArgs e)
+ {
+ var displays = Display.GetAllDisplays();
+ bool noMonitors = displays.Count == 0;
+
+ displays = displays.FindAll(d => d.DisplayName
+ .Equals(DISPLAY_ID, StringComparison.OrdinalIgnoreCase));
+
+ DisplayCount = displays.Count;
+ noMonitors = DisplayCount == 0 && noMonitors;
+
+ DisplayChanged?.Invoke(displays, noMonitors);
+ }
+
+ static void SessionEnding(object sender, SessionEndingEventArgs e)
+ {
+ Config.DisplayCount = DisplayCount;
+ }
+
+ public static Device.Status QueryStatus()
+ {
+ return Device.QueryStatus(CLASS_GUID, HARDWARE_ID);
+ }
+
+ public static string QueryVersion()
+ {
+ Core.Update(VddHandle);
+ Core.Version(VddHandle, out int minor);
+
+ return $"0.{minor}";
+ }
+
+ public static int AddDisplay()
+ {
+ Core.Add(VddHandle, out int index);
+ Core.Update(VddHandle);
+
+ return index;
+ }
+
+ public static void RemoveDisplay(int index)
+ {
+ Core.Remove(VddHandle, index);
+ Core.Update(VddHandle);
+ }
+
+ public static void RemoveLastDisplay()
+ {
+ if (DisplayCount > 0)
+ {
+ int index = DisplayCount - 1;
+ RemoveDisplay(index);
+ }
+ }
+
+ static unsafe class Core
+ {
+ public const uint IOCTL_ADD = 0x0022e004;
+ public const uint IOCTL_REMOVE = 0x0022a008;
+ public const uint IOCTL_UPDATE = 0x0022a00c;
+ public const uint IOCTL_VERSION = 0x0022e010;
+
+ static int IoControl(IntPtr handle, uint code, byte[] data)
+ {
+ var InBuffer = new byte[32];
+ var Overlapped = new Native.OVERLAPPED();
+ int OutBuffer = 0;
+
+ if (data != null)
+ Array.Copy(data, InBuffer, Math.Min(data.Length, InBuffer.Length));
+
+ fixed (byte* input = InBuffer)
+ {
+ Overlapped.hEvent = Native.CreateEventA(IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
+ Native.DeviceIoControl(handle, code, input, InBuffer.Length, &OutBuffer, sizeof(int), IntPtr.Zero, ref Overlapped);
+ Native.GetOverlappedResult(handle, ref Overlapped, out var NumberOfBytesTransferred, true);
+
+ if (Overlapped.hEvent != IntPtr.Zero)
+ Native.CloseHandle(Overlapped.hEvent);
+ }
+
+ return OutBuffer;
+ }
+
+ public static void Version(IntPtr handle, out int minor)
+ {
+ // Remove() takes only 2 bytes for index
+ // so this 4 bytes return could be a combination ((major << 16) | minor)
+ // it should be clear when Parsec VDD comes to v1.0
+ minor = IoControl(handle, IOCTL_VERSION, null);
+ }
+
+ public static void Update(IntPtr handle)
+ {
+ IoControl(handle, IOCTL_UPDATE, null);
+ }
+
+ public static void Add(IntPtr handle, out int index)
+ {
+ index = IoControl(handle, IOCTL_ADD, null);
+ }
+
+ public static void Remove(IntPtr handle, int index)
+ {
+ // 16-bit BE index
+ var indexData = BitConverter.GetBytes((ushort)unchecked(index & 0xFFFF));
+ Array.Reverse(indexData);
+
+ IoControl(handle, IOCTL_REMOVE, indexData);
+ }
+ }
+
+ public static IList GetCustomDisplayModes()
+ {
+ var list = new List();
+
+ using (var vdd = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Parsec\\vdd", RegistryKeyPermissionCheck.ReadSubTree))
+ {
+ if (vdd != null)
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ using (var index = vdd.OpenSubKey($"{i}", RegistryKeyPermissionCheck.ReadSubTree))
+ {
+ if (index != null)
+ {
+ var width = index.GetValue("width");
+ var height = index.GetValue("height");
+ var hz = index.GetValue("hz");
+
+ if (width != null && height != null && hz != null)
+ {
+ list.Add(new Display.Mode
+ {
+ Width = Convert.ToUInt16(width),
+ Height = Convert.ToUInt16(height),
+ Hz = Convert.ToUInt16(hz),
+ });
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return list;
+ }
+
+ // Requires admin perm
+ public static void SetCustomDisplayModes(List modes)
+ {
+ using (var vdd = Registry.LocalMachine.CreateSubKey("SOFTWARE\\Parsec\\vdd", RegistryKeyPermissionCheck.ReadWriteSubTree))
+ {
+ if (vdd != null)
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ using (var index = vdd.CreateSubKey($"{i}", RegistryKeyPermissionCheck.ReadWriteSubTree))
+ {
+ if (i >= modes.Count && index != null)
+ {
+ index.Dispose();
+ vdd.DeleteSubKey($"{i}");
+ }
+ else if (index != null)
+ {
+ index.SetValue("width", modes[i].Width, RegistryValueKind.DWord);
+ index.SetValue("height", modes[i].Height, RegistryValueKind.DWord);
+ index.SetValue("hz", modes[i].Hz, RegistryValueKind.DWord);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ static unsafe class Native
+ {
+ [DllImport("kernel32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool DeviceIoControl(
+ IntPtr device, uint code,
+ void* lpInBuffer, int nInBufferSize,
+ void* lpOutBuffer, int nOutBufferSize,
+ IntPtr lpBytesReturned,
+ ref OVERLAPPED lpOverlapped
+ );
+
+ [DllImport("kernel32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool GetOverlappedResult(
+ IntPtr handle,
+ ref OVERLAPPED lpOverlapped,
+ out uint lpNumberOfBytesTransferred,
+ [MarshalAs(UnmanagedType.Bool)] bool bWait
+ );
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct OVERLAPPED
+ {
+ public IntPtr Internal;
+ public IntPtr InternalHigh;
+ public IntPtr Pointer;
+ public IntPtr hEvent;
+ }
+
+ [DllImport("kernel32.dll")]
+ public static extern IntPtr CreateEventA(IntPtr a, IntPtr b, IntPtr c, IntPtr d);
+
+ [DllImport("kernel32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool CloseHandle(IntPtr handle);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/ParsecVDisplay.csproj b/app/ParsecVDisplay.csproj
new file mode 100644
index 0000000..93ac694
--- /dev/null
+++ b/app/ParsecVDisplay.csproj
@@ -0,0 +1,162 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {2D44934F-B4CF-4F2C-BD03-AE60B71AD045}
+ WinExe
+ ParsecVDisplay
+ ParsecVDisplay
+ v4.5
+ 512
+ {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ 4
+ true
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\
+ DEBUG;TRACE
+ prompt
+ 4
+ false
+ true
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\
+ TRACE
+ prompt
+ 4
+ false
+ true
+
+
+
+
+
+
+ Properties\app.manifest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 4.0
+
+
+
+
+
+
+
+ MSBuild:Compile
+ Designer
+
+
+ Button.xaml
+
+
+ CloseButton.xaml
+
+
+ CustomPage.xaml
+
+
+ DisplayItem.xaml
+
+
+
+
+
+
+
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+ Designer
+
+
+ App.xaml
+ Code
+
+
+ MainWindow.xaml
+ Code
+
+
+
+
+ Code
+
+
+ True
+ True
+ Resources.resx
+
+
+ True
+ Settings.settings
+ True
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+ SettingsSingleFileGenerator
+ Settings.Designer.cs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/Properties/App.manifest b/app/Properties/App.manifest
new file mode 100644
index 0000000..c522bb4
--- /dev/null
+++ b/app/Properties/App.manifest
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/Properties/AssemblyInfo.cs b/app/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..153efa5
--- /dev/null
+++ b/app/Properties/AssemblyInfo.cs
@@ -0,0 +1,55 @@
+using System.Reflection;
+using System.Resources;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("ParsecVDisplay")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("ParsecVDisplay")]
+[assembly: AssemblyCopyright("Copyright © 2024")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+//In order to begin building localizable applications, set
+//CultureYouAreCodingWith in your .csproj file
+//inside a . For example, if you are using US english
+//in your source files, set the to en-US. Then uncomment
+//the NeutralResourceLanguage attribute below. Update the "en-US" in
+//the line below to match the UICulture setting in the project file.
+
+//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
+
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
+
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion(ParsecVDisplay.App.VERSION)]
+[assembly: AssemblyFileVersion(ParsecVDisplay.App.VERSION)]
diff --git a/app/Properties/Resources.Designer.cs b/app/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..ab3fb7d
--- /dev/null
+++ b/app/Properties/Resources.Designer.cs
@@ -0,0 +1,71 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace ParsecVDisplay.Properties
+{
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources
+ {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources()
+ {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager
+ {
+ get
+ {
+ if ((resourceMan == null))
+ {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ParsecVDisplay.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture
+ {
+ get
+ {
+ return resourceCulture;
+ }
+ set
+ {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/app/Properties/Resources.resx b/app/Properties/Resources.resx
new file mode 100644
index 0000000..af7dbeb
--- /dev/null
+++ b/app/Properties/Resources.resx
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/app/Properties/Settings.Designer.cs b/app/Properties/Settings.Designer.cs
new file mode 100644
index 0000000..ac50bc4
--- /dev/null
+++ b/app/Properties/Settings.Designer.cs
@@ -0,0 +1,30 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace ParsecVDisplay.Properties
+{
+
+
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
+ internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
+ {
+
+ private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+ public static Settings Default
+ {
+ get
+ {
+ return defaultInstance;
+ }
+ }
+ }
+}
diff --git a/app/Properties/Settings.settings b/app/Properties/Settings.settings
new file mode 100644
index 0000000..033d7a5
--- /dev/null
+++ b/app/Properties/Settings.settings
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/Resources/admin.png b/app/Resources/admin.png
new file mode 100644
index 0000000..dcc99a0
Binary files /dev/null and b/app/Resources/admin.png differ
diff --git a/app/Resources/background.png b/app/Resources/background.png
new file mode 100644
index 0000000..99bab61
Binary files /dev/null and b/app/Resources/background.png differ
diff --git a/app/Resources/github.png b/app/Resources/github.png
new file mode 100644
index 0000000..e09d3fd
Binary files /dev/null and b/app/Resources/github.png differ
diff --git a/app/Resources/icon.ico b/app/Resources/icon.ico
new file mode 100644
index 0000000..910e92f
Binary files /dev/null and b/app/Resources/icon.ico differ
diff --git a/app/Resources/settings.png b/app/Resources/settings.png
new file mode 100644
index 0000000..2d5d286
Binary files /dev/null and b/app/Resources/settings.png differ
diff --git a/app/Resources/sync.png b/app/Resources/sync.png
new file mode 100644
index 0000000..f3aee41
Binary files /dev/null and b/app/Resources/sync.png differ
diff --git a/app/Shadow.cs b/app/Shadow.cs
new file mode 100644
index 0000000..dd81ec6
--- /dev/null
+++ b/app/Shadow.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace ParsecVDisplay
+{
+ internal static class Shadow
+ {
+ [DllImport("dwmapi.dll")]
+ static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
+
+ [StructLayout(LayoutKind.Sequential)]
+ struct MARGINS
+ {
+ public int leftWidth;
+ public int rightWidth;
+ public int topHeight;
+ public int bottomHeight;
+ }
+
+ [DllImport("dwmapi.dll")]
+ static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS pMarInset);
+
+ public static void ApplyShadow(IntPtr hwnd)
+ {
+ var v = 2;
+ DwmSetWindowAttribute(hwnd, 2, ref v, 4);
+
+ var margins = new MARGINS
+ {
+ bottomHeight = 0,
+ leftWidth = 0,
+ rightWidth = 0,
+ topHeight = 1
+ };
+ DwmExtendFrameIntoClientArea(hwnd, ref margins);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Tray.cs b/app/Tray.cs
new file mode 100644
index 0000000..fa3c25f
--- /dev/null
+++ b/app/Tray.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Windows;
+using System.Windows.Forms;
+using System.Windows.Interop;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+namespace ParsecVDisplay
+{
+ internal static class Tray
+ {
+ static NotifyIcon Icon;
+ static Window Window;
+ static System.Windows.Controls.ContextMenu Menu;
+
+ public static void Init(Window window, System.Windows.Controls.ContextMenu menu)
+ {
+ Window = window;
+ Menu = menu;
+
+ Icon = new NotifyIcon();
+
+ var uri = new Uri(window.Icon.ToString());
+ var streamInfo = System.Windows.Application.GetResourceStream(uri);
+ Icon.Icon = new System.Drawing.Icon(streamInfo.Stream);
+
+ Icon.MouseClick += Icon_MouseClick;
+ Icon.DoubleClick += Icon_DoubleClick;
+
+ Icon.Visible = true;
+ }
+
+ public static void Uninit()
+ {
+ if (Icon != null)
+ {
+ Icon.Visible = false;
+ Icon.Dispose();
+ }
+ }
+
+ private static void Icon_DoubleClick(object sender, EventArgs e)
+ {
+ ShowApp();
+ }
+
+ private static void Icon_MouseClick(object sender, MouseEventArgs e)
+ {
+ if (e.Button == MouseButtons.Right)
+ {
+ Menu.IsOpen = true;
+
+ if (PresentationSource.FromVisual(Menu) is HwndSource hwndSource)
+ {
+ SetForegroundWindow(hwndSource.Handle);
+ }
+ }
+ }
+
+ public static void ShowApp()
+ {
+ if (Window == null) return;
+
+ if (Window.Dispatcher.Thread.ManagedThreadId != Thread.CurrentThread.ManagedThreadId)
+ {
+ Window.Dispatcher.BeginInvoke(new Action(ShowApp));
+ return;
+ }
+
+ if (Window.Visibility == Visibility.Hidden)
+ {
+ Window.Show();
+ }
+
+ if (PresentationSource.FromVisual(Window) is HwndSource hwndSource)
+ {
+ SetForegroundWindow(hwndSource.Handle);
+ }
+ }
+
+ [DllImport("user32.dll")]
+ static extern IntPtr SetForegroundWindow(IntPtr hwnd);
+ }
+}
\ No newline at end of file
diff --git a/app/setup/setup.iss b/app/setup/setup.iss
new file mode 100644
index 0000000..e643a4e
--- /dev/null
+++ b/app/setup/setup.iss
@@ -0,0 +1,64 @@
+
+#define MyAppName "ParsecVDisplay"
+#define MyAppPublisher "Nguyen Duy"
+#define MyAppURL "https://github.com/nomi-san/parsec-vdd"
+#define MyAppExeName "ParsecVDisplay.exe"
+#define MyAppCopyright "© 2024 Nguyen Duy. All rights reserved."
+
+#define _Major
+#define _Minor
+#define _Rev
+#define _Build
+#define VddVersion GetVersionComponents(".\parsec-vdd-setup.exe", _Major, _Minor, _Rev, _Build), Str(_Major) + "." + Str(_Minor)
+
+[Setup]
+AppId={{D2005B5A-A8C4-4B77-807F-155132973D5D}
+AppName={#MyAppName}
+AppVersion={#VddVersion}
+AppVerName={#MyAppName} v{#VddVersion}
+AppPublisher={#MyAppPublisher}
+AppPublisherURL={#MyAppURL}
+AppSupportURL={#MyAppURL}
+AppUpdatesURL={#MyAppURL}
+VersionInfoCompany={#MyAppPublisher}
+VersionInfoCopyright={#MyAppCopyright}
+VersionInfoVersion={#VddVersion}
+DefaultDirName={commonpf64}\{#MyAppName}
+UsePreviousAppDir=yes
+DisableProgramGroupPage=yes
+LicenseFile=..\..\LICENSE
+PrivilegesRequired=admin
+OutputDir=.\
+OutputBaseFilename={#MyAppName}-v{#VddVersion}-setup
+SetupIconFile=..\Resources\icon.ico
+Compression=lzma
+SolidCompression=yes
+WizardStyle=classic
+UninstallDisplayName={#MyAppName}
+UninstallDisplayIcon={app}\{#MyAppExeName}
+
+[Languages]
+Name: "english"; MessagesFile: "compiler:Default.isl"
+
+[Dirs]
+Name: "{app}"; Permissions: everyone-full
+Name: "{app}\driver-setup"; Permissions: everyone-full
+
+[Files]
+Source: "..\bin\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
+Source: ".\parsec-vdd-setup.exe"; DestDir: "{app}\driver"; Flags: ignoreversion
+
+[Icons]
+Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
+Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
+
+[Tasks]
+Name: add_startup; Description: "Add program to startup"
+Name: install_vdd; Description: "Install Parsec VDD v{#VddVersion}"
+
+[Run]
+Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent runascurrentuser;
+Filename: "{app}\driver\parsec-vdd-setup.exe"; Parameters: "/S"; Flags: runascurrentuser; Tasks: install_vdd
+
+[Registry]
+Root: HKCU; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; ValueType: string; ValueName: "{#MyAppName}"; ValueData: """{app}\{#MyAppExeName}"""; Flags: uninsdeletevalue; Tasks: add_startup
\ No newline at end of file