diff --git a/.github/workflows/headless-portability.yml b/.github/workflows/headless-portability.yml index 828c17f1..6cea0898 100644 --- a/.github/workflows/headless-portability.yml +++ b/.github/workflows/headless-portability.yml @@ -13,7 +13,6 @@ on: - "src/AcDream.Headless/**" - "src/AcDream.App/**" - "src/AcDream.UI.Abstractions/**" - - "src/AcDream.UI.ImGui/**" - "tests/AcDream.Core.Tests/**" - "tests/AcDream.Core.Net.Tests/**" - "tests/AcDream.Content.Tests/**" @@ -35,7 +34,6 @@ on: - "src/AcDream.Headless/**" - "src/AcDream.App/**" - "src/AcDream.UI.Abstractions/**" - - "src/AcDream.UI.ImGui/**" - "tests/AcDream.Core.Tests/**" - "tests/AcDream.Core.Net.Tests/**" - "tests/AcDream.Content.Tests/**" @@ -153,7 +151,7 @@ jobs: dotnet test ` tests/AcDream.App.Tests/AcDream.App.Tests.csproj ` -c Release ` - --filter "FullyQualifiedName~LinuxMonotonicFramePacingWaiterTests|FullyQualifiedName~LinuxPlatformBoundaryTests|FullyQualifiedName~GraphicalHostPlatformServicesTests|FullyQualifiedName~GraphicalLegacyConfigurationMigratorTests|FullyQualifiedName~GraphicalWindowBackendSelectionTests|FullyQualifiedName~GraphicalCapabilityRequirementsTests|FullyQualifiedName~StudioWindowTests" + --filter "FullyQualifiedName~LinuxMonotonicFramePacingWaiterTests|FullyQualifiedName~LinuxPlatformBoundaryTests|FullyQualifiedName~GraphicalHostPlatformServicesTests|FullyQualifiedName~GraphicalLegacyConfigurationMigratorTests|FullyQualifiedName~GraphicalWindowBackendSelectionTests" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Verify Linux package contract @@ -165,7 +163,6 @@ jobs: test -f "$root/AcDream.App.dll" test -f "$root/libglfw.so.3" test -f "$root/libopenal.so" - test -f "$root/libcimgui.so" test -f "$root/Rendering/Shaders/mesh_modern.vert" test -f "$root/plugins/AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.dll" test -f "$root/plugins/AcDream.Plugins.Smoke/plugin.json" @@ -183,45 +180,26 @@ jobs: --exclude='FramePacingWaiterFactory.cs' \ --exclude='WindowsHighResolutionFramePacingWaiter.cs' - - name: Verify actionable unsupported-driver gate - shell: bash - run: | - set -euo pipefail - root=artifacts/acdream-linux-x64 - report=artifacts/linux-x11-capabilities.json - set +e - ACDREAM_DISPLAY_PROTOCOL=x11 \ - xvfb-run -a "$root/AcDream.App" \ - ui-studio /tmp/not-needed \ - --mockup \ - --capability-report "$report" - code=$? - set -e - - test "$code" -eq 4 - test "$(jq -r '.ActiveDisplayProtocol' "$report")" = X11 - test "$(jq -r '.Lifecycle.ShutdownComplete' "$report")" = true - test "$(jq -r '.Lifecycle.OwnedWindowCount' "$report")" -eq 0 - test "$(jq -r '.Lifecycle.OwnedGlApiCount' "$report")" -eq 0 - test "$(jq -r '.Lifecycle.OwnedInputContextCount' "$report")" -eq 0 - jq -e '.SupportFailures | length > 0' "$report" - - # Campaign V slice V9. The GL job above proves the unsupported-driver gate - # fires on Mesa's software OpenGL; this one proves the Vulkan backend does the - # opposite on Mesa's software Vulkan. lavapipe passes the capability gate that - # llvmpipe-GL cannot, because mandatory GL_ARB_bindless_texture has no llvmpipe - # implementation while every Vulkan feature acdream requires is core 1.3 or a - # descriptor-indexing feature lavapipe implements. That makes this the first CI - # job in the project's history that renders a frame. + # Campaign V slice V9 built this job to prove lavapipe (Mesa's software + # Vulkan) passes the capability gate: every Vulkan feature acdream requires + # is core 1.3 or a descriptor-indexing feature lavapipe implements. That made + # it the first CI job in the project's history to render a frame. # - # NOT DONE HERE, deliberately: a GL-versus-Vulkan pixel comparison. Two - # independent reasons, either of which alone is disqualifying. First, the GL - # job never produces a frame at all: it asserts exit code 4, so there is no - # left-hand side. Second, even if llvmpipe-GL could run, the probe harness - # renders synthetic verification scenes rather than the world, and the world - # needs retail DATs that CI does not have and cannot be given. The real - # GL-versus-Vulkan differential is V7's, on the developer machine, against the - # DATs, with both clocks pinned. See plan section 5.5.20. + # Until Campaign V slice V11, linux-graphical (above) carried the mirror + # case: llvmpipe (Mesa's software OpenGL) FAILING the capability gate, + # because mandatory GL_ARB_bindless_texture has no llvmpipe implementation. + # V11 deleted the GL backend entirely, so there is no more GL capability + # gate for any driver to pass or fail — that job's "Verify actionable + # unsupported-driver gate" step went with it. "Verify the forced-unsupported + # gate exits 4" below is what now proves the exit-code-4 contract still + # fires, forcing an unsupported VULKAN feature instead. + # + # NOT DONE HERE, deliberately: a GL-versus-Vulkan pixel comparison. It was + # never viable in CI even before V11 — the probe harness renders synthetic + # verification scenes rather than the world, and the world needs retail DATs + # that CI does not have and cannot be given. The real GL-versus-Vulkan + # differential was V7's, on the developer machine, against the DATs, with + # both clocks pinned, before V11 deleted the GL arm it depended on. linux-vulkan: runs-on: ubuntu-latest diff --git a/tools/connected-backend-differential.route.txt b/tools/connected-backend-differential.route.txt deleted file mode 100644 index 995decbe..00000000 --- a/tools/connected-backend-differential.route.txt +++ /dev/null @@ -1,77 +0,0 @@ -# Campaign V slice V7: the connected route the GL-versus-Vulkan differential -# runs TWICE, once per backend, pairing the PNGs by name. -# -# Two separate launches have to agree pixel-for-pixel, which is a much stricter -# requirement than either connected route was written for. Everything below is -# in service of that: -# -# * Every teleloc carries the identity quaternion, so the heading at each stop -# is the same on both runs rather than whatever the previous heading was. -# * The world clock is pinned by ACDREAM_WORLD_TIME, which the gate script -# forces on both launches. It used to be pinned here instead, by pressing -# AcdreamCycleTimeOfDay three times (live -> 0.00 -> 0.25 -> 0.50), and -# THAT DID NOT WORK: SetDebugTime is transient by design -- it is the /time -# slash command's mechanism, and WorldTimeService.SyncFromServer clears it -# -- so ACE's next TimeSync un-pinned the clock seconds later, long before -# the first stop. V7 measured the consequence directly: two captures 45 s -# apart at ONE stop on ONE backend differ in 22.3% of the frame outdoors and -# 11.8% indoors, because the sun keeps moving. The presses are gone. -# * Nothing moves the character. The only thing between arrival and capture is -# a settle window, because a timed turn cannot be relied on to stop at the -# same angle twice. -# * There is no `checkpoint` verb here. The differential's verdict is the -# paired PNG, and the ownership ledger has its own gate -# (run-connected-world-lifecycle-gate.ps1) that this route does not duplicate. -# -# MSAA is forced OFF by the gate script rather than here, on BOTH launches. -# Sample positions are not specified across implementations and cost 8.8% of the -# frame at 4x -- measured, plan section 5.5.16 -- so a differential run with MSAA -# on measures the two drivers' sample patterns and nothing else. -# -# Residual divergence this route cannot remove, and which is a finding rather -# than a failure: PHASE. Two launches cannot agree on an emitter's age, a -# creature's animation cursor, a portal's scrolling texture, or the cloud -# sheet's position -- the cloud sheet in particular advances with WALL time and -# not with the Dereth clock the override above pins, which is why the offline -# pixel gate masks its own top 280 rows. The V6m smoke run measured the shape -# of it at stop 1 with MSAA off: 18.52% of the frame differed, 89% of that in -# the top 240 rows (cloud sheet, treeline, the portal), and rows 300-720 -- -# terrain, blending, roads, buildings, character, entire retained UI -- -# contributed about 9,500 pixels between them, almost all of it thin outlines -# on silhouette edges. That is why every stop below is chosen for its static -# content and why the settle windows are long. - -wait world-ready 90000 -wait world-visible 30000 -sleep 8000 - -# 1. Holtburg: the dense outdoor town. Terrain, terrain blending, the road -# overlay, static world meshes, scenery and the whole retained UI in one -# frame. This is the SMOKE stop -- the one pair -Smoke compares on its own. -command /teleloc 0xA9B40019 84.0 7.1 94.005 1 0 0 0 -wait materialized 1 90000 -wait world-visible 30000 -sleep 25000 -screenshot holtburg_town 15000 - -# 2. Facility Hub: an interior EnvCell, and the durable fix for the campaign's -# oldest coverage gap. Plan section 5.1 records that the offline gate sees no -# EnvCell at all and that EnvCellRenderer's Vulkan arm is proven by one -# eyes-on frame; this stop is the first automated instrument that looks at -# one. Cell 0x164 is >= 0x100, which is what makes it indoor: walls, floor, -# per-cell lighting and the doorway clip region rather than terrain. -command /teleloc 0x8A020164 70.35 -40.66 -5.9 1 0 0 0 -wait materialized 2 90000 -wait world-visible 30000 -sleep 25000 -screenshot facility_hub_interior 15000 - -# 3. Aerlinthe: dense island geometry, the water edge and a long view. The -# stop that exercises fog and the far end of the depth range, where a -# precision divergence between the two backends would show first. -command /teleloc 0x09040008 11.4 188.6 87.705 1 0 0 0 -wait materialized 3 90000 -wait world-visible 30000 -sleep 25000 -screenshot aerlinthe_island 15000 -sleep 3000 diff --git a/tools/run-backend-differential-gate.ps1 b/tools/run-backend-differential-gate.ps1 deleted file mode 100644 index 0b46cb7b..00000000 --- a/tools/run-backend-differential-gate.ps1 +++ /dev/null @@ -1,421 +0,0 @@ -<# -.SYNOPSIS - Campaign V slice V7 instrument: the GL-versus-Vulkan connected differential. - -.DESCRIPTION - Runs the SAME connected route twice against the local ACE server -- once with - ACDREAM_RENDER_BACKEND=gl, once with =vulkan -- pairs the screenshots by name, - compares each pair with the `compare-screenshots` CLI at the plan's pinned - tolerance, and prints one verdict table. - - This is the instrument for plan section 2's pixel-acceptance contract: - - compare-screenshots expected.png actual.png out.json - channel tolerance 2, maximum differing fraction 0.001, - MSAA off, ACDREAM_DAY_GROUP pinned, - at every deterministic checkpoint of the connected route. - - FOUR THINGS ARE FORCED, and each is load-bearing. - - 1. ACDREAM_MSAA_SAMPLES=0 ON BOTH LAUNCHES. Multisample resolve positions - are explicitly unspecified across implementations. Plan section 5.5.16 - MEASURED what leaving them on costs: 8.83% of the frame differed at 4x, - 81,359 pixels of 921,600, essentially all of it hugging foliage and - silhouette edges. That is two drivers' sample patterns, not a renderer - divergence, and it swamps the 0.001 threshold by ninety-fold. Forcing it - off on both runs is what makes the remaining difference attributable. - Note that it must be forced on BOTH -- matching a GL run at 4x against a - Vulkan run at 0 would be worse than either. - - 2. ACDREAM_DAY_GROUP and ACDREAM_WORLD_TIME pinned. The Dereth clock - advances with wall time, and it does not only move the sky: it moves the - SUN, so every lit surface in the scene drifts with it. The route used to - press AcdreamCycleTimeOfDay three times instead, and that was measured - NOT to hold -- SetDebugTime is transient by design and ACE's next - TimeSync clears it within seconds. What that cost, measured at V7 on two - captures 45 s apart at ONE stop on ONE backend: 22.3% of the frame at - Holtburg and 11.8% inside the Facility Hub. A cross-backend number taken - against that noise floor means nothing, which is why the day fraction is - now pinned by env var and the route no longer presses anything. - - 3. ACDREAM_SKY_PHASE_SECONDS pinned ON BOTH LAUNCHES. The day group and the - time-of-day override pin ONE of the sky's two clocks -- the one that - chooses the keyframe and the sun angle. The cloud sheet runs on the - other: TexVelocityX/Y accumulate against real elapsed time by design, - because retail's clouds drift independently of the date, so no amount of - world-clock pinning makes two launches agree about where they are. The - V6m smoke pair measured the cost of leaving it: 89% of an 18.52% - whole-frame difference lived in the top 240 rows. Pinning the phase is - instrument determinism -- it is off in every ordinary run, it changes one - input to a UV offset, and it is what lets this gate keep the SKY under - strict comparison instead of masking it, which is what -MaskTopPixels - would have cost. - - 4. A DESKTOP WITNESS per run, with the guards the repeat-run gate - (run-repeat-connected-gate.ps1) learned the hard way. The client's own - capture is the differential's subject, but a blank frame reads back as a - valid PNG (plan section 5.5.2), so a second instrument that shares - nothing with the renderer decides whether the run is worth comparing at - all. Its failure modes are the same three that gate documents: - - * an overlapping window or a locked screen -- CopyFromScreen grabs - whatever is composited at those coordinates, so run this with - nothing else on the primary monitor and the desktop unlocked; - * stray input -- a foregrounded window takes keystrokes and scrolls, - which moves the camera off the pinned stop and produces an enormous - spurious difference. The client log is scanned for camera-affecting - input afterwards and the run is aborted if any arrived; - * a hard kill -- ACE holds the session about three minutes and the - SECOND launch of this gate is the one that pays for it, so both runs - close through WM_CLOSE. - -.PARAMETER Out - Directory for both captures, the comparison reports and the verdict JSON. - -.PARAMETER Route - The route both backends run. Defaults to the differential route, which is - written for two-launch determinism -- see its header before substituting one. - -.PARAMETER Smoke - Compare only the FIRST stop. The route is truncated after its first - `screenshot` verb, so the run is short and the artifact records exactly what - ran. Use this to measure the distance between the backends without paying - for the whole route; the full route is the V7 gate. - -.PARAMETER Tolerance - Per-channel absolute difference allowed. Default 2, the plan's pinned value. - -.PARAMETER MaxDifferentFraction - Fraction of differing pixels allowed. Default 0.001, likewise pinned. - -.PARAMETER DayGroup - Sky day-group index pinned on both launches. Default 0. - -.PARAMETER WorldDayFraction - Dereth day fraction pinned on both launches, in [0, 1). Default 0.5 -- noon, - the brightest and flattest lighting the day has, which is what the route's - three AcdreamCycleTimeOfDay presses were aiming at before it was measured - that they do not hold. See forced item 2 above. - -.PARAMETER SkyPhaseSeconds - Elapsed-seconds value the sky's UV scroll is pinned to on both launches. - Default 0 -- the cloud sheet's authored origin. Any finite value works: the - offset is taken modulo 1 per axis. See forced item 3 above. - -.PARAMETER MaskTopPixels - Height in pixels of a top band excluded from every comparison. DEFAULT 0 -- - nothing is masked, and the gate is a strict identity check on the whole - frame. - - It exists because the V6m smoke run measured what the band costs and the - number belongs where the next reader will find it. With the world clock - pinned by the route and MSAA off, a GL-versus-Vulkan pair at Holtburg - differed in 18.52% of the frame -- and 89% of that difference sat in the top - 240 rows, which is the scrolling cloud sheet plus the treeline it sits - behind. The cloud sheet advances with WALL time, not the Dereth clock, so - two launches minutes apart can never agree there; this is the same - population the offline pixel gate masks its top 280 rows for. Rows 300-720 - -- terrain, blending, roads, buildings, the character and the whole retained - UI -- contributed about 9,500 differing pixels between them. - - Whether V7 accepts a mask, pins the cloud phase, or fixes something the - smoke was actually seeing is V7's decision, not this script's, which is why - the default is off. Do NOT reach for this to make a failing gate pass: plan - section 7.1 rule 2. - -.PARAMETER MinRenderedBytes - Desktop-grab PNG size below which a run is called blank and the comparison - is refused. Default 500000, the repeat gate's measured separator. - -.PARAMETER CooldownSeconds - Wait between the two launches so ACE clears the first graceful logout. - Default 15. - -.EXAMPLE - tools/run-backend-differential-gate.ps1 -Out artifacts/v7-differential -Smoke - tools/run-backend-differential-gate.ps1 -Out artifacts/v7-differential -#> -[CmdletBinding()] -param( - [Parameter(Mandatory = $true)][string]$Out, - [string]$Route, - [switch]$Smoke, - [int]$Tolerance = 2, - [double]$MaxDifferentFraction = 0.001, - [int]$DayGroup = 0, - [double]$WorldDayFraction = 0.5, - [double]$SkyPhaseSeconds = 0, - [int]$MaskTopPixels = 0, - [int]$MinRenderedBytes = 500000, - [int]$CooldownSeconds = 15, - [int]$RouteTimeoutSeconds = 600, - [switch]$SkipBuild -) - -$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' -if (-not $Route) { $Route = Join-Path $repo 'tools\connected-backend-differential.route.txt' } - -function Write-Step($message) { Write-Host "[differential] $message" } - -# --- Preconditions ------------------------------------------------------------ -if (Get-Process -Name AcDream.App -ErrorAction SilentlyContinue) { - throw 'AcDream.App is already running. This gate uses the shared test account and must not steal its session.' -} -if (@(Get-NetUDPEndpoint -LocalPort 9000 -ErrorAction SilentlyContinue).Count -eq 0) { - throw 'local ACE is not listening on UDP port 9000.' -} -if (-not (Test-Path $Route)) { throw "Route not found: $Route" } - -if (-not $SkipBuild) { - Write-Step 'building Release' - & dotnet build (Join-Path $repo 'AcDream.slnx') -c Release --nologo -v q | Out-Null - if ($LASTEXITCODE -ne 0) { throw "Release build failed with exit code $LASTEXITCODE." } -} -if (-not (Test-Path $exe)) { throw "Client not found at $exe." } -if (-not (Test-Path $cli)) { throw "AcDream.Cli not found at $cli." } - -if (Test-Path $Out) { Remove-Item -Recurse -Force $Out } -New-Item -ItemType Directory -Force -Path $Out | Out-Null - -# The route both runs execute. -Smoke truncates after the first capture; the -# truncated file is written into the artifact directory so what ran is on record. -$routeLines = Get-Content -LiteralPath $Route -if ($Smoke) { - $firstShot = -1 - for ($i = 0; $i -lt $routeLines.Count; $i++) { - if ($routeLines[$i].TrimStart().StartsWith('screenshot ')) { $firstShot = $i; break } - } - if ($firstShot -lt 0) { throw "The route has no screenshot verb to truncate at: $Route" } - $routeLines = @($routeLines[0..$firstShot]) + @('sleep 3000') - Write-Step "smoke mode: route truncated after its first capture ($($firstShot + 1) lines)" -} -$effectiveRoute = Join-Path $Out 'route.txt' -Set-Content -Encoding utf8 -Path $effectiveRoute -Value $routeLines - -Add-Type -AssemblyName System.Drawing -Add-Type @' -using System; -using System.Runtime.InteropServices; -public static class DifferentialGateSurface { - [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); - [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); - [DllImport("user32.dll")] public static extern bool GetClientRect(IntPtr hWnd, out RECT r); - [DllImport("user32.dll")] public static extern bool ClientToScreen(IntPtr hWnd, ref POINT p); - [StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; } - [StructLayout(LayoutKind.Sequential)] public struct POINT { public int X, Y; } -} -'@ - -function Invoke-BackendRun([string]$Backend) { - $dir = Join-Path $Out $Backend - New-Item -ItemType Directory -Force -Path $dir | Out-Null - $log = Join-Path $dir 'client.log' - $grab = Join-Path $dir 'desktop-grab.png' - - $env:ACDREAM_DAT_DIR = Join-Path $env:USERPROFILE "Documents\Asheron's Call" - $env:ACDREAM_LIVE = '1' - $env:ACDREAM_TEST_HOST = '127.0.0.1' - $env:ACDREAM_TEST_PORT = '9000' - $env:ACDREAM_TEST_USER = 'testaccount' - $env:ACDREAM_TEST_PASS = 'testpassword' - $env:ACDREAM_RETAIL_UI = '1' - $env:ACDREAM_DEVTOOLS = '0' - $env:ACDREAM_DAY_GROUP = "$DayGroup" - # Both launches, always. See the .DESCRIPTION notes 1 and 3. - $env:ACDREAM_MSAA_SAMPLES = '0' - $env:ACDREAM_SKY_PHASE_SECONDS = - $SkyPhaseSeconds.ToString([System.Globalization.CultureInfo]::InvariantCulture) - $env:ACDREAM_WORLD_TIME = - $WorldDayFraction.ToString([System.Globalization.CultureInfo]::InvariantCulture) - $env:ACDREAM_RENDER_BACKEND = $Backend - $env:ACDREAM_UI_PROBE_SCRIPT = $effectiveRoute - $env:ACDREAM_AUTOMATION_ARTIFACT_DIR = $dir - - Write-Step "launching $Backend (MSAA off, day group $DayGroup)" - # Normal window style: the desktop witness needs a composited frame. - $proc = Start-Process -FilePath $exe -WorkingDirectory $repo ` - -RedirectStandardOutput $log -RedirectStandardError "$log.err" -PassThru - - $completed = $false - $deadline = (Get-Date).AddSeconds($RouteTimeoutSeconds) - while ((Get-Date) -lt $deadline) { - if (Select-String -Path $log -SimpleMatch '[UI-PROBE] UI probe script complete' ` - -ErrorAction SilentlyContinue) { $completed = $true; break } - if ($proc.HasExited) { break } - Start-Sleep -Milliseconds 1000 - } - - # The desktop witness, taken while the client is still in its trailing sleep - # and therefore still rendering the last scene of the route. - $grabBytes = 0 - if (-not $proc.HasExited) { - try { - $proc.Refresh() - $h = $proc.MainWindowHandle - if ($h -ne [IntPtr]::Zero) { - [DifferentialGateSurface]::ShowWindow($h, 9) | Out-Null # SW_RESTORE - [DifferentialGateSurface]::SetForegroundWindow($h) | Out-Null - Start-Sleep -Milliseconds 1200 - $r = New-Object DifferentialGateSurface+RECT - [DifferentialGateSurface]::GetClientRect($h, [ref]$r) | Out-Null - $p = New-Object DifferentialGateSurface+POINT - [DifferentialGateSurface]::ClientToScreen($h, [ref]$p) | Out-Null - $w = $r.R - $r.L; $ht = $r.B - $r.T - if ($w -gt 0 -and $ht -gt 0) { - $bmp = New-Object System.Drawing.Bitmap $w, $ht - $g = [System.Drawing.Graphics]::FromImage($bmp) - $g.CopyFromScreen($p.X, $p.Y, 0, 0, (New-Object System.Drawing.Size $w, $ht)) - $bmp.Save($grab, [System.Drawing.Imaging.ImageFormat]::Png) - $g.Dispose(); $bmp.Dispose() - $grabBytes = (Get-Item $grab).Length - } - } - } - catch { Write-Step "desktop grab failed on ${Backend}: $_" } - } - - # Graceful close, always: a hard kill leaves ACE holding the session about - # three minutes, and the second launch is the one that would pay for it. - $app = Get-Process -Name AcDream.App -ErrorAction SilentlyContinue - if ($app) { - $app.CloseMainWindow() | Out-Null - if (-not $app.WaitForExit(20000)) { - Write-Step "WM_CLOSE timed out on $Backend; forcing" - $app | Stop-Process -Force - } - } - - Remove-Item Env:\ACDREAM_RENDER_BACKEND -ErrorAction SilentlyContinue - Remove-Item Env:\ACDREAM_MSAA_SAMPLES -ErrorAction SilentlyContinue - Remove-Item Env:\ACDREAM_SKY_PHASE_SECONDS -ErrorAction SilentlyContinue - Remove-Item Env:\ACDREAM_WORLD_TIME -ErrorAction SilentlyContinue - - $strayInput = @(Select-String -Path $log -Pattern 'ScrollUp|ScrollDown|ZoomIn|ZoomOut|CameraZoom' ` - -CaseSensitive -ErrorAction SilentlyContinue) - $witness = if ($grabBytes -ge $MinRenderedBytes) { 'RENDERED' } - elseif ($grabBytes -gt 0) { 'BLANK' } - else { 'NO-CAPTURE' } - - return [pscustomobject][ordered]@{ - Backend = $Backend - RouteCompleted = $completed - GrabBytes = $grabBytes - Witness = $witness - StrayInputLines = @($strayInput | Select-Object -First 5 | ForEach-Object { $_.Line }) - Screenshots = Join-Path $dir 'screenshots' - Log = $log - } -} - -$gl = Invoke-BackendRun 'gl' -Write-Step "cooldown ${CooldownSeconds}s so ACE clears the graceful logout" -Start-Sleep -Seconds $CooldownSeconds -$vk = Invoke-BackendRun 'vulkan' - -# --- Sanity, before any pixel is compared ------------------------------------ -$aborts = @() -foreach ($run in @($gl, $vk)) { - if (-not $run.RouteCompleted) { $aborts += "$($run.Backend): the route did not complete" } - if ($run.Witness -ne 'RENDERED') { - $aborts += "$($run.Backend): desktop witness says $($run.Witness) ($($run.GrabBytes) B) -- locked screen, an overlapping window, or a blank frame" - } - if ($run.StrayInputLines.Count -gt 0) { - $aborts += "$($run.Backend): camera-affecting input reached the window; the capture is not comparable" - } -} - -$pairs = @() -if ($aborts.Count -eq 0) { - $glShots = @(Get-ChildItem $gl.Screenshots -Filter *.png -ErrorAction SilentlyContinue) - if ($glShots.Count -eq 0) { $aborts += 'gl: no screenshots were captured' } - - # The comparer skips any pixel whose mask alpha is non-zero, so the excluded - # band is opaque and everything compared is transparent. Same construction as - # the offline pixel gate's sky mask; off unless -MaskTopPixels was given. - $maskPath = $null - if ($MaskTopPixels -gt 0 -and $glShots.Count -gt 0) { - $probeImage = [System.Drawing.Bitmap]::FromFile($glShots[0].FullName) - $mw = $probeImage.Width; $mh = $probeImage.Height - $probeImage.Dispose() - $mask = New-Object System.Drawing.Bitmap($mw, $mh, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb) - $graphics = [System.Drawing.Graphics]::FromImage($mask) - $graphics.Clear([System.Drawing.Color]::FromArgb(0, 0, 0, 0)) - $opaque = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(255, 255, 0, 255)) - $graphics.FillRectangle($opaque, 0, 0, $mw, [Math]::Min($MaskTopPixels, $mh)) - $graphics.Dispose(); $opaque.Dispose() - $maskPath = Join-Path $Out 'mask.png' - $mask.Save($maskPath, [System.Drawing.Imaging.ImageFormat]::Png) - $mask.Dispose() - Write-Step "masking the top $MaskTopPixels rows of every pair" - } - - foreach ($shot in $glShots) { - $counterpart = Join-Path $vk.Screenshots $shot.Name - if (-not (Test-Path $counterpart)) { - $pairs += [pscustomobject][ordered]@{ - Stop = $shot.BaseName; Verdict = 'UNPAIRED' - DifferentPixels = $null; Fraction = $null; MaxChannelDelta = $null - } - continue - } - $report = Join-Path $Out "compare-$($shot.BaseName).json" - if ($maskPath) { - & dotnet $cli compare-screenshots $shot.FullName $counterpart $report $Tolerance $MaxDifferentFraction $maskPath | Out-Null - } - else { - & dotnet $cli compare-screenshots $shot.FullName $counterpart $report $Tolerance $MaxDifferentFraction | Out-Null - } - $verdict = Get-Content $report -Raw | ConvertFrom-Json - $outcome = if ($verdict.passed) { 'MATCH' } else { 'DIVERGED' } - $pairs += [pscustomobject][ordered]@{ - Stop = $shot.BaseName - Verdict = $outcome - DifferentPixels = $verdict.differentPixels - Fraction = $verdict.differentPixelFraction - MaxChannelDelta = $verdict.maximumChannelDelta - } - } -} - -# --- Verdict ------------------------------------------------------------------ -Write-Host '' -if ($aborts.Count -gt 0) { - Write-Host '[differential] ABORTED before comparison:' -ForegroundColor Red - $aborts | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } -} -else { - $pairs | Format-Table -AutoSize | Out-String | Write-Host -} - -$diverged = @($pairs | Where-Object Verdict -ne 'MATCH') -$report = [pscustomobject][ordered]@{ - Passed = ($aborts.Count -eq 0 -and $diverged.Count -eq 0) - Smoke = [bool]$Smoke - Commit = (& git -C $repo rev-parse HEAD).Trim() - Route = $Route - EffectiveRoute = $effectiveRoute - ChannelTolerance = $Tolerance - MaxDifferentPixelFraction = $MaxDifferentFraction - MsaaSamples = 0 - DayGroup = $DayGroup - WorldDayFraction = $WorldDayFraction - SkyPhaseSeconds = $SkyPhaseSeconds - MaskTopPixels = $MaskTopPixels - Runs = @($gl, $vk) - Pairs = @($pairs) - Aborts = @($aborts) -} -$reportPath = Join-Path $Out 'differential.json' -$report | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $reportPath -Encoding utf8 -Write-Step "report: $reportPath" - -if ($aborts.Count -gt 0) { exit 2 } -if ($diverged.Count -gt 0) { - Write-Host ("[differential] FAILED: {0}/{1} stop(s) diverged." -f $diverged.Count, $pairs.Count) -ForegroundColor Red - exit 1 -} -Write-Step ("PASS: {0}/{0} stop(s) match at tolerance {1} / fraction {2}." -f $pairs.Count, $Tolerance, $MaxDifferentFraction) -exit 0 diff --git a/tools/run-connected-world-lifecycle-gate.ps1 b/tools/run-connected-world-lifecycle-gate.ps1 index 20ce0cdb..1ceb1d09 100644 --- a/tools/run-connected-world-lifecycle-gate.ps1 +++ b/tools/run-connected-world-lifecycle-gate.ps1 @@ -262,12 +262,12 @@ function Invoke-Session( $env:ACDREAM_AUTOMATION_ARTIFACT_DIR = $artifactDir $env:ACDREAM_DUMP_MOVE_TRUTH = $null $env:ACDREAM_WB_DIAG = $null - # Campaign V slice V10: cleared, not set. This route is renderer-agnostic and - # its job after the cutover is to exercise whatever backend the process - # defaults to -- which is Vulkan. An ambient override inherited from the - # caller's shell would quietly turn that into a GL run reported as a default - # one, so the variable is cleared the same way every other unwanted knob here - # is. Use ACDREAM_RENDER_BACKEND on a manual launch to reach the escape hatch. + # Campaign V slice V10 cleared this rather than setting it, because the route + # is renderer-agnostic and its job after the cutover was to exercise whatever + # backend the process defaults to -- which was Vulkan even before Campaign V + # slice V11 deleted the GL arm this variable used to select. The clear stays + # harmless now that nothing reads it: cleared the same way every other + # unwanted knob here is, for a variable that is never set by this gate again. $env:ACDREAM_RENDER_BACKEND = $null $env:ACDREAM_COLLISION_SHADOW_EVERY = if ($CollisionShadowEvery -gt 0) { "$CollisionShadowEvery" } else { $null } diff --git a/tools/run-offline-pixel-gate.ps1 b/tools/run-offline-pixel-gate.ps1 index f95ef850..30ac9a66 100644 --- a/tools/run-offline-pixel-gate.ps1 +++ b/tools/run-offline-pixel-gate.ps1 @@ -16,13 +16,10 @@ those still need the connected route, so slices touching them (V4e, V4g) must additionally be gated by the user. - Determinism levers, all four forced here rather than left to the ambient + Determinism levers, all three forced here rather than left to the ambient environment, because a capture that inherits any of them is not comparable against one that did not: - * ACDREAM_RENDER_BACKEND -- see -Backend. Since slice V10 flipped the - process default, "whatever the environment says" is no longer a stable - description of what got captured. * ACDREAM_DAY_GROUP -- the sky day group, so weather and the keyframe do not drift between runs. * ACDREAM_WORLD_TIME -- the Dereth day fraction. This moves the SUN, so @@ -32,10 +29,10 @@ * ACDREAM_SKY_PHASE_SECONDS -- the cloud sheet's UV scroll, which runs on wall time and is independent of the world clock by design. - ACDREAM_MSAA_SAMPLES is forced to 0 for the same reason the backend - differential forces it: multisample resolve positions are explicitly - unspecified across implementations, and at 4x they swamp the threshold - ninety-fold with two drivers' sample patterns. See plan section 5.5.16. + ACDREAM_MSAA_SAMPLES is forced to 0 because multisample resolve positions + are explicitly unspecified across implementations, and at 4x they swamp + the threshold ninety-fold with two drivers' sample patterns. See plan + section 5.5.16. A fixed warm-up lets streaming settle before the frame is captured. @@ -46,15 +43,6 @@ Optional. A directory from a previous capture. When supplied, every PNG is compared against its namesake and the script fails on any mismatch. -.PARAMETER Backend - Which backend to capture with. DEFAULT 'vulkan', following slice V10's - cutover: the gate must capture what the client actually ships. - - Pass 'gl' to capture through the escape hatch -- which is also how this - script produces the GL-era left-hand side of a cross-backend comparison, and - how the escape hatch itself gets verified. Pass '' to let the process default - decide, which is only useful for proving what that default is. - .PARAMETER WarmupMs Milliseconds to let the world stream and settle before capturing. Default 12000, which reliably reaches a fully populated view on the reference machine. @@ -71,8 +59,7 @@ .PARAMETER WorldDayFraction Dereth day fraction pinned on the launch, in [0, 1). Default 0.5 -- noon, the - brightest and flattest lighting the day has, and the value the backend - differential pins. + brightest and flattest lighting the day has. .PARAMETER SkyPhaseSeconds Elapsed-seconds value the sky's UV scroll is pinned to. Default 0, the cloud @@ -122,17 +109,11 @@ tools/run-offline-pixel-gate.ps1 -Out artifacts/gate-base git stash pop tools/run-offline-pixel-gate.ps1 -Out artifacts/gate-head -Baseline artifacts/gate-base - -.EXAMPLE - # Cross-backend: the GL-era capture, then Vulkan against it. - tools/run-offline-pixel-gate.ps1 -Out artifacts/v10-gl -Backend gl - tools/run-offline-pixel-gate.ps1 -Out artifacts/v10-vk -Baseline artifacts/v10-gl #> [CmdletBinding()] param( [Parameter(Mandatory = $true)][string]$Out, [string]$Baseline, - [string]$Backend = 'vulkan', [int]$WarmupMs = 12000, [int]$DayGroup = 0, [double]$WorldDayFraction = 0.5, @@ -191,11 +172,8 @@ $env:ACDREAM_WORLD_TIME = $WorldDayFraction.ToString($invariant) $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 } -if ($Backend) { $env:ACDREAM_RENDER_BACKEND = $Backend } -else { Remove-Item Env:\ACDREAM_RENDER_BACKEND -ErrorAction SilentlyContinue } -$backendLabel = if ($Backend) { $Backend } else { 'process default' } -Write-Step "launching offline client (backend $backendLabel, warmup ${WarmupMs}ms, day group $DayGroup, day fraction $WorldDayFraction, sky phase $SkyPhaseSeconds, MSAA $MsaaSamples)" +Write-Step "launching offline client (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 @@ -231,7 +209,6 @@ finally { $app | Stop-Process -Force } } - Remove-Item Env:\ACDREAM_RENDER_BACKEND -ErrorAction SilentlyContinue 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 diff --git a/tools/run-offline-vulkan-capture.ps1 b/tools/run-offline-vulkan-capture.ps1 index 4702a388..da5850db 100644 --- a/tools/run-offline-vulkan-capture.ps1 +++ b/tools/run-offline-vulkan-capture.ps1 @@ -3,9 +3,13 @@ Campaign V: one offline Vulkan capture with the validation layer proven loaded. .DESCRIPTION - The same deterministic offline scene the GL pixel gate captures, rendered on - ACDREAM_RENDER_BACKEND=vulkan. VK_LOADER_DEBUG=layer makes the loader print - the layer it inserts, so "validation was on" is evidence rather than a claim. + The same deterministic offline scene run-offline-pixel-gate.ps1 captures. + Before Campaign V slice V10 flipped the process default, this needed + ACDREAM_RENDER_BACKEND=vulkan to reach the Vulkan arm at all; V11 then + deleted the GL arm the variable used to select, so it is now a no-op left + for the record rather than something this script depends on. + VK_LOADER_DEBUG=layer makes the loader print the layer it inserts, so + "validation was on" is evidence rather than a claim. #> [CmdletBinding()] param( diff --git a/tools/run-portal-churn-soak.ps1 b/tools/run-portal-churn-soak.ps1 index d74027a9..4006997d 100644 --- a/tools/run-portal-churn-soak.ps1 +++ b/tools/run-portal-churn-soak.ps1 @@ -1,15 +1,15 @@ <# .SYNOPSIS - Campaign V slice V11 step 0: the #256/#257 discriminator, run on both backends. + Repeated-portal-churn regression soak: memory growth and visual drift. .DESCRIPTION - Issues #256 (server-spawned signs and portals go invisible after repeated - portal runs, while staying interactive) and #257 (working set grows to - ~1.5 GB over the same session) were both observed on the Vulkan backend in - one long live session. Both filings demand the SAME discriminator, and both - demand it BEFORE V11 deletes the OpenGL backend: - - run the same repeated-churn route on GL and on Vulkan. + Originated as Campaign V slice V11 step 0: the #256/#257 discriminator, run + on both the GL and Vulkan backends before V11 deleted the former. Issues + #256 (server-spawned signs and portals go invisible after repeated portal + runs, while staying interactive) and #257 (working set grows to ~1.5 GB + over the same session) were both observed on the Vulkan backend in one long + live session, and running the same repeated-churn route on both arms told + the campaign whether either bug predated Vulkan or was new to it: growth / drift on BOTH arms -> a pre-existing publication or resource lifetime bug that the campaign merely @@ -17,11 +17,17 @@ growth / drift on VULKAN ONLY -> the new arm's resource lifecycle, and deleting its predecessor would destroy the only reference implementation that - can answer the question. + could answer the question. - After V11 the GL arm does not exist and this question costs far more to - answer, which is why this script runs first and why its verdict can stop the - slice. + Campaign V slice V11 deleted the GL backend, so that specific + before-and-after question can no longer be asked of any future bug — but + the soak itself does not depend on having two arms. Comparing a fresh + Holtburg capture against a much-later one from the SAME arm is still the + closest automated instrument to what a user's long session actually shows, + and the memory/entity/GPU trend analysis below never needed a second arm to + begin with. -Backends now defaults to Vulkan alone; pass additional values + only to compare arms that still exist (a future backend, a driver + variant) — 'gl' is no longer a valid launch and the app ignores it. WHAT IT MEASURES, per stop, per cycle: @@ -58,10 +64,10 @@ determinism levers the differential gate forces are forced here for the same reason -- an unpinned sun or cloud sheet would swamp the signal. - NOT A PIXEL GATE. This script never compares the two backends to each other; - that is run-backend-differential-gate.ps1's job and its thresholds are the - campaign's. Here each arm is compared only with itself, and the verdict is - about growth and drift. + NOT A PIXEL GATE. This script never compares two backends to each other — + even when -Backends names more than one arm, each is still compared only + with itself. The verdict is about growth and drift within an arm, not + parity between arms. .PARAMETER Out Directory for both arms' logs, checkpoints, captures, samples and the verdict. @@ -72,8 +78,8 @@ warmup", and three stops per cycle makes 90 transits at the default. .PARAMETER Backends - Which arms to run, in order. Default gl then vulkan, so the arm under - suspicion runs second and cannot be blamed for warming the machine. + Which arms to run, in order. Default 'vulkan' alone — the only backend + that exists since Campaign V slice V11 deleted the GL arm. .PARAMETER ShotCycles Cycles on which Holtburg is captured. Default 1, 10, 20 and the last cycle. @@ -89,7 +95,7 @@ param( [Parameter(Mandatory = $true)][string]$Out, [int]$Cycles = 30, - [string[]]$Backends = @('gl', 'vulkan'), + [string[]]$Backends = @('vulkan'), [int[]]$ShotCycles, [int]$SettleMilliseconds = 6000, [int]$ShotSettleMilliseconds = 12000, @@ -150,7 +156,7 @@ $stops = @( ) $lines = [System.Collections.Generic.List[string]]::new() -$lines.Add('# GENERATED by tools/run-portal-churn-soak.ps1 -- the V11 step-0 discriminator.') +$lines.Add('# GENERATED by tools/run-portal-churn-soak.ps1 -- the portal-churn regression soak.') $lines.Add("# $Cycles cycles x $($stops.Count) stops. Do not hand-edit; regenerate.") $lines.Add('') $lines.Add('wait world-ready 90000') diff --git a/tools/run-repeat-connected-gate.ps1 b/tools/run-repeat-connected-gate.ps1 index 3a25225e..5ead283a 100644 --- a/tools/run-repeat-connected-gate.ps1 +++ b/tools/run-repeat-connected-gate.ps1 @@ -112,11 +112,12 @@ sleep 20000 $env:ACDREAM_RETAIL_UI = '1' $env:ACDREAM_UI_PROBE_SCRIPT = Join-Path $dir 'probe.txt' $env:ACDREAM_AUTOMATION_ARTIFACT_DIR = $dir - # Campaign V slice V10: deliberately cleared rather than set. The blank-world - # defect this gate exists for is a property of whichever backend the client - # actually starts, so the gate must launch the process default -- Vulkan - # after the cutover -- and an ambient override from the caller's shell would - # silently make it a GL run wearing the default's report. + # Campaign V slice V10 deliberately cleared this rather than setting it, + # because the blank-world defect this gate exists for is a property of + # whichever backend the client actually starts, and the gate had to launch + # the process default -- Vulkan after the cutover, and the ONLY backend + # since Campaign V slice V11 deleted the GL arm this variable used to + # select. The clear is now a no-op kept for the record. Remove-Item Env:\ACDREAM_RENDER_BACKEND -ErrorAction SilentlyContinue $log = Join-Path $dir 'client.log'