Skip to content

Instantly share code, notes, and snippets.

@stek29
Last active August 20, 2026 15:40
Show Gist options
  • Select an option

  • Save stek29/a3710d8d8f20cb78c666e9239fa57e92 to your computer and use it in GitHub Desktop.

Select an option

Save stek29/a3710d8d8f20cb78c666e9239fa57e92 to your computer and use it in GitHub Desktop.
Sunshine prep-command hook for dynamic Virtual Display Driver modes

Sunshine VDD dynamic-mode hook

sunshine-vdd-mode.ps1 is a Windows prep-command hook for upstream Sunshine and VirtualDrivers/Virtual-Display-Driver.

It reads Sunshine's SUNSHINE_CLIENT_WIDTH, SUNSHINE_CLIENT_HEIGHT, and SUNSHINE_CLIENT_FPS environment variables, validates the requested mode, and checks C:\VirtualDisplayDriver\vdd_settings.xml.

  • Known modes continue without reloading VDD.
  • Missing resolutions or refresh rates are added atomically.
  • Fractional refresh rates are preserved to millihertz.
  • VDD is refreshed with the supported UTF-16LE SETDISPLAYCOUNT 1 named-pipe command.
  • Errors produce a nonzero exit code and a concise log at C:\VirtualDisplayDriver\sunshine-vdd-mode.log.

Sunshine prep command

powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\VirtualDisplayDriver\sunshine-vdd-mode.ps1"

Run the command without Sunshine's Elevated option so it executes in the active interactive user session.

This hook is intended to complement Sunshine's native display-device configuration:

dd_configuration_option = ensure_only_display
dd_resolution_option = auto
dd_refresh_rate_option = auto
dd_hdr_option = auto

Set output_name separately to the stable Sunshine device ID for your VDD monitor. Do not copy a host-specific device ID from another machine.

Finding output_name on Windows

  1. Make sure VDD is enabled and restart Sunshine.

  2. Open Sunshine's log. For a default installation it is usually:

    C:\Program Files\Sunshine\config\sunshine.log
    
  3. Find the startup block beginning with Currently available display devices:.

  4. Locate the entry whose friendly_name is VDD by MTT and copy its device_id value, including the braces.

  5. Put that value in Sunshine's configuration, for example:

    output_name = {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}

Use device_id, not display_name. A name such as \\.\DISPLAY13 can change whenever Windows re-enumerates displays.

The device_id is designed to be the persistent selector and has been observed to survive ordinary VDD SETDISPLAYCOUNT 1 reloads. It is not an eternal identifier: re-check it after reinstalling or upgrading VDD, changing its EDID/monitor identity or monitor count, removing/recreating the Windows display device, or when Sunshine reports that the configured device cannot be found. If it changes, replace output_name with the new device_id; the hook itself does not need modification.

Important: first use of a new mode

Sunshine currently applies its native dd_* mode before running prep commands. If the client asks for a mode VDD does not advertise yet, the first connection adds the mode and reloads VDD but cannot use it immediately. Disconnect and reconnect once. Later connections to that mode work normally.

The client must enable Optimize Game Settings/SOPS for Sunshine's auto resolution, refresh-rate, and HDR settings to follow the stream request.

Assumptions

  • VDD is installed and its controller pipe is available as \\.\pipe\MTTVirtualDisplayPipe.
  • VDD settings live at C:\VirtualDisplayDriver\vdd_settings.xml.
  • One VDD monitor is used for streaming.
  • A console user session is active. Windows RDP can change session and display topology; a separate Sunshine bootstrap app that excludes this global prep command can be useful for reaching/unlocking the console first.

Do not replace the supported SETDISPLAYCOUNT 1 operation with the unsafe RELOAD_DRIVER command.

$ErrorActionPreference = 'Stop'
$SettingsPath = 'C:\VirtualDisplayDriver\vdd_settings.xml'
$PipeName = 'MTTVirtualDisplayPipe'
$LogPath = 'C:\VirtualDisplayDriver\sunshine-vdd-mode.log'
$Invariant = [Globalization.CultureInfo]::InvariantCulture
function Write-ModeLog([string]$Message) {
$line = '{0:yyyy-MM-dd HH:mm:ss.fff} {1}' -f [DateTime]::Now, $Message
Add-Content -LiteralPath $LogPath -Value $line -Encoding UTF8
Write-Output $line
}
function Read-Integer([string]$Name, [string]$Value, [int]$Minimum, [int]$Maximum) {
$parsed = 0
if ([string]::IsNullOrWhiteSpace($Value) -or
-not [int]::TryParse($Value, [Globalization.NumberStyles]::Integer, $Invariant, [ref]$parsed) -or
$parsed -lt $Minimum -or $parsed -gt $Maximum) {
throw "$Name must be an integer from $Minimum through $Maximum; received '$Value'."
}
return $parsed
}
function Read-RefreshMilliHz([string]$Value) {
$parsed = [decimal]0
if ([string]::IsNullOrWhiteSpace($Value) -or
-not [decimal]::TryParse($Value, [Globalization.NumberStyles]::Float, $Invariant, [ref]$parsed) -or
$parsed -lt 1 -or $parsed -gt 1000) {
throw "SUNSHINE_CLIENT_FPS must be a number from 1 through 1000; received '$Value'."
}
return [int][decimal]::Round($parsed * 1000, 0, [MidpointRounding]::AwayFromZero)
}
function ConvertTo-MilliHz([string]$Value) {
$parsed = [decimal]0
if (-not [decimal]::TryParse($Value, [Globalization.NumberStyles]::Float, $Invariant, [ref]$parsed)) {
return -1
}
return [int][decimal]::Round($parsed * 1000, 0, [MidpointRounding]::AwayFromZero)
}
function Send-VddReload {
$pipe = New-Object System.IO.Pipes.NamedPipeClientStream(
'.', $PipeName, [System.IO.Pipes.PipeDirection]::InOut,
[System.IO.Pipes.PipeOptions]::Asynchronous
)
try {
$pipe.Connect(4000)
$command = [Text.Encoding]::Unicode.GetBytes('SETDISPLAYCOUNT 1')
$pipe.Write($command, 0, $command.Length)
$pipe.Flush()
}
finally {
$pipe.Dispose()
}
}
$mutex = New-Object Threading.Mutex($false, 'Global\SunshineVddModeConfig')
$locked = $false
try {
$locked = $mutex.WaitOne(15000)
if (-not $locked) { throw 'Timed out waiting for the VDD settings lock.' }
if (-not (Test-Path -LiteralPath $SettingsPath -PathType Leaf)) {
throw "VDD settings file not found at $SettingsPath."
}
$width = Read-Integer 'SUNSHINE_CLIENT_WIDTH' $env:SUNSHINE_CLIENT_WIDTH 320 16384
$height = Read-Integer 'SUNSHINE_CLIENT_HEIGHT' $env:SUNSHINE_CLIENT_HEIGHT 200 16384
$refreshMilliHz = Read-RefreshMilliHz $env:SUNSHINE_CLIENT_FPS
$refreshText = ([decimal]$refreshMilliHz / 1000).ToString('0.###', $Invariant)
$document = New-Object Xml.XmlDocument
$document.PreserveWhitespace = $true
$document.Load($SettingsPath)
$matchingResolutions = @($document.SelectNodes('/vdd_settings/resolutions/resolution') | Where-Object {
[int]$_.width -eq $width -and [int]$_.height -eq $height
})
$globalRates = @($document.SelectNodes('/vdd_settings/global/g_refresh_rate') | ForEach-Object {
ConvertTo-MilliHz $_.InnerText
})
$localRates = @($matchingResolutions | ForEach-Object {
$_.SelectNodes('refresh_rate') | ForEach-Object { ConvertTo-MilliHz $_.InnerText }
})
if ($matchingResolutions.Count -gt 0 -and $refreshMilliHz -in @($globalRates + $localRates)) {
Write-ModeLog "Known mode: ${width}x${height}@$refreshText Hz."
exit 0
}
if ($matchingResolutions.Count -gt 0) {
$resolution = $matchingResolutions[0]
$rateNode = $document.CreateElement('refresh_rate')
$rateNode.InnerText = $refreshText
$lastChild = $resolution.LastChild
if ($lastChild -and $lastChild.NodeType -eq [Xml.XmlNodeType]::Whitespace) {
$null = $resolution.InsertBefore($document.CreateWhitespace("`r`n "), $lastChild)
$null = $resolution.InsertBefore($rateNode, $lastChild)
}
else {
$null = $resolution.AppendChild($rateNode)
}
}
else {
$resolutions = $document.SelectSingleNode('/vdd_settings/resolutions')
if (-not $resolutions) { throw 'The VDD XML has no resolutions section.' }
$resolution = $document.CreateElement('resolution')
foreach ($item in @(@('width', $width), @('height', $height), @('refresh_rate', $refreshText))) {
$node = $document.CreateElement([string]$item[0])
$node.InnerText = [string]$item[1]
$null = $resolution.AppendChild($node)
}
$lastChild = $resolutions.LastChild
if ($lastChild -and $lastChild.NodeType -eq [Xml.XmlNodeType]::Whitespace) {
$null = $resolutions.InsertBefore($document.CreateWhitespace("`r`n "), $lastChild)
$null = $resolutions.InsertBefore($resolution, $lastChild)
}
else {
$null = $resolutions.AppendChild($resolution)
}
}
$tempPath = "$SettingsPath.sunshine.tmp"
$backupPath = "$SettingsPath.sunshine.bak"
$document.Save($tempPath)
[IO.File]::Replace($tempPath, $SettingsPath, $backupPath, $true)
Send-VddReload
Write-ModeLog "Added mode: ${width}x${height}@$refreshText Hz; sent SETDISPLAYCOUNT 1. Reconnect once for Sunshine dd_* to use it."
exit 0
}
catch {
try { Write-ModeLog "ERROR: $($_.Exception.Message)" } catch { Write-Error $_.Exception.Message }
exit 1
}
finally {
if ($locked) { $mutex.ReleaseMutex() }
$mutex.Dispose()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment