11 KiB
Parsec VDD — Reverse-Engineered Reference
Protocol-level documentation of the Parsec Virtual Display Driver as exercised by Parsec's host application. This focuses on what the kernel-mode driver expects from user-mode callers: device discovery, IOCTL formats, lifecycle, and timing. It's intended as a reference for re-implementations (CLI tools, managers, third-party hosts).
1. Architecture
Parsec VDD is an Indirect Display Driver (IDD) miniport that creates virtual monitors on Windows 10+. Each running instance is identified by:
- Hardware ID:
Root\Parsec\VDA - Setup class GUID:
{4d36e968-e325-11ce-bfc1-08002be10318}(Display) - Device interface GUID:
{00b41627-04c4-429e-a26e-0265cf50c8fa}
A host creates a software device with SwDeviceCreate, then communicates
through the device interface GUID via DeviceIoControl.
Key constraints
- Maximum 16 virtual monitors per adapter (driver-side tracking is a
DWORD[16], with-1marking empty slots). - Maximum resolution clamped to 8192 × 4320 by the host.
- Default refresh rate is 60 Hz when not specified.
- Custom preset resolutions live in the registry — up to 5 slots.
2. Device Lifecycle
MM_adapter_create (SwDeviceCreate)
↓
MM_open_device (SetupDiGetClassDevs + CreateFileW)
↓
MM_connect (opens handle, spawns keep-alive thread)
↓
MM_device_add (IOCTL 0x22E004) × N monitors
↓
MM_keepalive_thread (IOCTL 0x22A00C every ~200 ms — keeps displays alive)
↓
host_vdd_privacy_thread (optional — blanks physical displays)
↓ [on disconnect]
MM_disconnect (IOCTL 0x22A008 × 16, close handle)
The device handle is opened asynchronously (FILE_FLAG_OVERLAPPED); every
IOCTL is issued with an OVERLAPPED structure and awaited via
GetOverlappedResultEx.
3. IOCTL Reference
All four IOCTLs use a 32-byte (0x20) zero-initialized input buffer.
| IOCTL | Hex | Function | Direction | In | Out | Timeout | Purpose |
|---|---|---|---|---|---|---|---|
CTL_CODE(0x22, 0x802, 0, 0) |
0x22E004 |
MM_device_add |
In/Out | 32 B | 4 B | 5000 ms | Create a virtual monitor. Output is the assigned monitor index (0–15). |
CTL_CODE(0x22, 0x802, 2, 0) |
0x22A008 |
MM_disconnect |
In | 32 B | 0 | 1000 ms | Remove a single monitor. InBuffer[1..4] = monitor index. |
CTL_CODE(0x22, 0x803, 2, 0) |
0x22A00C |
MM_update_keepalive |
In | 32 B | 0 | 1000 ms | Watchdog ping. Must be sent every ~200 ms or the driver removes all monitors after ~1 s. |
CTL_CODE(0x22, 0x804, 0, 0) |
0x22E010 |
MM_query_adapter_status |
In/Out | 32 B | 4 B | 1000 ms | Query the adapter. Output is a 4-byte status word (typically the driver minor version). |
Buffer layout
struct VDD_IOCTL_Input {
uint8_t reserved; // [0]
uint32_t monitor_index; // [1..4] — for remove/create
uint8_t padding[27]; // [5..31]
};
IoControl pattern (important)
The driver can return FALSE from DeviceIoControl with a non-ERROR_IO_PENDING
status code while still queuing the operation. The robust pattern,
mirrored from the reference C library:
- Call
DeviceIoControl— ignore its synchronous return. - Call
GetOverlappedResultEx(handle, &ov, ..., timeout, FALSE). - If the wait fails (timeout / error), call
CancelIoExfollowed by a blockingGetOverlappedResult(..., bWait = TRUE)before returning.
Skipping step 3 lets the kernel write into the (now-defunct) stack frame
that held the OVERLAPPED / output buffer, which AVs the next caller on the
same thread.
4. Adapter Creation — MM_adapter_create
Registers a software device at runtime via SwDeviceCreate. Waits on a
creation-complete callback (signaled by WaitForSingleObject). Once
registered, the device interface is enumerated with SetupDiGetClassDevs and
opened via CreateFileW using FILE_FLAG_OVERLAPPED | FILE_FLAG_WRITE_THROUGH | FILE_FLAG_NO_BUFFERING.
5. Monitor Create — MM_device_add
// Find the first empty slot (value == -1) in the 16-entry array
for (i = 0; i < 16; i++) {
if (context->monitors[i] == -1) break;
}
// Send IOCTL 0x22E004
DeviceIoControl(hDevice, 0x22E004, in_buf, 0x20, &out_index, 4, ...);
// Wait via GetOverlappedResultEx (timeout 5000 ms)
// out_index must be < 16; stored in context->monitors[i]
The returned monitor index becomes the driver index for that virtual
display. The UID seen later in monitor device-instance paths is
0x100 + driver_index (used to map back to a specific virtual monitor when
walking EnumDisplayDevices results).
6. Keep-Alive Watchdog — MM_keepalive_thread
Spawned by MM_connect. Loops continuously sending IOCTL_UPDATE at
~200 ms cadence:
while (*alive_flag) {
Sleep(100); // 100 ms granularity
QueryPerformanceCounter(&now);
QueryPerformanceFrequency(&freq);
elapsed_ms = (now - last) / (freq / 1000.0);
if (elapsed_ms > 200.0) {
MM_update_keepalive(context); // IOCTL 0x22A00C
last = now;
}
}
Critical behavior. If pings stop, the driver removes all virtual monitors after roughly 1 second. This is its built-in watchdog for cleaning up after host processes that crashed without explicit disconnect. Any re-implementation must keep up the cadence (a dedicated thread or 100 ms timer is the standard pattern).
7. Display Enumeration — MM_enumerate_displays
Walks all displays using the Windows configuration-manager APIs:
EnumDisplayDevicesW(NULL, i, ...)— outer loop over adapters.EnumDisplayDevicesW(adapter, j, EDD_GET_DEVICE_INTERFACE_NAME)— inner loop over monitors on that adapter.CM_Get_Device_Interface_PropertyW→ device node.CM_Locate_DevNodeW+CM_Get_DevNode_PropertyW→ hardware IDs.
Each entry is 836 bytes (209 DWORDs):
+0x004 adapter device ID (128 wchars)
+0x108 display name (32 wchars)
+0x148 monitor device ID (128 wchars)
+0x248 interface path (128 wchars)
Flags at DWORD offsets 4/5/6/7:
[4] = active, [5] = primary, [6] = removable, [7] = is-parsec
The is-parsec flag is determined by matching the hardware ID against
Root\Parsec\VDA (with case-insensitive substring search).
Note for RDP / cloud-server hosts:
EnumDisplayDevicesWis bound to the calling thread's window station — it cannot see console-session displays from inside an RDP session. A supplementalSetupDienumeration of the MONITOR class (GUID{4d36e96e-e325-11ce-bfc1-08002be10318}) walks the kernel device tree directly and is session-independent.
8. Custom Resolutions
Up to 5 custom resolutions can be added at slot indices 0–4. The driver reads them at adapter init from:
HKLM\SOFTWARE\Parsec\vdd\<slot>
w (REG_DWORD) width
h (REG_DWORD) height
hz (REG_DWORD) refresh rate
Written via:
RegCreateKeyExW(hkey, "SOFTWARE\\Parsec\\vdd\\<slot>", ..., KEY_READ | KEY_WRITE, ...);
RegSetValueExW(hkey, "w", REG_DWORD, &width, 4);
RegSetValueExW(hkey, "h", REG_DWORD, &height, 4);
RegSetValueExW(hkey, "hz", REG_DWORD, &refrate, 4);
To raise the 5-slot limit you'd have to patch the driver DLL itself
(mm.dll) — 5 is fixed in the driver-side parsing.
9. Privacy Mode — host_vdd_privacy_thread
Optional dedicated thread polling every 30 ms while the host wants to hide physical displays from a connected client:
WTSQuerySessionInformationW— check active session state.EnumWindows— detect whether any windows are still on real displays.SetDisplayConfig(flags=0x44)—SDC_TOPOLOGY_INTERNAL: blank/disable real displays.SetDisplayConfig(flags=0x84)—SDC_TOPOLOGY_EXTEND: restore real displays on disconnect.MM_query_adapter_status— poll for VDD adapter health.
When combined with IDD_DISPLAY only mode, this gives Parsec's "Privacy
Mode" — physical screens go dark while a virtual display drives the remote
client. Note: this conflicts with standalone managers like ParsecDisplay;
running both will fight over the topology. The README documents the
workaround (clear HKLM\SYSTEM\CurrentControlSet\Control\GraphicsDrivers\Connectivity).
10. Device Status Codes — host_vdd_init
Checks the device at Root\Parsec\VDA up to 4 times during startup.
Maps CM_Get_DevNode_Status flags to a numeric status:
| Status | Meaning |
|---|---|
| 0 | UNKNOWN |
| 2 | OK |
| 3 | UNKNOWN PROBLEM |
| 4 | DISABLED |
| 5 | DRIVER ERROR |
| 6 | RESTART REQUIRED |
| 7 | DISABLED SERVICE |
| other | NOT INSTALLED |
These are surfaced to the user as informative dialogs in Parsec's host UI ("driver disabled — re-enable in Device Manager", "restart required after install", etc.).
11. Adapter Recovery — host_vdd_adapter_monitor
A separate monitoring thread that handles driver disable/re-enable cycles (e.g. the user updates the driver in Device Manager):
- Polls every 100 ms (idle) or 1000 ms (active).
- Re-attempts
MM_adapter_createup to 3 times when the device disappears. - Bails out after 5 faults within 5 seconds to avoid CPU spin on a permanently broken driver.
- Adapter is dynamically re-created when the driver returns to a healthy state.
This is what makes the Parsec host survive a driver upgrade without a
restart — and the same pattern is mirrored in ParsecDisplay's
Vdd.Controller.StatusThread.
12. Session Startup Ordering
The order in which a Parsec host initializes display-related machinery at session start (excluding VUSB / gamepad / microphone code paths, which live outside this driver):
- Custom resolutions —
vdd_register_custom_resolution× 3 (indices 2–4). - Virtual displays —
host_vdd_initopens the device, spawns keep-alive + privacy threads, adds N monitors. - Resolution clamping — capped at 8192 × 4320 (configurable via session params).
- Screen blanking —
PostMessageW(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, -1). - Host video thread — creates the capture queue + encoding thread.
Function map
Behavioral labels used throughout this document. Names are conventional — choose any that suit your re-implementation.
| Function | Purpose |
|---|---|
MM_adapter_create |
SwDeviceCreate registration of the IDD adapter. |
MM_open_device |
SetupDiGetClassDevs + CreateFileW to obtain the device handle. |
MM_connect |
High-level init: open device, spawn keep-alive thread. |
MM_disconnect |
Tear down: remove all monitors, close handle. |
MM_device_add |
IOCTL 0x22E004 — create a virtual monitor. |
MM_query_adapter_status |
IOCTL 0x22E010 — adapter health / version. |
MM_update_keepalive |
IOCTL 0x22A00C — watchdog ping. |
MM_keepalive_thread |
Loop sending keep-alive at ~200 ms cadence. |
MM_enumerate_displays |
Walk all displays via EnumDisplayDevices + CM APIs. |
vdd_register_custom_resolution |
Write a (w, h, hz) triple into the registry slot. |
host_vdd_init |
Driver-status check + open handle + add initial monitors. |
host_vdd_privacy_thread |
Optional: hide physical displays while a session is active. |
host_vdd_adapter_monitor |
Survive driver disable/re-enable cycles. |