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 '..\..')) $exe = Join-Path $projectRoot 'Builds\Windows\x64\Lighting\Ironspore.exe' 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 / 1GB, 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' $arguments = @('-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 + '"')) 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"