param(
[Parameter(Mandatory = $true)][string]$ImagePath,
[Parameter(Mandatory = $true)][string]$VerifiedBootResult,
[Parameter(Mandatory = $true)][string]$VerifiedTcgBootResult,
[string]$OutputPath
)
$ErrorActionPreference = 'Stop'
$taskRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..'))
$launcher = Join-Path $taskRoot 'Tools/OpenCSharp.ps1'
if (!$OutputPath) { $OutputPath = Join-Path $taskRoot ('build/CSharpBoot/ViewportPlan-' + [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() + '.json') }
$checks = @()
foreach ($script in @($launcher, (Join-Path $taskRoot 'Tools/PackageCSharp.ps1'))) {
$bytes = [IO.File]::ReadAllBytes($script)
if ($bytes.Length -lt 3 -or $bytes[0] -ne 239 -or $bytes[1] -ne 187 -or $bytes[2] -ne 191) { throw ('UTF-8 BOM is required for PS5: ' + $script) }
$parseTokens = $null; $parseErrors = $null
[void][Management.Automation.Language.Parser]::ParseFile($script, [ref]$parseTokens, [ref]$parseErrors)
if ($parseErrors.Count -ne 0) { throw ($parseErrors | Out-String) }
}
$checks += 'PS5 parsing and UTF-8 BOM for launcher and packager'
$plans = @()
foreach ($profile in @('Default','FitToWindow','NativePixels','Fullscreen')) {
$switches = @{ PlanOnly = $true }
if ($profile -ne 'Default') { $switches[$profile] = $true }
$planPath = & $launcher -ImagePath $ImagePath -VerifiedBootResult $VerifiedBootResult -VerifiedTcgBootResult $VerifiedTcgBootResult @switches
$plan = Get-Content -Raw -Encoding UTF8 -LiteralPath $planPath | ConvertFrom-Json
$native = $profile -eq 'NativePixels'
if ($plan.ProcessStarted -ne $false -or $plan.AutomaticInput -ne $false -or $plan.PhysicalDiskAccess -ne $false) { throw 'Plan-only must not start QEMU or access physical media.' }
if ($plan.Display.ZoomToFit -ne (!$native) -or $plan.Display.HostViewportResampling -ne (!$native) -or $plan.Display.NativePixelSamplingRequested -ne $native) { throw ('Incorrect sampling metadata: ' + $profile) }
if ($plan.Display.GuestFramebufferResolutionUnchanged -ne $true) { throw 'Viewport must preserve guest framebuffer geometry.' }
$bootMemory = (Get-Content -Raw -Encoding UTF8 -LiteralPath $VerifiedBootResult | ConvertFrom-Json).MemoryMiB
$expectedMemory = if ($bootMemory) { [string]$bootMemory + 'M' } else { '512M' }
$memoryIndex = [Array]::IndexOf([object[]]$plan.Arguments, '-m')
if ($memoryIndex -lt 0 -or $plan.Arguments[$memoryIndex + 1] -ne $expectedMemory) { throw 'Launcher memory must match its accepted guest profile.' }
if ($plan.Display.Acceleration -ne 'tcg' -or $plan.Display.VerifiedTcgComparisonImageMatch -ne $true -or !$plan.Display.VerifiedTcgReceiptSha256) { throw 'Interactive auto must choose identical-image verified TCG.' }
$expectedZoom = if ($native) { 'gtk,zoom-to-fit=off' } else { 'gtk,zoom-to-fit=on' }
if ($expectedZoom -notin $plan.Arguments) { throw ('Wrong GTK viewport argument: ' + $profile) }
if ($plan.Display.DisplayBackend -ne 'gtk' -or $plan.Display.QemuDisplayArgument -ne $expectedZoom -or $plan.Display.HostGlEnabled -ne $false) { throw 'Default GTK metadata or disabled host GL policy changed.' }
if ($profile -eq 'Default' -and ($plan.Display.RequestedWindowStyle -ne 'Maximized' -or '-full-screen' -in $plan.Arguments)) { throw 'Default must fit an ordinary maximized window without preboot fullscreen.' }
if ($profile -eq 'Fullscreen' -and '-full-screen' -notin $plan.Arguments) { throw 'Explicit Fullscreen compatibility was lost.' }
$plans += [ordered]@{Profile=$profile;PlanPath=$planPath;Display=$plan.Display;Arguments=$plan.Arguments}
$checks += ($profile + ' viewport policy and no-process plan')
}
foreach ($plan in $plans) {
if ($plan.Display.NativeWidth -ne $plans[0].Display.NativeWidth -or $plan.Display.NativeHeight -ne $plans[0].Display.NativeHeight) { throw 'Host fit option changed guest native resolution.' }
}
$conflictRejected = $false
try { & $launcher -ImagePath $ImagePath -VerifiedBootResult $VerifiedBootResult -NativePixels -FitToWindow -PlanOnly | Out-Null }
catch { if ($_.Exception.Message -match 'mutually exclusive') { $conflictRejected = $true } else { throw } }
if (!$conflictRejected) { throw 'Conflicting viewport switches were accepted.' }
$checks += 'NativePixels and FitToWindow conflict rejected before starting a session'
$explicitPath = & $launcher -ImagePath $ImagePath -VerifiedBootResult $VerifiedBootResult -VerifiedTcgBootResult $VerifiedTcgBootResult -Acceleration whpx -PlanOnly
$explicit = Get-Content -Raw -Encoding UTF8 -LiteralPath $explicitPath | ConvertFrom-Json
if ($explicit.Display.Acceleration -ne 'whpx' -or $explicit.Display.AccelerationChoice -ne 'whpx' -or $explicit.ProcessStarted -ne $false) { throw 'Explicit WHPX plan support was lost.' }
$checks += 'explicit WHPX remains an opt-in plan without claiming GTK readiness'
$sdlPlans = @()
foreach ($accelerator in @('auto', 'whpx')) {
$sdlPath = & $launcher -ImagePath $ImagePath -VerifiedBootResult $VerifiedBootResult -VerifiedTcgBootResult $VerifiedTcgBootResult -Acceleration $accelerator -DisplayBackend sdl -PlanOnly
$sdl = Get-Content -Raw -Encoding UTF8 -LiteralPath $sdlPath | ConvertFrom-Json
$expectedAccelerator = if ($accelerator -eq 'auto') { 'tcg' } else { 'whpx' }
if ($sdl.Display.DisplayBackend -ne 'sdl' -or $sdl.Display.DisplayBackendChoice -ne 'sdl' -or $sdl.Display.QemuDisplayArgument -ne 'sdl,gl=off' -or 'sdl,gl=off' -notin $sdl.Arguments) { throw 'Explicit SDL backend did not select SDL with host OpenGL disabled.' }
if ($sdl.Display.HostGlEnabled -ne $false -or $sdl.Display.Acceleration -ne $expectedAccelerator -or $sdl.ProcessStarted -ne $false -or $sdl.AutomaticInput -ne $false -or $sdl.PhysicalDiskAccess -ne $false) { throw 'SDL plan changed acceleration, host GL, input or media policy.' }
if ($sdl.Display.VerifiedTcgComparisonImageMatch -ne $true -or !$sdl.Display.VerifiedTcgReceiptSha256) { throw 'SDL lost identical-image TCG comparison validation.' }
$sdlPlans += [ordered]@{AccelerationChoice=$accelerator;PlanPath=$sdlPath;Display=$sdl.Display;Arguments=$sdl.Arguments}
$checks += ('explicit SDL/' + $accelerator + ' preserves verified-image policy, exact display argument and no-process plan')
}
$sdlNativeRejected = $false
try { & $launcher -ImagePath $ImagePath -VerifiedBootResult $VerifiedBootResult -VerifiedTcgBootResult $VerifiedTcgBootResult -DisplayBackend sdl -NativePixels -PlanOnly | Out-Null }
catch { if ($_.Exception.Message -match 'NativePixels requires the GTK') { $sdlNativeRejected = $true } else { throw } }
if (!$sdlNativeRejected) { throw 'SDL claimed unsupported native-pixel sampling.' }
$checks += 'SDL rejects NativePixels rather than claiming unsupported sampling'
$wrongReceipt = Get-Content -Raw -Encoding UTF8 -LiteralPath $VerifiedTcgBootResult | ConvertFrom-Json
$wrongReceipt.ImageSha256 = '0' * 64
$wrongPath = Join-Path $taskRoot ('build/CSharpBoot/RejectedTcgComparison-' + [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() + '.json')
$wrongReceipt | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $wrongPath -Encoding UTF8
$wrongRejected = $false
try { & $launcher -ImagePath $ImagePath -VerifiedBootResult $VerifiedBootResult -VerifiedTcgBootResult $wrongPath -PlanOnly | Out-Null }
catch { if ($_.Exception.Message -match 'identical image') { $wrongRejected = $true } else { throw } }
if (!$wrongRejected) { throw 'Auto accepted another image TCG comparison.' }
$checks += 'auto rejects a TCG proof for a different image before starting a session'
$wrongSdlRejected = $false
try { & $launcher -ImagePath $ImagePath -VerifiedBootResult $VerifiedBootResult -VerifiedTcgBootResult $wrongPath -DisplayBackend sdl -Acceleration whpx -PlanOnly | Out-Null }
catch { if ($_.Exception.Message -match 'identical image') { $wrongSdlRejected = $true } else { throw } }
if (!$wrongSdlRejected) { throw 'Explicit SDL/WHPX accepted an invalid supplied TCG comparison.' }
$checks += 'explicit SDL/WHPX rejects a supplied TCG proof for a different image'
$imageHash = (Get-FileHash -LiteralPath $ImagePath -Algorithm SHA256).Hash.ToLowerInvariant()
$result = [ordered]@{Kind='LauncherViewportPlanRegression';Passed=$true;PowerShellVersion=$PSVersionTable.PSVersion.ToString();ImageSha256=$imageHash;VerifiedTcgBootResult=[IO.Path]::GetFullPath($VerifiedTcgBootResult);ExplicitWhpxPlan=$explicitPath;SdlPlans=$sdlPlans;Checks=$checks;Plans=$plans;ProcessStarted=$false;AutomaticInput=$false;PhysicalDiskAccess=$false;VisibleViewportMeasured=$false}
[void][IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($OutputPath)))
$result | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $OutputPath -Encoding UTF8
Write-Output ([IO.Path]::GetFullPath($OutputPath))
param(
[Parameter(Mandatory = $true)][string]$ImagePath,
[Parameter(Mandatory = $true)][string]$VerifiedBootResult,
[Parameter(Mandatory = $true)][string]$VerifiedTcgBootResult,
[string]$OutputPath
)
$ErrorActionPreference = 'Stop'
$taskRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..'))
$launcher = Join-Path $taskRoot 'Tools/OpenCSharp.ps1'
if (!$OutputPath) { $OutputPath = Join-Path $taskRoot ('build/CSharpBoot/ViewportPlan-' + [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() + '.json') }
$checks = @()
foreach ($script in @($launcher, (Join-Path $taskRoot 'Tools/PackageCSharp.ps1'))) {
$bytes = [IO.File]::ReadAllBytes($script)
if ($bytes.Length -lt 3 -or $bytes[0] -ne 239 -or $bytes[1] -ne 187 -or $bytes[2] -ne 191) { throw ('UTF-8 BOM is required for PS5: ' + $script) }
$parseTokens = $null; $parseErrors = $null
[void][Management.Automation.Language.Parser]::ParseFile($script, [ref]$parseTokens, [ref]$parseErrors)
if ($parseErrors.Count -ne 0) { throw ($parseErrors | Out-String) }
}
$checks += 'PS5 parsing and UTF-8 BOM for launcher and packager'
$plans = @()
foreach ($profile in @('Default','FitToWindow','NativePixels','Fullscreen')) {
$switches = @{ PlanOnly = $true }
if ($profile -ne 'Default') { $switches[$profile] = $true }
$planPath = & $launcher -ImagePath $ImagePath -VerifiedBootResult $VerifiedBootResult -VerifiedTcgBootResult $VerifiedTcgBootResult @switches
$plan = Get-Content -Raw -Encoding UTF8 -LiteralPath $planPath | ConvertFrom-Json
$native = $profile -eq 'NativePixels'
if ($plan.ProcessStarted -ne $false -or $plan.AutomaticInput -ne $false -or $plan.PhysicalDiskAccess -ne $false) { throw 'Plan-only must not start QEMU or access physical media.' }
if ($plan.Display.ZoomToFit -ne (!$native) -or $plan.Display.HostViewportResampling -ne (!$native) -or $plan.Display.NativePixelSamplingRequested -ne $native) { throw ('Incorrect sampling metadata: ' + $profile) }
if ($plan.Display.GuestFramebufferResolutionUnchanged -ne $true) { throw 'Viewport must preserve guest framebuffer geometry.' }
$bootMemory = (Get-Content -Raw -Encoding UTF8 -LiteralPath $VerifiedBootResult | ConvertFrom-Json).MemoryMiB
$expectedMemory = if ($bootMemory) { [string]$bootMemory + 'M' } else { '512M' }
$memoryIndex = [Array]::IndexOf([object[]]$plan.Arguments, '-m')
if ($memoryIndex -lt 0 -or $plan.Arguments[$memoryIndex + 1] -ne $expectedMemory) { throw 'Launcher memory must match its accepted guest profile.' }
if ($plan.Display.Acceleration -ne 'tcg' -or $plan.Display.VerifiedTcgComparisonImageMatch -ne $true -or !$plan.Display.VerifiedTcgReceiptSha256) { throw 'Interactive auto must choose identical-image verified TCG.' }
$expectedZoom = if ($native) { 'gtk,zoom-to-fit=off' } else { 'gtk,zoom-to-fit=on' }
if ($expectedZoom -notin $plan.Arguments) { throw ('Wrong GTK viewport argument: ' + $profile) }
if ($plan.Display.DisplayBackend -ne 'gtk' -or $plan.Display.QemuDisplayArgument -ne $expectedZoom -or $plan.Display.HostGlEnabled -ne $false) { throw 'Default GTK metadata or disabled host GL policy changed.' }
if ($profile -eq 'Default' -and ($plan.Display.RequestedWindowStyle -ne 'Maximized' -or '-full-screen' -in $plan.Arguments)) { throw 'Default must fit an ordinary maximized window without preboot fullscreen.' }
if ($profile -eq 'Fullscreen' -and '-full-screen' -notin $plan.Arguments) { throw 'Explicit Fullscreen compatibility was lost.' }
$plans += [ordered]@{Profile=$profile;PlanPath=$planPath;Display=$plan.Display;Arguments=$plan.Arguments}
$checks += ($profile + ' viewport policy and no-process plan')
}
foreach ($plan in $plans) {
if ($plan.Display.NativeWidth -ne $plans[0].Display.NativeWidth -or $plan.Display.NativeHeight -ne $plans[0].Display.NativeHeight) { throw 'Host fit option changed guest native resolution.' }
}
$conflictRejected = $false
try { & $launcher -ImagePath $ImagePath -VerifiedBootResult $VerifiedBootResult -NativePixels -FitToWindow -PlanOnly | Out-Null }
catch { if ($_.Exception.Message -match 'mutually exclusive') { $conflictRejected = $true } else { throw } }
if (!$conflictRejected) { throw 'Conflicting viewport switches were accepted.' }
$checks += 'NativePixels and FitToWindow conflict rejected before starting a session'
$explicitPath = & $launcher -ImagePath $ImagePath -VerifiedBootResult $VerifiedBootResult -VerifiedTcgBootResult $VerifiedTcgBootResult -Acceleration whpx -PlanOnly
$explicit = Get-Content -Raw -Encoding UTF8 -LiteralPath $explicitPath | ConvertFrom-Json
if ($explicit.Display.Acceleration -ne 'whpx' -or $explicit.Display.AccelerationChoice -ne 'whpx' -or $explicit.ProcessStarted -ne $false) { throw 'Explicit WHPX plan support was lost.' }
$checks += 'explicit WHPX remains an opt-in plan without claiming GTK readiness'
$sdlPlans = @()
foreach ($accelerator in @('auto', 'whpx')) {
$sdlPath = & $launcher -ImagePath $ImagePath -VerifiedBootResult $VerifiedBootResult -VerifiedTcgBootResult $VerifiedTcgBootResult -Acceleration $accelerator -DisplayBackend sdl -PlanOnly
$sdl = Get-Content -Raw -Encoding UTF8 -LiteralPath $sdlPath | ConvertFrom-Json
$expectedAccelerator = if ($accelerator -eq 'auto') { 'tcg' } else { 'whpx' }
if ($sdl.Display.DisplayBackend -ne 'sdl' -or $sdl.Display.DisplayBackendChoice -ne 'sdl' -or $sdl.Display.QemuDisplayArgument -ne 'sdl,gl=off' -or 'sdl,gl=off' -notin $sdl.Arguments) { throw 'Explicit SDL backend did not select SDL with host OpenGL disabled.' }
if ($sdl.Display.HostGlEnabled -ne $false -or $sdl.Display.Acceleration -ne $expectedAccelerator -or $sdl.ProcessStarted -ne $false -or $sdl.AutomaticInput -ne $false -or $sdl.PhysicalDiskAccess -ne $false) { throw 'SDL plan changed acceleration, host GL, input or media policy.' }
if ($sdl.Display.VerifiedTcgComparisonImageMatch -ne $true -or !$sdl.Display.VerifiedTcgReceiptSha256) { throw 'SDL lost identical-image TCG comparison validation.' }
$sdlPlans += [ordered]@{AccelerationChoice=$accelerator;PlanPath=$sdlPath;Display=$sdl.Display;Arguments=$sdl.Arguments}
$checks += ('explicit SDL/' + $accelerator + ' preserves verified-image policy, exact display argument and no-process plan')
}
$sdlNativeRejected = $false
try { & $launcher -ImagePath $ImagePath -VerifiedBootResult $VerifiedBootResult -VerifiedTcgBootResult $VerifiedTcgBootResult -DisplayBackend sdl -NativePixels -PlanOnly | Out-Null }
catch { if ($_.Exception.Message -match 'NativePixels requires the GTK') { $sdlNativeRejected = $true } else { throw } }
if (!$sdlNativeRejected) { throw 'SDL claimed unsupported native-pixel sampling.' }
$checks += 'SDL rejects NativePixels rather than claiming unsupported sampling'
$wrongReceipt = Get-Content -Raw -Encoding UTF8 -LiteralPath $VerifiedTcgBootResult | ConvertFrom-Json
$wrongReceipt.ImageSha256 = '0' * 64
$wrongPath = Join-Path $taskRoot ('build/CSharpBoot/RejectedTcgComparison-' + [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() + '.json')
$wrongReceipt | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $wrongPath -Encoding UTF8
$wrongRejected = $false
try { & $launcher -ImagePath $ImagePath -VerifiedBootResult $VerifiedBootResult -VerifiedTcgBootResult $wrongPath -PlanOnly | Out-Null }
catch { if ($_.Exception.Message -match 'identical image') { $wrongRejected = $true } else { throw } }
if (!$wrongRejected) { throw 'Auto accepted another image TCG comparison.' }
$checks += 'auto rejects a TCG proof for a different image before starting a session'
$wrongSdlRejected = $false
try { & $launcher -ImagePath $ImagePath -VerifiedBootResult $VerifiedBootResult -VerifiedTcgBootResult $wrongPath -DisplayBackend sdl -Acceleration whpx -PlanOnly | Out-Null }
catch { if ($_.Exception.Message -match 'identical image') { $wrongSdlRejected = $true } else { throw } }
if (!$wrongSdlRejected) { throw 'Explicit SDL/WHPX accepted an invalid supplied TCG comparison.' }
$checks += 'explicit SDL/WHPX rejects a supplied TCG proof for a different image'
$imageHash = (Get-FileHash -LiteralPath $ImagePath -Algorithm SHA256).Hash.ToLowerInvariant()
$result = [ordered]@{Kind='LauncherViewportPlanRegression';Passed=$true;PowerShellVersion=$PSVersionTable.PSVersion.ToString();ImageSha256=$imageHash;VerifiedTcgBootResult=[IO.Path]::GetFullPath($VerifiedTcgBootResult);ExplicitWhpxPlan=$explicitPath;SdlPlans=$sdlPlans;Checks=$checks;Plans=$plans;ProcessStarted=$false;AutomaticInput=$false;PhysicalDiskAccess=$false;VisibleViewportMeasured=$false}
[void][IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($OutputPath)))
$result | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $OutputPath -Encoding UTF8
Write-Output ([IO.Path]::GetFullPath($OutputPath))