test(render): add the repeat-run connected gate; record V4c/V4d re-land conditions

The V4c blank-world regression was intermittent - roughly one launch in three at the worst location, zero in seven at the parent - so a single connected capture passes the broken binary most of the time and gates nothing. The new gate runs N full connect-teleloc-render-screenshot cycles with graceful logout and a per-run verdict by screenshot content size, refuses to start if a client is already using the shared test account, and pins the teleloc because the failure rate is location-sensitive. Ten clean runs bound a one-in-three defect below roughly four percent.

Campaign doc 5.5 records the revert evidence and the binding re-land conditions: the GL ring write path moves to mapped unsynchronized writes, and V4c/V4d re-land only at 10/10 rendered plus a passing offline gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-27 22:43:03 +02:00
parent 543bc79f8a
commit 61f3c5d803
2 changed files with 152 additions and 0 deletions

View file

@ -603,6 +603,40 @@ as well, and the `SceneLighting` UBO (binding 1) by terrain and the four viewpor
portal renderers. GL binding points are global, so the safe move while those consumers
are still raw GL is to leave both bound as they are and convert them with the spine.
### 5.5 The V4c/V4d revert (2026-07-27) and the re-land conditions
V4c and V4d were reverted at `543bc79f`/`b537f3a9`/`ad61f250` after the first
connected sessions showed a **blank world** — UI rendered, sounds played, the log
was clean, `world-reveal` reported `visible=True`, and the user hit one AMD driver
timeout. The offline gate had passed both slices at noise level.
What the debugging established, with the connected screenshot-probe apparatus:
the defect is **intermittent (~1 in 3 at the worst location, 0 of 7 at the V4c
parent)** and scene-dependent; after the first world frame the default
framebuffer's colour reads return garbage (float depth bytes) and `glClear`
stops having any effect, with `glGetError` clean throughout — a GPU-side fault,
not an API error. Every added CPU↔GPU sync point monotonically suppresses it,
which also made the mechanism unprovable in situ. Best-supported cause: the
frame ring performs 1040 partial `glBufferSubData` updates per frame into a
buffer object that already-submitted same-frame draws are still reading; the
offline flat path issues 24 such updates, the connected PView path dozens —
which is exactly the offline/connected axis. This likely also explains the TDR.
The contract amendments (`111e7236` InverseAlpha, `c7f5f251` integer vertex
attributes + tiling binding) were **kept** — they are dark, test-covered, and
correct.
**Re-land conditions, binding:**
1. The GL ring's write path moves to `glMapBufferRange(WRITE | UNSYNCHRONIZED |
INVALIDATE_RANGE)` — the canonical GL ring idiom, which states the non-overlap
invariant to the driver instead of leaving `glBufferSubData`-into-an-in-use-
buffer to driver heuristics.
2. V4c and then V4d re-land as reverts-of-the-reverts plus the ring change, each
gated by `tools/run-repeat-connected-gate.ps1` at **10/10 rendered** — a single
connected run passes a broken binary ~70% of the time and gates nothing.
3. The offline pixel gate still passes, and the gate location stays pinned
(failure rate is location-sensitive; stray input moves the character).
### 5.4 The null-target `BeginPass` divergence (V4c) — must be undone at V6
V4c had to stop GL's `BeginPass` from binding framebuffer 0 when a pass declares

View file

@ -0,0 +1,118 @@
<#
.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 -> graceful close -> cooldown.
The verdict is by screenshot content size: a rendered 1280x720 world compresses
to ~1.5 MB; the blank-world failure produces a few-KB flat PNG. The teleloc is
pinned because the failure rate is location-sensitive, and stray input into a
minimized window silently moves the character between runs.
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
Screenshot size below which a run is called blank. 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.'
}
$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 1000
"@
$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
$log = Join-Path $dir 'client.log'
$proc = Start-Process -FilePath $ExePath -RedirectStandardOutput $log `
-RedirectStandardError "$log.err" -PassThru -WindowStyle Minimized
$shot = Join-Path $dir 'screenshots\repeat-run.png'
$deadline = (Get-Date).AddSeconds(180)
while ((Get-Date) -lt $deadline) {
if (Test-Path $shot) { Start-Sleep -Seconds 2; break }
if ($proc.HasExited) { break }
Start-Sleep -Seconds 2
}
$app = Get-Process -Name AcDream.App -ErrorAction SilentlyContinue
if ($app) {
$app.CloseMainWindow() | Out-Null
if (-not $app.WaitForExit(12000)) { $app | Stop-Process -Force }
}
$size = if (Test-Path $shot) { (Get-Item $shot).Length } else { 0 }
$verdict = if ($size -ge $MinRenderedBytes) { 'RENDERED' }
elseif ($size -gt 0) { 'BLANK' }
else { 'NO-CAPTURE' }
$results += [pscustomobject]@{ Run = $i; Verdict = $verdict; Bytes = $size }
Write-Host ("[repeat-gate] run {0}/{1}: {2} ({3} bytes)" -f $i, $Runs, $verdict, $size)
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
if ($blank.Count -gt 0) {
Write-Host ("[repeat-gate] FAILED: {0}/{1} runs did not render. Artifacts: {2}" -f $blank.Count, $Runs, $root) -ForegroundColor Red
exit 1
}
Write-Host ("[repeat-gate] PASS: {0}/{0} runs rendered. Artifacts: {1}" -f $Runs, $root)
exit 0