feat(render): implement Campaign AR and terrain fidelity
This commit is contained in:
parent
99cf26e00c
commit
7a5f96ede5
368 changed files with 50611 additions and 950 deletions
|
|
@ -6,11 +6,15 @@ param(
|
|||
[string]$AceLogPath = 'C:\ACE\Server\ACE_Log.txt',
|
||||
[switch]$SkipBuild,
|
||||
[int]$SessionTimeoutSeconds = 420,
|
||||
[int]$CollisionShadowEvery = 0
|
||||
[int]$CollisionShadowEvery = 0,
|
||||
[ValidateSet('retail', 'low', 'medium', 'high', 'auto')]
|
||||
[string]$RenderPackPreset = 'retail',
|
||||
[hashtable]$RenderPackSettingOverrides = @{}
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
. (Join-Path $PSScriptRoot 'connected-render-pack-gate-common.ps1')
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Account)) { $Account = 'testaccount' }
|
||||
if ([string]::IsNullOrWhiteSpace($Password)) { $Password = 'testpassword' }
|
||||
|
|
@ -231,7 +235,8 @@ function Invoke-Session(
|
|||
[string]$RoutePath,
|
||||
[bool]$Uncapped,
|
||||
[string[]]$ExpectedCheckpoints,
|
||||
[string[]]$ExpectedScreenshots)
|
||||
[string[]]$ExpectedScreenshots,
|
||||
[hashtable]$ScreenshotStateOverrides = @{})
|
||||
{
|
||||
$sessionDir = Join-Path $root $Label
|
||||
$artifactDir = Join-Path $sessionDir 'artifacts'
|
||||
|
|
@ -294,6 +299,8 @@ function Invoke-Session(
|
|||
|
||||
$client.Refresh()
|
||||
$processSample = [pscustomobject][ordered]@{
|
||||
ProcessId = $client.Id
|
||||
StartTimeUtc = $client.StartTime.ToUniversalTime().ToString('O')
|
||||
WorkingSetMiB = [Math]::Round($client.WorkingSet64 / 1MB, 1)
|
||||
PrivateMiB = [Math]::Round($client.PrivateMemorySize64 / 1MB, 1)
|
||||
HandleCount = $client.HandleCount
|
||||
|
|
@ -335,6 +342,18 @@ function Invoke-Session(
|
|||
$png = Join-Path $artifactDir "screenshots\$name.png"
|
||||
if (-not (Test-Png $png)) { $failures.Add("${Label}: missing or invalid screenshot '$png'") }
|
||||
}
|
||||
foreach ($name in $ExpectedScreenshots) {
|
||||
$expectedState = if ($ScreenshotStateOverrides.ContainsKey($name)) {
|
||||
$ScreenshotStateOverrides[$name]
|
||||
}
|
||||
else { $renderPackGate }
|
||||
Add-ConnectedRenderPackMetadataFailures `
|
||||
-ArtifactDirectory $artifactDir `
|
||||
-ScreenshotNames @($name) `
|
||||
-State $expectedState `
|
||||
-Failures $failures `
|
||||
-Label $Label
|
||||
}
|
||||
|
||||
$graceful = Close-ClientGracefully $client
|
||||
$client.Refresh()
|
||||
|
|
@ -388,6 +407,128 @@ function Invoke-Session(
|
|||
}
|
||||
}
|
||||
|
||||
function Add-AtmosphericTransitionSemanticGates([object]$Session) {
|
||||
if ($null -eq $Session) { return }
|
||||
$screenshots = Join-Path $Session.ArtifactDirectory 'screenshots'
|
||||
$rows = @{}
|
||||
foreach ($name in @(
|
||||
'transition_selected_high',
|
||||
'transition_disabled_retail',
|
||||
'transition_reenabled_high',
|
||||
'transition_resized_high',
|
||||
'transition_dusk_high',
|
||||
'transition_overcast_high',
|
||||
'transition_rain_high')) {
|
||||
$path = Join-Path $screenshots "$name.metadata.json"
|
||||
if (Test-Path -LiteralPath $path) {
|
||||
$rows[$name] = Get-Content -Raw -LiteralPath $path | ConvertFrom-Json
|
||||
}
|
||||
}
|
||||
if ($rows.Count -ne 7) { return }
|
||||
|
||||
$selected = $rows.transition_selected_high.RenderPack
|
||||
$disabled = $rows.transition_disabled_retail.RenderPack
|
||||
$reenabled = $rows.transition_reenabled_high.RenderPack
|
||||
if ([long]$disabled.ActivationGeneration -le [long]$selected.ActivationGeneration -or
|
||||
[long]$reenabled.ActivationGeneration -le [long]$disabled.ActivationGeneration) {
|
||||
$failures.Add('atmospheric-transitions: select/disable/re-enable activation generations were not strictly monotonic')
|
||||
}
|
||||
if ([int]$selected.ShadowCasterCount -le 0) {
|
||||
$failures.Add('atmospheric-transitions: dense outdoor High row published no directional-shadow casters')
|
||||
}
|
||||
if ([int]$selected.ShadowTransformChurn.LiveDynamicRootChanges -le 0) {
|
||||
$failures.Add('atmospheric-transitions: moving High row published no live-dynamic caster transform')
|
||||
}
|
||||
if ([int]$selected.ShadowTransformChurn.EquippedChildChanges -le 0) {
|
||||
$failures.Add('atmospheric-transitions: moving High row published no equipped-child caster transform')
|
||||
}
|
||||
$casterClasses = $selected.ShadowTransformChurn.CasterClasses
|
||||
if ($null -eq $casterClasses) {
|
||||
$failures.Add('atmospheric-transitions: High row published no authoritative caster-class diagnostics')
|
||||
}
|
||||
else {
|
||||
foreach ($property in @(
|
||||
'TerrainCommands',
|
||||
'OutdoorStatics',
|
||||
'Buildings',
|
||||
'AnimatedStatics',
|
||||
'LocalPlayers',
|
||||
'RemotePlayers',
|
||||
'NonPlayerCreatures',
|
||||
'OtherLiveDynamics',
|
||||
'EquippedChildren')) {
|
||||
if ($property -notin @($casterClasses.PSObject.Properties.Name)) {
|
||||
$failures.Add(
|
||||
"atmospheric-transitions: caster diagnostics omitted required '$property' metadata")
|
||||
}
|
||||
elseif ([int]$casterClasses.$property -lt 0) {
|
||||
$failures.Add(
|
||||
"atmospheric-transitions: caster diagnostics published negative '$property' metadata")
|
||||
}
|
||||
}
|
||||
foreach ($required in @(
|
||||
@('TerrainCommands', 'terrain command'),
|
||||
@('OutdoorStatics', 'outdoor-static scenery'),
|
||||
@('Buildings', 'building'),
|
||||
@('LocalPlayers', 'local-player'),
|
||||
@('EquippedChildren', 'equipped-child'))) {
|
||||
$property = [string]$required[0]
|
||||
if ([int]$casterClasses.$property -le 0) {
|
||||
$failures.Add(
|
||||
"atmospheric-transitions: dense outdoor High row published no $($required[1]) caster evidence")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$resized = $rows.transition_resized_high
|
||||
if ([int]$resized.Width -ne 1024 -or [int]$resized.Height -ne 768) {
|
||||
$failures.Add(
|
||||
"atmospheric-transitions: resized screenshot was $($resized.Width)x$($resized.Height), expected exact 1024x768 framebuffer")
|
||||
}
|
||||
$dusk = $rows.transition_dusk_high.RenderPack
|
||||
if ([Math]::Abs(
|
||||
[double]$dusk.SunElevationDegrees -
|
||||
[double]$reenabled.SunElevationDegrees) -lt 0.01) {
|
||||
$failures.Add('atmospheric-transitions: authored time change did not alter published sun elevation')
|
||||
}
|
||||
if ([string]$rows.transition_overcast_high.RenderPack.Weather -cnotmatch '(?i)^overcast$') {
|
||||
$failures.Add('atmospheric-transitions: first weather edge did not publish Overcast')
|
||||
}
|
||||
if ([string]$rows.transition_rain_high.RenderPack.Weather -cnotmatch '(?i)^rain$') {
|
||||
$failures.Add('atmospheric-transitions: second weather edge did not publish Rain')
|
||||
}
|
||||
}
|
||||
|
||||
function Get-FreshContextRecreationGate(
|
||||
[object]$FirstSession,
|
||||
[object]$SecondSession)
|
||||
{
|
||||
$definition = 'graceful full graphical-process teardown followed by a fresh process; the fresh process constructs a new Vulkan device/context/swapchain ownership graph'
|
||||
if ($null -eq $FirstSession -or $null -eq $SecondSession) {
|
||||
$failures.Add('new-context recreation could not be proven because a required session is missing')
|
||||
return [pscustomobject][ordered]@{
|
||||
Definition = $definition
|
||||
Passed = $false
|
||||
FirstProcess = $null
|
||||
SecondProcess = $null
|
||||
}
|
||||
}
|
||||
$firstIdentity = "$($FirstSession.Process.ProcessId)@$($FirstSession.Process.StartTimeUtc)"
|
||||
$secondIdentity = "$($SecondSession.Process.ProcessId)@$($SecondSession.Process.StartTimeUtc)"
|
||||
$passed = $FirstSession.GracefulExit -and
|
||||
$SecondSession.GracefulExit -and
|
||||
$firstIdentity -cne $secondIdentity
|
||||
if (-not $passed) {
|
||||
$failures.Add('new-context recreation did not prove graceful teardown and a distinct fresh process')
|
||||
}
|
||||
return [pscustomobject][ordered]@{
|
||||
Definition = $definition
|
||||
Passed = $passed
|
||||
FirstProcess = $firstIdentity
|
||||
SecondProcess = $secondIdentity
|
||||
}
|
||||
}
|
||||
|
||||
function Add-SameLocationGates([object]$CappedSession) {
|
||||
if ($null -eq $CappedSession) { return }
|
||||
$first = @($CappedSession.Checkpoints | Where-Object { $_.name -eq 'aerlinthe_first' }) | Select-Object -First 1
|
||||
|
|
@ -413,64 +554,136 @@ function Add-SameLocationGates([object]$CappedSession) {
|
|||
}
|
||||
}
|
||||
|
||||
if (@(Get-Process -Name AcDream.App -ErrorAction SilentlyContinue).Count -gt 0) {
|
||||
throw 'an AcDream.App client is already running; close it gracefully before the gate'
|
||||
$renderPackGate = New-ConnectedRenderPackGateState `
|
||||
-Root $root `
|
||||
-Preset $RenderPackPreset `
|
||||
-SettingOverrides $RenderPackSettingOverrides
|
||||
try {
|
||||
if (@(Get-Process -Name AcDream.App -ErrorAction SilentlyContinue).Count -gt 0) {
|
||||
throw 'an AcDream.App client is already running; close it gracefully before the gate'
|
||||
}
|
||||
if (@(Get-NetUDPEndpoint -LocalPort 9000 -ErrorAction SilentlyContinue).Count -eq 0) {
|
||||
throw 'local ACE is not listening on UDP port 9000'
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $AceLogPath)) {
|
||||
throw "ACE log was not found: $AceLogPath"
|
||||
}
|
||||
|
||||
if (-not $SkipBuild) {
|
||||
& dotnet build (Join-Path $Repository 'AcDream.slnx') -c Release --no-restore
|
||||
if ($LASTEXITCODE -ne 0) { throw "Release build failed with exit code $LASTEXITCODE" }
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $exe)) { throw "client executable not found: $exe" }
|
||||
|
||||
$binaryIdentity = Get-ConnectedGateBinaryIdentity `
|
||||
-Repository $Repository -Executable $exe -SkipBuild:$SkipBuild
|
||||
|
||||
$capped = Invoke-Session `
|
||||
'capped' `
|
||||
(Join-Path $Repository 'tools\connected-world-lifecycle.route.txt') `
|
||||
$false `
|
||||
@('capped_login', 'aerlinthe_first', 'rynthid', 'facility_hub', 'holtburg_after_dungeon', 'aerlinthe_revisit') `
|
||||
@('capped_login', 'aerlinthe_first', 'facility_hub', 'holtburg_after_dungeon', 'aerlinthe_revisit')
|
||||
|
||||
Add-SameLocationGates $capped
|
||||
|
||||
# The second process starts as soon as ACE records accepting the first
|
||||
# process's transport Disconnect. No elapsed-time settle delay hides a
|
||||
# shutdown race.
|
||||
$uncapped = Invoke-Session `
|
||||
'uncapped-reconnect' `
|
||||
(Join-Path $Repository 'tools\connected-world-reconnect.route.txt') `
|
||||
$true `
|
||||
@('uncapped_reconnect') `
|
||||
@('uncapped_reconnect')
|
||||
|
||||
$contextRecreation = Get-FreshContextRecreationGate $capped $uncapped
|
||||
|
||||
# The medium matrix row is the one canonical transition row. It starts
|
||||
# from a known enhanced selection, then proves an in-process High select,
|
||||
# retail disable, exact High re-enable, live resize, and authored
|
||||
# sun/weather changes without multiplying this long route across all five
|
||||
# preset rows.
|
||||
$transitionSession = $null
|
||||
if ($RenderPackPreset -eq 'medium') {
|
||||
$highExpectation = Get-ConnectedRenderPackExpectation -Preset high
|
||||
$retailExpectation = Get-ConnectedRenderPackExpectation -Preset retail
|
||||
$transitionSession = Invoke-Session `
|
||||
'atmospheric-transitions' `
|
||||
(Join-Path $Repository 'tools\connected-render-pack-transitions.route.txt') `
|
||||
$false `
|
||||
@('atmospheric_transitions') `
|
||||
@(
|
||||
'transition_selected_high',
|
||||
'transition_disabled_retail',
|
||||
'transition_reenabled_high',
|
||||
'transition_resized_high',
|
||||
'transition_dusk_high',
|
||||
'transition_overcast_high',
|
||||
'transition_rain_high') `
|
||||
@{
|
||||
transition_selected_high = $highExpectation
|
||||
transition_disabled_retail = $retailExpectation
|
||||
transition_reenabled_high = $highExpectation
|
||||
transition_resized_high = $highExpectation
|
||||
transition_dusk_high = $highExpectation
|
||||
transition_overcast_high = $highExpectation
|
||||
transition_rain_high = $highExpectation
|
||||
}
|
||||
Add-AtmosphericTransitionSemanticGates $transitionSession
|
||||
}
|
||||
|
||||
$report = [pscustomobject][ordered]@{
|
||||
Passed = $failures.Count -eq 0
|
||||
StartedUtc = $startedUtc.ToString('O')
|
||||
FinishedUtc = [DateTime]::UtcNow.ToString('O')
|
||||
Commit = $binaryIdentity.BinaryCommit
|
||||
SourceCommit = $binaryIdentity.SourceCommit
|
||||
BinaryProductVersion = $binaryIdentity.BinaryProductVersion
|
||||
BinaryCommit = $binaryIdentity.BinaryCommit
|
||||
BinaryMatchesSource = $binaryIdentity.BinaryMatchesSource
|
||||
SkipBuild = $binaryIdentity.SkipBuild
|
||||
SourceStatus = @(& git -C $Repository status --short)
|
||||
SessionName = $env:SESSIONNAME
|
||||
CollisionShadowEvery = $CollisionShadowEvery
|
||||
RenderPackSelection = (Get-ConnectedRenderPackGateReport $renderPackGate)
|
||||
ContextRecreation = $contextRecreation
|
||||
TransitionAutomation = [pscustomobject][ordered]@{
|
||||
Executed = $null -ne $transitionSession
|
||||
CanonicalRow = 'medium'
|
||||
Session = $transitionSession
|
||||
ProvenCasterRoutes = @(
|
||||
'terrain shadow-command publication',
|
||||
'outdoor-static scenery publication (including trees, without a tree discriminator)',
|
||||
'building caster publication',
|
||||
'local-player caster publication',
|
||||
'moving live-dynamic root transform publication',
|
||||
'equipped-child caster and moving-transform publication')
|
||||
RemainingCasterClassEvidence = @(
|
||||
'a second live client is still required to prove a nonzero remote-player caster count',
|
||||
'a deterministic populated connected row is still required to prove nonzero active animated-static and non-player creature counts',
|
||||
'create-object render metadata proves non-player creature, not hostile monster versus non-hostile NPC',
|
||||
'outdoor DAT scenery has no authoritative tree discriminator, so trees remain grouped with other outdoor statics')
|
||||
}
|
||||
VideoControllers = @(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue |
|
||||
ForEach-Object { [pscustomobject]@{
|
||||
Name = $_.Name
|
||||
DriverVersion = $_.DriverVersion
|
||||
AdapterRam = $_.AdapterRAM
|
||||
} })
|
||||
Failures = @($failures)
|
||||
Warnings = @($warnings)
|
||||
Sessions = @($sessions)
|
||||
}
|
||||
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $reportPath -Encoding utf8
|
||||
|
||||
Write-Output "REPORT=$reportPath"
|
||||
Write-Output "RESULT=$(if ($report.Passed) { 'PASS' } else { 'FAIL' })"
|
||||
foreach ($failure in $failures) { Write-Output "FAILURE=$failure" }
|
||||
foreach ($warning in $warnings) { Write-Output "WARNING=$warning" }
|
||||
}
|
||||
if (@(Get-NetUDPEndpoint -LocalPort 9000 -ErrorAction SilentlyContinue).Count -eq 0) {
|
||||
throw 'local ACE is not listening on UDP port 9000'
|
||||
finally {
|
||||
Restore-ConnectedRenderPackGateEnvironment $renderPackGate
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $AceLogPath)) {
|
||||
throw "ACE log was not found: $AceLogPath"
|
||||
}
|
||||
|
||||
if (-not $SkipBuild) {
|
||||
& dotnet build (Join-Path $Repository 'AcDream.slnx') -c Release --no-restore
|
||||
if ($LASTEXITCODE -ne 0) { throw "Release build failed with exit code $LASTEXITCODE" }
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $exe)) { throw "client executable not found: $exe" }
|
||||
|
||||
$capped = Invoke-Session `
|
||||
'capped' `
|
||||
(Join-Path $Repository 'tools\connected-world-lifecycle.route.txt') `
|
||||
$false `
|
||||
@('capped_login', 'aerlinthe_first', 'rynthid', 'facility_hub', 'holtburg_after_dungeon', 'aerlinthe_revisit') `
|
||||
@('capped_login', 'aerlinthe_first', 'facility_hub', 'holtburg_after_dungeon', 'aerlinthe_revisit')
|
||||
|
||||
Add-SameLocationGates $capped
|
||||
|
||||
# The second process starts as soon as ACE records accepting the first
|
||||
# process's transport Disconnect. No elapsed-time settle delay hides a
|
||||
# shutdown race.
|
||||
$null = Invoke-Session `
|
||||
'uncapped-reconnect' `
|
||||
(Join-Path $Repository 'tools\connected-world-reconnect.route.txt') `
|
||||
$true `
|
||||
@('uncapped_reconnect') `
|
||||
@('uncapped_reconnect')
|
||||
|
||||
$report = [pscustomobject][ordered]@{
|
||||
Passed = $failures.Count -eq 0
|
||||
StartedUtc = $startedUtc.ToString('O')
|
||||
FinishedUtc = [DateTime]::UtcNow.ToString('O')
|
||||
Commit = (& git -C $Repository rev-parse HEAD).Trim()
|
||||
SourceStatus = @(& git -C $Repository status --short)
|
||||
SessionName = $env:SESSIONNAME
|
||||
CollisionShadowEvery = $CollisionShadowEvery
|
||||
VideoControllers = @(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue |
|
||||
ForEach-Object { [pscustomobject]@{
|
||||
Name = $_.Name
|
||||
DriverVersion = $_.DriverVersion
|
||||
AdapterRam = $_.AdapterRAM
|
||||
} })
|
||||
Failures = @($failures)
|
||||
Warnings = @($warnings)
|
||||
Sessions = @($sessions)
|
||||
}
|
||||
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $reportPath -Encoding utf8
|
||||
|
||||
Write-Output "REPORT=$reportPath"
|
||||
Write-Output "RESULT=$(if ($report.Passed) { 'PASS' } else { 'FAIL' })"
|
||||
foreach ($failure in $failures) { Write-Output "FAILURE=$failure" }
|
||||
foreach ($warning in $warnings) { Write-Output "WARNING=$warning" }
|
||||
|
||||
if ($failures.Count -gt 0) { exit 1 }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue