diff --git a/app/Device.cs b/app/Device.cs index 495da54..a6e8c2b 100644 --- a/app/Device.cs +++ b/app/Device.cs @@ -156,18 +156,6 @@ namespace ParsecVDisplay return Status.UNKNOWN; } - 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; @@ -383,12 +371,6 @@ namespace ParsecVDisplay pid = 102, }; - public static DEVPROPKEY Device_DeviceDesc = new DEVPROPKEY - { - fmtid = Guid.Parse("{A45C254E-DF1C-4EFD-8020-67D146A850E0}"), - pid = 2 - }; - public static DEVPROPKEY Device_DriverVersion = new DEVPROPKEY { fmtid = Guid.Parse("{A8B865DD-2E3D-4094-AD97-E593A70C75D6}"), diff --git a/app/Display.cs b/app/Display.cs index 52db019..e0d3e6d 100644 --- a/app/Display.cs +++ b/app/Display.cs @@ -4,7 +4,6 @@ using System.Drawing; using System.Globalization; using System.Linq; using System.Runtime.InteropServices; -using Microsoft.Win32; namespace ParsecVDisplay { @@ -51,6 +50,71 @@ namespace ParsecVDisplay public override string ToString() => $"{Resolution} @ {RefreshRate}"; } + /// + /// Serializable snapshot of a display's mode + orientation. Used for + /// restoring displays across suspend/resume and across app sessions. + /// + public class State + { + public int Width; + public int Height; + public int Hz; + public Orientation Orientation; + + public string Pack() => $"{Width}x{Height}@{Hz}/{(int)Orientation}"; + + public static bool TryUnpack(string s, out State state) + { + state = null; + if (string.IsNullOrWhiteSpace(s)) + return false; + + var m = System.Text.RegularExpressions.Regex.Match(s.Trim(), + @"^(\d+)x(\d+)@(\d+)/([0-3])$"); + if (!m.Success) + return false; + + state = new State + { + Width = int.Parse(m.Groups[1].Value), + Height = int.Parse(m.Groups[2].Value), + Hz = int.Parse(m.Groups[3].Value), + Orientation = (Orientation)int.Parse(m.Groups[4].Value), + }; + return true; + } + } + + public State Snapshot() + { + return new State + { + Width = CurrentMode?.Width ?? 0, + Height = CurrentMode?.Height ?? 0, + Hz = CurrentMode?.Hz ?? 0, + Orientation = CurrentOrientation, + }; + } + + public static string PackStates(List states) + { + return string.Join(",", states.ConvertAll(s => s.Pack())); + } + + public static List UnpackStates(string packed) + { + var list = new List(); + if (string.IsNullOrWhiteSpace(packed)) + return list; + + foreach (var tok in packed.Split(',')) + { + if (State.TryUnpack(tok, out var s)) + list.Add(s); + } + return list; + } + public class ModeSet { public int Width; @@ -62,9 +126,7 @@ namespace ParsecVDisplay public int Identifier; public int CloneOf; public int Address; - public DateTime LastArrival; - public string Adapter; public string AdapterInstance; public DateTime AdapterArrival; @@ -148,48 +210,83 @@ namespace ParsecVDisplay } public bool ChangeMode(int? width, int? height, int? hz, Orientation? orientation) + { + return ChangeMode(width, height, hz, orientation, defer: false); + } + + /// + /// Apply a mode change. When is true the change is + /// staged with CDS_NORESET — the caller must invoke + /// to apply all staged changes atomically. Returns true on DISP_CHANGE_SUCCESSFUL. + /// + public bool ChangeMode(int? width, int? height, int? hz, Orientation? orientation, bool defer) { var mode = new Native.DEVMODE(); mode.dmSize = (short)Marshal.SizeOf(typeof(Native.DEVMODE)); - - if (Native.EnumDisplaySettings(DeviceName, -1, ref mode)) + + if (!Native.EnumDisplaySettings(DeviceName, -1, ref mode)) + return false; + + // dmFields must explicitly enumerate the fields we are changing + mode.dmFields = 0; + + if (width.HasValue) { - if (width.HasValue) - { - mode.dmPelsWidth = width.Value; - mode.dmFields |= /*DM_PELSWIDTH*/ 0x80000; - } - - if (height.HasValue) - { - mode.dmPelsHeight = height.Value; - mode.dmFields |= /*DM_PELSHEIGHT*/ 0x100000; - } - - if (hz.HasValue) - { - mode.dmDisplayFrequency = hz.Value; - mode.dmFields |= /*DM_DISPLAYFREQUENCY*/ 0x400000; - } - - 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, /*CDS_UPDATEREGISTRY*/ 0x1 | /*CDS_GLOBAL*/ /*0x8*/ 0, IntPtr.Zero) == 0; + mode.dmPelsWidth = width.Value; + mode.dmFields |= /*DM_PELSWIDTH*/ 0x80000; } - return false; + if (height.HasValue) + { + mode.dmPelsHeight = height.Value; + mode.dmFields |= /*DM_PELSHEIGHT*/ 0x100000; + } + + if (hz.HasValue) + { + mode.dmDisplayFrequency = hz.Value; + mode.dmFields |= /*DM_DISPLAYFREQUENCY*/ 0x400000; + } + + if (orientation.HasValue) + { + var newDO = orientation.Value; + mode.dmDisplayOrientation = newDO; + mode.dmFields |= /*DM_DISPLAYORIENTATION*/ 0x80; + + if (((int)newDO + (int)CurrentOrientation) % 2 != 0) + { + int t = mode.dmPelsWidth; + mode.dmPelsWidth = mode.dmPelsHeight; + mode.dmPelsHeight = t; + mode.dmFields |= 0x80000 | 0x100000; + } + } + + uint flags = /*CDS_UPDATEREGISTRY*/ 0x1; + if (defer) + flags |= /*CDS_NORESET*/ 0x10000000; + + int rc = Native.ChangeDisplaySettingsEx(DeviceName, ref mode, IntPtr.Zero, flags, IntPtr.Zero); + if (rc != 0 /* DISP_CHANGE_SUCCESSFUL */) + return false; + + // Refresh local cache so subsequent ChangeMode calls see the new state + if (width.HasValue) CurrentMode.Width = width.Value; + if (height.HasValue) CurrentMode.Height = height.Value; + if (hz.HasValue) CurrentMode.Hz = hz.Value; + if (orientation.HasValue) CurrentOrientation = orientation.Value; + + return true; + } + + /// + /// Apply all pending CDS_NORESET changes atomically. + /// Returns true if the commit was accepted (DISP_CHANGE_SUCCESSFUL). + /// + public static bool CommitChanges() + { + return Native.ChangeDisplaySettingsEx(null, IntPtr.Zero, IntPtr.Zero, 0, IntPtr.Zero) == 0; } public void TakeScreenshot(string saveFile) @@ -238,8 +335,6 @@ namespace ParsecVDisplay 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)); @@ -255,49 +350,48 @@ namespace ParsecVDisplay if ((dd2.StateFlags & Native.DISPLAY_DEVICE_ATTACHED) == 0) continue; - var pathIdx = paths.FindIndex(p => dd2.DeviceID.Contains(p.Replace('\\', '#'))); - if (pathIdx < 0) continue; + // Derive device instance ID directly from monitor.DeviceID + // (interface path) — avoids reading HKLM\...\monitor\Enum. + if (!TryParseInstanceId(dd2.DeviceID, out var instanceId)) + continue; - if (!displayMap.ContainsKey(paths[pathIdx])) + if (displayMap.ContainsKey(instanceId)) + continue; + + var display = new Display { - var display = new Display - { - Active = (dd2.StateFlags & Native.DISPLAY_DEVICE_ACTIVE) != 0, - Address = ParseDisplayAddress(paths[pathIdx]), - DeviceName = dd.DeviceName, - DisplayName = ParseDisplayCode(dd2.DeviceID), - }; + Active = (dd2.StateFlags & Native.DISPLAY_DEVICE_ACTIVE) != 0, + Address = ParseDisplayAddress(dd2.DeviceID), + DeviceName = dd.DeviceName, + DisplayName = ParseDisplayCode(dd2.DeviceID), + }; - if (display.Active) - { - if (prevActiveDisplay == null) - { - prevActiveDisplay = display; - } - else - { - cloneGroups.Add(new Tuple(prevActiveDisplay, display)); - } + 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); + display.FetchAllModes(); } + + if (Device.GetDeviceInstance(instanceId, out uint devInst)) + { + Device.GetParentDeviceInstance(devInst, out uint parentInst, out display.AdapterInstance); + display.AdapterArrival = Device.GetDeviceLastArrival(parentInst); + } + + displayMap.Add(instanceId, display); } } var displays = displayMap.Values.ToList(); - // Sort displays by adapter arrival time + // Sort displays by adapter arrival (older adapter → lower number), + // then by monitor Address (UID) within the same adapter — DeviceName + // would sort lexicographically (DISPLAY10 < DISPLAY2), which breaks + // numbering once the GDI ordinal crosses 9. displays.Sort((a, b) => { if (a.AdapterInstance == b.AdapterInstance) @@ -320,31 +414,50 @@ namespace ParsecVDisplay return displays; } - static List GetDisplayPaths() + /// + /// Convert a device interface path returned by EnumDisplayDevices with + /// EDD_GET_DEVICE_INTERFACE_NAME to a device instance ID accepted by + /// CM_Locate_DevNodeA. Example transform: + /// + /// \\?\DISPLAY#PSCCDD0#5&abc&UID256#{e6f07b5f-...} + /// → DISPLAY\PSCCDD0\5&abc&UID256 + /// + /// + static bool TryParseInstanceId(string interfacePath, out string instanceId) { - var paths = new List(); + instanceId = null; + if (string.IsNullOrEmpty(interfacePath)) + return false; - using (var key = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Services\monitor\Enum", false)) - { - if (key != null) - { - int count = Convert.ToInt32(key.GetValue("Count", 0)); + int start = interfacePath.StartsWith(@"\\?\") ? 4 : 0; + int end = interfacePath.IndexOf("#{", start, StringComparison.Ordinal); + if (end < 0) end = interfacePath.Length; + if (end <= start) return false; - for (int i = 0; i < count; ++i) - { - var path = key.GetValue($"{i}"); - paths.Add(Convert.ToString(path)); - } - } - } - - return paths; + instanceId = interfacePath.Substring(start, end - start).Replace('#', '\\'); + return true; } + /// + /// Parse the UID number out of a device interface path / device ID. + /// Walks contiguous digits after "UID" rather than relying on the + /// substring being numeric-to-end (handles trailing "#{guid}"). + /// static int ParseDisplayAddress(string path) { - var index = path.LastIndexOf("uid", StringComparison.OrdinalIgnoreCase); - int.TryParse(path.Substring(index + 3), out var address); + if (string.IsNullOrEmpty(path)) + return 0; + + int i = path.IndexOf("UID", StringComparison.OrdinalIgnoreCase); + if (i < 0) return 0; + i += 3; + + int end = i; + while (end < path.Length && path[end] >= '0' && path[end] <= '9') + end++; + + int address; + int.TryParse(path.Substring(i, end - i), out address); return address; } @@ -387,6 +500,10 @@ namespace ParsecVDisplay public static extern int ChangeDisplaySettingsEx(string lpszDeviceName, ref DEVMODE lpDevMode, IntPtr hwnd, uint dwflags, IntPtr lParam); + [DllImport("user32.dll", EntryPoint = "ChangeDisplaySettingsExA", CharSet = CharSet.Ansi)] + public static extern int ChangeDisplaySettingsEx(string lpszDeviceName, IntPtr lpDevMode, + IntPtr hwnd, uint dwflags, IntPtr lParam); + [StructLayout(LayoutKind.Sequential)] public struct DEVMODE {