fix(diag): resolve the multisampled backbuffer before reading it
Every automated pixel gate and every blank-world verdict in Campaign V is produced by FrameScreenshotController reading the default framebuffer with glReadPixels. The window is created with the quality preset's MSAA sample count, so that framebuffer is normally 4x multisampled -- and glReadPixels against a multisampled read framebuffer is undefined per the GL spec. The instrument the campaign has been using to decide "did the world render?" rested on an operation with no specified result. That is not a theoretical complaint. The blank-world investigation spent several rounds unable to tell "the renderer drew nothing" apart from "the readback did not return what the renderer drew", and it took an out-of-process desktop grab to separate them. A gate cannot arbitrate a rendering defect while its own read is unspecified. So the capture resolves first: when the default framebuffer is multisampled it blits the whole colour buffer into a single-sampled RGBA8 framebuffer with identical rectangles and GL_NEAREST -- the defined resolve -- and reads that. A single-sampled default framebuffer keeps the original direct read, so non-MSAA captures stay byte-for-byte what they were. The blit disables and restores the scissor test, because a blit is subject to it and a frame that left a rectangle armed would otherwise resolve only part of the image; that is the same self-contained-GL-state rule the render passes follow. The resolve target is created and destroyed per capture -- captures are rare, and a cache would have to track resize and context teardown for no gain. GlGpuDevice.CaptureBackbuffer had the identical undefined read. It now routes through the same path rather than being a second instrument to keep sound. The IDefaultFramebufferSurface seam grows the draw binding, the sample count, and the resolve operations, so the bind/query/blit/read/restore order stays assertable without a GL context; two new tests pin the resolve order and the resolve target's release on a failing read. Gates: Release build green. App tests 3,866 passed / 3 skipped. Offline pixel gate againstfed636b9passes at a differing fraction of 4.08e-05 against the 0.001 threshold -- which is exactly the same-commit control pair measured at this commit, i.e. indistinguishable from ambient noise. Same-commit controls re-measured at 17 px (fed636b9) and 23 px (here) out of 563,200; the recorded band in plan section 5.1 widens to 15-23 px, fraction <= 4.1e-05. Plan section 5.5.1 records what the connected investigation established: the interleaved A/B attribution (4/5 vs 0/5, p ~ 0.024), the desktop witness showing every depth-tested draw missing while the atmosphere clear and the complete retained UI present, the probe evidence that the CPU dispatched 3,331 statics with no GL error, and the falsification list -- including the ring glBufferSubData hazard, which condition 1 shipped against and did not fix. Section 5.5.2 records this session's second investigation, run against a staged (never committed) V4c with log-only glGet* probes, and it closes the shared-3-D-state hypothesis. The depth plane is bit-identical on blank and rendered frames -- test on, write mask on, GL_LESS, clear value 1.0, range [0,1], full viewport, full colour mask, no clip distances. The camera constants are sane and advancing. Forcing gl_ClipDistance off left the blank rate unchanged at 3/5. glGetGraphicsResetStatus returned NO_ERROR in all 1,814 samples across four blank runs, which also retires the "GPU-side fault" reading in its context-reset form. Two sharper facts replace it. Replacing only the frame clear colour with magenta makes a blank frame come back uniformly magenta under the complete retained UI, so no 3-D fragment is rasterized at all -- the world is not drawn-then-hidden, fogged, or overdrawn. And on a blank run the client's own capture of framebuffer 0 is RGBA(0,0,0,0) in every pixel, including pixels where the UI is visibly on screen at that moment. That survives this commit's resolve fix, so it is a second, independent instrument fault: the screenshot- byte verdict used by the repeat and A/B gates measures the readback, not the renderer, and those gates need to assert on the desktop witness instead. V4c is NOT re-landed. No fix was attempted, because the mechanism is not renderer state and does not sit in V4c's surface as this hypothesis predicted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
fed636b9c0
commit
e6362da5c2
4 changed files with 456 additions and 26 deletions
|
|
@ -480,6 +480,19 @@ masked, two independent same-commit pairs differ by **15 and 17 pixels out of
|
|||
threshold. The gate is a strict identity check on everything it covers, rather
|
||||
than a loose tolerance that would hide real regressions.
|
||||
|
||||
**Noise band re-measured 2026-07-28**, after the capture began resolving the
|
||||
multisampled default framebuffer instead of reading it through an unspecified
|
||||
operation (see §5.5). Two fresh same-commit control pairs — one at `fed636b9`,
|
||||
one at the resolve commit — differ by **17 and 23 pixels**, fractions
|
||||
`3.02e-05` and `4.08e-05`. The change itself measured `4.08e-05` against
|
||||
`fed636b9`, i.e. exactly its own same-commit control and therefore
|
||||
indistinguishable from ambient noise. The band is now **15–23 differing pixels,
|
||||
fraction ≤ 4.1e-05**, a ~24× margin under the 0.001 threshold. Two facts are
|
||||
worth keeping: the resolve moved essentially nothing in this scene, which says
|
||||
AMD's unspecified read was usually returning the resolved image already; and
|
||||
"usually" is exactly the property that makes an unspecified read useless as an
|
||||
instrument.
|
||||
|
||||
**Coverage.** Terrain and terrain blending, scenery, static world meshes, water,
|
||||
fog, and the entire retained UI (vitals, spell bar, toolbar, chat, radar).
|
||||
|
||||
|
|
@ -637,6 +650,107 @@ correct.
|
|||
3. The offline pixel gate still passes, and the gate location stays pinned
|
||||
(failure rate is location-sensitive; stray input moves the character).
|
||||
|
||||
#### 5.5.1 What the connected investigation established (2026-07-27/28)
|
||||
|
||||
Condition 1 landed at `8dec163f` and **did not fix the defect**. The ring's
|
||||
`glBufferSubData` hazard is therefore *falsified as the cause*; the map change is
|
||||
kept because it is the correct idiom regardless, but the paragraph above naming
|
||||
it "best-supported cause" is superseded by what follows.
|
||||
|
||||
**The defect does follow the V4c binary.** Blank rate drifts with machine state —
|
||||
the same binary measured 3/10 in one block and 5/5 in another — so consecutive
|
||||
blocks of A then B confound the change with the drift, and the first attributions
|
||||
were made that way. `tools/run-blank-world-ab-probe.ps1` interleaves the two
|
||||
builds inside one block so the drift is shared: **4/5 blank in the V4c arm versus
|
||||
0/5 in the parent arm, p ≈ 0.024**. That is the attribution; everything below is
|
||||
about mechanism.
|
||||
|
||||
**What a blank frame actually looks like, from outside the process.**
|
||||
`tools/run-blank-world-surface-probe.ps1` grabs the composited window off the
|
||||
desktop with `CopyFromScreen` — a witness that shares nothing with the renderer
|
||||
below the compositor. On a blank frame the desktop shows the atmosphere clear
|
||||
colour and the **complete retained UI**, with **all 3-D absent — including the
|
||||
raw-GL terrain and sky that V4c does not touch.** So the frame is drawn and
|
||||
presented; what is missing is every depth-tested draw, and only those.
|
||||
|
||||
Meanwhile `ACDREAM_PROBE_FLAP` reports, on those same blank frames, 3,331 statics
|
||||
dispatched, the correct PView branch, `fbo=0`, the full viewport, scissor off, and
|
||||
zero GL errors. The CPU decided to draw the world and the GL calls were accepted.
|
||||
|
||||
**Falsified:** the ring `glBufferSubData` hazard; a capture-FBO binding leak;
|
||||
reveal ordering; the AMD TDR; and CPU-side visibility. **Observed and unexplained:**
|
||||
`glReadPixels` probes *heal* the bug, while `glGetIntegerv`-shaped state queries do
|
||||
not — so any instrument that reads pixels changes the thing it measures.
|
||||
|
||||
**The verdicts themselves were unsound until `2026-07-28`.** The window is created
|
||||
with the quality preset's MSAA sample count, so the default framebuffer is 4x
|
||||
multisampled, and `glReadPixels` against a multisampled read framebuffer is
|
||||
undefined per the GL spec. Every automated pixel gate and every blank/rendered
|
||||
verdict in this campaign came through that read. `FrameScreenshotController` now
|
||||
blit-resolves the default framebuffer into a single-sampled RGBA8 framebuffer and
|
||||
reads that; a single-sampled default framebuffer keeps the original direct read.
|
||||
`GlGpuDevice.CaptureBackbuffer` routes through the same path, so there is one
|
||||
backbuffer read in the process rather than two instruments to keep sound. The
|
||||
offline gate's re-measured noise band is in §5.1: the resolve moved 23 pixels out
|
||||
of 563,200, exactly its own same-commit control, which says AMD's unspecified read
|
||||
was usually already returning the resolved image — and "usually" is what made it
|
||||
worthless as an instrument.
|
||||
|
||||
#### 5.5.2 The shared-3-D-state hypothesis is falsified (2026-07-28)
|
||||
|
||||
The natural reading of "all 3-D dies, depth-disabled UI survives, the atmosphere
|
||||
clear shows" is that something shared by every depth-tested draw — and by no UI
|
||||
draw — is poisoned. Four candidates were tested against a V4c build staged from
|
||||
`git revert --no-commit 543bc79f` (never committed) with log-only `glGet*` probes
|
||||
at the frame clear and at world-pass entry/exit. **All four are dead.**
|
||||
|
||||
| Candidate | How it was tested | Result |
|
||||
|---|---|---|
|
||||
| Depth plane (mask latched off across the clear, poisoned `glClearDepth`/`glDepthFunc`/depth range) | State sampled at `pre-clear`, `post-clear`, `landscape-in`, `landscape-out` | **Bit-identical on blank and rendered frames.** `DEPTH_TEST=on`, `DEPTH_WRITEMASK=on`, `DEPTH_FUNC=GL_LESS`, `DEPTH_CLEAR_VALUE=1.0`, range `[0,1]`, viewport `0,0,1280,720`, colour mask `1111`, scissor/stencil/blend/cull off, MSAA on, no clip distances enabled |
|
||||
| Camera constants | `ViewProjection` and eye logged at world-pass entry | Sane and advancing on blank frames; determinant `-1.688e-01`, eye stable at the pinned cell |
|
||||
| `gl_ClipDistance` (all `MaxPlanes` are enabled unconditionally around sky/terrain/entities, so an unwritten distance would clip everything 3-D and nothing 2-D) | `EnableClipDistances` forced to a no-op | Blank rate **3/5**, i.e. unchanged |
|
||||
| GPU context reset (the "GPU-side fault" reading) | `glGetGraphicsResetStatus` in the same probe | **1,814 samples across four blank runs: `NO_ERROR` every time** |
|
||||
|
||||
Two new facts were established, and they are sharper than anything before them.
|
||||
|
||||
**1. Zero 3-D fragments are rasterized — the world is not drawn-then-hidden.**
|
||||
Replacing only the frame clear colour with magenta (nothing else) makes a blank
|
||||
frame come back **uniformly magenta with the complete retained UI on top**. So
|
||||
the world is not being shaded to the fog colour, not being fogged out, and not
|
||||
being overdrawn: between the clear and the UI, not one 3-D fragment reaches the
|
||||
default framebuffer — while the CPU has dispatched the draws, GL accepted them,
|
||||
and every piece of state above is correct.
|
||||
|
||||
**2. The in-process capture does not observe the presented surface at all.** On a
|
||||
blank run the desktop grab shows the magenta clear plus the complete UI, and at
|
||||
that same moment the client's own capture of framebuffer 0 is
|
||||
**RGBA(0,0,0,0) in every pixel — including the pixels where the UI is visibly on
|
||||
screen.** This survives the §5.5.1 resolve fix, so it is a *second*, independent
|
||||
instrument fault: on a blank run, reading framebuffer 0 returns nothing even for
|
||||
content that demonstrably reached the display. Any verdict derived from
|
||||
screenshot bytes is therefore reporting the readback, not the renderer, and the
|
||||
`MinRenderedBytes` test in `run-repeat-connected-gate.ps1` /
|
||||
`run-blank-world-ab-probe.ps1` conflates the two. **The desktop witness is
|
||||
currently the only trustworthy verdict** and should be what those gates assert
|
||||
on.
|
||||
|
||||
The failure reproduces readily with a *visible* window (`WasIconic=False`
|
||||
throughout), so it is not a pixel-ownership artefact of the minimized gate
|
||||
window: 5/6, 4/5, 3/5 and 4/4 blank across four blocks at the pinned cell.
|
||||
|
||||
**Where this leaves the mechanism.** It is not renderer state and not a context
|
||||
reset; clears and UI draws reach the display while 3-D draws and pixel reads
|
||||
against the same framebuffer both come back empty. That combination points below
|
||||
the API — at how the default framebuffer's colour is being handled for this
|
||||
context — rather than at anything V4c writes. **V4c has therefore not been
|
||||
re-landed**, and no fix was attempted: the re-land conditions in §5.5 stand, but
|
||||
condition 2's gate must first be re-armed on the desktop witness, because the
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue