Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions .github/actions/set-display-resolution-vdd/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
name: Set up display resolution (via MttVDD virtual display driver)
description: >-
Install VirtualDrivers/Virtual-Display-Driver (IDD-based) silently via NefCon,
pre-write vdd_settings.xml so the driver spawns a single monitor at the
requested resolution, disable the Hyper-V Video adapter so MttVDD becomes the
primary display, and apply the requested per-monitor DPI scale via the shared
set-monitor-dpi sub-action. Use set-monitor-dpi directly later in the job to
switch DPI without re-installing VDD.

inputs:
width:
description: Display width in pixels
required: false
default: '3840'
height:
description: Display height in pixels
required: false
default: '2160'
target-dpi:
description: >-
Initial per-monitor DPI scale percentage (100, 125, 150, 175, 200, 225,
250, 300, …). Default 100. Reusable by calling ./.github/actions/set-monitor-dpi
to switch later in the same job.
required: false
default: '100'

runs:
using: composite
steps:
# Settings must exist before the driver loads so the first device init reads our config.
- name: Pre-write vdd_settings.xml
shell: pwsh
env:
VDD_WIDTH: ${{ inputs.width }}
VDD_HEIGHT: ${{ inputs.height }}
run: |
$vddDir = "C:\VirtualDisplayDriver"
New-Item -ItemType Directory -Path $vddDir -Force | Out-Null

$xml = @"
<?xml version='1.0' encoding='utf-8'?>
<vdd_settings>
<monitors><count>1</count></monitors>
<gpu><friendlyname>default</friendlyname></gpu>
<global>
<g_refresh_rate>60</g_refresh_rate>
</global>
<resolutions>
<resolution>
<width>$($env:VDD_WIDTH)</width>
<height>$($env:VDD_HEIGHT)</height>
<refresh_rate>60</refresh_rate>
</resolution>
</resolutions>
</vdd_settings>
"@
Set-Content -Path "$vddDir\vdd_settings.xml" -Value $xml -Encoding utf8
Get-Content "$vddDir\vdd_settings.xml"

# Adapted from VirtualDrivers/Virtual-Display-Driver Community Scripts/silent-install.ps1.
- name: Install Virtual Display Driver via NefCon
shell: pwsh
run: |
$tempDir = Join-Path $env:TEMP "VDDInstall"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null

# NefCon (driver install tool — pnputil alternative that handles INF + Root devices)
$NefConURL = "https://github.com/nefarius/nefcon/releases/download/v1.14.0/nefcon_v1.14.0.zip"
$NefConZip = Join-Path $tempDir "nefcon.zip"
Write-Host "Downloading NefCon..."
Invoke-WebRequest -Uri $NefConURL -OutFile $NefConZip -UseBasicParsing
Expand-Archive -Path $NefConZip -DestinationPath $tempDir -Force
$NefConExe = Join-Path $tempDir "x64\nefconw.exe"

# VDD driver (signed via SignPath)
$DriverURL = "https://github.com/VirtualDrivers/Virtual-Display-Driver/releases/download/25.7.23/VirtualDisplayDriver-x86.Driver.Only.zip"
$driverZip = Join-Path $tempDir "driver.zip"
Write-Host "Downloading VDD driver..."
Invoke-WebRequest -Uri $DriverURL -OutFile $driverZip -UseBasicParsing
Expand-Archive -Path $driverZip -DestinationPath $tempDir -Force

# Trust the driver's signing certificate so Windows accepts the install without prompting.
$catFile = Join-Path $tempDir "VirtualDisplayDriver\mttvdd.cat"
$catBytes = [System.IO.File]::ReadAllBytes($catFile)
$certificates = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
$certificates.Import($catBytes)
$certsFolder = Join-Path $tempDir "ExportedCerts"
New-Item -ItemType Directory -Path $certsFolder -Force | Out-Null
foreach ($cert in $certificates) {
$certPath = Join-Path $certsFolder "$($cert.Thumbprint).cer"
[System.IO.File]::WriteAllBytes($certPath, $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert))
Import-Certificate -FilePath $certPath -CertStoreLocation "Cert:\LocalMachine\TrustedPublisher" | Out-Null
}

Write-Host "Installing VDD driver..."
Push-Location $tempDir
& $NefConExe install ".\VirtualDisplayDriver\MttVDD.inf" "Root\MttVDD"
Pop-Location
Start-Sleep -Seconds 15
Write-Host "After install:"
Get-CimInstance Win32_VideoController | Format-Table Name, CurrentHorizontalResolution, CurrentVerticalResolution
Get-PnpDevice -Class Monitor -ErrorAction SilentlyContinue | Format-Table FriendlyName, Status

# DPI before Hyper-V disable so VDD inherits the scale and we skip a re-scale event.
# No PnP cycle: the upcoming Hyper-V disable is itself a display-config-change event
# which causes Windows to read PerMonitorSettings.
- name: Apply initial per-monitor DPI scale
uses: ./.github/actions/set-monitor-dpi
with:
target-dpi: ${{ inputs.target-dpi }}
monitor-height: ${{ inputs.height }}
pnp-cycle: 'false'

# MttVDD spawned a monitor at the requested resolution from vdd_settings.xml. Disable the
# Hyper-V Video adapter via PnP so MttVDD is the only display and inherits primary.
- name: Disable Hyper-V Video adapter (leave MttVDD as the only display)
shell: pwsh
run: |
Write-Host "Display adapters before disable:"
Get-PnpDevice -Class Display | Format-Table FriendlyName, Status, InstanceId
$hyperv = Get-PnpDevice -Class Display | Where-Object { $_.FriendlyName -like "*Hyper-V*" }
if ($hyperv) {
foreach ($dev in $hyperv) {
Write-Host "Disabling: $($dev.FriendlyName) ($($dev.InstanceId))"
Disable-PnpDevice -InstanceId $dev.InstanceId -Confirm:$false -ErrorAction Continue
}
Start-Sleep -Seconds 5
} else {
Write-Host "No Hyper-V Video adapter found — nothing to disable."
}
Write-Host "Display adapters after disable:"
Get-PnpDevice -Class Display | Format-Table FriendlyName, Status
Write-Host "Final display state:"
Get-CimInstance Win32_VideoController | Format-Table Name, CurrentHorizontalResolution, CurrentVerticalResolution
Get-PnpDevice -Class Monitor -ErrorAction SilentlyContinue | Format-Table FriendlyName, Status
27 changes: 27 additions & 0 deletions .github/actions/set-display-resolution/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Set up display resolution
description: >-
Resize the runner's primary display via the built-in Set-DisplayResolution
cmdlet. The Hyper-V Video adapter caps at 1920x1080; for higher resolutions
use set-display-resolution-vdd (IDD virtual display).

inputs:
width:
description: Display width in pixels (max 1920 on stock GH runners)
required: false
default: '1920'
height:
description: Display height in pixels (max 1080 on stock GH runners)
required: false
default: '1080'

runs:
using: composite
steps:
- name: Set display resolution to ${{ inputs.width }}x${{ inputs.height }}
shell: pwsh
run: |
Write-Host "Before:"
Get-CimInstance Win32_VideoController | Format-Table Name, CurrentHorizontalResolution, CurrentVerticalResolution
Set-DisplayResolution -Width ${{ inputs.width }} -Height ${{ inputs.height }} -Force
Write-Host "After:"
Get-CimInstance Win32_VideoController | Format-Table Name, CurrentHorizontalResolution, CurrentVerticalResolution
97 changes: 97 additions & 0 deletions .github/actions/set-monitor-dpi/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Set per-monitor DPI scale
description: >-
Override the per-monitor DPI scaling for every display Windows knows about. Re-runnable —
call as many times as needed within a job to switch DPI between test sessions.

inputs:
target-dpi:
description: >-
Target DPI scale percentage (100, 125, 150, 175, 200, 225, 250, 300, 350, 400, 450, 500).
required: true
monitor-height:
description: >-
Physical height of the active monitor in pixels. Used to derive the scale Windows
considers "recommended" (Windows Server 2025 picks `recommended_pct = height/720*100`
snapped to the standard table for GH-hosted runners regardless of EDID).
required: true
pnp-cycle:
description: >-
PnP-cycle active displays after writing the registry. Required for Windows to re-read
PerMonitorSettings mid-session (registry-only change is otherwise cached). Set to
'false' when this is the initial DPI setup right after a VDD install, since the
Hyper-V→VDD switch is already a display-config-change event that triggers the read.
required: false
default: 'true'

runs:
using: composite
steps:
# Writes HKCU\Control Panel\Desktop\PerMonitorSettings\<monitor-id>\DpiValue for each
# monitor enumerated under HKLM ScaleFactors. DpiValue is a signed-DWORD step offset from
# recommended; the standard scale table is { 100, 125, 150, 175, 200, 225, 250, 300, 350,
# 400, 450, 500 } percent. Applies live without session logoff via WM_SETTINGCHANGE.
- name: Apply per-monitor DPI scale (target ${{ inputs.target-dpi }}%)
shell: pwsh
env:
TARGET_DPI: ${{ inputs.target-dpi }}
MONITOR_HEIGHT: ${{ inputs.monitor-height }}
PNP_CYCLE: ${{ inputs.pnp-cycle }}
run: |
$scaleSteps = @(100, 125, 150, 175, 200, 225, 250, 300, 350, 400, 450, 500)
function ClosestStepIdx($pct) {
$best = 0; $bestDist = [int]::MaxValue
for ($i = 0; $i -lt $scaleSteps.Count; $i++) {
$d = [Math]::Abs($scaleSteps[$i] - $pct)
if ($d -lt $bestDist) { $bestDist = $d; $best = $i }
}
return $best
}

$target = [int]$env:TARGET_DPI
$height = [int]$env:MONITOR_HEIGHT
$recommendedPct = $height / 720.0 * 100
$recommendedIdx = ClosestStepIdx $recommendedPct
$targetIdx = ClosestStepIdx $target
$offset = $targetIdx - $recommendedIdx
$offsetDword = if ($offset -lt 0) { 0x100000000 + $offset } else { [uint32]$offset }
Write-Host "monitor ${height}p → recommended ~$([int]$recommendedPct)% (idx $recommendedIdx); target $target% (idx $targetIdx); offset=$offset (DWORD 0x$('{0:X8}' -f $offsetDword))"

$pms = "HKCU:\Control Panel\Desktop\PerMonitorSettings"
if (-not (Test-Path $pms)) { New-Item -Path $pms -Force | Out-Null }
$sf = "HKLM:\System\CurrentControlSet\Control\GraphicsDrivers\ScaleFactors"
if (-not (Test-Path $sf)) {
Write-Host "HKLM ScaleFactors path not present (no monitors registered) — DPI override skipped."
return
}
foreach ($m in Get-ChildItem -Path $sf) {
$sub = Join-Path $pms $m.PSChildName
if (-not (Test-Path $sub)) { New-Item -Path $sub -Force | Out-Null }
Set-ItemProperty -Path $sub -Name "DpiValue" -Value $offsetDword -Type DWord -Force
Write-Host " $($m.PSChildName) → DpiValue=$offset"
}

# PowerShell here-string `'@` at column 0 conflicts with YAML block-scalar indentation —
# build the C# source as a string array joined with newlines instead.
$cs = @(
'using System;',
'using System.Runtime.InteropServices;',
'public static class P {',
' [DllImport("user32.dll")]',
' public static extern int SendMessageTimeout(IntPtr h, uint m, IntPtr w, IntPtr l, uint f, uint t, out IntPtr r);',
'}'
) -join "`n"
Add-Type -TypeDefinition $cs
$r = [IntPtr]::Zero
[P]::SendMessageTimeout([IntPtr]0xFFFF, 0x001A, [IntPtr]::Zero, [IntPtr]::Zero, 2, 5000, [ref]$r) | Out-Null
Start-Sleep -Seconds 2

# PnP-cycle active displays so Windows re-reads PerMonitorSettings (registry-only
# change is otherwise cached after the first display config change of the session).
if ($env:PNP_CYCLE -eq 'true') {
$active = Get-PnpDevice -Class Display | Where-Object { $_.Status -eq 'OK' }
foreach ($d in $active) { Disable-PnpDevice -InstanceId $d.InstanceId -Confirm:$false -ErrorAction Continue }
Start-Sleep -Seconds 2
foreach ($d in $active) { Enable-PnpDevice -InstanceId $d.InstanceId -Confirm:$false -ErrorAction Continue }
Start-Sleep -Seconds 5
Write-Host "PnP-cycled $($active.Count) display device(s)"
}
123 changes: 123 additions & 0 deletions .github/workflows/test-windows-editor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: Test GameStudio (Editor Screenshots)

on:
workflow_dispatch:
inputs:
build-type:
description: Build
default: Debug
type: choice
options:
- Debug
- Release
schedule:
# Daily at 05:17 UTC — offset off top-of-hour to avoid GitHub Actions cron load spikes;
# gap from test-samples-screenshots (04:37) so they don't compete for runners.
- cron: '17 5 * * *'

concurrency:
group: test-windows-editor-${{ github.ref }}
cancel-in-progress: true

jobs:
Run:
name: Editor screenshots (${{ github.event.inputs.build-type || 'Debug' }})
runs-on: windows-2025-vs2026
env:
DOTNET_DbgEnableMiniDump: "1"
DOTNET_DbgMiniDumpType: "1"
DOTNET_DbgMiniDumpName: "${{ github.workspace }}\\crash-dumps\\dotnet_%p.dmp"
steps:
- uses: actions/checkout@v4
with:
lfs: true

- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Configure crash dumps
shell: pwsh
run: |
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting" /v DontShowUI /t REG_DWORD /d 1 /f
$dumpDir = "${{ github.workspace }}\crash-dumps"
New-Item -Path $dumpDir -ItemType Directory -Force | Out-Null
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps" /v DumpFolder /t REG_EXPAND_SZ /d $dumpDir /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps" /v DumpType /t REG_DWORD /d 1 /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps" /v DumpCount /t REG_DWORD /d 10 /f

# nuget.config's `stride-local` source points at bin/packages, which doesn't exist on a
# fresh checkout (auto-pack-deploy populates it on first build); pre-create it empty.
- name: Pre-create bin/packages so restore doesn't fail on missing local source
shell: pwsh
run: New-Item -ItemType Directory -Path bin/packages -Force | Out-Null

# PackageStore.LoadDefaultSettings(null) only reads the user + machine NuGet configs,
# not the workspace nuget.config — register bin/packages in the user config.
- name: Register bin/packages with user NuGet config
shell: pwsh
run: |
New-Item -Path "$env:APPDATA\NuGet" -ItemType Directory -Force | Out-Null
dotnet nuget add source "${{ github.workspace }}\bin\packages" --name stride-local --configfile "$env:APPDATA\NuGet\NuGet.Config"

# GH runners default to a 0x0 fallback display; WPF clamps the editor to that. The
# composite action installs an IDD virtual display so we get a real desktop surface.
- name: Set display resolution
uses: ./.github/actions/set-display-resolution-vdd
with:
width: '3840'
height: '2160'

- name: Build Stride.GameStudio.AutoTesting
run: |
dotnet build sources\editor\Stride.GameStudio.AutoTesting\Stride.GameStudio.AutoTesting.csproj `
-nr:false -v:m -p:WarningLevel=0 `
-p:Configuration=${{ github.event.inputs.build-type || 'Debug' }}

- name: Build Stride.Editor.Tests
run: |
dotnet build tests\editor\Stride.Editor.Tests.csproj `
-nr:false -v:m -p:WarningLevel=0 `
-p:Configuration=${{ github.event.inputs.build-type || 'Debug' }}

# EditorScreenshotTests.Capture is an xunit [Theory] over Fixtures(); each entry spawns
# the AutoTesting CLI as a subprocess (per-fixture WPF singleton-state isolation),
# snapshots the runner output into <worktree>/ui-test-out-dpi<N>/<fixture>/, then runs
# ScreenshotComparator.Compare against tests/editor/baselines/dpi<N>/. ANTHROPIC_API_KEY
# opts the comparator into Claude vision second-opinions on frames that exceed LPIPS —
# cost is bounded because Claude only fires on already-failing frames.
- name: Run editor screenshot tests
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
dotnet test tests\editor\Stride.Editor.Tests.csproj `
--no-build `
--filter "FullyQualifiedName~Stride.Editor.Tests.EditorScreenshotTests" `
--logger "trx;LogFileName=editor-screenshots.trx" `
--results-directory TestResults `
-p:Configuration=${{ github.event.inputs.build-type || 'Debug' }}

- name: Publish test report
if: always()
uses: phoenix-actions/test-reporting@v15
with:
name: 'Editor screenshot regression'
path: TestResults/*.trx
reporter: dotnet-trx
output-to: step-summary

- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v4
with:
name: editor-screenshots
path: ui-test-out-dpi*/
if-no-files-found: warn

- name: Upload crash dumps
if: always()
uses: actions/upload-artifact@v4
with:
name: editor-crash-dumps
path: crash-dumps/
if-no-files-found: ignore
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ screenshots
samplesGenerated
screenshot-regression-out
screenshot-out
ui-test-out-dpi*
# Auto-generated by samples/Directory.Build.targets when -p:StrideAutoTesting=true is passed
# (force-loads Stride.Games.AutoTesting at startup so its [ModuleInitializer] runs).
_AutoTestingBootstrap.g.cs
Expand Down
Loading
Loading