Campaign N Slice N1 (docs/plans/2026-07-29-network-transport-campaign.md
S2.1) - the direct #260 fix: every sent reliable packet is now cached and
re-emitted, header-rebuilt, when ACE NAKs a client-sequence gap. One lost
C2S datagram no longer voids every subsequent action for the session's
lifetime.
New src/AcDream.Core.Net/Transport/:
- TransportClock: injectable monotonic source + retail's 0.5 s interval
counter (ClientFlowQueue::IncrementLocalInterval @ 0x00547F10, tail
`intervalID_ += elapsed`; the same function's ~3 s TimeSync/Echo cadence
stays deferred per TS-58).
- SequenceMath: wrap-safe IsNewer/Max (TimeStampUtils::lhs_newer
@ 0x00543890, reduced to the signed-difference form).
- SentPacketStore: FIFO of ArrayPool-rented wire buffers; Add asserts
optionalLength == 0 (NetPacket::RemoveDisposableOptionalHeaders
@ 0x00549510 pinned as a no-op under standalone-control); FlushOlderThan
pops strictly-older wrap-safe (SentPacketStore::AddSentPacket
@ 0x0054AB00, Flush @ 0x0054ACD0).
- OutboundFlowQueue: owns the outbound ISAAC, highestIDSent (starts 1,
pre-increment, wrap 0xFFFFFFFF->1, never 0), the fragment sequence, the
store, the wrap-safe sorted dedup pending-resend list
(FlowQueue::EnqueueAcks @ 0x005488E0), and the flushNum_ ack watermark.
Cache commit happens AFTER a successful send
(FlowQueue::TransmitNewPackets @ 0x00547A60, commit site 0x00547C85).
NAK ids[0] folds into the watermark as retail's implicit cumulative ack
(RecipientData::ProcessNaks @ 0x00547010). A resend rebuilds ONLY the
20-byte header: flags Retransmission|EncryptedChecksum (|BlobFragments
with fragments), Time = current interval id, Sequence/Id/Iteration/
DataSize verbatim, checksum = fresh header hash + stored sealed checksum
(FlowQueue::TransmitAcks @ 0x005485B0, DequeueAck @ 0x005472F0). The
original ISAAC key rides inside the sealed value - no new keystream word
is ever drawn (CryptoSystem::EncryptData @ 0x0065FF40 non-null-key
path; landmines #1/#2). Resend only on explicit NAK (landmine #3).
- ReliableTransport: composition + Sweep() (interval clock, resends,
prune). The AckNakScheduler joins in N3/N4; ack behavior is untouched
this slice.
- TransportStats: unconditional counters (ResendsSent,
NakRequestsReceived, UncachedNakIds, AcksConsumed) + CacheDepth.
PacketCodec.FinalizeInPlace gains an overload returning (isaacKeyUsed,
sealedChecksum) where sealedChecksum is the pre-header-hash value -
payloadHash cleartext, isaacKey ^ payloadHash encrypted (retail
NetPacket::checksum_). The old signature forwards; encode bytes are
unchanged. Decode is untouched.
WorldSession integration is minimal: the transport is constructed at
ISAAC-seeding time (first reliable packet keeps sequence 2 / fragment 1,
byte-identical to pre-N1); SendGameMessage delegates (probe fseq/pseq now
read the transport); SendAck's borrowed sequence reads HighestIdSent
(identical value, behavior EXACTLY as-is this slice); ProcessDatagram
consumes RequestRetransmit + AckSequence BEFORE the unchanged reflex ack;
the sweep runs at the end of Tick() after the budget break AND inside
both blocking handshake pump loops (Connect step 4, EnterWorld
ServerReady - landmine #8), gated on _transportNegotiated; Dispose
returns the rented cache buffers.
Bookkeeping: TS-57 filed in the divergence register (uncached NAK ids
dropped silently + counted instead of retail's RejectRetransmit - ACE
no-ops the reject and the standalone unsequenced form would trip ACE's
watermark hole); TS-27 narrowed to the inbound direction in the same
commit; the stale WorldSession class-doc gap list corrected.
N0 fold-ins from the re-review: AceSessionModel.ProcessFragment split
into ACE's two literal branches (existing-buffer checks Complete,
NetworkSession.cs:495-507; new-buffer constructs + adds + TryAdds WITHOUT
checking Complete, :509-518), and the zero-count-fragment test now pins
the parked dead buffer (PartialFragmentBufferCount 0 -> 1). e3958610
recorded in the campaign ledger's N0 row.
Tests: 15 new in Transport/OutboundReliableTransportTests.cs - store
FIFO/strict/wrap-safe flush with rent/return balance via a counting
pool, interval-clock start/advance/wrap, resend header shape (flags
exactly 3 or 7, Time = interval, verbatim fields, checksum identity,
bit-identical body), resend-consumes-no-ISAAC-word, uncached-NAK
counting, ids[0] watermark fold + strict prune, wrap-safe ack max,
conformance resend verifying under AceCryptoModel with the ORIGINAL
parked key (Headroom 256, zero orphans, ordering restored), an
end-to-end FakeAceTransport lossy run (10 game actions, C2S #5 dropped,
all 10 dispatched in order, exactly one resend, session alive), and
zero-alloc steady-state SendGameMessage.
Gates: dotnet build green; AcDream.Core.Net.Tests 702/702; full-solution
Release 9,723 passed / 5 skipped / 0 failed; connected world-lifecycle
gate vs local ACE RESULT=PASS (0 failures, both sessions exit 0; one
pre-existing expected world-edge landblock-miss warning).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Addresses the N0 review findings against commit 7e9134b4. Test-only: no
production code changes.
F1 (blocking) - model Session.CheckState (Session.cs:93-110). A three-value
AceSessionState (AuthLoginRequest -> AuthConnectResponse -> AuthConnected)
advances on SendConnectRequest (AuthenticationHandler.cs:127, :232) and on the
accepted ConnectResponse (NetworkManager.cs:77). CheckState runs as the first
statement of Receive after TryParse - ahead of the ConnectResponse route and
ahead of VerifyCRC - so a LoginRequest out of state, a replayed
ConnectResponse, or any of AckSequence|TimeSync|EchoRequest|Flow during
AuthLoginRequest is dropped at zero keystream cost (ACE's PacketHeader.HasFlag
is ANY-of, PacketHeader.cs:70). New StateDropCount counter.
F2 - implement SendBundle faithfully (NetworkSession.cs:808-919). One
NetworkBundle per GameMessageGroup (NetworkBundle.cs:6-63), swapped out and
sent in ascending group order; the InvalidQueue bundle carries the ack /
TimeSync / EchoResponse optional headers. As many same-bundle fragments as fit
the 464-byte body budget now travel in ONE packet - one sequence, one keystream
word - and a message whose remaining data fills a packet splits across packets
with Count>1 fragments (:846-854, :874-888) via a port of ACE's server-side
MessageFragment (MessageFragment.cs:10-103). The old "one packet per message"
shortcut and its incorrect rationale are gone.
F3 - model the two-phase termination. Terminate arms PendingTermination with
the 2 s window (Session.cs:281-298, SessionTerminationDetails.cs:12); inbound
and outbound keep running through it (Session.cs:124-133), then the pump
completes the session work and releases the network resources
(NetworkManager.cs:366-369 -> Session.cs:300-334 -> NetworkSession.cs:958-974).
IsTerminated now means "termination armed"; IsReleased is the point of no
return.
F4 - port ACE's MessageBuffer exactly (MessageBuffer.cs:7-54): a List, not an
index-addressed array. An assembled stream under 4 bytes returns null and is
dropped WITHOUT advancing the fragment gate (:49-50 + NetworkSession.cs:504-506
removing the buffer either way), and a later fragment claiming a larger
Count/Index for the same sequence completes the message instead of throwing.
F5 - the C2S parse path now characterizes ACE: fragment parsing uses ACE's
complete validation (16 <= Size <= 464, ClientPacketFragment.cs:12-24) with no
Count==0 / Index>=Count rejection and with ReadBytes' short-read tolerance,
instead of inheriting acdream's stricter production layout check. The one
remaining strictness we inherit - the 1024-id cap on retransmit lists - is
documented as unreachable (ACE reads into a 1024-byte buffer, so a C2S datagram
can carry at most 250 ids).
F6 - class doc now states that C2S CRC verification reuses acdream's own
PacketHeaderOptional hashing, so the double is NOT an independent oracle on
optional-header wire layout, and names the two known asymmetries (ACE has no
inbound ConnectRequest parse; ACE hashes-but-does-not-advance on
LoginRequest / WorldLoginRequest / ConnectResponse).
F7 - hardened three weak tests: the NAK rate limit is probed at 0.9 s and at
exactly 1.0 s (both closed) before 1.1 s opens it; the session timeout is
probed at exactly 60 s after fixing the model's `>` to ACE's `>=`
(Session.cs:140); the cache prune pins that an entry exactly 120 s old survives
(:258 is strictly greater).
F9 - campaign doc section 9 ledger: N0 row marked complete.
Nine new tests; 687 Core.Net tests green in Release.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Campaign N slice N0 (docs/plans/2026-07-29-network-transport-campaign.md):
the referee that slices N1-N5 are graded against, test-project only, zero
production changes.
- VirtualClock: Stopwatch-shaped deterministic time source (fixed 100 ns
ticks) that N1 will inject behind the production TransportClock.
- AceCryptoModel: verbatim port of ACE CryptoSystem Search/ConsumeKey over
our IsaacRandom - 256-key window, parked-key set, Headroom/OrphanCount
diagnostics (CryptoSystem.cs:8-49 cited per method).
- AceSessionModel: transport-free ACE NetworkSession over raw datagrams,
every rule cited to NetworkSession.cs - CRC-before-everything silent
drop, cleartext-NAK early return (no timeout refresh, :283-308),
60 s timeout refresh (:329-331), exact-equality ack dedup exemption
(:342-347), desired+2 NAK trigger with 1 s limit (:351-363), >window
AbnormalSequenceReceived (:393-397), the :474-476 watermark hole,
ack-value cache prune (:663-673), fragment gate (:532-543), seq>=2
caching (:730), Retransmission-flag resends with the ORIGINAL IssacXor
(:675-686), RejectRetransmit, 2 s cleartext cumulative ack, 20 s
TimeSync, EchoResponse, 120 s cache prune (:251-262). ACE's raw
wrap-unsafe comparisons are modeled bug-for-bug, not fixed.
- LossyLink: deterministic drop/reorder/seeded-loss fault injector, pure
data structure.
- FakeAceTransport: IWorldSessionTransport binding a REAL WorldSession to
the model through the link, with the handshake scripted (ConnectRequest
reusing the negotiation fixture layout, CharacterList, ServerReady,
logoff confirmation) - genuine Connect/EnterWorld/Tick/Dispose with no
sockets.
- 19 new tests pin the double, including
CleartextNonAckAdvancesWatermark_TheAceHole (the self-induced wedge
behind scope rows TS-57/TS-58/AP-125), re-key = permanent orphan,
unrequested-resend window burn, the 115-id NAK cap boundary, and a
full no-socket session lifecycle with both ISAAC streams verified
aligned end-to-end.
Core.Net suite: 678 passed / 0 failed (659 existing + 19 new).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CLAUDE.md''s read-first list and the roadmap header now carry Campaign N
(retail reliable-transport port) as the active campaign and Campaign V
as the closed record.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The #260 investigation ended in a full root cause: acdream cannot
survive a single lost UDP packet in either direction. Outbound: the
server''s RequestRetransmit lists are parsed and consumed nowhere, and
no sent-packet cache exists - one lost C2S datagram permanently stalls
ACE''s ordered stream (actions void, position updates void, new areas
never stream: the whole #260/#256 symptom set). Inbound: the ISAAC
keystream is burned in arrival order, so one lost S2C datagram
permanently desyncs the cipher. Loopback ACE never drops packets,
which is why every historical gate passed.
The campaign doc pins the port target from the named retail decomp
(SentPacketStore/FlowQueue resend with reused ISAAC keys, the inbound
pre-drawn-key NAK set, the 2.0s cumulative ack / 0.6s NAK shared-gate
sweep, constants), the ACE constraint table Coldeve enforces (the
256-key crypto window, the exactly-AckSequence watermark rule, the
cleartext-NAK requirement), the Transport/ class design, slices N0-N6
with per-slice gates and Fable/Opus review assignments, the landmine
list, and eight divergence-register rows for the pieces that are
unsafe against ACE''s watermark hole.
#260 updated to point here; its memory half is closed as benign
(mapped-pak page residency + designed cache ceilings - measured, not
a leak).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 2026-07-29 Coldeve session 3 stuck the player in the portal tunnel
forever: generation 2 (Town Network, 0x00070156) published render but
composites/collision never became ready, and the reveal latch correctly
held the tunnel. The composite warmup queue in WbDrawDispatcher has
exactly two permanent-stall shapes - a GfxObj id that never resolves
(silent load failure, e.g. custom-server content absent from the baked
pak) or an upload budget that never reopens - and they are
indistinguishable from the reveal log alone.
ACDREAM_PROBE_REVEAL=1 (NetDiagnostics.ProbeReveal) now emits one
[composite-warmup] STALL line per second while warmup blocks a reveal:
pending count, queue depth, scan state, upload-budget gate, and the
first four pending GfxObj ids. Zero cost when off.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first armed ACDREAM_PROBE_NET launch died at exit 4 one second into
the world: the [net-tick] line read _inboundQueue.Reader.Count, but the
queue is built with SingleReader=true and that channel implementation
throws NotSupportedException from Count. The net thread now increments
and the frame thread decrements a probe-owned Interlocked counter
instead; behavior with the probe off is untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two-agent investigation refuted #260's as-filed hypotheses: every
UseWithTarget was acked (the J5.2 use gate never latched), and the LOH
leak is bounded sawtooth churn - the real climb is ~2.25 GB of native/
GPU memory (WS 3,261 vs managed 1,015 MiB at wedge). The wedge evidence
also showed why it could hide: the live combat toggle routes through the
generation-gated runtime command seam, and every rejection exit in that
chain (Disposed / StaleGeneration / !IsInWorld at Validate, plus the
operations slot reading IsInWorld=false when unbound) is COMPLETELY
silent - no log, no event.
ACDREAM_PROBE_NET=1 (NetDiagnostics owner, PhysicsDiagnostics pattern)
now arms three probe families, all zero-cost when off:
- [net-out] per reliable send at the SendGameMessage chokepoint: opcode,
GameAction type+sequence, fragment/packet sequence, managed thread id
(two tids would prove the cross-thread ISAAC-desync hypothesis alone),
and state; [net-out-EX] via an exception FILTER that logs without
catching, so propagation is unchanged.
- [net-tick] 1 Hz cadence from WorldSession.Tick: inbound/s, queue
depth, budget breaks, worst inter-tick gap (frame-stall witness),
out/s, acks/s.
- [cmd-gate] every silent runtime-command rejection with expected-vs-
view generation, lifecycle, and IsInWorld, plus the combat toggle
result (whose Inactive exit reads a DIFFERENT IsInWorld source).
One walked-portal repro session with this probe distinguishes all
remaining #260 wedge hypotheses. ISSUES.md #260 rewritten to the
corrected two-root framing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First solid reproduction of the Coldeve findings: after sustained walked portal-network use the client wedges - server-round-trip actions (portal use, combat toggle) produce zero outbound send while client-predicted movement still works, and the LOH climbs to 574 MB. Combat toggle x3 with no send is the smoking gun for the one-request-at-a-time use gate latching closed on an unacked UseWithTarget. gcdump captured in the broken state. Supersedes the framing of #256/#257 - both are likely facets. Investigation not started; forbidden workaround (gate timeout) called out explicitly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Repeat connected gate 3/3 rendered on both witnesses, world-lifecycle route PASS with its one documented expected warning, resource snapshots banked. Every gate the campaign defined has now been executed and passed on the shipped tree.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SoundId was not a subset of retail's table, the way its comment claimed.
It was an invention: 23 acdream-local names on acdream-local values, and
the values were wrong in the way that matters. FootstepDefault = 0x02 is
retail's Random. SwingSword = 0x10 is retail's Death2. Death = 0x60 is
retail's Explode. Anyone who reached for one of those names to compare
against a wire or dat value would have got a different sound.
Nothing referenced any of them by name -- grep for `SoundId.` across src
and tests returns nothing -- so this was a trap rather than a live defect,
the same shape the enum campaign found in DamageType. All 22 invented names
are deleted and retail's 205 replace them.
Three oracles agree exactly, on every name and every value: retail
acclient.h:4569 enum SoundType, ACE's Sound, and DatReaderWriter's Sound.
The third matters most. AudioHookSink already resolves SoundTable lookups
through DatReaderWriter.Enums.Sound, so that is the enum acdream actually
reads at runtime; our catalog now agrees with the values already flowing
through the dat path, and a conformance test pins the two so they cannot
drift apart.
On the "206 sounds" figure: retail's block holds 207 entries, being 205
sounds followed by NUM_SOUND_TYPES = 0xCD and FORCE_SoundType_32_BIT. The
first is a count and the second a width pin. Counting the former is where
206 came from. Neither is a member here, matching how the campaign treated
NUM_ATTACK_HEIGHTS and Num_HoldKeys -- a count is not a value the wire can
carry.
Behaviour is unchanged and could not be otherwise: the enum had no
consumers. IAudioEngine's three SoundId overloads are no-op stubs and the
live path takes wave ids and DatReaderWriter values.
The user's separate report that sound is "not working that good" is a
triggering, selection and attenuation question rather than a catalog one,
and is filed as its own Bucket B row in the post-Vulkan intake.
Also in this commit, by user decision: AC2D is retired as a reference. Its
clone and directory are gone and it must not be re-cloned. Everything we
took from it still stands and is written down -- the FSplitNESW terrain
split constants, the 0xF61C movement packet layout, the finding that a
client need not compute terrain Z itself -- so CLAUDE.md's reference list,
its hierarchy table, and the architecture doc's protocol row now point at
docs/research/2026-04-12-movement-deep-dive.md rather than erasing the
history. The reference count drops from six to five.
Core tests 3907 passed / 2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
acdream carried 16 status codes, curated by hand out of the CMotionInterp
and MoveToManager decompilation passes. The other 362 were unnamed, which
made every one of them a cast site waiting to happen. This slice takes the
whole table: 372 values under 378 names.
The oracle set is finally complete. All six vendored reference repos were
empty when the 2026-07-29 enum campaign ran, which is why it deferred this
decision; they are re-cloned now, so ACE's WeenieError could be read
directly instead of leaning on the UtilityBelt catalog alone.
The two agree without a single conflict. ACE has 369 members, no internal
value collisions. The catalog has 372, shares all 369 ACE names, and
disagrees on none of their values. Its three extras -- IsNowOpenFellowship
(0x050B), IsNowClosedFellowship (0x050C), LockedFellowshipCannotRecruit
(0x0518) -- each turn up in ACE's separate WeenieErrorWithString enum with
a `_` marking the interpolated name, so the catalog is just the less-split
view of the same client enum. All three are adopted on agreement between
two oracles, not on one.
Retail cannot arbitrate any of this. acclient.h has no counterpart enum;
its charError (26) is character-creation only. Recorded, not guessed
around.
Six values keep two names. acdream's NotGrounded, CrouchInCombatStance,
SitInCombatStance, SleepInCombatStance, ChatEmoteOutsideNonCombat and
ActionDepthExceeded are each anchored to a retail decompilation site, where
ACE's names for those values are server-side coinages. Rather than pick,
both are declared, acdream's first so ToString() is untouched.
Behaviour is unchanged, and there is no way for it not to be: nothing in
the tree branches on a WeenieError member. MotionInterpreter's switch is on
a motion type and merely returns one of these; WeenieErrorText.For switches
on a raw uint; the chat translation table WeenieErrorMessages is keyed on
uint throughout, so naming a code does not make it render. The one site
that moved is RemoteTeleportHook, where the (WeenieError)0x3Cu cast becomes
the now-named WeenieError.ITeleported at the same value.
Register row AP-15 is narrowed rather than retired. Its code-catalog caveat
is superseded -- an unnamed code is no longer a way for it to bite -- but
the sentences are still ACE's doc comments rather than retail's
string_table.bin, and that part stands.
The enum moved out of MotionInterpreter.cs into its own file at the same
namespace. At 372 members it does not belong inside a physics class file.
Core tests 3903 passed / 2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The WSI wedge cleared on its own and the armed tripwire caught the recovery. The offline pixel gate ran within minutes: post-deletion self-differential 13 px, and post-deletion versus the pre-deletion baseline also 13 px - removing 27,670 lines of OpenGL altered nothing about the Vulkan frame. Phase 1 of the overnight goal is complete as written; the connected-route reruns remain on the morning list as the goal's honesty hatch provided. #259 reclassified as transient and self-clearing, tripwire pattern recorded as the remedy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The #259 refinement (session-transition diagnosis, third gate attempt, cheapest-first morning remediation) as before - now without the 423 MB of session capture artifacts a git add -A accidentally swept into the previous tip commit. artifacts/ enters .gitignore so the mistake class is structurally impossible; the accidental commit is replaced via force-with-lease before anything consumed it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Closes out the GL deletion by fixing the CI workflow and developer gate
scripts that still assumed a GL arm existed to compare against, build,
or select via ACDREAM_RENDER_BACKEND.
.github/workflows/headless-portability.yml: linux-graphical's "Verify
actionable unsupported-driver gate" step is deleted outright — it ran
the deleted `ui-studio` CLI verb (Studio was removed at Commit 1) to
prove the GL capability gate rejects Mesa's llvmpipe driver, and there
is no more GL capability gate for any driver to pass or fail. Its test
filter dropped two dead entries (GraphicalCapabilityRequirementsTests,
deleted at Commit 2; StudioWindowTests, already gone). Its package
contract check dropped the libcimgui.so assertion (ImGui's native
bridge, deleted at Commit 1). linux-vulkan's explanatory comment, which
described GL's rejection as the reason no cross-backend pixel diff runs
in CI, is rewritten to explain there is no GL arm left at all. Two dead
src/AcDream.UI.ImGui/** path triggers (that project no longer exists)
are removed from both the pull_request and push filters.
tools/run-backend-differential-gate.ps1 and its dedicated route file
tools/connected-backend-differential.route.txt are deleted: the whole
script's purpose was comparing a GL launch against a Vulkan launch of
the same route, and there is no second arm left to compare. Single-arm
regression checking already exists via run-offline-pixel-gate.ps1's
-Baseline mechanism.
tools/run-portal-churn-soak.ps1 is simplified rather than deleted: its
repeated-portal-churn methodology (within-arm capture comparison,
memory/entity/GPU trend analysis) has value independent of the
GL-versus-Vulkan question it was built to answer for issues #256/#257
before V11. -Backends now defaults to @('vulkan') alone; the doc
comments are rewritten from "step 0 discriminator, run before V11" to
an ongoing single-arm regression soak.
tools/run-offline-pixel-gate.ps1 drops its now-nonfunctional -Backend
parameter (ACDREAM_RENDER_BACKEND has read zero call sites since
RuntimeOptions.RenderBackend was removed at Commit 2 — passing -Backend
gl silently launched Vulkan anyway) along with its GL-escape-hatch
example and every comment that referenced the now-deleted differential
gate. tools/run-connected-world-lifecycle-gate.ps1,
tools/run-offline-vulkan-capture.ps1, and
tools/run-repeat-connected-gate.ps1 keep their (harmless, already
no-op) ACDREAM_RENDER_BACKEND set/clear lines but have their
now-inaccurate "escape hatch" / "GL run" comments corrected to state
plainly that the variable is unread and the line is kept only for the
historical record.
No .cs files touched; `dotnet build AcDream.slnx -c Release` unaffected
(0 warnings, 0 errors, matching the prior commit's build).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Commit 2 deleted the GL rendering backend's implementations; this step
removes the package references and shader vocabulary they leave behind,
so nothing in the App project still spells Silk.NET.OpenGL.
Silk.NET.OpenGL and Silk.NET.OpenGL.Extensions.ARB are dropped from
AcDream.App.csproj. Chorizite.Core stays — the audit is NOT clean: its
Render.Enums (TextureFormat, BufferUsage) and Lib.BoundingBox types are
used directly and extensively across the Wb texture/mesh pipeline,
independent of the deleted GL IUniformBuffer implementers the package
comment used to cite. The stale comment is corrected in place.
IMeshPipelineDevice.Gl is removed along with the GL? gl parameter
threaded through WbMeshAdapter's four constructors, WorldRenderComposition's
CreateMeshAdapter, and VulkanMeshPipelineDevice's Gl => null
implementation — nothing read any of them once the legacy per-mesh
upload bodies were gone (confirmed by grep: the sole non-doc-comment hit
was a test assertion). While in WbMeshAdapter.Dispose(), found and fixed
a real bug along the way: its teardown still pattern-matched the deleted
GL GpuFrameFlightController to decide whether to wait for submitted work,
which VulkanFrameFlightController replaced at slice V6a without this site
being updated — so the wait had been silently dead on every Vulkan run
since then. Retargeted to VulkanFrameFlightController, which carries the
same WaitForSubmittedWork().
The GL pixel-format vocabulary (Silk.NET.OpenGL.PixelFormat/PixelType) that
WorldTextureArray/TextureFormatExtensions/TextureAtlasManager used for
upload validation is replaced by AcDream.Content's existing Silk.NET-free
UploadPixelFormat/UploadPixelType enums (added at MP1a to keep the bake
tool GL-free); two new members (Rgb, Red, Float) extend that enum with
their GL ABI constants to cover the full vocabulary WorldTextureArray
needs, since MP1a's original set only covered what the extractor itself
emits. ObjectMeshManager's App-boundary cast
`(Silk.NET.OpenGL.PixelFormat?)batch.UploadPixelFormat` becomes a direct
pass-through now that both sides share the type.
GpuBindingModel.StorageTextureTable (the GL-only binding=9 emulation of
the Vulkan texture table) is deleted and StorageBindingCount drops from
10 to 9; the descriptor-set-layout code that builds from that count
(VulkanPipelineLayouts, VulkanFrameBindings) is untouched and just
allocates one fewer always-dummy-seeded, always-unused binding.
Several fully dead GL-only classes came along for the ride, confirmed by
zero construction sites: SilkFramebufferViewportTarget
(NullFramebufferViewportTarget is the sole production
IFramebufferViewportTarget), SilkRenderGlStateReader
(NullRenderGlStateReader.Instance is the sole IRenderGlStateReader),
RuntimeRenderFrameClearPhase (VulkanRenderFrameClearPhase is the sole
IRenderFrameClearPhase, expressing the same atmosphere-clear logic as a
pass load-op instead), and GpuFrameTimer plus FrameProfiler's
GL-owning FrameBoundary(GL) overload and BeginGpuFrame/EndGpuFrame
bracket (RecordGpuSample is the only GPU-timing path any backend uses
now — the ACDREAM_WB_DIAG nested-query exclusion these existed for no
longer applies, since WbDrawDispatcher's own diagnostic GPU sampling
already moved to the device's Vulkan timer pool). GpuFrameFlightController
itself stays (never constructed with a real fence API in production, but
its retirement-ledger/serial-ring logic is backend-neutral and still
covered by its own unit tests) — only its GL-specific parts (the public
GL constructor overload, SilkGpuFenceApi) are deleted, since removing the
whole class would mean restructuring the frozen Slice-8 composition
shape's GpuFrameFlightController? threading, which is out of this
commit's scope. TextureParameters.cs and BufferUsageExtensions.cs
(zero callers each) are deleted outright.
common.glsl is deleted: nothing in the actual Vulkan .spv build reads
it. tools/ShaderCompiler/Program.cs compiles each .vert/.frag pair
directly and tools/ShaderCompiler/VulkanGlslPreamble.cs injects its own
complete self-contained preamble per file; common.glsl's textual
concatenation was exclusively Shader.cs's GL-only mechanism, deleted at
Commit 2. The five shader files that named it in comments
(mesh_modern.vert, particle.vert, particle.frag, sky.frag,
terrain_modern.frag) are corrected to point at VulkanGlslPreamble.cs
instead. mesh.vert/mesh.frag — the pre-N.5 legacy shader pair the
mandatory modern path already made unreachable, with zero C# consumers
and no compiled .spv — are deleted too. Regenerated via
tools/compile-shaders.ps1: 9/9 remaining shader pairs compile
(previously 9/10, with mesh the sole failure — the VulkanShaderManifestTests
doc comment's "nine of ten are not Vulkan-expressible" was already
stale before this commit).
Test fallout: dead-subject test methods/files are deleted rather than
patched (TextRendererFailureSafetyTests.cs, ClipFrameUploadTests.cs,
GpuResourceRetirementTransactionTests.cs's GL queue tests, one
WorldRenderDiagnosticsTests source-order test, one
RenderFrameResourceControllerTests clear-phase-order test); tests whose
subject moved or was renamed are updated in place rather than deleted
(GpuContractTests, VulkanCapabilityGateTests, MeshPipelineDeviceSeamTests'
pinned seven-member surface now reads six, ParticleBindlessInstanceTests'
cross-dialect check now covers the one surviving dialect,
WbMeshAdapterTests' misleadingly-named null-gl test — gpuDevice was
always the parameter that actually threw).
Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors,
with the Silk.NET.OpenGL/.Extensions.ARB package references physically
removed from the csproj (not just unreferenced in code).
Tests: full-solution `dotnet test` green across every project.
Zero remaining `using Silk.NET.OpenGL` anywhere in src/ or tests/.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Vulkan is the sole, user-signed-off backend (V10 landed) and step 1
already removed ImGui/Studio/DevTools. This step deletes the GL
rendering backend itself: every Gpu/Gl/** implementation, the Wb
ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/
BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache,
RenderBootstrap, and RenderFrameGlStateController.
GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/
OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses
its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and
RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone —
there is nothing left to select between. The five world-draw dual-arm
renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer,
ParticleRenderer, SkyRenderer) and the composition roots
(WorldRenderComposition, HostInputCameraComposition,
LivePresentationComposition, FrameRootComposition) collapse to their
RHI-only arm. GL-only diagnostic properties with a live external reader
(DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op
rather than disappearing, since the reader is out of this commit's
scope.
A few GL-flavored mechanisms turned out to be backend-neutral once
isolated: GlConstructionCleanupLedger is renamed
ResourceConstructionCleanupLedger (exception-chain walking has nothing
to do with GL), and GlfwNativePlatformProbe moved out of the otherwise
GL-only GraphicalCapabilityRecord.cs into
GraphicalWindowBackendSelection.cs before the rest of that file was
deleted.
Test files with no surviving subject are deleted outright
(GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests,
PortalDepthShaderParityTests, TextureCacheBindlessTests,
TextRendererFailureSafetyTests, ClipFrameUploadTests, every
Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests);
others get their dead GL-only members trimmed while their live
assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now
reads GpuBindingModel.StorageClipRegions, the same binding index under
its new backend-neutral name; GpuResourceRetirementTransactionTests
drops its OpenGLGraphicsDevice-subclassing test double and the two GL
queue tests it existed for). EnvCellRendererTests' construction helper
now builds a real ObjectMeshManager via VulkanMeshPipelineDevice
instead of passing null through a null-forgiving operator, since the
RHI constructor never tolerated a null mesh manager and the old GL
constructor (which did) is gone.
Deferred to the next two steps, deliberately not touched here: the
Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl
(WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale
csproj comment (the package itself is still load-bearing —
TextureFormat and friends are used well beyond the deleted
ManagedGLUniformBuffer), and the CI/gate scripts.
Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors.
Tests: full-solution `dotnet test` green across every project
(App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all
others 100%); the 2 App.Tests names that flake under full-suite
parallel execution (#250-family, documented pre-existing) pass in
isolation.
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>
RaiseAttribute, RaiseVital, and RaiseSkill each wrote a 64-bit xpSpent,
producing a 24-byte action where the server expects 20. ACE's
GameActionRaiseAttribute and its Vital and Skill siblings read
message.Payload.ReadUInt32(); holtburger's RaiseAttributeData declares
xp_spent: u32 and advances the offset by four. Both oracles agree, and the
four extra bytes were tail the server never reads.
These three are live-wired, from the character sheet through the command
router to SendRaiseAttribute, so this was shipping on every attribute, vital,
and skill raise. It has not caused a visible failure because ACE reads the low
dword and stops, and a single raise cost has never approached the dword
ceiling. That is luck about value ranges, not correctness about layout.
Worth noting the shape of the miss: the sibling builder BuildTrainSkill had
already been corrected to a 20-byte, 32-bit credits field, and its test is even
named U32CreditsNotU64. The same class of bug was found and fixed once in this
file and the other three cases were left behind.
The parameter stays ulong because the cost comes from 64-bit server XP tables
several layers up in the App and Runtime command chain; narrowing that end to
end is a separate change and is filed in the audit's open questions. Nothing is
lost at the wire: a cost that does not fit in a dword was never expressible
here.
The existing test asserted the 24-byte shape and is corrected, joined by a
theory that sweeps zero, one, a realistic cost, and uint.MaxValue across both
remaining builders.
Core.Net tests go 655 to 659.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CommunicationTransientString (0x02EB) required a trailing u32 chat type after
the message. The server does not send one. Because the string is padded to a
four-byte boundary, the remaining length after reading it was always zero, the
guard tripped, and the parser returned null for every transient string the
server has ever sent. Not most. Every one.
Three oracles agree there is no such field. ACE's
GameEventCommunicationTransientString writes exactly one WriteString16L and
stops. Retail's ClientCommunicationSystem::Handle_Communication__TransientString
at 0x0057d460 takes a single PStringBase<char> argument. holtburger carries no
type field for the event either.
ParseTransient now returns the string. The wiring supplies chat type 0, which
is ACE's ChatMessageType.Broadcast and which ACE's own LogTextTypeEnumMapper
comment names "Default" — the honest stand-in for a message the server sends
untyped. What retail's transient strings should actually look like is a
rendering question and belongs with the chat colour work, not here.
The existing round-trip test was itself appending the phantom trailing dword,
which is exactly why the wrong guard looked correct for as long as it did. It
is corrected to the real payload and joined by a case sweeping string lengths
zero through four, so no future padding-residue assumption can hide here again.
Core.Net tests go 654 to 655.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wire-stack audit checked the transport layer by hand and found it clean:
PacketHeader's seven fields match ACE's Pack order exactly, the optional-header
sections are parsed in ACE's order, and PacketHeaderFlags is a twenty-three of
twenty-three value match including the sparse gaps between 0x04 and 0x100 and
between 0x00800000 and 0x01000000.
Clean is worth freezing. These bits are not design choices; each one gates an
optional-header section, so a single wrong value shifts every following
section's offset and takes the packet checksum with it. The failure would not
look like a wrong flag, it would look like a corrupt connection. The enum is
small enough to pin exhaustively, so this transcribes ACE's declaration and
asserts both directions: every ACE flag exists here with ACE's value, and we
declare nothing ACE does not.
Core.Net tests go 630 to 654.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
HearSpeech decoded 0x02BB and 0x02BC with one layout. They do not share one.
ACE's GameMessageHearRangedSpeech writes senderID, range, chatMessageType
where GameMessageHearSpeech writes only senderID, chatMessageType, and
holtburger's HearRangedSpeechData declares the same range: f32 that
HearSpeechData lacks. Two oracles, no ambiguity.
The consequence was quiet rather than loud. The tail is twelve bytes, our
guard demanded eight, so nothing ever failed to parse. We read the guid
correctly, then read range's float bits as the chat type and discarded the
real one. A shout at range 60.0f arrived with a chat type of 0x42700000
instead of 0x0B. Nothing downstream consumes ChatType for local speech today,
which is why this survived, but the record is public and any future consumer
would have inherited garbage.
TryParse now branches its tail size on the opcode and Parsed gains Range,
which stays zero for local speech because there is no such field on that
wire. The existing ChatTests ranged case was itself built on the misreading,
constructing a local-shaped tail; it is corrected to the oracle layout and
now asserts both range and chat type rather than only the ranged flag.
New golden tests drive both opcodes through AceWireWriter in ACE's write
order, covering empty strings, string lengths one through four so every
residue of the four-byte padding rule is exercised, CP1252 accented names,
and a regression pin asserting the chat type is not the range float's bits.
A ranged body four bytes short is now rejected instead of silently decoded.
Core.Net tests go 617 to 630, all green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The golden-byte tests we had proved that a parser agreed with whoever typed
the hex literal. That is a weaker claim than it looks: if the author misread
the oracle, the test cements the misreading. This adds AceWireWriter, a
line-for-line mirror of ACE's Extensions.cs writers, so a fixture is produced
by the same algorithm the authoritative server uses. Each primitive cites the
ACE line it ports, including the string16L padding rule whose comment in ACE
reads "client expects string length to be a multiple of 4 including the 2
bytes for length".
On top of that harness, two inbound families get field-exact coverage they
had none of. VectorUpdate (0xF74E) is driven in GameMessageVectorUpdate.cs's
write order and pinned at ACE's declared 36-byte length, with cases for the
remote-jump +Z velocity, planar velocity plus yaw omega, rest, and
all-negative components so a sign or field-order slip cannot pass. The two
script-playback messages follow GameMessageScript.cs: PlayScriptId (0xF754)
as guid plus script DID, and PlayEffect (0xF755) as guid, type, and a free
intensity float. The NaN case documents the parser's deliberate choice to
retain non-finite intensities for the resolver to reject rather than coercing
them at parse time, which is behavior worth locking down.
Core.Net tests go 600 to 617, all green.
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>
Three more fields acdream already pulls off the wire and then carries as bare
numbers. AmmoType and MaterialType ride PublicWeenieDesc through CreateObject and
land on ClientObject as ushort/uint; ItemUseable and CombatUse arrive as
PropertyInt 16 and 51. Nothing named them, so every site that reasoned about them
did it in hex.
AmmoType (acclient.h:4221) and CombatUse (acclient.h:6523) are small and
unsurprising. ItemUseable (acclient.h:6478) is neither: it is two 16-bit halves,
low for where the used object must be and high for where its target must be, and
retail names roughly thirty specific combinations rather than expecting callers to
compose them. They are transcribed rather than composed because at least one is
not the union it looks like - SOURCE_CONTAINED_TARGET_OBJSELF_OR_CONTAINED is
0x880008, where composing ObjSelf|Contained|(Contained shifted 16) gives 0x800088.
A test asserts that specific non-equality so the shortcut cannot be reintroduced.
ItemAppraisalTextFormatter's ammunition sentence now reads through AmmoType instead
of matching 0x08/0x40/0x10/0x80/0x20/0x100 literals. The fold it performs - crystal
and chorizite variants collapsing to their base arrow/bolt/atlatl kind - was
already exactly right against retail's bit layout; this only gives it vocabulary.
No behavior change, and the appraisal tests confirm it.
Core tests 3,836 -> 3,894. Full suite 9,759 passed / 5 skipped, no failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With the two wrong enums corrected, the remaining wire-adjacent families diff
cleanly against the retail header - same values everywhere they overlap, just
fewer members on our side. This adopts the gaps.
EquipMask gains retail's eleven INVENTORY_LOC composite slot groups (acclient.h:
3193). The 32 primitive slots were already exact and stay pinned by EquipMaskTests;
what was missing were the groups the wire and the UI actually reason in - Armor,
Jewelry, ReadySlot, Weapon, WeaponReadySlot, the wrist/finger/sigil pairs, and
All. These are transcribed as literals, not derived, for the reason the previous
commit documents at length.
That transcription immediately earned itself. A type remark on EquipMask claimed
retail's CLOTHING_LOC composite "also sets bit 31, 0x80000000, which is not a
named INVENTORY_LOC primitive". It does not. CLOTHING_LOC is 0x080001FF: the nine
wear slots plus bit 27, which is the perfectly well-named Cloak slot. No
INVENTORY_LOC member touches bit 31 at all - ALL_LOC stops at bit 30. The remark
is corrected and a test now asserts the actual decomposition.
TransientStateFlags gains WaterContact (0x8) and CheckEthereal (0x100), the two
retail bits acdream's transition never declared. Neither is produced or consumed
yet; they are named so those slots cannot be quietly reused for an acdream-local
flag and then collide.
PhysicsStateFlags gains ReservedUnused1 (0x2) and ReservedUnused2 (0x2000), which
retail declares as UNUSED1_PS/UNNUSED2_PS. Same reasoning: reserved is a fact
worth recording.
AttackHeight gains Undef = 0. The three real heights are 1-based and were already
right; retail reserves 0 and the wire sends it, so it is now named instead of
arriving as an undefined cast. The numeric values are unchanged, so this renames
nothing at runtime.
Also checked and found already correct, so left alone: ObjectInfoState (matches
ObjectInfoEnum exactly, None being DEFAULT_OI), AttackType (every primitive plus
both composites - Unarmed 0x19 and MultiStrike 0x79E0 - land on retail's
literals), RadarBlipShape, RadarBehavior, MovementType, HoldKey, ParticleType, and
PhysicsDescriptionFlag. AttackType is worth calling out because the campaign's
extraction tooling reported it as a conflict; the tool reads one line per member
and had truncated a multi-line composite. The enum was fine.
RetailEnumConformanceTests grows tables for each of the above, each citing its
acclient.h line.
Core tests 3,785 -> 3,836. Full suite 9,701 passed / 5 skipped, no failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two enums disagreed with the retail client, and both disagreements were the
quiet kind - nothing read the wrong members, so nothing was visibly broken. They
were traps armed for the first person to write a comparison against them.
DamageType had its four drain/restore bits rotated. acdream assigned
Nether/Mana/Health/Stamina to 0x80/0x100/0x200/0x400; retail's DAMAGE_TYPE
(acclient.h:3788) assigns Health/Stamina/Mana/Nether. The ACE weenie corpus
attests retail's order independently - 0x100 Stamina, 0x200 Mana, 0x400 Nether -
and so does the vendored client-side enum catalog. Tellingly, both of acdream's
live damage-type name tables, CombatChatTranslator.FormatDamageType (ported from
holtburger) and ItemAppraisalTextFormatter.TryDamageTypeName, already used
retail's order reading the raw wire uint directly. The enum was the only thing in
the tree that was wrong. Retail's BASE_DAMAGE_TYPE (0x10000000) was also missing;
CombatChatTranslator already knew about it.
ItemType had two separate problems. The craft ladder was shifted one bit:
CraftAlchemyIntermediate sat on 0x02000000, which retail leaves unused, and an
invented CraftCookingIntermediate occupied 0x04000000, which is retail's real
alchemy-intermediate bit. The weenie corpus attests 0x04000000 as
Craft_Alchemy_Intermediate 235 times and contains no cooking-intermediate at all -
there is no such item type. Separately, the composite masks were recomputed
locally from the bits above them instead of transcribed, which is exactly how the
ladder drifted in the first place. That made Weapon (retail 0x101, melee|missile)
an exact alias of WeaponOrCaster (0x8101), and left Item at 0x830F where retail's
TYPE_ITEM is 0x2DFBEF - a mask two orders of magnitude broader. The composites are
now transcribed as literals with retail's value, not derived, and the five
retail-only masks acdream never had (portal/lockable magic targets, the
enchantable and redirectable targets, and the two vendor masks) come along.
Note for the reader wondering why the campaign trusted retail over the catalog
here: on CraftFletchingBase the catalog is the one that is wrong (it says
0x02000000; retail and acdream both say 0x01000000). No single oracle was assumed
correct - retail's header decided, with the weenie corpus as the tiebreak.
Behavior: no production code reads any changed member. The only reference in the
tree is a test that wants a nonzero HookItemTypes and does not care which. So no
branch changes and no wire behavior moves - but the values did change, which is
why this is a fix commit and not a data commit. No divergence-register row: these
were unintentional errors, now retired, not deviations we chose.
RetailEnumConformanceTests pins both enums to the acclient.h tables, asserts
acdream declares nothing retail does not, and calls out the two specific traps -
that 0x02000000 stays unclaimed, and that Weapon and WeaponOrCaster are no longer
the same value.
Core tests 3,726 -> 3,785.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
acdream has carried property IDs as bare uints since the beginning. The wire
parsers read `u32 property` and hand it to a `Dictionary<uint, int>`, and every
call site that cared re-derived the meaning from a comment - `EncumbranceVal`
was spelled `private const uint EncumbranceValProperty = 5u` in two different
files, `UiEffects` lived as "ACE enum value 18" in a doc comment, and
`AetheriaBitfield` as "322 / 0x142". That is 864 pieces of vocabulary the
codebase was expected to remember in prose.
This adds the seven enums - PropertyInt, PropertyInt64, PropertyBool,
PropertyFloat, PropertyString, PropertyDataId, PropertyInstanceId - under
AcDream.Core.Properties.
Every member is transcribed from an oracle; none is invented. Two independent
sources were extracted and diffed against each other: the vendored client-side
enum catalog at references/acclientlib/UtilityBelt.Common/Enums/Enums.cs (which
names these tables IntId/BoolId/FloatId/...), and the 38,985-file ACE weenie
export corpus at references/weenies/, whose every stat entry carries the numeric
key beside the enum member name in its `_comment`. The corpus attests 408 of the
864 members directly. Across all seven tables the two oracles produced zero
value conflicts, and the corpus contained no key the catalog was missing - the
catalog is a strict superset of everything 38,985 weenies actually set.
Three members disagree on spelling, never on value: the catalog says
ObjectType/HookObjectType/MerchandiseObjectTypes where ACE says
ItemType/HookItemType/MerchandiseItemTypes. acdream takes ACE's spelling, which
is what the weenie corpus emits (37,329 attestations for ItemType alone) and
what acdream's own ItemType enum already calls it. The catalog's alias is
recorded on each member.
This commit is vocabulary only - no parser reads these enums yet, so no branch
changes and no wire behavior moves. The bundles stay `Dictionary<uint, ...>`
precisely because an unknown key must still round-trip untouched; the enums
describe the keys we know, they do not constrain the ones we receive.
PropertyEnumConformanceTests pins the result: the full name/value table per
family, the uint underlying type, no two members sharing a value, and a separate
408-case theory asserting each weenie-attested pairing individually. A hand edit
to any enum now fails loudly instead of quietly mis-reading the wire.
Core tests 3,297 -> 3,726.
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>
The ImGui developer-tools stack (AcDream.UI.ImGui), UI Studio
(src/AcDream.App/Studio), and the DevToolsFramePresenter/
SettingsDevToolsCompositionPhase ImGui composition machinery are removed.
Vulkan never composed a DevTools frontend (DevToolsEnabled already forced
false whenever the backend was Vulkan); this commit makes that permanent by
deleting the only implementation rather than leaving a dead branch behind.
What moved: Studio/SampleData.cs is a live production dependency
(InteractionRetainedUiComposition's character-sheet fallback, plus three
UI.Layout test files) - git mv'd to src/AcDream.App/UI/Layout/SampleData.cs,
namespace AcDream.App.UI.Layout, and trimmed to the SampleCharacter API that
is actually still called (BuildObjectTable/AddItem/AddEquipped/the item-guid
and icon constants had zero callers left once the Studio fixture provider
that used them was deleted).
What survives as backend-neutral seams, per the tests that still exercise
them: IDevToolsFrameLifecycle (moved into RenderFramePreparationController.cs,
now always bound to null), IFramebufferDevToolsTarget/FramebufferDevToolsBinding
in FramebufferResizeController.cs (its concrete DevToolsFramebufferTarget
adapter is deleted), and IDevToolsGameplayCommands in
GameplayInputCommandController.cs (DevToolsGameplayCommands becomes a
documented no-op instead of forwarding to the deleted presenter). A follow-up
re-homes Settings/Debug onto the retained UI through IPanelRenderer; until
then keybind remapping falls back to editing keybinds.json.
DevToolsEnabled is now `private const bool DevToolsEnabled = false`.
RuntimeOptions.DevTools is unchanged and still reaches VulkanGraphicsContext
for the optional debug-utils extensions; Program.cs now logs one line when
ACDREAM_DEVTOOLS=1 explaining that the ImGui UI is gone and the flag is
Vulkan-only now.
Removed: AcDream.UI.ImGui (project + ImGui.NET/Silk.NET.OpenGL.Extensions.ImGui
package refs), src/AcDream.App/Studio (minus SampleData.cs),
DevToolsFramePresenter.cs and everything only it constructed
(ISettingsDevToolsCompositionFactory, RetailSettingsDevToolsCompositionFactory,
DevToolsCompositionOwner, IGameWindowSettingsDevToolsPublication,
SettingsDevToolsOptionalDependencies, the "developer tools" shutdown-ledger
stage and its DevTools-typed fields on IngressShutdownRoots/
RenderShutdownRoots), the ui-studio Program.cs verb, and the cimgui native
manifest entries in GraphicalHostPlatformServices. GameWindow.cs's DevTools
composition branch, its _vitalsVm/_debugVm/_devToolsComposition/
_devToolsFramePresenter/_devToolsCommandBus fields, and every settingsDevTools
.DevTools?.* access across FrameRootComposition.cs/SessionPlayerComposition.cs
are gone with it.
Build green; complete Release solution suite 8,830 / 5 skips (App Tests
4,097/3 skips run standalone - one #250-family zero-allocation test flakes
under the full parallel `dotnet test AcDream.slnx` run, a pre-existing,
documented class unrelated to this change).
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>