docs(render): the occlusion-query verdict on the V4c blank world, and the options

Runs the instrument section 5.5.2 asked for, on a V4c tree staged from
`git revert --no-commit 543bc79f` and never committed: GL_SAMPLES_PASSED around
the raw-GL terrain draw, the dispatcher's entity draws, and the retained-UI
flush, collected outside the frame that issued them, with the desktop witness as
the verdict. All probe code is stripped; what survives here is the two gate
scripts and section 5.5.3/5.5.4.

Building it found a fourth instrument fault. Reading a query result on the CPU
timeline - glGetQueryObject guarded by RESULT_AVAILABLE, one frame late -
deadlocks V4c at the first frame that draws the world: 4/4 runs, and five
dotnet-stack samples four seconds apart all show the render thread inside the
driver in that call. Not a probe defect - the same probe ran 4,420 clean frames
on the V4c parent, and instrumenting only the UI flush reproduces the wedge while
creating the query objects and never beginning one does not.

Routing the result into a persistently-mapped GL_QUERY_BUFFER instead - the
driver writes it on the GPU timeline, so no client wait is possible, and a
sentinel separates "reported zero" from "never reached" - does not wedge, and
gives the answer. On blank runs no query result is ever produced at any site for
the whole run, including the UI, in the same frames where the desktop grab plainly
shows the UI on screen. On the rendered run of the same binary, 1,068 frames, not
one missing result.

So the mission's fork resolves to "never completes", but not as a stall: frame
time holds at 5.5 ms for ~3,700 frames, the frame-flight fences keep retiring,
and present keeps working. Every channel that carries a result back from the GPU
is dead - pixel readback, CPU query read, GPU-timeline query write - and every
channel that carries none is fine. The transition is one sharp event at the first
world frame and never reverses, and that frame rasterizes correctly: 1,692,830
terrain and 317,561 entity samples, the same two numbers the parent reports for
its own first world frame.

Section 5.5.4 lays out the three options with their costs and recommends (C):
bring Vulkan up first and decide V4c afterwards, because running the identical
ported world path on the Vulkan backend on this GPU is both the cheapest test of
the driver-defect reading and work the campaign owes anyway. (B), accepting the
GL-side fork, is probably the right conclusion but should be adopted on a
measurement rather than an inference. No fix was attempted and V4c is not
re-landed.

Apparatus: run-repeat-connected-gate.ps1 and run-blank-world-ab-probe.ps1 now
assert on the desktop grab and record the client's own capture as a second
column, which is the re-arming section 5.5.2 required before re-land condition 2
can mean anything. Both verified end-to-end.

Gates: Release build clean; App tests 3,866 passed / 3 skipped; offline pixel
gate PASS at 3.37e-05 differing fraction (19 px of 563,200), inside the
documented 15-23 px band.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-28 01:44:34 +02:00
parent e6362da5c2
commit eb2ba4e5f0
3 changed files with 318 additions and 32 deletions

View file

@ -17,9 +17,12 @@
exonerate the change under test; a rate that follows the binary implicates it.
Each cycle is the repeat gate's cycle exactly: launch connected -> teleloc to
the pinned cell -> wait world-visible -> settle -> screenshot -> graceful close
-> cooldown. The verdict is the same screenshot-size test, so the two scripts'
numbers are directly comparable.
the pinned cell -> wait world-visible -> settle -> screenshot -> desktop grab
-> graceful close -> cooldown. The verdict is the same desktop-witness test,
so the two scripts' numbers are directly comparable - see that script's
description for why the client's own capture cannot be the verdict, and for
the "nothing else on the primary monitor, do not type" constraints that
grabbing the composited window imposes.
.PARAMETER Pairs
A/B pairs to run. Default 5 (ten launches).
@ -36,7 +39,8 @@
repeat gate; the failure rate is location-sensitive.
.PARAMETER MinRenderedBytes
Screenshot size below which a run is called blank. Default 500000.
PNG size below which an image is called blank. Applied to the desktop grab
(the verdict) and to the client capture (recorded only). Default 500000.
#>
[CmdletBinding()]
param(
@ -59,6 +63,20 @@ if (Get-Process -Name AcDream.App -ErrorAction SilentlyContinue) {
throw 'AcDream.App is already running. This probe 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 AbProbeSurface {
[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\blank-world-ab-$([DateTime]::Now.ToString('HHmmss'))"
New-Item -ItemType Directory -Force -Path $root | Out-Null
$results = @()
@ -77,7 +95,7 @@ wait materialized 1 90000
wait world-visible 30000
sleep 15000
screenshot ab-run 30000
sleep 1000
sleep 20000
"@
$env:ACDREAM_DAT_DIR = Join-Path $env:USERPROFILE "Documents\Asheron's Call"
@ -91,15 +109,44 @@ sleep 1000
$env:ACDREAM_AUTOMATION_ARTIFACT_DIR = $dir
$log = Join-Path $dir 'client.log'
# Normal window style: the frame must be composited on screen to be grabbed.
$proc = Start-Process -FilePath $arm.Exe -RedirectStandardOutput $log `
-RedirectStandardError "$log.err" -PassThru -WindowStyle Minimized
-RedirectStandardError "$log.err" -PassThru
$shot = Join-Path $dir 'screenshots\ab-run.png'
$grab = Join-Path $dir 'desktop-grab.png'
$deadline = (Get-Date).AddSeconds(180)
while ((Get-Date) -lt $deadline) {
if (Test-Path $shot) { Start-Sleep -Seconds 2; break }
if (Test-Path $shot) { break }
if ($proc.HasExited) { break }
Start-Sleep -Seconds 2
Start-Sleep -Milliseconds 500
}
if (Test-Path $shot) {
Start-Sleep -Milliseconds 800
try {
$proc.Refresh()
$h = $proc.MainWindowHandle
if ($h -ne [IntPtr]::Zero) {
[AbProbeSurface]::ShowWindow($h, 9) | Out-Null # SW_RESTORE
[AbProbeSurface]::SetForegroundWindow($h) | Out-Null
Start-Sleep -Milliseconds 1200
$r = New-Object AbProbeSurface+RECT
[AbProbeSurface]::GetClientRect($h, [ref]$r) | Out-Null
$p = New-Object AbProbeSurface+POINT
[AbProbeSurface]::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 "[ab-probe] desktop grab failed: $_"
}
}
$app = Get-Process -Name AcDream.App -ErrorAction SilentlyContinue
@ -108,15 +155,20 @@ sleep 1000
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' }
$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]@{
Pair = $pair; Arm = $arm.Label; Verdict = $verdict; Bytes = $size
Pair = $pair; Arm = $arm.Label; Verdict = $verdict; GrabBytes = $grabSize
ClientCapture = $clientView; ClientBytes = $shotSize
}
Write-Host ("[ab-probe] pair {0}/{1} arm {2}: {3} ({4} bytes)" -f `
$pair, $Pairs, $arm.Label, $verdict, $size)
Write-Host ("[ab-probe] pair {0}/{1} arm {2}: screen={3} ({4} B) client={5} ({6} B)" -f `
$pair, $Pairs, $arm.Label, $verdict, $grabSize, $clientView, $shotSize)
Start-Sleep -Seconds 10 # let ACE clear the graceful logout before the next login
}

View file

@ -11,11 +11,30 @@
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.
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'.
@ -32,7 +51,8 @@
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.
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(
@ -52,6 +72,20 @@ 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 = @()
@ -66,7 +100,7 @@ wait materialized 1 90000
wait world-visible 30000
sleep 15000
screenshot repeat-run 30000
sleep 1000
sleep 20000
"@
$env:ACDREAM_DAT_DIR = Join-Path $env:USERPROFILE "Documents\Asheron's Call"
@ -80,15 +114,46 @@ sleep 1000
$env:ACDREAM_AUTOMATION_ARTIFACT_DIR = $dir
$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 -WindowStyle Minimized
-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) { Start-Sleep -Seconds 2; break }
if (Test-Path $shot) { break }
if ($proc.HasExited) { break }
Start-Sleep -Seconds 2
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
@ -97,12 +162,20 @@ sleep 1000
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' }
$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' }
$results += [pscustomobject]@{ Run = $i; Verdict = $verdict; Bytes = $size }
Write-Host ("[repeat-gate] run {0}/{1}: {2} ({3} bytes)" -f $i, $Runs, $verdict, $size)
$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
}
@ -110,9 +183,14 @@ sleep 1000
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. Artifacts: {2}" -f $blank.Count, $Runs, $root) -ForegroundColor Red
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. Artifacts: {1}" -f $Runs, $root)
Write-Host ("[repeat-gate] PASS: {0}/{0} runs rendered on the desktop witness. Artifacts: {1}" -f $Runs, $root)
exit 0