From 5852bdb8779bf8ecbb9c8e6d09e3b3404932e0f4 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 29 Jul 2026 03:06:45 +0200 Subject: [PATCH] =?UTF-8?q?feat(render):=20Vulkan=20campaign=20V11=20step?= =?UTF-8?q?=204=20=E2=80=94=20retire=20GL=20from=20CI=20and=20gate=20scrip?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes out the GL deletion by fixing the CI workflow and developer gate scripts that still assumed a GL arm existed to compare against, build, or select via ACDREAM_RENDER_BACKEND. .github/workflows/headless-portability.yml: linux-graphical's "Verify actionable unsupported-driver gate" step is deleted outright — it ran the deleted `ui-studio` CLI verb (Studio was removed at Commit 1) to prove the GL capability gate rejects Mesa's llvmpipe driver, and there is no more GL capability gate for any driver to pass or fail. Its test filter dropped two dead entries (GraphicalCapabilityRequirementsTests, deleted at Commit 2; StudioWindowTests, already gone). Its package contract check dropped the libcimgui.so assertion (ImGui's native bridge, deleted at Commit 1). linux-vulkan's explanatory comment, which described GL's rejection as the reason no cross-backend pixel diff runs in CI, is rewritten to explain there is no GL arm left at all. Two dead src/AcDream.UI.ImGui/** path triggers (that project no longer exists) are removed from both the pull_request and push filters. tools/run-backend-differential-gate.ps1 and its dedicated route file tools/connected-backend-differential.route.txt are deleted: the whole script's purpose was comparing a GL launch against a Vulkan launch of the same route, and there is no second arm left to compare. Single-arm regression checking already exists via run-offline-pixel-gate.ps1's -Baseline mechanism. tools/run-portal-churn-soak.ps1 is simplified rather than deleted: its repeated-portal-churn methodology (within-arm capture comparison, memory/entity/GPU trend analysis) has value independent of the GL-versus-Vulkan question it was built to answer for issues #256/#257 before V11. -Backends now defaults to @('vulkan') alone; the doc comments are rewritten from "step 0 discriminator, run before V11" to an ongoing single-arm regression soak. tools/run-offline-pixel-gate.ps1 drops its now-nonfunctional -Backend parameter (ACDREAM_RENDER_BACKEND has read zero call sites since RuntimeOptions.RenderBackend was removed at Commit 2 — passing -Backend gl silently launched Vulkan anyway) along with its GL-escape-hatch example and every comment that referenced the now-deleted differential gate. tools/run-connected-world-lifecycle-gate.ps1, tools/run-offline-vulkan-capture.ps1, and tools/run-repeat-connected-gate.ps1 keep their (harmless, already no-op) ACDREAM_RENDER_BACKEND set/clear lines but have their now-inaccurate "escape hatch" / "GL run" comments corrected to state plainly that the variable is unread and the line is kept only for the historical record. No .cs files touched; `dotnet build AcDream.slnx -c Release` unaffected (0 warnings, 0 errors, matching the prior commit's build). Co-Authored-By: Claude Fable 5 --- .github/workflows/headless-portability.yml | 62 +-- .../connected-backend-differential.route.txt | 77 ---- tools/run-backend-differential-gate.ps1 | 421 ------------------ tools/run-connected-world-lifecycle-gate.ps1 | 12 +- tools/run-offline-pixel-gate.ps1 | 37 +- tools/run-offline-vulkan-capture.ps1 | 10 +- tools/run-portal-churn-soak.ps1 | 46 +- tools/run-repeat-connected-gate.ps1 | 11 +- 8 files changed, 72 insertions(+), 604 deletions(-) delete mode 100644 tools/connected-backend-differential.route.txt delete mode 100644 tools/run-backend-differential-gate.ps1 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'