135 lines
7.6 KiB
PowerShell
135 lines
7.6 KiB
PowerShell
param(
|
|
[ValidateSet('Query','Apply','Restore')][string]$Action='Query',
|
|
[string]$BackupPath='C:\ProgramData\VDI\single-display-20261103\before.xml'
|
|
)
|
|
$ErrorActionPreference='Stop'
|
|
# Run as the logged-in desktop user. Detach desktop outputs, never disable GPUs.
|
|
Add-Type -ReferencedAssemblies System.Xml,System.Core,System -TypeDefinition @'
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using System.Xml.Serialization;
|
|
public static class VdiSingleDisplay {
|
|
[StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)]
|
|
public struct Device {
|
|
public int cb;
|
|
[MarshalAs(UnmanagedType.ByValTStr,SizeConst=32)] public string Name;
|
|
[MarshalAs(UnmanagedType.ByValTStr,SizeConst=128)] public string Description;
|
|
public int Flags;
|
|
[MarshalAs(UnmanagedType.ByValTStr,SizeConst=128)] public string Id;
|
|
[MarshalAs(UnmanagedType.ByValTStr,SizeConst=128)] public string Key;
|
|
}
|
|
[StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)]
|
|
public struct Mode {
|
|
[MarshalAs(UnmanagedType.ByValTStr,SizeConst=32)] public string name;
|
|
public short spec,driver,size,extra;
|
|
public int fields,x,y,orientation,fixedOutput;
|
|
public short color,duplex,yres,tt,collate;
|
|
[MarshalAs(UnmanagedType.ByValTStr,SizeConst=32)] public string form;
|
|
public short logPixels;
|
|
public int bpp,width,height,flags,hz,icmMethod,icmIntent,media,dither,reserved1,reserved2,panWidth,panHeight;
|
|
}
|
|
public class Record { public Device device; public Mode mode; public bool hasMode; }
|
|
[DllImport("user32.dll",CharSet=CharSet.Unicode)] static extern bool EnumDisplayDevices(string name,uint index,ref Device device,uint flags);
|
|
[DllImport("user32.dll",CharSet=CharSet.Unicode)] static extern bool EnumDisplaySettings(string name,int index,ref Mode mode);
|
|
[DllImport("user32.dll",CharSet=CharSet.Unicode)] static extern int ChangeDisplaySettingsEx(string name,ref Mode mode,IntPtr hwnd,uint flags,IntPtr param);
|
|
[DllImport("user32.dll",CharSet=CharSet.Unicode,EntryPoint="ChangeDisplaySettingsExW")]
|
|
static extern int Commit(string name,IntPtr mode,IntPtr hwnd,uint flags,IntPtr param);
|
|
public static Record[] Query() {
|
|
var result=new List<Record>();
|
|
for(uint i=0;;i++) {
|
|
Device d=new Device(); d.cb=Marshal.SizeOf(typeof(Device));
|
|
if(!EnumDisplayDevices(null,i,ref d,0)) return result.ToArray();
|
|
if((d.Flags&8)!=0) continue;
|
|
Mode m=new Mode(); m.size=(short)Marshal.SizeOf(typeof(Mode));
|
|
bool ok=EnumDisplaySettings(d.Name,-1,ref m);
|
|
result.Add(new Record {device=d,mode=m,hasMode=ok});
|
|
}
|
|
}
|
|
static void Check(int code,string operation) {
|
|
if(code!=0) throw new InvalidOperationException(operation+" failed: "+code+" (no reboot will be requested)");
|
|
}
|
|
public static string Target(Record[] records) {
|
|
string found=null;
|
|
foreach(var r in records) if((r.device.Flags&1)!=0 && r.device.Description.IndexOf("Virtual Display Driver",StringComparison.OrdinalIgnoreCase)>=0) {
|
|
if(found!=null) throw new InvalidOperationException("Multiple active VDD outputs; refuse ambiguous selection");
|
|
if(!r.hasMode || r.mode.width<640 || r.mode.height<480) throw new InvalidOperationException("Invalid active VDD mode");
|
|
found=r.device.Name;
|
|
}
|
|
if(found==null) throw new InvalidOperationException("No active VDD; do not detach anything");
|
|
return found;
|
|
}
|
|
public static void Save(Record[] records,string path) {
|
|
using(var stream=new FileStream(path,FileMode.CreateNew,FileAccess.Write)) new XmlSerializer(typeof(Record[])).Serialize(stream,records);
|
|
}
|
|
public static Record[] Load(string path) {
|
|
using(var stream=File.OpenRead(path)) return (Record[])new XmlSerializer(typeof(Record[])).Deserialize(stream);
|
|
}
|
|
static void Stage(Record record,bool active,bool primary) {
|
|
Mode m=record.mode; m.size=(short)Marshal.SizeOf(typeof(Mode)); m.extra=0;
|
|
if(active) {
|
|
if(!record.hasMode) throw new InvalidOperationException("Missing original mode");
|
|
m.fields=0x20|0x40000|0x80000|0x100000|0x400000;
|
|
} else {
|
|
m.width=m.height=0; m.fields=0x20|0x80000|0x100000;
|
|
}
|
|
Check(ChangeDisplaySettingsEx(record.device.Name,ref m,IntPtr.Zero,0x10000001u|(primary?0x10u:0u),IntPtr.Zero),"Stage "+record.device.Name);
|
|
}
|
|
public static void Apply(Record[] before,string keep) {
|
|
foreach(var r0 in before) if(r0.device.Name==keep) {
|
|
Record r=new Record {device=r0.device,mode=r0.mode,hasMode=r0.hasMode};
|
|
Mode m=r.mode; m.x=m.y=0; r.mode=m; Stage(r,true,true);
|
|
}
|
|
foreach(var r in before) if(r.device.Name!=keep && (r.device.Flags&1)!=0) Stage(r,false,false);
|
|
Check(Commit(null,IntPtr.Zero,IntPtr.Zero,0,IntPtr.Zero),"Commit single display");
|
|
}
|
|
public static void Restore(Record[] before) {
|
|
Record[] now=Query();
|
|
foreach(var old in before) {
|
|
bool matched=false;
|
|
foreach(var r in now) if(r.device.Name==old.device.Name && r.device.Id==old.device.Id && r.device.Key==old.device.Key) matched=true;
|
|
if(!matched) throw new InvalidOperationException("Display identity changed; refuse stale restore: "+old.device.Name);
|
|
}
|
|
foreach(var r in before) if((r.device.Flags&4)!=0) Stage(r,true,true);
|
|
foreach(var r in before) if((r.device.Flags&4)==0) {
|
|
bool currentlyActive=false;
|
|
foreach(var current in now) if(current.device.Name==r.device.Name) currentlyActive=(current.device.Flags&1)!=0;
|
|
if((r.device.Flags&1)!=0 || currentlyActive) Stage(r,(r.device.Flags&1)!=0,false);
|
|
}
|
|
Check(Commit(null,IntPtr.Zero,IntPtr.Zero,0,IntPtr.Zero),"Restore topology");
|
|
}
|
|
}
|
|
'@
|
|
function Report {
|
|
@([VdiSingleDisplay]::Query() | ForEach-Object {
|
|
@{device=$_.device.Name; description=$_.device.Description; active=(($_.device.Flags-band 1)-ne 0);
|
|
primary=(($_.device.Flags-band 4)-ne 0); width=$_.mode.width; height=$_.mode.height; hz=$_.mode.hz; x=$_.mode.x; y=$_.mode.y}
|
|
}) | ConvertTo-Json -Depth 4 -Compress
|
|
}
|
|
if($Action -eq 'Query') { Report; exit 0 }
|
|
if($Action -eq 'Restore') { [VdiSingleDisplay]::Restore([VdiSingleDisplay]::Load($BackupPath)); Report; exit 0 }
|
|
$before=[VdiSingleDisplay]::Query()
|
|
$keep=[VdiSingleDisplay]::Target($before)
|
|
$target=@($before | Where-Object {$_.device.Name -eq $keep})[0]
|
|
if($target.mode.width -ne 2560 -or $target.mode.height -ne 1440 -or $target.mode.hz -ne 60 -or ($target.device.Flags-band 4)-eq 0) {
|
|
throw 'Required existing 2560x1440@60 VDD primary is absent; nothing changed'
|
|
}
|
|
$active=@($before | Where-Object {($_.device.Flags-band 1)-ne 0})
|
|
if($active.Count -eq 1 -and ($active[0].device.Flags-band 4)-ne 0) { Report; exit 0 }
|
|
New-Item -ItemType Directory -Force (Split-Path $BackupPath) | Out-Null
|
|
[VdiSingleDisplay]::Save($before,$BackupPath)
|
|
try {
|
|
[VdiSingleDisplay]::Apply($before,$keep)
|
|
Start-Sleep -Seconds 2
|
|
$actual=@([VdiSingleDisplay]::Query() | Where-Object {($_.device.Flags-band 1)-ne 0})
|
|
if($actual.Count -ne 1 -or $actual[0].device.Name -ne $keep -or ($actual[0].device.Flags-band 4)-eq 0) { throw 'Single VDD primary read-back failed' }
|
|
$old=@($before | Where-Object {$_.device.Name -eq $keep})[0]
|
|
if($actual[0].mode.width-ne $old.mode.width -or $actual[0].mode.height-ne $old.mode.height -or $actual[0].mode.hz-ne $old.mode.hz) { throw 'VDD mode changed unexpectedly' }
|
|
Report
|
|
} catch {
|
|
$failure=$_
|
|
[VdiSingleDisplay]::Restore($before)
|
|
throw $failure
|
|
}
|