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
{