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

@ -100,6 +100,23 @@
so a known, registered band (for instance AD-46's treeline) can be quantified
separately from the rest of the frame.
.PARAMETER OrbitDistanceMeters
Diagnostic initial orbit-camera distance in metres. Zero keeps acdream's
normal default.
.PARAMETER OrbitYawDegrees
Diagnostic initial orbit-camera heading in degrees. Omit to keep acdream's
normal default.
.PARAMETER OrbitPitchDegrees
Diagnostic initial orbit-camera elevation in degrees. Omit to keep
acdream's normal default. A shallow positive angle can put a low sun in
frame for ray and volumetric-shaft captures.
.PARAMETER Uncapped
Disable both VSync and the normal refresh-rate software limiter. Omit for
the capped product cadence. This is a diagnostic measurement mode only.
.PARAMETER SkipBuild
Skip the Release build (use when the caller already built).
@ -122,6 +139,22 @@ param(
[int]$Tolerance = 2,
[double]$MaxDifferentFraction = 0.001,
[int]$MaskTopPixels = 280,
[ValidateSet('retail', 'low', 'medium', 'high', 'auto')]
[string]$RenderPackPreset = 'retail',
[ValidatePattern('^[1-9][0-9]*x[1-9][0-9]*$')]
[string]$Resolution = '1280x720',
[ValidateRange(0.0, 5000.0)]
[double]$OrbitDistanceMeters = 0,
[Nullable[double]]$OrbitYawDegrees,
[ValidateRange(-89.0, 89.0)]
[Nullable[double]]$OrbitPitchDegrees,
[hashtable]$RenderPackSettingOverrides = @{},
[ValidateRange(0, 2048)]
[int]$RequiredRenderPackSamples = 0,
[ValidateRange(1000, 600000)]
[int]$RenderPackSampleTimeoutMs = 300000,
[switch]$AllowSafeRenderPackFallback,
[switch]$Uncapped,
[switch]$SkipBuild
)
@ -129,6 +162,11 @@ $ErrorActionPreference = 'Stop'
$repo = Split-Path -Parent $PSScriptRoot
$exe = Join-Path $repo 'src\AcDream.App\bin\Release\net10.0\AcDream.App.exe'
$cli = Join-Path $repo 'src\AcDream.Cli\bin\Release\net10.0\AcDream.Cli.dll'
. (Join-Path $PSScriptRoot 'atmospheric-performance-matrix-common.ps1')
if ($AllowSafeRenderPackFallback -and $RenderPackPreset -eq 'retail') {
throw '-AllowSafeRenderPackFallback is valid only for an explicitly selected enhanced preset.'
}
function Write-Step($message) { Write-Host "[pixel-gate] $message" }
@ -144,27 +182,114 @@ if (-not (Test-Path $exe)) { throw "Client not found at $exe. Build Release firs
if (Test-Path $Out) { Remove-Item -Recurse -Force $Out }
New-Item -ItemType Directory -Force -Path $Out | Out-Null
# Every capture owns a complete disposable path set. This prevents an offline
# gate from inheriting or rewriting the user's real pack selection, settings,
# plugins, screenshots, or pipeline cache.
$state = Join-Path $Out 'isolated-state'
$config = Join-Path $state 'config'
$data = Join-Path $state 'data'
$cache = Join-Path $state 'cache'
New-Item -ItemType Directory -Force -Path $config, $data, $cache | Out-Null
$packId = if ($RenderPackPreset -eq 'retail') { 'retail' } else { 'acdream.atmospheric' }
$packVersion = if ($RenderPackPreset -eq 'retail') { $null } else { '1.0.0' }
$presetId = if ($RenderPackPreset -eq 'retail') { 'off' } else { $RenderPackPreset }
$orderedOverrides = [ordered]@{}
foreach ($key in @($RenderPackSettingOverrides.Keys | Sort-Object)) {
$orderedOverrides[$key] = [string]$RenderPackSettingOverrides[$key]
}
$settings = [ordered]@{
display = [ordered]@{
resolution = $Resolution
fullscreen = $false
vsync = $false
renderPack = [ordered]@{
packId = $packId
packVersion = $packVersion
presetId = $presetId
settingOverrides = $orderedOverrides
}
}
version = 3
}
$settings | ConvertTo-Json -Depth 8 | Set-Content -Encoding utf8 `
-LiteralPath (Join-Path $config 'settings.json')
$probe = Join-Path $Out 'offline.probe.txt'
# The script runner reads one command per line. A single settled capture is the
# whole gate: a second stop would need camera movement, which offline has no
# deterministic way to drive.
Set-Content -Encoding utf8 -Path $probe -Value @"
sleep $WarmupMs
screenshot world-offline 30000
sleep 500
"@
$probeCommands = [Collections.Generic.List[string]]::new()
$probeCommands.Add("sleep $WarmupMs")
if ($RequiredRenderPackSamples -gt 0) {
# Explicit presets discard startup evidence after warmup. Auto deliberately
# owns a continuous rolling window for its hysteresis policy and rejects a
# diagnostic reset; after the same warmup we wait until its current stable
# resource generation contains one complete window.
if ($RenderPackPreset -ne 'auto') {
$probeCommands.Add('renderpack reset-performance')
}
$probeCommands.Add(
"wait render-pack-samples $RequiredRenderPackSamples $RenderPackSampleTimeoutMs")
if ($AllowSafeRenderPackFallback) {
# The screenshot controller reads the last completed swapchain image.
# A runtime Auto fallback can publish retail at the boundary that
# satisfies the wait while that completed image still belongs to the
# prior enhanced frame. Give the default path time to present fresh
# frames before pairing its pixels with the retail oracle.
$probeCommands.Add('sleep 2000')
}
}
$probeCommands.Add('screenshot world-offline 30000')
# Keep the process alive briefly after the PNG commit so this parent can record
# the matching process envelope, then ask the hidden client itself to execute
# the ordinary IWindow.Close shutdown path. Hidden windows intentionally have
# no MainWindowHandle, so WM_CLOSE cannot be the primary close mechanism.
$probeCommands.Add('sleep 4000')
$probeCommands.Add('close-client')
Set-Content -Encoding utf8 -Path $probe -Value $probeCommands
$log = Join-Path $Out 'client.log'
# --- 3. Launch offline --------------------------------------------------------
$previousLive = $env:ACDREAM_LIVE
$previousConfigDirectory = $env:ACDREAM_CONFIG_DIR
$previousDataDirectory = $env:ACDREAM_DATA_DIR
$previousCacheDirectory = $env:ACDREAM_CACHE_DIR
$previousOrbitDistance = $env:ACDREAM_ORBIT_DISTANCE_METERS
$previousOrbitYaw = $env:ACDREAM_ORBIT_YAW_DEGREES
$previousOrbitPitch = $env:ACDREAM_ORBIT_PITCH_DEGREES
$previousUncappedRender = $env:ACDREAM_UNCAPPED_RENDER
$previousExactFramebuffer = $env:ACDREAM_AUTOMATION_EXACT_FRAMEBUFFER
Remove-Item Env:\ACDREAM_LIVE -ErrorAction SilentlyContinue
$env:ACDREAM_DAT_DIR = Join-Path $env:USERPROFILE "Documents\Asheron's Call"
$env:ACDREAM_CONFIG_DIR = $config
$env:ACDREAM_DATA_DIR = $data
$env:ACDREAM_CACHE_DIR = $cache
$env:ACDREAM_NO_AUDIO = '1'
$env:ACDREAM_RETAIL_UI = '1'
$env:ACDREAM_DAY_GROUP = "$DayGroup"
$env:ACDREAM_UI_PROBE_SCRIPT = $probe
$env:ACDREAM_AUTOMATION_ARTIFACT_DIR = $Out
$env:ACDREAM_AUTOMATION_EXACT_FRAMEBUFFER = '1'
$env:ACDREAM_UNCAPPED_RENDER = if ($Uncapped) { '1' } else { $null }
if ($OrbitDistanceMeters -gt 0) {
$env:ACDREAM_ORBIT_DISTANCE_METERS = $OrbitDistanceMeters.ToString(
[System.Globalization.CultureInfo]::InvariantCulture)
} else {
Remove-Item Env:\ACDREAM_ORBIT_DISTANCE_METERS -ErrorAction SilentlyContinue
}
if ($null -ne $OrbitYawDegrees) {
$env:ACDREAM_ORBIT_YAW_DEGREES = ([double]$OrbitYawDegrees).ToString(
[System.Globalization.CultureInfo]::InvariantCulture)
} else {
Remove-Item Env:\ACDREAM_ORBIT_YAW_DEGREES -ErrorAction SilentlyContinue
}
if ($null -ne $OrbitPitchDegrees) {
$env:ACDREAM_ORBIT_PITCH_DEGREES = ([double]$OrbitPitchDegrees).ToString(
[System.Globalization.CultureInfo]::InvariantCulture)
} else {
Remove-Item Env:\ACDREAM_ORBIT_PITCH_DEGREES -ErrorAction SilentlyContinue
}
# The determinism pins, forced rather than inherited. See .DESCRIPTION.
$invariant = [System.Globalization.CultureInfo]::InvariantCulture
@ -173,13 +298,17 @@ $env:ACDREAM_SKY_PHASE_SECONDS = $SkyPhaseSeconds.ToString($invariant)
if ($MsaaSamples -ge 0) { $env:ACDREAM_MSAA_SAMPLES = "$MsaaSamples" }
else { Remove-Item Env:\ACDREAM_MSAA_SAMPLES -ErrorAction SilentlyContinue }
Write-Step "launching offline client (warmup ${WarmupMs}ms, day group $DayGroup, day fraction $WorldDayFraction, sky phase $SkyPhaseSeconds, MSAA $MsaaSamples)"
Write-Step "launching offline client (pack $packId/$presetId, $Resolution, $(if ($Uncapped) { 'uncapped' } else { 'capped' }), orbit ${OrbitDistanceMeters}m/$OrbitYawDegrees deg yaw/$OrbitPitchDegrees deg pitch, warmup ${WarmupMs}ms, day group $DayGroup, day fraction $WorldDayFraction, sky phase $SkyPhaseSeconds, MSAA $MsaaSamples)"
$proc = Start-Process -FilePath $exe -RedirectStandardOutput $log `
-RedirectStandardError "$log.err" -PassThru -WindowStyle Minimized
-RedirectStandardError "$log.err" -PassThru -WindowStyle Hidden
try {
$shots = Join-Path $Out 'screenshots'
$deadline = (Get-Date).AddMilliseconds($WarmupMs + 60000)
$sampleWaitMs = if ($RequiredRenderPackSamples -gt 0) {
$RenderPackSampleTimeoutMs
} else { 0 }
$deadline = (Get-Date).AddMilliseconds(
$WarmupMs + $sampleWaitMs + 60000)
$captured = $false
while ((Get-Date) -lt $deadline) {
if ((Test-Path $shots) -and (Get-ChildItem $shots -Filter *.png -ErrorAction SilentlyContinue)) {
@ -196,27 +325,111 @@ try {
}
# Let the probe script finish its trailing sleep so the PNG is fully flushed.
Start-Sleep -Milliseconds 1500
$proc.Refresh()
[pscustomobject][ordered]@{
SchemaVersion = 1
CapturedUtc = [DateTime]::UtcNow.ToString('O')
WorkingSetBytes = [long]$proc.WorkingSet64
PrivateMemoryBytes = [long]$proc.PrivateMemorySize64
HandleCount = [int]$proc.HandleCount
ThreadCount = [int]$proc.Threads.Count
} | ConvertTo-Json -Depth 3 | Set-Content -Encoding utf8 `
-LiteralPath (Join-Path $Out 'capture-process.json')
}
finally {
# Graceful close: WM_CLOSE runs the shutdown path, so the ownership ledger
# converges the way the lifecycle tests expect. No ACE session exists here,
# but keeping the habit means this script is safe to point at a live run too.
$app = Get-Process -Name AcDream.App -ErrorAction SilentlyContinue
if ($app) {
$app.CloseMainWindow() | Out-Null
if (-not $app.WaitForExit(10000)) {
Write-Step 'WM_CLOSE timed out; forcing'
$app | Stop-Process -Force
}
# The probe's close-client verb runs acdream's normal IWindow.Close path.
# Wait for that exact process; never target another AcDream.App instance.
# Force is cleanup-only and makes the gate fail rather than disguising a
# broken ownership/shutdown path as a successful capture.
$shutdownFailure = $null
$proc.Refresh()
if (-not $proc.HasExited -and -not $proc.WaitForExit(15000)) {
$shutdownFailure = 'in-process automation close timed out'
Write-Step "$shutdownFailure; forcing exact capture process"
Stop-Process -Id $proc.Id -Force
$proc.WaitForExit()
}
if ($null -eq $shutdownFailure -and $proc.ExitCode -ne 0) {
$shutdownFailure = "client exited with code $($proc.ExitCode)"
}
Remove-Item Env:\ACDREAM_MSAA_SAMPLES -ErrorAction SilentlyContinue
Remove-Item Env:\ACDREAM_WORLD_TIME -ErrorAction SilentlyContinue
Remove-Item Env:\ACDREAM_SKY_PHASE_SECONDS -ErrorAction SilentlyContinue
if ($null -eq $previousOrbitDistance) {
Remove-Item Env:\ACDREAM_ORBIT_DISTANCE_METERS -ErrorAction SilentlyContinue
} else { $env:ACDREAM_ORBIT_DISTANCE_METERS = $previousOrbitDistance }
if ($null -eq $previousOrbitYaw) {
Remove-Item Env:\ACDREAM_ORBIT_YAW_DEGREES -ErrorAction SilentlyContinue
} else { $env:ACDREAM_ORBIT_YAW_DEGREES = $previousOrbitYaw }
if ($null -eq $previousOrbitPitch) {
Remove-Item Env:\ACDREAM_ORBIT_PITCH_DEGREES -ErrorAction SilentlyContinue
} else { $env:ACDREAM_ORBIT_PITCH_DEGREES = $previousOrbitPitch }
if ($null -eq $previousUncappedRender) {
Remove-Item Env:\ACDREAM_UNCAPPED_RENDER -ErrorAction SilentlyContinue
} else { $env:ACDREAM_UNCAPPED_RENDER = $previousUncappedRender }
if ($null -eq $previousExactFramebuffer) {
Remove-Item Env:\ACDREAM_AUTOMATION_EXACT_FRAMEBUFFER -ErrorAction SilentlyContinue
} else {
$env:ACDREAM_AUTOMATION_EXACT_FRAMEBUFFER = $previousExactFramebuffer
}
if ($previousLive) { $env:ACDREAM_LIVE = $previousLive }
if ($null -eq $previousConfigDirectory) {
Remove-Item Env:\ACDREAM_CONFIG_DIR -ErrorAction SilentlyContinue
} else { $env:ACDREAM_CONFIG_DIR = $previousConfigDirectory }
if ($null -eq $previousDataDirectory) {
Remove-Item Env:\ACDREAM_DATA_DIR -ErrorAction SilentlyContinue
} else { $env:ACDREAM_DATA_DIR = $previousDataDirectory }
if ($null -eq $previousCacheDirectory) {
Remove-Item Env:\ACDREAM_CACHE_DIR -ErrorAction SilentlyContinue
} else { $env:ACDREAM_CACHE_DIR = $previousCacheDirectory }
if ($null -ne $shutdownFailure) {
throw $shutdownFailure
}
}
$captures = Get-ChildItem (Join-Path $Out 'screenshots') -Filter *.png
$captures = @(Get-ChildItem (Join-Path $Out 'screenshots') -Filter *.png)
Write-Step "captured $($captures.Count) screenshot(s) into $Out"
$metadataPath = Join-Path $Out 'screenshots\world-offline.metadata.json'
if (-not (Test-Path -LiteralPath $metadataPath)) {
throw "Render-pack screenshot metadata is missing at '$metadataPath'."
}
$metadata = Get-Content -Raw -LiteralPath $metadataPath | ConvertFrom-Json
if ($AllowSafeRenderPackFallback) {
$dimensions = $Resolution.Split('x')
$evidence = Test-AtmosphericPerformanceMetadataEvidence `
-MetadataPath $metadataPath `
-Preset $RenderPackPreset `
-ExpectedWidth ([int]$dimensions[0]) `
-ExpectedHeight ([int]$dimensions[1]) `
-AllowSafeFallback
if (-not $evidence.Passed) {
throw ('Capture is neither an active complete evidence window nor a safe ' +
'resource/capability fallback: ' + (@($evidence.Failures) -join '; '))
}
Write-Step ("render-pack capture outcome: {0}{1}" -f `
$evidence.Outcome,
$(if ($evidence.Outcome -eq 'Unavailable') {
" ($($evidence.UnavailableClassification): $($evidence.FailureReason))"
} else { '' }))
}
else {
if (($metadata.RenderPack.PackId -ne $packId) -or
($metadata.RenderPack.PresetId -ne $presetId)) {
throw ("Capture selected {0}/{1}, expected {2}/{3}. Failure: {4}" -f `
$metadata.RenderPack.PackId,
$metadata.RenderPack.PresetId,
$packId,
$presetId,
$metadata.RenderPack.FailureReason)
}
$expectedState = if ($RenderPackPreset -eq 'retail') { 0 } else { 2 }
if ([int]$metadata.RenderPack.State -ne $expectedState) {
throw ("Capture render-pack state was {0}, expected {1}. Failure: {2}" -f `
$metadata.RenderPack.State,
$expectedState,
$metadata.RenderPack.FailureReason)
}
}
# The offline window is minimised but still focusable, so a stray scroll or key
# press from whoever is at the keyboard can move the camera mid-capture. That