mirror of
https://github.com/nomi-san/parsec-vdd.git
synced 2026-08-07 13:50:45 +00:00
feat: add display state management with serialization and mode change deferral, remove use of registry for fetching display paths
This commit is contained in:
@@ -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}"),
|
||||
|
||||
+209
-92
@@ -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}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializable snapshot of a display's mode + orientation. Used for
|
||||
/// restoring displays across suspend/resume and across app sessions.
|
||||
/// </summary>
|
||||
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<State> states)
|
||||
{
|
||||
return string.Join(",", states.ConvertAll(s => s.Pack()));
|
||||
}
|
||||
|
||||
public static List<State> UnpackStates(string packed)
|
||||
{
|
||||
var list = new List<State>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a mode change. When <paramref name="defer"/> is true the change is
|
||||
/// staged with CDS_NORESET — the caller must invoke <see cref="CommitChanges"/>
|
||||
/// to apply all staged changes atomically. Returns true on DISP_CHANGE_SUCCESSFUL.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply all pending CDS_NORESET changes atomically.
|
||||
/// Returns true if the commit was accepted (DISP_CHANGE_SUCCESSFUL).
|
||||
/// </summary>
|
||||
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<string, Display>(StringComparer.OrdinalIgnoreCase);
|
||||
var cloneGroups = new List<Tuple<Display, Display>>();
|
||||
|
||||
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<Display, Display>(prevActiveDisplay, display));
|
||||
}
|
||||
if (display.Active)
|
||||
{
|
||||
if (prevActiveDisplay == null)
|
||||
prevActiveDisplay = display;
|
||||
else
|
||||
cloneGroups.Add(new Tuple<Display, Display>(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<string> GetDisplayPaths()
|
||||
/// <summary>
|
||||
/// 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:
|
||||
/// <code>
|
||||
/// \\?\DISPLAY#PSCCDD0#5&abc&UID256#{e6f07b5f-...}
|
||||
/// → DISPLAY\PSCCDD0\5&abc&UID256
|
||||
/// </code>
|
||||
/// </summary>
|
||||
static bool TryParseInstanceId(string interfacePath, out string instanceId)
|
||||
{
|
||||
var paths = new List<string>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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}").
|
||||
/// </summary>
|
||||
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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user