Files
MobileGL/docs/Disaggregated/notes/handoff/review-m2-r2.md
T

3.3 KiB

Review: M2 round 2 (pipe 4d207468 959ebc9e 6e18acaa 5ba34fcf + integrator f62703e9 1ab62ece) — integrator (Fable 5.1, main session)

Verdict: land (no change required before push; one cost recorded in CONTRACT-P7 §12 as M3's denominator).

The must-fix from round 1 (memo handle-reuse ABA) is closed the way the review asked: VkBufferManager::m_wireStoreDestroyEpoch is bumped on every path that destroys a wire store's VkBuffer and never reset; UniformManager folds it into the descriptor-reuse signature (mix64(epoch)) and records/compares it in FastRebindMemo, all under MOBILEGL_BUILD_DISAGGREGATED.

Verified on the pipe source, not the note:

  1. Every destroy site is covered. DeferWireRelease's immediate destroy (buffer.Destroy(); ++epoch), SweepDeferredWireReleases (returns at dead == 0 BEFORE the bump, so a sweep that freed nothing does not defeat the memo on the next draw), DestroyAllDeferredWireReleases (bump iff non-empty), Shutdown's m_wireBuffers.clear() (bump iff non-empty). DestroyWireBuffer moves the store into DeferWireRelease and only then erases the record, so the erase destroys nothing. The seven remaining buffer.Destroy() calls in the file are the non-wire records, which still die at the frame boundary that clears the memos.
  2. The epoch is the destroy count's parity, not the slice epoch: m_sliceEpochCounter moves on every WriteWireBuffer and would miss on every glBufferSubData; the destroy epoch moves ~26k times over bsl's 1.3M calls. Cost = one extra descriptor-set acquire+write on the first draw after each destroy event, which also feeds §6.1's in-frame descriptor-set growth (M3).
  3. FastRebindMemo aggregate initialiser order matches the struct (epoch last, guarded on both sides); the wire arm never takes the fast rebind (SetupWireDraw passes no sampler hint), so the signature half is the one the scenario reds through; the memo half is defensive and costs nothing.
  4. The red-once is deterministic by construction (no sleep, no fence-poll race): orphan → flush-without-wait → a staged copy keeps the recording pending so the blocking wait cannot take the memo-clearing drain → the next park's sweep destroys S1 → a 64 KiB mint through the freed handle cannot alias the 256-byte hole a 1 MiB pin bounds → the draw reads the dead store's colour without the epoch. Both arms red at base, green with the guards; the note's static-link finding (the Split arm runs the backend compiled into the test binary) is worth keeping for every future red-once.
  5. Integrator additions: RespecifyWireBuffer clears B3's lastUseSubmitIndex beside lastUseSerial (pipe-only field; the create path already zeroes it at :303, so 0 = never submitted); the parity exception list on pipe is keyed by exact tail (two merges ago) so the fourth Reclaim case is named there (the package branch excepts by prefix). ServerSpawn keeps SPIN_US / SERVER_AFFINITY / AUDIT for the child (tuning / audit values, no endpoint, role, path or size; ServerMain's catch (b) still verifies its four).

Gate on f62703e9: unit 2436, split 304, spawn 220, tcp 223, magma 101 / 80 / 71, full-split 539, G1 .text 0xa52203 nm +0/-0, census 79, ratchet 173, wire-declines 53/53/0 + 16/16, OpenRA DirectVulkan inproc + spawn ssim 1.000000 / 0 unsound lines, verify lanes 1150 / 1084; parity 0 errors after 1ab62ece.