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:
parent
e6362da5c2
commit
eb2ba4e5f0
3 changed files with 318 additions and 32 deletions
|
|
@ -749,7 +749,163 @@ screenshot-byte verdict it uses is now known to be measuring the wrong thing.
|
|||
The next instrument should be an occlusion query (`GL_SAMPLES_PASSED`) around the
|
||||
world pass, read back a frame later so it adds no sync point — that separates
|
||||
"the draws never executed" from "they executed and their output was discarded",
|
||||
which is the remaining fork.
|
||||
which is the remaining fork. **That instrument was built and run — see §5.5.3,
|
||||
which supersedes this section's "shared 3-D state" framing.**
|
||||
|
||||
#### 5.5.3 The occlusion-query verdict (2026-07-28): the GPU stops reporting
|
||||
|
||||
The instrument §5.5.2 asked for was built and run on a V4c tree staged from
|
||||
`git revert --no-commit 543bc79f` (never committed), with `GL_SAMPLES_PASSED`
|
||||
bracketing three sites — the raw-GL terrain draw, the dispatcher's entity draws,
|
||||
and the retained-UI flush — and the counts collected later, never in the frame
|
||||
that issued them. The desktop witness was the verdict throughout. All probe code
|
||||
was stripped before this commit; the apparatus changes that survive are the two
|
||||
gate scripts, now asserting on the desktop grab.
|
||||
|
||||
**Building it turned up a fourth instrument fault, and it is the sharpest one.**
|
||||
The obvious readback — `glGetQueryObject` into client memory, guarded by
|
||||
`GL_QUERY_RESULT_AVAILABLE` and read a frame late — **deadlocks the client on
|
||||
V4c.** Four consecutive runs wedged at the first frame that draws the world, and
|
||||
five `dotnet-stack` samples taken four seconds apart all show the render thread
|
||||
inside the driver under `GlDrawCounterProbe.Drain`, i.e. blocked in
|
||||
`glGetQueryObject`. It is not a probe defect: the identical probe ran 4,420
|
||||
frames on the V4c *parent* with normal counts and a normal 5 ms frame time, and
|
||||
the wedge does not need the world sites at all — instrumenting only the UI flush
|
||||
reproduces it, while creating the query objects and never beginning one does not.
|
||||
So on V4c, the mere existence of an outstanding occlusion query is enough to make
|
||||
a CPU-side result read never return.
|
||||
|
||||
The way past that is to never ask the driver for a result on the CPU timeline.
|
||||
The query result is instead written into a persistently-mapped, coherent
|
||||
`GL_QUERY_BUFFER`: `glGetQueryObject` with that buffer bound performs the write
|
||||
on the GPU timeline, so no client wait is possible by construction, and
|
||||
pre-filling each slot with a sentinel makes "the GPU reported zero samples" and
|
||||
"the GPU never reached this command" different observations. That instrument does
|
||||
not wedge, and it produced the table below.
|
||||
|
||||
**Per-frame counters, blank versus rendered, four runs on one V4c binary** —
|
||||
three blank and one rendered on the desktop witness, both instruments agreeing on
|
||||
the label in every run:
|
||||
|
||||
| | terrain | entities | UI | frames logged |
|
||||
|---|---|---|---|---|
|
||||
| Rendered run (run 3) | 1,718,771 | ~312,600 | 541,445 | 1,068, **zero** no-result |
|
||||
| Blank runs (1, 2, 4), steady state | no result | no result | no result | ~950 each, **every** query |
|
||||
| Blank runs, frame 43 (the one early world frame) | 1,692,830 | 317,561 | no result | — |
|
||||
| Parent build, same probe, frame 45 | 1,692,830 | 317,561 | 539,0xx | 4,420, zero pending |
|
||||
|
||||
Four things follow, and they are worth separating.
|
||||
|
||||
**1. The mission's three-way fork resolves to the third branch — but not as
|
||||
"submission stalls".** On a blank run no query result is ever produced, at any
|
||||
site, for the whole run. It is emphatically not "zero samples": the sentinel is
|
||||
untouched, so the GPU never executed the write. And yet the process is not
|
||||
stalled — frame time stays at a steady 5.5 ms for ~3,700 frames, the frame-flight
|
||||
fences keep retiring (`GpuFrameFlightController.RetireFence` spins on
|
||||
`glClientWaitSync` until the fence signals, so a stalled submission would freeze
|
||||
the client outright), and the compositor keeps showing the clear colour and the
|
||||
complete retained UI. The GPU is running the frame. What has stopped is
|
||||
everything the GPU is asked to *report*.
|
||||
|
||||
**2. The failure is total, not 3-D-specific.** The UI query dies on a blank run
|
||||
too — in the same frames where the desktop witness plainly shows the UI on
|
||||
screen. §5.5.2 read the symptom as "something shared by every depth-tested draw
|
||||
is poisoned"; that framing is now too narrow. Every GPU→CPU reporting channel
|
||||
tested is dead on a blank run — `glReadPixels` of framebuffer 0 returns
|
||||
RGBA(0,0,0,0) even over visible UI pixels, a CPU query read blocks forever, a
|
||||
GPU-timeline query write never lands — while the two channels that carry no
|
||||
result, fence signalling and present, keep working. The common factor is the
|
||||
*direction*: nothing comes back.
|
||||
|
||||
**3. The transition is a single sharp event at the first world frame, and it is
|
||||
irreversible.** In every blank run the UI query returns normal counts (539,010)
|
||||
for frames 1–42, the world draws for the first time at frame 43, and from that
|
||||
frame on nothing is ever reported again — 924 consecutive dead frames in run 1.
|
||||
The rendered run has no world draw at frame 43 (its first is frame 1,080) and
|
||||
never loses a single result. This is the same "after the first world frame"
|
||||
boundary §5.5 recorded from the colour reads, now measured on a second,
|
||||
independent channel.
|
||||
|
||||
**4. That first world frame rasterizes correctly — identically on both builds.**
|
||||
Frame 43 reports 1,692,830 terrain samples and 317,561 entity samples on V4c, and
|
||||
the parent's first world frame reports **the same two numbers**. The world is
|
||||
drawn, in full, exactly as the good build draws it. It is the last thing the GPU
|
||||
ever tells this process, and V4c is what decides whether that is the last thing.
|
||||
|
||||
**Where this leaves the mechanism.** Everything now points at the GPU→CPU
|
||||
reporting path for this context collapsing at the first world frame, with V4c's
|
||||
submission pattern as the trigger and nothing in V4c's own state as the cause —
|
||||
V4c does not touch the terrain draw, does not touch the UI flush, and §5.5.2
|
||||
already showed its renderer state is bit-identical on blank and rendered frames.
|
||||
A clean context with `glGetGraphicsResetStatus` = `NO_ERROR` on 1,814 samples
|
||||
does not lose its readback, its query results, and its ability to answer a query
|
||||
without blocking, all at once, because of anything expressible in the API. **No
|
||||
fix was attempted and V4c is still not re-landed.**
|
||||
|
||||
#### 5.5.4 Strategic options
|
||||
|
||||
Three ways forward, with the evidence for each.
|
||||
|
||||
**(A) Keep hunting for a V4c-side trigger we can remove.** The attribution is
|
||||
solid (§5.5.1: 4/5 versus 0/5 interleaved, p ≈ 0.024), so a trigger exists in the
|
||||
V4c diff and removing it would restore the no-fork plan. Against it: five
|
||||
mechanisms have now been falsified — the ring's `glBufferSubData` hazard, a
|
||||
capture-FBO leak, reveal ordering, shared 3-D state (depth plane, camera, clip
|
||||
distances, context reset), and CPU-side visibility — and the two facts that
|
||||
remain are *not expressible in the API*, which is exactly the shape of a hunt
|
||||
with no bottom. The remaining honest step would be a RenderDoc or GPU-crash-dump
|
||||
capture of the frame-43 boundary, or a bisect of the V4c diff into ~6 sub-commits
|
||||
each measured at 5 runs, which is roughly 3 hours of connected machine time per
|
||||
round and pins the user's machine for it.
|
||||
|
||||
**(B) Accept it as an AMD GL driver defect, keep the world on the legacy raw-GL
|
||||
path on the GL backend, and carry the V4c/V4d RHI ports forward for Vulkan
|
||||
only.** This is what the evidence supports: a defect that (i) follows a
|
||||
submission-pattern change, (ii) is invisible to every API-level state query,
|
||||
(iii) kills three unrelated readback channels simultaneously while leaving
|
||||
present and fences intact, and (iv) can be induced *harder* by adding a
|
||||
perfectly legal occlusion query, is a driver defect in 26.6.4 on the RX 9070 XT,
|
||||
not an application bug. The cost is real and must be stated plainly: it breaks
|
||||
§3.1's no-fork rule for the world path, so the GL backend keeps raw-GL world
|
||||
renderers while Vulkan gets RHI ones, and V4h's "seam complete" milestone can no
|
||||
longer mean "nothing raw-GL remains". V7's GL-versus-Vulkan differential then
|
||||
compares a raw-GL world against an RHI world rather than one contract against
|
||||
two backends, which weakens it precisely where it is most valuable. It also
|
||||
leaves the deleted-at-V11 GL path carrying code the campaign intended to retire
|
||||
early.
|
||||
|
||||
**(C) Reorder the campaign: bring Vulkan up first (V5/V6) and decide V4c
|
||||
afterwards.** This is the option the evidence actually suggests and it is not on
|
||||
the original menu. The whole point of V4c is to make the world path
|
||||
backend-agnostic; its only consumer that matters is Vulkan. If the GL stack of
|
||||
this driver is what breaks, then running the same ported code on the Vulkan
|
||||
backend is both the cheapest test of hypothesis (B) — if the identical RHI world
|
||||
path renders correctly on Vulkan on the same GPU, the defect is in the driver's
|
||||
GL stack, conclusively and in one measurement instead of a multi-hour bisect —
|
||||
and the shipping path. The sequencing cost is that V5/V6 must be written against
|
||||
an RHI whose world-path consumer is proven only offline, and that V4c's diff sits
|
||||
un-landed on a branch meanwhile; the sequencing invariants in §5.4 would need
|
||||
V4c/V4d/V4t moved after V6, with V4a/V4b/V4e/V4f/V4g (all landed or independent)
|
||||
unaffected.
|
||||
|
||||
**Recommendation: (C), with (B) as its fallback.** (A) is the only option with no
|
||||
bounded cost and the worst prior — five falsified mechanisms and two facts that
|
||||
live below the API. (B) is probably the right *conclusion*, but adopting it now
|
||||
means paying the no-fork penalty on the strength of an inference; one Vulkan
|
||||
bring-up turns that inference into a measurement, and it is work the campaign has
|
||||
to do regardless. If the RHI world path renders on Vulkan on this GPU, (B) is
|
||||
proven and can be adopted deliberately, with the fork scoped and documented
|
||||
rather than assumed. If it fails on Vulkan too, then the defect is ours after
|
||||
all, the trigger is in code we own, and (A) becomes worth its cost because it
|
||||
would then have a much smaller haystack.
|
||||
|
||||
**Re-land conditions, updated.** §5.5's three conditions stand, with two
|
||||
amendments: condition 2's gate now asserts on the desktop witness
|
||||
(`tools/run-repeat-connected-gate.ps1` and `tools/run-blank-world-ab-probe.ps1`
|
||||
grab the composited window and treat the client's own capture as a recorded
|
||||
second column), and no re-land attempt should be made before the (C) measurement,
|
||||
because a 10/10 pass on this machine cannot distinguish a fix from the defect's
|
||||
ordinary ~1-in-5 quiet streak.
|
||||
|
||||
### 5.4 The null-target `BeginPass` divergence (V4c) — must be undone at V6
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue