feat(render): implement Campaign AR and terrain fidelity

This commit is contained in:
Erik 2026-08-22 13:13:29 +02:00
parent 99cf26e00c
commit 7a5f96ede5
368 changed files with 50611 additions and 950 deletions

View file

@ -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 }