Skip to content

Commit 413ce3b

Browse files
Add Parsec VDD keepalive script and use it in CI
Replace the previous ParsecVDisplay portable download/start in the CI workflow with a new PowerShell script (scripts/parsec-vdd.ps1) that P/Invokes Win32 setupapi/kernel32 to add virtual displays and run a keepalive loop so the driver doesn't unplug them after ~1s. Update the workflow (.github/workflows/ci.yml) to launch the script with DisplayCount=2 and wait briefly for registration. Also tighten a unit test (tests/unit/windows/test_win_display_device_hdr.cpp) to ASSERT that setTopology succeeds instead of skipping when it fails.
1 parent e24994c commit 413ce3b

3 files changed

Lines changed: 180 additions & 18 deletions

File tree

.github/workflows/ci.yml

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -137,21 +137,17 @@ jobs:
137137
-OutFile "parsec-vdd-0.45.0.0.exe"
138138
Start-Process -FilePath ".\parsec-vdd-0.45.0.0.exe" -ArgumentList "/S" -Wait
139139
140-
# download the ParsecVDisplay app (portable) to control the driver
141-
DownloadFile `
142-
-Uri "https://github.com/nomi-san/parsec-vdd/releases/download/v0.45.1/ParsecVDisplay-v0.45-portable.zip" `
143-
-OutFile "ParsecVDisplay.zip"
144-
Expand-Archive -Path "ParsecVDisplay.zip" -DestinationPath "ParsecVDisplay"
145-
146-
# start the app in the background so it keeps virtual displays alive
147-
$appPath = (Resolve-Path "ParsecVDisplay\ParsecVDisplay.exe").Path
148-
Start-Process -FilePath $appPath
149-
Start-Sleep -Seconds 3
150-
151-
# add 2 virtual displays
152-
for ($i = 1; $i -le 2; $i++) {
153-
& $appPath add
154-
}
140+
# add virtual displays and keep them alive via a background process.
141+
# the script mirrors parsec-vdd.h via Win32 P/Invoke and runs a
142+
# keepalive loop, which is required to prevent the driver from
143+
# unplugging the displays after ~1 second.
144+
$scriptPath = Join-Path $env:GITHUB_WORKSPACE "scripts\parsec-vdd.ps1"
145+
$pwsh = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
146+
Start-Process -FilePath $pwsh `
147+
-ArgumentList "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", $scriptPath, "-DisplayCount", "2" `
148+
-WindowStyle Hidden
149+
# allow time for displays to be added and registered by Windows
150+
Start-Sleep -Seconds 5
155151
156152
- name: Setup python
157153
id: setup-python

scripts/parsec-vdd.ps1

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
param (
2+
[Parameter(Mandatory = $true)]
3+
[int]$DisplayCount
4+
)
5+
6+
Add-Type -TypeDefinition @"
7+
using System;
8+
using System.Runtime.InteropServices;
9+
using System.Threading;
10+
11+
public class ParsecVdd {
12+
const uint GENERIC_READ = 0x80000000;
13+
const uint GENERIC_WRITE = 0x40000000;
14+
const uint FILE_SHARE_READ = 0x00000001;
15+
const uint FILE_SHARE_WRITE = 0x00000002;
16+
const uint OPEN_EXISTING = 3;
17+
const uint FILE_FLAG_NO_BUFFERING = 0x20000000;
18+
const uint FILE_FLAG_OVERLAPPED = 0x40000000;
19+
const uint FILE_FLAG_WRITE_THROUGH = 0x80000000;
20+
const uint DIGCF_PRESENT = 0x00000002;
21+
const uint DIGCF_DEVICEINTERFACE = 0x00000010;
22+
const uint IOCTL_ADD = 0x0022e004;
23+
const uint IOCTL_UPDATE = 0x0022a00c;
24+
static readonly Guid VDD_ADAPTER_GUID = new Guid("00b41627-04c4-429e-a26e-0265cf50c8fa");
25+
26+
[StructLayout(LayoutKind.Sequential)]
27+
struct SP_DEVICE_INTERFACE_DATA {
28+
public int cbSize;
29+
public Guid interfaceClassGuid;
30+
public uint flags;
31+
public IntPtr reserved;
32+
}
33+
34+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
35+
struct SP_DEVICE_INTERFACE_DETAIL_DATA_A {
36+
public int cbSize;
37+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
38+
public string DevicePath;
39+
}
40+
41+
[StructLayout(LayoutKind.Sequential)]
42+
struct OVERLAPPED {
43+
public IntPtr Internal;
44+
public IntPtr InternalHigh;
45+
public IntPtr Pointer;
46+
public IntPtr hEvent;
47+
}
48+
49+
[DllImport("setupapi.dll", SetLastError = true)]
50+
static extern IntPtr SetupDiGetClassDevsA(
51+
ref Guid ClassGuid, IntPtr Enumerator, IntPtr hwndParent, uint Flags);
52+
53+
[DllImport("setupapi.dll", SetLastError = true)]
54+
static extern bool SetupDiEnumDeviceInterfaces(
55+
IntPtr DeviceInfoSet, IntPtr DeviceInfoData,
56+
ref Guid InterfaceClassGuid, uint MemberIndex,
57+
ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData);
58+
59+
[DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Ansi)]
60+
static extern bool SetupDiGetDeviceInterfaceDetailA(
61+
IntPtr DeviceInfoSet, ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData,
62+
ref SP_DEVICE_INTERFACE_DETAIL_DATA_A DeviceInterfaceDetailData,
63+
uint DeviceInterfaceDetailDataSize, out uint RequiredSize, IntPtr DeviceInfoData);
64+
65+
[DllImport("setupapi.dll")]
66+
static extern bool SetupDiDestroyDeviceInfoList(IntPtr DeviceInfoSet);
67+
68+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
69+
static extern IntPtr CreateFileA(
70+
string lpFileName, uint dwDesiredAccess, uint dwShareMode,
71+
IntPtr lpSecurityAttributes, uint dwCreationDisposition,
72+
uint dwFlagsAndAttributes, IntPtr hTemplateFile);
73+
74+
[DllImport("kernel32.dll", SetLastError = true)]
75+
static extern IntPtr CreateEvent(
76+
IntPtr lpEventAttributes, bool bManualReset,
77+
bool bInitialState, string lpName);
78+
79+
[DllImport("kernel32.dll", SetLastError = true)]
80+
static extern bool DeviceIoControl(
81+
IntPtr hDevice, uint dwIoControlCode,
82+
byte[] lpInBuffer, int nInBufferSize,
83+
out int lpOutBuffer, int nOutBufferSize,
84+
IntPtr lpBytesReturned, ref OVERLAPPED lpOverlapped);
85+
86+
[DllImport("kernel32.dll", SetLastError = true)]
87+
static extern bool GetOverlappedResultEx(
88+
IntPtr hFile, ref OVERLAPPED lpOverlapped,
89+
out uint lpNumberOfBytesTransferred,
90+
int dwMilliseconds, bool bAlertable);
91+
92+
[DllImport("kernel32.dll")]
93+
static extern bool CloseHandle(IntPtr hObject);
94+
95+
public static IntPtr OpenHandle() {
96+
var guid = VDD_ADAPTER_GUID;
97+
var devInfo = SetupDiGetClassDevsA(
98+
ref guid, IntPtr.Zero, IntPtr.Zero, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
99+
if (devInfo == new IntPtr(-1)) return new IntPtr(-1);
100+
101+
var ifaceData = new SP_DEVICE_INTERFACE_DATA();
102+
ifaceData.cbSize = Marshal.SizeOf(ifaceData);
103+
104+
for (uint i = 0; SetupDiEnumDeviceInterfaces(
105+
devInfo, IntPtr.Zero, ref guid, i, ref ifaceData); i++) {
106+
var detail = new SP_DEVICE_INTERFACE_DETAIL_DATA_A();
107+
detail.cbSize = IntPtr.Size == 8 ? 8 : 6;
108+
uint needed;
109+
SetupDiGetDeviceInterfaceDetailA(
110+
devInfo, ref ifaceData, ref detail,
111+
(uint)Marshal.SizeOf(detail), out needed, IntPtr.Zero);
112+
var handle = CreateFileA(
113+
detail.DevicePath,
114+
GENERIC_READ | GENERIC_WRITE,
115+
FILE_SHARE_READ | FILE_SHARE_WRITE,
116+
IntPtr.Zero, OPEN_EXISTING,
117+
FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED | FILE_FLAG_WRITE_THROUGH,
118+
IntPtr.Zero);
119+
if (handle != IntPtr.Zero && handle != new IntPtr(-1)) {
120+
SetupDiDestroyDeviceInfoList(devInfo);
121+
return handle;
122+
}
123+
}
124+
125+
SetupDiDestroyDeviceInfoList(devInfo);
126+
return new IntPtr(-1);
127+
}
128+
129+
static int IoControl(IntPtr vdd, uint code, byte[] input) {
130+
var inBuf = new byte[32];
131+
if (input != null) Array.Copy(input, inBuf, Math.Min(input.Length, inBuf.Length));
132+
var ov = new OVERLAPPED();
133+
ov.hEvent = CreateEvent(IntPtr.Zero, true, false, null);
134+
int outBuf = 0;
135+
DeviceIoControl(vdd, code, inBuf, inBuf.Length, out outBuf, 4, IntPtr.Zero, ref ov);
136+
uint transferred;
137+
GetOverlappedResultEx(vdd, ref ov, out transferred, 5000, false);
138+
if (ov.hEvent != IntPtr.Zero) CloseHandle(ov.hEvent);
139+
return outBuf;
140+
}
141+
142+
public static void Update(IntPtr vdd) { IoControl(vdd, IOCTL_UPDATE, null); }
143+
144+
public static int AddDisplay(IntPtr vdd) {
145+
int idx = IoControl(vdd, IOCTL_ADD, null);
146+
Update(vdd);
147+
return idx;
148+
}
149+
150+
public static void Keepalive(IntPtr vdd) {
151+
while (true) { Update(vdd); Thread.Sleep(100); }
152+
}
153+
}
154+
"@
155+
156+
$vdd = [ParsecVdd]::OpenHandle()
157+
if ($vdd -eq [IntPtr]::new(-1)) {
158+
Write-Error "Failed to open the Parsec VDD device handle."
159+
exit 1
160+
}
161+
162+
for ($i = 1; $i -le $DisplayCount; $i++) {
163+
$idx = [ParsecVdd]::AddDisplay($vdd)
164+
Write-Information "Added virtual display at index $idx"
165+
}
166+
167+
Write-Information "Keeping $DisplayCount virtual display(s) alive..."
168+
[ParsecVdd]::Keepalive($vdd)

tests/unit/windows/test_win_display_device_hdr.cpp

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,7 @@ TEST_F_S(GetSetHdrStates) {
7171
}
7272

7373
const auto topology_guard {makeTopologyGuard(m_win_dd)};
74-
if (!m_win_dd.setTopology(makeExtendedTopology(*available_devices))) {
75-
GTEST_SKIP_("Could not set extended topology (displays may not support it).");
76-
}
74+
ASSERT_TRUE(m_win_dd.setTopology(makeExtendedTopology(*available_devices)));
7775

7876
const auto hdr_states {m_win_dd.getCurrentHdrStates(display_device::win_utils::flattenTopology(m_win_dd.getCurrentTopology()))};
7977
if (!std::ranges::any_of(hdr_states, [](auto entry) -> bool {

0 commit comments

Comments
 (0)