vdi-cloud/tools/set-single-vdi-display.ps1

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
}