Tried the offline Vulkan self-differential once more at consolidated HEAD before conceding the night: the client still cannot create a window (same #259 signature - the fault vulkaninfo reproduces without our code). The shell is unelevated so a driver restart is unavailable, and a reboot would kill the session executing the goal. The negative is recorded with a timestamp so the morning rerun starts from evidence, not a re-bisect.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds `docs/reports/2026-07-29-overnight-report.md` and brings the Campaign V
report page's status section in line with what is actually true.
The report leads with what needs the user, because most of it is genuinely
blocked rather than merely unfinished: the #259 reboot and the five V11 runtime
gates behind it, the enum campaign's three decisions (adopt `WeenieError`
wholesale? adopt retail's `SoundId` table or record our subset as a divergence?
re-clone the reference repos), the wire audit's one instrumented session, and
the walked-portal follow-up for #256/#257. Then what landed, with numbers, then
Campaign V's final state.
Two notes worth surfacing rather than burying. `references/` is empty **in the
main checkout**, not just in a worktree — which is why 456 of the enum
campaign's 864 property members are single-sourced, and why CLAUDE.md's
"cross-reference at least two of them" is currently unexecutable as written.
And two `MEMORY.md` entries index research documents that exist nowhere: not in
the tree, not under any ref, and the memory directory has no `research/` folder
at all. Two separate sessions hit that wall tonight and both regenerated from
scratch. User-level memory is out of my reach, so it is recorded as a
needs-user item with the two documents that supersede them.
The Campaign V page said V8 was running, V9 pending and V10/V11 ahead. All four
have landed. Its status section now records the deletion (204 files, +1,870 /
-27,607), the three findings worth keeping — Chorizite survives on the pak
format rather than on `IUniformBuffer`, `Studio/SampleData.cs` was production
code, and `WbMeshAdapter.Dispose()`'s GPU-work wait had been silently dead since
V6a — and, plainly, that the runtime gates did not run because this machine
cannot create a Win32 Vulkan surface for any process. A gate that could not run
is not a gate that passed. The #248 and #250 entries are updated to DONE with
their outstanding halves named.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first commit converted the four members the issue named and left the other
sites alone, reasoning that none had been observed failing. A 20-run
complete-solution baseline disproved that within minutes:
run 2 LiveEntityRuntimeTests.AnimationView_HotSpatialTraversal…
run 14 StaticRenderProjectionJournalTests.ActiveAnimatedSynchronization…
run 18 StaticRenderProjectionJournalTests.ActiveAnimatedSynchronization…
run 19 CurrentRenderSceneOracleTests.SurfaceOverrideFingerprint…
Both new names are the same shape as the four — one warm call, then a
thousand-iteration loop inside the measured window — and neither had been
recorded anywhere. "Not observed failing" only ever meant "not yet observed",
and leaving known-shape sites in place would have guaranteed the acceptance gate
failed. Run 19 is the sharper lesson: the issue named
`SurfaceOverrideFingerprint_DictionaryHotPathAllocatesNothing`, and the first
commit converted a *different* test in that same file, so the actually-named
member was still on the old shape. Matching by file was not matching by test.
Every strict-zero site in the assembly is now on the probe — ten tests. Two came
out stricter rather than merely steadier:
`StaticRenderProjectionJournalTests` was measuring a synchronise whose journal
does **not** coalesce. Repeating it grew the journal by 1,000 entries per call —
192,000 by the end of a probe run — so the steady state the test claimed to
measure did not exist and the single-call window had been hiding it. Its step is
now the whole frame cycle, synchronise *and* drain, which puts `DrainTo` inside
the measured window for the first time and asserts the journal ends empty.
`RetailInboundEventDispatcherTests` asserted a hard-coded 1,001 callbacks. It
now counts its own dispatches and pins the callback count against that, so the
assertion still proves the fast path ran the callback every time without being
coupled to a loop bound that no longer exists.
Left alone deliberately: the four sites asserting a tolerance rather than zero —
`CellViewDedupTests` and `PortalProjectionTests`. Their ceilings already absorb
this noise and none has flaked; changing a bound in either direction is a
separate decision from fixing a measurement. Worth noting that
`PortalProjectionTests`' ceiling exists explicitly to tolerate "a
tiered-JIT/ArrayPool bookkeeping transition ... to the first measured batch",
which is exactly what the probe removes, so it could probably be tightened to
zero now — recorded in the issue rather than done here.
Solution build 0 warnings / 0 errors; App suite 3,941 passed / 3 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The zero-allocation family failed about one full-suite run in three, on
unchanged trees, and had been dismissed as inherent noise in
`GC.GetAllocatedBytesForCurrentThread` three separate times. It is not noise.
Reading the four members side by side, they share one root: **the measured
window was never the warmed path.**
UiDatFontTests 1 warm call, then a 10,000-iteration loop inline
RenderFrameProductTests 8 warm calls, then a 1,000-iteration loop inline
OracleTests 1 warm call, 1 measured call
ArchRenderSceneTests warms Apply(registrations), measures Apply(updates)
Two mechanisms come out of that table. A test method is JIT-compiled at tier 0
like anything else, and a long-running loop in tier-0 code gets replaced
mid-flight by on-stack replacement — which compiles on the thread running the
loop, so its bookkeeping is charged to the window being measured. That is the
first two. And `ArchRenderSceneTests` warmed one arm of a switch and measured
the other, so the measured call was the first ever into `ApplyUpdate` and paid
that arm's JIT, type loads and static initialisation inside the window;
`RenderFrameProductTests` warmed 8 times, below the tier-0 call-counting
threshold of 30, so promotion was still pending when measurement began.
That also explains the signature nobody could account for. Alone, the process is
quiet and the runtime has finished before the assertion arrives. Alongside eight
other test assemblies, tier-0 compilation never stops, the call-counting delay is
re-armed continually, and the work slides into the window. Clean in isolation,
failing under load, on a tree that changed nothing.
`ZeroAllocationProbe` invokes the step many times before measuring anything, then
measures windows that run the same already-warmed loop over the same
already-taken path. Each window is a batch of 32 invocations and it reports the
minimum across 4 of them. Both halves are load-bearing: the minimum is what
excludes a one-time cost, and the batch is what keeps the assertion as strong as
the loops it replaces — minimising over *single* invocations would report zero
for a path that allocates every tenth call, which is a real regression made
invisible. I had written it that way first and the apparatus test caught it.
**The bound is untouched: exactly zero, no tolerance, no retry, no assertion
relaxed.** `ZeroAllocationProbeTests` proves the apparatus can still fail — a
step allocating every call reads above zero and does throw, a first-invocation
cost reads as zero, a cost every tenth call is caught, and the one stated limit
(the batch must cover the period) is pinned as a test rather than left as prose.
Without those, a later edit could quietly make the whole family unfailable.
Twelve further sites in this assembly still use the hand-rolled shape. None has
been observed failing, and each needs its own repeatability analysis — several
mutate state or consume monotonic sequences — so they are listed in the issue
for adoption when next touched rather than converted blind at scale.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`FrustumPlanes.FromViewProjection` extracted the near plane with the
Gribb-Hartmann form written for OpenGL's `[-1,1]` clip-space z range. Every
acdream projection comes from `Matrix4x4.CreatePerspectiveFieldOfView` or
`CreateOrthographic`, whose range is `[0,1]`. Under `[-1,1]` the near plane is
the locus of `clip.z = -clip.w`, which is `col4 + col3`; under `[0,1]` it is
`clip.z = 0`, which is `col3` alone.
Concretely, the mismatch put the effective near threshold at `-n·f/(2f-n)` —
about 0.5 m where the retail chase camera asks for 1.0 m. That error only ever
kept geometry the true frustum would have dropped, never the reverse, which is
why it produced no visible defect and was filed instead of hot-fixed during
Campaign V. It is still wrong, and it is the same mistake that *was* visible in
`PortalProjection`, where it culled the cell behind a doorway the camera stood
close to.
The far plane is `col4 - col3` under both conventions and is untouched. A test
pins it anyway, so that a future edit to this function cannot drift it while
nobody is looking.
The acceptance criterion asked for a unit test pinning the extracted near
distance to the camera's near value, and that is what landed: a theory over four
near/far pairs asserting the plane is unit-length, faces down -Z, and stands off
the eye by exactly `nearDistance`, plus a kept/dropped pair straddling it. The
test was checked against the old formula before commit and fails all four cases
there — it measures the fix rather than merely accompanying it.
The other half of the acceptance criterion — unchanged culling in the offline
pixel gate and the connected route — could not be run: #259 has Win32 surface
creation failing machine-wide, so no gate that needs a window is available
tonight. Recorded as outstanding rather than assumed.
Solution build 0 errors; `AcDream.Core.Tests` 3,898 passed / 2 skipped / 3,900.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brings `github/overnight/wire-audit` (`41f74fcd`) forward onto the V11 tree.
Like the enum branch it was cut at `b70b9832`, and like the enum branch its
subject is disjoint from the deletion: the audit lives in `AcDream.Core.Net`
and its tests, V11 emptied `AcDream.App`. One conflict, in `docs/ISSUES.md`,
resolved below.
What it carries: three real parser fixes — ranged speech was carrying a range
float the parser ate, a chat type that is never sent was silently dropping every
transient string on it, and `xpSpent` is a dword on the wire where we were
writing eight bytes. Plus the transport flag word pinned against ACE across all
twenty-three bits, golden fixtures generated from ACE's own writer instead of
hand-typed hex, and the audit document covering all three hundred forty-nine
opcodes.
**The conflict, and how it was resolved.** Both this branch and V11's closeout
reopened#255 — the RetailDatLoader concurrency tests that measure the thread
pool rather than the loader — on the same day, from different trees, without
knowing about each other. Neither reopening is a duplicate of the other: the
V11 gate saw 2 failures in 5 complete-solution Release runs on the
post-deletion tree, the audit session saw 2 in 4 on the pre-deletion tree, and
both saw 124/124 in isolation every time. They independently reached the same
conclusion, that `TaskCreationOptions.LongRunning` is a hint rather than a
guarantee, and independently proposed the same fix, a rendezvous inside the read
stub.
So the two notes are merged into one issue with both evidence sets kept as
labelled subsections rather than one overwriting the other. Four failures across
nine runs on two trees is a materially stronger case than either half, and the
agreement between two blind observations is the part worth preserving. No
assertion was weakened and no retry was added; the fix itself remains open.
Verified on the merge result: Release build 0 errors, and
`AcDream.Core.Net.Tests` at 659 passed / 0 skipped, up exactly the 59 the branch
claimed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brings `github/overnight/enums` (`c19680fd`) forward onto the V11 tree. The
branch was cut at `b70b9832`, before the OpenGL deletion, and the two lines of
work turned out to be disjoint: the enum campaign lives entirely in
`AcDream.Core` and its tests, while V11 emptied `AcDream.App`. The merge is
clean — no conflicting file on either side.
What it carries: names for AC's seven property tables verified against two
oracles, a correction to `DamageType`'s rotated bits and `ItemType`'s shifted
craft ladder, the retail members the equipment and physics enums were missing,
and names for `AmmoType`, `CombatUse` and `ItemUseable`. Five commits, seventeen
files, +3,767 / -27 lines.
Verified on the merge result rather than on the branch: Release build 0 errors,
no new warning attributable to any file the branch touches, and
`AcDream.Core.Tests` at 3,893 passed / 2 skipped / 3,895. The campaign's
claimed +597 is exact — the `Properties` namespace alone runs 597 tests, all
passing.
The campaign's open decision items — whether to adopt `WeenieError` wholesale,
whether the `SoundId` subset is the right cut, and the re-clone of the ACE and
Chorizite references that `references/` no longer holds — are not settled here.
They are carried into the morning report as questions for the user.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records what V11 actually achieved and, more importantly, what it did not.
The deletion landed: 204 files, +1,870 / -27,607 lines across five commits.
Section 5.5.24 keeps the three findings that outlive the diff.
* Chorizite could NOT be dropped, and not for the reason section 6 predicted.
The risk register assumed the package survived only because the ManagedGL
types implemented IUniformBuffer from it. The audit found TextureFormat in
the IWorldTextureArray signature the VULKAN path implements, and
BoundingBox serialized into the pak format. Dropping it is a slice that
touches the on-disk format, not a V11 cleanup.
* Two traps the V11 row did not know about. Studio/SampleData.cs is
production code behind the character sheet's fallback, so it moved rather
than died; ACDREAM_DEVTOOLS also gates Vulkan debug-utils, so the flag
survives and now says out loud that its UI is gone.
* Deleting GL surfaced a real bug: WbMeshAdapter.Dispose() was still
pattern-matching the GpuFrameFlightController that V6a replaced, so its
wait for submitted GPU work had been silently dead on every Vulkan run
since. Removing the type turned a no-op into a compile error.
The runtime gates did not run, and the honest reason is written down rather
than smoothed over. The client dies at vkGetPhysicalDeviceSurfaceCapabilitiesKHR
in files V11 never touched. Bisecting put the failure at the PRE-V11 commit
whose Vulkan soak had passed 91 checkpoints three hours earlier, and
`vulkaninfo --summary` -- a Khronos tool with none of our code -- fails at the
same call. Win32 surface creation is broken machine-wide; Vulkan itself is
fine. That is issue #259, with the one-line diagnosis at the top so the next
person checks the machine before bisecting the tree.
So the row reads DELETED AND STATICALLY GREEN, RUNTIME GATES BLOCKED. Release
build is 0/0 and the complete Release suite is 8,999 / 5 skipped (-218 against
V10, every one a test that lost its subject). Nothing was relaxed to
manufacture a pass: section 7.1 rule 2 cuts both ways, and a gate that could
not run is not a gate that passed. The rerun list is in 5.5.24, and the
pre-deletion pixel baseline was captured BEFORE the deletion, so the
self-differential is still available whenever a window can be made again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retires the GL framing from the documents that described a two-backend,
two-UI-stack client, and files what the deletion left behind.
Divergence register:
* AD-46 (anisotropic tap pattern in dense alpha scenery) is REFRAMED rather
than retired. Its substance survives -- distant foliage may read denser
than retail's -- but it was measured GL-vs-Vulkan, and with GL gone it is
a Vulkan-vs-retail question against the D3D oracle it already cited. The
measurement is kept as the evidence that the residual is a driver tap
pattern; the row now records that it is no longer falsifiable by
self-differential, which is a real loss the deletion causes.
* AD-47 and AD-48 are NEW, and the campaign's own risk register scheduled
them here: MSAA sample positions (measured at 8.83% of the frame at 4x,
which is why every strict gate runs MSAA off -- and therefore why a
regression confined to the multisample path would not be caught) and
present pacing (#235 is the live instance).
* AD-17's justification moves from a GL clip-plane citation to Vulkan's
maxClipDistances floor, which is the same 8, so the divergence is
unchanged and only its authority moves.
* AP-92 keeps IUiViewportRenderer.TextureIsBottomUp rather than folding it
flat, because it is what let the origin question be answered by data.
Architecture and code structure: the layer diagram, the frame order, the
residency vocabulary and the reference table all said OpenGL. The UI section
said two stacks. Rule 3's rationale is rewritten around what actually
happened -- ImGui was deleted and not one panel, ViewModel or command had to
change, because none of them had ever imported ImGuiNET. That is the rule
paying for itself, so it is recorded as evidence rather than removed as
obsolete.
Issues: #258 files the dev-panel host as a decision rather than an accident,
and #255 is REOPENED. Its TaskCreationOptions.LongRunning fix asks the
scheduler for a thread but does not promise two callbacks overlap; under nine
concurrent test assemblies it still failed 2 of 5 whole-suite runs. The
earlier evidence tested a narrower pool, not a contended one. The fix it
needs is a rendezvous inside the read stub -- not a weakened assertion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wire-stack audit's final full-suite gate turned up
AnimationCache_CoalescesSameDidAndAllowsUnrelatedReadsInParallel failing twice
in four runs on Windows, on a tree that already carries #255's LongRunning fix.
Run alone the project passes 124 of 124 every time. Both failures were
--no-build runs, which start faster and so crowd the machine harder, which is
the original diagnosis rather than a new one.
TaskCreationOptions.LongRunning asks for a dedicated thread. It does not
promise one, and it does nothing about the other eight test projects saturating
the box while these two forty-millisecond sleeps are supposed to overlap. The
assertion on MaxConcurrentReads is therefore still measuring the host.
The note suggests the shape that would actually close it: gate both threads on
a barrier so each is provably inside the read before either is released. Then
the count is a property of the loader instead of the scheduler and no amount of
load can move it. Filed rather than fixed because this session's diff is
confined to Core.Net, its tests, and docs, and the audit had no business
editing content loaders on its way past.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bucket B item 2 of the post-Campaign-V intake, delivered as a ledger. Every
entry in ACE's three opcode enums gets a row grounded in acdream source read
during the audit, with a file:line citation, compared field by field against
ACE's serializer or handler. The prior status columns were deliberately not
used as input; they are compared against the result instead.
Two blockers lead the document because they outlast it. The vendored reference
repos are empty directories in the main checkout, not just in worktrees, so
CLAUDE.md's instruction to cross-reference at least two of them is currently
unexecutable as written; working copies were recovered elsewhere on disk and
Chorizite.ACProtocol was not found anywhere. And the 2026-06-04 wire catalog
this audit was meant to diff against was never committed and does not exist on
disk, though MEMORY.md still links to it. The row universe therefore comes from
ACE's enums, which is more complete than the missing catalog's 256, and drift
is measured against the in-repo Phase-M matrix instead.
The corpus could not support the frequency ranking the mission asked for.
ACDREAM_DUMP_OPCODES was not set for any recorded session, so there are zero
unhandled-opcode lines across every log. That is reported rather than papered
over, and it is why no new parsers were written for unobserved gaps: the bar
was observed plus two oracles, and the first half was unsatisfiable. One
instrumented re-run would fix this, and it leads the open questions.
The most useful thing the audit found is a pattern rather than a single bug.
Three confirmed wire divergences each had a passing test built on the same
wrong assumption as the parser it was defending. The Phase-M matrix had marked
all three rows complete, and for ranged speech it even recorded the mistake as
a design note, "same parser as 0x02BB". A status column can be wrong on the day
it is written.
Counts: 130 parsed or built-and-wired, 11 partial, 75 stub or dead-builder, 128
missing, 5 server-side, across 349 rows. The stub block is concentrated in
fellowship, housing, trade, allegiance, and vendor, which is exactly the surface
the intake's bucket-C items will need.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ledger the campaign owed: which oracles were actually available, what each
family's end state is, what got fixed and why, and - the part that matters most
for whoever picks this up - the twelve things that could not be settled from an
oracle and are therefore recorded as open questions rather than guessed.
Two findings deserve to survive past the morning report.
The first is that five of the six vendored reference repos named in CLAUDE.md are
empty directories in this environment. ACE, Chorizite, holtburger, ACViewer, AC2D
and DatReaderWriter contain nothing, so the campaign re-anchored on the retail
header itself - which CLAUDE.md ranks above ACE anyway - with the UtilityBelt
enum catalog and the 38,985-file ACE weenie corpus as cross-checks. That turned
out to be the more rigorous arrangement rather than a compromise, because of the
second finding: the catalog is wrong about CraftFletchingBase, where retail and
acdream agree. Trusting any single source, including the one the brief nominated,
would have introduced a bug. Retail's header decided every disagreement and the
weenie corpus broke ties.
Also recorded: the 2026-06-04 property-enum divergence note that this work was
supposed to build on does not exist - not in the tree, not under any ref, not in
the memory directory, which has no research/ subfolder at all. The MEMORY.md index
points at both it and a magic-number audit that is equally absent. The sweep was
regenerated from scratch instead, and landed on 864 property members against the
missing note's remembered 929. Someone should repoint those index entries.
The Bucket B intake row is marked done and points here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Twelve user-provided forward items sorted into three buckets: three land on already-staged work (equipped-child picking and vendor slices are the world-interaction program's own next steps; the Settings tab is the filed V11 dev-panels follow-up), three are verification campaigns whose research already exists (property-enum divergence doc, wire-message catalog, the retail physics workflow - starting with the observed long-jump landing bounce), and six are new feature bodies for milestone sequencing (login/char creation, summoning, fellowship/allegiance tabs, in-game map, chat color/text fidelity from the cdb-captured retail values, missing slash commands). Intake ledger only; sequencing happens in the roadmap after V11 closes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Issues #256 (server-spawned signs and portals go invisible after repeated
portal runs, while staying interactive) and #257 (working set grows to
~1.5 GB over the same session) were observed together in one long live
Vulkan session, and both filings demanded the same thing before V11 deletes
the OpenGL backend: run the churn on GL too. Vulkan-only growth or drift
would mean the new arm's resource lifecycle is broken, and deleting its only
reference implementation while that was true would be wrong even with the
cutover signed.
So the discriminator is built and run first, and it can stop the slice.
tools/run-portal-churn-soak.ps1 generates a route of N cycles over three
portal-bearing stops taken from the two existing connected routes, runs it
once per backend from one binary, and measures three things the existing
instruments do not measure together:
* working set and private bytes, sampled from the OS every two seconds and
joined to each checkpoint by timestamp -- the client's own snapshot has
no view of its own working set, which is exactly #257's quantity;
* the published-versus-live pair already in the checkpoint JSON, because
"alive in the object table, gone from the presentation" is #256's whole
symptom and a drift between those halves at the SAME stop across cycles
is what would show it;
* a within-arm capture comparison -- cycle 1 against cycles 10, 20 and 30
at a pinned viewpoint -- plus a difference map and a row histogram,
because a number cannot tell an absent object from a walking NPC and the
map can.
Every teleloc carries the identity quaternion so the heading repeats, and
the four determinism levers the differential gate forces are forced here for
the same reason: an unpinned sun would swamp the signal.
Result at 90 transits per arm, 91 checkpoints, zero errors, graceful exits:
neither arm reproduces either symptom. Working set means agree to 1 MiB
(GL 1864, VK 1863) and warm-half drift is NEGATIVE on both (-48.0, -27.0).
GPU accounting is exactly constant per arm. worldEntities holds 10,382 at
all thirty cycles on both. The difference maps show every building, the
portal, the statue and the treeline still drawn at cycle 30.
That refutes the one outcome that would have blocked V11, and it does not
identify the pre-existing bug -- so both issues stay OPEN with the negative
recorded, and the follow-up named: walked portal transits rather than
/teleloc, which do not take the same path into the transit state machine.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
THIS CUTOVER AWAITS THE USER'S VISUAL SIGN-OFF. It is not complete. Section 7
of the campaign plan names the V10 sign-off as the only required user stop
besides gate failures, and it has not been given. This commit flips the default
and runs the battery so that the sign-off has evidence in front of it.
ROLLBACK, one line: `git revert` of this commit. It restores the GL default,
the pre-V10 escape-hatch polarity and the gate scripts' inherited backend
together; nothing else has to move with it.
An unset, empty or unrecognised ACDREAM_RENDER_BACKEND now yields
RenderBackendKind.Vulkan. Only `gl` or `opengl`, case-insensitive, selects
OpenGL. The polarity of the typo case flipped with the default and on purpose:
before V10 an unrecognised token had to land on GL because Vulkan was dark and a
typo must never silently start a backend that cannot draw; after V10 it has to
land on Vulkan for the same reason read the other way, because GL is the backend
V11 deletes. `opengl` is honoured beside `gl` because an escape hatch exists to
be found.
Three gate scripts follow the flip. run-offline-pixel-gate.ps1 gains -Backend
(default vulkan) and now FORCES all four determinism levers — backend, day
group, world day fraction, sky phase — plus ACDREAM_MSAA_SAMPLES=0, instead of
inheriting any of them. run-repeat-connected-gate.ps1 and
run-connected-world-lifecycle-gate.ps1 CLEAR ACDREAM_RENDER_BACKEND rather than
setting it, so what they exercise is the process default and an ambient override
in a caller's shell cannot make a GL run wear the default's report.
TEST PIN UPDATED, flagged as required: RenderBackend_DefaultsToGl becomes
RenderBackend_DefaultsToVulkan, and RenderBackend_AnythingElseStaysOnGl splits
into RenderBackend_SelectsGlOnlyForTheEscapeHatchTokens and
RenderBackend_AnythingElseStaysOnVulkan. Five cases replace two. No other test
is touched, weakened or deleted.
AD-46's divergence-register row moves from "dormant until the V10 cutover" to
live, in this commit, per the same-commit register rule.
Battery, all on the new default:
complete Release suite 9,222 passed / 5 skipped / 0 failed (9 projects)
+5 against the pre-flip 9,217; the +5 are this
slice's own escape-hatch cases
#250 family, singly 4/4 pass (none failed in the whole-suite run)
repeat connected gate PASS 3/3 on both columns
world-lifecycle route PASS, 0 failures, both sessions graceful at exit 0
validation layer inserted at instance AND device level by the loader,
zero errors and zero warnings, real frame captured
GL escape hatch verified by two offline launches: 4.3.0 Core Profile
Context, bindless present, exit 0
Every connected launch in the battery reached Vulkan with no environment
variable set, which is the flip itself under test rather than an assertion
about it.
THE PIXEL GATE IS NOT MET, AND WAS NOT RELAXED. Vulkan against a GL-era capture
taken at this commit through the escape hatch, MSAA off and both clocks pinned:
1.099e-03 masked / 3.764e-02 whole-frame, against a 0.001 threshold. 97.9% of
the difference is in the treeline band, and the masked residual of 619 px — set
against a same-backend control of 10 px — sits entirely on the silhouettes of
distant alpha-blended scenery. That is AD-46's registered population; section
5.5.19 measured the same quantity at 497 px / 8.8e-04. Below the band the two
backends are photometrically identical: mean luminance differs by 0.01 of 255.
No baseline was regenerated and no mask or tolerance was widened.
Two instrument findings are recorded in section 5.5.23. The offline gate's sky
mask is still load-bearing — this slice tried retiring it on the reasoning that
V7's clock pins had made it obsolete, and the control refuted that: two launches
of the same binary still differ by 1,011 px on GL and 482 px on Vulkan, almost
all of it in the band. The default went back to 280 with the measurement written
into the script's help. And the repeat gate's desktop witness needs an
uncontested primary monitor: a first attempt reported 1/3, and the two failing
grabs turn out to be a web browser and Discord composited over the client rect,
not a blank frame — the client's Vulkan capture rendered in all six runs.
Nothing GL, ImGui or Studio is deleted. That is V11's scope and it is untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user played a full session on Coldeve (production ACE fork) on the Vulkan backend and accepted V10: it felt good, graceful logout confirmed, two findings filed rather than blocking. #256: server-spawned signs and portals in the town portal network went invisible after repeated transits while remaining interactive - presentation-half, not wire. #257: working set ballooned to ~1.5 GB over the session against V8's ~880 MiB stationary measurement - a per-transit accumulation profile V8's vehicles never exercised. Both carry the same pre-V11 discriminator: run the identical portal-churn route on the GL backend while it still exists, because after V11 deletes it, backend attribution costs far more.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fork at 5.5.5 dissolved V4h: the Vulkan arm got real declared passes at V6h/V6i-3/V6m, the frame plumbing crossed at V4a/V6g/V8, and the GL spine deliberately keeps its legacy shape until V11 deletes it. What is left - OpenGLGraphicsDevice retirement, the Chorizite audit, the architecture test that goes vacuous at deletion - was always V11 work. Closing the row stops the slice table implying outstanding seam work that no longer exists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The row has been carrying "implemented, first CI run pending" since the
slice landed, because a job that has never run is not evidence of
anything. Run 30393357552 is that evidence: all four jobs green, lavapipe
reporting llvmpipe (Cpu) at Vulkan 1.4.318 on Mesa 25.2.8, a 1280x720
35,594-byte captured frame, and the freshness step printing "all
committed .spv match a fresh compile" on the second operating system.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixing the two failures that were stopping portable-headless earlier let
the job reach AcDream.Content.Tests for the first time on either
operating system - the test loop exits on the first failing project, so
the windows leg had never got past the apt step and the ubuntu leg had
never got past Core.Net. Two RetailDatLoaderTests cases were waiting
there, and they failed on both.
Both assert on RawDatabase.MaxConcurrentReads after issuing two Task.Run
reads that each block 40 ms in Thread.Sleep. A pair of pool work items is
not a guarantee of two workers in flight: on a low-core or saturated pool
the second queues behind the first, the reads run back to back,
MaxConcurrentReads stays 1, and the assertion fails for a reason that has
nothing to do with the loader. Pinning the suite to two CPUs on Ubuntu
reproduces it 5 times in 6; Windows is clean 6 of 6 at sixteen cores,
which is why nobody had seen it.
The pairs now start with TaskCreationOptions.LongRunning on the default
scheduler, which asks for a thread each. No assertion is changed - they
still fail if the loader serialises. The two coalescing cases moved onto
the same helper on purpose: two callers genuinely in flight is the
situation coalescing exists for, and a sequential pair was only ever
exercising a cache hit. Ten of ten clean under the same pin.
Release build green. App tests 4,152 / 3 skipped. Content 124 / 124.
Filed as #255.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The lavapipe job did the thing it was built to do on its first attempt.
It accepted a Cpu device at API 1.4, created a device and read pixels
back, captured a real frame, and exited 4 when a feature was forced
unsupported. Three other things were red, and none of them were the
Vulkan backend.
The shader-freshness step aborted for two separate Linux faults in the
compiler tool. Disposing the Silk.NET API container unloads the native
module, and dlclose-ing libshaderc_shared.so leaves glslang's
process-level teardown running against unmapped code. Bisected with a
four-mode probe on Ubuntu 24.04: GetApi, CompilerInitialize and
CompilerRelease each exit 0, and adding only the container Dispose turns
the exit into SIGSEGV. That is the 134 CI reported. shaderc's own handles
are still released; the container is not, because the module's lifetime
is the process's and the process is one statement from returning.
Separately, a portable dotnet build leaves the native under
runtimes/linux-x64/native/ and makes reaching it Silk.NET's probing
problem, which it solved on a local Ubuntu 24.04 and did not solve on the
runner. The script now publishes the tool for the host RID, so the native
sits beside the assembly where AppContext.BaseDirectory finds it, and
checks for it by name so a regression says which file is missing rather
than which names failed.
With both fixed, the question section 5.5.20 left open has an answer:
Linux shaderc and Windows shaderc agree byte-for-byte at the pinned Silk
2.23.0. Eighteen of eighteen .spv identical, manifest identical. The byte
comparison stays a byte comparison.
The Windows leg of portable-headless was running sudo apt-get. That step
is older than this campaign - it is red in the 2026-07-27 main run too -
and it was misplaced rather than mis-conditioned. Nothing in that job
opens a display or links GL, and the graphical jobs that do call xvfb-run
take it from the runner image, so the step is deleted rather than
guarded. Every remaining step in the two-operating-system matrix is pwsh;
every bash step now lives in an ubuntu-only job.
The last failure was ours in a quieter way. WaitForCharacterLogOff-
Confirmation expressed its deadline only as a CancellationTokenSource,
whose timeout is published from a thread-pool timer callback, so on a
saturated pool the token stays unsignalled past the deadline while the
loop keeps draining items that are already queued. That is the case the
method exists to bound. Reproduced by pinning the suite to two CPUs on
Linux, which failed 2 of 6 where four CPUs and sixteen were clean, and
where CI failed 3 of 3. The drain now reads the deadline off the
monotonic clock as well; the token still bounds the asynchronous wait.
Ten of ten clean under the same pin. The test is untouched. Filed as
Release build green. App tests 4,152 / 3 skipped against the same 4,152 /
3 measured at base 32f9bcfa. Core.Net 600 / 600.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The verdict splits, and not where anyone expected. Measured on one machine, one
build, one day, physical console, uncapped, 4x MSAA on both arms, validation off:
Stationary LIGHT scene (6,675 entities, 780-870 FPS)
CPU p50 GL 1.127 ms -> VK 1.294 ms MISS +14.8%
CPU p99 GL 1.407 ms -> VK 1.531 ms MISS +8.8%
GPU p50 GL 0.651 ms -> VK 0.160 ms PASS -75.4%
Alloc/frm GL 77,664 B -> VK 11,440 B PASS -85.3%
Working GL 943.7 MiB -> VK 877.1 MiB PASS -7.0%
Stationary DENSE scene (21,024 entities, identical on both arms)
CPU p50 GL 5.934 ms -> VK 5.775 ms PASS -2.7%
CPU p99 GL 8.867 ms -> VK 7.354 ms PASS -17.1%
GPU p50 GL 1.673 ms -> VK 0.909 ms PASS -45.7%
Alloc/frm GL 82,016 B -> VK 15,752 B PASS -80.8%
Process CPU GL 1.246 cores -> VK 1.016 PASS -18.5% (Windows, not ours)
Canonical nine-stop route, identical world at all nine stops
Frames GL 30,378 -> VK 38,683 PASS +27.3%
CPU p50 GL 11.718 ms -> VK 9.166 ms PASS -21.8%
GPU p99 GL 2.325 ms -> VK 1.193 ms PASS -48.7%
Vulkan loses two rows in exactly one configuration: a stationary field at a frame
rate no player will ever see. The reason is measured rather than argued. A
temporary probe on BOTH arms, now stripped, attributes 0.148 ms/frame to required
Vulkan WSI and synchronisation calls - vkQueuePresentKHR 0.070, vkQueueSubmit2
0.027, the timeline wait 0.026, vkAcquireNextImageKHR 0.025 - against roughly
0.014 ms for GL's whole SwapBuffers. That cost is FIXED per frame, so it is 12%
of a 1.13 ms frame, 2.5% of a 5.9 ms one and under 1% of a dense-town frame,
while the GPU and allocation savings scale with the work. The sign of the CPU
comparison flips as soon as the frame contains a town.
The campaign's named cost centre is closed rather than carried a fourth time.
Bindings 4, 6, 7 and 8 costing a descriptor write per draw - forward-carried
since V6i-3 as the thing to fix if CPU were short - measures 0.031 ms for ALL
~216 draws of the frame, about 140 ns each and 2.4% of it. No Vulkan code was
changed to chase the miss: every lever the V8 row named was already taken
(coherent rings, one submit per frame), irrelevant to p50 (pipeline pre-warm),
measured and small (descriptors), or would have traded real memory for nothing
(a fourth swapchain image, when acquire is call cost and not waiting).
The methodological finding is worth reading before the numbers. The R6 soak is
NOT the vehicle the founding numbers came from - the G5 production profile states
its own conditions and they exclude the probe, the artifact owner and the
screenshot oracle - and it is biased AGAINST Vulkan, because
VulkanGraphicsContext arms retainBackbufferCapture exactly when
ACDREAM_AUTOMATION_ARTIFACT_DIR is set, making every Vulkan frame copy the whole
swapchain image while GL reads on demand. On one binary in one hour the soak
reports CPU p50 7.3 ms and 2,531 KiB/frame where the ordinary profile reports
1.13 ms and 77 KiB. The route table above is therefore conservative in Vulkan's
favour: it wins on the vehicle that charges it extra.
Gates: Release build green; App tests 4,152 / 3 skipped, the pre-slice baseline,
no #250-family failure; strict GL offline pixel gate against 13c8733d at 1.95e-05
(11 px of 563,200), inside the 9-31 band, so GL did not move; one connected
Vulkan run with VK_LAYER_KHRONOS_validation proven inserted by the loader at zero
errors and zero warnings; and BOTH R6 soaks green - Vulkan 506.6 s and GL 506.8 s,
zero failures, graceful exits - which discharges the soak half of V7's
outstanding list. RenderDoc is not installed on this machine, so that capture
carries to V10 with a cause rather than as an omission.
The recommendation: proceed to V10 and amend the acceptance table rather than
waive it, naming the scene and pacing the floor is judged at. Two natural
candidates are already in the evidence and Vulkan passes both outright. The
opposite reading - that the light-scene rows disqualify the cutover - is
available and has been given the same measurement space. That call is the user's
and this slice does not make it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Synthesizes the OpenGL-to-Vulkan migration for viewing on github.com: the
screenshot sequence from first Vulkan content through the Dereth world, sky,
portal tunnel and paperdoll; the six-cycle blank-world investigation that
falsified five mechanisms and broke four instruments before the interleaved
A/B probe and the 10/10 NVIDIA cross-vendor run pinned the defect to the AMD
GL driver; the V7 differential verdict tables, including the offline
GL-versus-Vulkan pair at 8.82e-04 inside threshold; the five compiled-clean-
but-still-wrong defects the migration's own pixel gates caught; and the
current V8/V9/V10/V11 status. Images are copied and renamed from the
untracked artifacts/ tree under docs/reports/assets/campaign-v/.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user's connected session on the Vulkan backend passed all six targeted checks with merits: the AD-46 treeline (no visible shimmer or moire - the register row's human-visibility question is answered as invisible), dyed gear composites, doorway look-out and threshold crossing, stacked translucency, icon/text crispness, and motion feel. This was the first human play session on the Vulkan renderer.
One finding, filed as #253: attribute/skill icons in the character sheet sit off-center in their cells and render fully opaque where retail uses vitae-window-like translucency. The V7 differential holds the retained UI identical between backends, so this is suspected pre-existing D.2b fidelity rather than a campaign regression - one GL-side look confirms, then it is post-campaign scope.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first CI job in this project's history that renders a frame.
The whole row rests on a decision V6g already made and paid for. When
section 5.5.8 cut set 0 from ten dynamic storage descriptors to four, four
was not merely under the RX 9070 XT's eight - it is Vulkan's guaranteed
minimum, so no conformant device can fail the layout. That is what makes a
software-device row possible at all. Every other requirement was then
checked against Mesa's lvp_device.c rather than assumed, and all seventeen
features the gate demands are true on lavapipe - including
samplerAnisotropy, which V7 made load-bearing eight commits ago and which a
software rasterizer would have been entirely within its rights to decline.
Three things had to exist before the job could:
1. The harness could not stop. VulkanBringUpHost presents until its window
closes, which is right at a desk and impossible in CI, where nothing ever
closes a window. ACDREAM_VULKAN_PROBE_FRAMES gives it a budget; unset or
malformed is zero, which keeps the interactive behaviour, so no existing
invocation changes. The budget never cuts the capture short - the loop
stays open until the screenshot has been attempted - because a run whose
entire product is a PNG must not be able to exit green with an empty
artifact directory. The decision is a pure static method, tested without
a window or a driver.
2. tools/compile-shaders.ps1 was Windows-only and nobody had noticed,
because nothing had ever run it anywhere else. It built its paths from
embedded 'src\AcDream.App\...' literals; a backslash is a separator on
Windows and an ordinary filename character everywhere else, so on Linux
that is one long nonexistent file name.
3. The report's jq paths were invisible to the compiler. Renaming a record
property or swapping the enum converter would have left every test green
and turned CI red on someone else's branch days later, with a failure
that reads like a driver problem. VulkanCapabilityReportContractTests
pins the exact strings the job greps and pins its packed-version
arithmetic against VulkanApiVersion's own unpacking.
The job, eleven steps: install lavapipe and Xvfb; record vulkaninfo as
evidence; publish linux-x64; run the Gpu.Vk tests on a second operating
system; probe the gate under a 24-bit Xvfb screen (the default is 8-bit,
which leaves the X11 WSI without a usable visual) and assert an accepting
verdict on a Cpu device at API >= 1.3 with a clean active probe; assert the
captured PNG is a real frame by IHDR dimensions and byte count; re-run with
ACDREAM_VULKAN_FORCE_UNSUPPORTED=timelineSemaphore and assert exit 4 with an
actionable refusal; recompile the shaders and compare. Artifacts upload on
always(), so a red run ships its own diagnosis.
The .spv step is what ties the committed binaries to their sources. The
existing App test hashes GLSL against the manifest, which catches "edited a
shader, forgot to recompile"; nothing caught a stale or hand-edited .spv.
Verified on Windows before shipping: 19/19 artifacts byte-identical to a
fresh compile, zero drift.
No GL-versus-Vulkan pixel compare, for two independent reasons recorded in
section 5.5.20: linux-graphical asserts exit 4, so there is no left-hand
side, and the probe renders synthetic scenes rather than the DAT world CI
cannot have. The two jobs now say something sharper than a pixel diff would
have - on the same software Mesa stack, GL is refused and Vulkan is accepted
and draws. Physical Linux GPU and Wayland rows stay deferred on the Slice L
precedent; no hosted runner offers either.
Gates: Release build green, zero errors. App tests 4,152 / 3 skipped against
a 4,134 / 3 baseline at this branch's base (9b7f4343) - eighteen new, all
from this slice. Workflow validated by a real YAML parse plus an Actions
schema check and bash -n over all nine extracted run blocks; no actionlint
was available locally and none was downloaded. The job itself has not run:
its first execution is the CI run this commit triggers, and the V9 row stays
partial until that is green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TransformUpdateBatch_ReusesRetainedStorage failed in two of six consecutive
whole-suite Release runs on a docs-only tree during Campaign V slice V7, and
passed 12/12 with its class run alone. Its assertion is the same shape as the
three already listed -- a GC.GetAllocatedBytesForCurrentThread() delta asserted
equal to zero across a warmed call -- so it is the known family rather than a new
defect. Recorded rather than re-investigated, which is the whole point of the
issue.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan section 5.5.19, the V7 slice row, two rows in the section 5.1 uncovered
table, and one divergence-register row. No product code changes.
WHAT V7 TURNED OUT TO BE. Three of V6m's four numbers were taken through an
instrument that was not holding the world still. The route pinned the Dereth
clock by pressing AcdreamCycleTimeOfDay, whose mechanism is the transient
/time override that SyncFromServer clears -- so ACE un-pinned it seconds into
every run this campaign has taken. Two captures 45 s apart at ONE stop on ONE
backend differ in 22.3% of the frame because the sun keeps moving. Pinning it
(commit 2) took the interior stop from 12.16% to 0.78% on its own.
THE THREE LEADS, ANSWERED.
Lead 3 was WRONG and the section says so. V6m recorded the interior stop as a
route defect on the theory that the indoor spring-arm camera settles to different
distances in two runs. It does not. The interior was lit differently because the
sun had moved. With the sun held still the stop drops by a factor of fifteen and
its entire remaining difference map is the player character -- the EnvCell's
walls, floor, doorway and per-cell ambient are black. EnvCellRenderer's Vulkan arm
has its numeric pair, V6m's defect-list item 2 is discharged, and no route change
was needed or made.
Lead 2 is closed by pinning the cloud phase rather than masking the band, so the
gate keeps the sky under strict comparison.
Lead 1 is half fix, half finding. The residual was predominantly the anisotropy
gap (commit 1). What survives is one population -- dense alpha-blended distant
scenery -- and isolating it needed a better instrument than the connected route.
THE INSTRUMENT V7 RECOMMENDS FORWARD. An offline GL-versus-Vulkan pair, which is
just the two existing capture scripts run with ACDREAM_WORLD_TIME and
ACDREAM_SKY_PHASE_SECONDS set in the invoking shell. No session, no server, no
entities, no camera settle, no wandering NPCs, and unattended:
GL vs GL same commit (control) 1,966 px 2.13e-03
VK vs VK same commit (control) 1,039 px 1.13e-03
GL vs VK whole frame 28,807 px 3.13e-02
GL vs VK everything below the tree band (rows 280+) 497 px 8.82e-04
Terrain, blending, roads, the water edge, fog, statics, scenery below the horizon
and the entire retained UI are at parity, inside the campaign's 0.001 threshold.
AD-46, FILED WITH ITS REFUTATIONS RATHER THAN ITS THEORY. The treeline band is an
anisotropic tap-pattern divergence between AMD's GL and Vulkan drivers. Three
competing explanations were tested and refuted, and section 4.7's predicted class
is one of them:
- not a sub-pixel offset -- an integer shift search finds (0, 0);
- not sharpness or LOD scale -- high-frequency energy matches within 5%;
- NOT DEPTH PRECISION. Forcing the Vulkan viewport's window-depth range to
[0.5, 1.0], which reproduces GL's compressed mapping exactly, moved the
whole-frame number by 3% (28,807 -> 27,852). The experiment was reverted. The
one pre-approved divergence class is not what this is, and the row says so
rather than borrowing its approval.
- It IS anisotropy, and there is no knob left: 41,509 differing pixels in the
band at anisotropy 1, 22,266 at 16, which is GL's value and retail's.
THE VERDICT TABLE. Full route, both backends, tolerance 2, MSAA off, day group 0,
world time 0.5, sky phase 0 (artifacts/v7-diff-c2):
holtburg_town 26,330 px 2.86e-02 EXCEPTION -- phase + AD-46
facility_hub_interior 7,176 px 7.79e-03 EXCEPTION -- phase
aerlinthe_island 62,892 px 6.82e-02 EXCEPTION -- AD-46 + dark floor
No stop passes and none of the three exceptions is a renderer defect; each is
named individually in the section, because "phase" is not an excuse unless it is
specific. Aerlinthe's is partly the instrument rather than either renderer: the
scene's mean luminance is 28/255 and half its differing pixels are exactly delta
3, one step over a tolerance that is absolute rather than relative. Changing that
tolerance is not V7's call.
CARRIED FORWARD, recorded in the section 5.1 table and the V7 row: a passing
connected stop needs authored per-stop masks that the gate script does not have
(it still has only the global -MaskTopPixels, deliberately defaulted to 0); the
portal depth mask has now gone three slices without drawing a pixel in an
automated run, and HouseExitWalkReplayTests names the cheapest target for it
(the Holtburg corner building, cell 0xA9B40170); whether AD-46 is visible to a
human is a user-stop question nobody has asked yet; and the R6 soak and RenderDoc
capture on Vulkan were not run.
Gates for this commit: docs only, so the code gates of commits 1 and 2 stand.
Complete Release suite 9,195 passed / 5 skipped with zero failures, and the GL
connected repeat gate at 3/3 RENDERED on both the desktop witness and the client
capture, both taken at this tree.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two changes, one measurement. The V6m smoke pair put GL versus Vulkan at
Holtburg at 18.52% of the frame differing at tolerance 2 with MSAA off. The same
stop on the same instrument now measures 9.05%, and the two populations these
address are gone from the difference map rather than merely smaller.
1. THE WORLD ATLASES WERE SAMPLED WITHOUT ANISOTROPY ON VULKAN, AND WITH THE
DEVICE MAXIMUM ON GL.
RhiWorldTextureArray -- the backend-neutral shared object/material atlas, and
the only IWorldTextureArray the Vulkan arm ever constructs -- registered its
clamp and repeat slots with GpuSamplerDescription.WorldClamp/WorldRepeat as
written, which carry MaxAnisotropy 1. The GL arm asks for the driver's own
GL_MAX_TEXTURE_MAX_ANISOTROPY twice over: ManagedGLTextureArray sets
GL_TEXTURE_MAX_ANISOTROPY on the image, and the two sampler objects its resident
bindless handles are built from (OpenGLGraphicsDevice.WrapSampler/ClampSampler)
set it again, which is the one that actually wins.
V6i-2 knew it was asking for 1 and said so in a comment -- "the world arm that
draws through these arrays is the next slice, and it is the one that can gate a
filtering change visually." That slice was V6j, the gate is V7, and this is it.
Retail settles the question rather than the GL arm settling it.
RenderDeviceD3D::SetDefaultD3DStates (0x005a3800) loops all sixteen sampler
stages and issues SetSamplerState(stage, 0xA, this->m_D3DCaps.MaxAnisotropy) at
0x005a4230. 0xA is D3DSAMP_MAXANISOTROPY and the argument is the device's
reported cap, not a setting -- so "as much anisotropy as this device has" is
retail's own rule, the GL arm is faithful to it, and asking for 1 diverged from
retail as well as from the shipping backend. No divergence-register row is owed
in either direction: this retires a Vulkan-only gap and lands on retail's value.
The fix asks for a ceiling rather than reading a limit back, because the pinned
RHI contract (plan section 3.3) carries no anisotropy field and is frozen. It
does not need one: VulkanGpuSampler already clamps MaxAnisotropy to
VkPhysicalDeviceLimits.maxSamplerAnisotropy, Vulkan guarantees that limit is at
least 16 wherever the samplerAnisotropy feature is supported -- which this
backend requires -- and 16 is where every desktop driver caps. The request and
the GL arm's read therefore land on the same number.
What it was worth, from the difference map at the same stop: the roof shingles
of both Holtburg cottages, which had been dense hatching across the whole
surface, and the stone courses of the near building are now black. Measured as
high-frequency energy (mean absolute neighbour difference, GL versus Vulkan) the
right-hand roof went from visibly blurred to a ratio of 0.999 and the wall to
1.023; every other textured region in the frame is between 0.99 and 1.02.
Grazing-angle surfaces are where anisotropy is the whole difference, which is
why a roof was the loudest thing in the frame.
2. THE SKY HAS TWO CLOCKS AND ONLY ONE OF THEM WAS PINNABLE.
ACDREAM_DAY_GROUP and the route's AcdreamCycleTimeOfDay presses pin the Dereth
date, which chooses the day group, the keyframe and the sun angle. The cloud
sheet does not read that clock: SkyRenderer accumulates TexVelocityX/Y against
DateTime.UtcNow minus its own construction time, by design, because retail's
clouds drift with real time regardless of the date. Two launches minutes apart
therefore cannot agree about where the clouds are no matter what the route does,
and the V6m smoke measured the cost -- 89% of its 18.52% sat in the top 240 rows.
ACDREAM_SKY_PHASE_SECONDS (RuntimeOptions.SkyAnimationPhaseSeconds ->
SkyRenderer.AnimationPhaseSecondsOverride) replaces that elapsed-seconds value
with a fixed one. Unset -- the default, and every ordinary run -- keeps the wall
clock, so nothing a user or the offline gate sees changes. The differential gate
forces it on both launches alongside MSAA and the day group; the offline gate
keeps its top-280 mask, because a same-commit GL pair still has the sun to
disagree about.
This is instrument determinism on the same footing as ACDREAM_DAY_GROUP, not a
workaround: it is one input to a UV offset, it is off by default, and no shipping
path reads it. The alternative on the table was -MaskTopPixels, which would have
permanently blinded the campaign's strictest instrument to the entire sky -- one
of the five surfaces the offline gate already cannot see. Rows 0-32 of the
Holtburg pair went from 23,090 differing pixels to 1,211, and what remains up
there is roof and portal rather than cloud.
WHAT THE SAME PAIR STILL SHOWS, unattributed and carried to the next commit: the
distant treeline, the player and the NPCs, and the animated portal. The portal is
phase and expected. The treeline is not filtering -- sharpness now matches within
5% and a shift search finds no sub-pixel offset -- and the two runs entered the
world at different last-logout positions (0xC95B0001 versus 0x09040008), so the
far-tier streaming history differed. That is the next thing to prove or refute.
Gates. Release build green. App tests 4,133 passed / 3 skipped against the
4,132/3 baseline (one new: the sky-phase parse). GL offline pixel gate against
the pre-change tree: 2.31e-05, 13 pixels of 563,200, inside the documented 9-31
band -- GL did not move. One offline Vulkan run with VK_LAYER_KHRONOS_validation
proven inserted by the loader: zero validation errors, zero warnings. Full
three-stop differential recorded at artifacts/v7-diff-c1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A full Release App run failed once at Issue181WallPressEquilibriumTests
.Diagnostic_WallPressedCamera_EyeWanderAndViewerCellStability. It passed in
isolation and did not recur across five further whole-suite runs, and the diff
under test touched only the world texture-creation stack -- nothing in camera,
visibility or physics. A cross-class parallelism race was the only plausible
mechanism, not a regression.
Ten App test classes share three process-global mutable statics, and xUnit runs
distinct test classes in parallel by default:
- CameraDiagnostics: AlignToSlope, CollideCamera, TranslationStiffness,
RotationStiffness, UseRetailChaseCamera. These are not merely written, they
are written AWAY from their defaults -- RetailChaseCameraTests sets
AlignToSlope and CollideCamera to false, and three classes set
UseRetailChaseCamera to false -- while RetailChaseCamera.Update,
CameraController.Active, CameraFrameController, WorldRenderFrameBuilder and
MouseLookController read them.
- RenderingDiagnostics.ProbeFlapEnabled, written by CornerFloodReplayTests
and Issue181WallPressEquilibriumTests.
- System.Console.Out, redirected by those same two classes to capture probe
output.
Every one of these classes already saved and restored in try/finally. That is
correct within a class and remains necessary, but it was never sufficient. A
finally bounds a mutation in TIME along its own thread; it cannot stop another
class from reading the static inside that window. Worse, two overlapping
save/restore pairs can interleave so the second restore writes back the FIRST
one's temporary value, leaving the global permanently wrong for the rest of the
run. The Console.Out case is the sharpest instance: an interleaved restore can
install a DISPOSED StringWriter as the process-wide Console.Out, which then
throws in unrelated tests. Serializing the sharers is what makes each class's
existing finally sufficient.
The fix is a marker CollectionDefinition applied to the ten sharing classes,
following the WorldEnvironmentControllerCollection precedent. No collection
fixture: several members are [Theory] cases that need different knob values per
case, so a fixture cannot own the save/restore without rewriting every member's
internals, and it would not help the read side at all. Because every member
references the same compile-time const for the collection name, the grouping
cannot silently drift via a typo.
Membership is deliberately narrow. It covers the eight writers plus two classes
that drive production code which READS a knob another member moves off its
default (HouseExitWalkReplayTests and CameraFrameControllerTests both run
RetailChaseCamera.Update and assert on the resulting eye). Classes that merely
construct a CameraController without a retail chase camera are NOT members --
their reads fall through the null branch and are insensitive.
No production code changed; no assertion was weakened, and no retry, sleep or
tolerance was added.
Verification. Base commit f6275f45 measured empirically at 3,763 passed / 3
skipped. Post-fix: 136 whole-suite Release runs. Every failure observed was in
the pre-existing zero-allocation family tracked as #250 (an Expected 0 / Actual
N bytes assertion), and none was in any collection member. A matched 55-run
baseline at f6275f45 reproduced that same family, confirming it predates this
change. Serialization cost is inside run-to-run noise: the suite is ~3 s of a
~4.5 s wall-clock dotnet test, and the ten serialized classes are a small
fraction of it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds section 5.5.18, a V6m row to the slice table and to section 5.1's
user-gate-debt table, and corrects section 5.1's "no connected route visits a
dungeon" claim - which was wrong in two directions.
connected-world-lifecycle.route.txt has carried a Facility Hub stop all along,
so the lifecycle gate did reach an interior even though nothing compared its
pixels; and reaching one turns out not to be the same as being able to compare
it.
The V7 list moves in three ways. Item 1 closes: every production renderer draws
on both arms. The appraisal-viewport half-discharge that V6k opened and V6l
carried closes too - the view was driven on both backends and inspected. And a
new item 7 opens, which is the honest result of firing the instrument once: with
the sky band and the animated portal masked, the smoke pair still differs in
1.17% of the frame, about 12x the pinned threshold, while the chat panel on its
own differs in 42 pixels of 106,560 - inside the threshold. So the 2-D retained
UI is already at parity and the residual lives in the 3-D pass, at silhouette
edges. That is a better starting position for V7 than a single aggregate number
would have been, and it is why the smoke was attributed rather than just
reported.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Section 5.5.17 records the slice: the instanced-vertex-input amendment and
particles (b1ad1d48), the stencil dimension and the portal mask (eced67d0), and
the offscreen viewports (2e8b8b91). The V4e row is no longer blocked and the V4g
row is no longer half-landed; the slice table gains a V6l row and section 5.1's
accumulated-debt table gains one for the two connected captures the offline gate
cannot reach.
Four defects are recorded as found by RUNNING rather than by validation, which
is the pattern this campaign keeps paying for: the standalone particle texture
cache and the entity-appearance composite cache were both bindless-only, so the
Vulkan arm could draw neither a textured particle nor any entity with a palette
override; a pipeline bakes one depth/stencil format, so an offscreen target's
depth had to take the device's; and the paperdoll rendered upside down because a
GL framebuffer's origin is bottom-left and a Vulkan image's is not.
The V7 list is rewritten. Nothing on it is blocked on a contract decision any
more. What is left is one absent renderer (PortalTunnelPresentation has no
Vulkan arm), EnvCellRenderer's arm narrowed from unproven to proven-by-one-frame
after a Marketplace interior rendered on Vulkan, the MSAA-off requirement, the
per-draw descriptor writes, the portal mask's two shader sources, and the
appraisal viewport's carried-forward half-discharge.
AP-92 is narrowed rather than retired: the private viewports are backend-neutral
targets on both arms and the blit's V origin is derived rather than assumed, so
the origin half of that row's risk column is closed. The rest of it - retail
renders each CreatureMode directly against a cloned CPhysicsObj - is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Section 5.5.16 reports both landed commits and the one that did not land.
The sky: V4f's content as a Vulkan arm, and the fourth defect of the
compiles-clean class - a 32-byte vertex stride declared for a 36-byte record,
found by capture rather than by validation, because AcDream.Core.Terrain.Vertex
carries a TerrainLayer member no sky attribute names. The generalisation is
written down: every .Rhi.cs arm restates a CPU record's footprint from memory and
only one of them now has a test.
Section 5.4 is marked DISCHARGED, with the distinction it turns on spelled out.
The divergence it describes has not been on the tree since the V4c revert took
that hunk with it; what the revert did not undo was the reason it existed, and
that is what V6k commit 2 closed. V7 is no longer blocked on it.
Particles are recorded as BLOCKED rather than deferred, with the measurement
behind it: GpuVertexLayout has one stride and no divisor and BindVertexBuffer
binds one buffer at vertex rate, so the contract can express instanced drawing
but not instanced vertex input - which is what both particle pipelines are built
on. Three ways out are stated, two of them contract changes and the third a
five-fold bandwidth amplification that does not scale to mesh particles. The
choice belongs to whoever owns section 3.3.
The V7 defect list is rewritten around what is now true, including two items the
slice found rather than inherited: PortalDepthMaskRenderer cannot be expressed
without a stencil dimension in GpuPipelineDescription, and a Vulkan viewport needs
sample-count pipeline variants as well as the layered sampled view the section
5.5.7 re-check turned into a loud precondition. The MSAA item stops being a
prediction and becomes a measurement: 8.83% of the frame at 4x, essentially all of
it alpha-to-coverage edges on foliage, with rows 300-720 contributing 980 of
81,359 differing pixels.
Section 5.1's debt table gains a V6k row saying which uncovered surfaces were
checked - the sky band across seven day groups, the paperdoll through a connected
inventory capture - and which two remain: the appraisal viewport, and the sun,
moon and rain cylinder a fixed outdoor camera cannot see.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Section 5.5.15 and the slice-table row. Three things worth having written down
rather than rediscovered.
The winding inversion V6c wrote was wrong and had never been asked a question:
every Vulkan consumer through V6i sets Cull = None, so the mapping had not decided
a fragment until the world arm arrived. That makes three defects this campaign has
found in a path that compiled, validated clean, and had a test - the descriptor
layouts and the TerrainClip set were the first two - and all three share the shape
of a test that asserts the behaviour rather than the requirement.
The A8 CullMode.Landblock override, which sections 5.5.13 and 5.5.14 both flagged
as due for an answer the moment world materials drew on a second backend, has one:
it carried over verbatim and is still load-bearing, so it is now a divergence with
two consumers rather than one. The dungeon pass in section 5.1's checklist is what
settles it, not this slice.
And the V7 defect list, so the differential is not run against a target that
cannot pass it: section 5.4's null-target divergence is still live on GL,
EnvCellRenderer's Vulkan arm and the deferred-alpha and doorway-scissor paths are
unexercised by the offline scene, and MSAA must come off.
The pixel-gate figures are recorded as a distribution rather than a number,
because 31 differing pixels sits at the documented band's top and a single value
there is not evidence either way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The slice brief was a Dereth PNG on Vulkan. There is not one, and §5.5.14 says
so in its first paragraph rather than at the end. What landed is the two
prerequisites — the mesh pipeline running on both arms, and the frame having a
world pass to record into — plus the seven measured findings the world arm needs
and that are cheaper to read than to re-derive.
Three of those correct earlier text rather than extending it, which is the part
worth reading:
- §5.5.8 offered to promote bindings 6-8 back to dynamic and said there were
"four unused dynamic slots to promote into". There are not. Vulkan's
guaranteed maxDescriptorSetStorageBuffersDynamic is 4, which is exactly what
V6g already spends, so the four bindings the world arm re-points per draw cost
a descriptor write each. That is bounded and correct, and it is why commit 2's
draw-time descriptor bind matters: without it the cost is ten scopes per draw
rather than one.
- V4c created every world pipeline with SampleCount = 1 because the GL backend
ignores it. Vulkan requires the pipeline to match the pass, and
alpha-to-coverage requires MSAA at all.
- The world renderers cannot each open their own pass on Vulkan, which is the
shape difference from V4c and follows directly from the MSAA resolve.
The slice table gains its V6i-3 row with the gate numbers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The three measurements were recorded out of order. Commit 1 measured 3.02e-05
(17 px), commit 2 measured 3.20e-05 (18 px) and commit 3 measured 1.60e-05
(9 px) — not the ascending sequence §5.5.13 listed. The band and the verdicts are
unchanged; what was wrong is which commit each number belongs to, which is
exactly what the figures exist to say.
Also records the suite totals in the V6i row.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan §5.5.10 recorded the blocker as a fact about types: "WbMeshAdapter owns an
OpenGLGraphicsDevice, so it is not constructible on Vulkan until slice V4t" —
which is the entire reason NullWbMeshAdapter exists. §5.5.12 item 6 then measured
how wide that dependency really is, and the answer is seven members out of a
760-line class: a GL context, the retirement queue, the shared instance VBO, and
two capability flags.
IMeshPipelineDevice is exactly that surface. OpenGLGraphicsDevice declares it and
every member already existed under a GL-specific name, so the shipping backend
executes not one changed statement — these are aliases, not behaviour.
Two casts moved, and they are what actually blocked construction:
- ObjectMeshManager downcast IGpuDevice to GlGpuDevice in its CONSTRUCTOR, so a
Vulkan-composed pipeline threw before running a statement. V4t put it there
because the class registered bindless handles itself; commit 2 moved that into
the array, leaving the field a pass-through for the raw-GL renderers' handle
table. The cast now lives on that one property and names the backend it was
composed against instead of reporting a failed cast.
- The atlas array factory is selected by IWorldTextureArrayFactory.For, which is
the one place the texture stack branches on a backend.
MeshPipelineDeviceSeamTests proves the decoupling rather than describing it: it
builds ObjectMeshManager against a device whose Gl is null, asserts it constructs,
asserts construction built no GL object, asserts the handle table refuses by name,
and asserts the factory picks the RHI arm. A reflection test pins the seam's
member set so a later slice cannot quietly widen it back out — the whole value
here is that it is narrow.
What this does NOT claim: the mesh pipeline does not RUN on Vulkan. Its upload
bodies are still raw GL — GlobalMeshBuffer, the VAO/IBO construction, the layer
transfers — and they now fail through one RequireGl() accessor that names the
slice that owns porting them, instead of failing at construction. WbMeshAdapter
still creates an OpenGLGraphicsDevice in its GL constructor, because there is no
second implementation to create yet. Those bodies are items 3–5 of §5.5.12's
remainder list, along with RetailPViewPassExecutor and the three world renderers'
submission arms.
§5.5.13 reports the whole of V6i-2 and the slice table gains its V6i row.
Gates: Release build; App tests 4,109 / 3 skips (the 4,086 baseline plus 23 across
the three commits); complete Release suite 9,172 / 5; strict GL offline pixel gate
vs 0ca802cd 1.60e-05 (9 px of 563,200 — the low end of the documented 9–31 px
control band, and fewer than a same-commit control has measured); GL connected
tools/run-repeat-connected-gate.ps1 -Runs 3 at 3/3 RENDERED on the desktop witness
and 3/3 on the client capture; one Vulkan composition-host run with
VK_LAYER_KHRONOS_validation proven inserted by the loader at zero errors, zero
warnings, no [shutdown] diagnostic, and a captured frame.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Slice V6i-1 recorded terrain_modern.vert's TerrainClip block as landing in the
wrong descriptor set under the Vulkan dialect. That was reasoning from the
missing ACDREAM_UBO_SET macro; it is now measured. Disassembling the committed
spv/terrain_modern.vert.spv shows the block as OpVariable ... Uniform decorated
DescriptorSet 0 / Binding 2, where set 0's layout declares a storage buffer.
The audit the section asked for is also done rather than deferred. Every
compiled .spv was disassembled: mesh_modern.vert's nine set-0 entries are all
StorageBuffer, correctly, and every other uniform block in every other shader
already carries the macro. terrain_modern.vert is the only one. sky.vert is the
precedent - it declares the SAME shared clip block as ACDREAM_UBO_SET binding =
2 - so there is no numbering question for the world arm to settle, only a
one-word omission to fix, plus declaring set 1 bindings 2 and 4 in the uniform
set layout. Binding 4 is section 5.5.8's still-open UniformSkyParams, which
sky.vert and sky.frag both declare.
Documentation only; no source, shader or .spv is touched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan section 5.5.8 recorded, and deliberately did not fix, that pointing one
binding at a second buffer within a frame silently corrupts the draws already
recorded against it: the backend rewrote the descriptor in place, and a
descriptor set's contents are read when the command buffer EXECUTES, not when it
was recorded. Nothing fired it while the Vulkan frame held only the retained UI.
Section 5.5.11 handed it forward as the first thing the world arm would hit,
because WbDrawDispatcher, EnvCellRenderer and TerrainModernRenderer each own
their own instance, batch and indirect buffers and all three bind set 0 in one
frame.
It is closed here, as its own commit and BEFORE the world arm, so that a blank or
corrupt first Vulkan world frame cannot be this defect wearing another face. That
sequencing is the point: sections 5.5.1 to 5.5.3 cost this campaign three days
because an instrument that was "usually right" sat underneath the thing being
measured.
What changed. There is no longer one (set 0, set 1) pair per flight slot; there
is an arena of them. VulkanBindingScopeArena - pure bookkeeping, no Vulkan
handles, nine unit tests - answers two questions per bind: which pair, and do its
descriptors need writing. VulkanFrameBindings keeps the Vulkan half: allocating
pairs from a growable pool list and writing the twelve descriptors when told to.
The scope key is the descriptor state itself - the ten storage buffer identities
and ranges, the plain bindings' offsets, and the two uniform buffer identities
and ranges. Deriving it is a decision, not an economy. The pinned contract has
nowhere to name a scope: BindStorageBuffer takes a buffer, an offset and a size,
and section 3.3 is frozen. Deriving also gives two properties a declared scope
would not: a renderer cannot forget to declare one, and two renderers that
genuinely share every buffer correctly share one pair rather than being told to
differ. A renderer's buffers are stable for its lifetime, so "distinct descriptor
state" is exactly "renderer scope".
Dynamic offsets stay free. A ring allocation moving between draws rides
vkCmdBindDescriptorSets's dynamic-offset array, so it costs neither a new pair
nor a descriptor write - section 4.4's "zero descriptor writes per frame"
property survives a frame having more than one binding state in it. Entries are
not invalidated at BeginFrame either, because the slot's previous submission has
retired and its descriptors still say what this frame is about to say; a steady
frame therefore rewrites nothing at all. An entry matched from the previous frame
is swapped below the live cursor so the rest of the frame cannot take it for a
different state - the ordering property the sixth test pins, where two renderers
swap submission order between frames.
What this does NOT do is draw a world. The captured Vulkan frame is still V6h's
retained UI over the fog clear, so the arena's multi-scope path is exercised by
its tests and not yet by a frame. That is recorded in the plan rather than
implied.
The plan's section 5.5.12 also records two blockers measured while scoping the
world arm and not fixed here: terrain_modern.vert declares TerrainClip without
ACDREAM_UBO_SET, so under the Vulkan dialect it lands at set 0 binding 2 where
the layout declares a storage buffer - the same class of gap 5.5.8 recorded for
UniformSkyParams, invisible until a terrain pipeline is created; and the offline
gate's scene takes the retail PView path rather than the flat safety path,
because ClipRoot falls back to Buildings.OutdoorNode, which puts
RetailPViewPassExecutor on the critical path to the first Vulkan Dereth frame and
makes the "terrain only" intermediate no cheaper than the whole arm.
Gates. Strict GL offline pixel gate against b9ab5890: 1.60e-05, 9 differing
pixels of 563,200, at the low end of the documented 9-31 px band and 62x under
the threshold - expected, since no GL file is touched. GL connected
run-repeat-connected-gate.ps1 -Runs 3: 3/3 RENDERED on the desktop witness and
3/3 on the client capture. One offline Vulkan run with VK_LAYER_KHRONOS_validation
proven inserted by the loader: zero errors, zero warnings, captured frame, no
[shutdown] diagnostic on either stream. App tests 4,086 / 3 skips (baseline 4,077
plus nine); complete Release suite 9,149 / 5. Issue #250's
SurfaceOverrideFingerprint_DictionaryHotPathAllocatesNothing failed once in a
whole-suite run and passed run alone, as that issue documents.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Marks V4t done in the slice table with its two commits and per-commit gate
numbers, adds §5.5.11, and ticks off step 3 of §5.5.9's corrected sequence.
§5.5.11 records four things the world arm needs and one it should not
re-derive: why §5.2's "the device's table is unreachable" argument expired
rather than being worked around (its premise was that V4c would move the world
renderers onto the encoder; §5.5.6 closed that, so the block became
indefinite); that V4t landed narrower than §5.3 sized it, keeping texture
creation and residency with the caches and moving only the table entry, with
`IGpuTexture` creation deferred to the world arm that actually cannot use a GL
handle; why `GroupKey`'s ordering survives a retype of one of its fields; and
the pixel-gate control measurement, because one capture read 5.50e-05 against a
documented 15–23 px band and the honest response to that is a control, not a
paragraph. The decisive number is the 9-px difference between captures at the
two V4t commits — two different commits, fewer differing pixels than the
same-commit control.
`GlBindlessHandleTable`'s own doc comment claimed four renderers own instances.
Three of them no longer do. It now names its one remaining owner, `SkyRenderer`,
says why that one is different (its textures come from the raw GL-name path V4t
did not retype), and names V4f as the slice that deletes both the table and the
class.
`common.glsl`'s binding-9 comment carries the same stale list. It is left alone
deliberately: the file is a shader source with compiled `.spv` artifacts and a
V9 freshness gate, so a comment-only edit there is not free, and what the
comment says about what binding 9 IS remains correct.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
V4t moves the world texture stack off the raw 64-bit ARB_bindless_texture
handle and onto GpuTextureSlot. This first commit does terrain only, because
terrain is the one branch of that stack whose producer and consumer are a
single pair — TerrainAtlas and TerrainModernRenderer — so it can carry the
new device seam on its own pixel gate before the mesh/composite/particle
retype lands on top of it.
Why the device's table can now be reached, when §5.2 said it could not.
That paragraph's reason was the flush: GlGpuDevice drains its dirty table
runs inside FlushBeforeDraw, which only an encoder-recorded draw reaches,
so a raw-GL renderer would sample a stale table. §5.5.6 then closed the GL
re-land of V4c/V4d, which means the world renderers stay raw GL through to
V10 — so "wait for the encoder" stopped being a plan and became an
indefinite block on V4t, which the Vulkan world arm cannot be written
without. The resolution is the smallest one that keeps the seam honest: the
drain is factored out as GlGpuDevice.FlushTextureTable, and a raw-GL
renderer calls it and binds TextureTableGlName at binding 9 itself,
immediately before its own draw — the same shape its retired private
GlBindlessHandleTable had, against a table that is now the device's. Nothing
else of the backend is exposed, and both members are deleted with the raw-GL
world path.
Residency ownership deliberately does NOT move. RegisterWorldTextureHandle
interns an already-resident handle and owns only the table entry; the atlas
still creates, makes resident and destroys its own textures. That is what
separates it from RegisterTexture, which owns the residency it creates, and
it is why this slice can retype the data model without also porting GL
texture creation onto IGpuTexture.
TerrainAtlas.GetBindlessHandles becomes GetTextureSlots(GlGpuDevice).
Registration is idempotent by handle, so the per-draw call is two dictionary
lookups — the cadence GetOrAdd already had. It is conditional on the handle
having changed because SetAnisotropic makes both textures non-resident and
re-acquires them: without that check a quality-preset change would strand a
slot holding a non-resident handle, so the superseded entry is retired in
the same step through the device's retirement queue.
Ordering is unaffected. Terrain's two slots travel as loose uniforms
(uTextureIndexA/B) and enter no sort and no bucket key, so a different slot
NUMBER changes nothing about what is drawn or in what order — only which
table index resolves to the same handle.
Gates. GL offline pixel gate vs cb2a70b8: 3.02e-05 (17 of 563,200 pixels),
exactly a same-commit control value and inside the documented 15-23 px /
<=4.1e-05 band. tools/run-repeat-connected-gate.ps1 -Runs 3: 3/3 RENDERED on
both the desktop witness and the client capture. One Vulkan composition-host
run with VK_LAYER_KHRONOS_validation proven inserted by the loader: zero
errors, zero warnings, empty validation log, converged ownership ledger. App
tests 4,075 / 3 skips (#250's zero-allocation test reran green singly).
One connected run of an earlier 3-run attempt died in the render loop with
"OpenGL returned unexpected fence wait status NoError (0x0)" from
GpuFrameFlightController.RetireFence. It did not reproduce in the following
three runs at this tree nor in three interleaved runs at cb2a70b8, and this
diff creates, deletes and waits on no fence. Filed as #251 rather than
attributed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The strict GL offline pixel gate re-run at b16f8206 measures 1.78e-05 (10 of
563,200 pixels) against 46d893f7, and the Vulkan validation run at that exact
tree is clean with an empty stderr and a UI frame identical to the pre-commit
capture. Also records a pre-existing test-isolation defect found while gating:
StreamingControllerPriorityApplyTests.DungeonCollapseBeforePromotionBase_RetiresProvisionalTerrainAndPendingStatics
fails alone under --filter and passes with its project, identically at the parent
commit, so it is an order dependency in the test rather than a regression.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ACDREAM_RENDER_BACKEND=vulkan now runs the real GameWindow composition rather
than a second main(). All nine phases execute: DAT load, streaming, camera,
entity table, session, and the real retained UiHost drawing through the RHI.
No world renderers — they are raw GL until V4t and the world arm behind it.
The offline log is the client's own (acdream.pak opened, 6266 spells, Region
0x13000000, "loading world view centered on 0xA9B4FFFF", fourteen retail
LayoutDesc lines, streaming radii), and the captured frame is the retail
retained UI: vitals, combat/spell bar with DAT scarab icons, the nine-slot
toolbar, chat with tabs and Send, radar/compass with dat-font glyphs. Sampled
against the GL capture the widgets agree — chat interior RGBA (25,24,27,158)
vs (22,21,23,158), vitals bar (117,1,0) and toolbar slot (0,11,17) identical.
Three seams, as §5.5.9 specified:
1. Platform acquisition — already generic — publishes GameWindowGraphics
instead of a bare GL. Phases that still speak raw GL read Graphics.Gl and
take their Vulkan arm when it is null; each branch names the slice that
removes it.
2. VulkanHostInputCameraCompositionFactory is a new file and the whole of the
Phase-1 fork: four graphics members differ, input/camera/pointer delegate.
The default factory is chosen inside the phase from the platform result.
HostInputCameraResult gained backend-neutral Retirement and FrameSlots.
3. The frame root forks on one condition. The GL world-scene assembly is
unchanged, wrapped in `if (gl is not null)`; the Vulkan arm's graph is one
backbuffer clear pass computing the same RenderFrameFoundation from the same
clock and weather owners, then private presentation over it.
§5.5.9's three TextureCache couplings are unpicked: the constructor takes GL?
and rejects bindless without one, world entry points route through a Gl
property that throws naming V4t, and the (GlGpuTexture) VRAM-accounting cast
became a backend test. That cast's stated reason — DrawSprite's texture-unit
binding — was already stale, deleted at V6d.
VulkanBringUpHost is reduced to the capability-probe harness it is named for:
the instance/surface/device/swapchain sequence moved into VulkanGraphicsContext,
which the composition host and the harness now share. It is reached only with
ACDREAM_VULKAN_PROBE=1.
One latent Vulkan defect surfaced and is fixed here. The first composition-host
frame died with ErrorDeviceLost; validation named VUID-vkCmdDraw-None-08600 —
descriptor set 2 never bound. VulkanGpuPassEncoder bound sets 0/1/2 only as a
side effect of BindStorageBuffer/BindUniformBuffer, so a pass sampling the
texture table while binding no buffer — every retained-UI and debug-line pass —
drew with the table unbound. It survived V6c-V6g because the bring-up host
always drew VulkanRhiScene first and the UI pass inherited its binds; the
composition host has no 3-D scene. The fix is one line in the encoder's
constructor beside the viewport and scissor defaults, which exist for exactly
the same reason: a pass opens with complete binding state rather than depending
on what preceded it.
Gates: strict GL offline pixel gate against 46d893f7 measures 1.24e-05 (7 of
563,200 pixels), inside the documented 15-23 px / 4.1e-05 band, so GL behaviour
did not move. App tests 4,075/3 skips; complete Release suite 9,138/5 skips.
One full Vulkan run with VK_LAYER_KHRONOS_validation: zero errors, zero
warnings. Both Vulkan runs converged the ownership ledger — no [shutdown]
diagnostic on either stream. The reduced probe harness presented 34,811
validation-clean frames.
No divergence-register row: GL is the shipping backend and the pixel gate proves
it unmoved; the Vulkan arm is not a retail deviation but a backend under
construction.
Next is V4t, the texture stack, which the world arm cannot be written without.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
§5.5.7 asked for "a Vulkan composition host — a slice the plan has never
scoped." This scopes it and stops there, for the same reason V6f stopped short
of the world fork: the honest measurement is worth more than a half-built second
path, and §3.1 and §7.1 rule 3 exist to prevent exactly the unexercised arm this
would otherwise have landed.
Three findings, all verified against source rather than inferred.
The fork seam mostly already exists. GameWindowPlatformResult<TGraphics, TInput>
is fully generic — only its call sites pin GL — and the host phase already has
IHostInputCameraCompositionFactory with a single Retail implementation, so the
Vulkan device arm is a new file rather than a modification. What does not exist
is a Vulkan frame root: FrameRootComposition assembles the clear phase, both
pass executors and the GPU profiler measurement from a GL handle plus six raw-GL
world renderers, none of which exist on Vulkan. That second assembly is the
slice's centre of gravity, and it is most of V4h.
The retained UI is NOT blocked on V4t. V6f wrote that the retail widget tree's
chrome comes from a GL-only TextureCache, which is true of the type but not of
the path: V4a and V6d already moved UploadUiTexture onto IGpuDevice, and the
public UploadRgba8 that IconComposer composes retail icons with routes into it.
The raw-_gl uploads that remain are the world's Texture2D/Texture2DArray paths,
which no UI draw reaches. Three small things stand in the way — a non-null GL in
TextureCache's constructor, one ((GlGpuTexture)texture).GlName cast for VRAM
accounting, and the UI-probe screenshot controller — and none of them is V4t.
So V4t is a hard prerequisite for the WORLD arm, not for the host, which moves
the host ahead of it in the sequence rather than behind.
That reordering is the point of the commit. The corrected sequence puts the
composition host at step 2, before V4t, because its acceptance criterion — the
real UI renders — makes it the first frame acdream draws on Vulkan that is the
client's own frame rather than a scene written to prove the backend.
Also records the argument for keeping VulkanBringUpHost as a capability-probe
harness: its window/instance/surface/device/swapchain sequence is what the
composition arm needs, and deleting it would mean writing that twice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
V6f ran the bring-up host once under VK_LAYER_KHRONOS_validation and found
seven VUIDs, every one of them on the path any world frame takes (plan
§5.5.7). This closes all of them, plus a fourth defect in the same log that
§5.5.7 did not call out. The host now runs validation-clean: zero errors and
zero warnings over 39,855 frames.
Nothing outside Gpu/Vk/ is touched, so the GL backend executes not one changed
statement. The offline pixel gate says so too — 4.08e-05 differing fraction
against f8dbe2ee, which is exactly the value the campaign recorded as its own
same-commit control (§5.1's 15–23 pixel band).
The dynamic-descriptor limit was a decision, not a patch. V6b declared all ten
of set 0's bindings STORAGE_BUFFER_DYNAMIC on the reasoning that the contract
lets a renderer bind any range per draw. That is true and still cost nothing to
honour for four of them: a dynamic descriptor buys exactly one thing, the
ability to address the SAME buffer at a DIFFERENT offset without a descriptor
write, which is the shape of a ring allocation and of nothing else. So the
ring-fed bindings — instances, batches, clip slots, instance light sets — stay
dynamic, and the ones pointing at a long-lived buffer written whole and bound
once per pass carry their offset in the descriptor instead. Binding 9 is the
clearest of those: it is the GL-only uvec2 handle table, which the Vulkan
backend never binds at all.
That lands on four dynamic storage descriptors. The RX 9070 XT allows eight, so
eight would have worked here — but four is Vulkan's GUARANTEED minimum, which
means no conformant device can fail this layout, and V9's lavapipe row and the
deferred physical Linux row both depend on that. The count is asserted against
maxDescriptorSetStorageBuffersDynamic in the capability record, so a device that
cannot serve it is rejected at startup in the report under the same exit-code-4
contract as every other requirement, rather than failing silently at
vkCreatePipelineLayout the way this one did.
Depth-off pipelines were malformed in any pass that has depth. Dynamic rendering
bakes the depth/stencil attachment format into the pipeline and requires it to
equal the pass's; V6c set it only when the pipeline itself tested or wrote
depth. Debug lines, the retained UI and the sky are all depth-off and all
composite over the main pass, so this was not an edge case. The same
GpuPipelineDescription is legitimately used both ways — ui-text opens its own
depth-less pass — so the description cannot answer the question and the backend
builds both variants, binding whichever matches what vkCmdBeginRendering was
actually handed rather than what the pass asked for. Both are built at startup
against the persisted cache, so no frame compiles one. A slice entitled to
change the contract should add a depth-format field the way V6d added
ColorFormat; this is the honest expression of the gap until then.
vk-backbuffer-depth and vk-backbuffer-msaa-color were created UNDEFINED and
never moved. Both now barrier on every backbuffer pass — from UNDEFINED on the
first use after Configure, from attachment-optimal with a write-after-write
dependency thereafter. The dependency matters on its own account, not just the
layout: two passes in one frame write both images and so does the next frame,
and Vulkan orders nothing between render-pass instances.
The fourth defect is the one worth reading twice. CaptureBackbuffer transitioned
the LAST PRESENTED swapchain image to TRANSFER_SRC and copied out of it. After
vkQueuePresentKHR that image belongs to the presentation engine and its contents
are not ours to read — and the pixels were usually right, which is precisely the
problem. This campaign spent three sections of its own plan (§5.5.1–§5.5.3)
discovering how much a capture instrument that is "usually right" can cost, and
shipping that shape on the new backend would have made every Vulkan PNG, and the
V7 differential built on them, formally undefined. The frame now copies its own
output into a host-readable buffer while it still owns the image, and the
capture reads that. Retention is opt-in, armed when an artifact directory
exists: one full-resolution copy per frame is worth nothing to a player and is
the entire instrument to a gate. The old one-shot command pool, device-idle wait
and per-capture readback buffer go with it.
Two gaps found and recorded in §5.5.8 rather than fixed, both outside this
slice's brief. UniformSkyParams (set 1, binding 4) is not in the uniform set
layout, so whoever first draws sky on Vulkan must add it. And a binding pointed
at two different buffers within one frame silently corrupts the earlier draws,
on dynamic and plain descriptors alike, because descriptor contents are read at
execution time — no consumer does that today, but WbDrawDispatcher and
EnvCellRenderer each own their own instance and batch buffers and both bind
bindings 0, 1, 3, 4 and 5 in one frame, so the Vulkan world arm has to know
before it is written.
Gates: Release build; App tests 4,075 passed / 3 skipped (baseline 4,073 + the
two new capability cases); GL offline pixel gate PASS at 4.08e-05; one
validation-layer Vulkan run, clean, with the captured PNG inspected and correct
in orientation, colour and glyph coverage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two things to write down after slice V6f.
The obligation §5 recorded against V6e is discharged. terrain_modern was "the one
production pair still not Vulkan-expressible"; its three blockers - the two loose
matrices, the loose tiling array, and the GL-only sampler-from-handle
construction - are gone, and 8/9 pairs now compile. The ninth is `mesh`, which
the plan already records as having no consumer at all, so every shader acdream
actually draws with is Vulkan-expressible. V6f gets its own slice row and its own
line in the user-gate debt table: terrain through a doorway clip region is the
one terrain path the offline gate cannot see, and it now has a second UBO binding
beside the clip block, so a bind-order mistake would surface exactly there.
The larger entry is §5.5.7, which records a measurement rather than an opinion.
§5.5.6 selected option (B) - V4c/V4d's content returning as the Vulkan world path
behind a fork at the thin submission seam - and V6f set out to build that fork.
It cannot be built yet, for a reason the plan had not stated: the Vulkan path
constructs no game state at all. GameWindow.Run returns at :695, before
Window.Create and therefore before OnLoad, which is the only caller of the
composition pipeline. A capture confirms it (artifacts/vk-world/): what the
Vulkan backend draws today is V6c's verification scene and V6d's generated UI
sprite, correctly and completely, and nothing else. A backend-selected fork would
therefore have a GL arm that runs and a Vulkan arm nothing can reach - the
unexercised second path §3.1 and §7.1 exist to prevent.
Worse for sequencing, the parked V4c/V4d code could not drive Vulkan even if it
were reached: it binds GL bindless handles as a storage buffer because §5.3
deferred the real port to V4t, and GroupKey carries the raw ulong. V4t is a hard
prerequisite, and it rewrites exactly the code the fork's Vulkan arm would
contain. Landing the fork first means writing that arm twice.
One validation-layer run is recorded with it, and it found two defects that
outlive the slice, both pre-existing and both on the path any world frame takes.
The pipeline layout declares all ten storage bindings as STORAGE_BUFFER_DYNAMIC
against a device limit of eight - the pinned binding model meeting a real limit,
wanting a decision rather than a patch. And any depth-off pipeline in a pass that
carries depth declares VK_FORMAT_UNDEFINED where the attachment's real format is
required. Also noted: the render-target-view-in-table usage V6f was told to
expect did NOT fire, so it should be re-checked rather than carried forward as
known-and-accepted.
The section closes with the recommended order - composition host, the validation
defects, V4t, then the fork - and with a cheaper intermediate milestone worth
considering: terrain, water and sky only, for which V6f's work is the whole
shader prerequisite.
Also: the roadmap's Campaign V paragraph gains a shipped-so-far line, and #250
gains a third test of the same class. One full App run during this slice reported
2,752 bytes against an expected 0 in
CurrentRenderSceneOracleTests.SurfaceOverrideFingerprint_DictionaryHotPathAllocatesNothing,
on a diff that touches only GLSL and terrain's uniform plumbing; it passed alone
and in four other full runs of the same binary.
Documentation only - no code, no gates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign V slice V6e, last of three. Sky was the hardest of the four pairs
because it was the only one that still worked the way a 2004 shader works: a
dozen loose uniforms pushed one glUniform call at a time, and a texture bound to
unit 0 with a sampler object chosen per submesh. Vulkan GLSL has neither a
default uniform block nor a way to declare a bare sampler, so both had to move —
and the second one had a sting in it.
The uniforms go into a `SkyParams` std140 block at uniform binding 4, the new
pre-authorized constant in GpuBindingModel (1, 2 and 3 are SceneLighting, the
terrain clip block and terrain tiling; the contract test now proves the three
constants and that literal 2 do not collide). Three matrices are 192 bytes on
their own, so the 96-byte push-constant block was never in the running. The
block's member order IS its layout: std140 aligns a vec3 to 16 bytes while using
12, so each of the three lighting vectors is followed by the float that rides in
its pad word, which is why colours and per-surface scalars interleave rather
than grouping by meaning. SkyParamsLayoutTests asserts all twelve offsets and
the 256-byte size, because getting one member wrong would read the sun direction
as a colour with no compile error, no link error and no GL error to say so.
The texture is the interesting half. sky.frag now reads through the shared table
(ACDREAM_SAMPLE_2D), and a bindless handle BAKES its sampler — so the
per-submesh Repeat-versus-ClampToEdge choice, which used to be a glBindSampler
on unit 0, becomes which slot the submesh asks for. SkyRenderer interns one
handle per (texture, wrap) pair, exactly as ManagedGLTextureArray has done since
the world path went bindless, and exactly the shape Vulkan's table has, where an
entry is a combined image sampler. Same two SamplerCache objects, same wrap
behaviour, consulted once at interning instead of once per draw. A pleasant
consequence: the sky no longer touches texture unit 0, so the load-bearing
`BindSampler(0, 0)` restore at the end of the pass — there because the binding
was global state that would otherwise force ClampToEdge on the next renderer —
has nothing left to undo and is gone.
Gates. Release build clean; App tests 4,072 passed / 3 skipped (4,057 baseline,
plus the sentinel guard from the previous commit and fourteen sky-layout
assertions). Offline pixel gate against 95f8c25f: 18 px of 563,200 compared
(3.20e-05), inside the documented 15–23 px band.
That gate masks the sky for determinism, so it proves nothing about this commit
and the sky renderer has no automated pixel coverage at all. What was done
instead: a base-versus-head offline capture at ALL SEVEN day groups, built by
stashing the change and rebuilding so the two runs differ only in this commit.
Every pair matches in gradient, cloud sheet, horizon band and fog — including
day group 2's salmon cloud band and day group 6's green one, which between them
exercise texture sampling, per-vertex tint, blend mode and fog. Then 3/3
RENDERED on the desktop-witness repeat-connected gate.
That bounds the risk; it does not close it. The offline camera is fixed and
looks down, so a thin band of dome is all it ever sees: the sun and moon
(additive, high) and the rain cylinder (the one sky mesh that surrounds the
camera, and the one whose REPEAT wrap is most visible) remain unproven. Recorded
as user-gate debt in §5.1 alongside V2c's and V4e's particles — check it by
standing outside at dawn or dusk, and by standing in rain.
Manifest: 8/9 pairs compile. `terrain_modern` is the last production pair, and
it is blocked on V4d's content rather than on dialect — details in §5.5's slice
table. `mesh` has no consumer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign V slice V6d, commit 3 of 3 — the evidence commit, which turned out to also be a bug-fix commit.
VulkanBringUpHost now builds a real UiHost and DebugLineRenderer on the Vulkan device and draws them after the V6c verification scene, in their own single-sampled load/store passes against the backbuffer — the same shape the GL client's HUD phase has. Nothing in the retained stack is backend-aware: UiRoot walks a real widget tree, each widget draws through UiRenderContext, and UiHost.Draw brackets it with TextRenderer.Begin/Flush. What it cannot be is the game's own UI, because the retail tree is built from LayoutDesc and DAT chrome by TextureCache, which stays a GL type until V4t; the sprites here are generated instead. The widget rectangles are authored at known pixel offsets from the top-left and nothing is mirror-symmetric, so a wrong Y flip would put the title bar at the bottom.
The frame this produced was wrong, and usefully so. Whole runs of the debug-line figure were missing. Vulkan's rasterization-order guarantees are scoped to one render-pass instance; between two instances writing the same attachment there is no implicit ordering, and that includes a multisample RESOLVE, which is part of the render pass and therefore equally unordered against what follows. TransitionBackbufferForRendering emitted its acquire barrier once per frame and returned for every pass after the first, so the second and third passes raced the first one's resolve. V6c's frame had exactly one backbuffer pass and could not see this; V6d's has three. A later backbuffer pass now gets a colour-attachment dependency instead of nothing, and keeps ColorAttachmentOptimal as its old layout rather than Undefined, which would have licensed discarding everything drawn so far. Every line renders continuously afterwards.
Inspection of artifacts/vk-ui/vulkan-bringup.png against the authored layout, by pixel probe:
The header panel is authored at (24,18), 420x96. Its tiled chrome fills exactly x 24..443 and y 18..113 — one pixel outside on any edge is the clear colour. The tile's lit edge appears at the top and left of every cell, so texture row 0 lands at the top and the V axis is not flipped. Both labels read left to right, right side up, through the font-coverage branch. The nested panel's border samples exactly (153,191,255) against an authored (0.6,0.75,1.0), unblended — the untextured branch is bit-exact. The badge sprite is authored at (460,58), 64x64, and its gradient starts at x=460 with the clear colour at 455 — the RGBA-modulate branch, sampling a table slot. The two debug-line segments land on their computed screen coordinates. All three fragment branches, the pixel-to-NDC mapping, the top-left origin, straight-alpha blending and table sampling are therefore all confirmed on Vulkan, which is everything the offline GL gate confirms about the same code on GL.
The plan's V6 milestone is amended rather than claimed: "full game frame on Vulkan" is not reachable while V4c/V4d are parked and the world renderers and TextureCache are still raw GL, so V6 delivers the backend plus the two renderers that can use it today. The accumulated user-gate table gains a V6d row for the paperdoll/appraisal viewport sprite — the one retained-UI texture the offline scene never draws, on a slice that changed how every UI texture is sampled.
App tests 4,057 passed / 3 skipped, unchanged. Offline pixel gate against f6f58a12: differing fraction 3.20e-05, 18 pixels of 563,200, inside the documented 15-23 pixel noise band — as expected, since this commit touches only Vulkan files and the campaign doc.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The exact V4c binary - verified by its embedded wb-mesh pipeline literals - rendered ten of ten repeat-gate cycles on a separate NVIDIA PC against the same ACE, same scene, same account, while the AMD box fails 30%+ of identical runs with the defect pinned to this binary at p=0.024. Two GL drivers, one failure. Option B is adopted: GL keeps the legacy world path to V10, the RHI world path ships on Vulkan, and the V4c/V4d GL re-land is closed rather than parked.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the decision that §5.5.4 recommended and the V5 bring-up now makes
actionable: option (C). No further GL-side attempt is made to re-land V4c or V4d
until the same ported world path has been measured on Vulkan.
The grounds are the three investigation sections read together. A blank run
rasterizes its first world frame correctly - frame 43's occlusion counters,
1,692,830 terrain and 317,561 entity samples, byte-match the parent build - and
then one irreversible event kills every GPU→CPU return channel at once:
readbacks come back RGBA(0,0,0,0) over UI pixels the desktop witness shows on
screen, a guarded glGetQueryObject deadlocks the render thread inside the
driver, and a GPU-timeline query-buffer write never lands on its sentinel.
Present and fences keep running at 5.5 ms throughout, on NO_ERROR from
glGetError and a clean glGetGraphicsResetStatus across 1,814 samples. Five
mechanisms are falsified and four independent instrument faults have turned up,
all of them below the API, all on one driver on one GPU.
That is not a shape any further GL-side bisect is well placed to resolve, so the
document now states the decision rule rather than leaving option (C) as a
recommendation. If the identical RHI world path renders correctly on Vulkan on
this GPU, the driver defect is proven and option (B) is adopted deliberately:
GL keeps the legacy world path through V10 as a documented, scoped exception to
§3.1's no-fork rule, confined to the thin submission seam. If it fails on Vulkan
too, the trigger is in code we own and the hunt resumes against a much smaller
haystack.
Two knock-on edits keep the plan self-consistent rather than leaving the reorder
stated in one place and contradicted in another. The slice table marks V4c and
V4d parked, V4t and V4e-V4h re-sequenced pending the verdict, and V5 shipped.
§5.4's sequencing invariants no longer claim V0→V4h is strictly sequential, and
they now carry the consequence that matters: V6 arrives before V4g and V4h, so
the Vulkan backend honours the contract's literal null target while GL still
carries the transitional inheritance. That makes §5.4's two removal obligations
more binding, not less, and V7's differential must not run until the removal has
happened - otherwise it would surface the divergence as a viewport rendering to
the wrong surface, which post-decision is indistinguishable from the fork option
(B) permits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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 against fed636b9 passes 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>