2026-11-12 18:03:04 +08:00
param (
[ ValidateSet ( 'Very Low' , 'Low' , 'Medium' , 'High' , 'Very High' , 'Ultra' )][ string ] $Quality = 'Medium' ,
[ int ] $SampleSeconds = 120 ,
[ int ] $Repetitions = 3 ,
[ int ] $WarmupSeconds = 60 ,
[ int ] $FrameCap = -1 ,
[ ValidateSet ( 0 , 1 )][ int ] $RouteBranch = 0 ,
[ string ] $OutputFolder ,
[ switch ] $VisualOnly
)
$ErrorActionPreference = 'Stop'
$projectRoot = [ IO.Path ]:: GetFullPath (( Join-Path $PSScriptRoot '..\..' ))
2026-11-12 18:49:29 +08:00
$exe = Join-Path $projectRoot 'Builds\Windows\x64\Integration\Ironspore.exe'
2026-11-12 18:03:04 +08:00
if ( -not ( Test-Path -LiteralPath $exe -PathType Leaf )) { throw 'Build the lighting player through Unity MCP first.' }
$otherGames = @ ( Get-CimInstance Win32_Process -Filter "name = 'Ironspore.exe'" )
if ( $otherGames . Count -gt 0 ) { throw 'Another Ironspore player is running. Finish it before starting an isolated validation run.' }
if ( -not $OutputFolder ) {
$stamp = [ DateTimeOffset ]:: UtcNow . ToOffset ([ TimeSpan ]:: FromHours ( 8 )). ToString ( 'yyyyMMdd-HHmmss' )
$OutputFolder = Join-Path $projectRoot ( 'Docs\QA\Lighting\TestResults\Run-' + $stamp )
}
$OutputFolder = [ IO.Path ]:: GetFullPath ( $OutputFolder )
if ( Test-Path -LiteralPath ( Join-Path $OutputFolder 'report.json' )) { throw 'The output already contains a report; choose a new folder.' }
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
$environment = [ ordered ] @ {
recordedUtc = [ DateTimeOffset ]:: UtcNow . ToString ( 'O' )
project = $projectRoot
cpu = @ ( Get-CimInstance Win32_Processor | Select-Object Name , NumberOfCores , NumberOfLogicalProcessors )
ramGiB = [ math ]:: Round (( Get-CimInstance Win32_ComputerSystem ). TotalPhysicalMemory / 1 GB, 2 )
gpu = @ ( Get-CimInstance Win32_VideoController | Select-Object Name , DriverVersion )
battery = @ ( Get-CimInstance Win32_Battery | Select-Object BatteryStatus , EstimatedChargeRemaining )
powerPlan = ( powercfg / getactivescheme | Out-String ). Trim ()
runtimeAssemblySha256 = ( Get-FileHash -LiteralPath ( Join-Path ( Split-Path -Parent $exe ) 'Ironspore_Data\Managed\Ironspore.Runtime.dll' ) -Algorithm SHA256 ). Hash
profileSha256 = ( Get-FileHash -LiteralPath ( Join-Path $projectRoot 'Assets\Game\Data\Lighting\Daylight.asset' ) -Algorithm SHA256 ). Hash
competingPlayers = @ ()
}
$gpuTool = Get-Command nvidia-smi -ErrorAction SilentlyContinue
if ( $gpuTool ) { $environment . nvidia = @ (& $gpuTool . Source - -query-gpu = name , driver_version , memory . total , power . draw , temperature . gpu , clocks . current . graphics - -format = csv , noheader ) }
[ IO.File ]:: WriteAllText (( Join-Path $OutputFolder 'environment.json' ), ( $environment | ConvertTo-Json -Depth 6 ), [ Text.UTF8Encoding ]:: new ( $false ))
$playerLog = Join-Path $OutputFolder 'Player.log'
2026-11-12 18:49:29 +08:00
$arguments = @ ( '-ironspore-data-path' ,( '"' + ( Join-Path $OutputFolder 'PlayerData' ) + '"' ), '-lighting-qa' , '-lighting-quality' ,( '"' + $Quality + '"' ), '-lighting-output' ,( '"' + $OutputFolder + '"' ), '-lighting-tag' ,( '"' + $Quality + '"' ), '-lighting-seconds' , $SampleSeconds , '-lighting-repeats' , $Repetitions , '-lighting-warmup' , $WarmupSeconds , '-lighting-cap' , $FrameCap , '-lighting-branch' , $RouteBranch , '-screen-width' , '1920' , '-screen-height' , '1080' , '-screen-fullscreen' , '1' , '-logFile' ,( '"' + $playerLog + '"' ))
2026-11-12 18:03:04 +08:00
if ( $VisualOnly ) { $arguments += '-lighting-visual-only' }
# This is the interactive game under visual verification, so expose its actual window.
$player = Start-Process -FilePath $exe -ArgumentList $arguments -WorkingDirectory ( Split-Path -Parent $exe ) -WindowStyle Normal -PassThru
Write-Output "Validating $Quality at 1920x1080; player PID $( $player . Id ) ; output $OutputFolder "
while ( -not $player . HasExited ) {
$competing = @ ( Get-CimInstance Win32_Process -Filter "name = 'Ironspore.exe'" | Where-Object { $_ . ProcessId -ne $player . Id })
if ( $competing . Count -gt 0 ) {
$environment . competingPlayers = @ ( $competing | Select-Object ProcessId , ExecutablePath )
[ IO.File ]:: WriteAllText (( Join-Path $OutputFolder 'environment.json' ), ( $environment | ConvertTo-Json -Depth 6 ), [ Text.UTF8Encoding ]:: new ( $false ))
$owned = Get-CimInstance Win32_Process -Filter ( "ProcessId = " + $player . Id )
if ( $owned -and $owned . ExecutablePath -eq $exe ) { Stop-Process -Id $player . Id }
throw 'A competing player started. This run is invalid; only this validation player was stopped.'
}
Start-Sleep -Seconds 2
$player . Refresh ()
}
$reportPath = Join-Path $OutputFolder 'report.json'
if ( -not ( Test-Path -LiteralPath $reportPath )) { throw 'The player exited without a report. Inspect Player.log.' }
$report = Get-Content -LiteralPath $reportPath -Raw | ConvertFrom-Json
if ( -not $report . completed -or -not $report . visualPassed ) { throw 'Visual verification did not pass; inspect report.json and screenshots.' }
if ( $report . dynamicResolution -or [ math ]:: Abs ( $report . renderWidthScale - 1 ) -gt .001 -or [ math ]:: Abs ( $report . renderHeightScale - 1 ) -gt .001 ) { throw 'The test did not render at native resolution.' }
if ( -not $VisualOnly ) {
if ( $report . segments . Count -ne 2 * $Repetitions ) { throw 'The expected camera/repetition samples are incomplete.' }
$report . segments | Select-Object name , meanFps , p95Ms , p99Ms , gpuP95Ms , waypointsVisited , passed | Format-Table
if ( @ ( $report . segments | Where-Object { -not $_ . passed }). Count -gt 0 ) { throw 'Performance or route coverage did not pass.' }
}
Write-Output "Completed validation: $reportPath "