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