acdream/tools/run-repeat-connected-gate.ps1
Erik 5852bdb877 feat(render): Vulkan campaign V11 step 4 — retire GL from CI and gate scripts
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 <noreply@anthropic.com>
2026-07-29 03:06:45 +02:00

203 lines
9.2 KiB
PowerShell

<#
.SYNOPSIS
Campaign V repeat-run connected gate: N full connect-render-screenshot cycles
against the local ACE server, with a pass/fail verdict per run.
.DESCRIPTION
The V4c blank-world regression was INTERMITTENT: roughly 1 launch in 3 at the
worst location, 0 in 7 at the parent commit. A single connected capture is
therefore worthless as a gate for this defect class - the broken binary
passes it most of the time. This script exists so no future slice can call
itself connected-clean on one lucky run.
Each run: launch connected -> teleloc to the pinned worst-case cell ->
wait for world-visible -> settle -> screenshot -> desktop grab -> graceful
close -> cooldown. The teleloc is pinned because the failure rate is
location-sensitive.
THE VERDICT IS THE DESKTOP GRAB, not the client's own screenshot. Campaign
plan section 5.5.2 established that on a blank run the client's read of
framebuffer 0 returns RGBA(0,0,0,0) in every pixel INCLUDING pixels where the
UI is demonstrably on screen - so a verdict derived from screenshot bytes is
reporting the readback apparatus, not the renderer. This script therefore
launches the window visible, brings it to the foreground, and grabs the
composited client rectangle with CopyFromScreen: a witness that shares
nothing with the renderer below the compositor. The client's own capture is
still recorded, as a second column, because a disagreement between the two is
itself a finding.
Observed magnitudes at 1280x720: a rendered desktop grab is ~1.6 MB, a blank
one (atmosphere clear plus the complete retained UI, no 3-D) is ~0.23 MB, and
a blank client capture is ~5 KB. One threshold separates all three.
Run this with nothing else on the primary monitor: CopyFromScreen captures
whatever is composited at those coordinates, so an overlapping window would be
captured instead of the client. A visible foregrounded window can also take
stray input, which moves the character off the pinned cell - do not type
while the gate is running.
Graceful close matters: a hard kill leaves ACE holding the session ~3 minutes
and every subsequent run fails with 'CharacterList not received'.
.PARAMETER Runs
Cycles to run. Default 10. The defect's observed rate was ~1/3, so ten clean
runs bound it below ~4% at 90% confidence - adequate for a re-land gate.
.PARAMETER ExePath
Client executable. Defaults to this repo's Release output.
.PARAMETER Teleloc
The pinned location, as the full /teleloc argument string. Default is the
cell with the worst observed failure rate (5/5 blank at the V4c HEAD).
.PARAMETER MinRenderedBytes
PNG size below which an image is called blank. Applied to both the desktop
grab (the verdict) and the client capture (recorded only). Default 500000.
#>
[CmdletBinding()]
param(
[int]$Runs = 10,
[string]$ExePath,
[string]$Teleloc = "0x2E430012 57.895 42.116 16.802 1 0 0 0",
[int]$MinRenderedBytes = 500000
)
$ErrorActionPreference = 'Stop'
$repo = Split-Path -Parent $PSScriptRoot
if (-not $ExePath) { $ExePath = Join-Path $repo 'src\AcDream.App\bin\Release\net10.0\AcDream.App.exe' }
if (-not (Test-Path $ExePath)) { throw "Client not found at $ExePath." }
# Never race a session someone is already playing: same account, same server.
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.'
}
Add-Type -AssemblyName System.Drawing
Add-Type @'
using System;
using System.Runtime.InteropServices;
public static class RepeatGateSurface {
[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; }
}
'@
$root = Join-Path $env:TEMP "claude\repeat-connected-$([DateTime]::Now.ToString('HHmmss'))"
New-Item -ItemType Directory -Force -Path $root | Out-Null
$results = @()
for ($i = 1; $i -le $Runs; $i++) {
$dir = Join-Path $root "run-$i"
New-Item -ItemType Directory -Force -Path $dir | Out-Null
Set-Content -Encoding utf8 (Join-Path $dir 'probe.txt') -Value @"
wait world-ready 90000
command /teleloc $Teleloc
wait materialized 1 90000
wait world-visible 30000
sleep 15000
screenshot repeat-run 30000
sleep 20000
"@
$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_UI_PROBE_SCRIPT = Join-Path $dir 'probe.txt'
$env:ACDREAM_AUTOMATION_ARTIFACT_DIR = $dir
# 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'
# Normal window style: the frame must be composited on screen to be grabbed.
$proc = Start-Process -FilePath $ExePath -RedirectStandardOutput $log `
-RedirectStandardError "$log.err" -PassThru
$shot = Join-Path $dir 'screenshots\repeat-run.png'
$grab = Join-Path $dir 'desktop-grab.png'
$deadline = (Get-Date).AddSeconds(180)
while ((Get-Date) -lt $deadline) {
if (Test-Path $shot) { break }
if ($proc.HasExited) { break }
Start-Sleep -Milliseconds 500
}
if (Test-Path $shot) {
# The client is now in its trailing sleep, still rendering this scene,
# so both images observe one steady frame.
Start-Sleep -Milliseconds 800
try {
$proc.Refresh()
$h = $proc.MainWindowHandle
if ($h -ne [IntPtr]::Zero) {
[RepeatGateSurface]::ShowWindow($h, 9) | Out-Null # SW_RESTORE
[RepeatGateSurface]::SetForegroundWindow($h) | Out-Null
Start-Sleep -Milliseconds 1200
$r = New-Object RepeatGateSurface+RECT
[RepeatGateSurface]::GetClientRect($h, [ref]$r) | Out-Null
$p = New-Object RepeatGateSurface+POINT
[RepeatGateSurface]::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()
}
}
} catch {
Write-Host "[repeat-gate] desktop grab failed: $_"
}
}
$app = Get-Process -Name AcDream.App -ErrorAction SilentlyContinue
if ($app) {
$app.CloseMainWindow() | Out-Null
if (-not $app.WaitForExit(12000)) { $app | Stop-Process -Force }
}
$grabSize = if (Test-Path $grab) { (Get-Item $grab).Length } else { 0 }
$shotSize = if (Test-Path $shot) { (Get-Item $shot).Length } else { 0 }
$verdict = if ($grabSize -ge $MinRenderedBytes) { 'RENDERED' }
elseif ($grabSize -gt 0) { 'BLANK' }
else { 'NO-CAPTURE' }
$clientView = if ($shotSize -ge $MinRenderedBytes) { 'RENDERED' }
elseif ($shotSize -gt 0) { 'BLANK' }
else { 'NO-CAPTURE' }
$results += [pscustomobject]@{
Run = $i; Verdict = $verdict; GrabBytes = $grabSize
ClientCapture = $clientView; ClientBytes = $shotSize
}
Write-Host ("[repeat-gate] run {0}/{1}: screen={2} ({3} B) client={4} ({5} B)" -f `
$i, $Runs, $verdict, $grabSize, $clientView, $shotSize)
Start-Sleep -Seconds 10 # let ACE clear the graceful logout before the next login
}
Write-Host ''
$blank = @($results | Where-Object Verdict -ne 'RENDERED')
$results | Format-Table -AutoSize | Out-String | Write-Host
$split = @($results | Where-Object { $_.Verdict -ne $_.ClientCapture })
if ($split.Count -gt 0) {
Write-Host ("[repeat-gate] note: {0}/{1} runs had the two instruments disagree." -f `
$split.Count, $Runs)
}
if ($blank.Count -gt 0) {
Write-Host ("[repeat-gate] FAILED: {0}/{1} runs did not render on the desktop witness. Artifacts: {2}" -f $blank.Count, $Runs, $root) -ForegroundColor Red
exit 1
}
Write-Host ("[repeat-gate] PASS: {0}/{0} runs rendered on the desktop witness. Artifacts: {1}" -f $Runs, $root)
exit 0