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
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue