diff --git a/docs/plans/2026-07-27-vulkan-campaign.md b/docs/plans/2026-07-27-vulkan-campaign.md index a05d8546..62c93057 100644 --- a/docs/plans/2026-07-27-vulkan-campaign.md +++ b/docs/plans/2026-07-27-vulkan-campaign.md @@ -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 10–40 partial `glBufferSubData` updates per frame into a +buffer object that already-submitted same-frame draws are still reading; the +offline flat path issues 2–4 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 diff --git a/tools/run-repeat-connected-gate.ps1 b/tools/run-repeat-connected-gate.ps1 new file mode 100644 index 00000000..41d4113f --- /dev/null +++ b/tools/run-repeat-connected-gate.ps1 @@ -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