853 lines
41 KiB
PowerShell
853 lines
41 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Capture the reproducible Atmospheric render-pack performance matrix.
|
|
|
|
.DESCRIPTION
|
|
Runs the existing isolated offline pixel gate for retail, Low, Medium,
|
|
High, and Auto at 1920x1080, 2560x1440, and 3840x2160. Every row receives the same
|
|
authored-time, camera, MSAA, and warmup pins. Capped and uncapped modes are
|
|
selected explicitly with -FramePacing.
|
|
|
|
The declared CPU and resident-memory ceilings are enforced for every
|
|
enhanced row. The declared GPU ceilings are specifically 1080p ceilings,
|
|
so 1440p and 4K GPU measurements are reported without comparing them to a
|
|
threshold that was never declared for those resolutions. The exact 1080p
|
|
Low/Medium/High CPU p50/p99, GPU p50/p99, and memory ceilings are immutable
|
|
in this script and are emitted in both summaries.
|
|
|
|
No credentials or live-session environment values are read or recorded.
|
|
This wrapper never compares against or writes the user's normal settings:
|
|
run-offline-pixel-gate.ps1 owns a separate isolated state directory for
|
|
every row.
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Out,
|
|
[ValidateSet('capped', 'uncapped', 'both')]
|
|
[string]$FramePacing = 'both',
|
|
[ValidateSet('retail', 'low', 'medium', 'high', 'auto')]
|
|
[string[]]$PresetSet = @('retail', 'low', 'medium', 'high', 'auto'),
|
|
[ValidateSet('1920x1080', '2560x1440', '3840x2160')]
|
|
[string[]]$ResolutionSet = @('1920x1080', '2560x1440', '3840x2160'),
|
|
[ValidateRange(45000, 600000)]
|
|
[int]$WarmupMs = 45000,
|
|
[int]$DayGroup = 0,
|
|
[ValidateRange(0.0, 1.0)]
|
|
[double]$WorldDayFraction = 0.5,
|
|
[double]$SkyPhaseSeconds = 0,
|
|
[ValidateRange(0.0, 5000.0)]
|
|
[double]$OrbitDistanceMeters = 0,
|
|
[Nullable[double]]$OrbitYawDegrees,
|
|
[ValidateRange(-89.0, 89.0)]
|
|
[Nullable[double]]$OrbitPitchDegrees,
|
|
[switch]$SkipBuild
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
$PSNativeCommandUseErrorActionPreference = $false
|
|
. (Join-Path $PSScriptRoot 'atmospheric-performance-matrix-common.ps1')
|
|
. (Join-Path $PSScriptRoot 'connected-render-pack-gate-common.ps1')
|
|
|
|
$repo = Split-Path -Parent $PSScriptRoot
|
|
$pixelGate = Join-Path $PSScriptRoot 'run-offline-pixel-gate.ps1'
|
|
$solution = Join-Path $repo 'AcDream.slnx'
|
|
$cli = Join-Path $repo 'src\AcDream.Cli\bin\Release\net10.0\AcDream.Cli.dll'
|
|
$outputRoot = [IO.Path]::GetFullPath($Out)
|
|
$jsonPath = Join-Path $outputRoot 'atmospheric-performance-matrix.json'
|
|
$markdownPath = Join-Path $outputRoot 'atmospheric-performance-matrix.md'
|
|
$presets = @($PresetSet | ForEach-Object { $_.ToLowerInvariant() } | Select-Object -Unique)
|
|
$screenshotLeaf = 'world-offline'
|
|
$resolutions = @($ResolutionSet | Select-Object -Unique)
|
|
$pacingModes = switch ($FramePacing) {
|
|
'capped' { @('capped') }
|
|
'uncapped' { @('uncapped') }
|
|
default { @('capped', 'uncapped') }
|
|
}
|
|
|
|
# These are the exact declarations in BuiltInAtmosphericRenderPack and the
|
|
# campaign plan's Performance budget table. Do not relax them in this tool.
|
|
$budgets = @{
|
|
low = [pscustomobject][ordered]@{
|
|
IncrementalCpuMillisecondsP50 = 0.15
|
|
IncrementalCpuMillisecondsP99 = 0.50
|
|
InclusiveGpuMillisecondsP50At1080p = 2.00
|
|
InclusiveGpuMillisecondsP99At1080p = 3.00
|
|
ResidentGpuBytes = 64L * 1024L * 1024L
|
|
}
|
|
medium = [pscustomobject][ordered]@{
|
|
IncrementalCpuMillisecondsP50 = 0.25
|
|
IncrementalCpuMillisecondsP99 = 0.75
|
|
InclusiveGpuMillisecondsP50At1080p = 3.25
|
|
InclusiveGpuMillisecondsP99At1080p = 4.50
|
|
ResidentGpuBytes = 128L * 1024L * 1024L
|
|
}
|
|
high = [pscustomobject][ordered]@{
|
|
IncrementalCpuMillisecondsP50 = 0.35
|
|
IncrementalCpuMillisecondsP99 = 1.00
|
|
InclusiveGpuMillisecondsP50At1080p = 4.50
|
|
InclusiveGpuMillisecondsP99At1080p = 6.00
|
|
ResidentGpuBytes = 256L * 1024L * 1024L
|
|
}
|
|
}
|
|
|
|
function Get-RequiredProperty {
|
|
param(
|
|
[Parameter(Mandatory = $true)][object]$Value,
|
|
[Parameter(Mandatory = $true)][string]$Name,
|
|
[Parameter(Mandatory = $true)][string]$Context
|
|
)
|
|
|
|
$property = $Value.PSObject.Properties[$Name]
|
|
if ($null -eq $property) {
|
|
throw "$Context is missing required property '$Name'."
|
|
}
|
|
return $property.Value
|
|
}
|
|
|
|
function Format-Invariant([double]$Value, [string]$Format = '0.###') {
|
|
return $Value.ToString($Format, [Globalization.CultureInfo]::InvariantCulture)
|
|
}
|
|
|
|
function Add-Failure(
|
|
[Collections.Generic.List[string]]$RowFailures,
|
|
[string]$Message)
|
|
{
|
|
$null = $RowFailures.Add($Message)
|
|
}
|
|
|
|
function Assert-MatrixContainedPath([string]$Root, [string]$Path) {
|
|
return Assert-ConnectedGateContainedPath $Root $Path
|
|
}
|
|
|
|
function Compare-MatrixFallbackFramebuffer {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Expected,
|
|
[Parameter(Mandatory = $true)][string]$Actual,
|
|
[Parameter(Mandatory = $true)][string]$ReportPath,
|
|
[Parameter(Mandatory = $true)][string]$MaskPath)
|
|
|
|
Add-Type -AssemblyName System.Drawing
|
|
$image = [System.Drawing.Bitmap]::FromFile($Actual)
|
|
try {
|
|
$mask = [System.Drawing.Bitmap]::new(
|
|
$image.Width,
|
|
$image.Height,
|
|
[System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
|
try {
|
|
$graphics = [System.Drawing.Graphics]::FromImage($mask)
|
|
try {
|
|
$graphics.Clear([System.Drawing.Color]::FromArgb(0, 0, 0, 0))
|
|
$opaque = [System.Drawing.SolidBrush]::new(
|
|
[System.Drawing.Color]::FromArgb(255, 255, 0, 255))
|
|
try {
|
|
$graphics.FillRectangle(
|
|
$opaque,
|
|
0,
|
|
0,
|
|
$image.Width,
|
|
[Math]::Min(280, $image.Height))
|
|
}
|
|
finally { $opaque.Dispose() }
|
|
}
|
|
finally { $graphics.Dispose() }
|
|
$mask.Save($MaskPath, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
}
|
|
finally { $mask.Dispose() }
|
|
}
|
|
finally { $image.Dispose() }
|
|
|
|
& dotnet $cli compare-screenshots `
|
|
$Expected $Actual $ReportPath 2 0.001 $MaskPath | Out-Null
|
|
$compareExitCode = $LASTEXITCODE
|
|
if (-not (Test-Path -LiteralPath $ReportPath -PathType Leaf)) {
|
|
return [pscustomobject][ordered]@{
|
|
Passed = $false
|
|
DifferentPixelFraction = $null
|
|
ExitCode = $compareExitCode
|
|
ReportPath = $ReportPath
|
|
Failure = 'fallback/default screenshot comparison produced no report'
|
|
}
|
|
}
|
|
$verdict = Get-Content -Raw -LiteralPath $ReportPath | ConvertFrom-Json
|
|
$passedProperty = $verdict.PSObject.Properties['passed']
|
|
if ($null -eq $passedProperty) { $passedProperty = $verdict.PSObject.Properties['Passed'] }
|
|
$fractionProperty = $verdict.PSObject.Properties['differentPixelFraction']
|
|
if ($null -eq $fractionProperty) {
|
|
$fractionProperty = $verdict.PSObject.Properties['DifferentPixelFraction']
|
|
}
|
|
$passed = $null -ne $passedProperty -and [bool]$passedProperty.Value
|
|
return [pscustomobject][ordered]@{
|
|
Passed = $passed
|
|
DifferentPixelFraction = if ($null -eq $fractionProperty) {
|
|
$null
|
|
} else { [double]$fractionProperty.Value }
|
|
ExitCode = $compareExitCode
|
|
ReportPath = $ReportPath
|
|
Failure = if ($passed) {
|
|
$null
|
|
} else { 'safe fallback framebuffer differs from its paired default beyond tolerance 2 / 0.001' }
|
|
}
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $pixelGate)) {
|
|
throw "Offline pixel gate not found at '$pixelGate'."
|
|
}
|
|
if (Test-Path -LiteralPath $outputRoot) {
|
|
throw "Output directory already exists: '$outputRoot'. Choose a new directory."
|
|
}
|
|
$outputParent = Split-Path -Parent $outputRoot
|
|
if (-not (Test-Path -LiteralPath $outputParent -PathType Container)) {
|
|
throw "Output parent directory does not exist: '$outputParent'."
|
|
}
|
|
Assert-ConnectedGateNoReparsePoint $outputParent
|
|
Assert-ConnectedGateSafeLeafName ([IO.Path]::GetFileName($outputRoot))
|
|
if (@(Get-Process -Name AcDream.App -ErrorAction SilentlyContinue).Count -gt 0) {
|
|
throw 'An AcDream.App client is already running; close it gracefully before the matrix.'
|
|
}
|
|
|
|
if (-not $SkipBuild) {
|
|
& dotnet build $solution -c Release --no-restore
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Release build failed with exit code $LASTEXITCODE."
|
|
}
|
|
}
|
|
|
|
$exe = Join-Path $repo 'src\AcDream.App\bin\Release\net10.0\AcDream.App.exe'
|
|
if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) {
|
|
throw "Client executable not found: '$exe'."
|
|
}
|
|
$binaryIdentity = Get-ConnectedGateBinaryIdentity `
|
|
-Repository $repo -Executable $exe -SkipBuild:$SkipBuild
|
|
$null = New-Item -ItemType Directory -Path $outputRoot
|
|
Assert-ConnectedGateNoReparsePoint $outputRoot
|
|
$sourceCommit = $binaryIdentity.SourceCommit
|
|
$sourceStatus = @($binaryIdentity.SourceTrackedStatus)
|
|
$powershellExecutable = (Get-Process -Id $PID).Path
|
|
$rows = [Collections.Generic.List[object]]::new()
|
|
$matrixFailures = [Collections.Generic.List[string]]::new()
|
|
$startedUtc = [DateTime]::UtcNow
|
|
$expectedCasterCount = $null
|
|
$expectedAdapterIdentity = $null
|
|
|
|
foreach ($pacing in $pacingModes) {
|
|
foreach ($resolution in $resolutions) {
|
|
$dimensions = $resolution.Split('x')
|
|
$expectedWidth = [int]$dimensions[0]
|
|
$expectedHeight = [int]$dimensions[1]
|
|
|
|
foreach ($preset in $presets) {
|
|
$rowId = "$pacing-$preset-$($resolution.Replace('x', 'x'))"
|
|
Assert-ConnectedGateSafeLeafName $rowId
|
|
$rowDirectory = Assert-MatrixContainedPath $outputRoot (Join-Path $outputRoot $rowId)
|
|
if (Test-Path -LiteralPath $rowDirectory) {
|
|
throw "Matrix row directory is not fresh: '$rowDirectory'."
|
|
}
|
|
$metadataPath = Assert-MatrixContainedPath $rowDirectory (
|
|
Join-Path $rowDirectory "screenshots\$screenshotLeaf.metadata.json")
|
|
$rowFailures = [Collections.Generic.List[string]]::new()
|
|
$captureExitCode = -1
|
|
$metadata = $null
|
|
$pack = $null
|
|
$performance = $null
|
|
$cpuSampleCount = $null
|
|
$absoluteReceiverCpuSampleCount = $null
|
|
$gpuSampleCount = $null
|
|
$cpuP50 = $null
|
|
$cpuP95 = $null
|
|
$cpuP99 = $null
|
|
$absoluteReceiverCpuP50 = $null
|
|
$absoluteReceiverCpuP95 = $null
|
|
$absoluteReceiverCpuP99 = $null
|
|
$gpuP50 = $null
|
|
$gpuP95 = $null
|
|
$gpuP99 = $null
|
|
$residentGpuBytes = $null
|
|
$transientGpuBytes = $null
|
|
$casterCount = $null
|
|
$cascadeCount = $null
|
|
$drawCalls = $null
|
|
$dispatchCalls = $null
|
|
$imageCount = $null
|
|
$bufferCount = $null
|
|
$effectiveQuality = $null
|
|
$activationState = $null
|
|
$failureReason = $null
|
|
$availability = if ($preset -eq 'retail') { 'Retail' } else { 'Unknown' }
|
|
$unavailableClassification = $null
|
|
$metadataSchemaVersion = $null
|
|
$packVersion = $null
|
|
$activationGeneration = $null
|
|
$topResidentGpuBytes = $null
|
|
$topTransientGpuBytes = $null
|
|
$classificationCalls = $null
|
|
$passIds = @()
|
|
$framebufferSha256 = $null
|
|
$processEvidence = $null
|
|
$adapterEvidence = $null
|
|
$budget = if ($preset -in @('retail', 'auto')) {
|
|
$null
|
|
} else { $budgets[$preset] }
|
|
$gpuBudgetApplies = $false
|
|
|
|
try {
|
|
$arguments = @(
|
|
'-NoProfile',
|
|
'-File', $pixelGate,
|
|
'-Out', $rowDirectory,
|
|
'-WarmupMs', "$WarmupMs",
|
|
'-DayGroup', "$DayGroup",
|
|
'-WorldDayFraction', (Format-Invariant $WorldDayFraction '0.################'),
|
|
'-SkyPhaseSeconds', (Format-Invariant $SkyPhaseSeconds '0.################'),
|
|
'-MsaaSamples', '0',
|
|
'-RenderPackPreset', $preset,
|
|
'-Resolution', $resolution,
|
|
'-OrbitDistanceMeters', (Format-Invariant $OrbitDistanceMeters '0.################'),
|
|
'-SkipBuild')
|
|
if ($preset -ne 'retail') {
|
|
$arguments += @(
|
|
'-RequiredRenderPackSamples', '2048',
|
|
'-RenderPackSampleTimeoutMs', '300000',
|
|
'-AllowSafeRenderPackFallback')
|
|
}
|
|
if ($null -ne $OrbitYawDegrees) {
|
|
$arguments += @(
|
|
'-OrbitYawDegrees',
|
|
(Format-Invariant ([double]$OrbitYawDegrees) '0.################'))
|
|
}
|
|
if ($null -ne $OrbitPitchDegrees) {
|
|
$arguments += @(
|
|
'-OrbitPitchDegrees',
|
|
(Format-Invariant ([double]$OrbitPitchDegrees) '0.################'))
|
|
}
|
|
if ($pacing -eq 'uncapped') {
|
|
$arguments += '-Uncapped'
|
|
}
|
|
|
|
& $powershellExecutable @arguments
|
|
$captureExitCode = $LASTEXITCODE
|
|
if ($captureExitCode -ne 0) {
|
|
Add-Failure $rowFailures (
|
|
"offline capture exited with code $captureExitCode")
|
|
}
|
|
if (-not (Test-Path -LiteralPath $metadataPath)) {
|
|
throw "screenshot metadata is missing at '$metadataPath'."
|
|
}
|
|
foreach ($capturedPath in @(
|
|
$rowDirectory,
|
|
(Join-Path $rowDirectory 'screenshots'),
|
|
(Join-Path $rowDirectory 'isolated-state'),
|
|
(Join-Path $rowDirectory 'isolated-state\cache'))) {
|
|
Assert-ConnectedGateNoReparsePoint $capturedPath
|
|
}
|
|
|
|
$metadata = Get-Content -Raw -LiteralPath $metadataPath | ConvertFrom-Json
|
|
$metadataSchemaVersion = [int](Get-RequiredProperty $metadata 'SchemaVersion' 'metadata')
|
|
$strictEvidence = Test-AtmosphericPerformanceMetadataEvidence `
|
|
-MetadataPath $metadataPath -Preset $preset `
|
|
-ExpectedWidth $expectedWidth -ExpectedHeight $expectedHeight `
|
|
-AllowSafeFallback:($preset -ne 'retail')
|
|
foreach ($strictFailure in @($strictEvidence.Failures)) {
|
|
Add-Failure $rowFailures $strictFailure
|
|
}
|
|
$availability = [string]$strictEvidence.Outcome
|
|
$unavailableClassification = $strictEvidence.UnavailableClassification
|
|
$classificationCalls = $strictEvidence.CpuClassificationCalls
|
|
$passIds = @($strictEvidence.PassIds)
|
|
$actualWidth = [int](Get-RequiredProperty $metadata 'Width' 'metadata')
|
|
$actualHeight = [int](Get-RequiredProperty $metadata 'Height' 'metadata')
|
|
if ($actualWidth -ne $expectedWidth -or $actualHeight -ne $expectedHeight) {
|
|
Add-Failure $rowFailures (
|
|
"capture extent was ${actualWidth}x${actualHeight}, expected $resolution")
|
|
}
|
|
|
|
$pack = Get-RequiredProperty $metadata 'RenderPack' 'metadata'
|
|
$activationState = [int](Get-RequiredProperty $pack 'State' 'RenderPack')
|
|
$actualPackId = [string](Get-RequiredProperty $pack 'PackId' 'RenderPack')
|
|
$actualPresetId = [string](Get-RequiredProperty $pack 'PresetId' 'RenderPack')
|
|
$packVersion = [string](Get-RequiredProperty $pack 'PackVersion' 'RenderPack')
|
|
$activationGeneration = [int](Get-RequiredProperty $pack 'ActivationGeneration' 'RenderPack')
|
|
$topResidentGpuBytes = [long](Get-RequiredProperty $pack 'RetainedGpuBytes' 'RenderPack')
|
|
$topTransientGpuBytes = [long](Get-RequiredProperty $pack 'TransientGpuBytes' 'RenderPack')
|
|
$effectiveQuality = [string](
|
|
Get-RequiredProperty $pack 'EffectiveQuality' 'RenderPack')
|
|
if ($availability -eq 'Active' -and $preset -eq 'auto') {
|
|
$budget = $budgets[$effectiveQuality]
|
|
if ($null -eq $budget) {
|
|
Add-Failure $rowFailures (
|
|
"Automatic resolved unknown effective quality '$effectiveQuality'")
|
|
}
|
|
}
|
|
$gpuBudgetApplies = $availability -eq 'Active' -and
|
|
$resolution -eq '1920x1080'
|
|
$failureReason = Get-RequiredProperty $pack 'FailureReason' 'RenderPack'
|
|
$expectedPackId = if ($preset -eq 'retail' -or $availability -eq 'Unavailable') {
|
|
'retail'
|
|
}
|
|
else {
|
|
'acdream.atmospheric'
|
|
}
|
|
$expectedPresetId = if ($preset -eq 'retail' -or $availability -eq 'Unavailable') {
|
|
'off'
|
|
} else { $preset }
|
|
$expectedState = if ($preset -eq 'retail') {
|
|
0
|
|
} elseif ($availability -eq 'Unavailable') {
|
|
3
|
|
} else { 2 }
|
|
if ($actualPackId -cne $expectedPackId) {
|
|
Add-Failure $rowFailures (
|
|
"pack was '$actualPackId', expected '$expectedPackId'")
|
|
}
|
|
if ($actualPresetId -cne $expectedPresetId) {
|
|
Add-Failure $rowFailures (
|
|
"preset was '$actualPresetId', expected '$expectedPresetId'")
|
|
}
|
|
if ($activationState -ne $expectedState) {
|
|
Add-Failure $rowFailures (
|
|
"activation state was $activationState, expected $expectedState")
|
|
}
|
|
if ($availability -ne 'Unavailable' -and
|
|
-not [string]::IsNullOrWhiteSpace([string]$failureReason)) {
|
|
Add-Failure $rowFailures "render-pack failure: $failureReason"
|
|
}
|
|
|
|
$casterCount = [int](Get-RequiredProperty $pack 'ShadowCasterCount' 'RenderPack')
|
|
$cascadeCount = [int](Get-RequiredProperty $pack 'CascadeDrawCount' 'RenderPack')
|
|
$drawCalls = [int](Get-RequiredProperty $pack 'DrawCalls' 'RenderPack')
|
|
$dispatchCalls = [int](Get-RequiredProperty $pack 'DispatchCalls' 'RenderPack')
|
|
$imageCount = [int](Get-RequiredProperty $pack 'ImageCount' 'RenderPack')
|
|
$bufferCount = [int](Get-RequiredProperty $pack 'BufferCount' 'RenderPack')
|
|
$performance = Get-RequiredProperty $pack 'Performance' 'RenderPack'
|
|
|
|
# Consume only the post-audit metric names. The old absolute
|
|
# CpuMilliseconds*/GpuMilliseconds* fields must never be used
|
|
# to decide an incremental budget row.
|
|
$cpuSampleCount = [int](Get-RequiredProperty `
|
|
$performance 'CpuSampleCount' 'RenderPack.Performance')
|
|
$absoluteReceiverCpuSampleCount = [int](Get-RequiredProperty `
|
|
$performance 'AbsoluteReceiverCpuSampleCount' 'RenderPack.Performance')
|
|
$gpuSampleCount = [int](Get-RequiredProperty `
|
|
$performance 'GpuSampleCount' 'RenderPack.Performance')
|
|
$cpuP50 = [double](Get-RequiredProperty `
|
|
$performance 'IncrementalCpuMillisecondsP50' 'RenderPack.Performance')
|
|
$cpuP95 = [double](Get-RequiredProperty `
|
|
$performance 'IncrementalCpuMillisecondsP95' 'RenderPack.Performance')
|
|
$cpuP99 = [double](Get-RequiredProperty `
|
|
$performance 'IncrementalCpuMillisecondsP99' 'RenderPack.Performance')
|
|
$absoluteReceiverCpuP50 = [double](Get-RequiredProperty `
|
|
$performance 'AbsoluteReceiverCpuMillisecondsP50' 'RenderPack.Performance')
|
|
$absoluteReceiverCpuP95 = [double](Get-RequiredProperty `
|
|
$performance 'AbsoluteReceiverCpuMillisecondsP95' 'RenderPack.Performance')
|
|
$absoluteReceiverCpuP99 = [double](Get-RequiredProperty `
|
|
$performance 'AbsoluteReceiverCpuMillisecondsP99' 'RenderPack.Performance')
|
|
$gpuP50 = [double](Get-RequiredProperty `
|
|
$performance 'InclusiveGpuMillisecondsP50' 'RenderPack.Performance')
|
|
$gpuP95 = [double](Get-RequiredProperty `
|
|
$performance 'InclusiveGpuMillisecondsP95' 'RenderPack.Performance')
|
|
$gpuP99 = [double](Get-RequiredProperty `
|
|
$performance 'InclusiveGpuMillisecondsP99' 'RenderPack.Performance')
|
|
$residentGpuBytes = [long](Get-RequiredProperty `
|
|
$performance 'ResidentGpuBytes' 'RenderPack.Performance')
|
|
$transientGpuBytes = [long](Get-RequiredProperty `
|
|
$performance 'TransientGpuBytes' 'RenderPack.Performance')
|
|
|
|
if ($availability -eq 'Active') {
|
|
if ($null -eq $expectedCasterCount) { $expectedCasterCount = $casterCount }
|
|
elseif ($casterCount -ne $expectedCasterCount) {
|
|
Add-Failure $rowFailures (
|
|
"shadow caster membership $casterCount differs from matrix oracle $expectedCasterCount")
|
|
}
|
|
}
|
|
|
|
$pngPath = Assert-MatrixContainedPath $rowDirectory (
|
|
Join-Path $rowDirectory "screenshots\$screenshotLeaf.png")
|
|
$framebufferSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $pngPath).Hash.ToLowerInvariant()
|
|
$processPath = Assert-MatrixContainedPath $rowDirectory (
|
|
Join-Path $rowDirectory 'capture-process.json')
|
|
$processEvidence = Get-Content -Raw -LiteralPath $processPath | ConvertFrom-Json
|
|
if ([int]$processEvidence.SchemaVersion -ne 1 -or
|
|
[long]$processEvidence.WorkingSetBytes -le 0 -or
|
|
[long]$processEvidence.PrivateMemoryBytes -le 0) {
|
|
Add-Failure $rowFailures 'process-memory evidence is missing or invalid'
|
|
}
|
|
$capabilityPath = Assert-MatrixContainedPath $rowDirectory (
|
|
Join-Path $rowDirectory 'isolated-state\cache\diagnostics\graphical-capabilities-vulkan.json')
|
|
$capability = Get-Content -Raw -LiteralPath $capabilityPath | ConvertFrom-Json
|
|
$adapterEvidence = [pscustomobject][ordered]@{
|
|
DeviceName = [string]$capability.DeviceName
|
|
DriverInfo = [string]$capability.DriverInfo
|
|
DeviceType = [string]$capability.DeviceType
|
|
SelectedDeviceIndex = [int]$capability.SelectedDeviceIndex
|
|
DeviceApiVersion = [string]$capability.DeviceApiVersion
|
|
}
|
|
$adapterIdentity = "$($adapterEvidence.DeviceName)|$($adapterEvidence.DriverInfo)|$($adapterEvidence.SelectedDeviceIndex)"
|
|
if ([string]::IsNullOrWhiteSpace($adapterEvidence.DeviceName)) {
|
|
Add-Failure $rowFailures 'Vulkan adapter identity is missing'
|
|
}
|
|
elseif ($null -eq $expectedAdapterIdentity) { $expectedAdapterIdentity = $adapterIdentity }
|
|
elseif ($adapterIdentity -cne $expectedAdapterIdentity) {
|
|
Add-Failure $rowFailures 'Vulkan adapter identity changed between matrix rows'
|
|
}
|
|
|
|
if ($preset -eq 'retail') {
|
|
if ($imageCount -ne 0 -or $bufferCount -ne 0 -or
|
|
$drawCalls -ne 0 -or $dispatchCalls -ne 0 -or
|
|
$casterCount -ne 0 -or $cascadeCount -ne 0 -or
|
|
$residentGpuBytes -ne 0 -or $transientGpuBytes -ne 0) {
|
|
Add-Failure $rowFailures (
|
|
'retail row activated pack resources, submissions, casters, or memory')
|
|
}
|
|
}
|
|
elseif ($availability -eq 'Unavailable') {
|
|
# The strict metadata oracle already proved exact retail
|
|
# identity, zero samples/work/resources, and an allow-listed
|
|
# resource/capability reason. No enhanced budget applies to
|
|
# a preset which never became active.
|
|
}
|
|
else {
|
|
if ($cpuSampleCount -le 0 -or $gpuSampleCount -le 0) {
|
|
Add-Failure $rowFailures (
|
|
"enhanced row has insufficient samples: cpu=$cpuSampleCount gpu=$gpuSampleCount")
|
|
}
|
|
if (-not [double]::IsFinite($cpuP50) -or $cpuP50 -lt 0 -or
|
|
-not [double]::IsFinite($cpuP99) -or $cpuP99 -lt 0 -or
|
|
-not [double]::IsFinite($gpuP50) -or $gpuP50 -lt 0 -or
|
|
-not [double]::IsFinite($gpuP99) -or $gpuP99 -lt 0 -or
|
|
$residentGpuBytes -lt 0) {
|
|
Add-Failure $rowFailures 'performance measurements are non-finite or negative'
|
|
}
|
|
if ($cpuP50 -gt $budget.IncrementalCpuMillisecondsP50) {
|
|
Add-Failure $rowFailures (
|
|
"incremental CPU p50 $(Format-Invariant $cpuP50) ms exceeds " +
|
|
"$(Format-Invariant $budget.IncrementalCpuMillisecondsP50) ms")
|
|
}
|
|
if ($cpuP99 -gt $budget.IncrementalCpuMillisecondsP99) {
|
|
Add-Failure $rowFailures (
|
|
"incremental CPU p99 $(Format-Invariant $cpuP99) ms exceeds " +
|
|
"$(Format-Invariant $budget.IncrementalCpuMillisecondsP99) ms")
|
|
}
|
|
if ($residentGpuBytes -gt $budget.ResidentGpuBytes) {
|
|
Add-Failure $rowFailures (
|
|
"resident GPU bytes $residentGpuBytes exceed $($budget.ResidentGpuBytes)")
|
|
}
|
|
if ($gpuBudgetApplies -and
|
|
$gpuP50 -gt $budget.InclusiveGpuMillisecondsP50At1080p) {
|
|
Add-Failure $rowFailures (
|
|
"inclusive GPU p50 $(Format-Invariant $gpuP50) ms exceeds the 1080p " +
|
|
"ceiling $(Format-Invariant $budget.InclusiveGpuMillisecondsP50At1080p) ms")
|
|
}
|
|
if ($gpuBudgetApplies -and
|
|
$gpuP99 -gt $budget.InclusiveGpuMillisecondsP99At1080p) {
|
|
Add-Failure $rowFailures (
|
|
"inclusive GPU p99 $(Format-Invariant $gpuP99) ms exceeds the 1080p " +
|
|
"ceiling $(Format-Invariant $budget.InclusiveGpuMillisecondsP99At1080p) ms")
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Add-Failure $rowFailures $_.Exception.Message
|
|
}
|
|
|
|
$passed = $rowFailures.Count -eq 0
|
|
$row = [pscustomobject][ordered]@{
|
|
Id = $rowId
|
|
FramePacing = $pacing
|
|
Preset = $preset
|
|
Resolution = $resolution
|
|
CaptureDirectory = $rowDirectory
|
|
MetadataPath = $metadataPath
|
|
CaptureExitCode = $captureExitCode
|
|
Passed = $passed
|
|
Failures = @($rowFailures)
|
|
Activation = [pscustomobject][ordered]@{
|
|
MetadataSchemaVersion = $metadataSchemaVersion
|
|
PackVersion = $packVersion
|
|
State = $activationState
|
|
Generation = $activationGeneration
|
|
EffectiveQuality = $effectiveQuality
|
|
FailureReason = $failureReason
|
|
Availability = $availability
|
|
UnavailableClassification = $unavailableClassification
|
|
}
|
|
FramebufferSha256 = $framebufferSha256
|
|
PairedDefaultFramebufferSha256 = $null
|
|
PairedDefaultComparison = $null
|
|
Process = $processEvidence
|
|
Adapter = $adapterEvidence
|
|
Samples = [pscustomobject][ordered]@{
|
|
IncrementalCpu = $cpuSampleCount
|
|
AbsoluteReceiverCpu = $absoluteReceiverCpuSampleCount
|
|
InclusiveGpu = $gpuSampleCount
|
|
}
|
|
PerformanceMilliseconds = [pscustomobject][ordered]@{
|
|
IncrementalCpuP50 = $cpuP50
|
|
IncrementalCpuP95 = $cpuP95
|
|
IncrementalCpuP99 = $cpuP99
|
|
AbsoluteReceiverCpuP50 = $absoluteReceiverCpuP50
|
|
AbsoluteReceiverCpuP95 = $absoluteReceiverCpuP95
|
|
AbsoluteReceiverCpuP99 = $absoluteReceiverCpuP99
|
|
InclusiveGpuP50 = $gpuP50
|
|
InclusiveGpuP95 = $gpuP95
|
|
InclusiveGpuP99 = $gpuP99
|
|
}
|
|
Resources = [pscustomobject][ordered]@{
|
|
TopLevelResidentGpuBytes = $topResidentGpuBytes
|
|
TopLevelTransientGpuBytes = $topTransientGpuBytes
|
|
ResidentGpuBytes = $residentGpuBytes
|
|
TransientGpuBytes = $transientGpuBytes
|
|
Images = $imageCount
|
|
Buffers = $bufferCount
|
|
}
|
|
Work = [pscustomobject][ordered]@{
|
|
ShadowCasters = $casterCount
|
|
CascadeDraws = $cascadeCount
|
|
DrawCalls = $drawCalls
|
|
DispatchCalls = $dispatchCalls
|
|
CpuClassificationCalls = $classificationCalls
|
|
PassIds = @($passIds)
|
|
}
|
|
Budget = if ($null -eq $budget) {
|
|
$null
|
|
}
|
|
else {
|
|
[pscustomobject][ordered]@{
|
|
IncrementalCpuMillisecondsP50 =
|
|
$budget.IncrementalCpuMillisecondsP50
|
|
IncrementalCpuMillisecondsP99 =
|
|
$budget.IncrementalCpuMillisecondsP99
|
|
InclusiveGpuMillisecondsP50At1080p =
|
|
$budget.InclusiveGpuMillisecondsP50At1080p
|
|
InclusiveGpuMillisecondsP99At1080p =
|
|
$budget.InclusiveGpuMillisecondsP99At1080p
|
|
ResidentGpuBytes = $budget.ResidentGpuBytes
|
|
GpuBudgetApplies = $gpuBudgetApplies
|
|
}
|
|
}
|
|
}
|
|
$null = $rows.Add($row)
|
|
foreach ($failure in $rowFailures) {
|
|
$null = $matrixFailures.Add("${rowId}: $failure")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($row in @($rows | Where-Object { $_.Preset -ne 'retail' })) {
|
|
$defaultRow = @($rows | Where-Object {
|
|
$_.Preset -eq 'retail' -and
|
|
$_.FramePacing -eq $row.FramePacing -and
|
|
$_.Resolution -eq $row.Resolution
|
|
})
|
|
if ($defaultRow.Count -ne 1 -or
|
|
[string]::IsNullOrWhiteSpace([string]$defaultRow[0].FramebufferSha256)) {
|
|
$message = "$($row.Id): paired default framebuffer digest is unavailable"
|
|
$null = $matrixFailures.Add($message)
|
|
$row.Passed = $false
|
|
$row.Failures = @($row.Failures) + $message
|
|
}
|
|
else {
|
|
$row.PairedDefaultFramebufferSha256 = $defaultRow[0].FramebufferSha256
|
|
if ($row.Activation.Availability -eq 'Unavailable') {
|
|
$expectedPng = Assert-MatrixContainedPath $defaultRow[0].CaptureDirectory (
|
|
Join-Path $defaultRow[0].CaptureDirectory "screenshots\$screenshotLeaf.png")
|
|
$actualPng = Assert-MatrixContainedPath $row.CaptureDirectory (
|
|
Join-Path $row.CaptureDirectory "screenshots\$screenshotLeaf.png")
|
|
$comparisonReport = Assert-MatrixContainedPath $row.CaptureDirectory (
|
|
Join-Path $row.CaptureDirectory 'compare-paired-default.json')
|
|
$comparisonMask = Assert-MatrixContainedPath $row.CaptureDirectory (
|
|
Join-Path $row.CaptureDirectory 'compare-paired-default-mask.png')
|
|
try {
|
|
$row.PairedDefaultComparison = Compare-MatrixFallbackFramebuffer `
|
|
-Expected $expectedPng `
|
|
-Actual $actualPng `
|
|
-ReportPath $comparisonReport `
|
|
-MaskPath $comparisonMask
|
|
if (-not $row.PairedDefaultComparison.Passed) {
|
|
$message = "$($row.Id): $($row.PairedDefaultComparison.Failure)"
|
|
$null = $matrixFailures.Add($message)
|
|
$row.Passed = $false
|
|
$row.Failures = @($row.Failures) + $message
|
|
}
|
|
}
|
|
catch {
|
|
$message = "$($row.Id): fallback/default screenshot comparison failed: $($_.Exception.Message)"
|
|
$null = $matrixFailures.Add($message)
|
|
$row.Passed = $false
|
|
$row.Failures = @($row.Failures) + $message
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$finishedUtc = [DateTime]::UtcNow
|
|
$report = [pscustomobject][ordered]@{
|
|
SchemaVersion = 1
|
|
Scope = 'offline fixed-scene pack diagnostics; final receiver CPU authority requires connected identical pack-off/on A/B'
|
|
Passed = $matrixFailures.Count -eq 0
|
|
StartedUtc = $startedUtc.ToString('O')
|
|
FinishedUtc = $finishedUtc.ToString('O')
|
|
SourceCommit = $sourceCommit
|
|
TrackedSourceStatus = @($sourceStatus)
|
|
BinaryProductVersion = $binaryIdentity.BinaryProductVersion
|
|
BinaryCommit = $binaryIdentity.BinaryCommit
|
|
BinaryMatchesSource = $binaryIdentity.BinaryMatchesSource
|
|
SkipBuild = $binaryIdentity.SkipBuild
|
|
FramePacing = $FramePacing
|
|
Pins = [pscustomobject][ordered]@{
|
|
WarmupMs = $WarmupMs
|
|
ExplicitPresetPerformanceWindowResetAfterWarmup = $true
|
|
AutomaticPerformanceWindowPolicy =
|
|
'settled rolling window; diagnostic reset prohibited'
|
|
RequiredEnhancedSamplesPerMetric = 2048
|
|
RenderPackSampleTimeoutMs = 300000
|
|
DayGroup = $DayGroup
|
|
WorldDayFraction = $WorldDayFraction
|
|
SkyPhaseSeconds = $SkyPhaseSeconds
|
|
MsaaSamples = 0
|
|
OrbitDistanceMeters = $OrbitDistanceMeters
|
|
OrbitYawDegrees = $OrbitYawDegrees
|
|
OrbitPitchDegrees = $OrbitPitchDegrees
|
|
Presets = $presets
|
|
Resolutions = $resolutions
|
|
}
|
|
Summary = [pscustomobject][ordered]@{
|
|
TotalRows = $rows.Count
|
|
PassedRows = @($rows | Where-Object Passed).Count
|
|
ActiveEnhancedRows = @($rows | Where-Object {
|
|
$_.Activation.Availability -eq 'Active' -and $_.Passed
|
|
}).Count
|
|
ResourceUnavailableRows = @($rows | Where-Object {
|
|
$_.Activation.UnavailableClassification -eq 'ResourceUnavailable' -and $_.Passed
|
|
}).Count
|
|
CapabilityUnavailableRows = @($rows | Where-Object {
|
|
$_.Activation.UnavailableClassification -eq 'CapabilityUnavailable' -and $_.Passed
|
|
}).Count
|
|
FailedRows = @($rows | Where-Object { -not $_.Passed }).Count
|
|
}
|
|
EvidenceOracle = [pscustomobject][ordered]@{
|
|
RequiredSamplesPerMetric = 2048
|
|
EqualEnhancedShadowCasterCount = $expectedCasterCount
|
|
CascadeDraws = [pscustomobject][ordered]@{ Low = 2; Medium = 3; High = 4 }
|
|
WarmedCpuClassificationCalls = 0
|
|
VulkanAdapterIdentity = $expectedAdapterIdentity
|
|
}
|
|
DeclaredBudgets = [pscustomobject][ordered]@{
|
|
Low = $budgets.low
|
|
Medium = $budgets.medium
|
|
High = $budgets.high
|
|
}
|
|
Rows = @($rows)
|
|
Failures = @($matrixFailures)
|
|
}
|
|
$report | ConvertTo-Json -Depth 12 |
|
|
Set-Content -Encoding utf8 -LiteralPath $jsonPath
|
|
|
|
$markdown = [Text.StringBuilder]::new()
|
|
$null = $markdown.AppendLine('# Atmospheric Rendering Performance Matrix')
|
|
$null = $markdown.AppendLine()
|
|
$null = $markdown.AppendLine('Scope: offline fixed-scene pack diagnostics. Final receiver CPU authority requires connected identical pack-off/on A/B.')
|
|
$null = $markdown.AppendLine()
|
|
$null = $markdown.AppendLine("- Result: **$(if ($report.Passed) { 'PASS' } else { 'FAIL' })**")
|
|
$null = $markdown.AppendLine("- Source commit: ``$sourceCommit``")
|
|
$null = $markdown.AppendLine("- Binary commit: ``$($binaryIdentity.BinaryCommit)`` (source match: $($binaryIdentity.BinaryMatchesSource))")
|
|
$null = $markdown.AppendLine("- Vulkan adapter: ``$expectedAdapterIdentity``")
|
|
$null = $markdown.AppendLine("- Frame pacing: ``$FramePacing``")
|
|
$null = $markdown.AppendLine(
|
|
"- Pins: warmup ${WarmupMs} ms; day group $DayGroup; world day fraction " +
|
|
"$(Format-Invariant $WorldDayFraction '0.################'); sky phase " +
|
|
"$(Format-Invariant $SkyPhaseSeconds '0.################') s; MSAA 0; orbit " +
|
|
"$(Format-Invariant $OrbitDistanceMeters) m")
|
|
$null = $markdown.AppendLine(
|
|
"- Evidence window: explicit presets reset diagnostics after warmup; Auto preserves " +
|
|
"its hysteresis-owned rolling window. Every active row waits for a complete 2048-sample " +
|
|
"CPU / receiver / GPU window (300000 ms timeout).")
|
|
$null = $markdown.AppendLine()
|
|
$null = $markdown.AppendLine('## Declared ceilings')
|
|
$null = $markdown.AppendLine()
|
|
$null = $markdown.AppendLine('| Preset | CPU p50 / p99 (ms) | GPU p50 / p99 at 1080p (ms) | Resident MiB |')
|
|
$null = $markdown.AppendLine('|---|---:|---:|---:|')
|
|
foreach ($preset in @('low', 'medium', 'high')) {
|
|
$budget = $budgets[$preset]
|
|
$null = $markdown.AppendLine(
|
|
"| $preset | $(Format-Invariant $budget.IncrementalCpuMillisecondsP50) / " +
|
|
"$(Format-Invariant $budget.IncrementalCpuMillisecondsP99) | " +
|
|
"$(Format-Invariant $budget.InclusiveGpuMillisecondsP50At1080p) / " +
|
|
"$(Format-Invariant $budget.InclusiveGpuMillisecondsP99At1080p) | " +
|
|
"$([Math]::Round($budget.ResidentGpuBytes / 1MB, 0)) |")
|
|
}
|
|
$null = $markdown.AppendLine()
|
|
$null = $markdown.AppendLine('## Rows')
|
|
$null = $markdown.AppendLine()
|
|
$null = $markdown.AppendLine('| Pacing | Preset | Resolution | Availability | CPU / receiver / GPU samples | CPU p50 / p95 / p99 ms | Receiver p50 / p95 / p99 ms | GPU p50 / p95 / p99 ms | Resident MiB | Process WS / private MiB | Casters | Cascades | Draw / dispatch | Framebuffer / paired-default SHA-256 | Result |')
|
|
$null = $markdown.AppendLine('|---|---|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---|---|')
|
|
foreach ($row in $rows) {
|
|
$cpu = if ($null -eq $row.PerformanceMilliseconds.IncrementalCpuP50) {
|
|
'n/a'
|
|
}
|
|
else {
|
|
"$(Format-Invariant $row.PerformanceMilliseconds.IncrementalCpuP50) / " +
|
|
"$(Format-Invariant $row.PerformanceMilliseconds.IncrementalCpuP95) / " +
|
|
"$(Format-Invariant $row.PerformanceMilliseconds.IncrementalCpuP99)"
|
|
}
|
|
$gpu = if ($null -eq $row.PerformanceMilliseconds.InclusiveGpuP50) {
|
|
'n/a'
|
|
}
|
|
else {
|
|
"$(Format-Invariant $row.PerformanceMilliseconds.InclusiveGpuP50) / " +
|
|
"$(Format-Invariant $row.PerformanceMilliseconds.InclusiveGpuP95) / " +
|
|
"$(Format-Invariant $row.PerformanceMilliseconds.InclusiveGpuP99)"
|
|
}
|
|
$receiver = if ($null -eq $row.PerformanceMilliseconds.AbsoluteReceiverCpuP50) {
|
|
'n/a'
|
|
}
|
|
else {
|
|
"$(Format-Invariant $row.PerformanceMilliseconds.AbsoluteReceiverCpuP50) / " +
|
|
"$(Format-Invariant $row.PerformanceMilliseconds.AbsoluteReceiverCpuP95) / " +
|
|
"$(Format-Invariant $row.PerformanceMilliseconds.AbsoluteReceiverCpuP99)"
|
|
}
|
|
$resident = if ($null -eq $row.Resources.ResidentGpuBytes) {
|
|
'n/a'
|
|
}
|
|
else {
|
|
Format-Invariant ($row.Resources.ResidentGpuBytes / 1MB)
|
|
}
|
|
$processMemory = if ($null -eq $row.Process) { 'n/a' } else {
|
|
"$(Format-Invariant ($row.Process.WorkingSetBytes / 1MB)) / " +
|
|
"$(Format-Invariant ($row.Process.PrivateMemoryBytes / 1MB))"
|
|
}
|
|
$digest = if ($null -eq $row.FramebufferSha256) { 'n/a' } else { $row.FramebufferSha256 }
|
|
$pairedDigest = if ($null -eq $row.PairedDefaultFramebufferSha256) { 'n/a' } else { $row.PairedDefaultFramebufferSha256 }
|
|
$result = if ($row.Passed -and $row.Activation.Availability -eq 'Unavailable') {
|
|
'UNAVAILABLE: ' + $row.Activation.UnavailableClassification + ' - ' +
|
|
(([string]$row.Activation.FailureReason) -replace '\|', '/')
|
|
}
|
|
elseif ($row.Passed) {
|
|
'PASS'
|
|
}
|
|
else {
|
|
'FAIL: ' + ((@($row.Failures) -join '; ') -replace '\|', '/')
|
|
}
|
|
$null = $markdown.AppendLine(
|
|
"| $($row.FramePacing) | $($row.Preset) | $($row.Resolution) | $($row.Activation.Availability) | " +
|
|
"$($row.Samples.IncrementalCpu) / $($row.Samples.AbsoluteReceiverCpu) / $($row.Samples.InclusiveGpu) | " +
|
|
"$cpu | $receiver | $gpu | $resident | $processMemory | $($row.Work.ShadowCasters) | " +
|
|
"$($row.Work.CascadeDraws) | $($row.Work.DrawCalls) / $($row.Work.DispatchCalls) | " +
|
|
"$digest / $pairedDigest | $result |")
|
|
}
|
|
if ($matrixFailures.Count -ne 0) {
|
|
$null = $markdown.AppendLine()
|
|
$null = $markdown.AppendLine('## Failures')
|
|
$null = $markdown.AppendLine()
|
|
foreach ($failure in $matrixFailures) {
|
|
$null = $markdown.AppendLine("- $failure")
|
|
}
|
|
}
|
|
$markdown.ToString() | Set-Content -Encoding utf8 -LiteralPath $markdownPath
|
|
|
|
Write-Output "JSON=$jsonPath"
|
|
Write-Output "MARKDOWN=$markdownPath"
|
|
Write-Output "RESULT=$(if ($report.Passed) { 'PASS' } else { 'FAIL' })"
|
|
if (-not $report.Passed) {
|
|
exit 1
|
|
}
|