acdream/tools/connected-render-pack-gate-common.ps1

418 lines
19 KiB
PowerShell

# Shared render-pack selection seam for connected graphical gates.
$script:ConnectedGateEnvironmentNames = @(
'ACDREAM_AUTOMATION_ARTIFACT_DIR', 'ACDREAM_CACHE_DIR',
'ACDREAM_COLLISION_SHADOW_DIR', 'ACDREAM_COLLISION_SHADOW_EVERY',
'ACDREAM_CONFIG_DIR', 'ACDREAM_DATA_DIR', 'ACDREAM_DAT_DIR',
'ACDREAM_DEVTOOLS', 'ACDREAM_DUMP_MOVE_TRUTH', 'ACDREAM_FRAME_HISTORY',
'ACDREAM_FRAME_PROF', 'ACDREAM_LIVE', 'ACDREAM_NET_DROP_DIR',
'ACDREAM_NET_DROP_PCT', 'ACDREAM_NET_DROP_SEED', 'ACDREAM_NO_AUDIO',
'ACDREAM_PAK_PATH', 'ACDREAM_RENDER_BACKEND', 'ACDREAM_RETAIL_UI',
'ACDREAM_AUTOMATION_EXACT_FRAMEBUFFER', 'ACDREAM_DAY_GROUP',
'ACDREAM_WORLD_TIME', 'ACDREAM_SKY_PHASE_SECONDS',
'ACDREAM_ORBIT_DISTANCE_METERS', 'ACDREAM_ORBIT_YAW_DEGREES',
'ACDREAM_ORBIT_PITCH_DEGREES', 'ACDREAM_VULKAN_DEVICE',
'ACDREAM_VULKAN_FORCE_UNSUPPORTED', 'ACDREAM_VULKAN_PROBE',
'ACDREAM_VULKAN_PROBE_FRAMES', 'ACDREAM_TEST_HOST',
'ACDREAM_TEST_PASS', 'ACDREAM_TEST_PORT', 'ACDREAM_TEST_USER',
'ACDREAM_UI_PROBE_DUMP', 'ACDREAM_UI_PROBE_SCRIPT',
'ACDREAM_UNCAPPED_RENDER', 'ACDREAM_WB_DIAG'
)
function Assert-ConnectedGateSafeLeafName {
param([Parameter(Mandatory = $true)][string]$Name)
if ($Name -notmatch '^[A-Za-z0-9][A-Za-z0-9_-]{0,79}$') {
throw "Unsafe screenshot leaf name '$Name'."
}
}
function Assert-ConnectedGateContainedPath {
param([Parameter(Mandatory = $true)][string]$Root,
[Parameter(Mandatory = $true)][string]$Path)
$rootFull = [IO.Path]::GetFullPath($Root).TrimEnd(
[IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar)
$pathFull = [IO.Path]::GetFullPath($Path)
$prefix = $rootFull + [IO.Path]::DirectorySeparatorChar
if (-not $pathFull.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) {
throw "Path '$pathFull' is not contained by gate root '$rootFull'."
}
return $pathFull
}
function Assert-ConnectedGateNoReparsePoint {
param([Parameter(Mandatory = $true)][string]$Path)
if ((Get-Item -LiteralPath $Path).Attributes -band [IO.FileAttributes]::ReparsePoint) {
throw "Gate path must not be a reparse point: '$Path'."
}
}
function Get-ConnectedRenderPackExpectation {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('retail', 'low', 'medium', 'high', 'auto')]
[string]$Preset
)
return [pscustomobject][ordered]@{
RequestedPreset = $Preset
PackId = if ($Preset -eq 'retail') { 'retail' } else { 'acdream.atmospheric' }
PackVersion = if ($Preset -eq 'retail') { $null } else { '1.0.0' }
PresetId = if ($Preset -eq 'retail') { 'off' } else { $Preset }
ExpectedState = if ($Preset -eq 'retail') { 0 } else { 2 }
ExpectedSchemaVersion = 1
}
}
function New-ConnectedRenderPackGateState {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$Root,
[Parameter(Mandatory = $true)]
[ValidateSet('retail', 'low', 'medium', 'high', 'auto')]
[string]$Preset,
[hashtable]$SettingOverrides = @{}
)
if ($Preset -eq 'retail' -and $SettingOverrides.Count -ne 0) {
throw 'Render-pack setting overrides require an enhanced render-pack preset.'
}
$rootFull = [IO.Path]::GetFullPath($Root)
if (-not (Test-Path -LiteralPath $rootFull -PathType Container)) {
throw "Connected gate root does not exist: '$rootFull'."
}
Assert-ConnectedGateNoReparsePoint $rootFull
$stateDirectory = Assert-ConnectedGateContainedPath $rootFull (Join-Path $rootFull 'isolated-state')
if (Test-Path -LiteralPath $stateDirectory) {
throw "Isolated gate state already exists: '$stateDirectory'."
}
# A connected closeout row must not inherit a diagnostic, content-path,
# budget, camera, weather, or device override from a prior shell run. Keep
# the explicitly-owned launch names for absent-variable restoration and
# also capture every currently-defined ACDREAM_* name so newly-added knobs
# fail isolated without requiring this seam to know their semantics first.
$transactionNames = @(
$script:ConnectedGateEnvironmentNames
Get-ChildItem Env: |
Where-Object { $_.Name -like 'ACDREAM_*' } |
Select-Object -ExpandProperty Name
) | Sort-Object -Unique
$previous = [ordered]@{}
foreach ($name in $transactionNames) {
$previous[$name] = [Environment]::GetEnvironmentVariable(
$name, [EnvironmentVariableTarget]::Process)
Remove-Item -LiteralPath "Env:$name" -ErrorAction SilentlyContinue
}
$configDirectory = Join-Path $stateDirectory 'config'
$dataDirectory = Join-Path $stateDirectory 'data'
$cacheDirectory = Join-Path $stateDirectory 'cache'
$null = New-Item -ItemType Directory -Path $configDirectory, $dataDirectory, $cacheDirectory
foreach ($path in @($stateDirectory, $configDirectory, $dataDirectory, $cacheDirectory)) {
Assert-ConnectedGateNoReparsePoint $path
}
$packId = if ($Preset -eq 'retail') { 'retail' } else { 'acdream.atmospheric' }
$packVersion = if ($Preset -eq 'retail') { $null } else { '1.0.0' }
$presetId = if ($Preset -eq 'retail') { 'off' } else { $Preset }
$expectedState = if ($Preset -eq 'retail') { 0 } else { 2 }
$orderedOverrides = [ordered]@{}
foreach ($key in @($SettingOverrides.Keys | Sort-Object)) {
if ([string]::IsNullOrWhiteSpace([string]$key)) {
throw 'Render-pack setting override IDs cannot be empty.'
}
$orderedOverrides[[string]$key] = [string]$SettingOverrides[$key]
}
[ordered]@{
display = [ordered]@{ renderPack = [ordered]@{
packId = $packId; packVersion = $packVersion; presetId = $presetId
settingOverrides = $orderedOverrides
} }
version = 3
} | ConvertTo-Json -Depth 8 |
Set-Content -Encoding utf8 -LiteralPath (Join-Path $configDirectory 'settings.json')
[Environment]::SetEnvironmentVariable('ACDREAM_CONFIG_DIR', $configDirectory, 'Process')
[Environment]::SetEnvironmentVariable('ACDREAM_DATA_DIR', $dataDirectory, 'Process')
[Environment]::SetEnvironmentVariable('ACDREAM_CACHE_DIR', $cacheDirectory, 'Process')
return [pscustomobject][ordered]@{
RequestedPreset = $Preset; PackId = $packId; PackVersion = $packVersion
PresetId = $presetId; ExpectedState = $expectedState; ExpectedSchemaVersion = 1
SettingOverrides = $orderedOverrides; StateDirectory = $stateDirectory
ConfigDirectory = $configDirectory; DataDirectory = $dataDirectory
CacheDirectory = $cacheDirectory; PreviousEnvironment = $previous
}
}
function Restore-ConnectedRenderPackGateEnvironment {
[CmdletBinding()]
param([Parameter(Mandatory = $true)][object]$State)
foreach ($entry in $State.PreviousEnvironment.GetEnumerator()) {
if ($null -eq $entry.Value) {
Remove-Item -LiteralPath "Env:$($entry.Key)" -ErrorAction SilentlyContinue
}
else {
[Environment]::SetEnvironmentVariable(
[string]$entry.Key, $entry.Value, [EnvironmentVariableTarget]::Process)
}
}
}
function New-ConnectedGraphicalSessionConfig {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][object]$State,
[Parameter(Mandatory = $true)][string]$Account,
[string]$HostName = '127.0.0.1',
[ValidateRange(1, 65535)][int]$Port = 9000,
[ValidateRange(0, [int]::MaxValue)][int]$CharacterIndex = 0,
[string]$CredentialEnvironmentVariable = 'ACDREAM_TEST_PASS',
[string[]]$Plugins = @(),
[string]$StatusFile
)
if ([string]::IsNullOrWhiteSpace($Account)) {
throw 'Connected graphical session account cannot be empty.'
}
if ([string]::IsNullOrWhiteSpace($HostName)) {
throw 'Connected graphical session host cannot be empty.'
}
if ($CredentialEnvironmentVariable -notmatch '^[A-Za-z_][A-Za-z0-9_]*$') {
throw "Unsafe credential environment-variable name '$CredentialEnvironmentVariable'."
}
$path = Assert-ConnectedGateContainedPath $State.StateDirectory (
Join-Path $State.StateDirectory 'graphical-session.json')
if (Test-Path -LiteralPath $path) {
throw "Connected graphical session config already exists: '$path'."
}
$session = [ordered]@{
id = 'connected-gate'
endpoint = [ordered]@{ host = $HostName; port = $Port }
account = $Account
character = [ordered]@{ index = $CharacterIndex }
credential = [ordered]@{
provider = 'Environment'
reference = $CredentialEnvironmentVariable
}
}
if ($PSBoundParameters.ContainsKey('Plugins')) {
$orderedPlugins = @($Plugins | ForEach-Object {
if ([string]::IsNullOrWhiteSpace($_)) {
throw 'Connected graphical session plugin IDs cannot be empty.'
}
$_
} | Sort-Object -Unique)
$session['plugins'] = $orderedPlugins
}
if ($PSBoundParameters.ContainsKey('StatusFile')) {
if ([string]::IsNullOrWhiteSpace($StatusFile)) {
throw 'Connected graphical session status file cannot be empty.'
}
$session['statusFile'] = Assert-ConnectedGateContainedPath `
$State.StateDirectory $StatusFile
}
[ordered]@{
version = 1
sessions = @($session)
} | ConvertTo-Json -Depth 8 |
Set-Content -LiteralPath $path -Encoding utf8
return $path
}
function Remove-ConnectedGraphicalSessionConfig {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][object]$State,
[Parameter(Mandatory = $true)][string]$Path
)
$containedPath = Assert-ConnectedGateContainedPath $State.StateDirectory $Path
if (Test-Path -LiteralPath $containedPath -PathType Leaf) {
Remove-Item -LiteralPath $containedPath -Force
}
}
function Get-ConnectedGateBinaryIdentity {
[CmdletBinding()]
param([Parameter(Mandatory = $true)][string]$Repository,
[Parameter(Mandatory = $true)][string]$Executable,
[switch]$SkipBuild)
$sourceCommit = (& git -C $Repository rev-parse HEAD).Trim().ToLowerInvariant()
$trackedStatus = @(& git -C $Repository status --short --untracked-files=all)
$productVersion = [Diagnostics.FileVersionInfo]::GetVersionInfo($Executable).ProductVersion
$match = [regex]::Match([string]$productVersion, '(?i)(?<![0-9a-f])([0-9a-f]{40})(?![0-9a-f])')
$binaryCommit = if ($match.Success) { $match.Groups[1].Value.ToLowerInvariant() } else { $null }
if ($null -eq $binaryCommit) {
throw "Measured binary ProductVersion does not contain a full source commit: '$productVersion'."
}
if ($binaryCommit -cne $sourceCommit) {
throw "Measured binary commit $binaryCommit differs from checked-out source commit $sourceCommit."
}
if ($trackedStatus.Count -ne 0) {
throw 'Connected closeout evidence cannot prove binary/source identity while source changes are present.'
}
return [pscustomobject][ordered]@{
SourceCommit = $sourceCommit; SourceTrackedStatus = $trackedStatus
BinaryProductVersion = $productVersion; BinaryCommit = $binaryCommit
BinaryMatchesSource = $true; SkipBuild = [bool]$SkipBuild
}
}
function Add-ConnectedRenderPackMetadataFailures {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$ArtifactDirectory,
[Parameter(Mandatory = $true)][string[]]$ScreenshotNames,
[Parameter(Mandatory = $true)][object]$State,
[Parameter(Mandatory = $true)][System.Collections.IList]$Failures,
[Parameter(Mandatory = $true)][string]$Label
)
try {
Assert-ConnectedGateNoReparsePoint $ArtifactDirectory
$screenshotsDirectory = Assert-ConnectedGateContainedPath $ArtifactDirectory (
Join-Path $ArtifactDirectory 'screenshots')
if (Test-Path -LiteralPath $screenshotsDirectory -PathType Container) {
Assert-ConnectedGateNoReparsePoint $screenshotsDirectory
}
}
catch {
$null = $Failures.Add("${Label}: unsafe artifact directory: $($_.Exception.Message)")
return
}
foreach ($name in $ScreenshotNames) {
try { Assert-ConnectedGateSafeLeafName $name }
catch { $null = $Failures.Add("${Label}: $($_.Exception.Message)"); continue }
$metadataPath = Assert-ConnectedGateContainedPath $ArtifactDirectory (
Join-Path $ArtifactDirectory "screenshots\$name.metadata.json")
if (-not (Test-Path -LiteralPath $metadataPath)) {
$null = $Failures.Add("${Label}: render-pack metadata is missing for screenshot '$name'")
continue
}
try {
$metadata = Get-Content -Raw -LiteralPath $metadataPath | ConvertFrom-Json
if ([int]$metadata.SchemaVersion -ne [int]$State.ExpectedSchemaVersion) {
$null = $Failures.Add("${Label}: screenshot '$name' schema was $($metadata.SchemaVersion), expected $($State.ExpectedSchemaVersion)")
}
$actual = $metadata.RenderPack
if ($null -eq $actual) {
$null = $Failures.Add("${Label}: screenshot '$name' has no RenderPack metadata")
continue
}
foreach ($comparison in @(
@('PackId', [string]$State.PackId),
@('PackVersion', [string]$State.PackVersion),
@('PresetId', [string]$State.PresetId))) {
$property = [string]$comparison[0]; $expected = [string]$comparison[1]
if ([string]$actual.$property -cne $expected) {
$null = $Failures.Add("${Label}: screenshot '$name' $property '$($actual.$property)', expected '$expected'")
}
}
if ([int]$actual.State -ne [int]$State.ExpectedState) {
$null = $Failures.Add("${Label}: screenshot '$name' state was $($actual.State), expected $($State.ExpectedState)")
}
$expectedGeneration = if ($State.RequestedPreset -eq 'retail') { 0 } else { 1 }
if ([int]$actual.ActivationGeneration -lt $expectedGeneration) {
$null = $Failures.Add("${Label}: screenshot '$name' activation generation was $($actual.ActivationGeneration), expected at least $expectedGeneration")
}
$allowedQuality = if ($State.RequestedPreset -eq 'auto') { @('low', 'medium', 'high') }
elseif ($State.RequestedPreset -eq 'retail') { @('off') }
else { @([string]$State.RequestedPreset) }
if ([string]$actual.EffectiveQuality -cnotin $allowedQuality) {
$null = $Failures.Add("${Label}: screenshot '$name' effective quality '$($actual.EffectiveQuality)' is invalid for '$($State.RequestedPreset)'")
}
if (-not [string]::IsNullOrWhiteSpace([string]$actual.FailureReason)) {
$null = $Failures.Add("${Label}: screenshot '$name' render-pack failure: $($actual.FailureReason)")
}
if ([long]$actual.RetainedGpuBytes -ne [long]$actual.Performance.ResidentGpuBytes) {
$null = $Failures.Add("${Label}: screenshot '$name' retained GPU byte ledgers disagree")
}
if ([long]$actual.TransientGpuBytes -ne [long]$actual.Performance.TransientGpuBytes) {
$null = $Failures.Add("${Label}: screenshot '$name' transient GPU byte ledgers disagree")
}
$worldTransformUsage =
$actual.PSObject.Properties['SharedWorldTransformUsedInstances']
if ($null -eq $worldTransformUsage) {
$null = $Failures.Add(
"${Label}: screenshot '$name' has no combined shared-world-transform usage")
}
if ($State.RequestedPreset -eq 'retail') {
foreach ($property in @(
'RetainedGpuBytes',
'TransientGpuBytes',
'ImageCount',
'BufferCount',
'DrawCalls',
'DispatchCalls',
'ShadowCasterCount',
'CascadeDrawCount',
'CpuClassificationCalls',
'SharedWorldTransformUsedInstances')) {
if ([long]$actual.$property -ne 0) {
$null = $Failures.Add(
"${Label}: retail screenshot '$name' $property was $($actual.$property), expected zero pack work")
}
}
if (@($actual.Passes).Count -ne 0) {
$null = $Failures.Add(
"${Label}: retail screenshot '$name' recorded pack passes, expected none")
}
}
else {
if ([long]$actual.RetainedGpuBytes -le 0 -or
[int]$actual.ImageCount -le 0 -or
([int]$actual.DrawCalls + [int]$actual.DispatchCalls) -le 0 -or
@($actual.Passes).Count -le 0) {
$null = $Failures.Add(
"${Label}: enhanced screenshot '$name' was active but recorded no complete pack graph work")
}
# Every built-in enhanced preset includes Tier 2. When authored
# outdoor sun/weather policy produces non-zero shadow strength,
# the same captured frame must prove caster and cascade work.
if ([bool]$actual.Outdoor -and
[double]$actual.DirectionalShadowStrength -gt 0) {
$expectedCascades = @{
low = 2
medium = 3
high = 4
}[[string]$actual.EffectiveQuality]
if ([int]$actual.ShadowCasterCount -le 0) {
$null = $Failures.Add(
"${Label}: enhanced outdoor screenshot '$name' had positive shadow strength but no shadow casters")
}
if ($null -ne $worldTransformUsage -and
[long]$worldTransformUsage.Value -le 0) {
$null = $Failures.Add(
"${Label}: enhanced outdoor screenshot '$name' had positive shadow strength but no combined shared-world-transform usage")
}
if ($null -eq $expectedCascades -or
[int]$actual.CascadeDrawCount -ne [int]$expectedCascades) {
$null = $Failures.Add(
"${Label}: enhanced outdoor screenshot '$name' rendered $($actual.CascadeDrawCount) cascades, expected $expectedCascades for '$($actual.EffectiveQuality)'")
}
}
}
}
catch {
$null = $Failures.Add("${Label}: render-pack metadata for screenshot '$name' is invalid: $($_.Exception.Message)")
}
}
}
function Get-ConnectedRenderPackGateReport {
[CmdletBinding()]
param([Parameter(Mandatory = $true)][object]$State)
return [pscustomobject][ordered]@{
RequestedPreset = $State.RequestedPreset; PackId = $State.PackId
PackVersion = $State.PackVersion; PresetId = $State.PresetId
ExpectedActivationState = $State.ExpectedState
ExpectedMetadataSchemaVersion = $State.ExpectedSchemaVersion
SettingOverrides = $State.SettingOverrides
IsolatedStateDirectory = $State.StateDirectory
}
}